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.