Skip to content

Synthesize a rental deep link when the feed publishes none (#2158) - #2161

Draft
bmander wants to merge 1 commit into
mainfrom
issue-2158-synthesized-rental-deep-link
Draft

Synthesize a rental deep link when the feed publishes none (#2158)#2161
bmander wants to merge 1 commit into
mainfrom
issue-2158-synthesized-rental-deep-link

Conversation

@bmander

@bmander bmander commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Closes #2158 — or argues that it can't be closed this way. Draft on purpose: it works end to end, and on the device it makes the tap worse. Opening it so the finding can be taken to the iOS side, which ships the same link.

What it does

Ports onebusaway-ios' RentalDeepLink. When a rental publishes no rentalUris — which is all 12,958 vehicles the Puget Sound OTP2 deployment serves — the app builds the operator's own "show me this vehicle" URI from its scheme and the vehicle's id, ranked feed URI > synthesized > operator app > operator site.

  • RentalLink.Synthesized holds URI components, not a string: Uri.Builder escapes each query value, so a vehicle id carrying & or = stays one parameter. It's assembled at the moment of the tap, so generated_at isn't a stamp from whenever the sheet happened to load.
  • TripVehicleRental.kind (VEHICLE/STATION) records which of OTP's two rental shapes an endpoint came from, set at the adapter where OTP states it. A dock's station id can't be sent as a selected_vehicle_id, and that's structural rather than inferred from whether a name was published.
  • mayNeedTheirApp / namesTheVehicle moved onto RentalLink, so the fallback filter and the chip's wording stop type-testing the variants.

The sourcing came out better than the issue expected

The issue flagged the scheme as reverse-engineered (ubahnverleih/WoBike) and generated_at's unit as inferred. Both turn out to be first-party. Lime publishes rental_uris.android in the cities where it turns the field on — on 2026-08-03 all 7,097 vehicles in data.lime.bike/api/partners/v1/gbfs/washington_dc/free_bike_status carried:

limebike://map?selected_vehicle_id=IBO2JSMUXZVUQ&generated_at=1785807940

under the android key, byte-identical to .ios. So limebike:// is not an iOS-only scheme, and generated_at is epoch seconds read off published values rather than guessed from magnitude. Seattle simply has the field switched off.

Bird gets no synthesis, on its own evidence: its GBFS rental_apps.android.discovery_uri is bird://charger-onboarding — for people who charge scooters — and nothing there takes a vehicle id. iOS carries a bird:// app-launch entry; Android has no use for one, since OperatorApp("co.bird.android") already does that and sends a rider without the app to the Play listing instead of nowhere.

🚩 Why this is a draft

The id is the one step nothing published can source, and the device says it's wrong.

Lime's own links carry a short code (IBO2JSMUXZVUQ). The GBFS bike_id beside it in the same record is a UUID — and the UUID is all OTP hands us, since vehicleId is that bike_id under its network prefix. Tapping a link built from the UUID on a Pixel 7 Pro with Lime installed produced Lime's "Resource not found" error screen. Not its map.

That breaks the premise the whole approach rests on, on both platforms. "Failure is graceful" covers the case where no app claims the schemeExternalIntents.openFeedUri returns false and the caller falls back. It does not cover this: Lime claims the scheme, accepts the URI, and fails inside its own app, past the point any fallback can see. On today's evidence a rider is left worse off than with #2156's plain app launch.

What this means for onebusaway-ios

The URI is byte-identical to what RentalDeepLink.target(for:) emits for the same vehicle — same scheme, host, parameter, timestamp unit, same strip-through-the-first-colon on the same OTP network:id. iOS reads its rentals from the same OTP2 deployment, so it sends the same UUID.

RentalDeepLinkTests.swift is 15 tests, every one of them an assertion about URL construction — escaping, ranking, the station case. None can observe what Lime does with the URL. The synthesis has never been shown to select a bike on either platform; what's verified is that the string is well-formed.

Caveat worth stating: the Lime iOS app is a different client from the Lime Android app tested here. Near-certainly the same backend lookup, but not proven.

Options from here

  1. Drop Lime's template, keep the mechanism — nothing synthesizes today, ready if an id source appears.
  2. Revert to Show the bikeshare provider in directions rows (#2150) #2156 behaviour and close Synthesize a rental deep link when the operator's feed publishes none #2158 as blocked on Lime.
  3. Ship as-is if the iOS side has evidence the id space is fine.

Testing

compileObaGoogleDebugKotlin -PwarningsAsErrors=true clean, spotlessCheck clean, unit suite green. RentalPickupsTest covers the ordering, the dock fall-through, the id stripping and Bird's deliberate absence; the new instrumented RentalDeepLinksTest covers the Uri assembly (delimiters in an id, epoch-seconds truncation) and has not been device-run. The behaviour above was verified by hand on a Pixel 7 Pro.

#2156's "Open in Lime" chip never opened Lime on a vehicle: `rentalUris`
is null on all 12,958 rental vehicles the Puget Sound OTP2 deployment
serves, and the iOS survey of all 48 Lime systems in MobilityData's
catalog found the same worldwide. The chip fell through to launching the
app at its home screen, dropping the rider on a map they must re-find
their own bike on.

Ported from onebusaway-ios' `RentalDeepLink`: when the feed publishes no
link, build one from the operator's own URI shape and the vehicle's id,
ranked below a feed-published URI and above the plain app launch.

The shape is Lime's own rather than reverse-engineered. Lime publishes
`rental_uris.android` in the cities where it turns the field on — all
7,097 vehicles in its Washington DC feed carry
`limebike://map?selected_vehicle_id=<id>&generated_at=<seconds>`, under
the `android` key and identical to `.ios` — which settles both the
Android scheme (iOS's `limebike://` does transfer) and `generated_at`'s
unit, neither of which the app now has to infer. Bird gets no synthesis:
it takes no vehicle id, and its own Android `discovery_uri` is
`bird://charger-onboarding`, a link for people who charge scooters.

- URIs are assembled with `Uri.Builder` from held components, so an id
  carrying `&`/`=` stays one parameter, and re-resolved at the tap so the
  stamp isn't the one from when the sheet was opened.
- `TripVehicleRental.kind` records which of OTP's two rental shapes an
  endpoint came from, so a dock's station id is never sent as a vehicle
  id — structural, not read off whether a name was published.

Human sign-off gate (CLAUDE.md, "No unsanctioned heuristics"), with a
device result against it: Lime's published links carry a short code, not
the GBFS `bike_id` UUID that OTP hands us, and tapping a link built from
the UUID on a Pixel 7 Pro with Lime installed produced Lime's "Resource
not found" error screen. So the graceful-failure premise this port rests
on — and that the iOS original rests on — does not hold: Lime claims the
scheme, accepts the URI and fails inside its own app, past the point any
fallback can see. The URI is byte-identical to what iOS emits for the
same vehicle, and iOS's tests assert only that the URL is well-formed,
so neither platform has shown the link selecting a bike.

Draft for that conversation, not for merge as it stands.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a9409421-8d9a-44bb-8251-3cce87a7a881

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

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.

Synthesize a rental deep link when the operator's feed publishes none

1 participant