aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md54
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--docs/superpowers/plans/2026-11-04-compose-cleanup.md14
-rw-r--r--docs/superpowers/specs/2026-11-02-compose-ui-overhaul-design.md21
4 files changed, 63 insertions, 28 deletions
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 <pkg> sh -c '...'` needs the whole command quoted as one `adb` argument
+(`adb shell "run-as <pkg> 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 "<node [^>]*$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 '<node [^>]*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 "<node [^>]*$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.