Modernize the trip-plan-change monitor; fix the OTP1→OTP2 "better plan" misfire#1979
Conversation
…n" misfire The trip-plan-change monitor could fire a surprise "We found a better trip plan" notification for a trip the user never (recently) planned. Root cause: itinerary identity was exact ordered-string equality over transit-leg tripId (ItineraryDescription.itineraryMatches), and tripId is the OTP gtfsId verbatim. OTP2 emits feed-prefixed ids (FeedId:TripId, e.g. 1:trip_5); OTP1 emits bare ids (trip_5). A monitor armed under one vocabulary can never string-match a re-plan under the other, so the decider fell through to ItineraryChanged and alerted "better plan" for an unchanged trip. A pending alarm / redelivered service intent can carry a pre-OTP2 bundle across an app update, reproducing it. Changes: - Compare on the normalized entity suffix, not the raw id. Extract the sanctioned OtpObaIdResolver rule (strip the FeedId: prefix; colon-only, since OBA ids are agency_entity and GTFS ids contain underscores) into a shared gtfsEntitySuffix() reused by the resolver and ItineraryDescription. This is the documented OTP2 id contract (schema.graphqls), not a new heuristic, and it bridges trip_5 <-> 1:trip_5. Raw ids stay in the persisted bundle, so a currently-pending pre-OTP2 alarm also compares correctly when it fires. - Version-stamp the persisted monitor state (EXTRA_MONITOR_VERSION / MONITOR_STATE_VERSION) behind a pure, unit-tested parseMonitorState(). A bundle from a newer, incompatible build stops the service silently instead of misfiring; an absent version is the compatible legacy v0. - Rewrite the notification: ItineraryChanged now says "Your planned trip changed" (not "better"); the deviation body gets its own string. Retire the old "better plan" strings and their (literally "better plan") translations. The change alert uses a fixed id (single monitored trip) instead of a not-guaranteed-unique tripIds hashCode. - Consolidate the enable-gate into TripPlanNotifications.isEnabled(): the in-app toggle AND the OS (app-level notifications + channel importance). Previously the app pref was ignored on O+, so the Settings switch did nothing there. Retire the hidden, UI-less live_updates preference. - Refresh stale OneBusAway#1939 comments (restore-into-directions is merged and live). Tests (JVM): gtfsEntitySuffix normalization; cross-scheme identity matching; the load-bearing decider regression (OTP1 armed vs OTP2 re-plan -> KeepMonitoring, not ItineraryChanged) plus the previously-untested null-delay and threshold- boundary branches; and the version/validity parse gate.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughTrip monitoring now normalizes OTP/GTFS identifiers, validates persisted monitor state, centralizes notification enablement checks, updates foreground-service handling, and revises trip-change notification navigation and text. ChangesTrip identity normalization
Notification enablement and scheduling
Validated monitoring service and alerts
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant TripResultsScreen
participant TripPlanNotifications
participant TripPlanMonitor
participant TripPlanMonitorService
participant HOME
TripResultsScreen->>TripPlanNotifications: check notification availability
TripResultsScreen->>TripPlanMonitor: start monitoring when enabled
TripPlanMonitor->>TripPlanMonitorService: deliver versioned monitor state
TripPlanMonitorService->>HOME: reopen directions with updated itineraries
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- Inline the redundant TripMonitorState wrapper into MonitorStateParse.Valid (callers only ever destructured .state.description/.state.departure). Rename the file to MonitorStateParse.kt so it matches its sole class. - Rename the service's private Bundle reader parseMonitorState(Bundle) -> readMonitorState() so it no longer overload-shadows the pure validator. - Give ItineraryDescription a primary constructor + eager normalizedTripIds val, dropping the two-declared-fields + by-lazy shape. No behavior change.
Problem
The trip-plan-change monitor could fire a surprise "We found a better trip plan" notification for a trip the user didn't recently plan — the feature that prompted this.
Root cause: itinerary identity was exact ordered-string equality over transit-leg
tripId(ItineraryDescription.itineraryMatches), andtripIdis the OTPgtfsIdverbatim from both adapters. The pinned OTP2 schema documentsTrip.gtfsIdas colon-delimitedFeedId:TripId(graphql/otp2/schema.graphqls); OTP1 emits bare ids (trip_5). A monitor armed under one id vocabulary can never string-match a re-plan under the other, soTripMonitorDecider.decidefell through toItineraryChangedand fired the "better plan" alert for a trip that never changed.START_REDELIVER_INTENT+ a persisted AlarmManager PendingIntent means a stale pre-OTP2 monitor can redeliver after an app update and re-plan under OTP2, reproducing it.The identity logic is the last piece untouched since the 2016 original; this modernizes it and folds in the adjacent debt found along the way.
Changes
1. Normalized itinerary identity (the fix). Compare on the entity suffix of each trip id, not the raw string. The
FeedId:prefix is stripped via a sharedgtfsEntitySuffix()extracted fromOtpObaIdResolver— the documented OTP2 id contract ("the entity id is identical on both sides, only the feed prefix differs"), reused by both the resolver andItineraryDescription. Sotrip_5(OTP1) and1:trip_5(OTP2) compare equal. Colon-only, deliberately (OBA ids areagency_entityand GTFS ids contain underscores). Raw ids stay in the persisted bundle, so the currently-pending pre-OTP2 alarm also compares correctly when it fires.2. Version-stamped monitor state. New
EXTRA_MONITOR_VERSION/MONITOR_STATE_VERSIONbehind a pure, unit-testedparseMonitorState(). A bundle from a newer/incompatible build (redelivered across an app update) stops the service silently instead of misfiring; an absent version = compatible legacy v0.3. Honest notification.
ItineraryChanged→ "Your planned trip changed" (not "better"); the deviation body gets its own string. Retires the old "better plan" strings and their translations (which literally said "better plan" in it/es/pl/fi). The change alert uses a fixed id (single monitored trip) rather than the not-guaranteed-uniquetripIds.hashCode().4. One enable-gate.
TripPlanNotifications.isEnabled()= the in-app toggle AND the OS (app-level notifications incl.POST_NOTIFICATIONS, and channel importance). Previously the app pref was ignored on O+, so the Settings switch did nothing on modern devices. Retires the hidden, UI-lesslive_updatespreference. (User-visible: the toggle now gates arming on all SDK levels.)5. Cleanup. Refreshes stale
#1939comments (restore-into-directions is merged and live).Non-goals: no multi-trip monitoring, no service/alarm re-architecture (#1732/#1733 stays), no OTP1 removal (fix works for both), no "actually better" ranking, no persisted-bundle/
TripItineraryJSON schema change.Tests (JVM, pure)
OtpGtfsIdsTest— colon split, unprefixed passthrough, underscore preserved, null.ItineraryDescriptionTest— cross-scheme matching, order-sensitivity, non-collision.TripMonitorDeciderTest— the load-bearing regression (OTP1 armed vs OTP2 re-plan ⇒KeepMonitoring/Deviation, neverItineraryChanged) + the previously-untested null-delay and threshold-boundary branches.TripMonitorStateTest— the version/validity gate (valid / legacy-v0 / missing / incompatible).OtpObaIdResolverTestandTripItineraryJsonTeststay green (no persisted-format change).Both flavors + test sources compile under
-PwarningsAsErrors=true;spotlessCheckpasses; installed and running on a device.Summary by CodeRabbit
New Features
Bug Fixes
Tests