Browse Source

Android: fix crash on a stale accessibility virtual view id (#22122)

* Android: never leave an accessibility node without text or content description

`ExploreByTouchHelper.createNodeForChild` validates every virtual view the
callback produces and throws

    Callbacks must add text or a content description in populateNodeForVirtualViewId()

when the node carries neither. It throws from inside an accessibility callback,
so the exception is not recoverable by the application: the process goes down.

`TextUtils.isEmpty` treats `""` as empty, so an empty string does not satisfy
the contract - only a non-empty value does. Two paths in
`OnPopulateNodeForVirtualView` could produce such a node:

* **A live peer with no accessible name.** `nodeInfo.Text ??= peer.GetName()`
  looks like it only assigns when nothing was set, but `AutomationPeer.GetName()`
  and `GetHelpText()` never return null - they collapse a missing value to
  `string.Empty` - so both assignments always run and can both assign `""`.
  This happens for any peer that is a pure container: a `Panel`, a `Border`, or
  the `TextSelectorLayer` that is added when a text selection starts. On a device
  with an accessibility service running, the first touch on a text field was
  enough to bring the application down.

* **A stale virtual view id.** Since #22024 peers are unregistered when their
  control leaves the visual tree, and the platform can still ask for a node it
  obtained earlier - it caches them, and it re-queries the accessibility focused
  one. The lookup then fails and the node was left untouched, which the same
  validation rejects.

Both are fixed by guaranteeing a non-empty content description. A single space
is used deliberately: it satisfies the platform contract without inventing a
label that screen readers would announce.

Note that `AutomationPeer.GetClassName()` has the same `?? string.Empty` shape,
so it cannot serve as the fallback.

### Testing

`Avalonia.Android` builds clean. The crash paths need a device with an
accessibility service enabled and are not reachable from the unit test projects;
the fix was verified on hardware (Android 13 kiosk) where the first touch on a
text field used to take the application down on every armed process.

* Android: default an unlabelled node to its type name, and correct the attribution

Review feedback: a single space only satisfies the platform contract, it does not
describe anything, and it hides an unnamed control instead of surfacing it. The
fallback is available - GetClassNameCore() is abstract and ControlAutomationPeer
returns Owner.GetType().Name - so a peer always has a type name, which is also what
nodeInfo.ClassName already carries.

This also corrects a claim made in the first revision of this change. The androidx
guard does not use TextUtils.isEmpty: in androidx.customview 1.1.0 and 1.2.0,
createNodeForChild tests getText() and getContentDescription() for null, strictly,
and the class contains no TextUtils reference at all (the isEmpty guard sits on the
event path, which throws populateEventForVirtualViewId()).

Since AutomationPeer.GetName() collapses a missing name to string.Empty, a node
built for an unnamed container was therefore never rejected. Only the stale virtual
view id path - reachable since #22024 unregisters peers on detach - produced a node
the platform refuses. The placeholder there is now string.Empty, and the type name
default is presented for what it is: an accessibility improvement, not a crash fix.

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

* Cut the explanatory comments down, per review on #22123

The rationale belongs in the pull request, not in the source. Only the two lines a
reader cannot infer from the code are kept.

* Drop the type name default, as requested in review

Keeps this PR to the crash fix alone. The default label for an unnamed node is a
separate discussion.

---------

Co-authored-by: ronnycohen <19652995+ronnycohen@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
pull/22130/head
Ronny 4 weeks ago
committed by GitHub
parent
commit
774f561a91
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 14
      src/Android/Avalonia.Android/AvaloniaAccessHelper.cs

14
src/Android/Avalonia.Android/AvaloniaAccessHelper.cs

@ -260,11 +260,23 @@ namespace Avalonia.Android
protected override void OnPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat? nodeInfo)
{
if (nodeInfo is null || !_peers.TryGetValue(virtualViewId, out AutomationPeer? peer))
if (nodeInfo is null)
{
return; // BAIL!! No work to be done
}
if (!_peers.TryGetValue(virtualViewId, out AutomationPeer? peer))
{
// The node must still be populated: ExploreByTouchHelper rejects one whose text,
// content description or bounds are unset.
nodeInfo.ContentDescription = string.Empty;
nodeInfo.Enabled = false;
nodeInfo.Focusable = false;
nodeInfo.ScreenReaderFocusable = false;
nodeInfo.SetBoundsInScreen(new(0, 0, 0, 0));
return;
}
// UI logical structure
foreach (AutomationPeer child in peer.GetChildren())
{

Loading…
Cancel
Save