Skip to content

List every Find My device in earshot, not just the imported ones - #264

Open
rubo77 wants to merge 3 commits into
parawanderer:mainfrom
rubo77:feature/nearby-scanner
Open

rubo77 wants to merge 3 commits into
parawanderer:mainfrom
rubo77:feature/nearby-scanner

Conversation

@rubo77

@rubo77 rubo77 commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

The app's BLE code only answers "is this one of my tags" - NearbyTagWatcher drops any frame whose address is not in the rolling-key index. This adds the other question, "what is out there at all", behind a new menu entry on the devices list.

  • ble/FindMyFrameKey: reconstructs the advertisement key from the 0x12 TLV and the BLE address it arrived on, for both payload shapes. Separated (len 0x19) yields the full 28-byte key over MAC + payload; nearby (len 0x02) yields only the six-byte prefix - which is the join key across both shapes inside one rotation, and finer than the MAC alone because it carries the two key bits the address cannot express. Walks the Apple TLV chain rather than reading offset zero, since iPhones prepend their own payloads.
  • ble/NearbyDeviceSighting + NearbyDeviceScanner: the generic counterpart of NearbyTagWatcher - same hardware scan filter (0x004C + type 0x12), same 20-minute scan restart ahead of the platform's long-scan downgrade, but no index, no MAC resolver, no Python. Emits every frame that parses.
  • ble/NearbyDeviceBook: one row per key prefix, newest sighting wins, pruned after 30s of silence - a tag that leaves sends nothing, so silence is the only departure signal. Sorted strongest signal first.
  • ble/ForeignTagNames: user names for foreign devices in SharedPreferences (deliberately not Room - a twelve-hex-chars-to-name map is what it is for, and a schema change would mean a migration). Keyed on the prefix so a name survives the separated/nearby flip; rotation re-keying is a different feature (rotation-tracking branch).
  • util/nearby/ProximityDistance: RSSI to rough metres (log-distance path loss, -59 dBm at 1m, rendered with "~" - honest about the precision the physics carries).
  • NearbyDevicesActivity + layout + NearbyDeviceListAdaptor + row layout: the screen the scanner exists for. Subscribes only while on top, rebuilds the list on sightings and on a 1s tick so departures age out live. Long-press renames via the same name dialog the tag page uses. BLE permission is asked once per screen, via BlePermissions like Maps.
  • Manifest declares the activity; my_devices_menu gains the entry, worded as a scan so nobody mistakes it for a network lookup.
  • 8 new strings across all 10 locales via add_strings.py.
  • Tests: FindMyFrameKeyTest pins the key layout for both shapes, the prefix join between them, the non-leading-TLV walk, and rejects junk; NearbyDeviceBookTest pins newest-wins, stale-resend, sort and pruning.

Not verified against a physical tag: no AirTag was at hand during development. The key reconstruction follows the layout FindMy.py's scanner.py implements and is pinned by JVM tests; frames where 0x12 does not lead the manufacturer blob are parsed by FindMyFrameKey but currently dropped upstream of it (see NearbyDeviceSighting.tlvBytes).

Copilot AI balanced review requested due to automatic review settings October 14, 2026 21:10
@rubo77
rubo77 deployed to Android Build October 14, 2026 21:10 — with GitHub Actions Active
@rubo77
rubo77 deployed to Android Build October 14, 2026 21:10 — with GitHub Actions Active

Copilot AI 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.

🟡 Changes recommended

Subscription leaks and incomplete scan handling can leave scanning active after departure or prevent nearby devices from appearing.

5 open findings
What changed in this PR

Adds a Bluetooth discovery screen alongside the imported-tag list to display nearby Find My devices.

Changes:

  • Adds advertisement-key parsing, live sighting aggregation, and scanning.
  • Adds device naming, approximate distances, and a localized discovery screen.
  • Adds JVM tests for key reconstruction and sighting aggregation.
File Description
app/​src/​test/​java/​dev/​wander/​android/​opentagviewer/​ble/​NearbyDeviceBookTest.java Tests aggregation and expiry.
app/​src/​test/​java/​dev/​wander/​android/​opentagviewer/​ble/​FindMyFrameKeyTest.java Tests key reconstruction.
app/​src/​main/​res/​values/​strings.xml Adds default discovery strings.
app/​src/​main/​res/​values-zh-rTW/​strings.xml Adds Traditional Chinese strings.
app/​src/​main/​res/​values-zh-rCN/​strings.xml Adds Simplified Chinese strings.
app/​src/​main/​res/​values-ru/​strings.xml Adds Russian strings.
app/​src/​main/​res/​values-nl/​strings.xml Adds Dutch strings.
app/​src/​main/​res/​values-ko/​strings.xml Adds Korean strings.
app/​src/​main/​res/​values-ja/​strings.xml Adds Japanese strings.
app/​src/​main/​res/​values-fr/​strings.xml Adds French strings.
app/​src/​main/​res/​values-en/​strings.xml Adds English strings.
app/​src/​main/​res/​values-de/​strings.xml Adds German strings.
app/​src/​main/​res/​menu/​my_devices_menu.xml Adds scan menu entry.
app/​src/​main/​res/​layout/​nearby_device_list_item.xml Defines device rows.
app/​src/​main/​res/​layout/​activity_nearby_devices.xml Defines discovery screen.
app/​src/​main/​java/​dev/​wander/​android/​opentagviewer/​util/​nearby/​ProximityDistance.java Estimates distance from signal strength.
app/​src/​main/​java/​dev/​wander/​android/​opentagviewer/​ui/​nearbydevices/​NearbyDeviceListAdaptor.java Displays sightings and names.
app/​src/​main/​java/​dev/​wander/​android/​opentagviewer/​NearbyDevicesActivity.java Coordinates scanning and interaction.
app/​src/​main/​java/​dev/​wander/​android/​opentagviewer/​MyDevicesListActivity.java Opens discovery screen.
app/​src/​main/​java/​dev/​wander/​android/​opentagviewer/​ble/​NearbyDeviceSighting.java Converts advertisements into sightings.
app/​src/​main/​java/​dev/​wander/​android/​opentagviewer/​ble/​NearbyDeviceScanner.java Scans without imported-tag matching.
app/​src/​main/​java/​dev/​wander/​android/​opentagviewer/​ble/​NearbyDeviceBook.java Groups, sorts, and expires sightings.
app/​src/​main/​java/​dev/​wander/​android/​opentagviewer/​ble/​ForeignTagNames.java Persists prefix-keyed names.
app/​src/​main/​java/​dev/​wander/​android/​opentagviewer/​ble/​FindMyFrameKey.java Reconstructs advertisement keys.
app/​src/​main/​AndroidManifest.xml Registers discovery activity.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread app/src/main/java/dev/wander/android/opentagviewer/ble/NearbyDeviceScanner.java Outdated
Comment thread app/src/main/java/dev/wander/android/opentagviewer/NearbyDevicesActivity.java Outdated
Comment thread app/src/main/java/dev/wander/android/opentagviewer/NearbyDevicesActivity.java Outdated
Comment thread app/src/main/java/dev/wander/android/opentagviewer/NearbyDevicesActivity.java Outdated
Comment thread app/src/main/res/layout/nearby_device_list_item.xml Outdated
@rubo77
rubo77 deployed to Android Build October 14, 2026 21:32 — with GitHub Actions Active
@rubo77
rubo77 deployed to Android Build October 14, 2026 21:32 — with GitHub Actions Active
@parawanderer

Copy link
Copy Markdown
Owner

#260 is merged and makes scripts/check_java_imports.py blocking in Static checks. This branch has two fully qualified java.util.function.Function references in ui/nearbydevices/NearbyDeviceListAdaptor.java (lines 40 and 64), so the check will fail on the next run. Fix with python scripts/check_java_imports.py --fix after merging main.

Drafted by Claude Code

rubo77 and others added 3 commits October 15, 2026 18:29
The app's BLE code only answers "is this one of my tags" - NearbyTagWatcher
drops any frame whose address is not in the rolling-key index. This adds the
other question, "what is out there at all", behind a new menu entry on the
devices list.

- ble/FindMyFrameKey: reconstructs the advertisement key from the 0x12 TLV
  and the BLE address it arrived on, for both payload shapes. Separated
  (len 0x19) yields the full 28-byte key over MAC + payload; nearby
  (len 0x02) yields only the six-byte prefix - which is the join key across
  both shapes inside one rotation, and finer than the MAC alone because it
  carries the two key bits the address cannot express. Walks the Apple TLV
  chain rather than reading offset zero, since iPhones prepend their own
  payloads.
- ble/NearbyDeviceSighting + NearbyDeviceScanner: the generic counterpart
  of NearbyTagWatcher - same hardware scan filter (0x004C + type 0x12),
  same 20-minute scan restart ahead of the platform's long-scan downgrade,
  but no index, no MAC resolver, no Python. Emits every frame that parses.
- ble/NearbyDeviceBook: one row per key prefix, newest sighting wins,
  pruned after 30s of silence - a tag that leaves sends nothing, so
  silence is the only departure signal. Sorted strongest signal first.
- ble/ForeignTagNames: user names for foreign devices in SharedPreferences
  (deliberately not Room - a twelve-hex-chars-to-name map is what it is
  for, and a schema change would mean a migration). Keyed on the prefix so
  a name survives the separated/nearby flip; rotation re-keying is a
  different feature (rotation-tracking branch).
- util/nearby/ProximityDistance: RSSI to rough metres (log-distance path
  loss, -59 dBm at 1m, rendered with "~" - honest about the precision the
  physics carries).
- NearbyDevicesActivity + layout + NearbyDeviceListAdaptor + row layout:
  the screen the scanner exists for. Subscribes only while on top, rebuilds
  the list on sightings and on a 1s tick so departures age out live.
  Long-press renames via the same name dialog the tag page uses. BLE
  permission is asked once per screen, via BlePermissions like Maps.
- Manifest declares the activity; my_devices_menu gains the entry,
  worded as a scan so nobody mistakes it for a network lookup.
- 8 new strings across all 10 locales via add_strings.py.
- Tests: FindMyFrameKeyTest pins the key layout for both shapes, the
  prefix join between them, the non-leading-TLV walk, and rejects junk;
  NearbyDeviceBookTest pins newest-wins, stale-resend, sort and pruning.

Not verified against a physical tag: no AirTag was at hand during
development. The key reconstruction follows the layout FindMy.py's
scanner.py implements and is pinned by JVM tests; frames where 0x12 does
not lead the manufacturer blob are parsed by FindMyFrameKey but currently
dropped upstream of it (see NearbyDeviceSighting.tlvBytes).

Co-Authored-By: Devin/SWE-2 <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- The hardware filter and the sighting parser both demanded the Find My
  record at manufacturer-data offset zero, but Apple's TLV chain does not
  guarantee it: Nearby Info and Handoff records routinely lead. The
  scanner now filters on the company ID alone and sorts the chain in
  software (NearbyDeviceSighting reuses FindMyFrameKey.findFindMyTlv),
  so a 0x12 behind another record still becomes a row. A new
  NearbyDeviceSightingTest pins that with the prefixed fixture.
- Bluetooth going off or a refused scan completed the subscription
  silently, and the refresh timer then overwrote the reason with "no
  devices in range". The screen now resubscribes on a 30-second retry -
  the same cadence MapsActivity.onNearbyWatchEnded uses - and the empty
  state distinguishes "Bluetooth is off or unavailable" from "nothing
  heard" via a new string in all locales.
- Removed the manual onResume() after the permission grant: the dialog
  pauses the activity, so the framework resumes it anyway, and the extra
  call subscribed a second scan whose disposable was then overwritten -
  onPause could only stop the newer pair.
- The MAC row used colorOutline at 12sp, 4.25:1 against the row
  background; colorOnSurfaceVariant clears the 4.5:1 normal-text floor.

Co-Authored-By: Devin/SWE-2 <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- NearbyDeviceBook: silence past GONE_AFTER_MS now marks a row offline
  instead of dropping it; snapshot returns live rows by RSSI first,
  then offline rows by most-recently-heard, and only forgets a prefix
  past RETAIN_GONE_MS (10 min) so a busy day cannot fill the list
  forever.
- The adaptor draws offline rows at half alpha and reports only
  "offline · <last seen>" - distance, battery and owner-state from the
  last hearing would read as current conditions.
- NearbyDeviceBookTest: offline retention, horizon pruning, and the
  live-before-offline ordering.
- nearby_device_offline in all ten locales.

Co-Authored-By: Devin/SWE-2 <158243242+devin-ai-integration[bot]@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Android Build — c6326d2b Deployed Oct 15, 2026 by rubo77 via build #338
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.

3 participants