Mobile App (Android) Side — FLW Mobile App (Sakhi / Saksham / Mitanin / Niramay / Xushrukha)
Document Type | High-Level Design (HLD) |
Component | FLW Mobile App – In-App Notifications (Android client) |
Owning Team | FLW Mobile App Engineering |
Counterpart System | AMRIT Backend (notification dispatch, list/mark/clear APIs) |
1. Introduction
1.1 Purpose
This document describes the high-level design of the in-app notifications feature on the mobile
(Android) side of the FLW app. It covers how the app receives, stores, and surfaces notifications
originating from the AMRIT backend — via both push delivery (Firebase Cloud Messaging) and a
REST list API — to ASHA, Supervisor, CHO, and ANM users, and how user interactions with those
notifications (read / dismiss / clear) are persisted locally and, where applicable, acknowledged back
to the server.
The document mirrors the structure of the existing HLD – Device Integration document on
Confluence, adapted to a mobile-client / backend-API integration instead of a vendor-device
integration.
1.2 Scope
In scope (this document, mobile side):
Firebase Cloud Messaging (FCM) push receipt and system-tray display.
FCM token registration and rotation with the backend.
Push-triggered retrieval of the authoritative notification list from AMRIT and persistence into a
local Room store.
Local Room table as the single source of truth for the toolbar bell badge and the notification
panel.Toolbar bell icon with unread-count badge in both host activities ( HomeActivity,
SupervisorActivity ) — covers ASHA, Supervisor, CHO, and ANM roles.
Full-screen notification panel: list rendering, tap-to-mark-read (synced to server), swipe-to-
dismiss (local soft-clear), clear-all (local soft-clear), empty state.
Strict per-user scoping of all local notification data.
Out of scope (owned elsewhere / later phase):
Server-side trigger logic that creates and dispatches notifications (e.g. incentive claim rejected,
verification reminders) — AMRIT backend responsibility.
Scheduling / recurrence / consolidation logic for the Supervisor 2-day verification reminder —
backend cron/scheduler responsibility, not mobile.
Deep-link navigation from a tapped notification to its target screen (e.g. incentive approval) —
Phase 2.
Server-side attribution/analytics of viewed / dismissed / cleared events.
Any changes to the AMRIT backend's own notification authoring/admin tooling.
2. System Overview
The mobile app is a thin client over one authoritative notification store on the AMRIT backend. It
never originates a notification's business content — it only receives delivery signals, fetches the
canonical list, renders it, and reports back simple interaction state.
┌─────────────────────────┐
│ AMRIT Backend (FLW) │
│ - notification list │
│ - mark/clear endpoints │
│ - (future) triggers & │
│ 2-day reminder cron │
└───────────┬─────────────┘
│ HTTPS / REST / JSON
┌────────────────┼─────────────────────┐
│ │ │
▼ ▼ │
┌───────────────────┐ ┌─────────────────────┐ │
│ Firebase Cloud │ │ flw-api/notification│ │
│ Messaging (FCM) │ │ /list, /read, │ │
│ (push transport) │ │ /clear, /clearAll │ │
└─────────┬──────────┘ └───────────┬───────────┘ │
│ push │ pull (debounced,│
▼ │ triggered by push)
┌────────────────────┐ │ │
│ FBMessaging │──────────────┘ │
│ (FirebaseMessaging │ onPushReceived() │
│ Service) │─────────────┐ │
└──────────┬───────────┘ │ │
│ system tray ▼ │
▼ ┌─────────────────────┐ │
[Android notification] │ NotificationRepository│◄──┘ markRead() acks│ (Singleton, Room- │ individual reads
│ backed source of │
│ truth) │
└───────────┬─────────┘
│ Room (SQLCipher)
▼
┌─────────────────────┐
│ NOTIFICATION table │
│ (per-user scoped) │
└───────────┬─────────┘
│ Flow<List> / Flow<Int>
┌───────────────┴────────────────┐
▼ ▼
┌───────────────────┐ ┌──────────────────────┐
│ BellBadgeHelper │ │ NotificationPanel │
│ (toolbar bell + │ │ Fragment/ViewModel/ │
│ unread badge) │ │ Adapter (full list UI) │
└───────────────────┘ └──────────────────────┘
in HomeActivity (ASHA) &
SupervisorActivity (Supervisor/CHO/ANM)
Three flows make up the feature:
1. Push & tray — an FCM message arrives; the app always shows a system-tray notification,
regardless of login state.
2. Push-triggered pull & persist — receipt of a push also triggers (debounced) a call to the
notification list API; the response is upserted into Room, scoped to the currently logged-in user.
3. Panel interaction — the bell badge and panel observe Room reactively; tapping a row marks it
read locally and acknowledges read to the server; swipe/clear-all soft-clear locally only.
3. Architecture
3.1 Key Components
Component File Responsibility
Push
receiver
Token
registration
Source of
truth /
orchestrator
FBMessaging.kt Extends FirebaseMessagingService ;
shows the system-tray notification on every
push; on token refresh, re-registers with
backend; on message receipt, signals the
repository to pull the canonical list.
FcmTokenUploader.kt NotificationRepository.kt Uploads the current FCM token for the logged-
in user; invoked from onNewToken and both
host activities' onCreate.
Singleton. Resolves the logged-in userId ;
exposes notifications / unreadCount
as Room-backed Flows; debounces push-triggered pulls; performs mark-read / dismiss /
clear actions.
Local
persistence
NotificationDao.kt Network
contract
Toolbar
badge
AmritApiService.kt BellBadgeHelper.kt Room DAO over the NOTIFICATION table;
every query is scoped by userId ; read/soft-
clear are UPDATEs, not deletes.
Retrofit endpoints for token update/clear and
notification list/read/clear (see §4).
Binds unreadCount to a bell+badge action
view in the toolbar menu of both host activities;
wires the click to open the panel.
Full-screen list UI: renders rows, empty state,
swipe-to-dismiss ( ItemTouchHelper ), clear-
all confirmation.
Panel UI NotificationPanelFragment.kt,
NotificationPanelViewModel.kt ,
NotificationAdapter.kt
Host
activities
HomeActivity.kt (ASHA),
SupervisorActivity.kt
(Supervisor/CHO/ANM)
Host the toolbar bell and the shared
NotificationPanelViewModel ; both roles
get identical behaviour via the same repository.
3.2 Technology / Interface
Transport (push): Firebase Cloud Messaging, data-only + notification payload; single shared
channel flw_notifications (defined in SakhiApplication ).
Transport (pull/ack): HTTPS REST / JSON over the existing AmritApiService Retrofit + Moshi
stack, same auth interceptor / bearer-token pipeline as every other AMRIT API call — no new
auth mechanism introduced.
Persistence: Room, SQLCipher-encrypted (consistent with the rest of the app's DB), one
NOTIFICATION table.
Reactivity: Kotlin Flow from DAO → repository → LiveData in the ViewModel → UI, so badge
and panel update automatically on any local write.
DI: Hilt; FBMessaging (framework-instantiated, can't use @Inject ) resolves its dependencies
via a Hilt EntryPoint.
4. Integration Interfaces
Operation Method & Path Purpose
Register /
refresh FCM
token
POST common-
api/firebaseNotification/updateToken
Associates the device's current
FCM token with the logged-in user.Clear FCM
token (logout)
POST common-
api/firebaseNotification/clearUserToken
Unbinds the token on logout so
pushes stop targeting a signed-out
session.
Get notification
list
POST flw-api/notification/list Mark one
notification read
PUT flw-
api/notification/{notificationId}/read
Mark all read POST flw-api/notification/markAllRead Clear (soft)
notifications
POST flw-api/notification/clear Clear all (soft) POST flw-api/notification/clearAll Canonical pull of the current user's
notifications; response upserted
into Room.
Acknowledges a read back to the
server; fired immediately after the
local Room update on row tap.
Defined in the contract; not
currently invoked by any mobile
code path (see §9).
Defined in the contract; not
currently invoked — swipe-to-
dismiss is local-only today (see
§9).
Defined in the contract; not
currently invoked — clear-all is
local-only today (see §9).
All list/read/clear endpoints return the shared AMRIT envelope shape ( {data, statusCode,
status} ), consistent with other AMRIT APIs in this app.
4.1 Notification Event Types
Event Type (server key) Confirmed? Trigger owner
INCENTIVE_CLAIMED CONFIRMED AMRIT backend, on claim submission.
ASHA_CLAIM_REJECTED BEST-
GUESS
AMRIT backend — trigger not yet
implemented (see §9); mobile Supervisor reject
flow does not fire anything client-side.
AMRIT backend — scheduler not yet
implemented (see §9).
AMRIT backend, not yet implemented.
SUPERVISOR_VERIFICATION_REMINDER BEST-
GUESS
ASHA_SUBMISSION_REMINDER BEST-
GUESS
ASHA_STAGE_CHANGE BEST-
GUESS
SUPERVISOR_AUTO_ROUTING BEST-
GUESS
AMRIT backend, not yet implemented.
AMRIT backend, not yet implemented.CHO_VERIFICATION_REMINDER BEST-
GUESS
ANM_VERIFICATION_REMINDER BEST-
GUESS
AMRIT backend, not yet implemented.
AMRIT backend, not yet implemented.
GENERIC— Fallback used when an incoming eventType
key doesn't match any known type; mapped to a
generic icon.
The mobile app is purely a renderer for these event types — it maps a server key to an icon and (for
one type) a nav target; it never decides when one is created.
5. Data Flow
5.1 End-to-End Sequence
FCM ──push──▶ FBMessaging.onMessageReceived()
│
│
├──▶ showNotification() ──▶ Android system tray (always, any login state
└──▶ NotificationRepository.onPushReceived()
│ (debounce 1500ms; trailing-call collapses bursts)
▼
pullAndSaveNotifications()
│
├──▶ AmritApiService.getNotifications()
│ │
│ HTTP 200, statusCode 200 ──▶ map DTO→Entity ──▶ notificati
│ HTTP 200, statusCode 401/5002 ──▶ refresh token, retry
│ HTTP 200, statusCode 5000 ──▶ treated as "no data", no-op
│ other/exception ──▶ swallowed, returns false (silent failu
▼
Room NOTIFICATION table (upsert by PK notificationId, idempotent)
│
▼ Flow emission
┌────────────┴─────────────┐
▼ ▼
BellBadgeHelper (badge count) NotificationPanelFragment (row list)
│ │
│ user taps a row
│ ▼
│ markRead(id) ──▶ Room UPDATE (local) + PUT .../read (serve
│ │
│ user swipes a row
│ ▼
│ dismiss(id) ──▶ Room UPDATE cleared=1 (local only, no serv
│ │
│ user taps "Clear notifications"
│ ▼
│ clearAll() ──▶ Room UPDATE cleared=1 for all rows (local o▼
Badge count recomputed automatically from Room Flow (no explicit refresh needed)
5.2 Notification States
Each row carries two independent boolean flags rather than a single state enum:
Flag Meaning Set by Synced to server?
read User has
acknowledged the
notification; affects
unread badge count
and row styling.
cleared Soft-deleted from
the user's visible list;
row is retained in
Room (not SQL-
deleted) so a later
list-pull can't
resurrect it.
Row tap in the panel. Swipe-to-dismiss (single row) or
"Clear notifications" (all rows).
YES — PUT
/notification/{id}/read
fires immediately after the local
write.
NO — /clear and
/clearAll endpoints exist in
the contract but are never called
(see §9).
viewed Reserved for
"notification was
displayed/scrolled
past" attribution.
NotificationDao.markViewed()
exists.
NO CALLER — not invoked
from any UI path today.
There is no visible-to-invisible-to-purged lifecycle beyond cleared ; a housekeeping query
deleteOlderThan(ts) exists in the DAO for eventual hard-deletion of old rows but is not currently
scheduled/called from anywhere.
5.3 Retrieval Strategy
Unlike a typical periodic-poll design, this implementation uses a push-triggered, debounced pull:
every FCM message received calls onPushReceived() , which pulls the full list from the server after a
1500 ms debounce window (a burst of pushes collapses into one pull). There is no periodic
background poll worker (e.g. WorkManager) and no pull-on-app-open or pull-on-panel-open —
a refresh() method exists on the panel ViewModel for this purpose but is not wired to any UI
trigger today. This means the local list is only ever refreshed by a push arriving; a device that misses
a push (app killed, notification-type message in background, network blip) will not see new
notifications until the next push. See §9 for the downstream implication.
6. Local Storage (Room)The NOTIFICATION Room table is the single source of truth the UI observes — it is deliberately not a
cache the app re-derives from the server on every screen open (see §5.3). Design points:
Idempotent upsert: primary key is the server-assigned notificationId , so re-pulling the same
list (or a push that duplicates a list item) is a no-op REPLACE, not a duplicate row.
Per-user scoping: every query/update takes userId as a bound parameter; there is no
global/unscoped query that could leak one user's notifications to another on a shared device.
Soft-clear over delete: dismiss/clear-all set a flag rather than deleting rows, preserving local
history and preventing a later list-pull from "resurrecting" a row the user already dismissed.
Encrypted at rest: stored in the same SQLCipher-encrypted Room database as all other
beneficiary data, no separate storage mechanism.
Column Type Notes
notificationId Long
Server-assigned; drives upsert idempotency.
(PK)
userId Long Scopes every query.
role String?
eventType String Maps to NotificationEventType for icon
selection.
navId String? Maps to NotificationNavTarget ; currently
unused by any navigation code (Phase 2).
title String
body String
priority String?
createdTs Long Stamped at receive/fetch time — server payload
carries no timestamp field.
read / cleared / viewed Boolean See §5.2.
senderUserId / receiverUserId /
beneficiaryId / activityId /
referenceId
Long? Context ids carried through from the payload,
currently not surfaced in the UI.
7. Non-Functional Considerations
Aspect Approach
Security | No new auth surface: token/list/read/clear calls ride the existing bearer-token-authenticated AmritApiService client; local data is inside the existing SQLCipher-encrypted database. |
Reliability | Room is the resilient local cache — UI keeps working (showing the last-known list/badge) even if the network/list-pull fails; pull failures are caught and logged, not surfaced as a crash. |
Performance | Debounced pull (1500 ms trailing window) avoids a network call per push in a burst; Room Flow avoids manual refresh plumbing across badge + panel. |
Scalability | Per-user-scoped queries keep result sets small regardless of total notification volume on the device; no unbounded query loads "all notifications for all users. " |
Error Handling | 401 / 5002 triggers a token refresh + retry; 5000 is treated as an intentional empty state; any other failure (timeout, malformed body, generic exception) is swallowed and logged via Timber, returning false without crashing or blocking the UI thread. |
Consistency | Idempotent upsert-by-PK plus soft-clear (not delete) means a pull racing with a local dismiss cannot resurrect a row the user already cleared. |
8. Assumptions & Dependencies
The AMRIT backend owns and will implement all notification-creation triggers (claim submitted,
claim rejected, verification reminders) — the mobile app assumes it will only ever need to render
what arrives via FCM/list API, never author notification content itself.
FCM delivery is assumed best-effort, not guaranteed — the design deliberately does not rely on
FCM alone (see push-triggered pull in §5.3), but currently has no fallback for a missed push
beyond the next push arriving.
The list/mark/clear API contract (request/response envelope, event-type keys, nav-id values) is
assumed stable once confirmed; some event types are still best-guess pending backend
confirmation (§4.1).
Backend is assumed to send data-only FCM payloads carrying notification_id for every
dispatch — a notification-type-only message with no data payload will still show in the
system tray but will not carry the metadata pull relies on.
9. Open Questions / Clarifications Pending
These are gaps identified against the current implementation, not yet resolved:
Mark-all-read / clear / clear-all are defined server-side but never called from mobile. Is this
intentional (backend doesn't need to know about local soft-clears) or a gap to close before the
feature is considered complete end-to-end?
No refresh path other than push. If a user opens the app/panel without a recent push, they see a
possibly-stale list. Should NotificationPanelViewModel.refresh() (already implemented butunwired) be called on panel open, or is a periodic poll worker (previously scoped as "T10") still
planned?
Who owns the Supervisor 2-day reminder scheduler and its consolidation logic (count of
pending ASHA-months, recurrence every 2 days, cutoff)? Confirmed as a backend-only concern
architecturally (a mobile app can't reliably fire a notification while not running), but no backend
scheduler exists yet either.
ASHA rejection notification trigger: the Supervisor/CHO/ANM "Reject claim" flow only PUTs
status/reason data today; nothing dispatches ASHA_CLAIM_REJECTED. Confirm this is entirely a
backend trigger to be added, with no mobile-side change needed beyond rendering it once it arrives.
viewed flag and deleteOlderThan() housekeeping query exist but have no caller — confirm
whether attribution reporting and local retention pruning are still planned, or dead code to remove.
10. Open Items / To Be Detailed in LLD
Exact request/response JSON schemas for markAllRead , clear , clearAll once/if wired from
mobile.
Deep-link routing table: full mapping of navId → destination screen for every event type (only
INCENTIVE_APPROVAL is defined today).
Retention/housekeeping policy: whether and when deleteOlderThan() should run, and what
age threshold to use.
Failure/retry policy detail for pullAndSaveNotifications() beyond the current 401/5002/5000
handling — e.g. exponential backoff, max retry count.
Confirmation of all "best-guess" event-type server keys and their exact payload field names once
backend triggers for those types are built.