From 87010a522e2b0eeecff85da96ae52544f117a6bf Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 2 Nov 2026 20:41:55 +0100 Subject: docs: start the compose branch for the UI overhaul Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'AGENTS.md') diff --git a/AGENTS.md b/AGENTS.md index 6708cd53..77f15fe1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,11 @@ Keep "GrapheneOS" out of the app name: it is that project's trademark. ## Repository layout and remotes -- `master` is the only branch. Upstream's `develop` was renamed; do not recreate it. +- `master` is the only long-lived branch. Upstream's `develop` was renamed; do not recreate it. +- Temporary exception: the Compose UI overhaul + (`docs/superpowers/specs/2026-11-02-compose-ui-overhaul-design.md`) lives on `compose` until + it is complete; nothing is released from it. `master` stays releasable (public suffix list, + hotfixes). Remove this bullet when `compose` merges. - `origin` fetches from the maintainer's own git server (the main repository) and pushes to it and to the GitHub fork (`danixland/HardenedPass`) in one `git push`. GitHub is a push mirror: never create commits on GitHub (bots, web edits), they would -- cgit v1.2.3 From 7948a668ca23985add032f506ce2cc8227d6a20f Mon Sep 17 00:00:00 2001 From: "Danilo M." 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 'AGENTS.md') 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 a3c105af9adb2b45ad52fe47218cc8827741dca9 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 3 Nov 2026 09:48:36 +0100 Subject: fix(git): get sshj's Ed25519 keys from Bouncy Castle Recent Conscrypt (Android 14 mainline updates) registers a KeyFactory for Ed25519. With sshj leaving provider selection to the JCA, its Ed25519KeyFactory now gets Conscrypt's key class, whose algorithm name sshj does not map to ssh-ed25519: KeyType.fromKey returns UNKNOWN and every Ed25519 host key fails with "Don't know how to encode key", so clone and sync over SSH break. 0.1.4 is affected since the sshj 0.41.2 update. Register a provider at position 1 that serves only KeyFactory.Ed25519 from Bouncy Castle (kept by the existing R8 rule). Everything else, including signatures with Android Keystore keys, still goes through normal JCA provider selection. Verified on an Android 14 tablet: clone over SSH failed before and works with the fix. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit ad3c63f665e6264ed1db42d6ae2dfc0947e33393) --- AGENTS.md | 4 ++++ .../java/app/passwordstore/util/git/sshj/SshjConfig.kt | 18 ++++++++++++++++++ .../app/passwordstore/util/git/sshj/SshjConfigTest.kt | 14 ++++++++++++++ 3 files changed, 36 insertions(+) (limited to 'AGENTS.md') diff --git a/AGENTS.md b/AGENTS.md index 91c0857b..f2e32456 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -122,6 +122,10 @@ passed. - `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. +- sshj gets Ed25519 keys from Bouncy Castle: `SshjConfig.kt` pins `KeyFactory.Ed25519` to it at + provider position 1. Conscrypt from Android 14 mainline updates serves that factory with keys + sshj maps to `KeyType.UNKNOWN` ("Don't know how to encode key"), breaking every Ed25519 host + and user key. The emulator image does not show it; a 2026-03 Android 14 tablet does. - 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/src/main/java/app/passwordstore/util/git/sshj/SshjConfig.kt b/app/src/main/java/app/passwordstore/util/git/sshj/SshjConfig.kt index b43d0007..3a3538ac 100644 --- a/app/src/main/java/app/passwordstore/util/git/sshj/SshjConfig.kt +++ b/app/src/main/java/app/passwordstore/util/git/sshj/SshjConfig.kt @@ -12,6 +12,7 @@ import com.hierynomus.sshj.transport.cipher.GcmCiphers import com.hierynomus.sshj.transport.kex.ExtInfoClientFactory import com.hierynomus.sshj.transport.mac.Macs import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile +import java.security.Provider import java.security.Security import logcat.LogPriority.VERBOSE import logcat.logcat @@ -32,6 +33,18 @@ import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile import org.bouncycastle.jce.provider.BouncyCastleProvider import org.slf4j.Logger +private object Ed25519KeyFactoryProvider : + Provider("HardenedPassEd25519", 1.0, "Ed25519 KeyFactory from Bouncy Castle") { + private fun readResolve(): Any = Ed25519KeyFactoryProvider + + init { + put( + "KeyFactory.Ed25519", + "org.bouncycastle.jcajce.provider.asymmetric.edec.KeyFactorySpi\$Ed25519", + ) + } +} + fun setUpBouncyCastleForSshj() { // Replace the Android BC provider with the Java BouncyCastle provider since the former does // not include all the required algorithms. @@ -48,6 +61,11 @@ fun setUpBouncyCastleForSshj() { runCatching { Class.forName("sun.security.jca.Providers") } Security.insertProviderAt(BouncyCastleProvider(), bcIndex + 1) } + // Recent Conscrypt (Android 14 mainline updates) serves KeyFactory "Ed25519" with keys whose + // algorithm name sshj does not map to ssh-ed25519 (KeyType.UNKNOWN, "Don't know how to encode + // key"), which breaks every Ed25519 host and user key. Pin only that factory to Bouncy Castle, + // ahead of every other provider; signatures still pick a provider per key. + Security.insertProviderAt(Ed25519KeyFactoryProvider, 1) logcat("setUpBouncyCastleForSshj", priority = VERBOSE) { "JCE providers: ${Security.getProviders().joinToString { "${it.name} (${it.version})" }}" } diff --git a/app/src/test/java/app/passwordstore/util/git/sshj/SshjConfigTest.kt b/app/src/test/java/app/passwordstore/util/git/sshj/SshjConfigTest.kt index af6eee47..e4bc3eec 100644 --- a/app/src/test/java/app/passwordstore/util/git/sshj/SshjConfigTest.kt +++ b/app/src/test/java/app/passwordstore/util/git/sshj/SshjConfigTest.kt @@ -6,7 +6,11 @@ package app.passwordstore.util.git.sshj import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertNull +import kotlin.test.assertTrue +import net.schmizz.sshj.common.Ed25519KeyFactory +import net.schmizz.sshj.common.KeyType import net.schmizz.sshj.common.SecurityUtils class SshjConfigTest { @@ -19,4 +23,14 @@ class SshjConfigTest { setUpBouncyCastleForSshj() assertNull(SecurityUtils.getSecurityProvider()) } + + // Conscrypt's Ed25519 keys map to KeyType.UNKNOWN in sshj, so Ed25519 keys must come from + // Bouncy Castle whatever provider the platform puts first. + @Test + fun ed25519KeysComeFromBouncyCastle() { + setUpBouncyCastleForSshj() + val key = Ed25519KeyFactory.getPublicKey(ByteArray(32) { 1 }) + assertTrue(key.javaClass.name.startsWith("org.bouncycastle."), key.javaClass.name) + assertEquals(KeyType.ED25519, KeyType.fromKey(key)) + } } -- cgit v1.2.3 From ca69bd81057fc8a9f65ff7e2f1d874697df8a6cc Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 4 Nov 2026 18:08:01 +0100 Subject: docs: record the finished Compose overhaul AGENTS.md drops the compose branch exception, describes the Compose UI and its activities, updates the emulator notes (serial pinning, prefixed helpers, Compose nodes, which windows are FLAG_SECURE, waiting for user storage after boot, FAB content-desc, text field clearing, run-as quoting) and the last device runs, and records why Material Components (Views) and fragment-ktx stay. The spec is marked complete with what was removed and kept, and says the merge waits for the maintainer's phone test; CONTRIBUTING.md no longer asks for view binding. The cleanup plan's emulator pass gets the harness fixes found while running it. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 54 ++++++++++++++++------ CONTRIBUTING.md | 2 +- .../plans/2026-11-04-compose-cleanup.md | 14 +++--- .../specs/2026-11-02-compose-ui-overhaul-design.md | 21 ++++++--- 4 files changed, 63 insertions(+), 28 deletions(-) (limited to 'AGENTS.md') diff --git a/AGENTS.md b/AGENTS.md index f2e32456..47028103 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,12 @@ Modules: `app` (UI, git sync, autofill service), `autofill-parser`, `crypto:comm `crypto:pgpainless`, `format:common`, `passgen:diceware`, `passgen:random`, `ui:compose`, `coroutine-utils`. Build conventions live in `build-logic/`. +The UI is Jetpack Compose with Material 3. `MainActivity` hosts every screen in one `NavHost` +(list, entry, editor, setup, settings); `LaunchActivity` is the only exported activity. The +autofill service starts its four activities (`AutofillFilterView`, `AutofillDecryptActivity`, +`AutofillSaveActivity`, `AutofillPublisherChangedActivity`) directly, so they stay separate and +draw with the same theme. + `applicationId` is `xyz.danix.hardenedpass` (debug: `.debug` suffix, label "HardenedPass Debug"), so it installs next to upstream's `app.passwordstore` and never updates over it. The Kotlin package stays `app.passwordstore`; renaming it is churn with no user-visible effect. @@ -21,11 +27,7 @@ Keep "GrapheneOS" out of the app name: it is that project's trademark. ## Repository layout and remotes -- `master` is the only long-lived branch. Upstream's `develop` was renamed; do not recreate it. -- Temporary exception: the Compose UI overhaul - (`docs/superpowers/specs/2026-11-02-compose-ui-overhaul-design.md`) lives on `compose` until - it is complete; nothing is released from it. `master` stays releasable (public suffix list, - hotfixes). Remove this bullet when `compose` merges. +- `master` is the only branch. Upstream's `develop` was renamed; do not recreate it. - `origin` fetches from the maintainer's own git server (the main repository) and pushes to it and to the GitHub fork (`danixland/HardenedPass`) in one `git push`. GitHub is a push mirror: never create commits on GitHub (bots, web edits), they would @@ -69,10 +71,23 @@ in `local.properties`: $ANDROID_HOME/emulator/emulator -avd aps-test -no-window -no-audio -no-boot-anim \ -no-snapshot-save -gpu swiftshader_indirect & - ./gradlew installDebug - -Drive it with `adb shell input`, `uiautomator dump` and `screencap`. Decrypt screens set -`FLAG_SECURE`, so screenshots of them are black; read them from the `uiautomator` dump. + ./gradlew :app:installDebug + +If a physical device is connected too, `export ANDROID_SERIAL=emulator-5554` first, so every +`adb` and Gradle install command targets the emulator. Agent shells keep no functions or +variables between calls: keep that export and any helpers in a sourced file, and prefix helper +names (`e2e_pass`, not `pass`, which is the real `pass` and reaches a real store). Drive it with +`adb shell input`, `uiautomator dump` and `screencap`; Compose nodes carry `text` and +`content-desc` in the dump, no resource ids, so match on those. FABs ("Import PGP key", "New +entry") carry only `content-desc`, and the list's search bar is itself an `EditText`. The entry +view, the editor and its generator sheet, the proxy screen, the passphrase dialogs and the +autofill decryption and save windows set `FLAG_SECURE`, so screenshots of them are black; read +them from the `uiautomator` dump. After boot, wait until the keyguard is dismissed and user +storage is unlocked before `adb uninstall` or `push-keys`: earlier, uninstall fails with +`DELETE_FAILED_INTERNAL_ERROR` and `/sdcard/Download` does not exist yet. In a Compose text +field, wait about a second after the tap before clearing it (Ctrl+A, DEL), or the clear misses. +`adb shell run-as sh -c '...'` needs the whole command quoted as one `adb` argument +(`adb shell "run-as sh -c '...'"`), or the device shell splits it. Fingerprint: `adb emu finger touch 1` after enrolling one in Settings (needs a lock PIN: `adb shell locksettings set-pin 1234` on the emulator only). Disable animations (`settings put global animator_duration_scale 0`, same for `transition_` and `window_`) @@ -85,8 +100,11 @@ worktree with `~/.android/debug.keystore`, so an in-place upgrade between them n re-signing. Autofill cannot be tested on the emulator image: Settings is denylisted in -`OreoAutofillService`, and the only browser (WebView Shell) is not a trusted browser. Use a -real device with a trusted browser (e.g. Firefox) instead: +`OreoAutofillService`, and the only browser (WebView Shell) is not a trusted browser. Its +activities are not exported; after `adb root` (the AOSP image allows it), `am start -n` opens the +picker, the publisher warning and the save editor with test extras, but not a real decryption +(it needs a client state `Bundle`). Use a real device with a trusted browser (e.g. Firefox) +instead: - `adb reverse tcp:2222 tcp:2222` lets the device clone `ssh://git@127.0.0.1:2222/...` from the e2e server over USB; serve a local login page the same way (`python3 -m http.server` on @@ -102,9 +120,10 @@ real device with a trusted browser (e.g. Firefox) instead: - Only read screens of the debug app, the test page and the autofill UI; the device holds the owner's real data. -Last full device run (2026-11-02): clone over SSH, PGP import, decrypt, TOTP, create and push, -conflict recovery, hard reset, fingerprint unlock (emulator) and Firefox autofill (tablet) all -passed. +Last full emulator run (2026-11-04, Compose UI): clone over SSH, PGP import, decrypt, TOTP, +create and push, conflict recovery (abort rebase, push a new branch), hard reset and +fingerprint unlock passed. Last Firefox autofill run on the tablet: 2026-11-04 (fill, passphrase +cache rule, no keys flow, picker, settings dialog), all passed. ## Verified facts and gotchas @@ -122,6 +141,13 @@ passed. - `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. +- Material Components (Views) stays after the Compose overhaul: the window themes derive from + `Theme.Material3.DayNight` (the AppCompat activities need it), and the git operations' View + dialogs (`BaseGitActivity`, `GitOperation`, `CredentialFinder` with + `git_credential_layout.xml`, `BreakOutOfDetached`, `StoreActions`), `DynamicColors` and the + `snackbar` helper use it. `oreo_autofill_dataset.xml` stays a layout: fill suggestions are + `RemoteViews` drawn by the system. `fragment-ktx` is declared only to pin `androidx.fragment` + (appcompat alone resolves an older one). - sshj gets Ed25519 keys from Bouncy Castle: `SshjConfig.kt` pins `KeyFactory.Ed25519` to it at provider position 1. Conscrypt from Android 14 mainline updates serves that factory with keys sshj maps to `KeyType.UNKNOWN` ("Don't know how to encode key"), breaking every Ed25519 host diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f83d413..f681cf52 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ We bundle a [`ignore-revs-file`](https://git-scm.com/docs/git-blame#Documentatio ## Source code conventions - Unless you're absolutely sure what you're doing, always prefer the [`unsafeLazy`](https://github.com/android-password-store/Android-Password-Store/blob/f870dd49138d63d9803783458abf1356176851db/app/src/main/java/app/passwordstore/util/extensions/Extensions.kt#L68) method over Kotlin's inbuilt `lazy` for lazily evaluated properties. -- For inflating a Fragment or Activity view, always use the [`viewBinding`](https://github.com/android-password-store/Android-Password-Store/blob/3a16d77db24b6d79d9d27ce3207d72a7a879f38a/app/src/main/java/dev/msfjarvis/aps/util/extensions/FragmentViewBindingDelegate.kt) extension. +- The UI is Jetpack Compose (Material 3): screens are stateless composables driven by a ViewModel. Do not add XML layouts, Fragments or view binding. ## Building the project diff --git a/docs/superpowers/plans/2026-11-04-compose-cleanup.md b/docs/superpowers/plans/2026-11-04-compose-cleanup.md index a09d545b..1e23ed22 100644 --- a/docs/superpowers/plans/2026-11-04-compose-cleanup.md +++ b/docs/superpowers/plans/2026-11-04-compose-cleanup.md @@ -1003,7 +1003,7 @@ APP=xyz.danix.hardenedpass.debug e2e_ui() { adb exec-out uiautomator dump /dev/tty; } e2e_center() { local b; b=$(e2e_ui | grep -o "]*$1[^>]*>" | sed -n "${2:-1}p" | grep -o 'bounds="[^"]*"' | grep -oE '[0-9]+' | tr '\n' ' '); [ -n "$b" ] || { echo "not found: $1" >&2; return 1; }; set -- $b; echo "$(( ($1 + $3) / 2 )) $(( ($2 + $4) / 2 ))"; } e2e_tap() { local c; c=$(e2e_center "$1" "$2") && adb shell input tap $c; sleep 1; } -e2e_field() { e2e_tap 'class="android.widget.EditText"' "$1"; adb shell input keycombination 113 29; adb shell input keyevent 67; adb shell input text "$2"; } +e2e_field() { e2e_tap 'class="android.widget.EditText"' "$1"; sleep 1; adb shell input keycombination 113 29; adb shell input keyevent 67; adb shell input text "$2"; } e2e_value() { e2e_ui | grep -o ']*class="android.widget.EditText"[^>]*>' | sed -n "${1}p" | grep -o ' text="[^"]*"' | cut -d'"' -f2; } e2e_seen() { e2e_ui | grep -q "$1" && echo "ok: $1" || echo "MISSING: $1"; } e2e_enabled() { e2e_ui | grep -o "]*$1[^>]*>" | head -1 | grep -o 'enabled="[a-z]*"'; } @@ -1044,6 +1044,8 @@ until [ "$(adb shell getprop sys.boot_completed | tr -d '\r')" = 1 ]; do sleep 2 adb shell locksettings set-pin 1234 2>/dev/null || true adb shell input keyevent KEYCODE_WAKEUP; adb shell input keyevent 82 adb shell input text 1234; adb shell input keyevent 66 +until [ "$(adb shell getprop sys.user.0.ce_available | tr -d '\r')" = true ] && \ + adb shell test -d /sdcard/Download; do sleep 2; done for s in animator_duration_scale transition_animation_scale window_animation_scale; do adb shell settings put global $s 0; done adb shell settings put secure autofill_service null @@ -1051,7 +1053,7 @@ adb uninstall $APP || true ./gradlew :app:installDebug scripts/e2e-env.sh push-keys ``` -(`set-pin` fails harmlessly when the AVD already has the PIN. `installDebug` and `push-keys` honor `ANDROID_SERIAL`.) +(`set-pin` fails harmlessly when the AVD already has the PIN. The wait matters: before the keyguard is dismissed and user storage is unlocked, `adb uninstall` fails with `DELETE_FAILED_INTERNAL_ERROR` and `/sdcard/Download` does not exist. `installDebug` and `push-keys` honor `ANDROID_SERIAL`.) - [ ] **Step 3: Clone over SSH, PGP import** @@ -1063,10 +1065,10 @@ e2e_field 1 ssh://git@10.0.2.2:2222/srv/git/store.git e2e_tap 'text="Manage"'; e2e_tap 'text="Import SSH key"'; e2e_pick aps-e2e-ssh-key e2e_seen 'text="Imported private key"'; adb shell input keyevent 4; sleep 1 e2e_tap 'text="Clone"'; sleep 15 -e2e_tap 'text="Import PGP key"' 2; e2e_pick aps-e2e-pgp-key.asc; e2e_tap 'text="OK"'; sleep 2 +e2e_tap 'content-desc="Import PGP key"'; e2e_pick aps-e2e-pgp-key.asc; e2e_tap 'text="OK"'; sleep 2 e2e_seen 'text="example.org"'; e2e_seen 'text="work"'; e2e_seen 'text="otp"'; e2e_top ``` -Expected: `MainActivity` on top with the cloned store (`example.org`, `work`, `otp`). (If the FAB is the first "Import PGP key" match on this image, use index 1.) +Expected: `MainActivity` on top with the cloned store (`example.org`, `work`, `otp`). (FABs expose their label as `content-desc`, not `text`, in the dump.) - [ ] **Step 4: Decrypt** @@ -1097,7 +1099,7 @@ The OTP screen never goes idle (no `uiautomator dump` on it) and `screencap` is ```bash source SCRATCHPAD/e2e.sh [ "$ANDROID_SERIAL" = emulator-5554 ] || exit 1 -e2e_tap 'text="New entry"'; e2e_tap 'text="Create new password"'; e2e_secure +e2e_tap 'content-desc="New entry"'; e2e_tap 'text="Create new password"'; e2e_secure adb shell input text regress.example.net e2e_field 2 r3gress-pw; e2e_field 3 dora e2e_tap 'text="Save"'; sleep 3; e2e_seen 'text="regress.example.net"' @@ -1244,7 +1246,7 @@ Expected: the View credential dialog shows the message, a password field (`passw ```bash source SCRATCHPAD/e2e.sh [ "$ANDROID_SERIAL" = emulator-5554 ] || exit 1 -adb shell run-as $APP sh -c 'mkdir -p files/store/badid && echo 0x1234ABCD > files/store/badid/.gpg-id && cp files/store/work/mail.example.org.gpg files/store/badid/x.gpg' +adb shell "run-as $APP sh -c 'mkdir -p files/store/badid && echo 0x1234ABCD > files/store/badid/.gpg-id && cp files/store/work/mail.example.org.gpg files/store/badid/x.gpg'" e2e_open; e2e_tap 'text="badid"'; e2e_tap 'text="x"'; sleep 1 adb exec-out screencap -p > "$OUT/toast.png" e2e_seen 'class="android.widget.EditText"' 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 3282c270..0404b2fb 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 @@ -1,6 +1,6 @@ # Compose UI overhaul: design -Date: 2026-11-02. Status: approved, not started. +Date: 2026-11-02. Status: complete (2026-11-04); merges into `master` after the maintainer's phone test. ## Goal @@ -138,10 +138,14 @@ order (both removed) becomes folders first, and the unused OpenKeychain SSH key ### Removed at the end -`fragment-ktx`, `recyclerview`, `recyclerview-selection`, `swiperefreshlayout`, -`modernandroidpreferences`, the view-binding helpers (`FragmentViewBindingDelegate`, -`FragmentExtensions`), all layout XML files, and Material Components (Views) if nothing still -needs it. +`recyclerview`, `recyclerview-selection`, `swiperefreshlayout`, `modernandroidpreferences`, +`preference`, `constraintlayout`, fastscroll, view binding and its helpers +(`FragmentViewBindingDelegate`, `FragmentExtensions`), `BasePGPActivity` and every layout XML +file but two. Kept: `oreo_autofill_dataset.xml` (fill suggestions are `RemoteViews`, drawn by the +system) and `git_credential_layout.xml` (the git credential prompt), with Material Components +(Views): the window themes, the git operations' dialogs and the snackbars still need it. Moving +those to Compose is a separate change. `fragment-ktx` stays declared as the version pin of +`androidx.fragment`. ## Security @@ -162,7 +166,9 @@ Work happens on a temporary `compose` branch, an explicit exception to the maste AGENTS.md (recorded there when the branch is created, removed at merge). `master` stays releasable for the monthly public suffix list update and hotfixes to 0.1.4. The branch is pushed to `origin` as a backup. CI is not extended to the branch: the local pre-commit check from -AGENTS.md runs before every commit, and CI runs at the merge. +AGENTS.md runs before every commit, and CI runs at the merge. `master` stays at its last release +until the maintainer has tested a release build of `compose` on the phone and says go; then +`compose` merges with one signed `--no-ff` merge commit and is deleted. ## Migration order @@ -190,4 +196,5 @@ graph through their old intents. (`scripts/e2e-env.sh`). - Before merge: the full device checklist from AGENTS.md (clone over SSH, PGP import, decrypt, TOTP, create and push, conflict recovery, hard reset, fingerprint unlock, Firefox autofill), - then the maintainer tests on the phone. + then the maintainer tests a release build of `compose` on the phone; the merge waits for the + maintainer's go. -- cgit v1.2.3