Bug 1616466 - Support expand/collapse android accessibility actions. r=Jamie,geckoview-reviewers,snorp

Differential Revision: https://phabricator.services.mozilla.com/D63290

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Eitan Isaacson 2020-02-25 22:37:00 +00:00
parent 1d8103c74a
commit 8e38a4587c
7 changed files with 119 additions and 14 deletions

View File

@ -192,7 +192,19 @@ nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
AccStateChangeEvent* event = downcast_accEvent(aEvent); AccStateChangeEvent* event = downcast_accEvent(aEvent);
auto state = event->GetState(); auto state = event->GetState();
if (state & states::CHECKED) { if (state & states::CHECKED) {
sessionAcc->SendClickedEvent(accessible, event->IsStateEnabled()); sessionAcc->SendClickedEvent(
accessible, java::SessionAccessibility::FLAG_CHECKABLE |
(event->IsStateEnabled()
? java::SessionAccessibility::FLAG_CHECKED
: 0));
}
if (state & states::EXPANDED) {
sessionAcc->SendClickedEvent(
accessible, java::SessionAccessibility::FLAG_EXPANDABLE |
(event->IsStateEnabled()
? java::SessionAccessibility::FLAG_EXPANDED
: 0));
} }
if (state & states::SELECTED) { if (state & states::SELECTED) {
@ -500,6 +512,14 @@ uint32_t AccessibleWrap::GetFlags(role aRole, uint64_t aState,
flags |= java::SessionAccessibility::FLAG_SELECTED; flags |= java::SessionAccessibility::FLAG_SELECTED;
} }
if (aState & states::EXPANDABLE) {
flags |= java::SessionAccessibility::FLAG_EXPANDABLE;
}
if (aState & states::EXPANDED) {
flags |= java::SessionAccessibility::FLAG_EXPANDED;
}
if ((aState & (states::INVISIBLE | states::OFFSCREEN)) == 0) { if ((aState & (states::INVISIBLE | states::OFFSCREEN)) == 0) {
flags |= java::SessionAccessibility::FLAG_VISIBLE_TO_USER; flags |= java::SessionAccessibility::FLAG_VISIBLE_TO_USER;
} }

View File

@ -71,7 +71,17 @@ void a11y::ProxyStateChangeEvent(ProxyAccessible* aTarget, uint64_t aState,
} }
if (aState & states::CHECKED) { if (aState & states::CHECKED) {
sessionAcc->SendClickedEvent(WrapperFor(aTarget), aEnabled); sessionAcc->SendClickedEvent(
WrapperFor(aTarget),
java::SessionAccessibility::FLAG_CHECKABLE |
(aEnabled ? java::SessionAccessibility::FLAG_CHECKED : 0));
}
if (aState & states::EXPANDED) {
sessionAcc->SendClickedEvent(
WrapperFor(aTarget),
java::SessionAccessibility::FLAG_EXPANDABLE |
(aEnabled ? java::SessionAccessibility::FLAG_EXPANDED : 0));
} }
if (aState & states::SELECTED) { if (aState & states::SELECTED) {

View File

@ -330,11 +330,9 @@ void SessionAccessibility::SendTextTraversedEvent(AccessibleWrap* aAccessible,
} }
void SessionAccessibility::SendClickedEvent(AccessibleWrap* aAccessible, void SessionAccessibility::SendClickedEvent(AccessibleWrap* aAccessible,
bool aChecked) { uint32_t aFlags) {
GECKOBUNDLE_START(eventInfo); GECKOBUNDLE_START(eventInfo);
// Boolean::FALSE/TRUE gets clobbered by a macro, so ugh. GECKOBUNDLE_PUT(eventInfo, "flags", java::sdk::Integer::ValueOf(aFlags));
GECKOBUNDLE_PUT(eventInfo, "checked",
java::sdk::Integer::ValueOf(aChecked ? 1 : 0));
GECKOBUNDLE_FINISH(eventInfo); GECKOBUNDLE_FINISH(eventInfo);
mSessionAccessibility->SendEvent( mSessionAccessibility->SendEvent(

View File

@ -80,7 +80,7 @@ class SessionAccessibility final
int32_t aStart, uint32_t aLen, bool aIsInsert, int32_t aStart, uint32_t aLen, bool aIsInsert,
bool aFromUser); bool aFromUser);
void SendSelectedEvent(AccessibleWrap* aAccessible, bool aSelected); void SendSelectedEvent(AccessibleWrap* aAccessible, bool aSelected);
void SendClickedEvent(AccessibleWrap* aAccessible, bool aChecked); void SendClickedEvent(AccessibleWrap* aAccessible, uint32_t aFlags);
void SendWindowContentChangedEvent(); void SendWindowContentChangedEvent();
void SendWindowStateChangedEvent(AccessibleWrap* aAccessible); void SendWindowStateChangedEvent(AccessibleWrap* aAccessible);
void SendAnnouncementEvent(AccessibleWrap* aAccessible, void SendAnnouncementEvent(AccessibleWrap* aAccessible,

View File

@ -0,0 +1,5 @@
<html>
<body>
<button onclick="this.setAttribute('aria-expanded', this.getAttribute('aria-expanded') == 'false')" aria-expanded="false">button</button>
</body>
</html>

View File

@ -773,6 +773,50 @@ class AccessibilityTest : BaseSessionTest() {
waitUntilClick(false) waitUntilClick(false)
} }
@Test fun testExpandable() {
var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID;
loadTestPage("test-expandable")
waitForInitialFocus(true)
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 1)
override fun onAccessibilityFocused(event: AccessibilityEvent) {
nodeId = getSourceId(event)
if (Build.VERSION.SDK_INT >= 21) {
val node = createNodeInfo(nodeId)
assertThat("button is expandable", node.actionList, hasItem(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND))
assertThat("button is not collapsable", node.actionList, not(hasItem(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE)))
}
}
})
provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_EXPAND, null)
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 1)
override fun onClicked(event: AccessibilityEvent) {
assertThat("Clicked event is from same node", getSourceId(event), equalTo(nodeId))
if (Build.VERSION.SDK_INT >= 21) {
val node = createNodeInfo(nodeId)
assertThat("button is collapsable", node.actionList, hasItem(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE))
assertThat("button is not expandable", node.actionList, not(hasItem(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND)))
}
}
})
provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_COLLAPSE, null)
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 1)
override fun onClicked(event: AccessibilityEvent) {
assertThat("Clicked event is from same node", getSourceId(event), equalTo(nodeId))
if (Build.VERSION.SDK_INT >= 21) {
val node = createNodeInfo(nodeId)
assertThat("button is expandable", node.actionList, hasItem(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND))
assertThat("button is not collapsable", node.actionList, not(hasItem(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE)))
}
}
})
}
@Test fun testSelectable() { @Test fun testSelectable() {
var nodeId = View.NO_ID var nodeId = View.NO_ID
loadTestPage("test-selectable") loadTestPage("test-selectable")

View File

@ -66,6 +66,8 @@ public class SessionAccessibility {
@WrapForJNI static final int FLAG_SELECTED = 1 << 14; @WrapForJNI static final int FLAG_SELECTED = 1 << 14;
@WrapForJNI static final int FLAG_VISIBLE_TO_USER = 1 << 15; @WrapForJNI static final int FLAG_VISIBLE_TO_USER = 1 << 15;
@WrapForJNI static final int FLAG_SELECTABLE = 1 << 16; @WrapForJNI static final int FLAG_SELECTABLE = 1 << 16;
@WrapForJNI static final int FLAG_EXPANDABLE = 1 << 17;
@WrapForJNI static final int FLAG_EXPANDED = 1 << 18;
static final int CLASSNAME_UNKNOWN = -1; static final int CLASSNAME_UNKNOWN = -1;
@WrapForJNI static final int CLASSNAME_VIEW = 0; @WrapForJNI static final int CLASSNAME_VIEW = 0;
@ -208,10 +210,12 @@ public class SessionAccessibility {
virtualViewId == View.NO_ID ? CLASSNAME_WEBVIEW : CLASSNAME_UNKNOWN, null); virtualViewId == View.NO_ID ? CLASSNAME_WEBVIEW : CLASSNAME_UNKNOWN, null);
return true; return true;
case AccessibilityNodeInfo.ACTION_CLICK: case AccessibilityNodeInfo.ACTION_CLICK:
case AccessibilityNodeInfo.ACTION_EXPAND:
case AccessibilityNodeInfo.ACTION_COLLAPSE:
nativeProvider.click(virtualViewId); nativeProvider.click(virtualViewId);
GeckoBundle nodeInfo = getMostRecentBundle(virtualViewId); GeckoBundle nodeInfo = getMostRecentBundle(virtualViewId);
if (nodeInfo != null) { if (nodeInfo != null) {
if ((nodeInfo.getInt("flags") & (FLAG_SELECTABLE | FLAG_CHECKABLE)) == 0) { if ((nodeInfo.getInt("flags") & (FLAG_SELECTABLE | FLAG_CHECKABLE | FLAG_EXPANDABLE)) == 0) {
sendEvent(AccessibilityEvent.TYPE_VIEW_CLICKED, virtualViewId, nodeInfo.getInt("className"), null); sendEvent(AccessibilityEvent.TYPE_VIEW_CLICKED, virtualViewId, nodeInfo.getInt("className"), null);
} }
} }
@ -539,6 +543,19 @@ public class SessionAccessibility {
node.setInputType(nodeInfo.getInt("inputType")); node.setInputType(nodeInfo.getInt("inputType"));
} }
// SDK 21 and above
if (Build.VERSION.SDK_INT >= 21) {
if ((flags & FLAG_EXPANDABLE) != 0) {
if ((flags & FLAG_EXPANDED) != 0) {
node.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
node.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE);
} else {
node.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE);
node.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
}
}
}
// SDK 23 and above // SDK 23 and above
if (Build.VERSION.SDK_INT >= 23) { if (Build.VERSION.SDK_INT >= 23) {
node.setContextClickable((flags & FLAG_CONTEXT_CLICKABLE) != 0); node.setContextClickable((flags & FLAG_CONTEXT_CLICKABLE) != 0);
@ -789,17 +806,28 @@ public class SessionAccessibility {
event.setScrollY(eventData.getInt("scrollY", -1)); event.setScrollY(eventData.getInt("scrollY", -1));
event.setMaxScrollX(eventData.getInt("maxScrollX", -1)); event.setMaxScrollX(eventData.getInt("maxScrollX", -1));
event.setMaxScrollY(eventData.getInt("maxScrollY", -1)); event.setMaxScrollY(eventData.getInt("maxScrollY", -1));
event.setChecked(eventData.getInt("checked") != 0); event.setChecked((eventData.getInt("flags") & FLAG_CHECKED) != 0);
} }
// Update cache and stored state from this event. // Update cache and stored state from this event.
switch (eventType) { switch (eventType) {
case AccessibilityEvent.TYPE_VIEW_CLICKED: case AccessibilityEvent.TYPE_VIEW_CLICKED:
if (cachedBundle != null && eventData != null && eventData.containsKey("checked")) { if (cachedBundle != null && eventData != null && eventData.containsKey("flags")) {
if (eventData.getInt("checked") != 0) { final int flags = eventData.getInt("flags");
cachedBundle.putInt("flags", cachedBundle.getInt("flags") | FLAG_CHECKED); if ((flags & FLAG_CHECKABLE) != 0) {
} else { if ((flags & FLAG_CHECKED) != 0) {
cachedBundle.putInt("flags", cachedBundle.getInt("flags") & ~FLAG_CHECKED); cachedBundle.putInt("flags", cachedBundle.getInt("flags") | FLAG_CHECKED);
} else {
cachedBundle.putInt("flags", cachedBundle.getInt("flags") & ~FLAG_CHECKED);
}
}
if ((flags & FLAG_EXPANDABLE) != 0) {
if ((flags & FLAG_EXPANDED) != 0) {
cachedBundle.putInt("flags", cachedBundle.getInt("flags") | FLAG_EXPANDED);
} else {
cachedBundle.putInt("flags", cachedBundle.getInt("flags") & ~FLAG_EXPANDED);
}
} }
} }
break; break;