Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for Android TalkBack #17704

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
acc1985
Start working on Android explore by touch
IsaMorphic Nov 30, 2024
bcb63c8
Start working on a more serious solution
IsaMorphic Dec 2, 2024
a185083
Reflectionless approach
IsaMorphic Dec 2, 2024
cc82df1
Allow multiple providers to be defined for the same AutomationPeer in…
IsaMorphic Dec 3, 2024
ce5ccda
Implement EmbeddableControlRootAutomationPeer
IsaMorphic Dec 4, 2024
7207039
Garbage collection
IsaMorphic Dec 4, 2024
6a71fa1
It's working!!
IsaMorphic Dec 4, 2024
5875aef
Get better readouts
IsaMorphic Dec 5, 2024
2d772a3
Implement rest of providers and improve performance
IsaMorphic Dec 5, 2024
cf694f6
Some cleanup for the PR!
IsaMorphic Dec 5, 2024
d64e513
Whoopsie!
IsaMorphic Dec 5, 2024
38db20f
Merge branch 'master' into feature/android-touch
jmacato Dec 7, 2024
5442ea3
Better text readouts for more descriptive elements
IsaMorphic Dec 7, 2024
e8f49f9
Fix bug with previous approach
IsaMorphic Dec 7, 2024
d584c84
Some final tweaks
IsaMorphic Dec 8, 2024
ada2c76
Last tweak!
IsaMorphic Dec 8, 2024
09a8885
Merge branch 'master' into feature/android-touch
IsaMorphic Dec 10, 2024
fb8b0cb
Slight improvements
IsaMorphic Dec 10, 2024
b0a7a59
Undo last change
IsaMorphic Dec 10, 2024
b6e5ae7
Fix bug where custom provider types would not be registered
IsaMorphic Dec 11, 2024
72593d1
Better TextBox compatibility with screen readers & TalkBack
IsaMorphic Dec 11, 2024
c61e17f
Fix regression for LabeledBy tests
IsaMorphic Dec 11, 2024
584bad3
Merge branch 'master' into feature/android-touch
jmacato Dec 12, 2024
2779026
Clean up provider code
IsaMorphic Dec 12, 2024
112f776
Final batch of fixes for TextBox behavior
IsaMorphic Dec 12, 2024
3b1fe3c
Append text instead of replacing it to fix buggy screen readers
IsaMorphic Dec 12, 2024
d617542
Even more fixes for buggy screen readers
IsaMorphic Dec 12, 2024
8ae698a
Remove english-specific state descriptions
IsaMorphic Dec 12, 2024
accc221
Code review improvements
IsaMorphic Dec 14, 2024
124b479
Merge branch 'master' into feature/android-touch
IsaMorphic Dec 14, 2024
72703a2
Merge branch 'master' into feature/android-touch
IsaMorphic Dec 16, 2024
774bd6c
Merge branch 'master' into feature/android-touch
IsaMorphic Dec 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Android.OS;
using AndroidX.Core.View;
using AndroidX.Core.View.Accessibility;
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Automation.Provider;

namespace Avalonia.Android.Automation
{
internal class ExpandCollapseNodeInfoProvider : NodeInfoProvider<IExpandCollapseProvider>
{
public ExpandCollapseNodeInfoProvider(AutomationPeer peer, int virtualViewId) : base(peer, virtualViewId)
{
}

public override bool PerformNodeAction(int action, Bundle? arguments)
{
IExpandCollapseProvider provider = GetProvider();
switch (action)
{
case AccessibilityNodeInfoCompat.ActionExpand:
provider.Expand();
return true;
case AccessibilityNodeInfoCompat.ActionCollapse:
provider.Collapse();
return true;
default:
return false;
}
}

public override void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo)
{
nodeInfo.AddAction(AccessibilityNodeInfoCompat.ActionExpand);
nodeInfo.AddAction(AccessibilityNodeInfoCompat.ActionCollapse);
}
}
}
14 changes: 14 additions & 0 deletions src/Android/Avalonia.Android/Automation/INodeInfoProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Android.OS;
using AndroidX.Core.View.Accessibility;

namespace Avalonia.Android.Automation
{
public interface INodeInfoProvider
{
int VirtualViewId { get; }

bool PerformNodeAction(int action, Bundle? arguments);

void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo);
}
}
33 changes: 33 additions & 0 deletions src/Android/Avalonia.Android/Automation/InvokeNodeInfoProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Android.OS;
using AndroidX.Core.View.Accessibility;
using Avalonia.Automation.Peers;
using Avalonia.Automation.Provider;

namespace Avalonia.Android.Automation
{
internal class InvokeNodeInfoProvider : NodeInfoProvider<IInvokeProvider>
{
public InvokeNodeInfoProvider(AutomationPeer peer, int virtualViewId) : base(peer, virtualViewId)
{
}

public override bool PerformNodeAction(int action, Bundle? arguments)
{
IInvokeProvider provider = GetProvider();
switch (action)
{
case AccessibilityNodeInfoCompat.ActionClick:
provider.Invoke();
return true;
default:
return false;
}
}

public override void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo)
{
nodeInfo.AddAction(AccessibilityNodeInfoCompat.ActionClick);
nodeInfo.Clickable = true;
}
}
}
29 changes: 29 additions & 0 deletions src/Android/Avalonia.Android/Automation/NodeInfoProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using Android.OS;
using AndroidX.Core.View.Accessibility;
using Avalonia.Automation.Peers;

namespace Avalonia.Android.Automation
{
internal delegate INodeInfoProvider NodeInfoProviderInitializer(AutomationPeer peer, int virtualViewId);

internal abstract class NodeInfoProvider<T> : INodeInfoProvider
{
private readonly AutomationPeer _peer;

public NodeInfoProvider(AutomationPeer peer, int virtualViewId)
{
_peer = peer;
VirtualViewId = virtualViewId;
}

public int VirtualViewId { get; }

public T GetProvider() => _peer.GetProvider<T>() ??
throw new InvalidOperationException($"Peer instance does not implement {nameof(T)}.");

public abstract bool PerformNodeAction(int action, Bundle? arguments);

public abstract void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Android.OS;
using AndroidX.Core.View.Accessibility;
using Avalonia.Automation.Peers;
using Avalonia.Automation.Provider;

namespace Avalonia.Android.Automation
{
internal class RangeValueNodeInfoProvider : NodeInfoProvider<IRangeValueProvider>
{
public RangeValueNodeInfoProvider(AutomationPeer peer, int virtualViewId) : base(peer, virtualViewId)
{
}

public override bool PerformNodeAction(int action, Bundle? arguments)
{
return false;
}

public override void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo)
{
IRangeValueProvider provider = GetProvider();
nodeInfo.RangeInfo = new AccessibilityNodeInfoCompat.RangeInfoCompat(
AccessibilityNodeInfoCompat.RangeInfoCompat.RangeTypeFloat,
(float)provider.Minimum, (float)provider.Maximum,
(float)provider.Value
);
}
}
}
51 changes: 51 additions & 0 deletions src/Android/Avalonia.Android/Automation/ScrollNodeInfoProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Android.OS;
using AndroidX.Core.View.Accessibility;
using Avalonia.Automation.Peers;
using Avalonia.Automation.Provider;

namespace Avalonia.Android.Automation
{
internal class ScrollNodeInfoProvider : NodeInfoProvider<IScrollProvider>
{
public ScrollNodeInfoProvider(AutomationPeer peer, int virtualViewId) : base(peer, virtualViewId)
{
}

public override bool PerformNodeAction(int action, Bundle? arguments)
{
IScrollProvider provider = GetProvider();
switch (action)
{
case AccessibilityNodeInfoCompat.ActionScrollForward:
if (provider.VerticallyScrollable)
{
provider.Scroll(ScrollAmount.NoAmount, ScrollAmount.SmallIncrement);
}
else if(provider.HorizontallyScrollable)
{
provider.Scroll(ScrollAmount.SmallIncrement, ScrollAmount.NoAmount);
}
return true;
case AccessibilityNodeInfoCompat.ActionScrollBackward:
if (provider.VerticallyScrollable)
{
provider.Scroll(ScrollAmount.NoAmount, ScrollAmount.SmallDecrement);
}
else if (provider.HorizontallyScrollable)
{
provider.Scroll(ScrollAmount.SmallDecrement, ScrollAmount.NoAmount);
}
return true;
default:
return false;
}
}

public override void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo)
{
nodeInfo.AddAction(AccessibilityNodeInfoCompat.ActionScrollForward);
nodeInfo.AddAction(AccessibilityNodeInfoCompat.ActionScrollBackward);
nodeInfo.Scrollable = true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Android.OS;
using AndroidX.Core.View.Accessibility;
using Avalonia.Automation.Peers;
using Avalonia.Automation.Provider;

namespace Avalonia.Android.Automation
{
internal class SelectionItemNodeInfoProvider : NodeInfoProvider<ISelectionItemProvider>
{
public SelectionItemNodeInfoProvider(AutomationPeer peer, int virtualViewId) : base(peer, virtualViewId)
{
}

public override bool PerformNodeAction(int action, Bundle? arguments)
{
ISelectionItemProvider provider = GetProvider();
switch (action)
{
case AccessibilityNodeInfoCompat.ActionSelect:
provider.Select();
return true;
default:
return false;
}
}

public override void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo)
{
nodeInfo.AddAction(AccessibilityNodeInfoCompat.ActionSelect);

ISelectionItemProvider provider = GetProvider();
nodeInfo.Selected = provider.IsSelected;
}
}
}
38 changes: 38 additions & 0 deletions src/Android/Avalonia.Android/Automation/ToggleNodeInfoProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Android.OS;
using AndroidX.Core.View;
using AndroidX.Core.View.Accessibility;
using Avalonia.Automation.Peers;
using Avalonia.Automation.Provider;

namespace Avalonia.Android.Automation
{
internal class ToggleNodeInfoProvider : NodeInfoProvider<IToggleProvider>
{
public ToggleNodeInfoProvider(AutomationPeer peer, int virtualViewId) : base(peer, virtualViewId)
{
}

public override bool PerformNodeAction(int action, Bundle? arguments)
{
IToggleProvider provider = GetProvider();
switch (action)
{
case AccessibilityNodeInfoCompat.ActionClick:
provider.Toggle();
return true;
default:
return false;
}
}

public override void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo)
{
nodeInfo.AddAction(AccessibilityNodeInfoCompat.ActionClick);
nodeInfo.Clickable = true;

IToggleProvider provider = GetProvider();
nodeInfo.Checked = provider.ToggleState == ToggleState.On;
nodeInfo.Checkable = true;
}
}
}
47 changes: 47 additions & 0 deletions src/Android/Avalonia.Android/Automation/ValueNodeInfoProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Android.OS;
using AndroidX.Core.View;
using AndroidX.Core.View.Accessibility;
using Avalonia.Automation.Peers;
using Avalonia.Automation.Provider;

namespace Avalonia.Android.Automation
{
internal class ValueNodeInfoProvider : NodeInfoProvider<IValueProvider>
{
public ValueNodeInfoProvider(AutomationPeer peer, int virtualViewId) : base(peer, virtualViewId)
{
}

public override bool PerformNodeAction(int action, Bundle? arguments)
{
IValueProvider provider = GetProvider();
switch (action)
{
case AccessibilityNodeInfoCompat.ActionSetText:
string? text = arguments?.GetCharSequence(
AccessibilityNodeInfoCompat.ActionArgumentSetTextCharsequence
);
provider.SetValue(provider.Value + text);
return true;

default:
return false;
}
}

public override void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo)
{
nodeInfo.AddAction(AccessibilityNodeInfoCompat.ActionSetText);

nodeInfo.ClassName = "android.widget.EditText";
nodeInfo.LiveRegion = ViewCompat.AccessibilityLiveRegionPolite;

IValueProvider provider = GetProvider();
nodeInfo.Editable = !provider.IsReadOnly;
nodeInfo.SetTextSelection(
provider.Value?.Length ?? 0,
provider.Value?.Length ?? 0
);
}
}
}
Loading
Loading