Skip to content

Commit efbaed9

Browse files
ronnycohenclaude
authored andcommitted
Android: never walk an InteropAutomationPeer during accessibility traversal (AvaloniaUI#22123)
* Android: never walk an InteropAutomationPeer during accessibility traversal InteropAutomationPeer - the peer of a native interop control - throws NotImplementedException from almost every member (GetOrCreateChildrenCore, GetNameCore, GetBoundingRectangleCore, IsEnabledCore...), because it is meant to be special-cased by each platform backend. macOS does so through AvnAutomationPeer.IsInteropPeer, Windows through AutomationNode.InteropAutomationNode. Android did not, and AvaloniaAccessHelper called those members without any guard. Any accessibility traversal reaching a screen that hosts a native control - a NativeControlHost, so a WebView or an embedded media surface - therefore threw from inside an accessibility callback, which is not recoverable: the process goes down. It is reproducible in one command on such a screen: adb shell uiautomator dump Reported by any accessibility client walking the tree, so a screen reader or an MDM agent would take the same path. Interop peers are now filtered at every point where a virtual view ID could be handed out: the point hit test, the visible-views enumeration, the focused peer, and the children walk. A virtual view ID is therefore never allocated for one, and OnPopulateNodeForVirtualView never has to deal with one - which matters, because it may not answer with an unpopulated node: ExploreByTouchHelper.createNodeForChild rejects that and throws too. Skipping is the correct behaviour here rather than merely the safe one: a native control is a real Android View, already exposed to the accessibility framework on its own. Presenting it a second time as an Avalonia virtual view would duplicate it. InteropAutomationPeer is internal, hence the InternalsVisibleTo entry - Avalonia.Native and Avalonia.Win32.Automation already have one for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Drop the explanatory comments, per review The rationale belongs in the pull request, not in the source. --------- Co-authored-by: ronnycohen <19652995+ronnycohen@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 948d092 commit efbaed9

2 files changed

Lines changed: 20 additions & 1 deletion

File tree

src/Android/Avalonia.Android/AvaloniaAccessHelper.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Avalonia.Automation;
99
using Avalonia.Automation.Peers;
1010
using Avalonia.Automation.Provider;
11+
using Avalonia.Controls.Automation.Peers;
1112
using Java.Lang;
1213

1314
namespace Avalonia.Android
@@ -84,6 +85,8 @@ public AvaloniaAccessHelper(AvaloniaView view) : base(view)
8485
}
8586
}
8687

88+
private static bool IsInteropPeer(AutomationPeer peer) => peer is InteropAutomationPeer;
89+
8790
private HashSet<INodeInfoProvider> GetOrCreateNodeInfoProvidersFromPeer(AutomationPeer peer, out int virtualViewId)
8891
{
8992
int peerViewId;
@@ -175,6 +178,11 @@ protected override int GetVirtualViewAt(float x, float y)
175178
AutomationPeer? peer = embeddedRootProvider?.GetPeerFromPoint(p);
176179
if (peer is not null)
177180
{
181+
if (IsInteropPeer(peer))
182+
{
183+
return InvalidId;
184+
}
185+
178186
int virtualViewId;
179187
if (peer.GetParent() is AutomationPeer parent &&
180188
!s_containerTypes.Contains(parent.GetAutomationControlType()))
@@ -191,7 +199,7 @@ protected override int GetVirtualViewAt(float x, float y)
191199
else
192200
{
193201
peer = embeddedRootProvider?.GetFocus();
194-
return peer is null ? InvalidId : _peerIds[peer];
202+
return peer is null || IsInteropPeer(peer) ? InvalidId : _peerIds[peer];
195203
}
196204
}
197205

@@ -204,6 +212,11 @@ protected override void GetVisibleVirtualViews(IList<Integer>? virtualViewIds)
204212

205213
foreach (AutomationPeer peer in _peers[0].GetChildren())
206214
{
215+
if (IsInteropPeer(peer))
216+
{
217+
continue;
218+
}
219+
207220
GetOrCreateNodeInfoProvidersFromPeer(peer, out int virtualViewId);
208221
virtualViewIds.Add(Integer.ValueOf(virtualViewId));
209222
}
@@ -255,6 +268,11 @@ protected override void OnPopulateNodeForVirtualView(int virtualViewId, Accessib
255268
// UI logical structure
256269
foreach (AutomationPeer child in peer.GetChildren())
257270
{
271+
if (IsInteropPeer(child))
272+
{
273+
continue;
274+
}
275+
258276
GetOrCreateNodeInfoProvidersFromPeer(child, out int childId);
259277
nodeInfo.AddChild(_view, childId);
260278
}

src/Avalonia.Controls/Avalonia.Controls.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<InternalsVisibleTo Include="AvaloniaUI.DiagnosticsSupport.Avalonia, PublicKey=$(AvaloniaPublicKey)" />
2323
<InternalsVisibleTo Include="Avalonia.LeakTests, PublicKey=$(AvaloniaPublicKey)" />
2424
<InternalsVisibleTo Include="Avalonia.Headless, PublicKey=$(AvaloniaPublicKey)" />
25+
<InternalsVisibleTo Include="Avalonia.Android, PublicKey=$(AvaloniaPublicKey)" />
2526
<InternalsVisibleTo Include="Avalonia.Native, PublicKey=$(AvaloniaPublicKey)" />
2627
<InternalsVisibleTo Include="Avalonia.Win32, PublicKey=$(AvaloniaPublicKey)" />
2728
<InternalsVisibleTo Include="Avalonia.Win32.Automation, PublicKey=$(AvaloniaPublicKey)" />

0 commit comments

Comments
 (0)