aboutsummaryrefslogtreecommitdiffstats
path: root/app/src
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-03 08:47:21 +0100
committerDanilo M. <danix@danix.xyz>2026-11-03 08:47:21 +0100
commitf6eceb406bfff8d36ccac7f7b0d428f148e76a55 (patch)
treec11d71bd10c9411a4e2130abd56c19ead42b910d /app/src
parenteff94a13a76e6f1f72e21848e6600bbbd731a660 (diff)
downloadHardenedPass-f6eceb406bfff8d36ccac7f7b0d428f148e76a55.tar.gz
HardenedPass-f6eceb406bfff8d36ccac7f7b0d428f148e76a55.zip
feat(app): open the Compose editor from the list and entry view
EditRoute replaces the legacy editor for new entries and edits. An edit copies its plaintext from the entry view's ViewModel below it, never through the route or saved state. Saving returns to the list, closing returns to where the editor was opened. FLAG_SECURE is now counted in MainActivity, so moving between the entry view and the editor never clears it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'app/src')
-rw-r--r--app/src/main/java/app/passwordstore/ui/editor/EditorDestination.kt262
-rw-r--r--app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt41
-rw-r--r--app/src/main/java/app/passwordstore/ui/list/ListDestination.kt5
-rw-r--r--app/src/main/java/app/passwordstore/ui/main/MainActivity.kt106
4 files changed, 359 insertions, 55 deletions
diff --git a/app/src/main/java/app/passwordstore/ui/editor/EditorDestination.kt b/app/src/main/java/app/passwordstore/ui/editor/EditorDestination.kt
new file mode 100644
index 00000000..8b66ba5e
--- /dev/null
+++ b/app/src/main/java/app/passwordstore/ui/editor/EditorDestination.kt
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package app.passwordstore.ui.editor
+
+import android.app.Activity
+import android.content.pm.PackageManager
+import androidx.activity.compose.BackHandler
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts.GetContent
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.stringResource
+import androidx.fragment.app.FragmentActivity
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.lifecycleScope
+import app.passwordstore.R
+import app.passwordstore.data.repo.PasswordRepository
+import app.passwordstore.ui.crypto.copyPasswordToClipboard
+import app.passwordstore.ui.crypto.getPGPIdentifiers
+import app.passwordstore.ui.list.FolderPicker
+import app.passwordstore.util.extensions.commitChange
+import app.passwordstore.util.extensions.getString
+import app.passwordstore.util.extensions.isInsideRepository
+import app.passwordstore.util.extensions.sharedPrefs
+import app.passwordstore.util.extensions.snackbar
+import app.passwordstore.util.settings.PreferenceKeys
+import com.google.android.material.snackbar.Snackbar
+import com.google.zxing.integration.android.IntentIntegrator
+import java.io.File
+import kotlinx.coroutines.launch
+
+/**
+ * The editor: wires [EditorScreen] to the [viewModel], the generator sheet, the folder picker, OTP
+ * import and the save path (checks, recipients from `.gpg-id`, encryption, write, git commit).
+ * [initial] fills the form the first time; null means the entry to edit is no longer in memory
+ * (process death) and the editor closes. [original] is the file being edited, null for a new entry.
+ * [generate] opens the generator at once (autofill's "generate password"). [onClose] leaves without
+ * saving; [onSaved] runs after the commit.
+ */
+@Composable
+fun EditorDestination(
+ activity: FragmentActivity,
+ initial: EditorForm?,
+ original: File?,
+ onClose: () -> Unit,
+ onSaved: (SaveResult.Saved) -> Unit,
+ modifier: Modifier = Modifier,
+ generate: Boolean = false,
+ viewModel: EditorViewModel = hiltViewModel(),
+) {
+ val root = PasswordRepository.getRepositoryDirectory()
+ // ponytail: filled once per ViewModel, so a recreated activity keeps the edits; an edit restored
+ // after process death has nothing to fill from (the entry view's plaintext is gone) and closes,
+ // back to the entry view's passphrase prompt.
+ val first = remember { initial != null && viewModel.start(initial) }
+ val lost = !viewModel.started
+ LaunchedEffect(lost) { if (lost) onClose() }
+ if (lost) return
+
+ val keyboard = LocalSoftwareKeyboardController.current
+ val focusManager = LocalFocusManager.current
+ val scope = rememberCoroutineScope()
+ var sheet by rememberSaveable { mutableStateOf(first && generate) }
+ var picker by rememberSaveable { mutableStateOf(false) }
+ var otp by rememberSaveable { mutableStateOf(false) }
+ var failure by remember { mutableStateOf<String?>(null) }
+ val form = viewModel.form
+ val hasOtp = remember(form.extra) { viewModel.hasOtp(form.extra) }
+ val hasCamera = remember {
+ activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
+ }
+ val showPassword = remember {
+ activity.sharedPrefs.getBoolean(PreferenceKeys.SHOW_PASSWORD, true)
+ }
+ val classicFirst = remember {
+ activity.sharedPrefs.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) == "classic"
+ }
+
+ fun say(message: Int) {
+ activity.snackbar(message = activity.getString(message))
+ }
+
+ fun addLine(line: String) {
+ viewModel.form = viewModel.form.copy(extra = withLine(viewModel.form.extra, line))
+ }
+
+ // The one way out without saving: only while no save runs, keyboard and focus dropped first.
+ fun close() {
+ if (viewModel.saving) return
+ keyboard?.hide()
+ focusManager.clearFocus()
+ onClose()
+ }
+
+ fun save(copy: Boolean) {
+ if (viewModel.saving) return
+ val current = viewModel.form
+ val target = entryFile(root, current)
+ val problem =
+ formError(current)
+ ?: R.string.message_error_destination_outside_repo.takeUnless {
+ target.isInsideRepository()
+ }
+ if (problem != null) return say(problem)
+ // Recipients from the nearest .gpg-id, as pass does; null: an invalid ID, already reported.
+ val ids = activity.getPGPIdentifiers(current.dir) ?: return
+ activity.lifecycleScope.launch {
+ when (val result = viewModel.save(ids, target, original, activity)) {
+ null -> Unit
+ is SaveResult.Failed -> failure = activity.getString(result.message, result.arg)
+ is SaveResult.Saved -> {
+ if (copy) activity.copyPasswordToClipboard(result.form.password)
+ result.staleOriginal?.let {
+ activity.snackbar(
+ message =
+ activity.getString(
+ R.string.password_creation_file_delete_fail_message,
+ it.nameWithoutExtension,
+ ),
+ length = Snackbar.LENGTH_LONG,
+ )
+ }
+ // ponytail: commit failures are not shown, as for the list's move and delete; a theme
+ // or locale change in the middle of a save cancels before this commit, and the next
+ // commit picks the file up.
+ activity.commitChange(
+ activity.getString(
+ if (original != null) R.string.git_commit_edit_text else R.string.git_commit_add_text,
+ target.relativeTo(root).path.removeSuffix(".gpg"),
+ )
+ )
+ keyboard?.hide()
+ focusManager.clearFocus()
+ onSaved(result)
+ }
+ }
+ }
+ }
+
+ BackHandler(onBack = ::close)
+
+ val scan =
+ rememberLauncherForActivityResult(StartActivityForResult()) { result ->
+ val text = IntentIntegrator.parseActivityResult(result.resultCode, result.data)?.contents
+ if (result.resultCode == Activity.RESULT_OK && text != null) {
+ addLine("$text\n")
+ say(R.string.otp_import_success)
+ } else {
+ say(R.string.otp_import_failure_generic)
+ }
+ }
+ val image =
+ rememberLauncherForActivityResult(GetContent()) { uri ->
+ if (uri == null) {
+ say(R.string.otp_import_failure_no_selection)
+ } else {
+ scope.launch {
+ val text = viewModel.readQr(uri, activity)
+ if (text != null) {
+ addLine(text)
+ say(R.string.otp_import_success)
+ } else {
+ say(R.string.otp_import_failure_generic)
+ }
+ }
+ }
+ }
+
+ Box(modifier) {
+ EditorScreen(
+ form = form,
+ editing = original != null,
+ saving = viewModel.saving,
+ showOtpActions = !hasOtp,
+ initiallyRevealed = showPassword,
+ focus =
+ when {
+ !first -> EditorFocus.None
+ form.name.isEmpty() -> EditorFocus.Name
+ original == null -> EditorFocus.Username
+ else -> EditorFocus.None
+ },
+ onFormChange = { viewModel.form = it },
+ onClose = ::close,
+ onSave = ::save,
+ onChangeFolder = { picker = true },
+ onGenerate = { sheet = true },
+ onScanQr =
+ if (hasCamera) {
+ {
+ scan.launch(
+ IntentIntegrator(activity)
+ .setOrientationLocked(false)
+ .setBeepEnabled(false)
+ .setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
+ .createScanIntent()
+ )
+ }
+ } else null,
+ onQrImage = { image.launch("image/*") },
+ onManualOtp = { otp = true },
+ )
+ if (sheet)
+ GeneratorSheet(
+ classicFirst = classicFirst,
+ random = viewModel.randomSpec(activity),
+ diceware = viewModel.dicewareSpec(),
+ onRandom = { spec -> viewModel.generateRandom(spec, activity) },
+ onDiceware = viewModel::generateDiceware,
+ onUse = { password ->
+ viewModel.form = viewModel.form.copy(password = password)
+ sheet = false
+ },
+ onDismiss = { sheet = false },
+ )
+ if (picker)
+ FolderPicker(
+ root = root,
+ start = form.dir,
+ onPick = { dir ->
+ viewModel.form = viewModel.form.copy(dir = dir)
+ picker = false
+ },
+ onDismiss = { picker = false },
+ )
+ if (otp)
+ OtpDialog(
+ onAdd = { secret, account ->
+ totpUri(secret, account)?.let(::addLine)
+ otp = false
+ },
+ onDismiss = { otp = false },
+ )
+ failure?.let { message ->
+ AlertDialog(
+ onDismissRequest = { failure = null },
+ title = { Text(stringResource(R.string.password_creation_file_fail_title)) },
+ text = { Text(message) },
+ confirmButton = {
+ TextButton(onClick = { failure = null }) { Text(stringResource(R.string.dialog_ok)) }
+ },
+ )
+ }
+ }
+}
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 5c16b5a3..1b3a75d2 100644
--- a/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt
+++ b/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt
@@ -7,7 +7,6 @@ package app.passwordstore.ui.entry
import android.app.Activity
import android.content.Intent
-import android.view.WindowManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.compose.foundation.layout.Box
@@ -22,7 +21,6 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
@@ -43,12 +41,9 @@ import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.SecureFlagPolicy
import androidx.hilt.navigation.compose.hiltViewModel
import app.passwordstore.R
-import app.passwordstore.data.passfile.PasswordEntry
import app.passwordstore.data.passfile.Totp
import app.passwordstore.data.repo.PasswordRepository
-import app.passwordstore.ui.crypto.BasePGPActivity
import app.passwordstore.ui.crypto.BasePGPActivity.Companion.getParentPath
-import app.passwordstore.ui.crypto.PasswordCreationActivity
import app.passwordstore.ui.crypto.copyPasswordToClipboard
import app.passwordstore.ui.crypto.copyTextToClipboard
import app.passwordstore.ui.crypto.getPGPIdentifiers
@@ -62,29 +57,21 @@ import app.passwordstore.util.settings.PreferenceKeys
import java.io.File
/**
- * One entry: wires [EntryScreen] to the [viewModel], the biometric and passphrase prompts, the
- * clipboard and the legacy editor. [onClose] leaves the destination; [onSaved] runs after the
- * editor saved the entry (the host returns to the list).
+ * One entry: wires [EntryScreen] to the [viewModel], the biometric and passphrase prompts and the
+ * clipboard. [onClose] leaves the destination; [onEdit] opens the editor on the decrypted entry.
+ * The host keeps FLAG_SECURE on while this is composed.
*/
@Composable
fun EntryDestination(
activity: MainActivity,
file: File,
onClose: () -> Unit,
- onSaved: () -> Unit,
+ onEdit: () -> Unit,
modifier: Modifier = Modifier,
viewModel: EntryViewModel = hiltViewModel(),
) {
val root = PasswordRepository.getRepositoryDirectory()
- // Secrets stay out of screenshots and recents while this destination is composed.
- // ponytail: plain add/clear, fine while no other secret-showing destination can overlap this
- // one in a transition; count in MainActivity once the editor (step 4) becomes a destination.
- DisposableEffect(Unit) {
- activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
- onDispose { activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) }
- }
-
// Routes come from the app's own listing or from LaunchActivity's check; check once more.
val inRepo = remember(file) { LaunchActivity.isFileInRepo(file.path, root) }
LaunchedEffect(inRepo) { if (!inRepo) onClose() }
@@ -105,17 +92,13 @@ fun EntryDestination(
}
}
- // ponytail: after process death during key import or the editor, the unlock flow can start
+ // ponytail: after process death during key import, the unlock flow can start
// twice or a prompt can be left over the list; rare, no secret exposed. Fix by tracking
// whether the flow already started if it shows up.
val keyImport =
rememberLauncherForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) unlock() else onClose()
}
- val editor =
- rememberLauncherForActivityResult(StartActivityForResult()) { result ->
- if (result.resultCode == Activity.RESULT_OK) onSaved()
- }
// ponytail: a theme or locale change while the biometric prompt is up loses that prompt's
// callback (the flow already counts as started); back out and reopen the entry.
@@ -150,7 +133,7 @@ fun EntryDestination(
otpFractionLeft = totp?.fractionLeft() ?: 0f,
showPassword = showPassword,
onBack = onClose,
- onEdit = { entry?.let { editor.launch(editIntent(activity, file, root, it)) } },
+ onEdit = { if (entry != null) onEdit() },
onShare = { fields?.password?.let { activity.startActivity(shareIntent(activity, it)) } },
onCopyName = { activity.copyTextToClipboard(file.nameWithoutExtension) },
onCopySecret = { activity.copyPasswordToClipboard(it) },
@@ -252,18 +235,6 @@ private fun PassphraseDialog(
)
}
-/** The legacy editor, filled exactly as the old decrypt screen filled it. */
-private fun editIntent(activity: Activity, file: File, root: File, entry: PasswordEntry): Intent =
- Intent(activity, PasswordCreationActivity::class.java)
- .setAction(Intent.ACTION_VIEW)
- .putExtra(BasePGPActivity.EXTRA_FILE_PATH, getParentPath(file.absolutePath, root.absolutePath))
- .putExtra(BasePGPActivity.EXTRA_REPO_PATH, root.absolutePath)
- .putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, file.nameWithoutExtension)
- .putExtra(PasswordCreationActivity.EXTRA_USERNAME, entry.username)
- .putExtra(PasswordCreationActivity.EXTRA_PASSWORD, entry.password)
- .putExtra(PasswordCreationActivity.EXTRA_EXTRA_CONTENT, entry.extraContentWithoutUsername)
- .putExtra(PasswordCreationActivity.EXTRA_EDITING, true)
-
/** Always a chooser, so the user can still cancel. */
private fun shareIntent(activity: Activity, password: String): Intent =
Intent.createChooser(
diff --git a/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt b/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt
index 5c248dcb..eff3cf02 100644
--- a/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt
+++ b/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt
@@ -60,7 +60,7 @@ private enum class ListDialog {
/**
* The list at [path] (relative to the store root): wires [ListScreen] to the [viewModel], the
* folder picker, the legacy activities and the store operations. [onOpenFolder] takes a path
- * relative to the root, [onOpenEntry] an entry file.
+ * relative to the root, [onOpenEntry] an entry file; [onNewEntry] opens the editor in this folder.
*/
@Composable
fun ListDestination(
@@ -68,6 +68,7 @@ fun ListDestination(
path: String,
onOpenFolder: (relativePath: String) -> Unit,
onOpenEntry: (File) -> Unit,
+ onNewEntry: () -> Unit,
modifier: Modifier = Modifier,
viewModel: SearchableRepositoryViewModel = hiltViewModel(),
) {
@@ -178,7 +179,7 @@ fun ListDestination(
onDismiss = { dialog = ListDialog.None },
onNewEntry = {
dialog = ListDialog.None
- ifInitialized { activity.createEntry(dir) }
+ ifInitialized(onNewEntry)
},
onNewFolder = {
dialog = ListDialog.None
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 174f7665..1a1ff500 100644
--- a/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt
@@ -5,9 +5,13 @@
package app.passwordstore.ui.main
-import android.content.Intent
import android.os.Bundle
+import android.view.WindowManager
import androidx.activity.compose.setContent
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@@ -16,8 +20,11 @@ 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.crypto.PasswordCreationActivity
+import app.passwordstore.ui.editor.EditorDestination
+import app.passwordstore.ui.editor.EditorForm
+import app.passwordstore.ui.editor.editorForm
import app.passwordstore.ui.entry.EntryDestination
+import app.passwordstore.ui.entry.EntryViewModel
import app.passwordstore.ui.git.base.BaseGitActivity
import app.passwordstore.ui.list.ListDestination
import app.passwordstore.ui.list.folderMoves
@@ -49,6 +56,13 @@ import kotlinx.serialization.Serializable
@Serializable data class EntryRoute(val filePath: String)
/**
+ * The editor: a new entry in [dir] (relative to the store root, "" is the root), or the entry
+ * [name] (file name without `.gpg`) there. An edit takes its plaintext from the entry view below it
+ * on the back stack, never from the route.
+ */
+@Serializable data class EditRoute(val dir: String, val name: String? = null)
+
+/**
* Single-activity host for the Compose UI. Not exported: it is only started by [LaunchActivity],
* after its biometric gate and path check. Extends [BaseGitActivity] (an AppCompatActivity, so also
* the FragmentActivity BiometricPrompt needs) because the git operations require one; it hosts no
@@ -59,6 +73,9 @@ class MainActivity : BaseGitActivity() {
@Inject lateinit var shortcutHandler: ShortcutHandler
+ /** Composed destinations that show secrets; FLAG_SECURE is on while this is above zero. */
+ private var secureHolds = 0
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val filePath = intent.getStringExtra(BasePGPActivity.EXTRA_FILE_PATH)
@@ -86,24 +103,66 @@ class MainActivity : BaseGitActivity() {
nav.navigate(EntryRoute(file.absolutePath))
}
},
+ onNewEntry = {
+ if (nav.currentBackStackEntry?.id == entry.id) nav.navigate(EditRoute(path))
+ },
)
}
composable<EntryRoute> { entry ->
+ val file = File(entry.toRoute<EntryRoute>().filePath)
+ SecureWindow(this@MainActivity)
EntryDestination(
activity = this@MainActivity,
- file = File(entry.toRoute<EntryRoute>().filePath),
+ file = file,
onClose = {
// Only the entry on top closes: the auto-close timer can race a back press.
if (nav.currentBackStackEntry?.id == entry.id) {
if (nav.previousBackStackEntry == null) finish() else nav.popBackStack()
}
},
+ onEdit = {
+ if (nav.currentBackStackEntry?.id == entry.id) {
+ val root = PasswordRepository.getRepositoryDirectory().canonicalFile
+ val dir = file.canonicalFile.parentFile?.relativeTo(root)?.path.orEmpty()
+ nav.navigate(EditRoute(dir, file.nameWithoutExtension))
+ }
+ },
+ )
+ }
+ composable<EditRoute> { entry ->
+ val route = entry.toRoute<EditRoute>()
+ val name = route.name
+ // An edit reads the decrypted entry from the entry view below it. After process
+ // death that ViewModel is new and empty, and the editor closes.
+ val decrypted =
+ if (name == null) null
+ else
+ remember(entry) {
+ // No entry view below (none on the stack): getBackStackEntry throws.
+ try {
+ nav.getBackStackEntry<EntryRoute>()
+ } catch (_: IllegalArgumentException) {
+ null
+ }
+ }
+ ?.let { hiltViewModel<EntryViewModel>(it).entry }
+ SecureWindow(this@MainActivity)
+ EditorDestination(
+ activity = this@MainActivity,
+ initial =
+ if (name == null) EditorForm(dir = route.dir)
+ else decrypted?.let { editorForm(it, route.dir, name) },
+ original =
+ name?.let {
+ File(File(PasswordRepository.getRepositoryDirectory(), route.dir), "$it.gpg")
+ },
+ onClose = { if (nav.currentBackStackEntry?.id == entry.id) nav.popBackStack() },
onSaved = {
- // Back to the list, which re-reads the store on resume. Opened from a shortcut,
- // the entry has no list below it: the list root replaces it.
+ // Back to the list, which re-reads the store on resume. An entry opened from a
+ // shortcut has no list below it: the list root replaces the stack.
if (nav.currentBackStackEntry?.id == entry.id) {
- if (nav.previousBackStackEntry != null) nav.popBackStack()
- else nav.navigate(ListRoute()) { popUpTo<EntryRoute> { inclusive = true } }
+ if (!nav.popBackStack<ListRoute>(inclusive = false))
+ nav.navigate(ListRoute()) { popUpTo<EntryRoute> { inclusive = true } }
}
},
)
@@ -113,6 +172,16 @@ class MainActivity : BaseGitActivity() {
}
}
+ /** Counts a destination that shows secrets; the first one turns FLAG_SECURE on. */
+ fun holdSecure() {
+ if (secureHolds++ == 0) window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+ }
+
+ /** Ends a [holdSecure]; FLAG_SECURE goes off when no secret-showing destination is left. */
+ fun releaseSecure() {
+ if (--secureHolds == 0) window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
+ }
+
/** Re-reads the store; starts onboarding and returns false when there is none yet. */
fun checkStore(): Boolean {
PasswordRepository.initialize()
@@ -145,17 +214,6 @@ class MainActivity : BaseGitActivity() {
shortcutHandler.addPinnedShortcut(item, item.createAuthEnabledIntent(this))
}
- fun createEntry(dir: File) {
- startActivity(
- Intent(this, PasswordCreationActivity::class.java)
- .putExtra(BasePGPActivity.EXTRA_FILE_PATH, dir.absolutePath)
- .putExtra(
- BasePGPActivity.EXTRA_REPO_PATH,
- PasswordRepository.getRepositoryDirectory().absolutePath,
- )
- )
- }
-
fun openSettings() {
launchActivity(SettingsActivity::class.java)
}
@@ -163,3 +221,15 @@ class MainActivity : BaseGitActivity() {
private fun entryItem(file: File) =
PasswordItem.newPassword(file.name, file, PasswordRepository.getRepositoryDirectory())
}
+
+/**
+ * FLAG_SECURE while composed. Counted in [MainActivity], because in a transition (entry view to
+ * editor and back) the entering destination is composed before the leaving one is disposed.
+ */
+@Composable
+private fun SecureWindow(activity: MainActivity) {
+ DisposableEffect(activity) {
+ activity.holdSecure()
+ onDispose { activity.releaseSecure() }
+ }
+}