Skip to content

Reconnect FCM and re-enable push notifications#141

Closed
bryantran24 wants to merge 3 commits into
mainfrom
feature/firebase-push-notifications
Closed

Reconnect FCM and re-enable push notifications#141
bryantran24 wants to merge 3 commits into
mainfrom
feature/firebase-push-notifications

Conversation

@bryantran24

@bryantran24 bryantran24 commented Feb 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes the FCM part of #135

Reconnect FCM and temporarily disable announcement polling. Also temporarily allow clearing notification dot through opening the app.

Notifications can now be sent via Firebase Cloud Messaging campaigns (title + text).

example:
image

@bryantran24
bryantran24 requested a review from Pahina0 February 3, 2026 23:52
@Pahina0

Pahina0 commented Mar 12, 2026

Copy link
Copy Markdown
Collaborator

will merge after server implements fcm and is tested

bryantran24 added a commit that referenced this pull request Jul 21, 2026
…main's architecture refactor (#163)

* establish top-level application package structure

- move Application class into app/
- move MainActivity into app/
- move DI modules into app/di

* reorganize API and repository architecture

- move Retrofit endpoints into ShuttleApi
- replace API helper with RemoteShuttleDataSource
- organize network DTOs and mappers
- move repository implementations into data/repository

* move map UI into feature package

* Organize active UI by feature

* organize local preferences and migrate announcements feature

* organize background components

* remove FlattenTypeAdapterFactory and use explicit DTO mapping

* Remove unused analytics

* organize connectivity extension

* resolve compose correctness warnings

* upgrade toolchain and dependencies

* migrate to Google OSS Licenses

* Update announcements

* Update setup feature

* Update settings feature

* Fix dev mode url change and add test

* Reorganize maps feature

* prevent crashes and clean up unused code

* fix(ui): improve pre-QA screen stability

* fix(network): harden API parsing and base URLs

* fix(settings): reset setup flow safely

* chore(build): enable release shrinking and trim dependencies

* chore: remove unused UI resources and helpers

* ci: run lint and release source checks

* chore(build): enable Gradle build caching

* chore(build): enable parallel Gradle tooling

* refactor(data): clarify data and dependency injection names

* test: add core JVM regression coverage

* test(ui): add core Compose behavior coverage

* refactor(navigation): migrate from Compose Destinations to Navigation 3

* refactor(network): remove NetworkResponseAdapter

* refactor(ui): replace Compose icons with Material Symbols

* refactor(network): replace Gson with Kotlin serialization

* ci: chain build and test Gradle tasks

* test cache

* ci: improve Gradle caching in build and release workflows

* test cache

* Update GitHub Actions Node 24 actions

* refactor(network): simplify API base URL configuration

* refactor(announcements): adopt banner API contract

Switch to the wrapped /announcements response with id/message/type/
active/expiresAt/createdAt fields, replacing the old subject/body/
start/end shape. Adds a type-safe AnnouncementType enum (falling back
to info for unknown values) and tolerant timestamp parsing that
handles offset/zoned ISO strings, non-zero-padded local timestamps
interpreted in America/New_York, and malformed values without
crashing. Displayability and severity/date ordering live on the model
so both the legacy screen and the upcoming map banner can share them.

Legacy announcement worker/screen/tests are updated to the new field
names since they directly construct the old shape; they are removed
outright in a later commit once the map banner replaces them.

* feat(map): display active announcement banners

Adds observeAnnouncements() to ShuttleRepository, polled by
MapsViewModel on its own 5-minute interval (started/stopped with the
map's lifecycle, independent of the 5 second vehicle polling flows).
Results are filtered to active/unexpired and sorted by severity then
recency before landing in MapsUiState; a failed refresh leaves the
previously loaded banners in place.

Adds a compact Material 3 banner container above the map: the most
severe announcement shows by default with a "show more" affordance
for the rest, each styled per type (error/warning/info) with both a
color and an icon + text label so meaning doesn't depend on color
alone. Markdown-style `[label](url)` links are parsed by a small
regex-based utility (no Markdown dependency) and rendered as clickable
text only when the URL is http/https; malformed markdown or unsafe
schemes fall back to plain text without crashing.

* refactor(notifications): remove API-driven push flow

Removes AnnouncementWorker and its daily WorkManager job now that the
map banner (previous commit) is the only surface for API
announcements; API data must never produce a system notification.
Also removes the now-legacy announcements screen/view model/nav route
and NotificationReceiver's "mark as read" broadcast action, along with
the notifications-read DataStore preference it fed.

Drops the WorkManager/Hilt-work dependencies and the manifest's
WorkManagerInitializer override since AnnouncementWorker was their only
consumer, and prunes the strings/drawable that existed solely for the
removed screen and notification action.

FirebaseService's content intent is repointed at MainActivity directly
(NotificationReceiver.openAnnouncements no longer exists) as a minimal
stand-in; the full Firebase push redesign lands in the next commit.

* feat(notifications): complete Firebase push handling

Rewrites FirebaseService to show the Firebase message's own title and
body on a dedicated push channel, using a per-message notification ID
(from the message ID, falling back to the clock) instead of one
constant ID that would silently replace every prior push. Tapping a
notification opens the map - already the app's home screen - with an
optional safe http/https `url` custom-data field opened externally
instead; MainActivity resolves that in both onCreate and onNewIntent
so cold-start and warm-start taps behave the same without duplicate
navigation entries. Deliberately does not port PR #141's
NotificationManager.cancelAll() on resume, since that would clear
unrelated notifications.

Adds manifest defaults (icon/color/channel) so Firebase Console
messages displayed automatically while the app is backgrounded or
killed - which never reach onMessageReceived - still get the right
appearance and open the same tap target.

Firebase is now fully decoupled from the shuttle API: removes
sendRegistrationToken and the POST /notifications/fcmdevices endpoint,
plus FirebaseService's ShuttleRepository injection and onNewToken
override, since Firebase manages its own registration and staff send
pushes manually from the Firebase Console. google-services.json is
left untouched - its package name and project data are valid for this
app.

The url-safety check moves to a shared core/util location so both the
map banner's Markdown links and the notification tap resolver use the
same validation.

* fix(announcements,maps): tolerate live API response and Play Services quirks

The real /announcements endpoint returns a bare JSON array rather than
the documented {"announcements": [...]} wrapper, which crashed every
fetch with JsonDecodingException. AnnouncementsResponseDto now has a
custom KSerializer that accepts either shape and decodes each entry
independently so one malformed announcement can't sink the whole list.

Also declares the org.apache.http.legacy library: Play Services' Maps
renderer module still references the legacy Apache HTTP client, which
was removed from the platform classpath in API 28+, crashing map load
on some emulator images with NoClassDefFoundError.

* feat(dev-tools): add FCM token logging and simulate-announcements toggle

Adds a debug-only onNewToken log of the FCM registration token so it
can be pasted into the Firebase Console's "Send test message" field,
which targets one device rather than the whole app.

Adds a "Simulate announcements" switch to the developer menu that
feeds three fixed sample banners (one per severity, one with a
Markdown link) into MapsViewModel instead of the live API response,
for exercising the banner UI while the shuttle isn't running. Turning
it off restarts the announcement poll for an immediate real fetch
rather than waiting for the next 5-minute tick.

Also updates google-services.json to the project actually used for
manual push testing.

* refactor(notifications): remove unused bus-tracking and departure channels

CHANNEL_TRACKING_BUS and CHANNEL_FIRING_DEPARTURES (and their groups)
were never wired to any feature and aren't planned for a while, so
they only showed up as empty categories in system notification
settings. Notifications now only registers the push channel; the old
channel/group IDs are added to the cleanup lists so they get deleted
from devices that already had them created.

* refactor(map): redesign announcement UI as a top strip and detail sheet

Replaces the stacked-cards-with-expand banner with a single-line
severity strip (icon, truncated message, +N count, chevron) pinned to
the top of the map, below the status bar and above the settings/layers
button column so the two never collide structurally. Tapping it opens
a modal sheet - matching the existing Schedule sheet's mechanics -
listing every active announcement in full with clickable links and a
"last updated" timestamp sourced from a new announcementsUpdatedAt
field on MapsUiState (omitted while simulating).

Also moves Schedule and Recenter to independently-anchored FABs at the
bottom corners (extended/labeled for Schedule, icon-only for Recenter)
now that nothing above them depends on the strip's height.

Considered and dropped a per-announcement dismiss control: since it
only lived in rememberSaveable state, it didn't survive a real app
restart, so its only guarantee was "won't reappear this session" -
too little value for the added state-tracking/pruning/UI complexity.
Critical (Error) announcements were never dismissible either way and
remain visible until the API resolves them.

* feat(map): add bottom navigation with Map and Schedule tabs

Introduces a MainTab-driven bottom NavigationBar in MapsScreen so
Schedule stops being a modal sheet triggered from a map FAB and
becomes its own tab. ScheduleSheet.kt is renamed to ScheduleContent.kt
and its ModalBottomSheet wrapper is dropped in favor of filling the
tab's content area directly. Recenter remains the map's only FAB.

This is an intermediate step ahead of a larger navigation/data refactor
(three-tab bar, standalone Schedule feature, ETAs tab, dev-mode fake
shuttles).

* feat(nav): add three-tab bottom navigation (Map, ETAs, Schedule)

Wrap MapsScreen in a Scaffold with a bottom NavigationBar and switch
between tabs locally instead of via Navigation3, so Map/ETAs/Schedule
can share one bottom bar. Vehicle and announcement polling is now
scoped to the Map tab's own composition lifetime so it stops when
another tab is selected. ETAs tab is a placeholder pending commit 3.

* refactor(schedule): extract Schedule into a standalone feature with caching

Move ScheduleContent/ScheduleUtils out of feature/map into a new
feature/schedule package, backed by its own ScheduleViewModel that
caches routes and schedule the same way MapsViewModel caches routes
(load-if-missing on init) and adds an explicit refresh() that drops
the cache and refetches, wired to a new refresh button in the
schedule header. MapsViewModel/MapsUiState no longer carry schedule
state.

* feat(etas): implement the ETAs tab and its stop details sheet

Replace the placeholder ETAs tab with a real EtasViewModel (caches
routes and polls vehicles/etas/velocities the same way MapsViewModel
does for the Map tab, scoped to the tab's own composition lifetime)
and a StopEtaList + StopEtaSheet UI: a route-filterable list of stops
with a live eta preview per stop, and a modal sheet with the full
list of vehicle etas for a tapped stop. A new EtaUtils inverts each
vehicle's stopTimes map into a per-stop view and is unit tested apart
from Compose/LatLng.

* feat(dev-tools): add developer-mode fake moving shuttles

Add a "Fake shuttles" dev menu toggle (mirrors simulate announcements)
that, together with dev options, drives two vehicles continuously
looping a real route's coordinates. The loop-interpolation math lives
in a plain-Kotlin FakeShuttleUtils (no LatLng) so it's unit testable,
and MapsViewModel publishes the result to a separate
MapsUiState.fakeVehicles field that MapContent renders in its own
marker loop, never merged into real vehicle data.

* fix(dev-tools,etas,schedule): hardcode fake shuttles and ETAs to north/west, drop schedule reload button

Fake shuttles were picking whichever route sorted first instead of a
real named route, so they didn't visibly follow anything; hardcode
one fake vehicle per NORTH and WEST route instead of two offset
vehicles on a single arbitrarily-picked route. Apply the same NORTH/
WEST allowlist to the ETAs tab so stray or test route entries never
show up as stops or filter chips. Remove the schedule page's reload
button per request; ScheduleViewModel.refresh() and its cache-
invalidation tests are unchanged.

* docs: move installation/architecture docs into the repo, refresh README

Bring the wiki's installation guide and a rewritten architecture
overview into docs/, since docs living in the same repo as the code
they describe are far more likely to stay in sync with it. README
now leads with what the app is, its relation to Shubble and the iOS
app, screenshots, and links to both docs pages.

* docs: add short KDoc comments across app/, core/, data/, and feature/

Every file under edu.rpi.shuttletracker gets a brief top-of-class or
top-of-file comment explaining what it's for, aimed at a new Android
contributor getting oriented in the codebase. Doc-only change - no
logic touched, aside from correcting two comments that had gone stale
(MapsViewModel's fake-shuttle doc still described the old "pick any
route" behavior; readApiResponse's doc didn't say what it actually
does).

* fix: gate simulated announcements on dev options, filter stray-route etas, fix schedule scroll and eta recomputation

- Gate simulateAnnouncements on devOptions (like fakeShuttles already
  is), so relocking developer options actually stops fake banners
  instead of leaving them stuck on until the preference is manually
  flipped back off.
- Filter eta vehicles by ETA_VISIBLE_ROUTES too, not just stops/filter
  chips, so a vehicle on a route outside NORTH/WEST can't still leak
  an eta chip onto a shared stop.
- Fix the schedule's auto-scroll: once every departure for the day has
  already passed, it now lands on the last (most recent) row instead
  of jumping back to the top of the morning schedule. Extracted as
  scrollIndexFor() so it's unit tested.
- Memoize EtasScreen's per-stop lookup with remember(), matching
  StopEtaList, instead of rebuilding it on every recomposition.

* test: add androidTest coverage for schedule/etas/settings, fix two device-run failures

Running these instrumented tests on a real emulator (rather than just
compiling them) turned up two real bugs:

- MapsScreenNavigationTest asserted the schedule subtitle as an exact
  full-text match, but it's only the first sentence of a longer
  string - onNodeWithText needs substring = true here.
- ScheduleContentTest relied on ScheduleContent's auto-expand
  LaunchedEffect having already run by the time the assertion fired;
  on device it hadn't, so "Student Union" was never found. Split into
  its own test that explicitly expands the row instead of assuming
  one already is.

Also defensively added composeRule.waitForIdle() wherever a test
depends on a viewModelScope.launch-driven preference write or a
ModalBottomSheet's open animation landing before the assertion runs
(AboutScreen's tap-to-unlock, both StopEtaSheet tests) - the same
class of timing issue, just not yet observed failing.

* Add fake eta support for dev tools and the ETAs tab

* Add ETAs home screen widget

* Update announcement and button colors

* Add navigation rail for wide/landscape screens

* Fix Android Lint errors: LocalActivity cast, ViewModel construction in tests
@bryantran24
bryantran24 deleted the feature/firebase-push-notifications branch July 21, 2026 02:05
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