Skip to content

Google Find Hub as a second location network - #244

Merged
parawanderer merged 6 commits into
parawanderer:mainfrom
alex-so-3:feature/find-hub-source
Oct 13, 2026
Merged

parawanderer merged 6 commits into
parawanderer:mainfrom
alex-so-3:feature/find-hub-source

Conversation

@alex-so-3

@alex-so-3 alex-so-3 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Adds Google Find Hub as a second location network next to Apple's.

The main use is tags that advertise on both networks, such as DIY trackers (e.g. Everytag-style firmware) that send Apple Find My and Google Find Hub frames at the same time. Such a tag is one device in the app, and the location reports from both networks are merged into its position and history. Tags that are only on Google work too.

Google reports come from a small self-hosted service, googlefind-service (Docker, built on GoogleFindMyTools). The app only talks to that service; nothing Google-specific is signed in on the phone. Without it configured, nothing changes.

What it adds:

  • Settings → Extra location sources: the service URL, an API token, and optional HTTP Basic Auth for a reverse proxy.
  • Add from Google Find Hub: adds a tracker of the Google account as a device.
  • Link Google tracker: on an existing device, so reports from both networks land on the same device. The map card says which network a position came from.
  • Google is asked for fresh locations only when the user refreshes a tag. A tracker with no reports yet is located once.
  • Ringing Google trackers: the owner ring from the Find Hub (FMDN) spec, over the Beacon Actions characteristic. The ring key and current EIDs come from the service; the ring key can make the tracker ring but cannot decrypt its locations.
  • Nearby for Google trackers: recognised by their EID in the 0xFEAA service data. Their battery flags are encrypted, so only the signal is shown.
  • Add OpenHaystack key: imports a .keys file (Macless Haystack / OpenHaystack format), including files with several keys for tags that rotate through them. They become ordinary custom_rolling_key_accessory devices, fetched through the Apple account like any self-made tag.
  • Ring failures on the map card are shown instead of being retried silently.

There is no Room schema change. The Google-specific data (the linked tracker id) is kept in SharedPreferences, because FindMy.py rewrites the accessory JSON and drops unknown keys. New strings are in all locales.

Depends on #242 and #241. This branch contains both commits: the ring code needs stopSound, and the map card and Nearby need the accessory JSON on the model. Once those are merged, the diff shrinks to the one feature commit.

Verified: on a phone with a Google account that has two off-the-shelf Find Hub trackers and one DIY tag on both networks:

  • the Google trackers are added, located on refresh, ring from the map card and from the device menu, and show up in Nearby;
  • the DIY tag linked to its Google tracker shows as one device with reports from both networks.

./gradlew testAll and add_strings.py --check pass. New JVM tests cover the FMDN ring command, report merging and .keys parsing.

Not verified:

  • the instrumented suite: I did not add instrumented tests for the two new screens (settings and import dialogs);
  • AMap with Google reports;
  • the Find Hub ring on trackers other than the two cheap ones I have. Vendors may differ from the spec, so the app retries the few layouts seen in practice.

@parawanderer

Copy link
Copy Markdown
Owner

Thanks for this huge contribution! This is the sort of thing I've been looking forward to myself in terms of getting the aggregate data off of multiple networks in one place, so I am quite thankful that somebody decided to implement it here! Apologies for not getting to this sooner as I had been busy with personal and other projects. Thank you as well for the notes and clarifications on this.

Reviewing it now & with a model, the prerequisite PRs were merged. I will likely add tests to keep this stable long-term.

alex-so-3 and others added 3 commits October 13, 2026 12:37
…-key .keys import

Adds Google Find Hub as a second location network next to Apple's, through a self-hosted
service built on GoogleFindMyTools (the "googlefind" service).

- Google trackers of the account can be added as devices (menu: "Add from Google Find Hub"),
  or linked to any existing device. A tag that advertises on both networks is then one
  device in the app, with the reports of both merged into its location and history. Reports
  carry provenance "google"; the map card says which network a position came from. Fresh
  locations are requested from Google only on a manual refresh; a tracker with no reports
  yet is located once.
- OpenHaystack keys can be imported from a .keys file ("Add OpenHaystack key"), including
  files holding several keys for tags that rotate through them; they are ordinary
  custom_rolling_key_accessory devices fetched through the Apple account.
- Ringing Google trackers: owner ring over the FMDN Beacon Actions characteristic, with the
  ring key and current EIDs from the googlefind service. Google trackers also show as
  "Nearby", recognised by EID (their battery flags are encrypted, so signal only).
- Ring failures on the map card are shown instead of being retried silently.
- Settings > Extra location sources: service URL, API token, optional HTTP Basic Auth.
- No Room schema change; strings in all locales.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016xeHfDLMuUeusX9PEgNEmY
…crypted

- FmdnRingTrigger wrote the descriptor and characteristic with API 33 overloads (minSdk is 24), which throws NoSuchMethodError on Android 12 and below, and only overrode the API 33 read and notify callbacks, so a ring there would wait out its timeout. It now uses BleGattSoundTrigger's compat helpers and the older callbacks too.
- A Google tracker's ring is now stopped when ringing stops: hasStopCommand covers FMDN as well as DULT.
- The API token and HTTP password are stored encrypted with a keystore key, like the Apple session. A value saved in plain text before this is moved over on first read.
- An http:// service address gets a warning under the field, since the app permits cleartext.
- The map card names the network only for Google reports, so Apple users' cards are unchanged, and the ring-failure toast is shown for Google trackers only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the menus, the Settings entry and its explainer, in every locale, and in the README's table of what the app supports. Removes source_apple, which nothing uses any more.

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

Copy link
Copy Markdown
Owner

Rebased onto main (force-pushed). #241, #242 and #243 are merged, so the branch is now your Find Hub commit plus two on top:

  • 71b975b: FmdnRingTrigger used the API 33 writeDescriptor/writeCharacteristic overloads (minSdk is 24), which throw NoSuchMethodError on Android 12 and below; it now uses BleGattSoundTrigger's compat helpers and also overrides the pre-33 read/notify callbacks. hasStopCommand covers FMDN, so stopping the ring stops a Google tracker. The API token and HTTP password are stored encrypted with a keystore key, and an http:// address gets a warning. The map card names the network only for Google reports, and the ring-failure toast is shown for Google trackers only, so nothing changes for users without the service.
  • b411978: Google Find Hub is marked experimental in the menus, Settings and the README.

Left for you, not blocking:

  • ReportSources calls googleReports(id, 0), up to 5000 rows, on every scheduled refresh of a tracker with nothing in the last 7 days, only to test for emptiness.
  • FmdnNearbyWatcher fetches /ring, ring key included, on every map resume just for the EIDs.
  • SourceLinks.forget() is never called, so a removed device keeps its Google link.
  • FmdnRingCommand.authKey duplicates the HMAC in ringWrite and is only used by the test.
  • EID matching assumes 20-byte EIDs; 32-byte EIDs never match.

Drafted by Claude Code, reviewed by @parawanderer

- ReportSourcesTest drives fetch through seams for the settings, links, service and clock: with no service configured an Apple device's fetch is returned untouched, a Google tracker never reaches Apple, a failure on either side keeps the other's reports, and a never-located tracker is asked for at most every 30 minutes.
- ExtraSourcesApiTest runs the client against a fake service on 127.0.0.1: Bearer versus Basic Auth with the token in X-Api-Token, a refusal naming its status, positionless reports skipped, EIDs lowercased. It needs the real org.json as a test dependency, since android.jar's is a stub on the JVM.
- On a device: ExtraSourcesSettingsTest checks the token and password are not on disk in plain text and that a plain value is moved over; ExtraSourcesLayoutTest inflates the screen in light and dark mode; ExtraSourcesSettingsScreenTest drives the http warning, the empty test, and saving.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SpinnersLookLikeSpinnersTest walks every layout, and the test-connection spinner had no track, square ends and no inward show/hide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@parawanderer
parawanderer deployed to Android Build October 13, 2026 11:13 — with GitHub Actions Active
@parawanderer
parawanderer deployed to Android Build October 13, 2026 11:13 — with GitHub Actions Active
The feature needs a self-hosted service, and nothing in the app said how to set one up. The settings screen now has "How to set this up", opening a wiki page that covers the service, what its secrets.json gives access to, and the app side. The README row points there too.

GoogleFindHubGuideLinkTest pins the page's address, since the wiki is where nothing would notice it break.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@parawanderer
parawanderer deployed to Android Build October 13, 2026 12:21 — with GitHub Actions Active
@parawanderer
parawanderer deployed to Android Build October 13, 2026 12:21 — with GitHub Actions Active
@parawanderer
parawanderer merged commit d5844b4 into parawanderer:main Oct 13, 2026
4 checks passed

This branch was successfully deployed

1 active deployment
Android Build — ce4806ed Deployed Oct 13, 2026 by parawanderer via build #325
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.

2 participants