Skip to content

Directions drawer: expandable legs, tap-to-frame, and a directions→route focus#1961

Merged
bmander merged 11 commits into
mainfrom
feature/directions-drawer-scroll-picker
Jul 20, 2026
Merged

Directions drawer: expandable legs, tap-to-frame, and a directions→route focus#1961
bmander merged 11 commits into
mainfrom
feature/directions-drawer-scroll-picker

Conversation

@bmander

@bmander bmander commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What this does

Reworks the trip-planner directions drawer to make the map a first-class part of reading directions, building on the sheet rework from #1951.

  • Everything scrolls. The option picker scrolls away with the step list instead of staying pinned, and overflow is shown with the same chevron language as the ETA strip.
  • One expandable card per leg. Tap a leg's body to frame it on the map; the chevron expands its steps/stops.
  • Tap a transit leg → a directions→route focus. The map recontextualizes onto that leg's route: the full route is de-emphasized, the ridden board→alight segment is drawn at normal weight on top, only that segment's stops are drawn, and the camera frames the segment (not the whole route). The route is narrowed to the ridden direction via the boarding stop.
  • Inline ETA strips. A transit leg's Board/Alight rows are their own tap targets (zoom to the stop) and carry a live ETA strip that reuses the arrivals pills wholesale — a pill tap focuses/animates/pings the vehicle exactly as on the arrivals drawer.
  • Device-local times. Itinerary times were formatted against the agency's GMT offset; they now render on the phone's clock.

Commits (phased, drawer-first)

  1. Directions picker scrolls with the list + overflow chevrons
  2. One expandable card per leg, tap a leg to frame it
  3. Tap a transit leg to focus its route (with OTP→OBA id resolution)
  4. Route focus: departing-stop arrivals board
  5. Route focus: draw the traveled board→alight segment thick
  6. Inline ETA strip under Board/Alight rows
  7. Times in the device's local zone
  8. Route focus: frame and emphasize only the ridden segment (thin base / normal segment / segment-only stops / single direction), extracted into a JVM-tested RouteSegmentHighlight
  9. Drawer polish: bigger stop tap targets, always-on ETA strips, 1.75× tinted chevrons

OTP → OBA id resolution

OTP ids (kcm:102574) and OBA ids (1_102574) use different schemes, so tapping a leg's route needs a mapping. OtpObaIdResolver derives it client-side and deterministically — feed id → agency id from the agency.gtfsId suffix, verified against agencies-with-coverage, with a name-based fallback — so no human-maintained OTP↔OBA directory is required for the Puget Sound (OTP2) region today. A proposed upstream extension to the OTP regions directory would make this explicit later.

⚠️ Heuristic to sign off on

Per CLAUDE.md's "no unsanctioned heuristics" rule, calling this out for a human OK before merge:

  • SEGMENT_STOP_TOLERANCE_METERS = 50.0 (RouteSegmentHighlight.onSegment): OTP2 does not return a transit leg's intermediate stop ids, so "which stops are on the ridden segment" can't be answered by exact id match. Stops are instead kept when they fall within 50 m of the clipped board→alight polyline. 50 m sits well below transit stop spacing, so the nearest off-segment stop isn't wrongly included. Failure mode: a stop within 50 m of the ridden path but not actually served by this leg could be drawn (or, conversely, a served stop set back >50 m from the decoded line dropped). The tolerance is documented at the call site.

Testing

  • Compiles clean under the CI gate (-PwarningsAsErrors=true) on both obaGoogle and obaMaplibre.
  • JVM units green, incl. new RouteSegmentHighlightTest and the DirectionCardGrouping / OtpPlanDecode / ConversionUtils suites.
  • Device-verified on a Pixel (Puget Sound region, OTP2) across earlier phases; a fresh end-to-end pass on the final rendering polish is still worth doing before merge.

Summary by CodeRabbit

  • New Features

    • Updated trip directions to work at the leg level: tap to frame/focus the leg, use expand/collapse to show steps.
    • Added Board/Alight stop labels with inline arrival predictions, including targeted map recentering per stop.
    • Highlighted the selected transit segment on the route map and improved restoring directions/route focus when navigating back.
    • Enhanced accessibility for itinerary controls and expand/collapse.
  • Bug Fixes

    • Improved matching of transit routes and stops across planning results.
    • Time displays now consistently use the device’s local time zone.
    • Better behavior for legs with missing or empty route geometry/coordinates.

bmander added 9 commits July 19, 2026 11:00
…vrons

Two UX tweaks to the directions drawer's option-card picker:

- Fold the picker into the directions LazyColumn as its first item so it
  scrolls out of sight as you read down the steps, instead of staying
  pinned above them.
- Flank the horizontally-scrollable picker with the ETA strip's overflow
  chevrons so options hanging off either edge are discoverable and one tap
  jumps a viewport. Extracted EtaStrip's private ScrollChevronGutter into a
  shared ui/compose/components composable and reused it in both places.
Restructure the directions list so each itinerary leg is a single card
instead of a flat step list (transit legs previously split into separate
board/alight rows). Tapping a card body frames the whole leg on the map;
a distinct expand button reveals the leg's steps (turn-by-turn for a walk
leg; intermediate stops + alight for a transit leg), each of which
recenters on its own point.

- Re-group the generator's flat, leg-ordered Direction list into one card
  per leg via a pure, JVM-tested DirectionCardGrouping.groupByLeg.
- Extend DirectionItem with legPoints (decoded leg polyline, for framing)
  and routeLeg identity (routeId + board/alight stop, threaded now for the
  upcoming route-focus phase). Add TripPlace.stopId from OTP2 stop.gtfsId.
- Frame a leg via a new FocusItineraryLeg map directive ->
  MapViewModel.focusItineraryLeg -> FramingIntent.Points (padding-aware,
  no per-flavor code).
…olution)

Tapping a transit leg card now drills into a route-subordinate-to-directions
focus: the map recontextualizes onto that leg's route (anchored to the
boarding stop's direction), and a map-background tap or Back returns to the
itinerary overview. CurrentFocus.Directions gains an optional routeFocus
sub-state carrying that; the shown itinerary is cached so the overview
redraws when the route sub-focus is popped.

OTP and OBA use different id namespaces (e.g. Puget Sound OTP `kcm:102574`
vs OBA `1_102574`), so a client-side OtpObaIdResolver maps them: OBA id =
`{agency}_{entity}`, where the entity number is taken from the OTP id and the
agency is derived from OTP's agency.gtfsId suffix, verified against
agencies-with-coverage, and matched by agency name on a miss (covers feeds
whose GTFS agency_id diverges from OBA's). Unresolvable legs degrade to plain
leg framing. Added agency.gtfsId to the OTP query; threaded agency id/name
through TripLeg -> RouteLegRef. Resolution is async on tap in HomeViewModel.
While a transit leg's route is in focus, swap the directions step list for the
departing stop's live arrivals board over the highlighted route, with a back
control (and map-background tap) returning to the itinerary overview.

DirectionsRouteFocus now carries a FocusedStop for the departing stop (built
from the resolved OBA stop id + the leg's board-stop name/code/point). A new
DirectionsRouteFocusSheet reuses the same per-stop arrivals session + panel as
the home focused-stop drawer (rememberArrivalsSession / ArrivalsSheetHost),
keyed to that stop, so it polls and renders identically; HomeScreen shows it in
place of the results sheet whenever routeFocus.boardStop is set. Route-row taps
in this board are inert for now.
While a transit leg's route is in focus, overlay the leg's own polyline (the
exact board→alight path from the OTP leg geometry) as a markedly thicker line
(2.4x vs the route's 1.5x) in the route's colour, so the portion the user rides
stands out along the full route.

Threaded via ShowRouteRequest.highlightedSegment -> MapViewModel.enterRoute ->
RouteMapController, which overlays it last in publishMapPresentation (so it sits
on top and survives re-publishes from vehicle polls / direction changes) and
resets it on stop(). New HIGHLIGHTED_SEGMENT_WIDTH_PROFILE in DetailZoomRamp.
Expanding a transit leg now shows, under each of its Get on / Get off rows,
that stop's live ETA strip — the same scrollable pill component as the arrivals
drawer. Tapping a pill focuses/animates/pings that trip's live vehicle by
riding the exact arrivals path (the shared row callbacks' onFocusVehicleOnMap →
a focusTripId ShowRouteRequest → focusDirectionsRouteVehicle → RouteMapController),
adding the leg's traveled segment; long-press opens the trip menu. The leg row
body still highlights the route on the map. Replaces the earlier route-focus
arrivals board (DirectionsRouteFocusSheet), which is removed.

OTP→OBA id resolution moves to build time: TripResultsRepository resolves each
transit leg's route/stop ids via OtpObaIdResolver so RouteLegRef/RouteStopRef
carry OBA ids synchronously (agency fields dropped, headsign added); the pure
grouping takes pre-resolved refs; HomeViewModel's route-leg focus is now sync.
The trip planner formatted arrival/departure times against the agency's
GMT offset (ConversionUtils' agencyWallClock + TripLeg.agencyTimeZoneOffset),
so a trip in another agency's zone rendered in that zone rather than on the
phone's clock. Format against ZoneId.systemDefault() / the device-default
TimeZone instead, and drop the now-unused agencyTimeZoneOffset field and its
adapter/test plumbing.
Tapping a transit leg recontextualizes the map onto just the ridden
board→alight span: the full route is de-emphasized, the segment is drawn at
normal weight on top, only the segment's stops are kept, and the camera frames
the segment rather than the whole route. The route is narrowed to the ridden
direction via the boarding stop, so only the relevant direction shows.

Extract the pure segment geometry (routePolylinesWithSegment / onSegment /
isDrawableSegment) into RouteSegmentHighlight with JVM tests so it stays out of
RouteMapController's state plumbing, and hold the originating ShowRouteRequest
directly in DirectionsRouteFocus so a back-press replays the focus faithfully
(including a followed vehicle's focusTripId).
Board/Alight rows become their own tap targets (titleMedium labels that zoom
to the stop), the inline ETA strip always shows while a transit leg is
expanded, and the expand chevrons are 1.75x larger and tinted like the row
icons. Thread the leg's OBA route/stop ids and points for route focus through
the shared geoPointOrNull helper and TripLegGeometry.decodedPoints().
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f9b7b6cb-ab8e-4b34-a9e0-de590bc982d6

📥 Commits

Reviewing files that changed from the base of the PR and between 6199aa3 and 51a7bc5.

📒 Files selected for processing (5)
  • onebusaway-android/src/main/java/org/onebusaway/android/directions/util/ConversionUtils.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/map/RouteMapController.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/directions/DirectionsFeature.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/tripresults/TripResultsScreen.kt
  • onebusaway-android/src/main/res/values/strings.xml
💤 Files with no reviewable changes (1)
  • onebusaway-android/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (4)
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/directions/DirectionsFeature.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/map/RouteMapController.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/directions/util/ConversionUtils.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/tripresults/TripResultsScreen.kt

📝 Walkthrough

Walkthrough

OTP2 GraphQL and adapter mappings now fetch and populate agency GTFS IDs and stop IDs; these flow into domain models with new optional fields and are resolved to OBA identifiers by a client-side resolver. Leg geometry is decoded on demand, grouped into leg-level direction cards with transit references, and used for route highlighting and map framing. Time formatting switched to device-local wall-clock calculations without agency offsets. Directions focus state carries optional route sub-focus for restoration. Trip-results UI was refactored to embed the picker in the list, split expand button from leg-body click, show Board/Alight stops with inline ETA strips, and add scrollable option navigation. A shared ScrollChevronGutter component was extracted, and home/directions wiring was extended to restore focus, cache itineraries, and manage leg/stop interactions.

Changes

Directions leg focus and transit identity

Layer / File(s) Summary
Itinerary identity mapping and card construction
onebusaway-android/src/main/graphql/otp2/Plan.graphql, api/adapters/Otp2PlanAdapters.kt, api/adapters/TripPlanAdapters.kt, directions/OtpObaIdResolver.kt, directions/model/TripItinerary.kt, util/GeoPoint.kt, ui/tripresults/DirectionCardGrouping.kt, ui/tripresults/TripResultsRepository.kt, ui/tripresults/TripResultsUiState.kt, tests: Otp2PlanDecodeTest.kt, OtpPlanDecodeTest.kt, OtpObaIdResolverTest.kt, DirectionCardGroupingTest.kt
OTP2 GraphQL now fetches route.agency.gtfsId; adapters map agency and stop GTFS IDs into TripLeg.agencyId and TripPlace.stopId. OtpObaIdResolver derives OBA route/stop IDs by verifying derived agency suffixes against covered agencies and falling back to name matching. DirectionCardGrouping groups legs with decoded polyline points and resolved RouteLegRef/RouteStopRef into direction cards. Tests validate resolution behavior and card grouping.
Device-local time formatting
directions/util/ConversionUtils.kt, directions/util/DirectionsGenerator.kt, tests/directions/util/ConversionUtilsTest.kt
getTimeWithContext and getTimeUpdated no longer take offsetGMT; both use ZoneId.systemDefault() for wall-clock calculations and optional injectable LocalDate for day comparisons. Real-time display in directions generator uses the new APIs without agency offsets. Tests pinned the wallClock helper's rounding and zone-shifting behavior.
Route segment highlighting and map framing
map/RouteSegmentHighlight.kt, map/ShowRouteRequest.kt, map/MapHost.kt, map/MapViewModel.kt, map/RouteMapController.kt, tests/map/RouteSegmentHighlightTest.kt
New RouteSegmentHighlight helpers overlay a traveled segment on route polylines and filter nearby stops by proximity. ShowRouteRequest carries optional highlightedSegment; MapViewModel.enterRoute forwards it to RouteMapController.start. RouteMapController applies segment-based polyline compositing and stop filtering during republish, and frames the segment or whole route. MapHost.frameItineraryLeg frames leg geometry independently.
Directions focus state, leg interactions, ETA strips, and home integration
ui/home/CurrentFocus.kt, ui/home/CurrentFocusPersistence.kt, ui/home/HomeSheetLogic.kt, ui/home/HomeViewModel.kt, ui/home/HomeScreen.kt, ui/home/map/MapFeature.kt, ui/home/directions/DirectionsFeature.kt, ui/tripresults/TripResultsScreen.kt, ui/tripresults/TripResultsViewModel.kt, ui/compose/components/ScrollChevronGutter.kt, ui/arrivals/components/EtaStrip.kt, res/values/strings.xml, androidTest/DirectionRowFocusTest.kt, tests: HomeViewModelTest.kt
CurrentFocus.Directions now carries optional routeFocus for restoration. HomeViewModel manages itinerary caching, leg/route focus transitions, and back-navigation restoration. TripResultsList embeds the picker as the first item with scrollable chevrons, accepts route-leg/leg/point focus callbacks and stopEtaStrip composable. DirectionRow split body click (frames/focuses) from expand (reveals sub-items), renders Board/Alight RouteStopLabel with inline DirectionStopEtaStrip when expanded with a transit leg. New ScrollChevronGutter component provides shared scroll controls. DirectionStopEtaStrip loads per-stop arrivals and renders matching route group. Directions row focus tests expanded to validate leg framing, expand behavior, transit focus, and stop interactions. String resources added for UI labels and accessibility descriptions.

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

Possibly related PRs

Suggested reviewers: aaronbrethorst

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.00% 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 clearly summarizes the main directions-drawer and route-focus changes in this PR.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/directions-drawer-scroll-picker

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: 2

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

99-124: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Parallelize per-leg route/stop resolution.

itinerary.legs.map { resolveRouteLeg(leg) } resolves each leg sequentially, and resolveRouteLeg itself awaits obaRouteId, then board, then alight one after another (3 sequential suspend calls per leg). For a multi-leg itinerary this serializes what are described as network-backed lookups, adding up latency each time a user opens directions for an option.

♻️ Suggested parallelization
-            val flat = DirectionsGenerator(itinerary.legs, context).directions
-            val routeLegRefs = itinerary.legs.map { leg ->
-                if (leg.mode?.isOnStreetNonTransit == true) null else resolveRouteLeg(leg)
-            }
-            DirectionCardGrouping.groupByLeg(itinerary.legs, flat, routeLegRefs)
+            val flat = DirectionsGenerator(itinerary.legs, context).directions
+            val routeLegRefs = coroutineScope {
+                itinerary.legs.map { leg ->
+                    async {
+                        if (leg.mode?.isOnStreetNonTransit == true) null else resolveRouteLeg(leg)
+                    }
+                }.awaitAll()
+            }
+            DirectionCardGrouping.groupByLeg(itinerary.legs, flat, routeLegRefs)
🤖 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/tripresults/TripResultsRepository.kt`
around lines 99 - 124, Parallelize the network-backed lookups in the directions
flow: update the per-leg mapping around routeLegRefs to resolve transit legs
concurrently, and update resolveRouteLeg so obaRouteId, board.resolveStop, and
alight.resolveStop run concurrently before constructing RouteLegRef. Preserve
null entries for non-transit legs and the existing output order.
onebusaway-android/src/main/java/org/onebusaway/android/ui/home/directions/DirectionsFeature.kt (1)

336-388: 🚀 Performance & Scalability | 🔵 Trivial

Concurrent arrivals polling from expanded legs. Each expanded transit leg mounts two rememberArrivalsSession instances (Board + Alight), each polling independently. With several legs expanded at once this fans out into multiple concurrent arrivals polls against the API. LazyColumn disposal caps this to on-screen legs, so it's bounded in practice, but consider collapsing sibling legs on expand (accordion behavior) or de-duplicating polls per route to reduce load if telemetry shows many legs expanded together.

🤖 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/home/directions/DirectionsFeature.kt`
around lines 336 - 388, Limit expanded transit legs to one at a time so
expanding a leg collapses its siblings before DirectionStopEtaStrip mounts.
Update the expansion-state owner rather than changing rememberArrivalsSession,
preserving the existing Board/Alight behavior for the active leg while
preventing multiple legs from polling concurrently.
🤖 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/directions/util/ConversionUtils.kt`:
- Around line 179-183: Update getTimeWithContext and getTimeUpdated to accept
today as a parameter and remove their internal LocalDate.now(zone) calls. Thread
the supplied today value through the existing today/tomorrow branching and
preserve the current zone-based behavior.

In
`@onebusaway-android/src/main/java/org/onebusaway/android/map/RouteMapController.kt`:
- Around line 288-291: Update RouteMapController.reframe so it assigns the
controller’s highlightedSegment from request.highlightedSegment before applying
direction, focus-trip, or framing behavior. Preserve the existing reframe
branching while ensuring empty or changed request values replace any stale
highlighted segment used by publishMapPresentation and showDirectionStops.

---

Nitpick comments:
In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/home/directions/DirectionsFeature.kt`:
- Around line 336-388: Limit expanded transit legs to one at a time so expanding
a leg collapses its siblings before DirectionStopEtaStrip mounts. Update the
expansion-state owner rather than changing rememberArrivalsSession, preserving
the existing Board/Alight behavior for the active leg while preventing multiple
legs from polling concurrently.

In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/tripresults/TripResultsRepository.kt`:
- Around line 99-124: Parallelize the network-backed lookups in the directions
flow: update the per-leg mapping around routeLegRefs to resolve transit legs
concurrently, and update resolveRouteLeg so obaRouteId, board.resolveStop, and
alight.resolveStop run concurrently before constructing RouteLegRef. Preserve
null entries for non-transit legs and the existing output order.
🪄 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: 4a1450c5-e3c7-4f81-9d07-8d04dfa98a37

📥 Commits

Reviewing files that changed from the base of the PR and between 6247ee0 and 6199aa3.

📒 Files selected for processing (37)
  • onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/tripresults/DirectionRowFocusTest.kt
  • onebusaway-android/src/main/graphql/otp2/Plan.graphql
  • onebusaway-android/src/main/java/org/onebusaway/android/api/adapters/Otp2PlanAdapters.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/api/adapters/TripPlanAdapters.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/directions/OtpObaIdResolver.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/directions/model/TripItinerary.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/directions/util/ConversionUtils.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/directions/util/DirectionsGenerator.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/map/DirectionsMapController.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/map/MapHost.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/map/MapViewModel.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/map/RouteMapController.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/map/RouteSegmentHighlight.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/map/ShowRouteRequest.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/arrivals/components/EtaStrip.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/compose/components/ScrollChevronGutter.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/CurrentFocus.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/CurrentFocusPersistence.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeSheetLogic.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeViewModel.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/directions/DirectionsFeature.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/MapFeature.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/tripresults/DirectionCardGrouping.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/tripresults/TripResultsRepository.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/tripresults/TripResultsScreen.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/tripresults/TripResultsUiState.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/tripresults/TripResultsViewModel.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/util/GeoPoint.kt
  • onebusaway-android/src/main/res/values/strings.xml
  • onebusaway-android/src/test/java/org/onebusaway/android/api/Otp2PlanDecodeTest.kt
  • onebusaway-android/src/test/java/org/onebusaway/android/api/OtpPlanDecodeTest.kt
  • onebusaway-android/src/test/java/org/onebusaway/android/directions/OtpObaIdResolverTest.kt
  • onebusaway-android/src/test/java/org/onebusaway/android/directions/util/ConversionUtilsTest.kt
  • onebusaway-android/src/test/java/org/onebusaway/android/map/RouteSegmentHighlightTest.kt
  • onebusaway-android/src/test/java/org/onebusaway/android/ui/home/HomeViewModelTest.kt
  • onebusaway-android/src/test/java/org/onebusaway/android/ui/tripresults/DirectionCardGroupingTest.kt
💤 Files with no reviewable changes (2)
  • onebusaway-android/src/test/java/org/onebusaway/android/api/OtpPlanDecodeTest.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/api/adapters/TripPlanAdapters.kt

Comment on lines +179 to +183
val zone = ZoneId.systemDefault()
val local = wallClock(time, zone)
val displayDate = Date.from(local.toInstant())
val localDay = local.toLocalDate()
val today = LocalDate.now(zone)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Read today from a parameter, not the clock, to keep these helpers pure.

getTimeWithContext and getTimeUpdated both call LocalDate.now(zone) internally for their today/tomorrow branching, while this same refactor extracted wallClock/isToday/isTomorrow as pure, parameterized (and directly unit-tested) helpers. These two entry points remain the one place still reading the clock inside the helper, and so remain untestable for their day-boundary branches.

♻️ Thread `today` through as a parameter
     fun getTimeWithContext(
         applicationContext: Context,
         time: Long,
         inLine: Boolean,
         color: Int = -1,
+        today: LocalDate = LocalDate.now(ZoneId.systemDefault()),
     ): CharSequence {
         ...
         val zone = ZoneId.systemDefault()
         val local = wallClock(time, zone)
         val displayDate = Date.from(local.toInstant())
         val localDay = local.toLocalDate()
-        val today = LocalDate.now(zone)

Apply the same pattern to getTimeUpdated's val today = LocalDate.now(zone) at line 245.

As per path instructions, **/*.{kt,java}: "Keep ETA and active-window helpers pure by passing the current time as a parameter instead of reading the clock inside the helper."

Also applies to: 240-245

🤖 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/directions/util/ConversionUtils.kt`
around lines 179 - 183, Update getTimeWithContext and getTimeUpdated to accept
today as a parameter and remove their internal LocalDate.now(zone) calls. Thread
the supplied today value through the existing today/tomorrow branching and
preserve the current zone-based behavior.

Source: Path instructions

bmander added 2 commits July 19, 2026 23:41
Resolve conflicts in DirectionsFeature.kt (keep both imports) and
TripResultsScreen.kt: adopt main's vendored AppIcons for the leg
expand/collapse chevron (main dropped the material-icons-core dependency),
keeping the PR's expandable board/alight card structure.
- RouteMapController.reframe(): update highlightedSegment from the request
  when it changes (re-emphasize polyline + re-filter stops), so tapping a
  different leg of the already-shown route doesn't keep the stale segment.
  start() already did this unconditionally; reframe() was the gap.
- ConversionUtils.getTimeWithContext/getTimeUpdated: take 'today' as a
  defaulted parameter instead of reading LocalDate.now() inside, matching
  the pure-helper rule the wallClock/isToday/isTomorrow extraction follows.
@bmander
bmander merged commit 63f0ade into main Jul 20, 2026
3 checks passed
@bmander
bmander deleted the feature/directions-drawer-scroll-picker branch July 20, 2026 07:09
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