Skip to content

Android home-screen widget: the HR and battery line has no accessible label #1799

Closed

Description

@ryanbr

The problem

NoopGlanceWidget — the standard Android home-screen widget — renders live HR and strap
battery as text with an inline glyph character rather than a labelled image:

// android/app/src/main/java/com/noop/widget/NoopGlanceWidget.kt:153-164
Row(verticalAlignment = Alignment.CenterVertically) {
    Text(text = snap.heartRate?.let { "♥ $it" } ?: "♥ - ", …)
    Spacer(modifier = GlanceModifier.width(10.dp))
    Text(text = snap.batteryPct?.let { "⚡ $it%" } ?: "⚡ - ", …)
}

There is no contentDescription and no semantics anywhere in the file, so TalkBack reads the
raw string. The ♥ and ⚡ are literal characters inside the text, which a screen reader either
announces by their Unicode names or skips entirely — either way the user gets a bare number with
no metric name and no unit. "58" and "84%" arrive as two anonymous figures.

What makes it clearly a defect rather than a gap

The sibling widget in the same package already does it correctly:

  • NoopCompactGlanceWidget.kt:140 — battery drawn as Image(ImageProvider(R.drawable.ic_widget_strap_battery), contentDescription = uiString(R.string.l10n_noop_compact_glance_widget_strap_battery_a6c7f09c))
  • NoopCompactGlanceWidget.kt:177 — the generic metric cell passes contentDescription = label

So the compact widget is accessible and the standard one is not. Within NoopGlanceWidget
itself the three score cells are fine — ScoreCell(label = uiString(R.string.l10n_noop_glance_widget_rest_…))
gives Rest, Charge and Effort real localised word labels. It is only the HR/battery row that is
unnamed.

Suggested shape

Either give the row proper semantics/contentDescription, or switch HR and battery to
Image(ImageProvider(…), contentDescription = …) the way the compact widget already does — the
second keeps the two widgets consistent and reuses the existing drawables
(ic_widget_strap_battery, ic_stat_heart).

Whatever is added must be localised through uiString(R.string.…), not a bare literal: the
widget's other copy already is, and #571 recorded that the i18n audit cannot see
contentDescription set inside a semantics {} lambda, so a literal there would pass CI and
ship English to every locale.

Scope notes

Not a twin of the Apple-side change in #1798, which fixes an inverted HR/HRV icon pairing plus
the same class of missing VoiceOver naming in vitalsFooter. That icon bug has no Android
counterpart at all: android/…/widget/WidgetSnapshot.kt carries no hrv or restingHr field,
so there is no HR/HRV pair to invert. Only the accessibility half has an Android analogue, and
it is a different mechanism (inline glyph text vs. an unlabelled SF Symbol).

Separately and already known: "Connected" and "Open NOOP to connect" in this same file are
bare English literals sitting beside localised uiString(…) calls. Both are in
Tools/i18n_audit_baseline.json, so the audit passes by design — worth folding into the same
pass if someone is in here anyway, but not a hidden CI hole.

Activity

  1. ryanbr commented on Sep 9, 2026

    @ryanbr
    OwnerAuthor

    Landed in b9b04c814 (#1800): the compact widget's heart-rate line now carries a contentDescription, so TalkBack reads a name and a unit instead of a bare number. The battery image beside it already had one.

    Shipped in testing build 439 onward, and in every build since.

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

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions