From 104c07f0bcc37d87ddc371d06827ff93ee3fe504 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 4 Nov 2026 17:37:46 +0100 Subject: refactor(app): fold BasePGPActivity into the autofill decryption AutofillDecryptActivity was its last subclass: it now sets FLAG_SECURE and injects the settings, crypto repository and dispatchers itself. The path and clipboard helpers move to CryptoExtensions.kt as top-level functions (a test pins the path helpers), and the FILE_PATH extra key moves, unchanged, to LaunchActivity. An invalid or short .gpg-id ID is reported with a toast: the snackbar never showed in the autofill decryption, which closes its window at once. Co-Authored-By: Claude Opus 5.5 --- .../passwordstore/data/password/PasswordItem.kt | 6 +- .../ui/autofill/AutofillDecryptActivity.kt | 16 +- .../app/passwordstore/ui/crypto/BasePGPActivity.kt | 181 --------------------- .../passwordstore/ui/crypto/CryptoExtensions.kt | 149 +++++++++++++++++ .../app/passwordstore/ui/entry/EntryDestination.kt | 2 +- .../java/app/passwordstore/ui/list/StoreActions.kt | 2 +- .../app/passwordstore/ui/main/LaunchActivity.kt | 8 +- .../java/app/passwordstore/ui/main/MainActivity.kt | 3 +- .../ui/crypto/CryptoExtensionsTest.kt | 27 +++ 9 files changed, 201 insertions(+), 193 deletions(-) delete mode 100644 app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt create mode 100644 app/src/main/java/app/passwordstore/ui/crypto/CryptoExtensions.kt create mode 100644 app/src/test/java/app/passwordstore/ui/crypto/CryptoExtensionsTest.kt (limited to 'app/src') diff --git a/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt b/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt index 999ad050..5d32108f 100644 --- a/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt +++ b/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt @@ -6,7 +6,7 @@ package app.passwordstore.data.password import android.content.Context import android.content.Intent -import app.passwordstore.ui.crypto.BasePGPActivity +import app.passwordstore.ui.crypto.getLongName import app.passwordstore.ui.main.LaunchActivity import java.io.File @@ -15,7 +15,7 @@ data class PasswordItem(val name: String, val type: Char, val file: File, val ro val fullPathToParent = file.absolutePath.replace(rootDir.absolutePath, "").replace(file.name, "") - val longName = BasePGPActivity.getLongName(fullPathToParent, rootDir.absolutePath, toString()) + val longName = getLongName(fullPathToParent, rootDir.absolutePath, toString()) override fun equals(other: Any?): Boolean { return (other is PasswordItem) && (other.file == file) @@ -36,7 +36,7 @@ data class PasswordItem(val name: String, val type: Char, val file: File, val ro /** Creates an [Intent] to launch this [PasswordItem] through the authentication process. */ fun createAuthEnabledIntent(context: Context): Intent { val intent = Intent(context, LaunchActivity::class.java) - intent.putExtra("FILE_PATH", file.absolutePath) + intent.putExtra(LaunchActivity.EXTRA_FILE_PATH, file.absolutePath) intent.action = LaunchActivity.ACTION_DECRYPT_PASS return intent } diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt index 39bf8cff..3f0fb5ef 100644 --- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt @@ -8,11 +8,14 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.IntentSender +import android.content.SharedPreferences import android.os.Build import android.os.Bundle +import android.view.WindowManager import android.view.autofill.AutofillManager import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import androidx.appcompat.app.AppCompatActivity import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -25,12 +28,14 @@ import androidx.lifecycle.lifecycleScope import app.passwordstore.Application.Companion.screenWasOff import app.passwordstore.R import app.passwordstore.crypto.PGPIdentifier +import app.passwordstore.data.crypto.CryptoRepository import app.passwordstore.data.crypto.PGPPassphraseCache import app.passwordstore.data.passfile.PasswordEntry import app.passwordstore.data.repo.PasswordRepository +import app.passwordstore.injection.prefs.SettingsPreferences import app.passwordstore.ui.compose.theme.APSTheme -import app.passwordstore.ui.crypto.BasePGPActivity import app.passwordstore.ui.crypto.getPGPIdentifiers +import app.passwordstore.ui.crypto.getParentPath import app.passwordstore.ui.entry.PassphraseDialog import app.passwordstore.ui.main.MainActivity import app.passwordstore.ui.setup.SetupStart @@ -38,6 +43,7 @@ import app.passwordstore.util.auth.BiometricAuthenticator import app.passwordstore.util.auth.BiometricAuthenticator.Result as BiometricResult import app.passwordstore.util.autofill.AutofillPreferences import app.passwordstore.util.autofill.AutofillResponseBuilder +import app.passwordstore.util.coroutines.DispatcherProvider import app.passwordstore.util.extensions.asLog import app.passwordstore.util.features.Feature.EnablePGPPassphraseCache import app.passwordstore.util.features.Features @@ -64,8 +70,11 @@ import logcat.logcat * FLAG_SECURE window shows only the "no keys" and passphrase dialogs. */ @AndroidEntryPoint -class AutofillDecryptActivity : BasePGPActivity() { +class AutofillDecryptActivity : AppCompatActivity() { + @SettingsPreferences @Inject lateinit var settings: SharedPreferences + @Inject lateinit var repository: CryptoRepository + @Inject lateinit var dispatcherProvider: DispatcherProvider @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory @Inject lateinit var features: Features @Inject lateinit var passphraseCache: PGPPassphraseCache @@ -97,6 +106,9 @@ class AutofillDecryptActivity : BasePGPActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // Before anything is drawn: the passphrase dialog's parent stays out of screenshots and + // recents. + window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) // 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. diff --git a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt deleted file mode 100644 index e40585d5..00000000 --- a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt +++ /dev/null @@ -1,181 +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.ClipData -import android.content.Intent -import android.content.SharedPreferences -import android.os.Build -import android.os.Bundle -import android.os.PersistableBundle -import android.view.WindowManager -import androidx.annotation.CallSuper -import androidx.annotation.StringRes -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.FragmentActivity -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.util.coroutines.DispatcherProvider -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.services.ClipboardService -import app.passwordstore.util.settings.Constants -import app.passwordstore.util.settings.PreferenceKeys -import dagger.hilt.android.AndroidEntryPoint -import java.io.File -import javax.inject.Inject - -@Suppress("Registered") -@AndroidEntryPoint -open class BasePGPActivity : AppCompatActivity() { - - /** [SharedPreferences] instance used by subclasses to persist settings */ - @SettingsPreferences @Inject lateinit var settings: SharedPreferences - @Inject lateinit var repository: CryptoRepository - @Inject lateinit var dispatcherProvider: DispatcherProvider - - /** - * [onCreate] sets the window up with the right flags to prevent auth leaks through screenshots or - * recent apps screen. - */ - @CallSuper - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) - } - - companion object { - - const val EXTRA_FILE_PATH = "FILE_PATH" - - /** Gets the relative path to the repository */ - fun getRelativePath(fullPath: String, repositoryPath: String): String = - fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/") - - /** Gets the Parent path, relative to the repository */ - fun getParentPath(fullPath: String, repositoryPath: String): String { - val relativePath = getRelativePath(fullPath, repositoryPath) - val index = relativePath.lastIndexOf("/") - return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/" - .replace("/+".toRegex(), "/") - } - - /** /path/to/store/social/facebook.gpg -> social/facebook */ - @JvmStatic - fun getLongName(fullPath: String, repositoryPath: String, basename: String): String { - var relativePath = getRelativePath(fullPath, repositoryPath) - return if (relativePath.isNotEmpty() && relativePath != "/") { - // remove preceding '/' - relativePath = relativePath.substring(1) - if (relativePath.endsWith('/')) { - relativePath + basename - } else { - "$relativePath/$basename" - } - } else { - basename - } - } - } -} - -/** - * Copies provided [text] to the clipboard, flagged sensitive. Below Android 11 it also shows a - * snackbar, which can be disabled by passing [showSnackbar] as false. - */ -fun FragmentActivity.copyTextToClipboard( - text: String?, - showSnackbar: Boolean = true, - @StringRes snackbarTextRes: Int = R.string.clipboard_copied_text, -) { - val clipboard = clipboard ?: return - val clip = ClipData.newPlainText("pgp_handler_result_pm", text) - clip.description.extras = - PersistableBundle().apply { putBoolean("android.content.extra.IS_SENSITIVE", true) } - clipboard.setPrimaryClip(clip) - if (showSnackbar && Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - snackbar(message = resources.getString(snackbarTextRes)) - } -} - -/** - * Copies a provided [password] string to the clipboard. This wraps [copyTextToClipboard] and starts - * off an instance of [ClipboardService] to provide a way of clearing the clipboard. - */ -fun FragmentActivity.copyPasswordToClipboard(password: String?) { - copyTextToClipboard(password) - val clearAfter = - sharedPrefs.getString(PreferenceKeys.GENERAL_SHOW_TIME)?.toIntOrNull() - ?: Constants.DEFAULT_DECRYPTION_TIMEOUT - - if (clearAfter != 0) { - val service = - Intent(this, ClipboardService::class.java).apply { - action = ClipboardService.ACTION_START - putExtra(ClipboardService.EXTRA_NOTIFICATION_TIME, clearAfter) - } - startForegroundService(service) - } -} - -/** - * Get a list of available [PGPIdentifier]s for the current password repository. This method throws - * when no identifiers were able to be parsed. If this method returns null, it means that an invalid - * identifier was encountered and further execution must stop to let the user correct the problem. - */ -fun FragmentActivity.getPGPIdentifiers(subDir: String): List? { - val repoRoot = PasswordRepository.getRepositoryDirectory() - val gpgIdentifierFile = - File(repoRoot, subDir).findTillRoot(".gpg-id", repoRoot) - ?: File(repoRoot, ".gpg-id").apply { createNewFile() } - val gpgIdentifiers = - gpgIdentifierFile - .readLines() - .filter { it.isNotBlank() } - .map { line -> - PGPIdentifier.fromString(line) - ?: run { - // The line being empty means this is most likely an empty `.gpg-id` - // file we created. Skip the validation so we can make the user add a - // real ID. - if (line.isEmpty()) return@run - // Apparently `gpg-id` being the first line is also acceptable? - if (line == "gpg-id") return@run - if (line.removePrefix("0x").matches("[a-fA-F0-9]{8}".toRegex())) { - snackbar(message = resources.getString(R.string.short_gpg_id)) - } else { - snackbar(message = resources.getString(R.string.invalid_gpg_id)) - } - return null - } - } - .filterIsInstance() - if (gpgIdentifiers.isEmpty()) { - error("Failed to parse identifiers from .gpg-id: ${gpgIdentifierFile.readText()}") - } - return gpgIdentifiers -} - -@Suppress("ReturnCount") -private fun File.findTillRoot(fileName: String, rootPath: File): File? { - val gpgFile = File(this, fileName) - if (gpgFile.exists()) return gpgFile - - if (this.absolutePath == rootPath.absolutePath) { - return null - } - val parent = parentFile - return if (parent != null && parent.exists()) { - parent.findTillRoot(fileName, rootPath) - } else { - null - } -} diff --git a/app/src/main/java/app/passwordstore/ui/crypto/CryptoExtensions.kt b/app/src/main/java/app/passwordstore/ui/crypto/CryptoExtensions.kt new file mode 100644 index 00000000..fbaa534f --- /dev/null +++ b/app/src/main/java/app/passwordstore/ui/crypto/CryptoExtensions.kt @@ -0,0 +1,149 @@ +/* + * 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.ClipData +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.PersistableBundle +import android.widget.Toast +import androidx.annotation.StringRes +import androidx.fragment.app.FragmentActivity +import app.passwordstore.R +import app.passwordstore.crypto.PGPIdentifier +import app.passwordstore.data.repo.PasswordRepository +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.services.ClipboardService +import app.passwordstore.util.settings.Constants +import app.passwordstore.util.settings.PreferenceKeys +import java.io.File + +/** Gets the relative path to the repository */ +private fun getRelativePath(fullPath: String, repositoryPath: String): String = + fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/") + +/** Gets the Parent path, relative to the repository */ +fun getParentPath(fullPath: String, repositoryPath: String): String { + val relativePath = getRelativePath(fullPath, repositoryPath) + val index = relativePath.lastIndexOf("/") + return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/" + .replace("/+".toRegex(), "/") +} + +/** /path/to/store/social/facebook.gpg -> social/facebook */ +fun getLongName(fullPath: String, repositoryPath: String, basename: String): String { + var relativePath = getRelativePath(fullPath, repositoryPath) + return if (relativePath.isNotEmpty() && relativePath != "/") { + // remove preceding '/' + relativePath = relativePath.substring(1) + if (relativePath.endsWith('/')) { + relativePath + basename + } else { + "$relativePath/$basename" + } + } else { + basename + } +} + +/** + * Copies provided [text] to the clipboard, flagged sensitive. Below Android 11 it also shows a + * snackbar, which can be disabled by passing [showSnackbar] as false. + */ +fun FragmentActivity.copyTextToClipboard( + text: String?, + showSnackbar: Boolean = true, + @StringRes snackbarTextRes: Int = R.string.clipboard_copied_text, +) { + val clipboard = clipboard ?: return + val clip = ClipData.newPlainText("pgp_handler_result_pm", text) + clip.description.extras = + PersistableBundle().apply { putBoolean("android.content.extra.IS_SENSITIVE", true) } + clipboard.setPrimaryClip(clip) + if (showSnackbar && Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + snackbar(message = resources.getString(snackbarTextRes)) + } +} + +/** + * Copies a provided [password] string to the clipboard. This wraps [copyTextToClipboard] and starts + * off an instance of [ClipboardService] to provide a way of clearing the clipboard. + */ +fun FragmentActivity.copyPasswordToClipboard(password: String?) { + copyTextToClipboard(password) + val clearAfter = + sharedPrefs.getString(PreferenceKeys.GENERAL_SHOW_TIME)?.toIntOrNull() + ?: Constants.DEFAULT_DECRYPTION_TIMEOUT + + if (clearAfter != 0) { + val service = + Intent(this, ClipboardService::class.java).apply { + action = ClipboardService.ACTION_START + putExtra(ClipboardService.EXTRA_NOTIFICATION_TIME, clearAfter) + } + startForegroundService(service) + } +} + +/** + * Get a list of available [PGPIdentifier]s for the current password repository. This method throws + * when no identifiers were able to be parsed. If this method returns null, an invalid identifier + * was encountered and reported with a toast: further execution must stop to let the user correct + * the problem. + */ +fun Context.getPGPIdentifiers(subDir: String): List? { + val repoRoot = PasswordRepository.getRepositoryDirectory() + val gpgIdentifierFile = + File(repoRoot, subDir).findTillRoot(".gpg-id", repoRoot) + ?: File(repoRoot, ".gpg-id").apply { createNewFile() } + val gpgIdentifiers = + gpgIdentifierFile + .readLines() + .filter { it.isNotBlank() } + .map { line -> + PGPIdentifier.fromString(line) + ?: run { + // The line being empty means this is most likely an empty `.gpg-id` + // file we created. Skip the validation so we can make the user add a + // real ID. + if (line.isEmpty()) return@run + // Apparently `gpg-id` being the first line is also acceptable? + if (line == "gpg-id") return@run + val message = + if (line.removePrefix("0x").matches("[a-fA-F0-9]{8}".toRegex())) R.string.short_gpg_id + else R.string.invalid_gpg_id + // A toast on the application context, not a snackbar: the autofill decryption + // finishes its window right after, and the toast must outlive it. + Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show() + return null + } + } + .filterIsInstance() + if (gpgIdentifiers.isEmpty()) { + error("Failed to parse identifiers from .gpg-id: ${gpgIdentifierFile.readText()}") + } + return gpgIdentifiers +} + +@Suppress("ReturnCount") +private fun File.findTillRoot(fileName: String, rootPath: File): File? { + val gpgFile = File(this, fileName) + if (gpgFile.exists()) return gpgFile + + if (this.absolutePath == rootPath.absolutePath) { + return null + } + val parent = parentFile + return if (parent != null && parent.exists()) { + parent.findTillRoot(fileName, rootPath) + } else { + null + } +} 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 e5341320..02b112f4 100644 --- a/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt +++ b/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt @@ -43,10 +43,10 @@ import androidx.hilt.navigation.compose.hiltViewModel import app.passwordstore.R import app.passwordstore.data.passfile.Totp import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.ui.crypto.BasePGPActivity.Companion.getParentPath import app.passwordstore.ui.crypto.copyPasswordToClipboard import app.passwordstore.ui.crypto.copyTextToClipboard import app.passwordstore.ui.crypto.getPGPIdentifiers +import app.passwordstore.ui.crypto.getParentPath import app.passwordstore.ui.main.LaunchActivity import app.passwordstore.ui.main.MainActivity import app.passwordstore.ui.setup.SetupStart diff --git a/app/src/main/java/app/passwordstore/ui/list/StoreActions.kt b/app/src/main/java/app/passwordstore/ui/list/StoreActions.kt index 2e148401..3bdede73 100644 --- a/app/src/main/java/app/passwordstore/ui/list/StoreActions.kt +++ b/app/src/main/java/app/passwordstore/ui/list/StoreActions.kt @@ -9,7 +9,7 @@ import android.content.Context import androidx.core.content.edit import app.passwordstore.R import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.ui.crypto.BasePGPActivity.Companion.getLongName +import app.passwordstore.ui.crypto.getLongName import app.passwordstore.ui.git.base.BaseGitActivity import app.passwordstore.util.autofill.AutofillMatcher import app.passwordstore.util.extensions.base64 diff --git a/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt b/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt index 6f07a51a..c0af5742 100644 --- a/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt @@ -12,7 +12,6 @@ import android.os.Looper import androidx.appcompat.app.AppCompatActivity import androidx.core.content.edit import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.ui.crypto.BasePGPActivity import app.passwordstore.util.auth.BiometricAuthenticator import app.passwordstore.util.auth.BiometricAuthenticator.Result import app.passwordstore.util.extensions.sharedPrefs @@ -61,10 +60,10 @@ class LaunchActivity : AppCompatActivity() { // This activity is exported, so the file path is untrusted input: only forward it when it // points at a file inside the password store. MainActivity is not exported and trusts it. val repoDir = PasswordRepository.getRepositoryDirectory() - val filePath = intent.getStringExtra(BasePGPActivity.EXTRA_FILE_PATH) + val filePath = intent.getStringExtra(EXTRA_FILE_PATH) val intentToStart = Intent(this, MainActivity::class.java) if (intent.action == ACTION_DECRYPT_PASS && isFileInRepo(filePath, repoDir)) - intentToStart.putExtra(BasePGPActivity.EXTRA_FILE_PATH, filePath) + intentToStart.putExtra(EXTRA_FILE_PATH, filePath) // A plain start replaces the task, so an instance already open (one whose store vanished, say) // is not left below the new one. An entry opens on top, back returns to where the user was. else intentToStart.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) @@ -77,6 +76,9 @@ class LaunchActivity : AppCompatActivity() { const val ACTION_DECRYPT_PASS = "DECRYPT_PASS" + /** The entry to open; home screen shortcuts carry this exact key, never change it. */ + const val EXTRA_FILE_PATH = "FILE_PATH" + /** True when [path] resolves, after following `..` and symlinks, to a file under [repoDir]. */ fun isFileInRepo(path: String?, repoDir: File): Boolean { if (path.isNullOrEmpty()) return false 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 a381ca84..ce5fb1df 100644 --- a/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt @@ -21,7 +21,6 @@ import androidx.navigation.toRoute import app.passwordstore.data.password.PasswordItem import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.ui.compose.theme.APSTheme -import app.passwordstore.ui.crypto.BasePGPActivity import app.passwordstore.ui.editor.EditorDestination import app.passwordstore.ui.editor.EditorForm import app.passwordstore.ui.editor.editorForm @@ -84,7 +83,7 @@ class MainActivity : BaseGitActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val filePath = intent.getStringExtra(BasePGPActivity.EXTRA_FILE_PATH) + val filePath = intent.getStringExtra(LaunchActivity.EXTRA_FILE_PATH) val setup = intent.getStringExtra(EXTRA_SETUP)?.let { name -> SetupStart.entries.firstOrNull { it.name == name } diff --git a/app/src/test/java/app/passwordstore/ui/crypto/CryptoExtensionsTest.kt b/app/src/test/java/app/passwordstore/ui/crypto/CryptoExtensionsTest.kt new file mode 100644 index 00000000..fee6bf72 --- /dev/null +++ b/app/src/test/java/app/passwordstore/ui/crypto/CryptoExtensionsTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2026 Danilo M. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.ui.crypto + +import kotlin.test.Test +import kotlin.test.assertEquals + +/** Pins the path helpers' behavior across their move out of BasePGPActivity. */ +class CryptoExtensionsTest { + + @Test + fun parentPathIsRelativeToTheStore() { + assertEquals("/social/", getParentPath("/store/social/facebook.gpg", "/store")) + assertEquals("/", getParentPath("/store/example.org.gpg", "/store")) + } + + @Test + fun longNameJoinsFolderAndName() { + // StoreActions passes absolute folders, PasswordItem its path relative to the store. + assertEquals("work/x", getLongName("/store/work", "/store", "x")) + assertEquals("social/facebook", getLongName("/social/", "/store", "facebook")) + assertEquals("x", getLongName("/store", "/store", "x")) + } +} -- cgit v1.2.3