Skip to content

Make scanner lifecycle explicit and production-safe - #32

Merged
mohdaquib merged 13 commits into
mohdaquib:mainfrom
rtsketo:main
Sep 1, 2026
Merged

mohdaquib merged 13 commits into
mohdaquib:mainfrom
rtsketo:main

Conversation

@rtsketo

@rtsketo rtsketo commented Aug 24, 2026

Copy link
Copy Markdown

Summary

  • add an explicit, main-thread toggleScanner(enabled) policy for trusted non-debuggable use
  • make explicit false override the debuggable default, remove every overlay, and block installation and scan APIs
  • keep uninstall() cleanup-safe and idempotent after disable
  • preserve automatic/manual routing, multi-resume handling, queued-uninstall protection, and safe built-in triggers
  • document AndroidX Startup timing, synchronous policy application, retained runtime tracking, and the zero-overhead manifest hard opt-out
  • add lifecycle and Robolectric regressions for disable, detach, routing cleanup, API guards, re-enable, and cleanup suppression

Scanner availability contract

State Debuggable app Non-debuggable app
No toggleScanner() call Enabled automatically Denied
toggleScanner(true) Enabled Enabled
toggleScanner(false) Disabled Disabled

toggleScanner(false) is a runtime control: it removes overlays and blocks scanner APIs while retaining lightweight AndroidX Startup callbacks and resumed-activity tracking so re-enable can install immediately. Variants requiring no scanner startup, metadata reads, lifecycle callbacks, or activity tracking can remove A11yScannerInitializer from the merged manifest as documented in the README.

Because AndroidX Startup runs before Application.onCreate, policies that default to disabled must call toggleScanner(false) synchronously from Application.onCreate before the first activity resumes. Asynchronous endpoint or remote-config resolution is explicitly discouraged.

Safety and compatibility

  • no existing public signature was removed or changed
  • toggleScanner(false) is an intentional behavior correction relative to fork release 2.1.0-rt.3: debug scanners now respect the explicit disable
  • uninstall() now always performs main-thread cleanup, even after scanner permission is disabled
  • positive allowlist and internal flavor/source-set guidance remains fail-closed

Validation

  • test
  • lint
  • createDebugUnitTestCoverageReport
  • coverage threshold check: 91.9% aggregate for scanner-core and scanner-rules (required 80%)
  • :sample:assembleRelease
  • dokkaGenerate
  • publishToMavenLocal with JITPACK_TAG=2.1.0-rt.4
  • generated POMs use 2.1.0-rt.4 for all modules and inter-module dependencies
  • git diff --check

Final isolated Gradle validation completed successfully: 458 actionable tasks.

Review feedback addressed

  • fail-closed trusted-build guidance now uses a positive allowlist and recommends internal variants/source sets
  • integration coverage exercises the real Activity/overlay/controller wiring, queued resume protection, manual routing fallback, trigger behavior, detach cleanup, debug disable, and cleanup-safe uninstall
  • runtime disable and manifest hard opt-out are now explicitly separated and documented

@rtsketo

rtsketo commented Aug 24, 2026

Copy link
Copy Markdown
Author

Follow-up fixes are pushed at 054e55c. This head restores manual-install routing, preserves surviving manual scanners, blocks queued resume callbacks after uninstall, gates trusted triggers in the documentation, and adds regression coverage. Validation passed: :scanner-ui:test, :scanner-ui:lint, :sample:assembleRelease, and git diff --check. Please re-review the latest head.

@mohdaquib mohdaquib left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the latest PR head (63f521f). The manual-install fallback and queued-resume/uninstall race identified in the earlier review are now addressed by selectEntry() and the pending/suppressed lifecycle state.

Two remaining recommendations:

  1. Medium — make trusted-build gating fail closed. The README example endpoint != PROD enables the scanner for unknown, uninitialized, or newly introduced endpoint values. Since this path ships scanner code in a non-debuggable app, prefer a positive allowlist (for example, an internal build flavor and an explicitly trusted endpoint). Where possible, recommend internalImplementation / src/internal instead of broad implementation / src/main placement.

  2. Test coverage — add integration coverage around the helper. The new unit tests validate ScannerLifecycle and selectEntry() in isolation, but an Activity/Robolectric or instrumentation test should verify that a posted resume cannot recreate a detached overlay and that manual install() remains the target of triggerScan() / notifyScreenChanged() after automatic entries pause or are removed.

@rtsketo

rtsketo commented Aug 24, 2026

Copy link
Copy Markdown
Author

Final two Medium findings are fixed at fd33739: repeated manual install is again a true no-op, and built-in long-press/shake callbacks atomically no-op when scanner permission has been disabled. Tests, lint, release assembly, and diff checks pass. Please review the latest head.

@mohdaquib mohdaquib left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up on the latest head (fd33739): the repeated-install routing fix and the new disabled-trigger no-op behavior look valid. Two earlier review requests remain unresolved:

  1. Medium — use fail-closed trusted-build gating. README.md still shows toggleScanner(enabled = endpoint != PROD). Unknown, uninitialized, or newly added endpoint values therefore enable the scanner. Please use a positive allowlist and prefer an internal flavor/source set, for example internalImplementation with src/internal, rather than broadly recommending implementation / src/main.

  2. Test coverage — add integration-level lifecycle/routing coverage. The existing tests exercise ScannerLifecycle and selectEntry() in isolation, but do not verify the actual Activity/overlay/controller wiring. Please cover queued resume after explicit uninstall, manual routing through triggerScan() / notifyScreenChanged(), automatic-to-manual fallback, and detach cleanup. The latest commit also needs regression tests proving repeated install() does not steal active routing and default long-press/shake triggers quietly no-op after disable or before installation.

@rtsketo rtsketo changed the title Allow explicit scanner use in production builds Make scanner lifecycle explicit and production-safe Aug 27, 2026
@rtsketo

rtsketo commented Aug 27, 2026

Copy link
Copy Markdown
Author

@mohdaquib Explicit follow-up to your review #32 (review):

  • Fail-closed gating: resolved. The README now requires a positive allowlist, recommends internal flavor/source-set placement, and warns against asynchronous policy resolution.
  • Integration coverage: resolved. Robolectric coverage now exercises queued resume after uninstall, automatic/manual routing and fallback, detach cleanup, disabled trigger behavior, overlay removal, active-route clearing, public API rejection, and cleanup-safe uninstall.
  • Startup clarity: the README and KDoc now distinguish runtime disable from the manifest hard opt-out that removes all scanner startup and Activity callback work.

The latest head is 5e62f9a. Full tests, lint, coverage threshold, release assembly, Dokka, local Maven publication for 2.1.0-rt.4, and diff checks pass. Please re-review the latest head.

@rtsketo

rtsketo commented Aug 27, 2026

Copy link
Copy Markdown
Author

@mohdaquib Explicit follow-up to your review #32 (review):

Both remaining requests are addressed on the current head:

  1. Trusted-build guidance is fail-closed, uses positive allowlisting, and prefers internal variants/source sets.
  2. Integration tests cover actual Activity/overlay/controller behavior for queued resume suppression, routing fallback, repeated install stability, detach cleanup, built-in trigger no-op behavior, explicit debug disable, and post-disable cleanup.

The final behavioral contract is now documented in the PR body and README: untouched policy follows build defaults, true enables all builds, false disables all builds, while manifest removal is the zero-overhead hard opt-out. Latest validated commit: 5e62f9a. Please re-review when convenient.

@mohdaquib
mohdaquib merged commit 71ad69c into mohdaquib:main Sep 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants