Skip to content

feat(install): route the per-module install queue through InstallServ…#123

Merged
luisguzman-adfa merged 1 commit into
mainfrom
feat/ADFA-4476-modules-through-service
Jul 2, 2026
Merged

feat(install): route the per-module install queue through InstallServ…#123
luisguzman-adfa merged 1 commit into
mainfrom
feat/ADFA-4476-modules-through-service

Conversation

@luisguzman-adfa

Copy link
Copy Markdown
Collaborator

…ice (ADFA-4476 slice 3)

Slice 1 moved the module-grid selection to the Activity-scoped ViewModel; slice 2 moved the scratch reset into the service. This slice moves the last Fragment-bound long op -- the per-module install queue -- into the foreground InstallService, so the service (not the Fragment) is the single owner of the loop.

Root cause it closes: the queue loop lived in InstallController with a Fragment-scoped re-entry flag (installInFlight), and DeployFragment.onResume() re-fired processNextInQueue() after a recreation. A theme toggle mid-install recreated the Activity, installInFlight was born false, and onResume launched a SECOND concurrent runrole in the same rootfs -> corruption. The ADFA-4519 stop-gaps (installingModuleKeys guard + in-flight display) mitigated it; this removes the cause: the service keeps advancing the queue regardless of the Fragment lifecycle, and there is never more than one runrole in flight.

  • ModuleQueueState + ModuleQueueRepository: app-scoped observable of the queue (current module, remaining, failed set, seq), mirroring InstallProgressRepository. Kept separate from InstallState because the queue has a different shape; the two ops are mutually exclusive at runtime.
  • InstallService: new ACTION_START_MODULES (+ EXTRA_MODULES) owns the dequeue loop: ModuleName.isAllowed() fail-closed (D2), the sed/echo/runrole command, the AnsibleRunOutcome verdict (non-zero OR error-in-stdout), exit 137 logged as a phantom-kill, revert-on-fail, and finish/fail reporting. Queue is persisted to iiab_queue_prefs as it drains. Cancel routes through the existing ACTION_CANCEL.
  • InstallController: processNextInQueue()/installInFlight/revertModuleInLocalVars/ saveQueueToPrefs removed; the launch button now just starts the service (startModuleQueue). verifyInstallationState() reads in-flight state from ModuleQueueRepository, never the half-written local_vars.yml.
  • DeployFragment: onResume() no longer re-fires the queue; it observes ModuleQueueRepository (renderModuleQueue) for the launch button + one-shot finish/fail snackbar, and drops installed modules from the selection on DONE. isBatchInstalling() is now derived from the repository (covers a queue started before a recreation), so mutual exclusion holds.
  • Supersedes the ADFA-4458/4519 re-entry guard and the installingModuleKeys set (removed from InstallHost + DownloadStateViewModel). The ADFA-4519 showSnackbar() fix is kept and reused.
  • Test: AnsibleRunOutcomeTest gains the exit-137 (phantom-kill) case.

Stacks on ADFA-4519 (PR #120); rebases cleanly onto main once that merges.

…ice (ADFA-4476 slice 3)

Slice 1 moved the module-grid selection to the Activity-scoped ViewModel; slice 2
moved the scratch reset into the service. This slice moves the last Fragment-bound
long op -- the per-module install queue -- into the foreground InstallService, so
the service (not the Fragment) is the single owner of the loop.

Root cause it closes: the queue loop lived in InstallController with a
Fragment-scoped re-entry flag (installInFlight), and DeployFragment.onResume()
re-fired processNextInQueue() after a recreation. A theme toggle mid-install
recreated the Activity, installInFlight was born false, and onResume launched a
SECOND concurrent runrole in the same rootfs -> corruption. The ADFA-4519 stop-gaps
(installingModuleKeys guard + in-flight display) mitigated it; this removes the
cause: the service keeps advancing the queue regardless of the Fragment lifecycle,
and there is never more than one runrole in flight.

- ModuleQueueState + ModuleQueueRepository: app-scoped observable of the queue
  (current module, remaining, failed set, seq), mirroring InstallProgressRepository.
  Kept separate from InstallState because the queue has a different shape; the two
  ops are mutually exclusive at runtime.
- InstallService: new ACTION_START_MODULES (+ EXTRA_MODULES) owns the dequeue loop:
  ModuleName.isAllowed() fail-closed (D2), the sed/echo/runrole command, the
  AnsibleRunOutcome verdict (non-zero OR error-in-stdout), exit 137 logged as a
  phantom-kill, revert-on-fail, and finish/fail reporting. Queue is persisted to
  iiab_queue_prefs as it drains. Cancel routes through the existing ACTION_CANCEL.
- InstallController: processNextInQueue()/installInFlight/revertModuleInLocalVars/
  saveQueueToPrefs removed; the launch button now just starts the service
  (startModuleQueue). verifyInstallationState() reads in-flight state from
  ModuleQueueRepository, never the half-written local_vars.yml.
- DeployFragment: onResume() no longer re-fires the queue; it observes
  ModuleQueueRepository (renderModuleQueue) for the launch button + one-shot
  finish/fail snackbar, and drops installed modules from the selection on DONE.
  isBatchInstalling() is now derived from the repository (covers a queue started
  before a recreation), so mutual exclusion holds.
- Supersedes the ADFA-4458/4519 re-entry guard and the installingModuleKeys set
  (removed from InstallHost + DownloadStateViewModel). The ADFA-4519 showSnackbar()
  fix is kept and reused.
- Test: AnsibleRunOutcomeTest gains the exit-137 (phantom-kill) case.

Stacks on ADFA-4519 (PR #120); rebases cleanly onto main once that merges.
@luisguzman-adfa luisguzman-adfa force-pushed the feat/ADFA-4476-modules-through-service branch from e817b73 to d11394e Compare July 2, 2026 11:33
@luisguzman-adfa luisguzman-adfa marked this pull request as ready for review July 2, 2026 12:19
@luisguzman-adfa luisguzman-adfa merged commit e934a46 into main Jul 2, 2026
1 check passed
@luisguzman-adfa luisguzman-adfa deleted the feat/ADFA-4476-modules-through-service branch July 2, 2026 12:20
@luisguzman-adfa

Copy link
Copy Markdown
Collaborator Author

Validated on device — PR operates correctly and the theme-toggle regression is gone.

With a two-module queue, toggling the theme repeatedly mid-install now keeps a single ./runrole at a time (no more concurrent runs / corruption), the queue keeps advancing across the recreation without pausing, and the module grid shows honest in-progress state (no false "installed"). Reset (#122) and the rootfs install still work. Rebased onto current main and conflicts resolved — mergeable.

luisguzman-adfa added a commit that referenced this pull request Jul 3, 2026
…e-first (ADFA-4466)

Wires Firebase Analytics as the analytics sink and treats the device as constantly
online (the Firebase SDK buffers offline and flushes on reconnect). The shared
store-and-forward backbone (ADFA-4504) and the Cloudflare Worker are out of scope for
now; AnalyticsEvent/DeliveryManager stay in the tree as that parked backbone but are no
longer on the analytics path.

Foundation:
- build.gradle: google-services plugin 4.4.2 (root classpath) + firebase-bom 33.7.0 +
  firebase-analytics (Java API) in :app.
- AndroidManifest: disable Advertising ID / SSAID collection and ad personalization
  (K2Go may serve minors; operational analytics only).
- CI: inject GOOGLE_SERVICES_JSON into android-sanity-check and android-release-build so
  the google-services plugin has google-services.json at build time. .gitignore keeps a
  real one from being committed.

Client:
- AnalyticsClient routed to FirebaseAnalytics.logEvent, consent-gated: every method is a
  no-op unless the operator opted in, and setAnalyticsCollectionEnabled(consent) keeps the
  SDK from gathering anything while OFF. install_id + binaries_tag are anonymous user
  properties; device/OS/app-version/country are Firebase's own automatic dimensions.
- Existing events kept (first_run, app_opened, session) and now reach Firebase.
- Phase-1 operational API added: install_started/completed/failed, server_started/stopped
  (enum + bucketed params only, never raw messages or exact durations).
- install_started is wired at the rootfs-install kickoff. install_completed/failed and
  server_* call-sites are a small follow-up sequenced AFTER the in-flight InstallService
  PRs (#122 reset, #123 module queue) to keep this PR conflict-free and wire them at the
  correct op-aware points.
- Consent is applied immediately at startup and on the Setup / About toggles.

Tests:
- AnalyticsBuckets (pure) + AnalyticsBucketsTest (JVM) for uptime bucketing.
- AnalyticsConsentGateTest updated for the Firebase sink (consent default OFF; the gated
  emit path runs without throwing in both states — Firebase's buffer isn't introspectable
  on-device).

Open item for the team: confirm which Firebase project to use (reuse the existing K2Go
App Distribution project vs a dedicated telemetry project) and that it has a GA4 property
with org.iiab.controller registered; the GOOGLE_SERVICES_JSON secret must point at it.
luisguzman-adfa added a commit that referenced this pull request Jul 3, 2026
Now that ADFA-4476 (#122 reset, #123 module queue) is merged, the InstallService
terminal and the server-alive poll are stable, so the four Phase-1 events deferred
earlier are wired at their correct, operation-aware points. Phase 1 is complete: all
8 K2Go events emit (consent-gated, no-op unless opted in).

- install_completed: InstallService.finishSuccess(), gated to a real install
  (not reset/module mode), with tier + success=true.
- install_failed: the four install-pipeline failure sites, each with its phase
  (download/extract) and a coarse reason_code (network/no_archive/extract_error/
  exception) — never the raw message. Reset/module failures do not emit it.
- server_started / server_stopped: a single updateServerAlive() chokepoint in
  MainActivity fires exactly on the false<->true transition; server_stopped carries a
  coarse uptime bucket, never an exact duration.

Branch rebased onto current main (was cut from a stale #115 base).
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.

1 participant