Complete Android adaptive and accessibility hardening (#105)
This commit is contained in:
1 parent
f4a6edbc68
commit
45d78aa5b3
33 files changed
+4596
-1376
No files matched your search
+170
-89
@@ -1,108 +1,189 @@
|
||||
# Android hardening
|
||||
|
||||
The Android presentation layer uses resource strings for Kotlin-owned notices,
|
||||
action labels, counts, and durations. Rust-owned presentation text remains opaque:
|
||||
frontends do not parse or translate it. Repeated editor actions include the field
|
||||
label, and hidden editor values are checked against the actual Android accessibility
|
||||
node tree as well as Compose semantics. Compose accessibility checks run before
|
||||
interactions in every app screen test.
|
||||
Kotlin-owned notices, action labels, counts and durations use Android resources.
|
||||
Rust-owned presentation text stays opaque; frontends do not parse or translate it.
|
||||
Screen tests run Compose accessibility checks, including the finished mutation,
|
||||
camera, provider and TOTP screens. Hidden editor/viewer values are also checked
|
||||
against Android's actual accessibility node tree. Explicit buttons and semantic
|
||||
actions provide alternatives to swipe, reorder and pull-to-refresh gestures.
|
||||
Consent and selection rows have at least 48 dp touch targets.
|
||||
|
||||
Blank name searches do not traverse the password store. QR luminance inputs reject
|
||||
zero or greater-than-4096 dimensions before allocating the decoder image; owned
|
||||
decoded luminance bytes are zeroized. License loading runs on the IO dispatcher.
|
||||
Editor keystrokes coalesce encrypted recovery checkpoints for 150 ms; save,
|
||||
reorder, and recovery wait for the latest checkpoint. This changes persistence
|
||||
scheduling, not authorization or the interactive unlock boundary.
|
||||
The shell, provider selection and all modal dialogs avoid separating/occluding
|
||||
hinges. Book posture uses the largest unobstructed pane; tabletop uses the upper
|
||||
pane. Bounds are in window coordinates, offsets remain correct in RTL, and insets
|
||||
already removed by the pane are consumed before applying system-bar/IME padding.
|
||||
Dialog titles, forms and actions can scroll at large font sizes; form state stays
|
||||
composed while scrolling. Regressions exercise live flat/book/tabletop changes,
|
||||
preserve an entered commit message, check action bounds against the hinge, and
|
||||
reach every generator control at 200% text before submitting its entered length.
|
||||
|
||||
Blank name searches do not traverse the store. QR luminance inputs reject zero or
|
||||
greater-than-4096 dimensions before allocation and owned decoded bytes are
|
||||
zeroized. License loading runs on IO. Editor input coalesces encrypted recovery
|
||||
checkpoints for 150 ms; save/reorder/recovery wait for the latest checkpoint.
|
||||
Interactive authorization remains in Rust and is never bypassed to hide latency.
|
||||
|
||||
Foreground cleanup synchronously masks protected surfaces and clears registered
|
||||
inputs. The parent composition observes the cleanup generation before rebuilding
|
||||
content; its reveal effect checks the latest generation again before exposing the
|
||||
clean foreground. This fixes a permanent black surface after failed authentication
|
||||
and tab navigation. The regression was observed failing before the fix, and
|
||||
computer use verified failed unlock followed by usable Search in the final build.
|
||||
|
||||
## Reproducible gates
|
||||
|
||||
From the repository root, run the four required Cargo checks and:
|
||||
|
||||
```sh
|
||||
CARGO=/opt/homebrew/opt/rustup/bin/cargo python3 .gitea/scripts/android_ci.py test
|
||||
CARGO=/opt/homebrew/opt/rustup/bin/cargo python3 .gitea/scripts/android_ci.py build
|
||||
CARGO=/opt/homebrew/opt/rustup/bin/cargo python3 .gitea/scripts/android_ci.py emulator <serial>
|
||||
CARGO=/opt/homebrew/opt/rustup/bin/cargo python3 .gitea/scripts/android_ci.py performance <serial>
|
||||
```
|
||||
|
||||
The emulator gates check ARM ABI, emulator identity, unlocked credential storage,
|
||||
actual keyguard state and wakefulness before installing or invoking tests. Run
|
||||
one emulator suite at a time. It reports completion only after the runner returns
|
||||
success. The completed harness and target processes are stopped even on failure
|
||||
or timeout, retaining application data. ADB calls have a 15-minute ceiling.
|
||||
Platform coverage includes real CameraX frames, background/resume and
|
||||
Keystore round-trip/tamper behavior, including rejection of an unsupported
|
||||
unlocked-device key flag on API 26. Temporary test camera permission is restored.
|
||||
|
||||
Preserve each AVD and install updates over the same package. After a crash, stop
|
||||
the affected instrumentation, cold boot the same AVD without wiping data, unlock
|
||||
it and verify the visible app before repeating validation. A runner process alone
|
||||
is not evidence of progress. The 2026-10-15 results below use fresh post-restart
|
||||
runs; preliminary results from before the emulator restart are excluded.
|
||||
|
||||
## Native journey budgets
|
||||
|
||||
The `android_ci.py emulator <serial>` gate requires exactly one numeric measurement
|
||||
for each journey below and rejects missing, duplicate, malformed, or over-budget
|
||||
values. Raw fixture output remains local; published reports contain numeric values
|
||||
and test status only. The large-tree fixture contains 1,000 valid encrypted entries
|
||||
in 50 folders. These timings measure native/repository work, not rendered frame
|
||||
latency or a physical device.
|
||||
The gate rejects missing, duplicate, malformed or over-budget measurements. Raw
|
||||
fixture output stays local; published reports retain reviewed numbers and status.
|
||||
The tree fixture contains 10,000 encrypted files in 50 folders, replicated from
|
||||
Rust-produced public fixture ciphertext. Cached
|
||||
TOTP/provider preparation uses 100 catalogued encrypted entries while locked,
|
||||
with 10 warmups and 30 samples per p95 measurement. Authenticated fill separately
|
||||
uses real Rust/GPG decryption and explicit consent.
|
||||
|
||||
| Journey | Required budget | API 26 ARM64 / 4 KB | API 37 ARM64 / strict 16 KB |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 100 editor input updates | < 500 ms | 4 ms | 6 ms |
|
||||
| Editor update p95 | < 32,000 μs | 61 μs | 102 μs |
|
||||
| TOTP discovery cache rebuild | < 15,000 ms | 1,924 ms | 1,861 ms |
|
||||
| QR image analysis | < 5,000 ms | 99 ms | 91 ms |
|
||||
| 1,000-entry tree navigation | < 3,000 ms | 11 ms | 11 ms |
|
||||
| 1,000-entry name search | < 5,000 ms | 27 ms | 26 ms |
|
||||
| Native journey | Budget | API 26 / 4 KB | API 37 / 4 KB | API 37 / strict 16 KB |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 100 editor input updates | < 500 ms | 4 ms | 6 ms | 12 ms |
|
||||
| Editor update p95 | < 32,000 us | 44 us | 70 us | 231 us |
|
||||
| TOTP discovery cache rebuild | < 15,000 ms | 1,831 ms | 1,892 ms | 2,516 ms |
|
||||
| QR image analysis | < 5,000 ms | 86 ms | 93 ms | 122 ms |
|
||||
| 10,000-entry tree navigation | < 3,000 ms | 75 ms | 95 ms | 121 ms |
|
||||
| 10,000-entry name search | < 5,000 ms | 228 ms | 281 ms | 355 ms |
|
||||
| Cached TOTP open p95 | < 1,000,000 us | 5,059 us | 8,574 us | 5,322 us |
|
||||
| Warm TOTP refresh p95 | < 3,000,000 us | 7,353 us | 10,509 us | 7,890 us |
|
||||
| Locked provider preparation p95 | < 1,000,000 us | 3,775 us | 6,607 us | 4,155 us |
|
||||
| Authenticated provider fill | < 5,000 ms | 1,212 ms | 1,278 ms | 1,247 ms |
|
||||
|
||||
Both configurations reported `OK (31 tests)` for app instrumentation and passed
|
||||
the native core journey and bridge suite. Opt-in inspection and biometric tests
|
||||
remain assumption-skipped unless explicitly requested; these runs do not establish
|
||||
their manual coverage. Native load samples ranged from 32.7–35.2 ms on API 26 and
|
||||
63.3–78.7 ms on API 37 with strict 16 KB pages. Five debug cold starts ranged from
|
||||
202–279 ms and 708–1,086 ms respectively. These debug activity timings are separate
|
||||
from release Macrobenchmark startup measurements.
|
||||
All three configurations passed app/core/bridge/platform instrumentation and five
|
||||
real debug cold starts after the emulator restart. Strict 16 KB validation disabled
|
||||
the bionic/PackageManager 4 KB application compatibility path. Its five native-load
|
||||
samples were 66.4-89.7 ms and debug activity starts were 836-932 ms.
|
||||
|
||||
## Adaptive checks
|
||||
These are native/repository timings on emulators, not rendered-frame latency or
|
||||
physical-device performance. The API 37 / 4 KB authenticated-fill figure comes
|
||||
from a separate targeted native test after its full suite. Opt-in inspection and
|
||||
biometric tests remain assumption-skipped during ordinary instrumentation; their
|
||||
presence in the runner's reported test count does not establish manual coverage.
|
||||
After the foreground-cleanup fix, the final app suite was repeated on all three
|
||||
configurations: each reported 53 tests with no failures.
|
||||
|
||||
`Tier2WindowTest` exercises searchable Preferences and keyboard activation using
|
||||
actual window dimensions. The API 26 emulator passed compact (411 dp), medium
|
||||
(640 dp), expanded (1,067 dp), 200% font with RTL, landscape with 200% font and RTL,
|
||||
and a smaller window with 200% font. Size, density, font scale, and rotation settings
|
||||
were restored after the run. This covers window geometry; it does not establish
|
||||
hinge occlusion or folded/tabletop posture behavior.
|
||||
## Adaptive and environment checks
|
||||
|
||||
The shell consumes IME insets, and the activity explicitly requests `adjustResize`.
|
||||
The optional `hardeningIme=true` instrumentation argument opens a real keyboard
|
||||
and checks shell content bounds against the platform IME inset. It reproduced an
|
||||
overlap before the shared-shell fix and passes afterward on API 26 and API 37 with
|
||||
strict 16 KB pages. Temporarily enable `show_ime_with_hard_keyboard` for this check
|
||||
and restore its original value afterward. Full app instrumentation also reported
|
||||
`OK (31 tests)` with `hardeningRtl=true` on the strict 16 KB image.
|
||||
On 2026-10-15 API 26 passed actual compact (411 dp), medium (640 dp), expanded
|
||||
(1,067 dp), 200% text with RTL, landscape with 200% text and RTL, and smaller-window
|
||||
checks. The real keyboard opened and shell bounds ended above its IME inset.
|
||||
Live resize from 640 to 450 dp preserved a query entered into the actual Rust
|
||||
search repository across activity recreation, followed by layout/keyboard checks.
|
||||
All size, density, font, rotation and IME settings were restored.
|
||||
|
||||
## Release benchmarks
|
||||
The Pixel Fold AVD passed unfolded, half-open book, half-open tabletop, folded
|
||||
cover and tabletop with a real IME. The controller waits for the active inner/cover
|
||||
display before setting rotation, then verifies the committed posture and rotation;
|
||||
issuing rotation during the cover-to-inner switch previously left the keyboard
|
||||
case on the wrong layout. Computer use also inspected the actual setup/key-selection
|
||||
screens in tabletop posture. Original posture, physical rotation and IME settings
|
||||
were restored.
|
||||
|
||||
`android_ci.py performance <serial>` installs the benchmark app over the retained
|
||||
installation and records five cold and five warm startup iterations. The benchmark
|
||||
variant uses release code and shrinking, debug signing, and shell profiling; the
|
||||
distributed release variant does not enable profiling. `ReportDrawnWhen` waits for
|
||||
the shell to leave its native loading state. Compilation is preserved rather than
|
||||
resetting application data.
|
||||
The available ARM images do not advertise freeform/desktop support. An explicit
|
||||
freeform launch on the foldable was accepted but fell back to fullscreen; it is
|
||||
not counted as desktop validation. Actual split-screen, live resize and expanded
|
||||
window geometry are covered. Desktop-specific system decorations remain an image
|
||||
availability limit; no x86_64 or physical-device coverage is implied.
|
||||
|
||||
The startup gate requires five complete numeric initial/full-display samples for
|
||||
each mode, with cold startup below 5,000 ms and warm startup below 3,000 ms.
|
||||
Before installing the generated profile, release-like median full-display times
|
||||
were 268.6 ms cold / 131.2 ms warm on API 26 and 1,203.5 ms cold / 427.0 ms warm on
|
||||
API 37. These emulator observations preserve existing compilation state and are
|
||||
not a claim of profile speedup. Raw traces and device metadata stay local; CI
|
||||
artifacts retain only API/ABI and the reviewed numeric samples.
|
||||
The old API 26 Pixel Launcher crashed during a tablet-size transition. The blocked
|
||||
attempt was stopped and its display overrides restored. Remaining cases passed
|
||||
with Settings in front during transitions, avoiding that launcher defect. An
|
||||
actual split-screen smoke check also showed IronStorage alongside Settings;
|
||||
setup controls remained reachable by scrolling and fullscreen was restored.
|
||||
|
||||
`android_ci.py profiles <serial>` generates a profile from startup and the existing
|
||||
Preferences journey. Generation requires API 34 or newer so compilation reset
|
||||
does not fall back to reinstalling the app on older non-rooted images. Inspect the
|
||||
generated local artifacts before promoting them into the app source set. Generation
|
||||
uses a non-obfuscated `profile` variant, while startup measurements use the shrunk
|
||||
`benchmark` variant. Both use release source code and debug signing.
|
||||
The current phone passed the full app suite with 200% system text, RTL, high text
|
||||
contrast, grayscale and all three animation scales at zero. A focused follow-up
|
||||
also covered the final scrolling dialogs and tabletop generator. The native
|
||||
cached-catalog/provider fixture passed under forced deep doze (still IDLE after
|
||||
the run), battery saver, disabled Wi-Fi/mobile data, and Android's forced-low
|
||||
storage notification. A separate native cached-catalog/provider run passed on connected metered Wi-Fi;
|
||||
Settings showed Metered and the active Wi-Fi network lacked NOT_METERED. Automatic
|
||||
network usage was restored afterward. These network checks exercise local cached
|
||||
work, not an unconfigured remote synchronization service.
|
||||
Every modified setting was restored. This is storage-low
|
||||
notification coverage, not a destructive attempt to fill the emulator filesystem.
|
||||
Rust transaction/checkpoint tests separately cover failing writes and rollback.
|
||||
App tests cover memory-pressure masking, saved-state redaction and recreation;
|
||||
five real process cold starts additionally exercise retained app storage.
|
||||
|
||||
The committed `app/src/main/baseline-prof.txt` was generated on the API 37 ARM64
|
||||
emulator from the startup/Preferences journey on 2026-10-11, with 22,331 rules and
|
||||
SHA-256 `687afdc199dacf0736dc1c577767dea9b81ddc987b5b61b02b5afed5e501b503`.
|
||||
Regenerate it with the `profiles` command after changing those journeys; use the
|
||||
untimestamped `BaselineProfile_startupAndPreferences-baseline-prof.txt` output,
|
||||
check that application class names are unobfuscated, and rebuild the release APK
|
||||
to verify its compiled `assets/dexopt/baseline.prof` remains within ART's size limit.
|
||||
Computer use authenticated and decrypted the retained public provider fixture on
|
||||
strict-16-KB API 37 using its source-defined passphrase. Its six Git commits and
|
||||
clean worktree survived install-over updates. The password stayed Hidden with
|
||||
explicit Reveal/Copy/Edit/Lock controls. TalkBack was enabled through Android
|
||||
Studio and verified as a bound service with touch exploration active. Computer
|
||||
use focused the native Hidden value and activated/cancelled the native editor
|
||||
through TalkBack. This is separate from Compose checks, whose UI automation
|
||||
normally suppresses accessibility services. Spoken audio was not verified.
|
||||
|
||||
## Completion boundary
|
||||
Switch Access was enabled through its launcher and Android Settings. Its USB
|
||||
setup reported no connected device; manual Next assignment did not capture the
|
||||
emulator's volume keys or forwarded host keyboard. No scanning result is claimed.
|
||||
This is a limit of the current emulator/input setup; keyboard/D-pad and semantic-action
|
||||
coverage remain in the presentation suite. TalkBack and Switch Access were
|
||||
restored to off, and no switch assignments were created.
|
||||
|
||||
Issue #105 explicitly depends on every Android feature UI issue. CameraX (#106),
|
||||
TOTP list/detail (#107), mutation workflows (#108), Credential Manager/Autofill
|
||||
(#109), and TOTP discovery UI (#110) are still open. Their final screen accessibility,
|
||||
camera interruption, cached-open/refresh presentation, and provider response
|
||||
latency cannot be certified by tests of the existing screens. Keep #105 open until
|
||||
those implementations and the remaining environment/posture checks are complete.
|
||||
ARMv7 is package-validated; the installed runnable emulator images are ARM64.
|
||||
The other retained phone vault rejected its historical public test passphrase;
|
||||
that journey is not a successful manual unlock. Rejection and subsequent Search
|
||||
navigation did verify the foreground-cleanup fix. No credential or vault reset
|
||||
was performed. The separately source-defined provider fixture above supplies the
|
||||
successful native decryption journey.
|
||||
|
||||
On 2026-10-11 the user explicitly requested committing the independent implementation
|
||||
now and delaying final coverage. Issues #106–#110 received `critical`; resume those
|
||||
eligible Android enhancements in numeric order, then revisit #105. Remaining
|
||||
checks include TalkBack/Switch Access and contrast/motion modes across the finished
|
||||
screens, real hinge/posture and live-resize coverage, and low-memory, doze, battery,
|
||||
network, camera-interruption, and storage-pressure scenarios. Do not close #105
|
||||
based solely on the current instrumentation and window-geometry results.
|
||||
## Release startup and profiles
|
||||
|
||||
`performance` installs the shrunk benchmark variant over the retained app. It uses
|
||||
release source, debug signing and shell profiling; distributed release builds do
|
||||
not enable profiling. `ReportDrawnWhen` waits for native shell loading to finish.
|
||||
CompilationMode.Ignore preserves compilation and application data. Five complete
|
||||
initial/full-display samples are required for each startup mode; every cold sample
|
||||
must be below 5,000 ms and every warm sample below 3,000 ms.
|
||||
|
||||
The fresh API 26 full-display samples were 191.0-216.4 ms cold and 56.5-84.2 ms
|
||||
warm. The current strict-16-KB API 37 samples were 246.4-475.9 ms cold and
|
||||
78.5-139.3 ms warm. These are observations with retained compilation, not a profile-speedup
|
||||
comparison.
|
||||
|
||||
`profiles <serial>` requires API 34 or newer, avoiding the older non-rooted
|
||||
benchmark library fallback that can reinstall the app. The unshrunk profile
|
||||
variant records startup, searchable Preferences, Passwords, Search and cached
|
||||
TOTP navigation. Inspect generated application names before promoting the
|
||||
untimestamped `BaselineProfile_startupAndPreferences-baseline-prof.txt`; rebuild
|
||||
the release APK and verify compiled `assets/dexopt/baseline.prof` size afterward.
|
||||
|
||||
The committed profile was regenerated on API 37 ARM64 / strict 16 KB on
|
||||
2026-10-15. It has 24,647 rules, including 2,826 application rules, and SHA-256
|
||||
`947599f29c326a02a55bab8f12ac5cff3743560319993424667ec864fc862929`.
|
||||
The release APK contains an 10,148-byte `baseline.prof` and 1,376-byte
|
||||
`baseline.profm`, both checked after the final package build.
|
||||
|
||||
ARMv7 is compiled/package-audited. Available runnable images are ARM64; x86_64
|
||||
and physical-device validation are outside the user-directed milestone scope.
|
||||
Reference in new issue
Block a user