Skip to content

Show the tag card's battery as an icon instead of the word "medium" #228

Description

@parawanderer

The map's tag cards currently spell the battery out in words:

Nearby (●●●○○) · Battery medium

An icon says the same thing in a fraction of the width, and that row is competing for space with the tag name, the signal dots and the time. This is a self-contained front-end change — no Apple protocol, no account, no network.

What to change

NearbyTagLabel.shortBatteryLabel returns a string resource per level. The four levels come from FindMyAdvertisement.BatteryLevel:

Level Meaning
FULL
MEDIUM
LOW
VERY_LOW critically low

Four states, and exactly four. There is no "unknown" on this path — batteryLevelOf defaults to FULL, so every sighting has one of these.

The text is assembled in MapsActivity at the nearby_now_with_battery_and_signal / nearby_now_with_battery strings, so the icon needs to go into that row rather than into the string.

Where the icons come from

This project already uses Material Symbols and they are Apache 2.0, so nothing new needs adding. Every icon in app/src/main/res/drawable/ is an exported vector with viewportWidth="960" — that is the Material Symbols export, and history_24px.xml is a good one to copy the shape of, including the ?attr/colorControlNormal tint that makes it follow the theme.

Please do not pull in an icon dependency for four glyphs.

Things that will trip you up

  • An icon is not an accessibility substitute for words. Whatever replaces the text needs a contentDescription, so battery_short_full and friends probably survive as descriptions rather than being deleted. A screen reader user should still hear "battery medium".
  • Strings are edited through a script, never by hand. Ten locales; python scripts/add_strings.py prints the usage, and --check runs in CI. Hand-editing ten files is how one locale silently keeps the old wording.
  • Contrast is a number here. A tinted icon on the card background should clear 3:1 as a non-text element. There is prior art: a selected map pin once shipped at 1.23:1 — present, correct, invisible.
  • Dark mode is a second render, not an afterthought. About half of what breaks in this app breaks in only one mode.
  • It wants a test, and the cheap kind is enough. SystemColorsLayoutTest is the pattern: build a themed context, inflate, assert. What is worth asserting is that each of the four levels resolves to a different drawable and that the drawable actually loads — a battery icon that silently renders as nothing is the exact failure the history timeline shipped with once. CONTRIBUTING.md has the how.

What the pull request needs to show

This is a visual change, so the review is visual. Two things, and neither needs a phone:

1. Screenshots — all four states, both themes. Eight images, or four if you put the states side by side. Before and after, so the space saved is visible rather than asserted.

You do not need a device or an emulator of your own. .claude/skills/device-screenshots/ documents the pattern the repo already uses: inflate against a themed context, draw to a bitmap, and write it to the directory the test runner hands you, which comes back to the host. SystemColorsLayoutTest is a worked example, and createConfigurationContext with UI_MODE_NIGHT_YES is how dark mode is rendered without touching a device setting.

2. Tests, and a note on what you checked. The bar is not high but it is specific:

  • each of the four levels resolves to a different drawable, and each drawable actually loads the way the app loads it
  • the row still measures sensibly — an icon that measures to zero is invisible and green
  • the contentDescription is set for every state
  • the contrast figure for the tinted icon against the card, in both themes

Say what you ran and what you did not. A pull request here is expected to state plainly what was verified and what was assumed — an honest gap is worth more than an implied "it works". If you cannot run the instrumented suite, say so and describe what you checked instead; that is a normal answer and not a blocker.

One habit worth borrowing: break the thing on purpose and confirm the test goes red. Four tests passing first try is a reason for suspicion rather than confidence.

Optional second half: the history list

The per-tag history rows show the raw status byte:

acc 65 m · conf 0 · status 144 = 0x90 = 0b10010000

Bits 6-7 of that byte are the battery, and they are now confirmed against real hardware — two AirTags watched across a battery change reported 0x90 (low) on months-old cells, then 0x10 (full) and 0x50 (medium) once replaced, with every other bit unchanged.

But that panel deliberately shows the number and no interpretation, and a comment in LocationReportFields records the reason: it is debug metadata, the raw byte is certainly right, and a label beside it would be the app's opinion competing with the tag's on a screen meant for evidence.

So treat this half as not yet decided. If you want to do it, say so on this issue first and let it be settled — and whoever does it has to update that comment in the same change, or the code and the file arguing against it will contradict each other.

The first half needs no such discussion. Take it on its own if you prefer.

Interactively co-authored by Claude Code and @parawanderer

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    @appIssues regarding the OpenTagViewer Android appenhancementNew feature or requestgood first issueGood for newcomers

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions