From bcd2d061288004020cc437694dbf4218abc52c89 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 2 Nov 2026 21:39:54 +0100 Subject: refactor(app): remove the legacy PasswordStore list The Compose list replaces it. The autofill inline suggestion's attribution intent now opens LaunchActivity, behind the biometric gate. Co-Authored-By: Claude Opus 5.5 --- app/src/main/res/layout/activity_pwdstore.xml | 19 ---------- app/src/main/res/layout/folder_dialog_fragment.xml | 38 -------------------- app/src/main/res/layout/item_create_sheet.xml | 42 ---------------------- app/src/main/res/layout/password_recycler_view.xml | 3 +- 4 files changed, 1 insertion(+), 101 deletions(-) delete mode 100644 app/src/main/res/layout/activity_pwdstore.xml delete mode 100644 app/src/main/res/layout/folder_dialog_fragment.xml delete mode 100644 app/src/main/res/layout/item_create_sheet.xml (limited to 'app/src/main/res/layout') diff --git a/app/src/main/res/layout/activity_pwdstore.xml b/app/src/main/res/layout/activity_pwdstore.xml deleted file mode 100644 index 295efd9c..00000000 --- a/app/src/main/res/layout/activity_pwdstore.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/folder_dialog_fragment.xml b/app/src/main/res/layout/folder_dialog_fragment.xml deleted file mode 100644 index 7860c9b4..00000000 --- a/app/src/main/res/layout/folder_dialog_fragment.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_create_sheet.xml b/app/src/main/res/layout/item_create_sheet.xml deleted file mode 100644 index 10882a00..00000000 --- a/app/src/main/res/layout/item_create_sheet.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/layout/password_recycler_view.xml b/app/src/main/res/layout/password_recycler_view.xml index 49b7ff82..c2b5114f 100644 --- a/app/src/main/res/layout/password_recycler_view.xml +++ b/app/src/main/res/layout/password_recycler_view.xml @@ -9,8 +9,7 @@ android:layout_height="match_parent" android:clipChildren="false" android:clipToPadding="false" - android:orientation="vertical" - tools:context="app.passwordstore.ui.passwords.PasswordFragment"> + android:orientation="vertical"> Date: Mon, 2 Nov 2026 23:40:47 +0100 Subject: refactor(app): remove the legacy decrypt screen The Compose entry view replaces DecryptActivity, its layouts, menu and field adapter, and upstream's unused ViewPasswordScreen. BasePGPActivity keeps what the editor and autofill still need. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 5 +- app/lint-baseline.xml | 22 -- app/lint.xml | 3 - app/src/main/AndroidManifest.xml | 4 - .../app/passwordstore/data/password/FieldItem.kt | 51 ---- .../passwordstore/ui/adapters/FieldItemAdapter.kt | 101 ------- .../app/passwordstore/ui/crypto/BasePGPActivity.kt | 7 - .../app/passwordstore/ui/crypto/DecryptActivity.kt | 298 --------------------- .../passwordstore/ui/crypto/ViewPasswordScreen.kt | 132 --------- app/src/main/res/drawable/divider.xml | 10 - app/src/main/res/drawable/ic_content_copy.xml | 15 -- app/src/main/res/layout/decrypt_layout.xml | 65 ----- app/src/main/res/layout/item_field.xml | 30 --- app/src/main/res/menu/pgp_handler.xml | 28 -- .../specs/2026-11-02-compose-ui-overhaul-design.md | 3 + 15 files changed, 6 insertions(+), 768 deletions(-) delete mode 100644 app/src/main/java/app/passwordstore/data/password/FieldItem.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/adapters/FieldItemAdapter.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/crypto/ViewPasswordScreen.kt delete mode 100644 app/src/main/res/drawable/divider.xml delete mode 100644 app/src/main/res/drawable/ic_content_copy.xml delete mode 100644 app/src/main/res/layout/decrypt_layout.xml delete mode 100644 app/src/main/res/layout/item_field.xml delete mode 100644 app/src/main/res/menu/pgp_handler.xml (limited to 'app/src/main/res/layout') diff --git a/AGENTS.md b/AGENTS.md index 77f15fe1..91c0857b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,8 +119,9 @@ passed. (`:private-key:`). Never allowlist anything but generated test keys. - `app/lint-baseline.xml` entries for library jars are keyed on the jar's cache path, version and hash included. A dependency bump that moves a baselined jar needs the path updated. -- `LaunchActivity` is exported and forwards `FILE_PATH` to `DecryptActivity` (not exported) - only after `isFileInRepo` checks it canonically resolves inside the store. +- `LaunchActivity` is exported and forwards `FILE_PATH` to `MainActivity` (not exported) only + after `isFileInRepo` checks it canonically resolves inside the store; `MainActivity` opens it as + `EntryRoute`, whose destination checks it again before decrypting. - AGP 9 applies packaging excludes (`AndroidCommon`, e.g. `**/*.txt`) to unit-test java resources as well. A module whose tests load such a file must remove the exclude locally (see `passgen/diceware/build.gradle.kts`). diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index d9be2a91..20c4683f 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -62,28 +62,6 @@ column="17"/> - - - - - - - - - - - diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9bfb3b6a..d534d899 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,10 +44,6 @@ android:label="@string/activity_proxy_label" android:windowSoftInputMode="adjustResize" /> - - , - private val showPassword: Boolean, - private val copyTextToClipboard: (text: String?) -> Unit, -) : RecyclerView.Adapter() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldItemViewHolder { - val binding = ItemFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return FieldItemViewHolder(binding.root, binding) - } - - override fun onBindViewHolder(holder: FieldItemViewHolder, position: Int) { - holder.bind(fieldItemList[position], showPassword, copyTextToClipboard) - } - - override fun getItemCount(): Int { - return fieldItemList.size - } - - fun updateOTPCode(totp: Totp, labelFormat: String) { - var otpItemPosition = -1 - fieldItemList = - fieldItemList.mapIndexed { position, item -> - if (item.type == FieldItem.ItemType.OTP) { - otpItemPosition = position - return@mapIndexed FieldItem.createOtpField(labelFormat, totp) - } - - return@mapIndexed item - } - - notifyItemChanged(otpItemPosition) - } - - class FieldItemViewHolder(itemView: View, val binding: ItemFieldBinding) : - RecyclerView.ViewHolder(itemView) { - - fun bind(fieldItem: FieldItem, showPassword: Boolean, copyTextToClipboard: (String?) -> Unit) { - with(binding) { - itemText.hint = fieldItem.label - itemTextContainer.hint = fieldItem.label - itemText.setText(fieldItem.value) - - when (fieldItem.action) { - FieldItem.ActionType.COPY -> { - itemTextContainer.apply { - endIconDrawable = - ContextCompat.getDrawable(itemView.context, R.drawable.ic_content_copy) - endIconMode = TextInputLayout.END_ICON_CUSTOM - setEndIconOnClickListener { copyTextToClipboard(itemText.text.toString()) } - } - itemText.transformationMethod = null - } - FieldItem.ActionType.HIDE -> { - itemTextContainer.apply { - endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE - setOnClickListener { copyTextToClipboard(itemText.text.toString()) } - } - itemText.apply { - transformationMethod = - if (!showPassword) { - PasswordTransformationMethod.getInstance() - } else { - null - } - if (fieldItem.type == FieldItem.ItemType.PASSWORD) { - typeface = - ResourcesCompat.getFont( - binding.root.context, - composeR.font.jetbrainsmono_nl_regular, - ) - } - setOnClickListener { copyTextToClipboard(itemText.text.toString()) } - } - } - } - } - } - } -} 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 95dc67ad..ba49a62c 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt @@ -50,13 +50,6 @@ open class BasePGPActivity : AppCompatActivity() { /** Full path to the password file being worked on */ val fullPath by unsafeLazy { intent.getStringExtra("FILE_PATH")!! } - /** - * Name of the password file - * - * Converts personal/auth.foo.org/john_doe@example.org.gpg to john_doe.example.org - */ - val name: String by unsafeLazy { File(fullPath).nameWithoutExtension } - /** Action to invoke if [keyImportAction] succeeds. */ private var onKeyImport: (() -> Unit)? = null private val keyImportAction = diff --git a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt deleted file mode 100644 index 8b2109bf..00000000 --- a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt +++ /dev/null @@ -1,298 +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.Intent -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import androidx.core.content.edit -import androidx.fragment.app.setFragmentResultListener -import androidx.lifecycle.lifecycleScope -import app.passwordstore.Application.Companion.screenWasOff -import app.passwordstore.R -import app.passwordstore.crypto.PGPIdentifier -import app.passwordstore.data.crypto.PGPPassphraseCache -import app.passwordstore.data.passfile.PasswordEntry -import app.passwordstore.data.password.FieldItem -import app.passwordstore.databinding.DecryptLayoutBinding -import app.passwordstore.ui.adapters.FieldItemAdapter -import app.passwordstore.util.auth.BiometricAuthenticator -import app.passwordstore.util.auth.BiometricAuthenticator.Result as BiometricResult -import app.passwordstore.util.extensions.getString -import app.passwordstore.util.extensions.unsafeLazy -import app.passwordstore.util.extensions.viewBinding -import app.passwordstore.util.features.Feature.EnablePGPPassphraseCache -import app.passwordstore.util.features.Features -import app.passwordstore.util.settings.Constants -import app.passwordstore.util.settings.PreferenceKeys -import com.github.michaelbull.result.onFailure -import com.github.michaelbull.result.runCatching -import dagger.hilt.android.AndroidEntryPoint -import java.io.ByteArrayOutputStream -import java.io.File -import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import logcat.LogPriority.ERROR -import logcat.asLog -import logcat.logcat - -@AndroidEntryPoint -class DecryptActivity : BasePGPActivity() { - - @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory - @Inject lateinit var passphraseCache: PGPPassphraseCache - @Inject lateinit var features: Features - - private val binding by viewBinding(DecryptLayoutBinding::inflate) - private val relativeParentPath by unsafeLazy { getParentPath(fullPath, repoPath) } - private var passwordEntry: PasswordEntry? = null - private var retries = 0 - private var clearCache = true - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - title = name - with(binding) { - setContentView(root) - passwordCategory.text = relativeParentPath - passwordFile.text = name - passwordFile.setOnLongClickListener { - copyTextToClipboard(name) - true - } - } - if ( - features.isEnabled(EnablePGPPassphraseCache) && - BiometricAuthenticator.canAuthenticate(this@DecryptActivity) - ) { - BiometricAuthenticator.authenticate( - this@DecryptActivity, - R.string.biometric_prompt_title_gpg_passphrase_cache, - ) { authResult -> - requireKeysExist { decrypt(isError = false, authResult) } - } - } else { - requireKeysExist { decrypt(isError = false, BiometricResult.CanceledByUser) } - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.pgp_handler, menu) - passwordEntry?.let { entry -> - menu.findItem(R.id.edit_password).isVisible = true - if (!entry.password.isNullOrBlank()) { - menu.findItem(R.id.share_password_as_plaintext).isVisible = true - menu.findItem(R.id.copy_password).isVisible = true - } - } - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> onBackPressedDispatcher.onBackPressed() - R.id.edit_password -> editPassword() - R.id.share_password_as_plaintext -> shareAsPlaintext() - R.id.copy_password -> copyPasswordToClipboard(passwordEntry?.password) - else -> return super.onOptionsItemSelected(item) - } - return true - } - - /** - * Automatically finishes the activity after [PreferenceKeys.GENERAL_SHOW_TIME] seconds decryption - * succeeded to prevent information leaks from stale activities. - */ - private fun startAutoDismissTimer() { - lifecycleScope.launch { - val timeout = - settings.getString(PreferenceKeys.GENERAL_SHOW_TIME)?.toIntOrNull() - ?: Constants.DEFAULT_DECRYPTION_TIMEOUT - if (timeout != 0) { - delay(timeout.seconds) - finish() - } - } - } - - /** - * Edit the current password and hide all the fields populated by encrypted data so that when the - * result triggers they can be repopulated with new data. - */ - private fun editPassword() { - val intent = Intent(this, PasswordCreationActivity::class.java) - intent.action = Intent.ACTION_VIEW - intent.putExtra("FILE_PATH", relativeParentPath) - intent.putExtra("REPO_PATH", repoPath) - intent.putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, name) - intent.putExtra(PasswordCreationActivity.EXTRA_USERNAME, passwordEntry?.username) - intent.putExtra(PasswordCreationActivity.EXTRA_PASSWORD, passwordEntry?.password) - intent.putExtra( - PasswordCreationActivity.EXTRA_EXTRA_CONTENT, - passwordEntry?.extraContentWithoutUsername, - ) - intent.putExtra(PasswordCreationActivity.EXTRA_EDITING, true) - startActivity(intent) - finish() - } - - private fun shareAsPlaintext() { - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, passwordEntry?.password) - type = "text/plain" - } - // Always show a picker to give the user a chance to cancel - startActivity( - Intent.createChooser(sendIntent, resources.getText(R.string.send_plaintext_password_to)) - ) - } - - private fun decrypt(isError: Boolean, authResult: BiometricResult) { - val gpgIdentifiers = getPGPIdentifiers(relativeParentPath) ?: return - lifecycleScope.launch(dispatcherProvider.main()) { - when (authResult) { - // Internally handled by the prompt dialog - is BiometricResult.Retry -> {} - // If the dialog is dismissed for any reason, prompt for passphrase - is BiometricResult.CanceledByUser, - is BiometricResult.CanceledBySystem, - is BiometricResult.Failure, - is BiometricResult.HardwareUnavailableOrDisabled -> - askPassphrase(isError, gpgIdentifiers, authResult) - is BiometricResult.Success -> { - /* clear passphrase cache on first use after application startup or if screen was off; - also make sure to purge a stale cache after caching has been disabled via PGP settings */ - clearCache = settings.getBoolean(PreferenceKeys.CLEAR_PASSPHRASE_CACHE, true) - if (screenWasOff && clearCache) { - passphraseCache.clearAllCachedPassphrases(this@DecryptActivity) - screenWasOff = false - } - val cachedPassphrase = - passphraseCache.retrieveCachedPassphrase(this@DecryptActivity, gpgIdentifiers.first()) - if (cachedPassphrase != null) { - decryptWithPassphrase(cachedPassphrase, gpgIdentifiers, authResult) - } else { - askPassphrase(isError, gpgIdentifiers, authResult) - } - } - } - } - } - - private suspend fun askPassphrase( - isError: Boolean, - gpgIdentifiers: List, - authResult: BiometricResult, - ) { - if (retries < MAX_RETRIES) { - retries += 1 - } else { - finish() - } - if (!repository.isPasswordProtected(gpgIdentifiers)) { - decryptWithPassphrase(passphrase = "", gpgIdentifiers, authResult) - return - } - val dialog = - PasswordDialog.newInstance( - cacheEnabled = features.isEnabled(EnablePGPPassphraseCache), - clearCache = clearCache, - ) - if (isError) { - dialog.setError() - } - dialog.show(supportFragmentManager, "PASSWORD_DIALOG") - dialog.setFragmentResultListener(PasswordDialog.PASSWORD_RESULT_KEY) { key, bundle -> - if (key == PasswordDialog.PASSWORD_RESULT_KEY) { - val passphrase = bundle.getString(PasswordDialog.PASSWORD_PHRASE_KEY)!! - clearCache = bundle.getBoolean(PasswordDialog.PASSWORD_CLEAR_KEY) - lifecycleScope.launch(dispatcherProvider.main()) { - decryptWithPassphrase(passphrase, gpgIdentifiers, authResult) { - runCatching { - if (authResult is BiometricResult.Success) { - passphraseCache.cachePassphrase( - this@DecryptActivity, - gpgIdentifiers.first(), - passphrase, - ) - settings.edit { putBoolean(PreferenceKeys.CLEAR_PASSPHRASE_CACHE, clearCache) } - } - } - .onFailure { e -> logcat { e.asLog() } } - } - } - } - } - } - - private suspend fun decryptWithPassphrase( - passphrase: String, - identifiers: List, - authResult: BiometricResult, - onSuccess: suspend () -> Unit = {}, - ) { - val message = withContext(dispatcherProvider.io()) { File(fullPath).readBytes().inputStream() } - val outputStream = ByteArrayOutputStream() - val result = repository.decrypt(passphrase, identifiers, message, outputStream) - if (result.isOk) { - val entry = passwordEntryFactory.create(result.value.toByteArray()) - passwordEntry = entry - createPasswordUI(entry) - startAutoDismissTimer() - onSuccess() - } else { - logcat(ERROR) { result.error.stackTraceToString() } - decrypt(isError = true, authResult = authResult) - } - } - - private suspend fun createPasswordUI(entry: PasswordEntry) = - withContext(dispatcherProvider.main()) { - val labelFormat = resources.getString(R.string.otp_label_format) - val showPassword = settings.getBoolean(PreferenceKeys.SHOW_PASSWORD, true) - invalidateOptionsMenu() - - val items = arrayListOf() - if (!entry.password.isNullOrBlank()) { - items.add(FieldItem.createPasswordField(getString(R.string.password), entry.password!!)) - if (settings.getBoolean(PreferenceKeys.COPY_ON_DECRYPT, false)) { - copyPasswordToClipboard(entry.password) - } - } - - if (entry.hasTotp()) { - items.add(FieldItem.createOtpField(labelFormat, entry.totp.first())) - } - - if (!entry.username.isNullOrBlank()) { - items.add(FieldItem.createUsernameField(getString(R.string.username), entry.username!!)) - } - - entry.extraContent.forEach { (key, value) -> - items.add(FieldItem.createFreeformField(key, value)) - } - - val adapter = FieldItemAdapter(items, showPassword) { text -> copyTextToClipboard(text) } - binding.recyclerView.adapter = adapter - binding.recyclerView.itemAnimator = null - - if (entry.hasTotp()) { - lifecycleScope.launch { entry.totp.collect { adapter.updateOTPCode(it, labelFormat) } } - } - } - - private companion object { - private const val MAX_RETRIES = 3 - } -} diff --git a/app/src/main/java/app/passwordstore/ui/crypto/ViewPasswordScreen.kt b/app/src/main/java/app/passwordstore/ui/crypto/ViewPasswordScreen.kt deleted file mode 100644 index f2a67706..00000000 --- a/app/src/main/java/app/passwordstore/ui/crypto/ViewPasswordScreen.kt +++ /dev/null @@ -1,132 +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.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.capitalize -import androidx.compose.ui.text.intl.Locale -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.CopyButton -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 -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking - -/** Composable to show a decrypted [PasswordEntry]. */ -@Composable -fun ViewPasswordScreen( - entryName: String, - entry: PasswordEntry, - onNavigateUp: () -> 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, - readOnly = true, - modifier = Modifier.padding(bottom = SpacingMedium).fillMaxWidth(), - ) - } - if (entry.hasTotp()) { - val totp by entry.totp.collectAsState(runBlocking { entry.totp.first() }) - TextField( - value = totp.value, - onValueChange = {}, - readOnly = true, - label = { Text("OTP (expires in ${totp.remainingTime.inWholeSeconds}s)") }, - trailingIcon = { CopyButton(totp.value, R.string.copy_label) }, - modifier = Modifier.padding(bottom = SpacingMedium).fillMaxWidth(), - ) - } - if (entry.username != null) { - TextField( - value = entry.username!!, - onValueChange = {}, - readOnly = true, - label = { Text(stringResource(R.string.username)) }, - trailingIcon = { CopyButton(entry.username!!, R.string.copy_label) }, - modifier = Modifier.padding(bottom = SpacingMedium).fillMaxWidth(), - ) - } - ExtraContent(entry = entry) - } - } - } -} - -@Composable -private fun ExtraContent(entry: PasswordEntry, modifier: Modifier = Modifier) { - entry.extraContent.forEach { (label, value) -> - TextField( - value = value, - onValueChange = {}, - readOnly = true, - label = { Text(label.capitalize(Locale.current)) }, - trailingIcon = { CopyButton(value, R.string.copy_label) }, - modifier = modifier.padding(bottom = SpacingMedium).fillMaxWidth(), - ) - } -} - -@Preview -@Composable -private fun ViewPasswordScreenPreview() { - APSTheme { - ViewPasswordScreen(entryName = "Test Entry", entry = createTestEntry(), onNavigateUp = {}) - } -} - -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/res/drawable/divider.xml b/app/src/main/res/drawable/divider.xml deleted file mode 100644 index 39ec0614..00000000 --- a/app/src/main/res/drawable/divider.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_content_copy.xml b/app/src/main/res/drawable/ic_content_copy.xml deleted file mode 100644 index cdee1758..00000000 --- a/app/src/main/res/drawable/ic_content_copy.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/layout/decrypt_layout.xml b/app/src/main/res/layout/decrypt_layout.xml deleted file mode 100644 index 88c0fb1a..00000000 --- a/app/src/main/res/layout/decrypt_layout.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_field.xml b/app/src/main/res/layout/item_field.xml deleted file mode 100644 index 79697a56..00000000 --- a/app/src/main/res/layout/item_field.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/menu/pgp_handler.xml b/app/src/main/res/menu/pgp_handler.xml deleted file mode 100644 index a8b6a524..00000000 --- a/app/src/main/res/menu/pgp_handler.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - 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 654bbf60..75a599fa 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 @@ -35,6 +35,9 @@ the maintainer's phone stays on 0.1.4 until then. - First group: password (masked, reveal toggle, copy), username (copy), one-time code with a countdown bar (copy). - Second group: remaining extra fields, copy where it makes sense. +- Overflow: copy name, share as plaintext. The password starts masked or shown per the existing + "Show the password" setting. Saving in the editor returns to the list. Password and one-time + code copies are cleared from the clipboard after the timeout. ### Editor -- cgit v1.2.3 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 (limited to 'app/src/main/res/layout') 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 From 2911783653f699e41a07bb9536460d9d0a1c3b70 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 3 Nov 2026 14:18:42 +0100 Subject: refactor(app): remove the legacy setup screens The Compose welcome, git server, SSH key, key generation and PGP keys screens replace OnboardingActivity with its fragments, GitServerConfigActivity with BasicBottomSheet, SshKeyImportActivity, SshKeyGenActivity, ShowSshKeyFragment, PGPKeyImportActivity and PGPKeyListActivity with KeyList and PGPKeyListViewModel, their layouts and manifest entries, and ui:compose's APSAppBar. Resources only they used are removed too. Co-Authored-By: Claude Opus 5.5 --- app/lint.xml | 6 - app/src/main/AndroidManifest.xml | 29 -- .../passwordstore/ui/dialogs/BasicBottomSheet.kt | 178 ------------ .../ui/git/config/GitServerConfigActivity.kt | 304 --------------------- .../ui/onboarding/activity/OnboardingActivity.kt | 27 -- .../ui/onboarding/fragments/CloneFragment.kt | 78 ------ .../onboarding/fragments/KeySelectionFragment.kt | 77 ------ .../ui/onboarding/fragments/WelcomeFragment.kt | 33 --- .../passwordstore/ui/pgp/PGPKeyImportActivity.kt | 109 -------- .../java/app/passwordstore/ui/pgp/PGPKeyList.kt | 169 ------------ .../app/passwordstore/ui/pgp/PGPKeyListActivity.kt | 94 ------- .../ui/sshkeygen/ShowSshKeyFragment.kt | 39 --- .../ui/sshkeygen/SshKeyGenActivity.kt | 173 ------------ .../ui/sshkeygen/SshKeyImportActivity.kt | 66 ----- .../util/viewmodel/PGPKeyListViewModel.kt | 48 ---- app/src/main/res/drawable/ic_add_48dp.xml | 15 - .../main/res/drawable/ic_arrow_back_black_24dp.xml | 15 - app/src/main/res/layout/activity_git_clone.xml | 94 ------- app/src/main/res/layout/activity_onboarding.xml | 11 - app/src/main/res/layout/activity_ssh_keygen.xml | 69 ----- app/src/main/res/layout/basic_bottom_sheet.xml | 62 ----- app/src/main/res/layout/fragment_clone.xml | 96 ------- app/src/main/res/layout/fragment_key_selection.xml | 80 ------ app/src/main/res/layout/fragment_welcome.xml | 67 ----- app/src/main/res/values-de/strings.xml | 5 - app/src/main/res/values-fr/strings.xml | 4 - app/src/main/res/values-gl/strings.xml | 5 - app/src/main/res/values-hdpi/dimens.xml | 10 - app/src/main/res/values-it/strings.xml | 5 - app/src/main/res/values-ko-rKR/strings.xml | 5 - app/src/main/res/values-pl-rPL/strings.xml | 4 - app/src/main/res/values-pt-rBR/strings.xml | 5 - app/src/main/res/values-ru/strings.xml | 5 - app/src/main/res/values-zh-rCN/strings.xml | 7 - app/src/main/res/values/dimens.xml | 4 - app/src/main/res/values/strings.xml | 5 - .../specs/2026-11-02-compose-ui-overhaul-design.md | 17 ++ .../main/kotlin/app/passwordstore/ui/APSAppBar.kt | 76 ------ 38 files changed, 17 insertions(+), 2079 deletions(-) delete mode 100644 app/src/main/java/app/passwordstore/ui/dialogs/BasicBottomSheet.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/onboarding/activity/OnboardingActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/onboarding/fragments/WelcomeFragment.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/pgp/PGPKeyList.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/pgp/PGPKeyListActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/sshkeygen/ShowSshKeyFragment.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyGenActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyImportActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/util/viewmodel/PGPKeyListViewModel.kt delete mode 100644 app/src/main/res/drawable/ic_add_48dp.xml delete mode 100644 app/src/main/res/drawable/ic_arrow_back_black_24dp.xml delete mode 100644 app/src/main/res/layout/activity_git_clone.xml delete mode 100644 app/src/main/res/layout/activity_onboarding.xml delete mode 100644 app/src/main/res/layout/activity_ssh_keygen.xml delete mode 100644 app/src/main/res/layout/basic_bottom_sheet.xml delete mode 100644 app/src/main/res/layout/fragment_clone.xml delete mode 100644 app/src/main/res/layout/fragment_key_selection.xml delete mode 100644 app/src/main/res/layout/fragment_welcome.xml delete mode 100644 app/src/main/res/values-hdpi/dimens.xml delete mode 100644 ui/compose/src/main/kotlin/app/passwordstore/ui/APSAppBar.kt (limited to 'app/src/main/res/layout') diff --git a/app/lint.xml b/app/lint.xml index c0d501e0..8834cbd7 100644 --- a/app/lint.xml +++ b/app/lint.xml @@ -13,12 +13,6 @@ - - - - - - diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0429b2ac..546bdde4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,11 +33,6 @@ tools:ignore="GoogleAppIndexingWarning" tools:targetApi="s"> - - - - - - - - diff --git a/app/src/main/java/app/passwordstore/ui/dialogs/BasicBottomSheet.kt b/app/src/main/java/app/passwordstore/ui/dialogs/BasicBottomSheet.kt deleted file mode 100644 index e689f3bd..00000000 --- a/app/src/main/java/app/passwordstore/ui/dialogs/BasicBottomSheet.kt +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.dialogs - -import android.content.Context -import android.content.DialogInterface.OnDismissListener -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.ViewTreeObserver -import android.widget.FrameLayout -import androidx.annotation.StringRes -import androidx.core.view.isVisible -import app.passwordstore.R -import app.passwordstore.databinding.BasicBottomSheetBinding -import app.passwordstore.util.extensions.viewBinding -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.bottomsheet.BottomSheetDialogFragment - -/** - * [BottomSheetDialogFragment] that exposes a simple [androidx.appcompat.app.AlertDialog] like API - * through [BasicBottomSheet.Builder] to create a similar UI, just at the bottom of the screen. - */ -class BasicBottomSheet -private constructor( - val title: String?, - val message: String, - val positiveButtonLabel: String?, - val negativeButtonLabel: String?, - val positiveButtonClickListener: View.OnClickListener?, - val negativeButtonClickListener: View.OnClickListener?, - val onDismissListener: OnDismissListener?, -) : BottomSheetDialogFragment() { - - private val binding by viewBinding(BasicBottomSheetBinding::bind) - - private var behavior: BottomSheetBehavior? = null - private val bottomSheetCallback = - object : BottomSheetBehavior.BottomSheetCallback() { - override fun onSlide(bottomSheet: View, slideOffset: Float) {} - - override fun onStateChanged(bottomSheet: View, newState: Int) { - if (newState == BottomSheetBehavior.STATE_COLLAPSED) { - dismiss() - } - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View? { - if (savedInstanceState != null) dismiss() - return layoutInflater.inflate(R.layout.basic_bottom_sheet, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - view.viewTreeObserver.addOnGlobalLayoutListener( - object : ViewTreeObserver.OnGlobalLayoutListener { - override fun onGlobalLayout() { - view.viewTreeObserver.removeOnGlobalLayoutListener(this) - val dialog = dialog as BottomSheetDialog? ?: return - behavior = dialog.behavior - behavior?.apply { - state = BottomSheetBehavior.STATE_EXPANDED - peekHeight = 0 - addBottomSheetCallback(bottomSheetCallback) - } - if (!title.isNullOrEmpty()) { - binding.bottomSheetTitle.isVisible = true - binding.bottomSheetTitle.text = title - } - binding.bottomSheetMessage.text = message - if (positiveButtonClickListener != null) { - positiveButtonLabel?.let { buttonLbl -> binding.bottomSheetOkButton.text = buttonLbl } - binding.bottomSheetOkButton.isVisible = true - binding.bottomSheetOkButton.setOnClickListener { - positiveButtonClickListener.onClick(it) - dismiss() - } - } - if (negativeButtonClickListener != null) { - binding.bottomSheetCancelButton.isVisible = true - negativeButtonLabel?.let { buttonLbl -> - binding.bottomSheetCancelButton.text = buttonLbl - } - binding.bottomSheetCancelButton.setOnClickListener { - negativeButtonClickListener.onClick(it) - dismiss() - } - } - if (onDismissListener != null) { - dialog.setOnDismissListener(onDismissListener) - } - } - } - ) - } - - override fun dismiss() { - super.dismiss() - behavior?.removeBottomSheetCallback(bottomSheetCallback) - } - - class Builder(val context: Context) { - - private var title: String? = null - private var message: String? = null - private var positiveButtonLabel: String? = null - private var negativeButtonLabel: String? = null - private var positiveButtonClickListener: View.OnClickListener? = null - private var negativeButtonClickListener: View.OnClickListener? = null - private var onDismissListener: OnDismissListener? = null - - fun setTitleRes(@StringRes titleRes: Int): Builder { - this.title = context.resources.getString(titleRes) - return this - } - - fun setTitle(title: String): Builder { - this.title = title - return this - } - - fun setMessageRes(@StringRes messageRes: Int): Builder { - this.message = context.resources.getString(messageRes) - return this - } - - fun setMessage(message: String): Builder { - this.message = message - return this - } - - fun setPositiveButtonClickListener( - buttonLabel: String? = null, - listener: View.OnClickListener, - ): Builder { - this.positiveButtonClickListener = listener - this.positiveButtonLabel = buttonLabel - return this - } - - fun setNegativeButtonClickListener( - buttonLabel: String? = null, - listener: View.OnClickListener, - ): Builder { - this.negativeButtonClickListener = listener - this.negativeButtonLabel = buttonLabel - return this - } - - fun setOnDismissListener(onDismissListener: OnDismissListener): Builder { - this.onDismissListener = onDismissListener - return this - } - - fun build(): BasicBottomSheet { - require(message != null) { "Message needs to be set" } - return BasicBottomSheet( - title, - message!!, - positiveButtonLabel, - negativeButtonLabel, - positiveButtonClickListener, - negativeButtonClickListener, - onDismissListener, - ) - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt b/app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt deleted file mode 100644 index 2a1d3ebd..00000000 --- a/app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package app.passwordstore.ui.git.config - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.view.MenuItem -import android.view.View -import androidx.core.os.postDelayed -import androidx.core.view.isVisible -import androidx.core.widget.doOnTextChanged -import androidx.lifecycle.lifecycleScope -import app.passwordstore.R -import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.databinding.ActivityGitCloneBinding -import app.passwordstore.ui.dialogs.BasicBottomSheet -import app.passwordstore.ui.git.base.BaseGitActivity -import app.passwordstore.util.extensions.snackbar -import app.passwordstore.util.extensions.viewBinding -import app.passwordstore.util.settings.AuthMode -import app.passwordstore.util.settings.GitSettings -import app.passwordstore.util.settings.Protocol -import com.github.michaelbull.result.fold -import com.github.michaelbull.result.onFailure -import com.github.michaelbull.result.runCatching -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import logcat.LogPriority.ERROR -import logcat.asLog -import logcat.logcat - -/** - * Activity that encompasses both the initial clone as well as editing the server config for future - * changes. - */ -class GitServerConfigActivity : BaseGitActivity() { - - private val binding by viewBinding(ActivityGitCloneBinding::inflate) - - private lateinit var newAuthMode: AuthMode - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val isClone = intent?.extras?.getBoolean("cloning") ?: false - if (isClone) { - binding.saveButton.text = getString(R.string.clone_button) - } - setContentView(binding.root) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - newAuthMode = gitSettings.authMode - - binding.authModeGroup.apply { - when (newAuthMode) { - AuthMode.SshKey -> check(binding.authModeSshKey.id) - AuthMode.Password -> check(binding.authModePassword.id) - AuthMode.None -> clearChecked() - } - addOnButtonCheckedListener { _, _, _ -> - if (checkedButtonIds.isEmpty()) { - newAuthMode = AuthMode.None - } else { - when (checkedButtonId) { - binding.authModeSshKey.id -> newAuthMode = AuthMode.SshKey - binding.authModePassword.id -> newAuthMode = AuthMode.Password - View.NO_ID -> newAuthMode = AuthMode.None - } - } - } - } - - binding.serverUrl.setText( - gitSettings.url.also { - if (it.isNullOrEmpty()) return@also - setAuthModes(it.startsWith("http://") || it.startsWith("https://")) - } - ) - - binding.serverUrl.doOnTextChanged { text, _, _, _ -> - if (text.isNullOrEmpty()) return@doOnTextChanged - setAuthModes(text.startsWith("http://") || text.startsWith("https://")) - } - - binding.clearHostKeyButton.isVisible = gitSettings.hasSavedHostKey() - binding.clearHostKeyButton.setOnClickListener { - gitSettings.clearSavedHostKey() - Snackbar.make( - binding.root, - getString(R.string.clear_saved_host_key_success), - Snackbar.LENGTH_LONG, - ) - .show() - it.isVisible = false - } - binding.saveButton.setOnClickListener { - val newUrl = binding.serverUrl.text.toString().trim() - // If url is of type john_doe@example.org:12435/path/to/repo, then not adding `ssh://` - // in the beginning will cause the port to be seen as part of the path. Let users know - // about it and offer a quickfix. - if (newUrl.contains(PORT_REGEX)) { - if (newUrl.startsWith("https://")) { - BasicBottomSheet.Builder(this) - .setTitleRes(R.string.https_scheme_with_port_title) - .setMessageRes(R.string.https_scheme_with_port_message) - .setPositiveButtonClickListener { - binding.serverUrl.setText(newUrl.replace(PORT_REGEX, "/")) - } - .build() - .show(supportFragmentManager, "SSH_SCHEME_WARNING") - return@setOnClickListener - } else if (!newUrl.startsWith("ssh://")) { - BasicBottomSheet.Builder(this) - .setTitleRes(R.string.ssh_scheme_needed_title) - .setMessageRes(R.string.ssh_scheme_needed_message) - .setPositiveButtonClickListener { - @Suppress("SetTextI18n") binding.serverUrl.setText("ssh://$newUrl") - } - .build() - .show(supportFragmentManager, "SSH_SCHEME_WARNING") - return@setOnClickListener - } - } - if (newUrl.startsWith("git://")) { - BasicBottomSheet.Builder(this) - .setTitleRes(R.string.git_scheme_disallowed_title) - .setMessageRes(R.string.git_scheme_disallowed_message) - .setPositiveButtonClickListener {} - .build() - .show(supportFragmentManager, "SSH_SCHEME_WARNING") - return@setOnClickListener - } - when ( - val updateResult = - gitSettings.updateConnectionSettingsIfValid( - newAuthMode = newAuthMode, - newUrl = binding.serverUrl.text.toString().trim(), - ) - ) { - GitSettings.UpdateConnectionSettingsResult.FailedToParseUrl -> { - Snackbar.make( - binding.root, - getString(R.string.git_server_config_save_error), - Snackbar.LENGTH_LONG, - ) - .show() - } - is GitSettings.UpdateConnectionSettingsResult.MissingUsername -> { - when (updateResult.newProtocol) { - Protocol.Https -> - BasicBottomSheet.Builder(this) - .setTitleRes(R.string.ssh_scheme_needed_title) - .setMessageRes(R.string.git_server_config_save_missing_username_https) - .setPositiveButtonClickListener {} - .build() - .show(supportFragmentManager, "HTTPS_MISSING_USERNAME") - Protocol.Ssh -> - BasicBottomSheet.Builder(this) - .setTitleRes(R.string.ssh_scheme_needed_title) - .setMessageRes(R.string.git_server_config_save_missing_username_ssh) - .setPositiveButtonClickListener {} - .build() - .show(supportFragmentManager, "SSH_MISSING_USERNAME") - } - } - GitSettings.UpdateConnectionSettingsResult.Valid -> { - if (isClone && PasswordRepository.repository == null) PasswordRepository.initialize() - if (!isClone) { - Snackbar.make( - binding.root, - getString(R.string.git_server_config_save_success), - Snackbar.LENGTH_SHORT, - ) - .show() - Handler(Looper.getMainLooper()).postDelayed(500) { finish() } - } else { - cloneRepository() - } - } - is GitSettings.UpdateConnectionSettingsResult.AuthModeMismatch -> { - val message = - getString( - R.string.git_server_config_save_auth_mode_mismatch, - updateResult.newProtocol, - updateResult.validModes.joinToString(", "), - ) - Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show() - } - } - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> { - onBackPressedDispatcher.onBackPressed() - true - } - else -> super.onOptionsItemSelected(item) - } - } - - private fun setAuthModes(isHttps: Boolean) = - with(binding) { - if (isHttps) { - authModeSshKey.isVisible = false - authModePassword.isVisible = true - if (authModeGroup.checkedButtonId != authModePassword.id) authModeGroup.clearChecked() - } else { - authModeSshKey.isVisible = true - authModePassword.isVisible = true - if (authModeGroup.checkedButtonId == View.NO_ID) authModeGroup.check(authModeSshKey.id) - } - } - - /** Clones the repository, the directory exists, deletes it */ - private fun cloneRepository() { - val localDir = - requireNotNull(PasswordRepository.getRepositoryDirectory()) { - "Repository directory must be set before cloning" - } - val localDirFiles = localDir.listFiles() ?: emptyArray() - // Warn if non-empty folder unless it's a just-initialized store that has just a .git folder - if ( - localDir.exists() && - localDirFiles.isNotEmpty() && - !(localDirFiles.size == 1 && localDirFiles[0].name == ".git") - ) { - MaterialAlertDialogBuilder(this) - .setTitle(R.string.dialog_delete_title) - .setMessage(resources.getString(R.string.dialog_delete_msg, localDir.toString())) - .setCancelable(false) - .setPositiveButton(R.string.dialog_delete) { dialog, _ -> - runCatching { - lifecycleScope.launch { - val snackbar = - snackbar( - message = getString(R.string.delete_directory_progress_text), - length = Snackbar.LENGTH_INDEFINITE, - ) - withContext(dispatcherProvider.io()) { - localDir.deleteRecursively() - localDir.mkdirs() - } - snackbar.dismiss() - launchGitOperation(GitOp.CLONE) - .fold( - success = { - setResult(RESULT_OK) - finish() - }, - failure = { err -> promptOnErrorHandler(err) { finish() } }, - ) - } - } - .onFailure { e -> - e.printStackTrace() - MaterialAlertDialogBuilder(this).setMessage(e.message).show() - } - dialog.cancel() - } - .setNegativeButton(R.string.dialog_do_not_delete) { dialog, _ -> dialog.cancel() } - .show() - } else { - runCatching { - // Silently delete & replace the lone .git folder if it exists - if (localDir.exists() && localDirFiles.size == 1 && localDirFiles[0].name == ".git") { - localDir.deleteRecursively() - } - } - .onFailure { e -> - logcat(ERROR) { e.asLog() } - MaterialAlertDialogBuilder(this).setMessage(e.message).show() - } - lifecycleScope.launch { - launchGitOperation(GitOp.CLONE) - .fold( - success = { - setResult(RESULT_OK) - finish() - }, - failure = { promptOnErrorHandler(it) }, - ) - } - } - } - - companion object { - - private val PORT_REGEX = ":[0-9]{1,5}/".toRegex() - - fun createCloneIntent(context: Context): Intent { - return Intent(context, GitServerConfigActivity::class.java).apply { - putExtra("cloning", true) - } - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/onboarding/activity/OnboardingActivity.kt b/app/src/main/java/app/passwordstore/ui/onboarding/activity/OnboardingActivity.kt deleted file mode 100644 index 709c2302..00000000 --- a/app/src/main/java/app/passwordstore/ui/onboarding/activity/OnboardingActivity.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.onboarding.activity - -import android.os.Bundle -import androidx.activity.addCallback -import androidx.appcompat.app.AppCompatActivity -import app.passwordstore.R -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class OnboardingActivity : AppCompatActivity(R.layout.activity_onboarding) { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - supportActionBar?.hide() - val callback = onBackPressedDispatcher.addCallback(enabled = false) { finishAffinity() } - supportFragmentManager.addOnBackStackChangedListener { - if (supportFragmentManager.backStackEntryCount == 0) { - callback.isEnabled = true - } - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt b/app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt deleted file mode 100644 index a9d933b6..00000000 --- a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.onboarding.fragments - -import android.os.Bundle -import android.view.View -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.edit -import androidx.fragment.app.Fragment -import app.passwordstore.R -import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.databinding.FragmentCloneBinding -import app.passwordstore.ui.git.config.GitServerConfigActivity -import app.passwordstore.util.extensions.finish -import app.passwordstore.util.extensions.performTransactionWithBackStack -import app.passwordstore.util.extensions.sharedPrefs -import app.passwordstore.util.extensions.unsafeLazy -import app.passwordstore.util.extensions.viewBinding -import app.passwordstore.util.settings.PreferenceKeys -import com.github.michaelbull.result.onFailure -import com.github.michaelbull.result.runCatching -import logcat.LogPriority.ERROR -import logcat.asLog -import logcat.logcat - -class CloneFragment : Fragment(R.layout.fragment_clone) { - - private val binding by viewBinding(FragmentCloneBinding::bind) - - private val settings by unsafeLazy { requireActivity().applicationContext.sharedPrefs } - - private val cloneAction = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == AppCompatActivity.RESULT_OK) { - settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } - finish() - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.cloneRemote.setOnClickListener { cloneToHiddenDir() } - binding.createLocal.setOnClickListener { createRepository() } - } - - /** Clones a remote Git repository to the app's private directory */ - private fun cloneToHiddenDir() { - cloneAction.launch(GitServerConfigActivity.createCloneIntent(requireContext())) - } - - private fun createRepository() { - val localDir = PasswordRepository.getRepositoryDirectory() - runCatching { - check(localDir.exists() || localDir.mkdir()) { "Failed to create directory!" } - PasswordRepository.createRepository(localDir) - if (!PasswordRepository.isInitialized) { - PasswordRepository.initialize() - } - parentFragmentManager.performTransactionWithBackStack(KeySelectionFragment.newInstance()) - } - .onFailure { e -> - logcat(ERROR) { e.asLog() } - if (!localDir.delete()) { - logcat { "Failed to delete local repository: $localDir" } - } - finish() - } - } - - companion object { - - fun newInstance(): CloneFragment = CloneFragment() - } -} diff --git a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt b/app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt deleted file mode 100644 index 64c2e124..00000000 --- a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.onboarding.fragments - -import android.content.SharedPreferences -import android.os.Bundle -import android.view.View -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.edit -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import app.passwordstore.R -import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.databinding.FragmentKeySelectionBinding -import app.passwordstore.injection.prefs.SettingsPreferences -import app.passwordstore.ui.pgp.PGPKeyListActivity -import app.passwordstore.util.coroutines.DispatcherProvider -import app.passwordstore.util.extensions.commitChange -import app.passwordstore.util.extensions.finish -import app.passwordstore.util.extensions.snackbar -import app.passwordstore.util.extensions.viewBinding -import app.passwordstore.util.settings.PreferenceKeys -import com.google.android.material.snackbar.Snackbar -import dagger.hilt.android.AndroidEntryPoint -import java.io.File -import javax.inject.Inject -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -@AndroidEntryPoint -class KeySelectionFragment : Fragment(R.layout.fragment_key_selection) { - - @Inject @SettingsPreferences lateinit var settings: SharedPreferences - @Inject lateinit var dispatcherProvider: DispatcherProvider - private val binding by viewBinding(FragmentKeySelectionBinding::bind) - private val gpgKeySelectAction = - registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == AppCompatActivity.RESULT_OK) { - val data = result.data ?: return@registerForActivityResult - val selectedKey = - data.getStringExtra(PGPKeyListActivity.EXTRA_SELECTED_KEY) - ?: return@registerForActivityResult - lifecycleScope.launch { - withContext(dispatcherProvider.io()) { - val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id") - gpgIdentifierFile.writeText(selectedKey) - } - settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } - requireActivity() - .commitChange(getString(R.string.git_commit_gpg_id, getString(R.string.app_name))) - finish() - } - } else { - requireActivity() - .snackbar( - message = getString(R.string.gpg_key_select_mandatory), - length = Snackbar.LENGTH_LONG, - ) - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.selectKey.setOnClickListener { - gpgKeySelectAction.launch(PGPKeyListActivity.newSelectionActivity(requireContext())) - } - } - - companion object { - - fun newInstance() = KeySelectionFragment() - } -} diff --git a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/WelcomeFragment.kt b/app/src/main/java/app/passwordstore/ui/onboarding/fragments/WelcomeFragment.kt deleted file mode 100644 index 44881c64..00000000 --- a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/WelcomeFragment.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.onboarding.fragments - -import android.os.Bundle -import android.view.View -import androidx.annotation.Keep -import androidx.fragment.app.Fragment -import app.passwordstore.R -import app.passwordstore.databinding.FragmentWelcomeBinding -import app.passwordstore.ui.settings.SettingsActivity -import app.passwordstore.util.extensions.launchActivity -import app.passwordstore.util.extensions.performTransactionWithBackStack -import app.passwordstore.util.extensions.viewBinding - -@Keep -class WelcomeFragment : Fragment(R.layout.fragment_welcome) { - - private val binding by viewBinding(FragmentWelcomeBinding::bind) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.letsGo.setOnClickListener { - parentFragmentManager.performTransactionWithBackStack(CloneFragment.newInstance()) - } - binding.settingsButton.setOnClickListener { - requireActivity().launchActivity(SettingsActivity::class.java) - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt b/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt deleted file mode 100644 index d276c552..00000000 --- a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -@file:Suppress("BlockingMethodInNonBlockingContext") - -package app.passwordstore.ui.pgp - -import android.os.Bundle -import androidx.activity.result.contract.ActivityResultContracts.OpenDocument -import androidx.appcompat.app.AppCompatActivity -import app.passwordstore.R -import app.passwordstore.crypto.KeyUtils.tryGetId -import app.passwordstore.crypto.PGPKey -import app.passwordstore.crypto.PGPKeyManager -import app.passwordstore.crypto.errors.KeyAlreadyExistsException -import com.github.michaelbull.result.Result -import com.github.michaelbull.result.runCatching -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject -import kotlinx.coroutines.runBlocking - -@AndroidEntryPoint -class PGPKeyImportActivity : AppCompatActivity() { - - /** - * A [ByteArray] containing the contents of the previously selected file. This is necessary for - * the replacement case where we do not want users to have to pick the file again. - */ - private var lastBytes: ByteArray? = null - @Inject lateinit var keyManager: PGPKeyManager - - private val pgpKeyImportAction = - registerForActivityResult(OpenDocument()) { uri -> - runCatching { - if (uri == null) { - return@runCatching null - } - val keyInputStream = - contentResolver.openInputStream(uri) - ?: throw IllegalStateException("Failed to open selected file") - val bytes = keyInputStream.use { `is` -> `is`.readBytes() } - importKey(bytes, false) - } - .run(::handleImportResult) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - pgpKeyImportAction.launch(arrayOf("*/*")) - } - - override fun onDestroy() { - lastBytes = null - super.onDestroy() - } - - private fun importKey(bytes: ByteArray, replace: Boolean): PGPKey? { - lastBytes = bytes - val (key, error) = runBlocking { keyManager.addKey(PGPKey(bytes), replace = replace) } - if (replace) { - lastBytes = null - } - if (error != null) throw error - return key - } - - private fun handleImportResult(result: Result) { - if (result.isOk) { - val key = result.value - if (key == null) { - setResult(RESULT_CANCELED) - finish() - // This return convinces Kotlin that the control flow for `key == null` definitely - // terminates here and allows for a smart cast below. - return - } - MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.pgp_key_import_succeeded)) - .setMessage(getString(R.string.pgp_key_import_succeeded_message, tryGetId(key))) - .setPositiveButton(android.R.string.ok) { _, _ -> - setResult(RESULT_OK) - finish() - } - .setCancelable(false) - .show() - } else { - if (result.error is KeyAlreadyExistsException && lastBytes != null) { - MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.pgp_key_import_failed)) - .setMessage(getString(R.string.pgp_key_import_failed_replace_message)) - .setPositiveButton(R.string.dialog_yes) { _, _ -> - handleImportResult(runCatching { importKey(lastBytes!!, replace = true) }) - } - .setNegativeButton(R.string.dialog_no) { _, _ -> finish() } - .setCancelable(false) - .show() - } else { - MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.pgp_key_import_failed)) - .setMessage(result.error.message) - .setPositiveButton(android.R.string.ok) { _, _ -> finish() } - .setCancelable(false) - .show() - } - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyList.kt b/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyList.kt deleted file mode 100644 index 312f5fb0..00000000 --- a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyList.kt +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.pgp - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import app.passwordstore.R -import app.passwordstore.crypto.PGPIdentifier -import app.passwordstore.ui.compose.theme.APSTheme -import app.passwordstore.ui.compose.theme.SpacingLarge -import app.passwordstore.util.extensions.conditional -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList - -@Composable -fun KeyList( - identifiers: ImmutableList, - onItemClick: (identifier: PGPIdentifier) -> Unit, - modifier: Modifier = Modifier, - onKeySelected: ((identifier: PGPIdentifier) -> Unit)? = null, -) { - if (identifiers.isEmpty()) { - Column( - modifier = modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Image( - painter = painterResource(id = R.drawable.ic_launcher_foreground), - contentDescription = "Password Store logo", - ) - Text(stringResource(R.string.pgp_key_manager_no_keys_guidance)) - } - } else { - LazyColumn(modifier = modifier) { - items(identifiers) { identifier -> - KeyItem(identifier = identifier, onItemClick = onItemClick, onKeySelected = onKeySelected) - } - } - } -} - -@Composable -private fun KeyItem( - identifier: PGPIdentifier, - onItemClick: (identifier: PGPIdentifier) -> Unit, - modifier: Modifier = Modifier, - onKeySelected: ((identifier: PGPIdentifier) -> Unit)? = null, -) { - var isDeleting by remember { mutableStateOf(false) } - DeleteConfirmationDialog( - isDeleting = isDeleting, - onDismiss = { isDeleting = false }, - onConfirm = { - onItemClick(identifier) - isDeleting = false - }, - ) - val label = - when (identifier) { - is PGPIdentifier.KeyId -> identifier.id.toString() - is PGPIdentifier.UserId -> identifier.email - } - Row( - modifier = - modifier.padding(SpacingLarge).fillMaxWidth().conditional(onKeySelected != null) { - clickable { onKeySelected?.invoke(identifier) } - }, - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = label, - modifier = Modifier.weight(1f), - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - if (onKeySelected == null) { - IconButton(onClick = { isDeleting = true }) { - Icon( - painter = painterResource(R.drawable.ic_delete_24dp), - stringResource(id = R.string.delete), - ) - } - } - } -} - -@Suppress("NOTHING_TO_INLINE") -@Composable -private inline fun DeleteConfirmationDialog( - isDeleting: Boolean, - noinline onDismiss: () -> Unit, - noinline onConfirm: () -> Unit, -) { - if (isDeleting) { - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text(text = stringResource(R.string.pgp_key_manager_delete_confirmation_dialog_title)) - }, - confirmButton = { - TextButton(onClick = onConfirm) { Text(text = stringResource(R.string.dialog_yes)) } - }, - dismissButton = { - TextButton(onClick = onDismiss) { Text(text = stringResource(R.string.dialog_no)) } - }, - ) - } -} - -@Preview -@Composable -private fun KeyListPreview() { - APSTheme { - Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) { - KeyList( - identifiers = - listOfNotNull( - PGPIdentifier.fromString("ultramicroscopicsilicovolcanoconiosis@example.com"), - PGPIdentifier.fromString("0xB950AE2813841585"), - ) - .toPersistentList(), - onItemClick = {}, - ) - } - } -} - -@Preview -@Composable -private fun EmptyKeyListPreview() { - APSTheme { - Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) { - KeyList(identifiers = persistentListOf(), onItemClick = {}) - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyListActivity.kt b/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyListActivity.kt deleted file mode 100644 index 77b9aaf1..00000000 --- a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyListActivity.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.pgp - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult -import androidx.activity.viewModels -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import app.passwordstore.R -import app.passwordstore.ui.APSAppBar -import app.passwordstore.ui.compose.theme.APSTheme -import app.passwordstore.util.viewmodel.PGPKeyListViewModel -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class PGPKeyListActivity : ComponentActivity() { - - private val viewModel: PGPKeyListViewModel by viewModels() - private val keyImportAction = - registerForActivityResult(StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - viewModel.updateKeySet() - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val isSelecting = intent.extras?.getBoolean(EXTRA_KEY_SELECTION) ?: false - setContent { - APSTheme { - Scaffold( - topBar = { - APSAppBar( - title = stringResource(R.string.activity_label_pgp_key_manager), - navigationIcon = painterResource(R.drawable.ic_arrow_back_black_24dp), - onNavigationIconClick = { finish() }, - backgroundColor = MaterialTheme.colorScheme.surface, - ) - }, - floatingActionButton = { - FloatingActionButton( - onClick = { keyImportAction.launch(Intent(this, PGPKeyImportActivity::class.java)) } - ) { - Icon( - painter = painterResource(R.drawable.ic_add_48dp), - stringResource(R.string.pref_import_pgp_key_title), - ) - } - }, - ) { paddingValues -> - KeyList( - identifiers = viewModel.keys, - onItemClick = viewModel::deleteKey, - modifier = Modifier.padding(paddingValues), - onKeySelected = - if (isSelecting) { - { identifier -> - val result = Intent() - result.putExtra(EXTRA_SELECTED_KEY, identifier.toString()) - setResult(RESULT_OK, result) - finish() - } - } else null, - ) - } - } - } - } - - companion object { - const val EXTRA_SELECTED_KEY = "SELECTED_KEY" - private const val EXTRA_KEY_SELECTION = "KEY_SELECTION_MODE" - - fun newSelectionActivity(context: Context): Intent { - val intent = Intent(context, PGPKeyListActivity::class.java) - intent.putExtra(EXTRA_KEY_SELECTION, true) - return intent - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/sshkeygen/ShowSshKeyFragment.kt b/app/src/main/java/app/passwordstore/ui/sshkeygen/ShowSshKeyFragment.kt deleted file mode 100644 index 3f04e35e..00000000 --- a/app/src/main/java/app/passwordstore/ui/sshkeygen/ShowSshKeyFragment.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package app.passwordstore.ui.sshkeygen - -import android.app.Dialog -import android.content.Intent -import android.os.Bundle -import androidx.fragment.app.DialogFragment -import app.passwordstore.R -import app.passwordstore.util.git.sshj.SshKey -import com.google.android.material.dialog.MaterialAlertDialogBuilder - -class ShowSshKeyFragment : DialogFragment() { - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val activity = requireActivity() - val publicKey = SshKey.sshPublicKey - return MaterialAlertDialogBuilder(requireActivity()).run { - setMessage(getString(R.string.ssh_keygen_message, publicKey)) - setTitle(R.string.your_public_key) - setNegativeButton(R.string.ssh_keygen_later) { _, _ -> - (activity as? SshKeyGenActivity)?.finish() - } - setPositiveButton(R.string.ssh_keygen_share) { _, _ -> - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, publicKey) - } - startActivity(Intent.createChooser(sendIntent, null)) - (activity as? SshKeyGenActivity)?.finish() - } - create() - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyGenActivity.kt b/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyGenActivity.kt deleted file mode 100644 index 618a4be0..00000000 --- a/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyGenActivity.kt +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package app.passwordstore.ui.sshkeygen - -import android.content.SharedPreferences -import android.os.Bundle -import android.security.keystore.UserNotAuthenticatedException -import android.view.MenuItem -import android.view.View -import android.view.inputmethod.InputMethodManager -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.edit -import androidx.core.content.getSystemService -import androidx.lifecycle.lifecycleScope -import app.passwordstore.R -import app.passwordstore.databinding.ActivitySshKeygenBinding -import app.passwordstore.injection.prefs.GitPreferences -import app.passwordstore.util.auth.BiometricAuthenticator -import app.passwordstore.util.auth.BiometricAuthenticator.Result -import app.passwordstore.util.coroutines.DispatcherProvider -import app.passwordstore.util.extensions.keyguardManager -import app.passwordstore.util.extensions.viewBinding -import app.passwordstore.util.git.sshj.SshKey -import com.github.michaelbull.result.fold -import com.github.michaelbull.result.runCatching -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -private enum class KeyGenType(val generateKey: suspend (requireAuthentication: Boolean) -> Unit) { - Rsa({ requireAuthentication -> - SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Rsa, requireAuthentication) - }), - Ecdsa({ requireAuthentication -> - SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Ecdsa, requireAuthentication) - }), - Ed25519({ requireAuthentication -> - SshKey.generateKeystoreWrappedEd25519Key(requireAuthentication) - }), -} - -@AndroidEntryPoint -class SshKeyGenActivity : AppCompatActivity() { - - private var keyGenType = KeyGenType.Ecdsa - private val binding by viewBinding(ActivitySshKeygenBinding::inflate) - @GitPreferences @Inject lateinit var gitPrefs: SharedPreferences - @Inject lateinit var dispatcherProvider: DispatcherProvider - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - with(binding) { - generate.setOnClickListener { - if (SshKey.exists) { - MaterialAlertDialogBuilder(this@SshKeyGenActivity).run { - setTitle(R.string.ssh_keygen_existing_title) - setMessage(R.string.ssh_keygen_existing_message) - setPositiveButton(R.string.ssh_keygen_existing_replace) { _, _ -> - lifecycleScope.launch { generate() } - } - setNegativeButton(R.string.ssh_keygen_existing_keep) { _, _ -> - setResult(RESULT_CANCELED) - finish() - } - show() - } - } else { - lifecycleScope.launch { generate() } - } - } - keyTypeGroup.check(R.id.key_type_ecdsa) - keyTypeExplanation.setText(R.string.ssh_keygen_explanation_ecdsa) - keyTypeGroup.addOnButtonCheckedListener { _, checkedId, isChecked -> - if (isChecked) { - keyGenType = - when (checkedId) { - R.id.key_type_ed25519 -> KeyGenType.Ed25519 - R.id.key_type_ecdsa -> KeyGenType.Ecdsa - R.id.key_type_rsa -> KeyGenType.Rsa - else -> throw IllegalStateException("Impossible key type selection") - } - keyTypeExplanation.setText( - when (keyGenType) { - KeyGenType.Ed25519 -> R.string.ssh_keygen_explanation_ed25519 - KeyGenType.Ecdsa -> R.string.ssh_keygen_explanation_ecdsa - KeyGenType.Rsa -> R.string.ssh_keygen_explanation_rsa - } - ) - } - } - keyRequireAuthentication.isEnabled = keyguardManager.isDeviceSecure - keyRequireAuthentication.isChecked = keyRequireAuthentication.isEnabled - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> { - onBackPressedDispatcher.onBackPressed() - true - } - else -> super.onOptionsItemSelected(item) - } - } - - private suspend fun generate() { - binding.generate.apply { - text = getString(R.string.ssh_key_gen_generating_progress) - isEnabled = false - } - binding.generate.text = getString(R.string.ssh_key_gen_generating_progress) - val result = runCatching { - withContext(dispatcherProvider.io()) { - val requireAuthentication = binding.keyRequireAuthentication.isChecked - if (requireAuthentication) { - val result = - withContext(dispatcherProvider.main()) { - suspendCoroutine { cont -> - BiometricAuthenticator.authenticate( - this@SshKeyGenActivity, - R.string.biometric_prompt_title_ssh_keygen, - ) { result -> - // Do not cancel on failed attempts as these are handled by the - // authenticator UI. - if (result !is Result.Retry) cont.resume(result) - } - } - } - if (result !is Result.Success) - throw UserNotAuthenticatedException(getString(R.string.biometric_auth_generic_failure)) - } - keyGenType.generateKey(requireAuthentication) - } - } - gitPrefs.edit { remove("ssh_key_local_passphrase") } - binding.generate.apply { - text = getString(R.string.ssh_keygen_generate) - isEnabled = true - } - result.fold( - success = { ShowSshKeyFragment().show(supportFragmentManager, "public_key") }, - failure = { e -> - e.printStackTrace() - MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.error_generate_ssh_key)) - .setMessage(getString(R.string.ssh_key_error_dialog_text) + e.message) - .setPositiveButton(getString(R.string.dialog_ok)) { _, _ -> - setResult(RESULT_OK) - finish() - } - .show() - }, - ) - hideKeyboard() - } - - private fun hideKeyboard() { - val imm = getSystemService() ?: return - var view = currentFocus - if (view == null) { - view = View(this) - } - imm.hideSoftInputFromWindow(view.windowToken, 0) - } -} diff --git a/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyImportActivity.kt b/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyImportActivity.kt deleted file mode 100644 index e4740741..00000000 --- a/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyImportActivity.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.sshkeygen - -import android.net.Uri -import android.os.Bundle -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity -import app.passwordstore.R -import app.passwordstore.util.git.sshj.SshKey -import com.github.michaelbull.result.onFailure -import com.github.michaelbull.result.runCatching -import com.google.android.material.dialog.MaterialAlertDialogBuilder - -class SshKeyImportActivity : AppCompatActivity() { - - private val sshKeyImportAction = - registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? -> - if (uri == null) { - finish() - return@registerForActivityResult - } - runCatching { - SshKey.import(uri) - Toast.makeText( - this, - resources.getString(R.string.ssh_key_success_dialog_title), - Toast.LENGTH_LONG, - ) - .show() - setResult(RESULT_OK) - finish() - } - .onFailure { e -> - MaterialAlertDialogBuilder(this) - .setTitle(resources.getString(R.string.ssh_key_error_dialog_title)) - .setMessage(e.message) - .setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> finish() } - .show() - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (SshKey.exists) { - MaterialAlertDialogBuilder(this).run { - setTitle(R.string.ssh_keygen_existing_title) - setMessage(R.string.ssh_keygen_existing_message) - setPositiveButton(R.string.ssh_keygen_existing_replace) { _, _ -> importSshKey() } - setNegativeButton(R.string.ssh_keygen_existing_keep) { _, _ -> finish() } - setOnCancelListener { finish() } - show() - } - } else { - importSshKey() - } - } - - private fun importSshKey() { - sshKeyImportAction.launch(arrayOf("*/*")) - } -} diff --git a/app/src/main/java/app/passwordstore/util/viewmodel/PGPKeyListViewModel.kt b/app/src/main/java/app/passwordstore/util/viewmodel/PGPKeyListViewModel.kt deleted file mode 100644 index 5c7429cd..00000000 --- a/app/src/main/java/app/passwordstore/util/viewmodel/PGPKeyListViewModel.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.util.viewmodel - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import app.passwordstore.crypto.KeyUtils -import app.passwordstore.crypto.PGPIdentifier -import app.passwordstore.crypto.PGPKeyManager -import com.github.michaelbull.result.map -import com.github.michaelbull.result.onSuccess -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.launch - -@HiltViewModel -class PGPKeyListViewModel @Inject constructor(private val keyManager: PGPKeyManager) : ViewModel() { - var keys: ImmutableList by mutableStateOf(persistentListOf()) - - init { - updateKeySet() - } - - fun updateKeySet() { - viewModelScope.launch { - keyManager - .getAllKeys() - .map { keys -> keys.mapNotNull { key -> KeyUtils.tryGetEmail(key) } } - .onSuccess { keys = it.toPersistentList() } - } - } - - fun deleteKey(identifier: PGPIdentifier) { - viewModelScope.launch { - keyManager.removeKey(identifier) - updateKeySet() - } - } -} diff --git a/app/src/main/res/drawable/ic_add_48dp.xml b/app/src/main/res/drawable/ic_add_48dp.xml deleted file mode 100644 index 7192e79d..00000000 --- a/app/src/main/res/drawable/ic_add_48dp.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_arrow_back_black_24dp.xml b/app/src/main/res/drawable/ic_arrow_back_black_24dp.xml deleted file mode 100644 index 234364a9..00000000 --- a/app/src/main/res/drawable/ic_arrow_back_black_24dp.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/layout/activity_git_clone.xml b/app/src/main/res/layout/activity_git_clone.xml deleted file mode 100644 index b2f699b1..00000000 --- a/app/src/main/res/layout/activity_git_clone.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_onboarding.xml b/app/src/main/res/layout/activity_onboarding.xml deleted file mode 100644 index d1153f5d..00000000 --- a/app/src/main/res/layout/activity_onboarding.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/layout/activity_ssh_keygen.xml b/app/src/main/res/layout/activity_ssh_keygen.xml deleted file mode 100644 index 49548b36..00000000 --- a/app/src/main/res/layout/activity_ssh_keygen.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/basic_bottom_sheet.xml b/app/src/main/res/layout/basic_bottom_sheet.xml deleted file mode 100644 index b45fc9c9..00000000 --- a/app/src/main/res/layout/basic_bottom_sheet.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_clone.xml b/app/src/main/res/layout/fragment_clone.xml deleted file mode 100644 index 9cb34761..00000000 --- a/app/src/main/res/layout/fragment_clone.xml +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_key_selection.xml b/app/src/main/res/layout/fragment_key_selection.xml deleted file mode 100644 index b9141960..00000000 --- a/app/src/main/res/layout/fragment_key_selection.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_welcome.xml b/app/src/main/res/layout/fragment_welcome.xml deleted file mode 100644 index c68cd56a..00000000 --- a/app/src/main/res/layout/fragment_welcome.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a9fa4b00..493391a8 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -171,7 +171,6 @@ Zeige das Passwort. Soll das entschlüsselte Passwort sichtbar sein? Dies deaktiviert nicht das Kopieren. Passwort unverschlüsselt senden an… - App-Icon Eintrag zum Ausfüllen auswählen Phishing-resistente Suche @@ -283,16 +282,12 @@ Es gab einen Konflikt während des Rebase-Prozesses. Ihr lokaler Branch %1$s wurde auf einen anderen Branch namens %2$s gepusht.\nNutzen Sie diesen Branch, um den Konflikt auf dem Server zu beheben Das Repository befindet sich nicht im Rebase-Prozess; es ist nicht nötig, den Zustand auf einen weiteren Branch zu pushen. - Um fortzufahren, muss ein PGP-Schlüssel ausgewählt werden. Dateiname bereits vergeben Ein Ordner mit diesem Namen existiert bereits. - Los geht\'s - Wählen Sie einen \nRepository-Typ. Wählen Sie aus, ob Sie ein lokales Repository anlegen, oder ein Remote-Repository klonen möchten. Remote-Repository klonen Lokales Repository anlegen - Wählen Sie einen\nPGP-Schlüssel. Wählen einen PGP-Schlüssel, mit dem der Store initialisiert werden soll. Schlüssel auswählen diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d41048f8..d2a469e7 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -159,7 +159,6 @@ 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 Envoyer le mot de passe en clair via… - Icône de l\'application Sélectionnez l\'entrée à remplir Recherche protégée contre le hameçonnage @@ -271,12 +270,9 @@ Un fichier portant ce nom existe déjà Un dossier portant ce nom existe déjà - Allons-y - Sélectionnez le type de dépôt \n Sélectionnez si vous voulez créer un dépôt local ou cloner un dépôt distant. Cloner le dépôt distant Créer un dépôt local - Sélectionnez\nla clé GPG Sélectionnez une clé GPG pour initialiser votre magasin Sélectionner la clé diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 3fb321e7..ad4c663d 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -172,7 +172,6 @@ Mostrar contrasinal Controlar a visibilidade do contrasinal unha vez descifrado, esto non desactiva o copiado do contrasinal Enviar contrasinal como texto plano usando… - Icona da app Elixe o elemento a completar Busca resistente ao phishing @@ -285,16 +284,12 @@ a app desde unha fonte de confianza, como a Play Store, Amazon Appstore, F-Droid Hai un conflito ó realizar rebase. A túa póla local %1$s fixo push a outra póla chamada %2$s\nUsa esta póla para resolver o conflito na túa computadora O repositorio non cambiou de base, non é preciso cambiar a outra póla - Hai que elexir unha chave GPG para continuar Xa existe un ficheiro con ese nome Xa existe un cartafol con ese nome - Imos alá - Escolle \nTipo de repositorio Escolle se queres crear un repositorio local ou clonar un remoto. Clonar repositorio remoto Crear repositorio local - Elixe\nChave\nGPG Elixe a chave GPG coa que queres iniciar a almacenaxe Elixe chave diff --git a/app/src/main/res/values-hdpi/dimens.xml b/app/src/main/res/values-hdpi/dimens.xml deleted file mode 100644 index f9a452aa..00000000 --- a/app/src/main/res/values-hdpi/dimens.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - 50dp - 70dp - 48dp - diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 72bcb009..8edc9d3d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -154,7 +154,6 @@ Mostra la password Controlla la visibilità delle password una volta decrittografate. Questo non disabilita la copia negli appunti. Invia password come testo semplice usando… - Icona app Seleziona la voce da compilare Ricerca resistente al phishing @@ -265,12 +264,9 @@ Un file con quel nome esiste già Una cartella con quel nome esiste già - Andiamo - Seleziona \nTipo di repository Seleziona se vuoi creare un repo locale o clonarne uno remoto. Clona repo remoto Crea repo locale - Seleziona\nChiave\nGPG Seleziona una chiave GPG con cui inizializzare il tuo archivio Seleziona chiave @@ -315,7 +311,6 @@ Trovato .gpg-id, ma contiene un ID chiave, un\'impronta o un ID utente non valido Trovato .gpg-id, ma contiene un ID esadecimale corto, che non è supportato Merge non riuscito, il repository è in conflitto. - Per proseguire devi selezionare una chiave GPG Il protocollo git:// è sconsigliato Il protocollo git di git-daemon non cifra il trasporto e non è adatto a operazioni sicure. Impostazioni proxy diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 3efbed68..aaa438dd 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -167,7 +167,6 @@ 비밀번호 표시하기 암호가 해독된 후 비밀번호의 공개 여부를 제어합니다. 그렇다고 해서 클립보드 복사가 비활성화되지는 않습니다. 비밀번호를 일반 텍스트로 보내는 중… - 앱 아이콘 입력할 항목 선택 피싱 방지 검색 @@ -281,16 +280,12 @@ username@example.com:… 리베이스하는 동안 충돌이 발생했습니다. 로컬 %1$s 브랜치가 %2$s이라는 다른 브랜치로 푸시되었습니다.\n이 브랜치를 사용하여 컴퓨터에서 충돌을 해결하세요. 이 저장소는 리베이스가 아니므로 다른 브랜치로 푸시할 필요가 없습니다. - 계속하려면 GPG 키를 선택해야 합니다 동일한 이름의 파일이 이미 있습니다 이 이름의 폴더가 이미 존재합니다 - 시작합니다! - 저장소 유형 \n선택 로컬 리포지토리를 만들 것인지, 원격 리포지토리를 복제할 것인지 선택합니다. 원격 저장소 복제 로컬 저장소 생성 - GPG 키\n선택 스토어를 초기화할 GPG 키를 선택합니다 키 선택 diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index 65f14ce5..815cf41c 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -168,7 +168,6 @@ Pokaż hasło Kontroluj widoczność haseł po odszyfrowaniu. To nie wyłącza kopiowania do schowka. Wyślij niezaszyfrowane hasło, używając… - Ikona aplikacji Wybierz wpis do wypełnienia Wyszukiwanie odporne na wyłudzenie @@ -278,12 +277,9 @@ Plik o tej nazwie już istnieje Folder o tej nazwie już istnieje - Zaczynajmy - Wybierz \ntyp repozytorium Zaznacz, czy chcesz utworzyć lokalne repozytorium, czy sklonować zdalne repozytorium. Klonuj zdalne repozytorium Utwórz lokalne repozytorium - Wybierz\nklucz GPG Wybierz klucz GPG, którym zainicjować swoje repozytorium Wybierz klucz diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 86ebfb24..a1d1a38c 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -171,7 +171,6 @@ Mostrar a senha Controle a visibilidade das senhas quando descriptografadas. Isso não desativa a cópia para a área de transferência. Enviar senha como texto simples usando… - Ícone do aplicativo Selecione a entrada para preencher Busca resistente a phishing @@ -283,16 +282,12 @@ Houve um conflito ao executar o rebase. Houve um push de branch %1$s local para outra branch chamada %2$s\n Use esta branch para resolver o conflito no seu computador O repositório não está sendo rebased, não há necessidade de fazer push para outro branch - A seleção de uma chave GPG é necessária para prosseguir Já existe um arquivo com esse nome Já existe uma pasta com esse nome - Vamos lá - Selecione \nTipo de Repositório Selecione se você quer criar um repositório local ou clonar um repositório remoto. Clonar repositório remoto Criar repositório local - Selecione\nChave GPG Selecione uma chave GPG para inicializar o armazenamento com Selecione a chave diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 8873b34f..d0934a4d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -164,7 +164,6 @@ Показывать пароли Видимость расшифрованных паролей, не влияет на копирование в буфер. Поделиться паролем в открытом виде с помощью - Иконка приложения Выберите запись для заполнения Устойчивый к фишингу поиск @@ -275,16 +274,12 @@ При попытке rebase\'а возник конфликт. Ваша локальная ветка %1$s была запушена %2$s\n Используйте эту ветку для разрешения конфликтов с помощью компьютера Не нужно пушить в другую ветку - rebase не был запущен - Для продолжения необходимо выбрать GPG-ключ Файл с таким именем уже существует Папка с таким именем уже существует - Поехали - Выберите \nтип репозитория Создать локальное хранилище, или клонировать удаленный репозиторий. Клонировать удаленный репозиторий Создать локальное хранилище - Выберите\nGPG ключ Выберите GPG ключ для инициализации хранилища Выберите ключ diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 746b74d2..ddcdda1a 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -172,7 +172,6 @@ 显示密码 控制密码解密后的可见性,这不会禁用复制到剪贴板 使用明文发送密码… - 应用图标 选择要填写的条目 防钓鱼搜索 @@ -285,18 +284,12 @@ personal.com 使用此分支解决计算机上的冲突 存储库没有变更,无需推送到另一个分支 - 选择一个 GPG 密钥才能继续 该名称的文件已存在 该名称的文件夹已存在 - 开始配置 - 选择仓库模式 你是要同步仓库还是现在创建一个仓库呢? 同步远程仓库 创建本地仓库 - 选择 -GPG -密钥 选择一个GPG密钥来初始化您的仓库 选择密钥 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index e5d7ff94..8351a358 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -7,9 +7,5 @@ 16dp 16dp - 16dp 8dp - 100dp - 140dp - 48dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dc8069b8..59ba74cb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -199,7 +199,6 @@ Show the password Control the visibility of the passwords once decrypted. This does not disable copying to clipboard. Send password as plaintext using… - App icon Select entry to fill into @@ -321,17 +320,13 @@ The repository is not rebasing, no need to push to another branch - Selecting a GPG key is necessary to proceed A file by that name already exists A folder by that name already exists - Let\'s Go - Select \nRepository Type Select if you want to create a local repo or clone a remote repo. Clone Remote Repo Create Local Repo - Select\nGPG Key Select a GPG key to initialize your store with Select 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 7eab1692..94f76479 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 @@ -51,6 +51,23 @@ the maintainer's phone stays on 0.1.4 until then. 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. +### Setup + +- Without a store the app opens on a welcome screen: clone a remote repository or create a + local store; Settings is reachable from it. +- The git server screen serves the clone and later edits: URL, authentication mode (SSH key or + password; none or password for HTTP(S)), the SSH key's state with "Manage", "Clear saved host + key". A failed clone stays on the screen, so URL or key can be fixed and the clone retried. +- The SSH key screen shows the key's state and, for a key generated on the device, the public + key (copy, share); it imports a key file (refusing files sshj cannot read, such as OpenPGP + keys) and opens key generation (RSA, ECDSA, Ed25519, optionally behind the screen lock). +- One PGP keys screen manages keys (import, delete), picks the key of a new local store (the + repository is created only then, its `.gpg-id` naming the key by its uppercase long key ID, as + `pass init ` does), and imports the key a store needs: after a clone that no + imported key can decrypt, and when an entry is opened with no key imported. +- Screens outside the graph (Settings, autofill, git operations) and the entry view's key + import open these through a `MainActivity` setup intent until steps 6 and 7. + ## Architecture ### Activities diff --git a/ui/compose/src/main/kotlin/app/passwordstore/ui/APSAppBar.kt b/ui/compose/src/main/kotlin/app/passwordstore/ui/APSAppBar.kt deleted file mode 100644 index 7ac99023..00000000 --- a/ui/compose/src/main/kotlin/app/passwordstore/ui/APSAppBar.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui - -import android.annotation.SuppressLint -import androidx.compose.foundation.layout.RowScope -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import app.passwordstore.ui.compose.preview.DevicePreviews -import app.passwordstore.ui.compose.preview.ThemePreviews -import app.passwordstore.ui.compose.theme.APSTheme -import app.passwordstore.ui.compose.theme.SpacingMedium - -@SuppressLint("ComposableLambdaParameterNaming") // The lint doesn't really apply to `actions` -@Composable -@OptIn(ExperimentalMaterial3Api::class) -public fun APSAppBar( - title: String, - backgroundColor: Color, - navigationIcon: Painter?, - modifier: Modifier = Modifier, - onNavigationIconClick: (() -> Unit) = {}, - actions: @Composable RowScope.() -> Unit = {}, -) { - TopAppBar( - title = { Text(text = title) }, - navigationIcon = { - if (navigationIcon != null) { - IconButton(onClick = onNavigationIconClick) { - Icon(painter = navigationIcon, contentDescription = "Back navigation button") - } - } - }, - actions = actions, - colors = TopAppBarDefaults.topAppBarColors(containerColor = backgroundColor), - modifier = modifier.shadow(SpacingMedium), - ) -} - -@ThemePreviews -@DevicePreviews -@Composable -private fun APSAppBarPreview() { - APSTheme { - APSAppBar( - title = "Preview", - backgroundColor = MaterialTheme.colorScheme.surface, - navigationIcon = rememberVectorPainter(Icons.AutoMirrored.Filled.ArrowBack), - actions = { - IconButton(onClick = {}) { - Icon( - painter = rememberVectorPainter(Icons.Filled.Search), - contentDescription = "Search items", - ) - } - }, - ) - } -} -- cgit v1.2.3 From cebdf8dd4720388d56d9af537fe81fd927e51779 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 3 Nov 2026 18:50:51 +0100 Subject: refactor(app): remove the legacy settings screens The Compose settings, git config, commit log and proxy screens replace SettingsActivity with its six preference providers, GitConfigActivity with TextInputDialog, GitLogActivity with its adapter and GitLogModel, and ProxySelectorActivity, with their layouts and manifest entries. The dead OpenKeystore key ID row goes with its string, the Android 14 platform bug summary and normal_margin lose their last users, and the setup intent loses the two starts only Settings used. Co-Authored-By: Claude Opus 5.5 --- app/src/main/AndroidManifest.xml | 23 --- .../passwordstore/ui/dialogs/TextInputDialog.kt | 71 -------- .../ui/git/config/GitConfigActivity.kt | 176 ------------------- .../app/passwordstore/ui/git/log/GitLogActivity.kt | 49 ------ .../app/passwordstore/ui/git/log/GitLogAdapter.kt | 65 ------- .../java/app/passwordstore/ui/main/MainActivity.kt | 8 +- .../ui/proxy/ProxySelectorActivity.kt | 109 ------------ .../passwordstore/ui/settings/AutofillSettings.kt | 114 ------------ .../passwordstore/ui/settings/GeneralSettings.kt | 120 ------------- .../app/passwordstore/ui/settings/MiscSettings.kt | 76 -------- .../app/passwordstore/ui/settings/PGPSettings.kt | 136 --------------- .../passwordstore/ui/settings/PasswordSettings.kt | 48 ----- .../ui/settings/RepositorySettings.kt | 193 --------------------- .../passwordstore/ui/settings/SettingsActivity.kt | 129 -------------- .../passwordstore/ui/settings/SettingsProvider.kt | 15 -- .../java/app/passwordstore/ui/setup/SetupGraph.kt | 2 - .../java/app/passwordstore/util/git/GitLogModel.kt | 60 ------- app/src/main/res/layout/activity_git_config.xml | 113 ------------ app/src/main/res/layout/activity_git_log.xml | 22 --- .../layout/activity_preference_recyclerview.xml | 18 -- .../main/res/layout/activity_proxy_selector.xml | 106 ----------- app/src/main/res/layout/dialog_text_input.xml | 25 --- app/src/main/res/layout/git_log_row_layout.xml | 45 ----- .../main/res/layout/oreo_autofill_instructions.xml | 51 ------ app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-gl/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 2 - app/src/main/res/values-ko-rKR/strings.xml | 1 - app/src/main/res/values-pl-rPL/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - app/src/main/res/values/dimens.xml | 1 - app/src/main/res/values/strings.xml | 2 - .../specs/2026-11-02-compose-ui-overhaul-design.md | 21 ++- 36 files changed, 22 insertions(+), 1786 deletions(-) delete mode 100644 app/src/main/java/app/passwordstore/ui/dialogs/TextInputDialog.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/git/config/GitConfigActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/git/log/GitLogActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/git/log/GitLogAdapter.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/proxy/ProxySelectorActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/settings/AutofillSettings.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/settings/GeneralSettings.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/settings/MiscSettings.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/settings/SettingsActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/settings/SettingsProvider.kt delete mode 100644 app/src/main/java/app/passwordstore/util/git/GitLogModel.kt delete mode 100644 app/src/main/res/layout/activity_git_config.xml delete mode 100644 app/src/main/res/layout/activity_git_log.xml delete mode 100644 app/src/main/res/layout/activity_preference_recyclerview.xml delete mode 100644 app/src/main/res/layout/activity_proxy_selector.xml delete mode 100644 app/src/main/res/layout/dialog_text_input.xml delete mode 100644 app/src/main/res/layout/git_log_row_layout.xml delete mode 100644 app/src/main/res/layout/oreo_autofill_instructions.xml (limited to 'app/src/main/res/layout') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 546bdde4..677321c7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,12 +33,6 @@ tools:ignore="GoogleAppIndexingWarning" tools:targetApi="s"> - - - - - - - - - * doSomething(bundle.getString(TextInputDialog.BUNDLE_KEY_TEXT)) - * } - * ``` - */ -class TextInputDialog : DialogFragment() { - private val binding by unsafeLazy { DialogTextInputBinding.inflate(layoutInflater) } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val builder = MaterialAlertDialogBuilder(requireContext()) - builder.setView(binding.root) - builder.setTitle(arguments?.getString(BUNDLE_KEY_TITLE)) - arguments?.getString(BUNDLE_KEY_HINT)?.let { hint -> - binding.textInputLayout.isHintEnabled = true - binding.textInputLayout.isHintAnimationEnabled = true - binding.textInputLayout.hint = hint - } - builder.setPositiveButton(android.R.string.ok) { dialogInterface, _ -> - setFragmentResult(REQUEST_KEY, bundleOf(BUNDLE_KEY_TEXT to binding.editText.text.toString())) - dialogInterface.dismiss() - } - val dialog = builder.create() - dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) - return dialog - } - - override fun onCancel(dialog: DialogInterface) { - super.onCancel(dialog) - finish() - } - - companion object { - const val REQUEST_KEY = "text_input_dialog" - const val BUNDLE_KEY_TEXT = "text" - private const val BUNDLE_KEY_TITLE = "title" - private const val BUNDLE_KEY_HINT = "hint" - - fun newInstance(title: String, hint: String? = null): TextInputDialog { - val args = bundleOf(BUNDLE_KEY_TITLE to title, BUNDLE_KEY_HINT to hint) - val dialog = TextInputDialog() - dialog.arguments = args - return dialog - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/git/config/GitConfigActivity.kt b/app/src/main/java/app/passwordstore/ui/git/config/GitConfigActivity.kt deleted file mode 100644 index 695029d4..00000000 --- a/app/src/main/java/app/passwordstore/ui/git/config/GitConfigActivity.kt +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package app.passwordstore.ui.git.config - -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.util.Patterns -import android.view.MenuItem -import androidx.core.os.postDelayed -import androidx.fragment.app.setFragmentResultListener -import androidx.lifecycle.lifecycleScope -import app.passwordstore.R -import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.databinding.ActivityGitConfigBinding -import app.passwordstore.ui.dialogs.TextInputDialog -import app.passwordstore.ui.git.base.BaseGitActivity -import app.passwordstore.ui.git.log.GitLogActivity -import app.passwordstore.util.extensions.asLog -import app.passwordstore.util.extensions.launchActivity -import app.passwordstore.util.extensions.viewBinding -import com.github.michaelbull.result.fold -import com.github.michaelbull.result.getOrElse -import com.github.michaelbull.result.onFailure -import com.github.michaelbull.result.runCatching -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar -import kotlinx.coroutines.launch -import logcat.LogPriority.ERROR -import logcat.logcat -import org.eclipse.jgit.lib.Constants -import org.eclipse.jgit.lib.Repository -import org.eclipse.jgit.lib.RepositoryState - -class GitConfigActivity : BaseGitActivity() { - - private val binding by viewBinding(ActivityGitConfigBinding::inflate) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - if (gitSettings.authorName.isEmpty()) binding.gitUserName.requestFocus() - else binding.gitUserName.setText(gitSettings.authorName) - binding.gitUserEmail.setText(gitSettings.authorEmail) - setupTools() - binding.saveButton.setOnClickListener { - val email = binding.gitUserEmail.text.toString().trim() - val name = binding.gitUserName.text.toString().trim() - if (!email.matches(Patterns.EMAIL_ADDRESS.toRegex())) { - MaterialAlertDialogBuilder(this) - .setMessage(getString(R.string.invalid_email_dialog_text)) - .setPositiveButton(getString(R.string.dialog_ok), null) - .show() - } else { - gitSettings.authorEmail = email - gitSettings.authorName = name - Snackbar.make( - binding.root, - getString(R.string.git_server_config_save_success), - Snackbar.LENGTH_SHORT, - ) - .show() - Handler(Looper.getMainLooper()).postDelayed(500) { finish() } - } - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> { - onBackPressedDispatcher.onBackPressed() - true - } - else -> super.onOptionsItemSelected(item) - } - } - - /** Sets up the UI components of the tools section. */ - private fun setupTools() { - val repo = PasswordRepository.repository - if (repo != null) { - binding.gitHeadStatus.text = headStatusMsg(repo) - // enable the abort button only if we're rebasing or merging - val needsAbort = - repo.repositoryState.isRebasing || repo.repositoryState == RepositoryState.MERGING - binding.gitAbortRebase.isEnabled = needsAbort - binding.gitAbortRebase.alpha = if (needsAbort) 1.0f else 0.5f - } - binding.gitLog.setOnClickListener { - runCatching { launchActivity(GitLogActivity::class.java) } - .onFailure { ex -> logcat(ERROR) { ex.asLog("Failed to start GitLogActivity") } } - } - binding.gitAbortRebase.setOnClickListener { abortRebase() } - binding.gitResetToRemote.setOnClickListener { resetToRemote() } - binding.gitGc.setOnClickListener { - lifecycleScope.launch { - launchGitOperation(GitOp.GC) - .fold( - success = ::finishOnSuccessHandler, - failure = { err -> promptOnErrorHandler(err) { finish() } }, - ) - } - } - } - - private fun resetToRemote() { - val dialog = - TextInputDialog.newInstance(getString(R.string.git_utils_reset_remote_branch_title)) - dialog.show(supportFragmentManager, "BRANCH_INPUT_DIALOG") - dialog.setFragmentResultListener(TextInputDialog.REQUEST_KEY) { _, bundle -> - val result = bundle.getString(TextInputDialog.BUNDLE_KEY_TEXT) - if (!result.isNullOrEmpty()) { - remoteBranch = result - lifecycleScope.launch { - launchGitOperation(GitOp.RESET) - .fold( - success = ::finishOnSuccessHandler, - failure = { err -> promptOnErrorHandler(err) { finish() } }, - ) - } - } - } - } - - private fun abortRebase() { - lifecycleScope.launch { - launchGitOperation(GitOp.BREAK_OUT_OF_DETACHED) - .fold( - success = { - val branch = PasswordRepository.getCurrentBranch() - MaterialAlertDialogBuilder(this@GitConfigActivity).run { - setTitle(resources.getString(R.string.git_abort_and_push_title)) - setMessage( - resources.getString( - R.string.git_break_out_of_detached_success, - branch, - "conflicting-$branch-...", - ) - ) - setOnDismissListener { finish() } - setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> } - show() - } - }, - failure = { err -> promptOnErrorHandler(err) { finish() } }, - ) - } - } - - /** - * Returns a user-friendly message about the current state of HEAD. - * - * The state is recognized to be either pointing to a branch or detached. - */ - private fun headStatusMsg(repo: Repository): String { - return runCatching { - val headRef = repo.findRef(Constants.HEAD) - if (headRef.isSymbolic) { - val branchName = headRef.target.name - val shortBranchName = Repository.shortenRefName(branchName) - getString(R.string.git_head_on_branch, shortBranchName) - } else { - val commitHash = headRef.objectId.abbreviate(8).name() - getString(R.string.git_head_detached, commitHash) - } - } - .getOrElse { ex -> - logcat(ERROR) { "Error getting HEAD reference\n${ex}" } - getString(R.string.git_head_missing) - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/git/log/GitLogActivity.kt b/app/src/main/java/app/passwordstore/ui/git/log/GitLogActivity.kt deleted file mode 100644 index a72881d1..00000000 --- a/app/src/main/java/app/passwordstore/ui/git/log/GitLogActivity.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.git.log - -import android.os.Bundle -import android.view.MenuItem -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import app.passwordstore.databinding.ActivityGitLogBinding -import app.passwordstore.ui.git.base.BaseGitActivity -import app.passwordstore.util.extensions.viewBinding - -/** - * Displays the repository's git commits in git-log fashion. - * - * It provides basic information about each commit by way of a non-interactive RecyclerView. - */ -class GitLogActivity : BaseGitActivity() { - - private val binding by viewBinding(ActivityGitLogBinding::inflate) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - createRecyclerView() - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> { - finish() - true - } - else -> super.onOptionsItemSelected(item) - } - } - - private fun createRecyclerView() { - binding.gitLogRecyclerView.apply { - setHasFixedSize(true) - addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL)) - adapter = GitLogAdapter() - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/git/log/GitLogAdapter.kt b/app/src/main/java/app/passwordstore/ui/git/log/GitLogAdapter.kt deleted file mode 100644 index 95186188..00000000 --- a/app/src/main/java/app/passwordstore/ui/git/log/GitLogAdapter.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.git.log - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import app.passwordstore.databinding.GitLogRowLayoutBinding -import app.passwordstore.util.git.GitCommit -import app.passwordstore.util.git.GitLogModel -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle -import java.util.Locale -import logcat.LogPriority.ERROR -import logcat.logcat - -private fun shortHash(hash: String): String { - return hash.substring(0 until 8) -} - -private fun stringFrom(date: Instant): String { - return DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) - .withLocale(Locale.getDefault()) - .withZone(ZoneId.systemDefault()) - .format(date) -} - -/** @see GitLogActivity */ -class GitLogAdapter : RecyclerView.Adapter() { - - private val model = GitLogModel() - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val inflater = LayoutInflater.from(parent.context) - val binding = GitLogRowLayoutBinding.inflate(inflater, parent, false) - return ViewHolder(binding) - } - - override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { - val commit = model.get(position) - if (commit == null) { - logcat(ERROR) { "There is no git commit for view holder at position $position." } - return - } - viewHolder.bind(commit) - } - - override fun getItemCount() = model.size - - class ViewHolder(private val binding: GitLogRowLayoutBinding) : - RecyclerView.ViewHolder(binding.root) { - - fun bind(commit: GitCommit) = - with(binding) { - gitLogRowMessage.text = commit.shortMessage - gitLogRowHash.text = shortHash(commit.hash) - gitLogRowTime.text = stringFrom(commit.time) - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt b/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt index 4d618cca..75062f6b 100644 --- a/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt @@ -258,13 +258,13 @@ class MainActivity : BaseGitActivity() { private const val EXTRA_SETUP = "SETUP" /** - * Opens one setup screen in an instance of its own, for callers outside the graph (legacy - * Settings, autofill, git operations) and for those waiting for a result (the entry view's + * Opens one setup screen in an instance of its own, for callers outside the graph (autofill, + * the git operations' "No SSH key" dialog) and for those waiting for a result (the entry view's * missing key). Leaving that screen finishes the instance; the PGP keys screen sets RESULT_OK * once a key was imported. */ - // ponytail: a second instance instead of in-graph navigation, until steps 6 and 7 move - // Settings and autofill into the graph. + // ponytail: a second instance instead of in-graph navigation, until step 7 moves autofill + // into the graph and the git operations' dialogs become Compose dialogs. fun setupIntent(context: Context, start: SetupStart): Intent = Intent(context, MainActivity::class.java).putExtra(EXTRA_SETUP, start.name) } diff --git a/app/src/main/java/app/passwordstore/ui/proxy/ProxySelectorActivity.kt b/app/src/main/java/app/passwordstore/ui/proxy/ProxySelectorActivity.kt deleted file mode 100644 index cb5fbf40..00000000 --- a/app/src/main/java/app/passwordstore/ui/proxy/ProxySelectorActivity.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.proxy - -import android.content.SharedPreferences -import android.net.InetAddresses -import android.os.Build -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.util.Patterns -import android.view.MenuItem -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.edit -import androidx.core.os.postDelayed -import androidx.core.widget.doOnTextChanged -import app.passwordstore.R -import app.passwordstore.databinding.ActivityProxySelectorBinding -import app.passwordstore.injection.prefs.ProxyPreferences -import app.passwordstore.util.extensions.getString -import app.passwordstore.util.extensions.viewBinding -import app.passwordstore.util.proxy.ProxyUtils -import app.passwordstore.util.settings.GitSettings -import app.passwordstore.util.settings.PreferenceKeys -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -@AndroidEntryPoint -class ProxySelectorActivity : AppCompatActivity() { - - @Inject lateinit var gitSettings: GitSettings - @ProxyPreferences @Inject lateinit var proxyPrefs: SharedPreferences - @Inject lateinit var proxyUtils: ProxyUtils - private val binding by viewBinding(ActivityProxySelectorBinding::inflate) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - with(binding) { - proxyHost.setText(proxyPrefs.getString(PreferenceKeys.PROXY_HOST)) - proxyUser.setText(proxyPrefs.getString(PreferenceKeys.PROXY_USERNAME)) - proxyPrefs - .getInt(PreferenceKeys.PROXY_PORT, -1) - .takeIf { it != -1 } - ?.let { proxyPort.setText("$it") } - proxyPassword.setText(proxyPrefs.getString(PreferenceKeys.PROXY_PASSWORD)) - save.setOnClickListener { saveSettings() } - proxyHost.doOnTextChanged { text, _, _, _ -> - if (text != null) { - proxyHost.error = - if (isNumericAddress(text) || text.matches(WEB_ADDRESS_REGEX)) { - null - } else { - getString(R.string.invalid_proxy_url) - } - } - } - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressedDispatcher.onBackPressed() - } - else -> return super.onOptionsItemSelected(item) - } - return true - } - - private fun isNumericAddress(text: CharSequence): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - InetAddresses.isNumericAddress(text as String) - } else { - @Suppress("DEPRECATION") Patterns.IP_ADDRESS.matcher(text).matches() - } - } - - private fun saveSettings() { - proxyPrefs.edit { - binding.proxyHost.text - ?.toString() - ?.takeIf { it.isNotEmpty() } - .let { gitSettings.proxyHost = it } - binding.proxyUser.text - ?.toString() - ?.takeIf { it.isNotEmpty() } - .let { gitSettings.proxyUsername = it } - binding.proxyPort.text - ?.toString() - ?.takeIf { it.isNotEmpty() } - ?.let { gitSettings.proxyPort = it.toInt() } - binding.proxyPassword.text - ?.toString() - ?.takeIf { it.isNotEmpty() } - .let { gitSettings.proxyPassword = it } - } - proxyUtils.setDefaultProxy() - Handler(Looper.getMainLooper()).postDelayed(500) { finish() } - } - - private companion object { - private val WEB_ADDRESS_REGEX = Patterns.WEB_URL.toRegex() - } -} diff --git a/app/src/main/java/app/passwordstore/ui/settings/AutofillSettings.kt b/app/src/main/java/app/passwordstore/ui/settings/AutofillSettings.kt deleted file mode 100644 index afb2a691..00000000 --- a/app/src/main/java/app/passwordstore/ui/settings/AutofillSettings.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.settings - -import android.annotation.SuppressLint -import android.content.Intent -import android.provider.Settings -import androidx.appcompat.widget.AppCompatTextView -import androidx.core.net.toUri -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import app.passwordstore.BuildConfig -import app.passwordstore.R -import app.passwordstore.util.extensions.autofillManager -import app.passwordstore.util.settings.PreferenceKeys -import com.github.androidpasswordstore.autofillparser.BrowserAutofillSupportLevel -import com.github.androidpasswordstore.autofillparser.getInstalledBrowsersWithAutofillSupportLevel -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import de.Maxr1998.modernpreferences.PreferenceScreen -import de.Maxr1998.modernpreferences.helpers.editText -import de.Maxr1998.modernpreferences.helpers.onClick -import de.Maxr1998.modernpreferences.helpers.switch -import de.Maxr1998.modernpreferences.preferences.SwitchPreference - -class AutofillSettings(private val activity: FragmentActivity) : SettingsProvider { - - private val isAutofillServiceEnabled: Boolean - get() { - return activity.autofillManager?.hasEnabledAutofillServices() == true - } - - private fun showAutofillDialog(pref: SwitchPreference) { - val observer = LifecycleEventObserver { _, event -> - when (event) { - Lifecycle.Event.ON_RESUME -> { - pref.checked = isAutofillServiceEnabled - } - else -> {} - } - } - MaterialAlertDialogBuilder(activity).run { - setTitle(R.string.pref_autofill_enable_title) - @SuppressLint("InflateParams") - val layout = activity.layoutInflater.inflate(R.layout.oreo_autofill_instructions, null) - val supportedBrowsersTextView = layout.findViewById(R.id.supportedBrowsers) - supportedBrowsersTextView.text = - getInstalledBrowsersWithAutofillSupportLevel(context).joinToString(separator = "\n") { - val appLabel = it.first - val supportDescription = - when (it.second) { - BrowserAutofillSupportLevel.None -> - activity.getString(R.string.oreo_autofill_no_support) - BrowserAutofillSupportLevel.FlakyFill -> - activity.getString(R.string.oreo_autofill_flaky_fill_support) - BrowserAutofillSupportLevel.PasswordFill -> - activity.getString(R.string.oreo_autofill_password_fill_support) - BrowserAutofillSupportLevel.PasswordFillAndSaveIfNoAccessibility -> - activity.getString( - R.string.oreo_autofill_password_fill_and_conditional_save_support - ) - BrowserAutofillSupportLevel.GeneralFill -> - activity.getString(R.string.oreo_autofill_general_fill_support) - BrowserAutofillSupportLevel.GeneralFillAndSave -> - activity.getString(R.string.oreo_autofill_general_fill_and_save_support) - } - "$appLabel: $supportDescription" - } - setView(layout) - setPositiveButton(R.string.dialog_ok) { _, _ -> - val intent = - Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE).apply { - data = "package:${BuildConfig.APPLICATION_ID}".toUri() - } - activity.startActivity(intent) - } - setNegativeButton(R.string.dialog_cancel, null) - setOnDismissListener { pref.checked = isAutofillServiceEnabled } - activity.lifecycle.addObserver(observer) - show() - } - } - - override fun provideSettings(builder: PreferenceScreen.Builder) { - builder.apply { - switch(PreferenceKeys.AUTOFILL_ENABLE) { - titleRes = R.string.pref_autofill_enable_title - defaultValue = isAutofillServiceEnabled - onClick { - if (isAutofillServiceEnabled) { - activity.autofillManager?.disableAutofillServices() - } else { - showAutofillDialog(this) - } - false - } - } - editText(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME) { - dependency = PreferenceKeys.AUTOFILL_ENABLE - titleRes = R.string.preference_default_username_title - summaryProvider = { activity.getString(R.string.preference_default_username_summary) } - } - editText(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES) { - dependency = PreferenceKeys.AUTOFILL_ENABLE - titleRes = R.string.preference_custom_public_suffixes_title - summaryProvider = { activity.getString(R.string.preference_custom_public_suffixes_summary) } - textInputHintRes = R.string.preference_custom_public_suffixes_hint - } - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/settings/GeneralSettings.kt b/app/src/main/java/app/passwordstore/ui/settings/GeneralSettings.kt deleted file mode 100644 index e3594269..00000000 --- a/app/src/main/java/app/passwordstore/ui/settings/GeneralSettings.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.settings - -import android.content.pm.ShortcutManager -import android.os.Build -import androidx.core.content.edit -import androidx.core.content.getSystemService -import androidx.fragment.app.FragmentActivity -import app.passwordstore.R -import app.passwordstore.util.auth.BiometricAuthenticator -import app.passwordstore.util.auth.BiometricAuthenticator.Result -import app.passwordstore.util.extensions.sharedPrefs -import app.passwordstore.util.settings.DirectoryStructure -import app.passwordstore.util.settings.PreferenceKeys -import de.Maxr1998.modernpreferences.PreferenceScreen -import de.Maxr1998.modernpreferences.helpers.onClick -import de.Maxr1998.modernpreferences.helpers.singleChoice -import de.Maxr1998.modernpreferences.helpers.switch -import de.Maxr1998.modernpreferences.preferences.choice.SelectionItem - -class GeneralSettings(private val activity: FragmentActivity) : SettingsProvider { - - override fun provideSettings(builder: PreferenceScreen.Builder) { - builder.apply { - val themeValues = activity.resources.getStringArray(R.array.app_theme_values) - val themeOptions = activity.resources.getStringArray(R.array.app_theme_options) - val themeItems = - themeValues.zip(themeOptions).map { SelectionItem(it.first, it.second, null) } - singleChoice(PreferenceKeys.APP_THEME, themeItems) { - initialSelection = activity.resources.getString(R.string.app_theme_def) - titleRes = R.string.pref_app_theme_title - } - - val values = activity.resources.getStringArray(R.array.directory_structure_values) - val titles = activity.resources.getStringArray(R.array.directory_structure_entries) - val items = values.zip(titles).map { SelectionItem(it.first, it.second, null) } - singleChoice(PreferenceKeys.DIRECTORY_STRUCTURE, items) { - initialSelection = DirectoryStructure.DEFAULT.value - titleRes = R.string.pref_directory_structure - } - - val sortValues = activity.resources.getStringArray(R.array.sort_order_values) - val sortOptions = activity.resources.getStringArray(R.array.sort_order_entries) - val sortItems = sortValues.zip(sortOptions).map { SelectionItem(it.first, it.second, null) } - singleChoice(PreferenceKeys.SORT_ORDER, sortItems) { - initialSelection = sortValues[0] - titleRes = R.string.pref_sort_order_title - } - - switch(PreferenceKeys.DISABLE_SYNC_ACTION) { - titleRes = R.string.pref_disable_sync_on_pull_title - summaryRes = R.string.pref_disable_sync_on_pull_summary - defaultValue = false - } - - switch(PreferenceKeys.FILTER_RECURSIVELY) { - titleRes = R.string.pref_recursive_filter_title - summaryRes = R.string.pref_recursive_filter_summary - defaultValue = true - } - - switch(PreferenceKeys.SEARCH_ON_START) { - titleRes = R.string.pref_search_on_start_title - summaryRes = R.string.pref_search_on_start_summary - defaultValue = false - } - - switch(PreferenceKeys.SHOW_HIDDEN_CONTENTS) { - titleRes = R.string.pref_show_hidden_title - summaryRes = R.string.pref_show_hidden_summary - defaultValue = false - } - - // See https://github.com/android-password-store/Android-Password-Store/issues/2802 - val disableAuth = Build.VERSION.SDK_INT == Build.VERSION_CODES.UPSIDE_DOWN_CAKE - val canAuthenticate = BiometricAuthenticator.canAuthenticate(activity) - switch(PreferenceKeys.BIOMETRIC_AUTH_2) { - titleRes = R.string.pref_biometric_auth_title - defaultValue = false - enabled = !disableAuth - summaryRes = - if (disableAuth) R.string.pref_biometric_auth_summary_disabled_platform - else if (canAuthenticate) R.string.pref_biometric_auth_summary - else R.string.pref_biometric_auth_summary_error - onClick { - enabled = false - val isChecked = checked - activity.sharedPrefs.edit { - BiometricAuthenticator.authenticate(activity) { result -> - when (result) { - is Result.Success -> { - // Apply the changes - putBoolean(PreferenceKeys.BIOMETRIC_AUTH_2, checked) - enabled = true - } - is Result.Retry -> {} - else -> { - // If any error occurs, revert back to the previous - // state. This - // catch-all clause includes the cancellation case. - putBoolean(PreferenceKeys.BIOMETRIC_AUTH_2, !checked) - checked = !isChecked - enabled = true - } - } - } - } - activity.getSystemService()?.apply { - removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList()) - } - false - } - } - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/settings/MiscSettings.kt b/app/src/main/java/app/passwordstore/ui/settings/MiscSettings.kt deleted file mode 100644 index 9a5d1e3b..00000000 --- a/app/src/main/java/app/passwordstore/ui/settings/MiscSettings.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.settings - -import android.content.Context -import android.content.Intent -import android.net.Uri -import androidx.activity.result.contract.ActivityResultContracts -import androidx.documentfile.provider.DocumentFile -import androidx.fragment.app.FragmentActivity -import app.passwordstore.BuildConfig -import app.passwordstore.R -import app.passwordstore.util.services.PasswordExportService -import app.passwordstore.util.settings.PreferenceKeys -import de.Maxr1998.modernpreferences.PreferenceScreen -import de.Maxr1998.modernpreferences.helpers.onClick -import de.Maxr1998.modernpreferences.helpers.pref -import de.Maxr1998.modernpreferences.helpers.switch - -class MiscSettings(activity: FragmentActivity) : SettingsProvider { - - private val storeExportAction = - activity.registerForActivityResult( - object : ActivityResultContracts.OpenDocumentTree() { - override fun createIntent(context: Context, input: Uri?): Intent { - return super.createIntent(context, input).apply { - flags = - Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION or - Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or - Intent.FLAG_GRANT_PREFIX_URI_PERMISSION - } - } - } - ) { uri: Uri? -> - if (uri == null) return@registerForActivityResult - val targetDirectory = DocumentFile.fromTreeUri(activity.applicationContext, uri) - - if (targetDirectory != null) { - val service = - Intent(activity.applicationContext, PasswordExportService::class.java).apply { - action = PasswordExportService.ACTION_EXPORT_PASSWORD - putExtra("uri", uri) - } - - activity.startForegroundService(service) - } - } - - override fun provideSettings(builder: PreferenceScreen.Builder) { - builder.apply { - pref(PreferenceKeys.EXPORT_PASSWORDS) { - titleRes = R.string.prefs_export_passwords_title - summaryRes = R.string.prefs_export_passwords_summary - onClick { - storeExportAction.launch(null) - true - } - } - switch(PreferenceKeys.CLEAR_CLIPBOARD_HISTORY) { - defaultValue = false - titleRes = R.string.pref_clear_clipboard_title - summaryRes = R.string.pref_clear_clipboard_summary - } - switch(PreferenceKeys.ENABLE_DEBUG_LOGGING) { - defaultValue = false - titleRes = R.string.pref_debug_logging_title - summaryRes = R.string.pref_debug_logging_summary - visible = !BuildConfig.DEBUG - } - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt b/app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt deleted file mode 100644 index 46e62faa..00000000 --- a/app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.settings - -import androidx.core.content.edit -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.lifecycleScope -import app.passwordstore.R -import app.passwordstore.data.crypto.PGPPassphraseCache -import app.passwordstore.ui.main.MainActivity -import app.passwordstore.ui.setup.SetupStart -import app.passwordstore.util.auth.BiometricAuthenticator -import app.passwordstore.util.extensions.sharedPrefs -import app.passwordstore.util.features.Feature -import app.passwordstore.util.settings.PreferenceKeys -import de.Maxr1998.modernpreferences.PreferenceScreen -import de.Maxr1998.modernpreferences.helpers.onClick -import de.Maxr1998.modernpreferences.helpers.pref -import de.Maxr1998.modernpreferences.helpers.switch -import kotlinx.coroutines.launch - -class PGPSettings( - private val activity: FragmentActivity, - private val passphraseCache: PGPPassphraseCache, -) : SettingsProvider { - - override fun provideSettings(builder: PreferenceScreen.Builder) { - builder.apply { - pref("_") { - titleRes = R.string.pref_pgp_key_manager_title - persistent = false - onClick { - activity.startActivity(MainActivity.setupIntent(activity, SetupStart.PgpKeys)) - false - } - } - switch(PreferenceKeys.ASCII_ARMOR) { - titleRes = R.string.pref_pgp_ascii_armor_title - persistent = true - } - switch(Feature.EnablePGPPassphraseCache.configKey) { - enabled = BiometricAuthenticator.canAuthenticate(activity) - titleRes = R.string.pref_passphrase_cache_title - summaryRes = R.string.pref_passphrase_cache_summary - defaultValue = false - onClick { - if (BiometricAuthenticator.canAuthenticate(activity)) { - val promptTitle = - if (checked) R.string.pref_passphrase_cache_authenticate_enable - else R.string.pref_passphrase_cache_authenticate_disable - - BiometricAuthenticator.authenticate(activity, promptTitle) { result -> - when (result) { - is BiometricAuthenticator.Result.Success -> { - /* Any successful change of this setting clears the passphrase - * cache for safety */ - activity.lifecycleScope.launch { - passphraseCache.clearAllCachedPassphrases(activity) - } - activity.sharedPrefs.edit { - putBoolean(Feature.EnablePGPPassphraseCache.configKey, checked) - if (!checked) remove(PreferenceKeys.CLEAR_PASSPHRASE_CACHE) - } - } - is BiometricAuthenticator.Result.Retry -> {} - else -> { - /* revert back to previous state in case of error or cancellation */ - checked = !checked - activity.sharedPrefs.edit { - putBoolean(Feature.EnablePGPPassphraseCache.configKey, checked) - } - } - } - } - } else { - /* we may get here if device lock has been disabled while PGP settings - * screen was left open */ - checked = false - enabled = false - activity.sharedPrefs.edit { - putBoolean(Feature.EnablePGPPassphraseCache.configKey, false) - } - } - false - } - } - switch(PreferenceKeys.CLEAR_PASSPHRASE_CACHE) { - dependency = Feature.EnablePGPPassphraseCache.configKey - titleRes = R.string.pref_passphrase_cache_auto_clear_title - summaryRes = R.string.pref_passphrase_cache_auto_clear_summary - defaultValue = true - /* Clear the cache once when unchecking; this is to prevent a malicious user (someone - * knowing the screen-lock pin, but not knowing the PGP passphrase) from bypassing cache - * clearing via the settings. Disabling it requires authentication, and cache clearing - * stays enabled in case of any authentication failure. */ - onClick { - if (!checked) { - if ( - BiometricAuthenticator.canAuthenticate(activity) && - activity.sharedPrefs.getBoolean(Feature.EnablePGPPassphraseCache.configKey, false) - ) { - BiometricAuthenticator.authenticate( - activity, - R.string.pref_passphrase_cache_auto_clear_authenticate_disable, - ) { result -> - when (result) { - is BiometricAuthenticator.Result.Success -> { - activity.sharedPrefs.edit { - putBoolean(PreferenceKeys.CLEAR_PASSPHRASE_CACHE, false) - } - activity.lifecycleScope.launch { - passphraseCache.clearAllCachedPassphrases(activity) - } - } - is BiometricAuthenticator.Result.Retry -> {} - else -> { - activity.sharedPrefs.edit { remove(PreferenceKeys.CLEAR_PASSPHRASE_CACHE) } - checked = true - } - } - } - } else { - activity.sharedPrefs.edit { remove(PreferenceKeys.CLEAR_PASSPHRASE_CACHE) } - checked = true - enabled = false - } - } - false - } - } - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt b/app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt deleted file mode 100644 index 9fe4cd42..00000000 --- a/app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.settings - -import android.text.InputType -import androidx.fragment.app.FragmentActivity -import app.passwordstore.R -import app.passwordstore.util.settings.PreferenceKeys -import de.Maxr1998.modernpreferences.PreferenceScreen -import de.Maxr1998.modernpreferences.helpers.editText -import de.Maxr1998.modernpreferences.helpers.singleChoice -import de.Maxr1998.modernpreferences.helpers.switch -import de.Maxr1998.modernpreferences.preferences.choice.SelectionItem - -class PasswordSettings(private val activity: FragmentActivity) : SettingsProvider { - - override fun provideSettings(builder: PreferenceScreen.Builder) { - builder.apply { - val values = activity.resources.getStringArray(R.array.pwgen_provider_values) - val labels = activity.resources.getStringArray(R.array.pwgen_provider_labels) - val items = values.zip(labels).map { SelectionItem(it.first, it.second, null) } - singleChoice(PreferenceKeys.PREF_KEY_PWGEN_TYPE, items) { - initialSelection = "diceware" - titleRes = R.string.pref_password_generator_type_title - } - editText(PreferenceKeys.GENERAL_SHOW_TIME) { - titleRes = R.string.pref_clipboard_timeout_title - summaryProvider = { timeout -> - activity.getString(R.string.pref_clipboard_timeout_summary, timeout ?: "45") - } - textInputType = InputType.TYPE_CLASS_NUMBER - } - switch(PreferenceKeys.SHOW_PASSWORD) { - titleRes = R.string.show_password_pref_title - summaryRes = R.string.show_password_pref_summary - defaultValue = true - } - switch(PreferenceKeys.COPY_ON_DECRYPT) { - titleRes = R.string.pref_copy_title - summaryRes = R.string.pref_copy_summary - defaultValue = false - } - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt b/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt deleted file mode 100644 index 23dd2829..00000000 --- a/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.settings - -import android.content.SharedPreferences -import android.content.pm.ShortcutManager -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult -import androidx.core.content.edit -import androidx.core.content.getSystemService -import androidx.fragment.app.FragmentActivity -import app.passwordstore.R -import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.injection.prefs.GitPreferences -import app.passwordstore.ui.git.config.GitConfigActivity -import app.passwordstore.ui.main.MainActivity -import app.passwordstore.ui.proxy.ProxySelectorActivity -import app.passwordstore.ui.setup.SetupStart -import app.passwordstore.util.extensions.getString -import app.passwordstore.util.extensions.launchActivity -import app.passwordstore.util.extensions.sharedPrefs -import app.passwordstore.util.extensions.snackbar -import app.passwordstore.util.extensions.unsafeLazy -import app.passwordstore.util.git.sshj.SshKey -import app.passwordstore.util.settings.GitSettings -import app.passwordstore.util.settings.PreferenceKeys -import com.github.michaelbull.result.onFailure -import com.github.michaelbull.result.runCatching -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent -import de.Maxr1998.modernpreferences.Preference -import de.Maxr1998.modernpreferences.PreferenceScreen -import de.Maxr1998.modernpreferences.helpers.onClick -import de.Maxr1998.modernpreferences.helpers.pref -import de.Maxr1998.modernpreferences.helpers.switch - -class RepositorySettings(private val activity: FragmentActivity) : SettingsProvider { - - private val generateSshKey = - activity.registerForActivityResult(StartActivityForResult()) { - showSshKeyPref?.visible = SshKey.canShowSshPublicKey - } - - private val hiltEntryPoint by unsafeLazy { - EntryPointAccessors.fromApplication( - activity.applicationContext, - RepositorySettingsEntryPoint::class.java, - ) - } - - private var showSshKeyPref: Preference? = null - - override fun provideSettings(builder: PreferenceScreen.Builder) { - val encryptedPreferences = hiltEntryPoint.encryptedPreferences() - val gitSettings = hiltEntryPoint.gitSettings() - - builder.apply { - switch(PreferenceKeys.REBASE_ON_PULL) { - titleRes = R.string.pref_rebase_on_pull_title - summaryRes = R.string.pref_rebase_on_pull_summary - summaryOnRes = R.string.pref_rebase_on_pull_summary_on - defaultValue = true - } - pref(PreferenceKeys.GIT_SERVER_INFO) { - titleRes = R.string.pref_edit_git_server_settings - visible = PasswordRepository.isGitRepo() - onClick { - activity.startActivity(MainActivity.setupIntent(activity, SetupStart.GitServer)) - true - } - } - pref(PreferenceKeys.PROXY_SETTINGS) { - titleRes = R.string.pref_edit_proxy_settings - visible = gitSettings.url?.startsWith("https") == true && PasswordRepository.isGitRepo() - onClick { - activity.launchActivity(ProxySelectorActivity::class.java) - true - } - } - pref(PreferenceKeys.GIT_CONFIG) { - titleRes = R.string.pref_edit_git_config - visible = PasswordRepository.isGitRepo() - onClick { - activity.launchActivity(GitConfigActivity::class.java) - true - } - } - pref(PreferenceKeys.SSH_KEY) { - titleRes = R.string.pref_import_ssh_key_title - visible = PasswordRepository.isGitRepo() - onClick { - generateSshKey.launch(MainActivity.setupIntent(activity, SetupStart.Ssh)) - true - } - } - pref(PreferenceKeys.SSH_KEYGEN) { - titleRes = R.string.pref_ssh_keygen_title - onClick { - generateSshKey.launch(MainActivity.setupIntent(activity, SetupStart.SshGenerate)) - true - } - } - showSshKeyPref = - pref(PreferenceKeys.SSH_SEE_KEY) { - titleRes = R.string.pref_ssh_see_key_title - visible = PasswordRepository.isGitRepo() && SshKey.canShowSshPublicKey - onClick { - generateSshKey.launch(MainActivity.setupIntent(activity, SetupStart.Ssh)) - true - } - } - pref(PreferenceKeys.CLEAR_SAVED_PASS) { - fun Preference.updatePref() { - val sshPass = encryptedPreferences.getString(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) - val httpsPass = encryptedPreferences.getString(PreferenceKeys.HTTPS_PASSWORD) - if (sshPass == null && httpsPass == null) { - visible = false - return - } - titleRes = - when { - httpsPass != null -> R.string.clear_saved_passphrase_https - else -> R.string.clear_saved_passphrase_ssh - } - visible = true - requestRebind() - } - onClick { - updatePref() - true - } - updatePref() - } - pref(PreferenceKeys.SSH_OPENKEYSTORE_CLEAR_KEY_ID) { - titleRes = R.string.pref_title_openkeystore_clear_keyid - visible = - activity.sharedPrefs.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID)?.isNotEmpty() - ?: false - onClick { - activity.sharedPrefs.edit { putString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, null) } - visible = false - true - } - } - pref(PreferenceKeys.GIT_DELETE_REPO) { - titleRes = R.string.pref_git_delete_repo_title - summaryRes = R.string.pref_git_delete_repo_summary - onClick { - val repoDir = PasswordRepository.getRepositoryDirectory() - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.pref_dialog_delete_title) - .setMessage(activity.getString(R.string.dialog_delete_msg, repoDir)) - .setCancelable(false) - .setPositiveButton(R.string.dialog_delete) { dialogInterface, _ -> - runCatching { - PasswordRepository.closeRepository() - PasswordRepository.getRepositoryDirectory().let { dir -> - dir.deleteRecursively() - dir.mkdirs() - } - } - .onFailure { it.message?.let { message -> activity.snackbar(message = message) } } - - activity.getSystemService()?.apply { - removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList()) - } - activity.sharedPrefs.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false) } - dialogInterface.cancel() - activity.finish() - } - .setNegativeButton(R.string.dialog_do_not_delete) { dialogInterface, _ -> - run { dialogInterface.cancel() } - } - .show() - true - } - } - } - } - - @EntryPoint - @InstallIn(SingletonComponent::class) - interface RepositorySettingsEntryPoint { - fun gitSettings(): GitSettings - - @GitPreferences fun encryptedPreferences(): SharedPreferences - } -} diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsActivity.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsActivity.kt deleted file mode 100644 index 02addd84..00000000 --- a/app/src/main/java/app/passwordstore/ui/settings/SettingsActivity.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.settings - -import android.os.Bundle -import android.view.MenuItem -import androidx.activity.OnBackPressedCallback -import androidx.appcompat.app.AppCompatActivity -import androidx.core.os.BundleCompat -import app.passwordstore.R -import app.passwordstore.data.crypto.PGPPassphraseCache -import app.passwordstore.databinding.ActivityPreferenceRecyclerviewBinding -import app.passwordstore.util.extensions.viewBinding -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dagger.hilt.android.AndroidEntryPoint -import de.Maxr1998.modernpreferences.Preference -import de.Maxr1998.modernpreferences.PreferencesAdapter -import de.Maxr1998.modernpreferences.helpers.screen -import de.Maxr1998.modernpreferences.helpers.subScreen -import javax.inject.Inject - -@AndroidEntryPoint -class SettingsActivity : AppCompatActivity() { - - @Inject lateinit var passphraseCache: PGPPassphraseCache - private val miscSettings = MiscSettings(this) - private val autofillSettings = AutofillSettings(this) - private val passwordSettings = PasswordSettings(this) - private val repositorySettings = RepositorySettings(this) - private val generalSettings = GeneralSettings(this) - private lateinit var pgpSettings: PGPSettings - - private val binding by viewBinding(ActivityPreferenceRecyclerviewBinding::inflate) - private val preferencesAdapter: PreferencesAdapter - get() = binding.preferenceRecyclerView.adapter as PreferencesAdapter - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - Preference.Config.dialogBuilderFactory = { context -> MaterialAlertDialogBuilder(context) } - pgpSettings = PGPSettings(this, passphraseCache) - val screen = - screen(this) { - subScreen { - collapseIcon = true - titleRes = R.string.pref_category_general_title - iconRes = R.drawable.app_settings_alt_24px - generalSettings.provideSettings(this) - } - subScreen { - collapseIcon = true - titleRes = R.string.pref_category_autofill_title - iconRes = R.drawable.ic_wysiwyg_24px - autofillSettings.provideSettings(this) - } - subScreen { - collapseIcon = true - titleRes = R.string.pref_category_passwords_title - iconRes = R.drawable.ic_password_24px - passwordSettings.provideSettings(this) - } - subScreen { - collapseIcon = true - titleRes = R.string.pref_category_repository_title - iconRes = R.drawable.ic_call_merge_24px - repositorySettings.provideSettings(this) - } - subScreen { - collapseIcon = true - titleRes = R.string.pref_category_misc_title - iconRes = R.drawable.ic_miscellaneous_services_24px - miscSettings.provideSettings(this) - } - subScreen { - collapseIcon = true - titleRes = R.string.pref_category_pgp_title - iconRes = R.drawable.ic_lock_open_24px - pgpSettings.provideSettings(this) - } - } - val backPressedCallback = - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - preferencesAdapter.goBack() - } - } - onBackPressedDispatcher.addCallback(backPressedCallback) - val adapter = PreferencesAdapter(screen) - adapter.onScreenChangeListener = - PreferencesAdapter.OnScreenChangeListener { subScreen, entering -> - backPressedCallback.isEnabled = entering - supportActionBar?.title = - if (!entering) { - getString(R.string.action_settings) - } else { - getString(subScreen.titleRes) - } - } - if (savedInstanceState != null) { - BundleCompat.getParcelable( - savedInstanceState, - "adapter", - PreferencesAdapter.SavedState::class.java, - ) - ?.let(adapter::loadSavedState) - } - binding.preferenceRecyclerView.adapter = adapter - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putParcelable("adapter", preferencesAdapter.getSavedState()) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> - if (!preferencesAdapter.goBack()) { - super.onOptionsItemSelected(item) - } else { - true - } - else -> super.onOptionsItemSelected(item) - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsProvider.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsProvider.kt deleted file mode 100644 index cacca80f..00000000 --- a/app/src/main/java/app/passwordstore/ui/settings/SettingsProvider.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.settings - -import de.Maxr1998.modernpreferences.PreferenceScreen - -/** Used to generate a uniform API for all settings UI classes. */ -interface SettingsProvider { - - /** Inserts the settings items for the class into the given [builder]. */ - fun provideSettings(builder: PreferenceScreen.Builder) -} diff --git a/app/src/main/java/app/passwordstore/ui/setup/SetupGraph.kt b/app/src/main/java/app/passwordstore/ui/setup/SetupGraph.kt index c2c2a2ac..40dfae74 100644 --- a/app/src/main/java/app/passwordstore/ui/setup/SetupGraph.kt +++ b/app/src/main/java/app/passwordstore/ui/setup/SetupGraph.kt @@ -33,10 +33,8 @@ import kotlinx.serialization.Serializable /** The setup screens [MainActivity.setupIntent] opens on their own. */ enum class SetupStart(val route: Any) { - GitServer(GitServerRoute(clone = false)), Ssh(SshKeyRoute), SshGenerate(SshKeyGenRoute), - PgpKeys(PgpKeysRoute()), PgpImport(PgpKeysRoute(KeysMode.Need)), } diff --git a/app/src/main/java/app/passwordstore/util/git/GitLogModel.kt b/app/src/main/java/app/passwordstore/util/git/GitLogModel.kt deleted file mode 100644 index 329db6d5..00000000 --- a/app/src/main/java/app/passwordstore/util/git/GitLogModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.util.git - -import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.util.extensions.asLog -import app.passwordstore.util.extensions.hash -import app.passwordstore.util.extensions.time -import app.passwordstore.util.extensions.unsafeLazy -import com.github.michaelbull.result.getOrElse -import com.github.michaelbull.result.runCatching -import logcat.LogPriority.ERROR -import logcat.logcat -import org.eclipse.jgit.api.Git -import org.eclipse.jgit.revwalk.RevCommit - -private val TAG = GitLogModel::class.java.simpleName - -private fun commits(): Iterable { - val repo = PasswordRepository.repository - if (repo == null) { - logcat(TAG, ERROR) { "Could not access git repository" } - return listOf() - } - return runCatching { Git(repo).log().call() } - .getOrElse { e -> - logcat(TAG, ERROR) { e.asLog("Failed to obtain git commits") } - listOf() - } -} - -/** - * Provides [GitCommit] s from a git-log of the password git repository. - * - * All commits are acquired on the first request to this object. - */ -class GitLogModel { - - // All commits are acquired here at once. Acquiring the commits in batches would not have been - // entirely sensible because the amount of computation required to obtain commit number n from - // the log includes the amount of computation required to obtain commit number n-1 from the log. - // This is because the commit graph is walked from HEAD to the last commit to obtain. - // Additionally, tests with 1000 commits in the log have not produced a significant delay in the - // user experience. - private val cache: MutableList by unsafeLazy { - commits() - .map { GitCommit(it.hash, it.shortMessage, it.authorIdent.name, it.time) } - .toMutableList() - } - val size = cache.size - - fun get(index: Int): GitCommit? { - if (index >= size) - logcat(ERROR) { "Cannot get git commit with index $index. There are only $size." } - return cache.getOrNull(index) - } -} diff --git a/app/src/main/res/layout/activity_git_config.xml b/app/src/main/res/layout/activity_git_config.xml deleted file mode 100644 index b43ec6d6..00000000 --- a/app/src/main/res/layout/activity_git_config.xml +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_git_log.xml b/app/src/main/res/layout/activity_git_log.xml deleted file mode 100644 index e9884db0..00000000 --- a/app/src/main/res/layout/activity_git_log.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/activity_preference_recyclerview.xml b/app/src/main/res/layout/activity_preference_recyclerview.xml deleted file mode 100644 index 805bc79a..00000000 --- a/app/src/main/res/layout/activity_preference_recyclerview.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/activity_proxy_selector.xml b/app/src/main/res/layout/activity_proxy_selector.xml deleted file mode 100644 index b83ca9f7..00000000 --- a/app/src/main/res/layout/activity_proxy_selector.xml +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/dialog_text_input.xml b/app/src/main/res/layout/dialog_text_input.xml deleted file mode 100644 index c8539273..00000000 --- a/app/src/main/res/layout/dialog_text_input.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/git_log_row_layout.xml b/app/src/main/res/layout/git_log_row_layout.xml deleted file mode 100644 index e996f6af..00000000 --- a/app/src/main/res/layout/git_log_row_layout.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/oreo_autofill_instructions.xml b/app/src/main/res/layout/oreo_autofill_instructions.xml deleted file mode 100644 index 66881390..00000000 --- a/app/src/main/res/layout/oreo_autofill_instructions.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 618aac0d..08b74717 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -215,7 +215,6 @@ Biometrische Authentifizierung Falls aktiviert, werden Sie beim Starten der App nach Ihrem Fingerabdruck gefragt. Fingerabdrucksensor fehlt oder ist nicht ansprechbar. - Lösche gespeicherte OpenKeystore SSH-Schlüssel-ID. Ihr öffentlicher Schlüssel Fehler beim Erstellen des SSH-Schlüssels Alle Dateien und Ordner anzeigen diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4cc7d29d..796884d7 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -203,7 +203,6 @@ Activer l\'authentification biométrique Lorsque cette option est activée, Password Store vous demandera votre empreinte digitale au lancement Lecteur d\'empreinte digitale non accessible ou manquant - Effacer la clé d\'identification SSH d\'OpenKeystore mémorisée Votre clé publique Une erreur est survenue pendant la génération de la clé ssh Afficher tous les fichiers et dossiers diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 34b1ce6a..b79e2990 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -217,7 +217,6 @@ a app desde unha fonte de confianza, como a Play Store, Amazon Appstore, F-Droid Activar autenticación biométrica Ao activala, Password Store vaiche pedir a túa pegada dactilar ao iniciar a app O hardware de pegada dixital non é accesible ou existente - Eliminar o ID lembrado da chave SSH en OpenKeystore A túa chave pública Algo fallou ao intentar crear a chave-ssh Mostar cartafoles ocultos diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 28a38301..ee7affd2 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -198,7 +198,6 @@ Abilita autenticazione biometrica Se attiva, HardenedPass chiede l\'impronta digitale all\'avvio dell\'app L\'hardware delle impronte digitali non è accessibile o mancante - Elimina l\'ID della Chiave SSH di OpenKeystore memorizzato La tua chiave pubblica Errore durante la generazione della chiave SSH Mostra tutti i file e cartelle @@ -303,7 +302,6 @@ Autenticati per disattivare la cancellazione automatica Cancella automaticamente le frasi segrete memorizzate Cancella le frasi segrete memorizzate allo spegnimento dello schermo - Funzione non disponibile a causa di un bug della piattaforma Impossibile cifrare la password, non è stato salvato nulla: %1$s Nessuna immagine selezionata Trovato .gpg-id, ma contiene un ID chiave, un\'impronta o un ID utente non valido diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 06e0a555..b2836176 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -211,7 +211,6 @@ 생체 인증 사용 비밀번호 저장소를 활성화하면 앱을 시작할 때 지문을 입력하라는 메시지가 표시됩니다 지문 하드웨어에 액세스할 수 없거나 누락된 경우 - 저장된 OpenKeyStore SSH 키 ID 지우기 공개 키 Ssh 키를 생성하는 동안 오류가 발생했습니다. 모든 파일 및 폴더 표시 diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index 6bdb966c..34272cae 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -210,7 +210,6 @@ Włącz uwierzytelnianie biometryczne Gdy włączone, Password Store zapyta Cię o odcisk palca podczas uruchamiania aplikacji Czytnik linii papilarnych jest niedostępny - Wyczyść zapamiętany identyfikator klucza SSH OpenKeystore Twój klucz publiczny Błąd podczas generowania klucza SSH Pokaż ukryte pliki i foldery diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 8f40c831..9f60b64e 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -215,7 +215,6 @@ Ativar autenticação biométrica Quando ativado, o Password Store irá pedir a sua impressão digital ao iniciar o aplicativo Hardware de impressão digital não acessível ou ausente - Limpar ID de chave SSH lembrada do OpenKeystore Sua chave pública Erro ao tentar gerar a chave SSH Mostrar todos os arquivos e pastas diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index af3078bc..298d9508 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -208,7 +208,6 @@ Включить биометрическую аутентификацию Когда ключено, Password Store будет запрашивать ваш опечаток пальца при каждом запуске приложения Сенсор отпечатка пальца не доступен или отсутствует - Очистить сохраненный SSH Key идентификатор OpenKystortore Ваш публичный ключ Возникла ошибка при попытке генерации ssh ключа Показать все файлы и директории diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 959d5fac..85125fc2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -216,7 +216,6 @@ 启用生物识别认证 如果启用,Password Store 将在启动应用时提示您使用指纹进行身份验证 指纹硬件无法访问或丢失 - 清除记住的 OpenKeystore SSH 密钥ID 你的公开密钥 尝试生成SSH密钥时出错 显示所有文件和目录 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 8351a358..44d4f581 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -7,5 +7,4 @@ 16dp 16dp - 8dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 403ee2ae..653cf72c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -247,8 +247,6 @@ Enable biometric authentication When enabled, HardenedPass will prompt you for your fingerprint when launching the app Fingerprint hardware not accessible or missing - Feature unavailable due to a platform bug - Clear remembered OpenKeystore SSH Key ID Your public key Error while trying to generate the ssh-key Show all files and folders 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 94f76479..b3d0f0cd 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 @@ -65,8 +65,22 @@ the maintainer's phone stays on 0.1.4 until then. repository is created only then, its `.gpg-id` naming the key by its uppercase long key ID, as `pass init ` does), and imports the key a store needs: after a clone that no imported key can decrypt, and when an entry is opened with no key imported. -- Screens outside the graph (Settings, autofill, git operations) and the entry view's key - import open these through a `MainActivity` setup intent until steps 6 and 7. +- Settings opens these within the graph. Autofill, the git operations' "No SSH key" dialog and + the entry view's key import open them through a `MainActivity` setup intent until step 7. + +### Settings + +- Settings lists its categories (general, autofill, passwords, repository, misc, PGP); each opens + a screen of rows (switches, single choices, text values, actions) over the existing preference + keys. Switches behind the screen lock store their value only after a successful prompt. +- Rows that lead elsewhere navigate within `MainActivity`: git server, SSH key, key generation, + PGP keys, proxy, and the git configuration screen (author name and email, commit log, abort + rebase, hard reset, garbage collection). +- Destructive actions ask first: deleting the repository (which never follows symlinks out of the + store and returns to the welcome screen), clearing a saved password or passphrase, the hard + reset. +- The list's sort orders are folders first and recently used; "files first" and "type + independent" are gone, and stored values fall back to folders first. ## Architecture @@ -102,7 +116,8 @@ the maintainer's phone stays on 0.1.4 until then. Crypto, git, autofill parsing, preference keys, the Keystore-backed secret storage, clipboard handling and the passphrase cache. Existing stores, keys and settings carry over without -migration. +migration, with two exceptions from step 6: a stored "files first" or "type independent" sort +order (both removed) becomes folders first, and the unused OpenKeychain SSH key ID is dropped. ### Removed at the end -- cgit v1.2.3 From fa95b737778ab08203c679d716c1279f51cbd409 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 4 Nov 2026 11:23:25 +0100 Subject: feat(app): move the autofill picker to Compose AutofillFilterView draws the picker with the shared theme: origin header, search field, entry rows in a grouped surface and the three switches, over the same SearchableRepositoryViewModel search. Rows name the origin identifier and show the account and folders below it. The first tap wins, so a double tap no longer starts two decryptions. PasswordViewHolder, the RecyclerView adapter, the two layouts, their drawables, the multiselect color and their lint baseline entries go. Co-Authored-By: Claude Opus 5.5 --- app/lint-baseline.xml | 33 ---- .../ui/autofill/AutofillFilterScreen.kt | 144 +++++++++++++++++ .../ui/autofill/AutofillFilterView.kt | 178 ++++++++------------- .../ui/autofill/PasswordViewHolder.kt | 16 -- .../viewmodel/SearchableRepositoryViewModel.kt | 127 --------------- .../res/drawable/ic_keyboard_arrow_right_24dp.xml | 15 -- .../main/res/drawable/password_row_background.xml | 20 --- .../res/layout/activity_oreo_autofill_filter.xml | 132 --------------- .../main/res/layout/oreo_autofill_filter_row.xml | 69 -------- app/src/main/res/values-night/colors_material3.xml | 1 - app/src/main/res/values/colors_material3.xml | 1 - 11 files changed, 215 insertions(+), 521 deletions(-) create mode 100644 app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterScreen.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/autofill/PasswordViewHolder.kt delete mode 100644 app/src/main/res/drawable/ic_keyboard_arrow_right_24dp.xml delete mode 100644 app/src/main/res/drawable/password_row_background.xml delete mode 100644 app/src/main/res/layout/activity_oreo_autofill_filter.xml delete mode 100644 app/src/main/res/layout/oreo_autofill_filter_row.xml (limited to 'app/src/main/res/layout') diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index a19d0ad9..cd7a1217 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -98,17 +98,6 @@ file="src/main/res/mipmap-xxxhdpi/ic_launcher.png"/> - - - - - - - - - - - - diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterScreen.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterScreen.kt new file mode 100644 index 00000000..49e19799 --- /dev/null +++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterScreen.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2026 Danilo M. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.ui.autofill + +import androidx.compose.foundation.Image +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.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import app.passwordstore.R +import app.passwordstore.ui.compose.EntryRow +import app.passwordstore.ui.compose.GroupedSurface +import app.passwordstore.ui.compose.theme.SpacingLarge +import app.passwordstore.ui.list.ListRow +import app.passwordstore.ui.settings.PreferenceSwitch +import kotlinx.collections.immutable.ImmutableList + +/** + * The autofill picker: which site or app is being filled ([origin], as the user can check it), a + * search over the store's entries, the matching [rows] (null while the first search runs), and the + * switches for phishing-resistant search ([showStrict]: websites only), recording the pick as a + * match ([matchLabel]) and clearing earlier matches. [onPick] takes the entry's absolute path. + */ +@Composable +fun AutofillFilterScreen( + origin: String, + query: String, + rows: ImmutableList?, + showStrict: Boolean, + strict: Boolean, + matchLabel: String, + match: Boolean, + clear: Boolean, + onQueryChange: (String) -> Unit, + onStrictChange: (Boolean) -> Unit, + onMatchChange: (Boolean) -> Unit, + onClearChange: (Boolean) -> Unit, + onPick: (path: String) -> Unit, + modifier: Modifier = Modifier, +) { + val fillInto = stringResource(R.string.oreo_autofill_select_and_fill_into) + Surface(modifier.fillMaxSize()) { + Column(Modifier.padding(vertical = SpacingLarge)) { + Image( + painterResource(R.drawable.ic_launcher_foreground), + contentDescription = stringResource(R.string.app_name), + modifier = Modifier.size(60.dp).align(Alignment.CenterHorizontally), + ) + Text( + buildAnnotatedString { + append(fillInto) + append("\n") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append(origin) } + }, + modifier = Modifier.fillMaxWidth().padding(horizontal = SpacingLarge), + textAlign = TextAlign.Center, + ) + OutlinedTextField( + value = query, + onValueChange = onQueryChange, + modifier = Modifier.fillMaxWidth().padding(SpacingLarge), + label = { Text(stringResource(R.string.action_search)) }, + leadingIcon = { + Icon(painterResource(R.drawable.ic_search_24dp), contentDescription = null) + }, + trailingIcon = + if (query.isNotEmpty()) { + { + IconButton(onClick = { onQueryChange("") }) { + Icon(Icons.Filled.Close, stringResource(R.string.list_clear_search)) + } + } + } else null, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + ) + Box(Modifier.weight(1f).fillMaxWidth()) { + when { + rows == null -> Unit + rows.isEmpty() -> + Text( + stringResource(R.string.oreo_autofill_filter_no_results), + modifier = Modifier.padding(horizontal = SpacingLarge), + style = MaterialTheme.typography.titleMedium, + ) + else -> + // One lazy item per row: a cleared search lists the whole store. The surface wraps + // a short list and is capped by the box for a long one. + GroupedSurface(Modifier.fillMaxWidth()) { + LazyColumn { + items(rows, key = { it.path }) { row -> + EntryRow( + name = row.label, + subtitle = row.subtitle, + onClick = { onPick(row.path) }, + ) + } + } + } + } + } + if (showStrict) + PreferenceSwitch( + stringResource(R.string.oreo_autofill_strict_domain_search), + strict, + onStrictChange, + ) + PreferenceSwitch(matchLabel, match, onMatchChange) + PreferenceSwitch( + stringResource(R.string.oreo_autofill_matches_clear_existing), + clear, + onClearChange, + ) + } + } +} diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt index b027657e..44994646 100644 --- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt +++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt @@ -10,36 +10,35 @@ import android.content.Intent import android.content.IntentSender import android.os.Build import android.os.Bundle -import android.view.View import android.view.autofill.AutofillManager -import android.widget.TextView +import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.core.text.bold -import androidx.core.text.buildSpannedString -import androidx.core.text.underline -import androidx.core.widget.addTextChangedListener -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.LinearLayoutManager +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.passwordstore.R -import app.passwordstore.data.password.PasswordItem -import app.passwordstore.databinding.ActivityOreoAutofillFilterBinding +import app.passwordstore.data.repo.PasswordRepository +import app.passwordstore.ui.compose.theme.APSTheme +import app.passwordstore.ui.list.ListRow import app.passwordstore.util.autofill.AutofillMatcher import app.passwordstore.util.autofill.AutofillPreferences import app.passwordstore.util.coroutines.DispatcherProvider -import app.passwordstore.util.extensions.viewBinding -import app.passwordstore.util.settings.DirectoryStructure import app.passwordstore.util.viewmodel.FilterMode import app.passwordstore.util.viewmodel.ListMode import app.passwordstore.util.viewmodel.SearchMode -import app.passwordstore.util.viewmodel.SearchableRepositoryAdapter import app.passwordstore.util.viewmodel.SearchableRepositoryViewModel import com.github.androidpasswordstore.autofillparser.FormOrigin import dagger.hilt.android.AndroidEntryPoint +import java.io.File import javax.inject.Inject -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.launch +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import logcat.LogPriority.ERROR import logcat.logcat @@ -85,11 +84,12 @@ class AutofillFilterView : AppCompatActivity() { } private lateinit var formOrigin: FormOrigin - private lateinit var directoryStructure: DirectoryStructure - private val binding by viewBinding(ActivityOreoAutofillFilterBinding::inflate) private val model: SearchableRepositoryViewModel by viewModels() + /** Set by the first tap on a row: a double tap must not start a second decryption. */ + private var picked = false + private val decryptAction = registerForActivityResult(StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { @@ -100,7 +100,6 @@ class AutofillFilterView : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(binding.root) setFinishOnTouchOutside(true) val params = window.attributes @@ -129,111 +128,76 @@ class AutofillFilterView : AppCompatActivity() { return } } - directoryStructure = AutofillPreferences.directoryStructure(this) - supportActionBar?.hide() - bindUI() - updateSearch() setResult(RESULT_CANCELED) - } - private fun bindUI() { - with(binding) { - rvPassword.apply { - adapter = - SearchableRepositoryAdapter( - R.layout.oreo_autofill_filter_row, - ::PasswordViewHolder, - lifecycleScope, - dispatcherProvider, - ) { item, _ -> - val file = item.file.relativeTo(item.rootDir) - val pathToIdentifier = directoryStructure.getPathToIdentifierFor(file) - val identifier = directoryStructure.getIdentifierFor(file) - val accountPart = directoryStructure.getAccountPartFor(file) - check(identifier != null || accountPart != null) { - "At least one of identifier and accountPart should always be non-null" - } - title.text = - if (identifier != null) { - buildSpannedString { - if (pathToIdentifier != null) append("$pathToIdentifier/") - bold { underline { append(identifier) } } - } - } else { - accountPart - } - subtitle.apply { - if (identifier != null && accountPart != null) { - text = accountPart - visibility = View.VISIBLE - } else { - visibility = View.GONE - } - } - } - .onItemClicked { _, item -> decryptAndFill(item) } - layoutManager = LinearLayoutManager(context) - } - search.apply { - val initialSearch = formOrigin.getPrettyIdentifier(applicationContext, untrusted = false) - setText(initialSearch, TextView.BufferType.EDITABLE) - addTextChangedListener { updateSearch() } - } - origin.text = buildSpannedString { - append(getString(R.string.oreo_autofill_select_and_fill_into)) - append("\n") - bold { append(formOrigin.getPrettyIdentifier(applicationContext, untrusted = true)) } - } - strictDomainSearch.apply { - visibility = if (formOrigin is FormOrigin.Web) View.VISIBLE else View.GONE - isChecked = formOrigin is FormOrigin.Web - setOnCheckedChangeListener { _, _ -> updateSearch() } - } - shouldMatch.text = - getString( - R.string.oreo_autofill_match_with, - formOrigin.getPrettyIdentifier(applicationContext), + val isWeb = formOrigin is FormOrigin.Web + val initialQuery = formOrigin.getPrettyIdentifier(applicationContext, untrusted = false) + // Before anything is composed, so the ViewModel's default root listing (folders included) is + // never shown. The LaunchedEffect below repeats it with equal arguments, which emits nothing. + search(initialQuery, isWeb) + val root = PasswordRepository.getRepositoryDirectory() + val directoryStructure = AutofillPreferences.directoryStructure(this) + // ponytail: cold flow, so each return to STARTED walks the store again; fine for a picker open + // a few seconds. Share it with stateIn in the ViewModel if a large store makes it slow. + val rows = + model.searchResult + .map { result -> + autofillRows(result.passwordItems.map { it.file }, root, directoryStructure) + } + .flowOn(dispatcherProvider.io()) + val origin = formOrigin.getPrettyIdentifier(applicationContext, untrusted = true) + val matchLabel = + getString( + R.string.oreo_autofill_match_with, + formOrigin.getPrettyIdentifier(applicationContext), + ) + + setContent { + APSTheme { + var query by rememberSaveable { mutableStateOf(initialQuery) } + var strict by rememberSaveable { mutableStateOf(isWeb) } + var match by rememberSaveable { mutableStateOf(true) } + var clear by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(query, strict) { search(query, strict) } + val shown by rows.collectAsStateWithLifecycle?>(initialValue = null) + AutofillFilterScreen( + origin = origin, + query = query, + rows = shown, + showStrict = isWeb, + strict = strict, + matchLabel = matchLabel, + match = match, + clear = clear, + onQueryChange = { query = it }, + onStrictChange = { strict = it }, + onMatchChange = { match = it }, + onClearChange = { clear = it }, + onPick = { path -> decryptAndFill(File(path), match, clear) }, ) - lifecycleScope.launch { handleSearchResults() } - } - } - - private suspend fun handleSearchResults() { - model.searchResult.collect { result -> - val list = result.passwordItems - (binding.rvPassword.adapter as SearchableRepositoryAdapter).submitList(list) { - binding.rvPassword.scrollToPosition(0) - } - // Switch RecyclerView out for a "no results" message if the new list is empty and - // the message is not yet shown (and vice versa). - if ( - (list.isEmpty() && binding.rvPasswordSwitcher.nextView.id == binding.rvPasswordEmpty.id) || - (list.isNotEmpty() && binding.rvPasswordSwitcher.nextView.id == binding.rvPassword.id) - ) { - binding.rvPasswordSwitcher.showNext() } } } - private fun updateSearch() { + /** The old picker's search: fuzzy or strict domain, recursive, entries only. */ + private fun search(query: String, strict: Boolean) { model.search( - binding.search.text.toString().trim(), - filterMode = - if (binding.strictDomainSearch.isChecked) FilterMode.StrictDomain else FilterMode.Fuzzy, + query.trim(), + filterMode = if (strict) FilterMode.StrictDomain else FilterMode.Fuzzy, searchMode = SearchMode.RecursivelyInSubdirectories, listMode = ListMode.FilesOnly, ) } - private fun decryptAndFill(item: PasswordItem) { - if (binding.shouldClear.isChecked) - AutofillMatcher.clearMatchesFor(applicationContext, formOrigin) - if (binding.shouldMatch.isChecked) - AutofillMatcher.addMatchFor(applicationContext, formOrigin, item.file) + private fun decryptAndFill(file: File, match: Boolean, clear: Boolean) { + if (picked) return + picked = true + if (clear) AutofillMatcher.clearMatchesFor(applicationContext, formOrigin) + if (match) AutofillMatcher.addMatchFor(applicationContext, formOrigin, file) // intent?.extras? is checked to be non-null in onCreate decryptAction.launch( - AutofillDecryptActivity.makeDecryptFileIntent(item.file, intent!!.extras!!, this) + AutofillDecryptActivity.makeDecryptFileIntent(file, intent!!.extras!!, this) ) } } diff --git a/app/src/main/java/app/passwordstore/ui/autofill/PasswordViewHolder.kt b/app/src/main/java/app/passwordstore/ui/autofill/PasswordViewHolder.kt deleted file mode 100644 index 24a28564..00000000 --- a/app/src/main/java/app/passwordstore/ui/autofill/PasswordViewHolder.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package app.passwordstore.ui.autofill - -import android.view.View -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import app.passwordstore.R - -class PasswordViewHolder(view: View) : RecyclerView.ViewHolder(view) { - - val title: TextView = itemView.findViewById(R.id.title) - val subtitle: TextView = itemView.findViewById(R.id.subtitle) -} diff --git a/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt b/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt index f8cbf1e4..25ea2cf7 100644 --- a/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt +++ b/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt @@ -7,20 +7,8 @@ package app.passwordstore.util.viewmodel import android.app.Application import android.content.SharedPreferences import android.os.Parcelable -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup import androidx.annotation.VisibleForTesting import androidx.lifecycle.AndroidViewModel -import androidx.recyclerview.selection.ItemDetailsLookup -import androidx.recyclerview.selection.ItemKeyProvider -import androidx.recyclerview.selection.Selection -import androidx.recyclerview.selection.SelectionPredicates -import androidx.recyclerview.selection.SelectionTracker -import androidx.recyclerview.selection.StorageStrategy -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView import app.passwordstore.data.password.PasswordItem import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.injection.prefs.SettingsPreferences @@ -35,9 +23,7 @@ import com.github.androidpasswordstore.sublimefuzzy.Fuzzy import dagger.hilt.android.lifecycle.HiltViewModel import java.io.File import java.text.Collator -import java.util.Locale import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -51,9 +37,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import kotlinx.coroutines.yield -import me.zhanghai.android.fastscroll.PopupTextProvider private fun File.toPasswordItem() = if (isFile) PasswordItem.newPassword(name, this, PasswordRepository.getRepositoryDirectory()) @@ -399,114 +383,3 @@ constructor( } } } - -private object PasswordItemDiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: PasswordItem, newItem: PasswordItem) = - oldItem.file.absolutePath == newItem.file.absolutePath - - override fun areContentsTheSame(oldItem: PasswordItem, newItem: PasswordItem) = oldItem == newItem -} - -open class SearchableRepositoryAdapter( - private val layoutRes: Int, - private val viewHolderCreator: (view: View) -> T, - private val coroutineScope: CoroutineScope, - private val dispatcherProvider: DispatcherProvider, - private val viewHolderBinder: - suspend T.(item: PasswordItem, dispatcherProvider: DispatcherProvider) -> Unit, -) : ListAdapter(PasswordItemDiffCallback), PopupTextProvider { - - fun > makeSelectable( - recyclerView: RecyclerView, - itemDetailsLookupCreator: (recyclerView: RecyclerView) -> T, - ) { - selectionTracker = - SelectionTracker.Builder( - "SearchableRepositoryAdapter", - recyclerView, - itemKeyProvider, - itemDetailsLookupCreator(recyclerView), - StorageStrategy.createStringStorage(), - ) - .withSelectionPredicate(SelectionPredicates.createSelectAnything()) - .build() - .apply { - addObserver( - object : SelectionTracker.SelectionObserver() { - override fun onSelectionChanged() { - this@SearchableRepositoryAdapter.onSelectionChangedListener?.invoke( - requireSelectionTracker().selection - ) - } - } - ) - } - } - - private var onItemClickedListener: ((holder: T, item: PasswordItem) -> Unit)? = null - - open fun onItemClicked( - listener: (holder: T, item: PasswordItem) -> Unit - ): SearchableRepositoryAdapter { - check(onItemClickedListener == null) { - "Only a single listener can be registered for onItemClicked" - } - onItemClickedListener = listener - return this - } - - private var onSelectionChangedListener: ((selection: Selection) -> Unit)? = null - - open fun onSelectionChanged( - listener: (selection: Selection) -> Unit - ): SearchableRepositoryAdapter { - check(onSelectionChangedListener == null) { - "Only a single listener can be registered for onSelectionChanged" - } - onSelectionChangedListener = listener - return this - } - - private val itemKeyProvider = - object : ItemKeyProvider(SCOPE_MAPPED) { - override fun getKey(position: Int) = getItem(position).stableId - - override fun getPosition(key: String) = - (0 until itemCount).firstOrNull { getItem(it).stableId == key } ?: RecyclerView.NO_POSITION - } - - private var selectionTracker: SelectionTracker? = null - - fun requireSelectionTracker() = selectionTracker!! - - private val selectedFiles - get() = requireSelectionTracker().selection.map { File(it) } - - fun getSelectedItems() = selectedFiles.map { it.toPasswordItem() } - - fun getPositionForFile(file: File) = itemKeyProvider.getPosition(file.absolutePath) - - final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): T { - val view = LayoutInflater.from(parent.context).inflate(layoutRes, parent, false) - return viewHolderCreator(view) - } - - final override fun onBindViewHolder(holder: T, position: Int) { - val item = getItem(position) - holder.apply { - coroutineScope.launch(dispatcherProvider.mainImmediate()) { - viewHolderBinder.invoke(holder, item, dispatcherProvider) - } - selectionTracker?.let { itemView.isSelected = it.isSelected(item.stableId) } - itemView.setOnClickListener { - // Do not emit custom click events while the user is selecting items. - if (selectionTracker?.hasSelection() != true) onItemClickedListener?.invoke(holder, item) - } - } - } - - final override fun getPopupText(view: View, position: Int): String { - return getItem(position).name[0].toString().uppercase(Locale.getDefault()) - } -} diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_right_24dp.xml b/app/src/main/res/drawable/ic_keyboard_arrow_right_24dp.xml deleted file mode 100644 index 6861aa87..00000000 --- a/app/src/main/res/drawable/ic_keyboard_arrow_right_24dp.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/password_row_background.xml b/app/src/main/res/drawable/password_row_background.xml deleted file mode 100644 index 0f681cd8..00000000 --- a/app/src/main/res/drawable/password_row_background.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_oreo_autofill_filter.xml b/app/src/main/res/layout/activity_oreo_autofill_filter.xml deleted file mode 100644 index 0641f5c6..00000000 --- a/app/src/main/res/layout/activity_oreo_autofill_filter.xml +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/oreo_autofill_filter_row.xml b/app/src/main/res/layout/oreo_autofill_filter_row.xml deleted file mode 100644 index b4284966..00000000 --- a/app/src/main/res/layout/oreo_autofill_filter_row.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/values-night/colors_material3.xml b/app/src/main/res/values-night/colors_material3.xml index 22e4e8f0..c26d7ec6 100644 --- a/app/src/main/res/values-night/colors_material3.xml +++ b/app/src/main/res/values-night/colors_material3.xml @@ -4,5 +4,4 @@ --> - #66eeeeee diff --git a/app/src/main/res/values/colors_material3.xml b/app/src/main/res/values/colors_material3.xml index 3a7341b5..ecd34921 100644 --- a/app/src/main/res/values/colors_material3.xml +++ b/app/src/main/res/values/colors_material3.xml @@ -60,5 +60,4 @@ #BA1B1B - #668eacbb -- cgit v1.2.3 From 2dab5815f769e878f4c88e0304ccf2699b86ede9 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 4 Nov 2026 11:25:02 +0100 Subject: feat(app): move the publisher warning to Compose AutofillPublisherChangedActivity draws the warning with the shared theme. Keep Autofill disabled is now the primary button, and the Possible phishing attempt title replaces the logo. Re-enabling still takes two taps (Advanced information first) and still clears the app's matches before returning the prepared fill response; every other way out fills nothing. Touches that reach the warning through another app's window are now dropped (filterTouchesWhenObscured on the decor view), so an overlay cannot trick the user into re-enabling. Co-Authored-By: Claude Opus 5.5 --- app/lint-baseline.xml | 22 --- .../autofill/AutofillPublisherChangedActivity.kt | 115 +++++++------- .../ui/autofill/PublisherChangedScreen.kt | 121 ++++++++++++++ .../activity_oreo_autofill_publisher_changed.xml | 174 --------------------- app/src/main/res/values/dimens.xml | 1 - 5 files changed, 175 insertions(+), 258 deletions(-) create mode 100644 app/src/main/java/app/passwordstore/ui/autofill/PublisherChangedScreen.kt delete mode 100644 app/src/main/res/layout/activity_oreo_autofill_publisher_changed.xml (limited to 'app/src/main/res/layout') diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index cd7a1217..f77d22dd 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -98,28 +98,6 @@ file="src/main/res/mipmap-xxxhdpi/ic_launcher.png"/> - - - - - - - - + logcat(ERROR) { e.asLog("Failed to retrieve package info for $appPackage") } + finish() + return + } + val appName = + getString( + R.string.oreo_autofill_warning_publisher_app_name, + packageManager.getApplicationLabel(appInfo), + ) + val installed = + getString( + R.string.oreo_autofill_warning_publisher_install_time, + DateUtils.getRelativeTimeSpanString(packageInfo.firstInstallTime), + ) + val details = + getString(R.string.oreo_autofill_warning_publisher_advanced_info_template, appPackage, hash) + setContent { + APSTheme { + PublisherChangedScreen( + appName = appName, + installed = installed, + details = details, + onKeepDisabled = { finish() }, + onReset = { reset(appPackage) }, ) - finish() } } } - private fun showPackageInfo() { - runCatching { - with(binding) { - val packageInfo = - packageManager.getPackageInfoCompat(appPackage, PackageManager.GET_META_DATA) - val installTime = DateUtils.getRelativeTimeSpanString(packageInfo.firstInstallTime) - warningAppInstallDate.text = - getString(R.string.oreo_autofill_warning_publisher_install_time, installTime) - val appInfo = - packageManager.getApplicationInfoCompat(appPackage, PackageManager.GET_META_DATA) - warningAppName.text = - getString( - R.string.oreo_autofill_warning_publisher_app_name, - packageManager.getApplicationLabel(appInfo), - ) - - val currentHash = - computeCertificatesHash(this@AutofillPublisherChangedActivity, appPackage) - warningAppAdvancedInfo.text = - getString( - R.string.oreo_autofill_warning_publisher_advanced_info_template, - appPackage, - currentHash, - ) - } - } - .onFailure { e -> - logcat(ERROR) { e.asLog("Failed to retrieve package info for $appPackage") } - finish() - } + /** Forgets the app's matches and fills with the response the service prepared for this case. */ + private fun reset(appPackage: String) { + AutofillMatcher.clearMatchesFor(this, FormOrigin.App(appPackage)) + val fillResponse = + IntentCompat.getParcelableExtra( + intent, + EXTRA_FILL_RESPONSE_AFTER_RESET, + FillResponse::class.java, + ) + setResult( + RESULT_OK, + Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillResponse) }, + ) + finish() } } diff --git a/app/src/main/java/app/passwordstore/ui/autofill/PublisherChangedScreen.kt b/app/src/main/java/app/passwordstore/ui/autofill/PublisherChangedScreen.kt new file mode 100644 index 00000000..8c210b79 --- /dev/null +++ b/app/src/main/java/app/passwordstore/ui/autofill/PublisherChangedScreen.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2026 Danilo M. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.ui.autofill + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import app.passwordstore.R +import app.passwordstore.ui.compose.theme.SpacingLarge +import app.passwordstore.ui.compose.theme.SpacingLargest + +/** + * Warns that the app being filled ([appName], installed [installed]) is signed by another publisher + * than when an entry was first matched to it. [onKeepDisabled] fills nothing. [onReset] forgets the + * old matches and fills; it is offered only after "Advanced information", which shows [details] + * (package and certificate hash), so re-enabling always takes two deliberate taps. + */ +@Composable +fun PublisherChangedScreen( + appName: String, + installed: String, + details: String, + onKeepDisabled: () -> Unit, + onReset: () -> Unit, + modifier: Modifier = Modifier, +) { + var advanced by rememberSaveable { mutableStateOf(false) } + val center = Modifier.fillMaxWidth() + Surface(modifier) { + Column( + Modifier.verticalScroll(rememberScrollState()).padding(SpacingLargest), + verticalArrangement = Arrangement.spacedBy(SpacingLarge), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + painterResource(R.drawable.ic_warning_red_24dp), + contentDescription = + stringResource(R.string.oreo_autofill_warning_publisher_warning_sign_description), + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.error, + ) + // ponytail: stringResource drops the footer's in every locale but Italian (whose footer + // has none); the red title carries the emphasis. Keep spans with getText if it is missed. + Text( + stringResource(R.string.oreo_autofill_warning_publisher_dataset_title), + modifier = center, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge, + ) + Text( + stringResource(R.string.oreo_autofill_warning_publisher_header), + style = MaterialTheme.typography.bodyMedium, + ) + Text( + appName, + modifier = center, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium, + ) + Text( + installed, + modifier = center, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodySmall, + ) + Text( + stringResource(R.string.oreo_autofill_warning_publisher_footer), + style = MaterialTheme.typography.bodyMedium, + ) + Button(onClick = onKeepDisabled, modifier = center) { + Text( + stringResource(R.string.oreo_autofill_warning_publisher_changed_disable_autofill_button) + ) + } + if (!advanced) { + OutlinedButton(onClick = { advanced = true }, modifier = center) { + Text(stringResource(R.string.oreo_autofill_warning_publisher_advanced_info_button)) + } + } else { + OutlinedButton(onClick = onReset, modifier = center) { + Text(stringResource(R.string.oreo_autofill_warning_publisher_reenable_button)) + } + SelectionContainer { + Text( + details, + modifier = center, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), + ) + } + } + } + } +} diff --git a/app/src/main/res/layout/activity_oreo_autofill_publisher_changed.xml b/app/src/main/res/layout/activity_oreo_autofill_publisher_changed.xml deleted file mode 100644 index 89d64fe4..00000000 --- a/app/src/main/res/layout/activity_oreo_autofill_publisher_changed.xml +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 44d4f581..de2ac854 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -6,5 +6,4 @@ 16dp - 16dp -- cgit v1.2.3 From 1d564b91ecb1b52c585e21f10596e80bc69292d4 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 4 Nov 2026 11:28:40 +0100 Subject: feat(app): move the autofill decryption prompts to Compose AutofillDecryptActivity shows the no keys dialog and the entry view's passphrase dialog in Compose over its transparent FLAG_SECURE window. The flow starts once, on the first window focus, instead of on every onStart: Android 14 evicts a screen lock prompt shown before the activity is on top, and a restart showed a second passphrase dialog. After a key import it restarts on the next focus. Dismissing the no keys dialog now finishes instead of leaving an invisible window. Rotation and resizing no longer recreate the activity; other recreations cancel the fill. The passphrase is now cached only after a successful screen lock prompt, as the entry view does: a passphrase typed after a cancelled or unavailable prompt was cached whenever the cache feature was on. PasswordDialog, its layout, dimens.xml and requireKeysExist lose their last user and go. Co-Authored-By: Claude Opus 5.5 --- app/src/main/AndroidManifest.xml | 3 + .../ui/autofill/AutofillDecryptActivity.kt | 198 ++++++++++++++------- .../app/passwordstore/ui/autofill/AutofillModel.kt | 9 + .../app/passwordstore/ui/crypto/BasePGPActivity.kt | 44 ----- .../app/passwordstore/ui/crypto/PasswordDialog.kt | 105 ----------- .../app/passwordstore/ui/entry/EntryDestination.kt | 7 +- app/src/main/res/layout/dialog_password_entry.xml | 35 ---- app/src/main/res/values/dimens.xml | 9 - .../passwordstore/ui/autofill/AutofillModelTest.kt | 18 ++ 9 files changed, 166 insertions(+), 262 deletions(-) delete mode 100644 app/src/main/java/app/passwordstore/ui/crypto/PasswordDialog.kt delete mode 100644 app/src/main/res/layout/dialog_password_entry.xml delete mode 100644 app/src/main/res/values/dimens.xml (limited to 'app/src/main/res/layout') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bcddf4c3..15d6447e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -83,8 +83,11 @@ android:resource="@xml/oreo_autofill_service" /> + ?>(null) + + // The import's activity is still on top when its result arrives: the flow restarts on the next + // window focus, so the screen lock prompt is not shown (and evicted) behind it. + private val keyImport = + registerForActivityResult(StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) started = false else finish() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // ponytail: a recreation (theme or locale change; rotation and resizing do not recreate, see + // the manifest) cancels the fill instead of resuming a half-done flow; the user taps the field + // again. Keep the flow's state in a ViewModel if this ever matters. + if (savedInstanceState != null) { + finish() + return + } + filePath = intent?.getStringExtra(EXTRA_FILE_PATH) ?: run { logcat(ERROR) { "AutofillDecryptActivity started without EXTRA_FILE_PATH" } finish() return } - val clientState = + clientState = intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE) ?: run { logcat(ERROR) { "AutofillDecryptActivity started without EXTRA_CLIENT_STATE" } finish() return } - val isSearchAction = intent?.getBooleanExtra(EXTRA_SEARCH_ACTION, true)!! - val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match + val isSearchAction = intent.getBooleanExtra(EXTRA_SEARCH_ACTION, true) + action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match directoryStructure = AutofillPreferences.directoryStructure(this) logcat { action.toString() } - requireKeysExist { - if ( - features.isEnabled(EnablePGPPassphraseCache) && BiometricAuthenticator.canAuthenticate(this) - ) { - BiometricAuthenticator.authenticate( - this, - R.string.biometric_prompt_title_gpg_passphrase_cache, - ) { authResult -> - decrypt(filePath, clientState, action, authResult) - } - } else { - decrypt(filePath, clientState, action, BiometricResult.CanceledByUser) + setContent { + APSTheme { + if (noKeys) + AlertDialog( + onDismissRequest = { finish() }, + title = { Text(stringResource(R.string.no_keys_imported_dialog_title)) }, + text = { Text(stringResource(R.string.no_keys_imported_dialog_message)) }, + confirmButton = { + TextButton( + onClick = { + noKeys = false + keyImport.launch( + MainActivity.setupIntent(this@AutofillDecryptActivity, SetupStart.PgpImport) + ) + } + ) { + Text(stringResource(R.string.button_label_import)) + } + }, + ) + val identifiers = passphraseFor + if (identifiers != null) + PassphraseDialog( + wrong = false, + cacheEnabled = features.isEnabled(EnablePGPPassphraseCache), + clearCache = clearCache, + onSubmit = onSubmit@{ passphrase, clear -> + // Cleared before decrypting: a second submit in the same frame does nothing. + if (passphraseFor == null) return@onSubmit + passphraseFor = null + clearCache = clear + lifecycleScope.launch(dispatcherProvider.main()) { + decryptWithPassphrase(identifiers, passphrase) + } + }, + onCancel = { finish() }, + ) } } } - private fun decrypt( - filePath: String, - clientState: Bundle, - action: AutofillAction, - authResult: BiometricResult, - ) { + // The flow waits for window focus, as LaunchActivity's prompt does: Android 14's SystemUI + // dismisses a screen lock prompt shown before its activity is on top. It runs once (again only + // after a key import); the old onStart start ran it on every return to this activity. + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + if (!hasFocus || started || isFinishing) return + started = true + start() + } + + /** Asks for a key import first while no PGP key is imported. */ + private fun start() { + if (isFinishing) return + lifecycleScope.launch { if (repository.hasKeys()) unlock() else noKeys = true } + } + + private fun unlock() { + if ( + features.isEnabled(EnablePGPPassphraseCache) && BiometricAuthenticator.canAuthenticate(this) + ) { + BiometricAuthenticator.authenticate( + this, + R.string.biometric_prompt_title_gpg_passphrase_cache, + ) { authResult -> + decrypt(authResult) + } + } else { + decrypt(BiometricResult.CanceledByUser) + } + } + + private fun decrypt(authResult: BiometricResult) { + auth = authResult val gpgIdentifiers = getPGPIdentifiers( getParentPath(filePath, PasswordRepository.getRepositoryDirectory().toString()) @@ -112,8 +208,7 @@ class AutofillDecryptActivity : BasePGPActivity() { is BiometricResult.CanceledBySystem, is BiometricResult.CanceledByUser, is BiometricResult.Failure, - is BiometricResult.HardwareUnavailableOrDisabled -> - askPassphrase(filePath, gpgIdentifiers, clientState, action) + is BiometricResult.HardwareUnavailableOrDisabled -> askPassphrase(gpgIdentifiers) is BiometricResult.Success -> { /* clear passphrase cache on first use after application startup or if screen was off; also make sure to purge a stale cache after caching has been disabled via PGP settings */ @@ -128,56 +223,25 @@ class AutofillDecryptActivity : BasePGPActivity() { gpgIdentifiers.first(), ) if (cachedPassphrase != null) { - decryptWithPassphrase( - File(filePath), - gpgIdentifiers, - clientState, - action, - cachedPassphrase, - ) + decryptWithPassphrase(gpgIdentifiers, cachedPassphrase) } else { - askPassphrase(filePath, gpgIdentifiers, clientState, action) + askPassphrase(gpgIdentifiers) } } } } } - private suspend fun askPassphrase( - filePath: String, - identifiers: List, - clientState: Bundle, - action: AutofillAction, - ) { + private suspend fun askPassphrase(identifiers: List) { if (!repository.isPasswordProtected(identifiers)) { - decryptWithPassphrase(File(filePath), identifiers, clientState, action, password = "") + decryptWithPassphrase(identifiers, password = "") return } - val dialog = - PasswordDialog.newInstance( - cacheEnabled = features.isEnabled(EnablePGPPassphraseCache), - clearCache = clearCache, - ) - dialog.show(supportFragmentManager, "PASSWORD_DIALOG") - dialog.setFragmentResultListener(PasswordDialog.PASSWORD_RESULT_KEY) { key, bundle -> - if (key == PasswordDialog.PASSWORD_RESULT_KEY) { - val value = bundle.getString(PasswordDialog.PASSWORD_PHRASE_KEY)!! - clearCache = bundle.getBoolean(PasswordDialog.PASSWORD_CLEAR_KEY) - lifecycleScope.launch(dispatcherProvider.main()) { - decryptWithPassphrase(File(filePath), identifiers, clientState, action, value) - } - } - } + passphraseFor = identifiers } - private suspend fun decryptWithPassphrase( - filePath: File, - identifiers: List, - clientState: Bundle, - action: AutofillAction, - password: String, - ) { - val credentials = decryptCredential(filePath, password, identifiers) + private suspend fun decryptWithPassphrase(identifiers: List, password: String) { + val credentials = decryptCredential(File(filePath), password, identifiers) if (credentials == null) { setResult(RESULT_CANCELED) } else { @@ -219,7 +283,7 @@ class AutofillDecryptActivity : BasePGPActivity() { .onSuccess { result -> return runCatching { runCatching { - if (features.isEnabled(EnablePGPPassphraseCache)) { + if (cachesPassphrase(features.isEnabled(EnablePGPPassphraseCache), auth)) { passphraseCache.cachePassphrase(this, identifiers.first(), password) settings.edit { putBoolean(PreferenceKeys.CLEAR_PASSPHRASE_CACHE, clearCache) diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillModel.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillModel.kt index ac2d0948..9f436ea2 100644 --- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillModel.kt +++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillModel.kt @@ -6,6 +6,7 @@ package app.passwordstore.ui.autofill import app.passwordstore.ui.list.ListRow +import app.passwordstore.util.auth.BiometricAuthenticator.Result as BiometricResult import app.passwordstore.util.settings.DirectoryStructure import java.io.File import kotlinx.collections.immutable.ImmutableList @@ -36,3 +37,11 @@ fun autofillRows( ListRow(file.absolutePath, identifier ?: account ?: file.nameWithoutExtension, subtitle, 0) } .toImmutableList() + +/** + * Whether autofill may cache the passphrase it just decrypted with: only with the cache feature on + * ([cacheEnabled]) and after a successful screen lock prompt ([auth]), as the entry view does. A + * passphrase typed after a cancelled, failed or unavailable prompt is never kept. + */ +fun cachesPassphrase(cacheEnabled: Boolean, auth: BiometricResult): Boolean = + cacheEnabled && auth is BiometricResult.Success 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 6831761b..e40585d5 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt @@ -12,19 +12,15 @@ import android.os.Build import android.os.Bundle import android.os.PersistableBundle import android.view.WindowManager -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.annotation.CallSuper import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.lifecycleScope import app.passwordstore.R import app.passwordstore.crypto.PGPIdentifier import app.passwordstore.data.crypto.CryptoRepository import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.injection.prefs.SettingsPreferences -import app.passwordstore.ui.main.MainActivity -import app.passwordstore.ui.setup.SetupStart import app.passwordstore.util.coroutines.DispatcherProvider import app.passwordstore.util.extensions.clipboard import app.passwordstore.util.extensions.getString @@ -33,29 +29,14 @@ import app.passwordstore.util.extensions.snackbar import app.passwordstore.util.services.ClipboardService import app.passwordstore.util.settings.Constants import app.passwordstore.util.settings.PreferenceKeys -import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import java.io.File import javax.inject.Inject -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext @Suppress("Registered") @AndroidEntryPoint open class BasePGPActivity : AppCompatActivity() { - /** Action to invoke if [keyImportAction] succeeds. */ - private var onKeyImport: (() -> Unit)? = null - private val keyImportAction = - registerForActivityResult(StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - onKeyImport?.invoke() - onKeyImport = null - } else { - finish() - } - } - /** [SharedPreferences] instance used by subclasses to persist settings */ @SettingsPreferences @Inject lateinit var settings: SharedPreferences @Inject lateinit var repository: CryptoRepository @@ -71,31 +52,6 @@ open class BasePGPActivity : AppCompatActivity() { window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) } - /** - * Function to execute [onKeysExist] only if there are PGP keys imported in the app's key manager. - */ - fun requireKeysExist(onKeysExist: () -> Unit) { - lifecycleScope.launch { - val hasKeys = repository.hasKeys() - if (!hasKeys) { - withContext(dispatcherProvider.main()) { - MaterialAlertDialogBuilder(this@BasePGPActivity) - .setTitle(resources.getString(R.string.no_keys_imported_dialog_title)) - .setMessage(resources.getString(R.string.no_keys_imported_dialog_message)) - .setPositiveButton(resources.getString(R.string.button_label_import)) { _, _ -> - onKeyImport = onKeysExist - keyImportAction.launch( - MainActivity.setupIntent(this@BasePGPActivity, SetupStart.PgpImport) - ) - } - .show() - } - } else { - onKeysExist() - } - } - } - companion object { const val EXTRA_FILE_PATH = "FILE_PATH" diff --git a/app/src/main/java/app/passwordstore/ui/crypto/PasswordDialog.kt b/app/src/main/java/app/passwordstore/ui/crypto/PasswordDialog.kt deleted file mode 100644 index d845d689..00000000 --- a/app/src/main/java/app/passwordstore/ui/crypto/PasswordDialog.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 android.app.Dialog -import android.content.DialogInterface -import android.os.Bundle -import android.view.KeyEvent -import android.view.WindowManager -import androidx.core.os.bundleOf -import androidx.core.view.isVisible -import androidx.core.widget.doOnTextChanged -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.setFragmentResult -import app.passwordstore.R -import app.passwordstore.databinding.DialogPasswordEntryBinding -import app.passwordstore.util.extensions.finish -import app.passwordstore.util.extensions.unsafeLazy -import com.google.android.material.dialog.MaterialAlertDialogBuilder - -/** [DialogFragment] to request a password from the user and forward it along. */ -class PasswordDialog : DialogFragment() { - - private val binding by unsafeLazy { DialogPasswordEntryBinding.inflate(layoutInflater) } - private var isError: Boolean = false - private var clearCacheChecked: Boolean = true - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val builder = MaterialAlertDialogBuilder(requireContext()) - builder.setView(binding.root) - builder.setTitle(R.string.password) - - val cacheEnabled = requireArguments().getBoolean(CACHE_ENABLED_EXTRA, false) - binding.autoClearCache.isVisible = cacheEnabled - - if (cacheEnabled) { - clearCacheChecked = requireArguments().getBoolean(AUTO_CLEAR_CACHE_EXTRA) - binding.autoClearCache.isChecked = clearCacheChecked - binding.autoClearCache.setOnCheckedChangeListener { _, isChecked -> - clearCacheChecked = isChecked - } - } - - builder.setPositiveButton(android.R.string.ok) { _, _ -> setPasswordAndDismiss() } - val dialog = builder.create() - dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) - dialog.setOnShowListener { - if (isError) { - binding.passwordField.error = getString(R.string.git_operation_wrong_password) - } - binding.passwordEditText.doOnTextChanged { _, _, _, _ -> binding.passwordField.error = null } - binding.passwordEditText.setOnKeyListener { _, keyCode, _ -> - if (keyCode == KeyEvent.KEYCODE_ENTER) { - setPasswordAndDismiss() - return@setOnKeyListener true - } - false - } - } - dialog.window?.setFlags( - WindowManager.LayoutParams.FLAG_SECURE, - WindowManager.LayoutParams.FLAG_SECURE, - ) - return dialog - } - - fun setError() { - isError = true - } - - override fun onCancel(dialog: DialogInterface) { - super.onCancel(dialog) - finish() - } - - private fun setPasswordAndDismiss() { - val password = binding.passwordEditText.text.toString() - setFragmentResult( - PASSWORD_RESULT_KEY, - bundleOf(PASSWORD_PHRASE_KEY to password, PASSWORD_CLEAR_KEY to clearCacheChecked), - ) - dismissAllowingStateLoss() - } - - companion object { - - private const val CACHE_ENABLED_EXTRA = "CACHE_ENABLED" - private const val AUTO_CLEAR_CACHE_EXTRA = "AUTO_CLEAR_CACHE" - - const val PASSWORD_RESULT_KEY = "password_result" - const val PASSWORD_PHRASE_KEY = "password_phrase" - const val PASSWORD_CLEAR_KEY = "password_clear" - - fun newInstance(cacheEnabled: Boolean, clearCache: Boolean): PasswordDialog { - val extras = - bundleOf(CACHE_ENABLED_EXTRA to cacheEnabled, AUTO_CLEAR_CACHE_EXTRA to clearCache) - val fragment = PasswordDialog() - fragment.arguments = extras - return fragment - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt b/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt index 637c9b56..e5341320 100644 --- a/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt +++ b/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt @@ -173,9 +173,12 @@ fun EntryDestination( } } -/** Asks for the PGP passphrase, as the old `PasswordDialog` did. */ +/** + * Asks for the PGP passphrase (entry view and autofill). The passphrase lives only in `remember` + * and the dialog's window is FLAG_SECURE. + */ @Composable -private fun PassphraseDialog( +internal fun PassphraseDialog( wrong: Boolean, cacheEnabled: Boolean, clearCache: Boolean, diff --git a/app/src/main/res/layout/dialog_password_entry.xml b/app/src/main/res/layout/dialog_password_entry.xml deleted file mode 100644 index bbfbc506..00000000 --- a/app/src/main/res/layout/dialog_password_entry.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml deleted file mode 100644 index de2ac854..00000000 --- a/app/src/main/res/values/dimens.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - 16dp - diff --git a/app/src/test/java/app/passwordstore/ui/autofill/AutofillModelTest.kt b/app/src/test/java/app/passwordstore/ui/autofill/AutofillModelTest.kt index 8907aa54..39f7f358 100644 --- a/app/src/test/java/app/passwordstore/ui/autofill/AutofillModelTest.kt +++ b/app/src/test/java/app/passwordstore/ui/autofill/AutofillModelTest.kt @@ -5,10 +5,13 @@ package app.passwordstore.ui.autofill +import app.passwordstore.util.auth.BiometricAuthenticator.Result as BiometricResult import app.passwordstore.util.settings.DirectoryStructure import java.io.File import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class AutofillModelTest { @@ -70,4 +73,19 @@ class AutofillModelTest { rows(DirectoryStructure.FileBased, "b.org.gpg", "a.org.gpg").map { it.second }, ) } + + @Test + fun passphraseIsCachedOnlyAfterTheScreenLock() { + assertTrue(cachesPassphrase(true, BiometricResult.Success(null))) + val notConfirmed = + listOf( + BiometricResult.CanceledByUser, + BiometricResult.CanceledBySystem, + BiometricResult.HardwareUnavailableOrDisabled, + BiometricResult.Failure(null, "failed"), + BiometricResult.Retry, + ) + for (auth in notConfirmed) assertFalse(cachesPassphrase(true, auth), auth.toString()) + assertFalse(cachesPassphrase(false, BiometricResult.Success(null))) + } } -- cgit v1.2.3 From 4e771dd5e04bf40abfd3db16801865a5d1f953a2 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 4 Nov 2026 17:40:54 +0100 Subject: build(app): drop the View-era dependencies and view binding Nothing uses preference, recyclerview, recyclerview-selection, fastscroll or modernandroidpreferences since the Compose screens replaced the Views, nor view binding. fragment-ktx stays as the pin that keeps androidx.fragment at 1.8.4. The preference widget override, the multiselect attribute, the toolbar and bottom sheet styles and the lint ignores for deleted layouts go. Lint checks unused ids again. Co-Authored-By: Claude Opus 5.5 --- app/build.gradle.kts | 6 +----- app/lint.xml | 5 ----- app/src/main/res/layout/map_preference_widget_switch.xml | 13 ------------- app/src/main/res/values/attrs.xml | 10 ---------- app/src/main/res/values/styles_material3.xml | 12 ------------ app/src/main/res/values/themes_material3.xml | 8 -------- .../kotlin/app/passwordstore/gradle/ApplicationPlugin.kt | 5 +---- .../src/main/kotlin/app/passwordstore/gradle/LintConfig.kt | 4 ---- gradle/libs.versions.toml | 5 ----- 9 files changed, 2 insertions(+), 66 deletions(-) delete mode 100644 app/src/main/res/layout/map_preference_widget_switch.xml delete mode 100644 app/src/main/res/values/attrs.xml delete mode 100644 app/src/main/res/values/styles_material3.xml (limited to 'app/src/main/res/layout') diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5f7a8ee8..053d7478 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { implementation(libs.androidx.constraintlayout) implementation(libs.androidx.core.ktx) implementation(libs.androidx.documentfile) + // Pins androidx.fragment (FragmentActivity, BiometricPrompt): appcompat alone resolves 1.5.4. implementation(libs.androidx.fragment.ktx) implementation(libs.androidx.hilt.navigation.compose) implementation(libs.bundles.androidxLifecycle) @@ -70,9 +71,6 @@ dependencies { implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.material) implementation(libs.androidx.navigation.compose) - implementation(libs.androidx.preference) - implementation(libs.androidx.recyclerview) - implementation(libs.androidx.recyclerviewSelection) implementation(libs.compose.ui.tooling) implementation(libs.dagger.hilt.android) @@ -84,13 +82,11 @@ dependencies { implementation(libs.aps.sublimeFuzzy) implementation(libs.aps.zxingAndroidEmbedded) - implementation(libs.thirdparty.fastscroll) implementation(libs.thirdparty.jgit) { exclude(group = "org.apache.httpcomponents", module = "httpclient") } implementation(libs.thirdparty.kotlinResult) implementation(libs.thirdparty.logcat) - implementation(libs.thirdparty.modernAndroidPrefs) implementation(libs.thirdparty.leakcanary.plumber) implementation(libs.thirdparty.sshj) implementation(libs.thirdparty.bouncycastle.bcprov) diff --git a/app/lint.xml b/app/lint.xml index 8834cbd7..44b84e59 100644 --- a/app/lint.xml +++ b/app/lint.xml @@ -5,7 +5,6 @@ - @@ -20,10 +19,6 @@ - - - - diff --git a/app/src/main/res/layout/map_preference_widget_switch.xml b/app/src/main/res/layout/map_preference_widget_switch.xml deleted file mode 100644 index 566e72d4..00000000 --- a/app/src/main/res/layout/map_preference_widget_switch.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml deleted file mode 100644 index a1d3c974..00000000 --- a/app/src/main/res/values/attrs.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/values/styles_material3.xml b/app/src/main/res/values/styles_material3.xml deleted file mode 100644 index 329905e1..00000000 --- a/app/src/main/res/values/styles_material3.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/values/themes_material3.xml b/app/src/main/res/values/themes_material3.xml index 0ce3796f..0ec74a45 100644 --- a/app/src/main/res/values/themes_material3.xml +++ b/app/src/main/res/values/themes_material3.xml @@ -10,8 +10,6 @@ @bool/light_status_bar ?android:colorBackground ?android:colorBackground - @style/APSThemeM3.Toolbar - @style/APSThemeM3.BottomSheetDialog @@ -43,12 +41,6 @@ true - -