Skip to content

Surface trip-plan errors as a classified, dismissible snackbar#1942

Merged
bmander merged 2 commits into
mainfrom
feature/trip-plan-error-snackbar
Jul 18, 2026
Merged

Surface trip-plan errors as a classified, dismissible snackbar#1942
bmander merged 2 commits into
mainfrom
feature/trip-plan-error-snackbar

Conversation

@bmander

@bmander bmander commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

What

Replaces the flat String trip-plan error with a structured error ontology and surfaces it as a persistent, dismissible Material 3 snackbar over the directions map (in place of the old DirectionsMessageCard).

Model — TripPlanError

A coarse Category (each with a stable header string + a Severity) plus the exact OTP1/OTP2 detail message, so none of the fine-grained wire information is flattened away.

  • Both planner paths classify into it via testable top-level functions — otp1ErrorFor (OTP1 REST integer codes) and otp2ErrorFor (OTP2 GraphQL RoutingErrorCode + InputField) — and throw a TripPlanException. It extends IOException, so the existing runCatching / runCatchingCancellable wrapping and the trip-plan monitor's empty-list-on-failure path are unaffected.
  • TripPlanViewModel unwraps via Throwable.toTripPlanError(), falling back to Unknown.
  • Wire knowledge stays at the planner boundary; the UI only ever sees Category.headerRes + detailRes.

UI — the snackbar

  • One stable, severity-coloured header per category (every no-route failure reads "Cannot find route") over a single reason line (the exact wire message), with an explicit ✕ dismiss.
  • Persists until dismissed or the plan state changes — unlike a toast/auto-dismissing snackbar, a deterministic failure (outside coverage, no route, no service at this time) isn't lost if the user glances away.
  • Severity colours: blue = tip (INFO), amber = non-error (WARNING), red = failure (ERROR). Red reuses the theme error role; warning/info are new semantic text tokens (Material ships only error), tuned per light/dark so the header stays legible on the inverting inverseSurface bar.

Notes

  • Presentation + model change; no data/endpoint changes. All existing detail strings are reused — only 6 new category headers and the severity colour tokens are added.
  • The retired standalone trip-plan screen's error path was already gone; this only touches the home directions flow. Other error.message sites (route info, trip details) are unrelated and untouched.

Testing

  • TripPlanErrorMappingTest — exhaustively covers both classifiers (every OTP1 code and OTP2 RoutingErrorCode → expected category + detail).
  • TripPlanViewModelTest — a classified failure surfaces its TripPlanError; an unclassified throwable falls back to Unknown.
  • Compiles clean under the strict -PwarningsAsErrors gate for both obaGoogle and obaMaplibre; installed and exercised on a Pixel 7 Pro.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added clearer, categorized trip-planning error messages for connectivity, location, schedule, route, advisory, and request issues.
    • Introduced a dismissible snackbar displaying error severity and helpful details.
    • Improved dark- and light-theme styling for error severity indicators.
  • Bug Fixes

    • Standardized error handling across trip-planning services, including unknown and empty-result failures.
    • Improved location-specific guidance for origin and destination errors.
  • Tests

    • Added coverage for error classification and ViewModel handling of known and unknown failures.

Replace the flat String trip-plan error with a structured TripPlanError
(category + severity + specific detail) and render it as a persistent,
dismissible Material 3 snackbar over the directions map, in place of the old
DirectionsMessageCard.

- TripPlanError: a coarse Category (each with a stable header string and a
  Severity) plus the exact OTP1/OTP2 detail message, so no wire information is
  flattened away. Both planner paths classify into it via testable top-level
  functions (otp1ErrorFor for OTP1 REST codes, otp2ErrorFor for OTP2 GraphQL
  RoutingErrorCode) and throw a TripPlanException (an IOException, so the
  existing runCatching/monitor flows are unaffected); the ViewModel maps
  failures via Throwable.toTripPlanError().
- Snackbar: a consistent severity-coloured header (every no-route failure reads
  "Cannot find route") over a single reason line, with an explicit dismiss.
  Persists until dismissed or the plan state changes, so a deterministic failure
  isn't lost to an auto-timeout.
- Severity colours use the theme error role plus new semantic warning/info text
  tokens (Material ships only error); values are tuned per light/dark so the
  header stays legible on the inverting inverseSurface bar.
- Tests: TripPlanErrorMappingTest covers both classifiers exhaustively;
  TripPlanViewModelTest covers classified vs. Unknown-fallback failures.
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@bmander, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 16 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f2864584-eb04-4079-bb19-2a69613499d2

📥 Commits

Reviewing files that changed from the base of the PR and between bdcb6ec and ac5f184.

📒 Files selected for processing (3)
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/directions/DirectionsFeature.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/tripplan/TripPlanRepository.kt
  • onebusaway-android/src/test/java/org/onebusaway/android/ui/tripplan/TripPlanErrorMappingTest.kt
📝 Walkthrough

Walkthrough

Trip-planning failures now use typed TripPlanError values across OTP1, OTP2, repository, ViewModel, and directions UI paths. The UI renders categorized, severity-colored snackbar errors with localized headers and details.

Changes

Trip-planning error flow

Layer / File(s) Summary
Typed error contracts
onebusaway-android/src/main/java/org/onebusaway/android/ui/tripplan/TripPlanError.kt, onebusaway-android/src/main/java/org/onebusaway/android/ui/tripplan/TripPlanFormState.kt
Defines categorized trip-planning errors, typed exceptions, fallback conversion, and PlanResult.Error(error: TripPlanError).
OTP failure classification
onebusaway-android/src/main/java/org/onebusaway/android/ui/tripplan/TripPlanRepository.kt, onebusaway-android/src/main/java/org/onebusaway/android/ui/tripplan/Otp2Planner.kt, onebusaway-android/src/main/java/org/onebusaway/android/api/contract/OtpPlanModels.kt
Maps OTP1 and OTP2 failures to structured errors and throws TripPlanException for transport, routing, empty-result, and request failures.
ViewModel and directions error UI
onebusaway-android/src/main/java/org/onebusaway/android/ui/tripplan/TripPlanViewModel.kt, onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt, onebusaway-android/src/main/java/org/onebusaway/android/ui/home/directions/DirectionsFeature.kt, onebusaway-android/src/main/res/values/*.xml, onebusaway-android/src/main/res/values-night/colors.xml
Propagates typed failures and renders categorized snackbar headers, localized details, severity colors, and dismissal handling.
Error mapping validation
onebusaway-android/src/test/java/org/onebusaway/android/ui/tripplan/*Test.kt
Tests OTP1 and OTP2 mappings, location-specific details, unknown fallbacks, and ViewModel error propagation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TripPlanner
  participant TripPlanRepository
  participant TripPlanViewModel
  participant HomeScreen
  participant DirectionsErrorSnackbar
  TripPlanner->>TripPlanRepository: classify OTP failure
  TripPlanRepository->>TripPlanViewModel: throw TripPlanException
  TripPlanViewModel->>HomeScreen: publish PlanResult.Error
  HomeScreen->>DirectionsErrorSnackbar: render TripPlanError
  DirectionsErrorSnackbar->>TripPlanViewModel: clearPlanResult on dismiss
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main UI and error-model change: classified trip-plan errors shown in a dismissible snackbar.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/trip-plan-error-snackbar

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
onebusaway-android/src/main/java/org/onebusaway/android/ui/tripplan/TripPlanRepository.kt (1)

107-109: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider defaulting to NoRoute when the server omits an error object.

If the server returns an empty itinerary list without an error object, response.error?.id ?: -1 defaults to -1, which otp1ErrorFor maps to TripPlanError.Unknown. Since the companion object documentation for TripPlanError.NoRoute explicitly notes it is for the "(defensive) empty-results case", consider using it as the fallback here so users see "Cannot find route" instead of an unclassified error.

💡 Proposed defensive fallback
-        if (itineraries.isNullOrEmpty()) {
-            throw TripPlanException(otp1ErrorFor(response.error?.id ?: -1))
-        }
+        if (itineraries.isNullOrEmpty()) {
+            val errorId = response.error?.id
+            val error = if (errorId != null) otp1ErrorFor(errorId) else TripPlanError.NoRoute
+            throw TripPlanException(error)
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/tripplan/TripPlanRepository.kt`
around lines 107 - 109, Update the empty-itinerary handling in the
TripPlanRepository flow to use TripPlanError.NoRoute when response.error is
absent, while preserving otp1ErrorFor mapping when an error object is provided.
Ensure the defensive empty-results case reports “Cannot find route” instead of
Unknown.
onebusaway-android/src/test/java/org/onebusaway/android/ui/tripplan/TripPlanErrorMappingTest.kt (1)

26-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the OTP1 coverage exhaustive as documented.

Several OtpErrorId members—including combined geocode, ambiguity, and triangle variants—are not asserted. Add table-driven cases for every enum member, or narrow the KDoc claim.

Also applies to: 38-82

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@onebusaway-android/src/test/java/org/onebusaway/android/ui/tripplan/TripPlanErrorMappingTest.kt`
around lines 26 - 29, Add exhaustive table-driven OTP1 cases in
TripPlanErrorMappingTest covering every OtpErrorId member, including combined
geocode, ambiguity, and triangle variants, and assert each mapped category and
detail string; otherwise narrow the KDoc claim to match the existing coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/home/directions/DirectionsFeature.kt`:
- Around line 292-323: Add a polite accessibility live region to the
`DirectionsErrorSnackbar` `Surface` by applying `semantics { liveRegion =
LiveRegionMode.Polite }` to its modifier, ensuring newly displayed planning
errors are announced without changing the existing layout or dismissal behavior.

---

Nitpick comments:
In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/tripplan/TripPlanRepository.kt`:
- Around line 107-109: Update the empty-itinerary handling in the
TripPlanRepository flow to use TripPlanError.NoRoute when response.error is
absent, while preserving otp1ErrorFor mapping when an error object is provided.
Ensure the defensive empty-results case reports “Cannot find route” instead of
Unknown.

In
`@onebusaway-android/src/test/java/org/onebusaway/android/ui/tripplan/TripPlanErrorMappingTest.kt`:
- Around line 26-29: Add exhaustive table-driven OTP1 cases in
TripPlanErrorMappingTest covering every OtpErrorId member, including combined
geocode, ambiguity, and triangle variants, and assert each mapped category and
detail string; otherwise narrow the KDoc claim to match the existing coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aea1dfc0-d3ac-4307-88bc-adfc40d5f996

📥 Commits

Reviewing files that changed from the base of the PR and between 05b2801 and bdcb6ec.

📒 Files selected for processing (13)
  • onebusaway-android/src/main/java/org/onebusaway/android/api/contract/OtpPlanModels.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/directions/DirectionsFeature.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/tripplan/Otp2Planner.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/tripplan/TripPlanError.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/tripplan/TripPlanFormState.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/tripplan/TripPlanRepository.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/tripplan/TripPlanViewModel.kt
  • onebusaway-android/src/main/res/values-night/colors.xml
  • onebusaway-android/src/main/res/values/colors.xml
  • onebusaway-android/src/main/res/values/strings.xml
  • onebusaway-android/src/test/java/org/onebusaway/android/ui/tripplan/TripPlanErrorMappingTest.kt
  • onebusaway-android/src/test/java/org/onebusaway/android/ui/tripplan/TripPlanViewModelTest.kt

…ve test

- DirectionsErrorSnackbar: mark the surface as a polite live region so a screen
  reader announces a newly surfaced planning failure.
- OTP1 empty-result with no error object now classifies as NoRoute ("Cannot find
  route") instead of Unknown, mirroring the OTP2 path.
- TripPlanErrorMappingTest: make the OTP1 coverage exhaustive (table-driven over
  every OtpErrorId, with an assertion that fails if a new code lacks a mapping),
  matching the test's documented claim.
@bmander
bmander merged commit c96996d into main Jul 18, 2026
3 checks passed
@bmander
bmander deleted the feature/trip-plan-error-snackbar branch July 18, 2026 07:25
bmander added a commit that referenced this pull request Jul 18, 2026
* Surface trip-plan errors as a classified, dismissible snackbar

Replace the flat String trip-plan error with a structured TripPlanError
(category + severity + specific detail) and render it as a persistent,
dismissible Material 3 snackbar over the directions map, in place of the old
DirectionsMessageCard.

- TripPlanError: a coarse Category (each with a stable header string and a
  Severity) plus the exact OTP1/OTP2 detail message, so no wire information is
  flattened away. Both planner paths classify into it via testable top-level
  functions (otp1ErrorFor for OTP1 REST codes, otp2ErrorFor for OTP2 GraphQL
  RoutingErrorCode) and throw a TripPlanException (an IOException, so the
  existing runCatching/monitor flows are unaffected); the ViewModel maps
  failures via Throwable.toTripPlanError().
- Snackbar: a consistent severity-coloured header (every no-route failure reads
  "Cannot find route") over a single reason line, with an explicit dismiss.
  Persists until dismissed or the plan state changes, so a deterministic failure
  isn't lost to an auto-timeout.
- Severity colours use the theme error role plus new semantic warning/info text
  tokens (Material ships only error); values are tuned per light/dark so the
  header stays legible on the inverting inverseSurface bar.
- Tests: TripPlanErrorMappingTest covers both classifiers exhaustively;
  TripPlanViewModelTest covers classified vs. Unknown-fallback failures.

* Address PR #1942 review: a11y live region, NoRoute fallback, exhaustive test

- DirectionsErrorSnackbar: mark the surface as a polite live region so a screen
  reader announces a newly surfaced planning failure.
- OTP1 empty-result with no error object now classifies as NoRoute ("Cannot find
  route") instead of Unknown, mirroring the OTP2 path.
- TripPlanErrorMappingTest: make the OTP1 coverage exhaustive (table-driven over
  every OtpErrorId, with an assertion that fails if a new code lacks a mapping),
  matching the test's documented claim.

* Pin resolved trip-plan endpoints on the map as they're set

Drop a green (From) / red (To) pin on the home map as soon as each directions
endpoint resolves to coordinates — a map-tap pick, a long-press "directions
from/to here", or a selected placename/address — before a full itinerary is
planned. So a single-endpoint state already shows the point.

- DirectionsMapController.setEndpoints(from, to): draws/updates the two endpoint
  pins via the same MapHost.addMarker green/red hues the itinerary's own
  start/end pins already use, diffing against the current pins so an unchanged
  endpoint keeps its marker (no flicker) and a null endpoint drops its pin.
- The pins are driven from HomeScreen (a LaunchedEffect on the resolved From/To
  coords) through the existing MapDirective channel
  (SetDirectionsEndpoints -> MapViewModel.setDirectionsEndpoints), matching the
  interleaved ShowItinerary/ClearItinerary lifecycle. The itinerary's own pins
  supersede these once one draws; leaving directions clears them.

Note: the MapLibre renderer drops the generic-marker hue (a pre-existing gap the
itinerary start/end pins already share), so the pins are green/red on obaGoogle
but color-indistinguishable on obaMaplibre.
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.

1 participant