Skip to content

Unify stop and route focus banners#1897

Merged
bmander merged 3 commits into
mainfrom
agent/focus-banner
Jul 16, 2026
Merged

Unify stop and route focus banners#1897
bmander merged 3 commits into
mainfrom
agent/focus-banner

Conversation

@bmander

@bmander bmander commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace the route-only overlay with a shared focus banner for stops and routes
  • move stop identity, favorite, and service-alert actions out of the arrivals drawer and into the banner
  • add consistent left-side focus chrome with stop/route identity icons and favorite state
  • present focused-stop service alerts in a centered dialog
  • show nested route focus as compact route badges with its dismiss action in the banner
  • keep map framing and arrivals-sheet sizing synchronized with the complete measured banner
  • remove the obsolete drawer header and selected-row dismiss plumbing

Why

Stop focus and route focus previously presented their identity and actions in different parts of the screen. That split made the current focus harder to scan and caused banner actions to resolve at different times. A single focus surface gives both modes consistent placement, loading behavior, and map clearance.

Validation

  • ./gradlew check :onebusaway-android:compileObaGoogleDebugAndroidTestKotlin -x lint -PwarningsAsErrors=true
  • Google and MapLibre unit-test variants
  • 5 focused FocusBannerTest instrumentation tests on a Pixel 7 Pro
  • installed and manually reviewed the final build on-device

Part of #1894 — this PR lays the shared stop/route focus-banner groundwork; the bike-station
variant the issue calls for is a follow-up, so it intentionally does not auto-close #1894.

Summary by CodeRabbit

  • New Features

    • Introduced a map focus banner for stops and routes, including favorites, recentering, service alerts, route actions, and direction selection.
    • Added a service alerts dialog for browsing and selecting alerts.
    • Added an improved way to close map focus, clearing stop and any subordinate route selection together.
  • Improvements

    • Updated map layout/spacing to frame the focus banner correctly.
    • Refreshed arrivals service-alerts rendering and simplified arrivals row selection controls.
  • Tests

    • Added new UI coverage for focus-banner accessibility and service-alert dialog behavior.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The home map overlay now uses a stop-or-route FocusBanner, while arrivals state is managed through a shared ArrivalsSession. Service alerts move into a modal dialog, and map padding tracks the focus banner edge. Legacy route-header and arrivals-header UI wiring is removed.

Changes

Focus banner and arrivals refactor

Layer / File(s) Summary
Arrivals session, panel, and service-alert flow
onebusaway-android/src/main/java/org/onebusaway/android/ui/home/arrivals/..., onebusaway-android/src/main/java/org/onebusaway/android/ui/arrivals/...
Arrivals ownership moves into a stop-keyed session, ArrivalsPanel renders supplied state, service alerts use shared content and a modal dialog, and selected-route close UI is removed.
Focus banner UI and home integration
onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/..., onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt, onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/home/map/FocusBannerTest.kt
A new stop-or-route FocusBanner supports favorite, alert, recenter, subordinate-route, framing, and direction actions, replacing RouteHeaderOverlay in HomeScreen.
Map focus geometry and clearing
onebusaway-android/src/main/java/org/onebusaway/android/map/..., 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/test/java/org/onebusaway/android/...
Map padding and sheet positioning use the focus-banner bottom edge, while clearMapFocus() clears focus state and emits ClearFocus.

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

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR does not implement the requested BikeStation variant or station-focused banner behavior from #1894. Add BikeStation support to FocusBannerState and wire selected bike-station identity, info, padding, and close actions.
Out of Scope Changes check ⚠️ Warning Most changes overhaul stop/route focus UI, alerts, and map framing, but #1894 only asks for bike-station focus support. Trim unrelated stop/route refactors or add the missing bike-station banner work so the PR aligns with #1894.
Docstring Coverage ⚠️ Warning Docstring coverage is 45.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main refactor: unifying the stop and route focus banners.
✨ 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 agent/focus-banner

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.

@bmander
bmander marked this pull request as ready for review July 16, 2026 20:03

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
onebusaway-android/src/main/java/org/onebusaway/android/ui/arrivals/components/ArrivalsPanel.kt (1)

80-92: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clear stale short-list height when the next stop is not fully laid out.

HomeScreen retains contentPx across stops, but this callback emits nothing when the new list’s last item is off-screen. Switching from a short stop to a long stop therefore leaves the old short height active and keeps the new peek undersized. Report the “content exceeds viewport” case or reset the measurement when the session changes.

🤖 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/arrivals/components/ArrivalsPanel.kt`
around lines 80 - 92, Update the contentHeightPx measurement and onContentHeight
callback in ArrivalsPanel so a new stop clears the previous height when the last
item is not fully laid out, while preserving the measured height for fully
laid-out short lists. Ensure the measurement is reset or emits the “content
exceeds viewport” state when the session/content changes, preventing
HomeScreen’s retained contentPx from leaving the peek undersized.
🤖 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/androidTest/java/org/onebusaway/android/ui/home/arrivals/ServiceAlertsDialogTest.kt`:
- Around line 28-30: Resolve the deprecated API warning in the composeRule
declarations for ServiceAlertsDialogTest.kt lines 28-30 and FocusBannerTest.kt
lines 35-37 by replacing the deprecated API where possible; if suppression
remains necessary, add a one-line rationale with a tracking-issue link to each
`@Suppress`("DEPRECATION") declaration.

In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt`:
- Around line 214-222: Update the focusBannerBottomPx state declaration in the
HomeScreen composable to use unkeyed remember so focus changes preserve the
measured banner height. Reset this state only when the FocusBanner disappears,
while retaining the existing focusBannerTopEdge and setFocusBannerBottomEdge
flow.

In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeViewModel.kt`:
- Around line 473-480: Update clearMapFocus() to reset both pending map-focus
fields alongside the existing focus hierarchy state. Ensure pendingMapFocus and
its associated pending stop latch are cleared when the explicit close control
runs, preventing later arrivals from consuming stale focus state.

In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBanner.kt`:
- Around line 82-114: Add a BikeStation variant to FocusBannerState and render
it using the appropriate bike-station presentation. In
onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt:356-381,
map CurrentFocus.BikeStation to that state. In
onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeSheetLogic.kt:37-44,
return the measured banner edge for BikeStation focus alongside the existing
stop and route cases.
- Around line 72-76: Give CompactRouteDismissAction a minimum interactive touch
target instead of using the 22dp dismiss icon bounds as the click area, while
preserving the existing icon appearance and shared 40dp sizing conventions in
FocusBanner.kt. Update the related FocusBannerTest assertion to verify a usable
minimum target size rather than exactly 22dp. Affected sites: FocusBanner.kt
lines 72-76 define the sizing context and require no direct change unless needed
for reuse; FocusBanner.kt lines 300-310 require the dismiss-action container
change; FocusBanner.kt lines 436-449 require no direct change unless they share
the same dismiss-action implementation; FocusBannerTest.kt lines 133-146 require
the expectation update.

In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBannerViewModel.kt`:
- Around line 38-49: Update toggleRouteFavorite in FocusBannerViewModel to
serialize rapid favorite updates or use an atomic repository-level toggle,
rather than deriving favorite from the potentially stale favoriteRouteIds
snapshot. Ensure consecutive taps are applied in order so two toggles restore
the route to its original state.

In
`@onebusaway-android/src/test/java/org/onebusaway/android/ui/home/HomeSheetLogicTest.kt`:
- Around line 44-53: The test around focusBannerTopEdge must treat
CurrentFocus.BikeStation as using the shared focus banner rather than a
bannerless state. Update the bike-station expectation in `stop and route focus
use the measured focus banner edge` and add the corresponding bike-station
banner presentation in the focus UI logic, reusing the existing banner behavior
and measured-edge handling.

---

Outside diff comments:
In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/arrivals/components/ArrivalsPanel.kt`:
- Around line 80-92: Update the contentHeightPx measurement and onContentHeight
callback in ArrivalsPanel so a new stop clears the previous height when the last
item is not fully laid out, while preserving the measured height for fully
laid-out short lists. Ensure the measurement is reset or emits the “content
exceeds viewport” state when the session/content changes, preventing
HomeScreen’s retained contentPx from leaving the peek undersized.
🪄 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: 3c8ffce1-bc6c-47f0-b127-a04cfa938cca

📥 Commits

Reviewing files that changed from the base of the PR and between 2049fdb and 1d6b124.

📒 Files selected for processing (22)
  • onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/arrivals/ArrivalsPanelHeaderTest.kt
  • onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/home/arrivals/ServiceAlertsDialogTest.kt
  • onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/home/map/FocusBannerTest.kt
  • onebusaway-android/src/main/AndroidManifest.xml
  • 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/render/MapRenderState.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/arrivals/ArrivalsScreen.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/arrivals/components/ArrivalRows.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/arrivals/components/ArrivalsPanel.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/arrivals/ArrivalsSheetHost.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/arrivals/ServiceAlertsDialog.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBanner.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBannerViewModel.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/RouteHeaderOverlay.kt
  • onebusaway-android/src/main/res/values/strings.xml
  • onebusaway-android/src/test/java/org/onebusaway/android/map/render/MapRenderStatePaddingTest.kt
  • onebusaway-android/src/test/java/org/onebusaway/android/ui/home/HomeSheetLogicTest.kt
  • onebusaway-android/src/test/java/org/onebusaway/android/ui/home/HomeViewModelTest.kt
💤 Files with no reviewable changes (3)
  • onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/arrivals/ArrivalsPanelHeaderTest.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/RouteHeaderOverlay.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/arrivals/components/ArrivalRows.kt

Comment on lines +28 to +30
@Suppress("DEPRECATION")
@get:Rule
val composeRule = createComposeRule()

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

Resolve or properly document both compiler-warning suppressions.

  • onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/home/arrivals/ServiceAlertsDialogTest.kt#L28-L30: fix the deprecated API warning or add the required rationale and issue link.
  • onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/home/map/FocusBannerTest.kt#L35-L37: apply the same treatment.

As per coding guidelines, “Maintain zero Kotlin compiler warnings. Fix warnings at their source; use @Suppress only when necessary, with a one-line rationale and tracking-issue link.”

📍 Affects 2 files
  • onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/home/arrivals/ServiceAlertsDialogTest.kt#L28-L30 (this comment)
  • onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/home/map/FocusBannerTest.kt#L35-L37
🤖 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/androidTest/java/org/onebusaway/android/ui/home/arrivals/ServiceAlertsDialogTest.kt`
around lines 28 - 30, Resolve the deprecated API warning in the composeRule
declarations for ServiceAlertsDialogTest.kt lines 28-30 and FocusBannerTest.kt
lines 35-37 by replacing the deprecated API where possible; if suppression
remains necessary, add a one-line rationale with a tracking-issue link to each
`@Suppress`("DEPRECATION") declaration.

Source: Coding guidelines

Comment on lines +72 to +76
// The banner's route action icons (switch-direction / cancel) share one size + tint so they read as
// one control group: a larger-than-default 36dp icon in a deliberately tightened 40dp touch box
// (below Material's 48dp default — a conscious trade-off for a compact header banner).
private val HEADER_ICON_SIZE = 36.dp
private val HEADER_ICON_BUTTON_SIZE = 40.dp

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure first.
git ls-files onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBanner.kt \
             onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/home/map/FocusBannerTest.kt

echo
echo "== FocusBanner.kt outline =="
ast-grep outline onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBanner.kt --view expanded

echo
echo "== FocusBannerTest.kt outline =="
ast-grep outline onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/home/map/FocusBannerTest.kt --view expanded

Repository: OneBusAway/onebusaway-android

Length of output: 1441


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== FocusBanner.kt (header icon + compact dismiss sections) =="
sed -n '60,90p;290,330p;430,470p' onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBanner.kt

echo
echo "== FocusBannerTest.kt (dismiss target test) =="
sed -n '125,155p' onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/home/map/FocusBannerTest.kt

Repository: OneBusAway/onebusaway-android

Length of output: 5892


🌐 Web query:

Compose Material3 IconButton modifier size minimum touch target 48dp

💡 Result:

In Material Design 3 and Jetpack Compose, the standard IconButton component is designed with a minimum touch target size of 48dp x 48dp to meet accessibility guidelines [1][2]. Key points regarding touch target sizing: 1. Automatic Handling: Many Material 3 components, including IconButton, automatically enforce this 48dp minimum touch target size [1][2]. Even if an icon appears visually smaller (e.g., 24dp x 24dp), the underlying component adds the necessary padding or expansion to ensure the total interactive area reaches 48dp x 48dp [3][4][5]. 2. Modifier.minimumInteractiveComponentSize: For custom components or when you need to explicitly ensure a component meets the 48dp minimum size requirement in your layout, you can use the Modifier.minimumInteractiveComponentSize modifier [6][5]. This modifier reserves at least 48dp in size for the component, preventing touch target overlap and ensuring accessibility compliance [6]. 3. Important Consideration: While Compose automatically expands the touch target region of interactive elements if they are smaller than 48dp, this can lead to overlapping touch areas if elements are placed too close together [7][5]. To avoid this, it is recommended to ensure that interactive elements are either visually sized to 48dp or spaced with sufficient padding (typically at least 8dp) to prevent touch target overlap [3][7][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== FocusBanner.kt around CompactRouteDismissAction and its caller =="
sed -n '240,340p' onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBanner.kt

echo
echo "== Search for minimumInteractiveComponentSize / minimum touch target in the repo =="
rg -n "minimumInteractiveComponentSize|minimumTouchTarget|touch target|minimum touch" onebusaway-android/src/main/java onebusaway-android/src/androidTest -S

Repository: OneBusAway/onebusaway-android

Length of output: 4641


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '410,440p' onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBanner.kt

Repository: OneBusAway/onebusaway-android

Length of output: 1184


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '440,460p' onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBanner.kt

Repository: OneBusAway/onebusaway-android

Length of output: 975


Give the dismiss icon a minimum touch target.

  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBanner.kt#L300-L310: CompactRouteDismissAction still makes the 22dp icon the full click target; wrap it in a minimum interactive-size container.
  • onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/home/map/FocusBannerTest.kt#L133-L146: assert a usable target size instead of exactly 22dp.
📍 Affects 2 files
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBanner.kt#L72-L76 (this comment)
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBanner.kt#L300-L310
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBanner.kt#L436-L449
  • onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/home/map/FocusBannerTest.kt#L133-L146
🤖 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/map/FocusBanner.kt`
around lines 72 - 76, Give CompactRouteDismissAction a minimum interactive touch
target instead of using the 22dp dismiss icon bounds as the click area, while
preserving the existing icon appearance and shared 40dp sizing conventions in
FocusBanner.kt. Update the related FocusBannerTest assertion to verify a usable
minimum target size rather than exactly 22dp. Affected sites: FocusBanner.kt
lines 72-76 define the sizing context and require no direct change unless needed
for reuse; FocusBanner.kt lines 300-310 require the dismiss-action container
change; FocusBanner.kt lines 436-449 require no direct change unless they share
the same dismiss-action implementation; FocusBannerTest.kt lines 133-146 require
the expectation update.

Comment on lines +82 to +114
sealed interface FocusBannerState {
val isFavorite: Boolean
val favoriteEnabled: Boolean
@get:DrawableRes val focusIconRes: Int
@get:StringRes val focusDescriptionRes: Int

data class SubordinateRoute(
val shortName: String,
val color: Int? = null,
)

data class Stop(
val title: String,
val direction: String?,
override val isFavorite: Boolean,
override val favoriteEnabled: Boolean,
val hasAlerts: Boolean,
val subordinateRoutes: List<SubordinateRoute> = emptyList(),
val subordinateHeadsign: String? = null,
) : FocusBannerState {
override val focusIconRes = R.drawable.stop_flag
override val focusDescriptionRes = R.string.stop_shortcut
}

data class Route(
val header: RouteHeader,
override val isFavorite: Boolean,
) : FocusBannerState {
override val favoriteEnabled: Boolean get() = header.routeId != null
override val focusIconRes = R.drawable.ic_route
override val focusDescriptionRes = R.string.route_shortcut
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Implement bike-station focus across presentation and geometry.

The linked issue requires bike-share stations, but this flow remains stop-or-route only.

  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBanner.kt#L82-L114: add a BikeStation presentation variant and rendering.
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt#L356-L381: construct that state from CurrentFocus.BikeStation.
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeSheetLogic.kt#L37-L44: return its measured banner edge for bike-station focus.
📍 Affects 3 files
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBanner.kt#L82-L114 (this comment)
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt#L356-L381
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeSheetLogic.kt#L37-L44
🤖 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/map/FocusBanner.kt`
around lines 82 - 114, Add a BikeStation variant to FocusBannerState and render
it using the appropriate bike-station presentation. In
onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt:356-381,
map CurrentFocus.BikeStation to that state. In
onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeSheetLogic.kt:37-44,
return the measured banner edge for BikeStation focus alongside the existing
stop and route cases.

Comment on lines +44 to +53
fun `stop and route focus use the measured focus banner edge`() {
val stopRoute = StopRouteSelection(
originHeadsign = null,
legs = listOf(RouteLeg("route", "40")),
)
assertEquals(120, routeFocusTopEdge(CurrentFocus.Stop(stop, stopRoute), 120, 240))
assertEquals(240, routeFocusTopEdge(CurrentFocus.Route(RouteTarget("route")), 120, 240))
assertEquals(0, routeFocusTopEdge(CurrentFocus.Stop(stop), 120, 240))
assertEquals(0, routeFocusTopEdge(CurrentFocus.None, 120, 240))
assertEquals(240, focusBannerTopEdge(CurrentFocus.Stop(stop, stopRoute), 240))
assertEquals(240, focusBannerTopEdge(CurrentFocus.Route(RouteTarget("route")), 240))
assertEquals(240, focusBannerTopEdge(CurrentFocus.Stop(stop), 240))
assertEquals(0, focusBannerTopEdge(CurrentFocus.None, 240))
assertEquals(0, focusBannerTopEdge(CurrentFocus.BikeStation("bike"), 240))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not codify bike-station focus as bannerless.

Line 53 expects no measured edge for CurrentFocus.BikeStation, but issue #1894 requires bike stations to use the shared focus banner and reserve map padding. Add the bike-station banner presentation and expect its measured edge here instead.

🤖 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/home/HomeSheetLogicTest.kt`
around lines 44 - 53, The test around focusBannerTopEdge must treat
CurrentFocus.BikeStation as using the shared focus banner rather than a
bannerless state. Update the bike-station expectation in `stop and route focus
use the measured focus banner edge` and add the corresponding bike-station
banner presentation in the focus UI logic, reusing the existing banner behavior
and measured-edge handling.

- Document the createComposeRule() @Suppress("DEPRECATION") in the two new
  test files with the standard #1792 rationale, matching the repo convention.
- Keep focusBannerBottomPx unkeyed so switching between two equal-height
  banners doesn't reset it to 0 (onSizeChanged only fires on size change),
  which framed the map as if no banner showed.
- Clear the pending map-focus latches in clearMapFocus() so a stop closed
  before its arrivals load can't leak a recenter onto the next stop.
- Serialize route-favorite toggles via an optimistic override so rapid
  double-taps build on the last intent instead of a stale store snapshot;
  the override is reconciled away once the store catches up.

Bike-station focus (the rest of #1894) is deferred to a follow-up; the PR
description now references the issue without auto-closing it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@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.

♻️ Duplicate comments (1)
onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBannerViewModel.kt (1)

63-64: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Read from the synchronous override state to prevent the rapid-tap race condition.

Because favoriteRouteIds is a combined StateFlow, its value updates asynchronously after an optimistic override. A second rapid tap will read the stale favoriteRouteIds.value before the flow emits the result of the first tap, effectively ignoring the second toggle.

Read directly from the immediate optimisticFavorites map to ensure back-to-back taps compute their intent sequentially, fulfilling the PR objective to serialize rapid updates.

🐛 Proposed fix
-        val favorite = routeId !in favoriteRouteIds.value
-        optimisticFavorites.update { it + (routeId to favorite) }
+        val favorite = !(optimisticFavorites.value[routeId] ?: (routeId in favoriteRouteIds.value))
+        optimisticFavorites.update { it + (routeId to favorite) }
🤖 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/map/FocusBannerViewModel.kt`
around lines 63 - 64, Update the toggle logic in the surrounding ViewModel
method to derive the current favorite state from the immediate
optimisticFavorites map, falling back to favoriteRouteIds only when no override
exists. Compute the next toggle from that synchronous state before updating
optimisticFavorites, so consecutive taps are applied sequentially.
🤖 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.

Duplicate comments:
In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBannerViewModel.kt`:
- Around line 63-64: Update the toggle logic in the surrounding ViewModel method
to derive the current favorite state from the immediate optimisticFavorites map,
falling back to favoriteRouteIds only when no override exists. Compute the next
toggle from that synchronous state before updating optimisticFavorites, so
consecutive taps are applied sequentially.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f81d16d-f17e-4aec-992d-43fa59bc0b28

📥 Commits

Reviewing files that changed from the base of the PR and between 1d6b124 and e90277f.

📒 Files selected for processing (5)
  • onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/home/arrivals/ServiceAlertsDialogTest.kt
  • onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/home/map/FocusBannerTest.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeViewModel.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/FocusBannerViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (4)
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeViewModel.kt
  • onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/home/arrivals/ServiceAlertsDialogTest.kt
  • onebusaway-android/src/androidTest/java/org/onebusaway/android/ui/home/map/FocusBannerTest.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt

@bmander
bmander merged commit af1833d into main Jul 16, 2026
3 checks passed
@bmander
bmander deleted the agent/focus-banner branch July 16, 2026 20:44
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.

Add bike-station support to the home focus banner

1 participant