-
-
Notifications
You must be signed in to change notification settings - Fork 49
CCCT-2520 Start-Of-App Routing And Back Navigation Spec #3844
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
conroy-ricketts
wants to merge
18
commits into
master
Choose a base branch
from
CCCT-2520-app-navigation-changes-spec
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
0b6222c
CCCT-2520 App Navigation Changes Spec
conroy-ricketts 7d79af6
CCCT-2520 App Navigation Changes Spec
conroy-ricketts c946a4d
CCCT-2520 App Navigation Changes Spec
conroy-ricketts eb53ef2
CCCT-2520 App Navigation Changes Spec
conroy-ricketts 303439a
CCCT-2520 App Navigation Changes Spec
conroy-ricketts 5a9a52d
CCCT-2520 App Navigation Changes Spec
conroy-ricketts f2f50d1
CCCT-2520 App Navigation Changes Spec
conroy-ricketts 02aec83
CCCT-2520 App Navigation Changes Spec
conroy-ricketts ee38122
Merge branch 'master' of github.com:dimagi/commcare-android into CCCT…
conroy-ricketts 5de361e
Merge branch 'master' of github.com:dimagi/commcare-android into CCCT…
conroy-ricketts b094529
CCCT-2520 App Navigation Changes Spec
conroy-ricketts 0008ebd
CCCT-2520 App Navigation Changes Spec
conroy-ricketts dc8d7fa
CCCT-2520 App Navigation Changes Spec
conroy-ricketts 9f8cf77
CCCT-2520 App Navigation Changes Spec
conroy-ricketts 5dc5ae5
CCCT-2520 App Navigation Changes Spec
conroy-ricketts f8706c9
CCCT-2520 App Navigation Changes Spec
conroy-ricketts fb9f52b
Merge remote-tracking branch 'origin/master' into CCCT-2520-app-navig…
conroy-ricketts 28cef7d
CCCT-2520 App Navigation Changes Spec
conroy-ricketts File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
160 changes: 160 additions & 0 deletions
160
docs/superpowers/specs/2026-07-23-app-navigation-changes-design.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| # App Navigation — Technical Spec (Start-of-App Routing & Back Navigation) | ||
|
|
||
| > **Important: Read [this tab of the design doc](https://docs.google.com/document/d/1rntc16FW2Jfr6CbzNcZ9RDOxIRrmfcWNU6_d5WL1rA8/edit?tab=t.ongpgraabwyz) first.** | ||
| > | ||
| > It owns **all user-facing flow charts and visuals.** | ||
|
|
||
| **Prerequisite:** the Opportunity Home composition work, which provides the opportunity screen that can run a CommCare app inside itself instead of launching a separate one. The inline silent-login hand-off returns a finished login to that already-open screen; this spec depends on it. | ||
|
|
||
| ## Scope | ||
|
|
||
| **1. `DispatchActivity` becomes a one-time router.** It stays the single startup router — no second routing class — modified to decide **once per cold start** rather than re-evaluating in `onResume`, and to add a Connect-home branch. The decision logic is extracted into a testable pure helper it calls. | ||
|
|
||
| **2. The screen the router opens becomes the task root.** The sign-in, account and Connect list screens are consolidated into one navigation surface (the "shell"); the CommCare app runtime stays a separately launched screen. Because nothing sits beneath the landing screen, back exits from it as ordinary platform behaviour — no per-case activity flags. The exception is a deep link that opens something nested, where the screens it sits under are seeded first. | ||
|
|
||
| ## Routing | ||
|
|
||
| ```mermaid | ||
| flowchart TD | ||
| E([App entry]) --> I{"Intent-driven?<br/>ACTION_VIEW install · KEY_REQUIRE_REFRESH<br/>deep link · push"} | ||
| I -->|Yes| Target[Target from the intent] --> Need{"PersonalID credentials<br/>needed for this branch?"} | ||
| I -->|No| S{Active session?} | ||
| S -->|Yes| Prov["Resume using the stored record of how<br/>the session was created, not evaluateAppState"] --> Need | ||
| S -->|No| Seat{An app already seated?} | ||
| Seat -->|Yes| SignIn["Sign in for that app,<br/>not the list to re-pick"] --> Need | ||
| Seat -->|No| Config[Decide from what the user has set up] --> Need | ||
| Need -->|No| Open["Open the target as the task root,<br/>seeding any screens it sits under"] | ||
| Need -->|Yes| Unlock{Unlock} | ||
| Unlock -->|Success| Open | ||
| Unlock -->|Cancel or fail| Apps{CommCare apps installed?} | ||
| Apps -->|Yes| Login[Login page] | ||
| Apps -->|No| Intro[Intro page] | ||
| ``` | ||
|
|
||
| **Which home an active session resumes to** depends on how that session was created. A session signed in through PersonalID, on an app tied to an opportunity, resumes Opportunity Home; a session signed in with a username and password, or on an app with no opportunity, resumes the CommCare app home. | ||
|
|
||
| The unlock gate belongs to the router, so no entry point can bypass it — `ConnectUnlockFragment` stops being the startup host. Entries from inside the app, after the session has already expired, keep today's per-call helpers; north-star collapses both into the one gate. | ||
|
|
||
| ## The back path | ||
|
|
||
| | Entry | Task stack, bottom → top | | ||
| |---|----------------------------------------------------------| | ||
| | Cold start, Connect user with a current opportunity | Opp Home | | ||
| | Cold start, Connect user with no current opportunity | Opp List | | ||
| | Cold start, traditional CommCare or PersonalID user | App Home (Login page when there is no session to resume) | | ||
| | Notification into a chat | Opp Home → Messaging channel list → Chat | | ||
| | Sidebar section opened from inside an opportunity | Opp Home → Section | | ||
| | A second sidebar section opened after the first | Opp Home → New section (the first section is closed) | | ||
|
|
||
| The stack therefore never holds more than three screens. | ||
|
|
||
| Screens are seeded only when the app opens the user below their home. Ordinary in-app navigation already leaves real history to retrace: a chat opened from a task on Opportunity Home has history, so back returns there. | ||
|
|
||
| Seeding spans activities, not nav graphs — Messaging is a separate activity with its own `NavHost` and Opportunity Home is in another, so the path is a task stack. `NavDeepLinkBuilder` is not used: it synthesizes only within one graph, and it bypasses the unlock gate. | ||
|
|
||
| ## The app-bar slot | ||
|
|
||
| A **back-swipe gesture** needs no separate rule, but note that on sidebar screens `DrawerLayout` automatically claims the left edge, so a left-edge swipe opens the sidebar while a right-edge swipe goes back. | ||
|
|
||
| ## Sidebar | ||
|
|
||
| **The Connect screens have no sidebar today.** Opportunity List, Opportunity Home, Messaging and Work History all need drawer support added. | ||
|
|
||
| ## Tabs | ||
|
|
||
| The selected Delivery tab is an **argument** to the destination rather than a destination of its own, so a deep link can open a specific tab. | ||
|
|
||
| ## Session lifecycle | ||
|
|
||
| - **Expiry:** Standard Home → CommCare Apps list; Opportunity Home → silent re-login, foreground-visible even when resumed from background. Neither relies on `DispatchActivity` re-dispatch. | ||
| - **Forget PersonalID:** already closes the user session — it additionally needs to **re-run the startup router** rather than hardcoding a destination. | ||
|
|
||
| ## Analytics | ||
|
|
||
| **Which control the user pressed** — app-bar arrow vs. system back. Android does not distinguish a back *gesture* from the back *button* (both arrive through the same callback), so they cannot be reported separately. | ||
|
|
||
| ## Interim vs. north-star | ||
|
|
||
| - **North-star:** a single `NavHost` shell; the landing screen is the graph's start destination. | ||
| - **Interim:** on today's activities the router makes the landing screen the task root. That removes the need for `appLaunchedFromConnect` / `finishAffinity`, which existed only because something else sat beneath the launch screen and had to be suppressed. `REORDER_TO_FRONT` stays for reusing a running home. `DispatchActivity` stays the single router and continues to own the corrupted-database and recovery paths, plus launches that come from outside the app. | ||
|
|
||
| ## Testing | ||
|
|
||
| - **Router** — a pure function of its inputs, so unit-test every landing outcome, the case where an active session skips the rest of the decision, and the cases where the last-used opportunity has ended or no longer exists. | ||
| - **Back navigation** — Robolectric regression tests over the back-path table above (assert the stack, not internals): back exits from each persona's landing screen, a sidebar section returns to the workspace, two sidebar sections in succession, and a notification into a nested screen walks down rather than exiting. | ||
| - **Startup boundaries** — external `ACTION_VIEW` install, verification refresh, cold-start unlock cancellation with and without apps installed, backgrounded session expiry, and Forget-PersonalID with an active session. | ||
|
|
||
| --- | ||
|
|
||
| ## Implementation notes (may be skipped when reviewing) | ||
|
|
||
| ***For the implementer. This adds no reviewer-facing behavior beyond the sections above. Pinned links are to `master` @ `dc7697645`; unpinned `Class:line` references are to `master` @ `aef4da2c0`.*** | ||
|
|
||
| ### Router inputs → source | ||
|
|
||
| | Input | Source | | ||
| |---|---| | ||
| | Active session? (~24h) | [`CommCareApplication.getSession().isActive()`](https://github.com/dimagi/commcare-android/blob/dc7697645fefd99de4e234be569bd8447fb6e0ba/app/src/org/commcare/CommCareApplication.java#L969) | | ||
| | Seated-app linkage | [`PersonalIdManager.evaluateAppState`](https://github.com/dimagi/commcare-android/blob/dc7697645fefd99de4e234be569bd8447fb6e0ba/app/src/org/commcare/connect/PersonalIdManager.java#L520) / [`ConnectJobHelper.getJobForSeatedApp`](https://github.com/dimagi/commcare-android/blob/dc7697645fefd99de4e234be569bd8447fb6e0ba/app/src/org/commcare/connect/ConnectJobHelper.kt#L23) | | ||
| | PersonalID status | [`PersonalIdManager.isloggedIn()`](https://github.com/dimagi/commcare-android/blob/dc7697645fefd99de4e234be569bd8447fb6e0ba/app/src/org/commcare/connect/PersonalIdManager.java#L149) | | ||
| | Connect access + opportunities | [`ConnectUserDatabaseUtil.hasConnectAccess()`](https://github.com/dimagi/commcare-android/blob/dc7697645fefd99de4e234be569bd8447fb6e0ba/app/src/org/commcare/connect/database/ConnectUserDatabaseUtil.java#L44) + opportunity records | | ||
| | Installed apps | [`MultipleAppsUtil.usableAppsPresent()`](https://github.com/dimagi/commcare-android/blob/dc7697645fefd99de4e234be569bd8447fb6e0ba/app/src/org/commcare/utils/MultipleAppsUtil.java#L50) | | ||
| | Last-accessed opportunity / last session context | new persistence (below) | | ||
|
|
||
| ### Making the landing screen the task root | ||
|
|
||
| Launch it with `FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK` so nothing from the routing sequence is left beneath it. `LoginActivity` already removes itself on success (`setResult(RESULT_OK); finish()` at `:425`, `:433`, `:503`, `:624`), so no change is needed there — do not make it stay in the stack. | ||
|
|
||
| Deep links are the only case that needs seeding: build the path in one call with `Activity.startActivities(Intent[])`, or `TaskStackBuilder` when the entry point is a notification `PendingIntent`, so the intermediate activities exist before the target is shown. Seed only when the task is being created; an existing task is left alone. | ||
|
|
||
| **Sidebar sections replacing each other.** Launching a section from the drawer finishes the section activity currently on top (if any) before starting the new one, rather than relying on intent flags — `FLAG_ACTIVITY_CLEAR_TOP` clears *ancestors*, not siblings, so it will not pop Messaging when launching Work History. In the shell this becomes `popUpTo(<workspace destination>)` followed by `navigate`, and the depth bound falls out of the graph rather than needing enforcement. | ||
|
|
||
| ### Slot enforcement | ||
|
|
||
| Interim: `BaseDrawerActivity` gains a declared slot per screen (sidebar / back / none) and configures the toggle and `setDisplayHomeAsUpEnabled` from it, replacing the per-activity `onOptionsItemSelected` handling. North-star: `AppBarConfiguration(topLevelDestinationIds)` with `NavigationUI.setupActionBarWithNavController`, listing the sidebar screens as top-level. | ||
|
|
||
| ### Unlock mechanics | ||
|
|
||
| [`PersonalIdUnlocker`](https://github.com/dimagi/commcare-android/blob/dc7697645fefd99de4e234be569bd8447fb6e0ba/app/src/org/commcare/personalId/PersonalIdUnlocker.kt): cancel and failure both resolve through `connectActivityComplete(false)`. `unlock` takes an `UnlockPolicy`; `SESSION_WITH_TIME_THRESHOLD` skips the prompt when unlocked within the last 10 minutes. `lastUnlockTime` is in-memory, so it does not survive process death — a background kill means a fresh prompt. | ||
|
|
||
| Helpers that stay in place for sessions that have already expired: `ConnectNavHelper.unlockAndGoToConnectJobsList` / `unlockAndGoToMessaging` / `unlockAndGoToWorkHistory`, called from `BaseDrawerActivity`. | ||
|
shubham1g5 marked this conversation as resolved.
|
||
|
|
||
| At cold start, show a splash / branded base behind the prompt — Android 12+ `SplashScreen` kept on screen via `setKeepOnScreenCondition`, or a splash-themed `windowBackground`, with `androidx.core:core-splashscreen` for pre-12 — so the biometric/PIN dialog isn't over a blank window. | ||
|
shubham1g5 marked this conversation as resolved.
|
||
|
|
||
| ### Sidebar code changes | ||
|
|
||
| `ConnectActivity` and `ConnectMessagingActivity` extend `NavigationHostCommCareActivity`; `PersonalIdWorkHistoryActivity` extends `CommCareActivity`. None extend `BaseDrawerActivity`, which is why those screens have no drawer. Move all three onto `BaseDrawerActivity` and override `shouldShowDrawer()` to return `shouldShowDrawerAfterCheck(true)`, matching what `LoginActivity` and `StandardHomeActivity` already do. The shell absorbs them later, at which point the drawer comes from the shell activity instead. | ||
|
|
||
| `NavDrawerHelper.drawerShownBefore()` and the `shouldShowDrawerAfterCheck(requirePersonalIDLogin)` gate stay as they are. | ||
|
|
||
| ### Nothing to remove for `Up` | ||
|
|
||
| `NavUtils` / `navigateUpFromSameTask` are unused across the app. `CommCareActivity.onOptionsItemSelected:261` routes `android.R.id.home` to `onBackPressed()`. The single `android.support.PARENT_ACTIVITY` entry in the manifest (`ReportProblemActivity`) is inert because nothing calls `navigateUp`. `FormEntryActivity:651` raises the quit prompt from the arrow — leave as-is. | ||
|
|
||
| ### Tabs | ||
|
|
||
| `ConnectDeliveryHomeFragment` hosts the tabs and already accepts the wanted tab as a `TAB_POSITION` argument, defaulting to Dashboard. Deep links just need to set it. | ||
|
|
||
| ### Persistence | ||
|
|
||
| Add to `PersonalIdUserPreferences`, which `forgetUser()` already clears: last-accessed opportunity (`jobUUID`), last session context (`manual` / `PersonalId-non-opportunity` / `PersonalId-on-opportunity-X`), and a per-opportunity terminal-state acknowledgment flag (drives reopen-once-then-fall-back-to-list for ended opportunities). | ||
|
|
||
| ### Failure fallback — rejected alternative | ||
|
|
||
| Do not launch Opportunity Home on top of Login for-result to make the fallback automatic: back from a *successful* Opportunity Home would then reveal Login and need an exit flag / `finishAffinity` to suppress. | ||
|
|
||
| ### Sign-in ordering | ||
|
|
||
| Opportunity Home loads first and fires sign-in simultaneously; the Start button and overflow menu stay disabled until sign-in succeeds. The Opportunity Home composition work already gates those actions on whether a session is attached (`areActionsAvailable()`), so no new mechanism is needed. | ||
|
|
||
| ### Analytics wiring | ||
|
|
||
| `FirebaseAnalyticsUtil` with a new constant in `CCAnalyticsEvent` / `CCAnalyticsParam`, following the existing `reportX` static pattern. The control-used event fires from the shared slot handling (app-bar arrow) and from the back-pressed path (system back); there is no public API distinguishing gesture from button, so do not add a third value. | ||
|
|
||
| ### Rotation and process death | ||
|
|
||
| Task stacks survive rotation and are restored by the system after process death, so paths need no special handling. The one exception is `lastUnlockTime` above: a resume after process death re-prompts unlock. | ||
|
|
||
| ### Upgrade | ||
|
|
||
| No migration is needed. If the OS restores a task created by the previous version, that task keeps its old stack shape until it is finished; the new model applies from the next task creation. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.