Mac and Android still override HandleAccEvent for platform specific behaviour other than firing the event. The Android behaviour can be unified properly in future work. ATK is the platform layer with the most churn because there were inconsistencies in the way local and remote events were handled. I'm reasonably sure these were unintentional inconsistencies, so I've done my best to unify them. Differential Revision: https://phabricator.services.mozilla.com/D183704
516 lines
17 KiB
C++
516 lines
17 KiB
C++
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "AccessibleWrap.h"
|
|
|
|
#include "JavaBuiltins.h"
|
|
#include "LocalAccessible-inl.h"
|
|
#include "HyperTextAccessible-inl.h"
|
|
#include "AccAttributes.h"
|
|
#include "AccEvent.h"
|
|
#include "AndroidInputType.h"
|
|
#include "DocAccessibleWrap.h"
|
|
#include "SessionAccessibility.h"
|
|
#include "TextLeafAccessible.h"
|
|
#include "TraversalRule.h"
|
|
#include "Pivot.h"
|
|
#include "Platform.h"
|
|
#include "nsAccessibilityService.h"
|
|
#include "nsEventShell.h"
|
|
#include "nsIAccessibleAnnouncementEvent.h"
|
|
#include "nsIAccessiblePivot.h"
|
|
#include "nsAccUtils.h"
|
|
#include "nsTextEquivUtils.h"
|
|
#include "nsWhitespaceTokenizer.h"
|
|
#include "RootAccessible.h"
|
|
#include "TextLeafRange.h"
|
|
|
|
#include "mozilla/a11y/PDocAccessibleChild.h"
|
|
#include "mozilla/jni/GeckoBundleUtils.h"
|
|
#include "mozilla/a11y/DocAccessibleParent.h"
|
|
#include "mozilla/Maybe.h"
|
|
|
|
// icu TRUE conflicting with java::sdk::Boolean::TRUE()
|
|
// https://searchfox.org/mozilla-central/rev/ce02064d8afc8673cef83c92896ee873bd35e7ae/intl/icu/source/common/unicode/umachine.h#265
|
|
// https://searchfox.org/mozilla-central/source/__GENERATED__/widget/android/bindings/JavaBuiltins.h#78
|
|
#ifdef TRUE
|
|
# undef TRUE
|
|
#endif
|
|
|
|
using namespace mozilla::a11y;
|
|
using mozilla::Maybe;
|
|
|
|
//-----------------------------------------------------
|
|
// construction
|
|
//-----------------------------------------------------
|
|
AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc)
|
|
: LocalAccessible(aContent, aDoc), mID(SessionAccessibility::kUnsetID) {
|
|
if (!IPCAccessibilityActive()) {
|
|
MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
|
|
SessionAccessibility::RegisterAccessible(this);
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
// destruction
|
|
//-----------------------------------------------------
|
|
AccessibleWrap::~AccessibleWrap() {}
|
|
|
|
nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
|
|
auto accessible = static_cast<AccessibleWrap*>(aEvent->GetAccessible());
|
|
NS_ENSURE_TRUE(accessible, NS_ERROR_FAILURE);
|
|
DocAccessibleWrap* doc =
|
|
static_cast<DocAccessibleWrap*>(accessible->Document());
|
|
if (doc) {
|
|
switch (aEvent->GetEventType()) {
|
|
case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
|
|
if (accessible != aEvent->Document() && !aEvent->IsFromUserInput()) {
|
|
AccCaretMoveEvent* caretEvent = downcast_accEvent(aEvent);
|
|
HyperTextAccessible* ht = AsHyperText();
|
|
// Pivot to the caret's position if it has an expanded selection.
|
|
// This is used mostly for find in page.
|
|
if ((ht && ht->SelectionCount())) {
|
|
DOMPoint point =
|
|
AsHyperText()->OffsetToDOMPoint(caretEvent->GetCaretOffset());
|
|
if (LocalAccessible* newPos =
|
|
doc->GetAccessibleOrContainer(point.node)) {
|
|
static_cast<AccessibleWrap*>(newPos)->PivotTo(
|
|
java::SessionAccessibility::HTML_GRANULARITY_DEFAULT, true,
|
|
true);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case nsIAccessibleEvent::EVENT_SCROLLING_START: {
|
|
accessible->PivotTo(
|
|
java::SessionAccessibility::HTML_GRANULARITY_DEFAULT, true, true);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
nsresult rv = LocalAccessible::HandleAccEvent(aEvent);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
accessible->HandleLiveRegionEvent(aEvent);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void AccessibleWrap::Shutdown() {
|
|
if (!IPCAccessibilityActive()) {
|
|
MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
|
|
SessionAccessibility::UnregisterAccessible(this);
|
|
}
|
|
LocalAccessible::Shutdown();
|
|
}
|
|
|
|
bool AccessibleWrap::DoAction(uint8_t aIndex) const {
|
|
if (ActionCount()) {
|
|
return LocalAccessible::DoAction(aIndex);
|
|
}
|
|
|
|
if (mContent) {
|
|
// We still simulate a click on an accessible even if there is no
|
|
// known actions. For the sake of bad markup.
|
|
DoCommand();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
Accessible* AccessibleWrap::DoPivot(Accessible* aAccessible,
|
|
int32_t aGranularity, bool aForward,
|
|
bool aInclusive) {
|
|
Accessible* pivotRoot = nullptr;
|
|
if (aAccessible->IsRemote()) {
|
|
// If this is a remote accessible provide the top level
|
|
// remote doc as the pivot root for thread safety reasons.
|
|
DocAccessibleParent* doc = aAccessible->AsRemote()->Document();
|
|
while (doc && !doc->IsTopLevel()) {
|
|
doc = doc->ParentDoc();
|
|
}
|
|
MOZ_ASSERT(doc, "Failed to get top level DocAccessibleParent");
|
|
pivotRoot = doc;
|
|
}
|
|
a11y::Pivot pivot(pivotRoot);
|
|
// Depending on the start accessible, the pivot rule will either traverse
|
|
// local or remote accessibles exclusively.
|
|
TraversalRule rule(aGranularity, aAccessible->IsLocal());
|
|
Accessible* result = aForward ? pivot.Next(aAccessible, rule, aInclusive)
|
|
: pivot.Prev(aAccessible, rule, aInclusive);
|
|
|
|
if (result && (result != aAccessible || aInclusive)) {
|
|
return result;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool AccessibleWrap::PivotTo(int32_t aGranularity, bool aForward,
|
|
bool aInclusive) {
|
|
Accessible* result = DoPivot(this, aGranularity, aForward, aInclusive);
|
|
if (result) {
|
|
MOZ_ASSERT(result->IsLocal());
|
|
// Dispatch a virtual cursor change event that will be turned into an
|
|
// android accessibility focused changed event in the parent.
|
|
PivotMoveReason reason = aForward ? nsIAccessiblePivot::REASON_NEXT
|
|
: nsIAccessiblePivot::REASON_PREV;
|
|
LocalAccessible* localResult = result->AsLocal();
|
|
RefPtr<AccEvent> event = new AccVCChangeEvent(
|
|
localResult->Document(), this, localResult, reason, eFromUserInput);
|
|
nsEventShell::FireEvent(event);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void AccessibleWrap::ExploreByTouch(float aX, float aY) {
|
|
a11y::Pivot pivot(RootAccessible());
|
|
TraversalRule rule;
|
|
|
|
Accessible* maybeResult = pivot.AtPoint(aX, aY, rule);
|
|
LocalAccessible* result = maybeResult ? maybeResult->AsLocal() : nullptr;
|
|
|
|
if (result && result != this) {
|
|
RefPtr<AccEvent> event =
|
|
new AccVCChangeEvent(result->Document(), this, result,
|
|
nsIAccessiblePivot::REASON_POINT, eFromUserInput);
|
|
nsEventShell::FireEvent(event);
|
|
}
|
|
}
|
|
|
|
static TextLeafPoint ToTextLeafPoint(Accessible* aAccessible, int32_t aOffset) {
|
|
if (HyperTextAccessibleBase* ht = aAccessible->AsHyperTextBase()) {
|
|
return ht->ToTextLeafPoint(aOffset);
|
|
}
|
|
|
|
return TextLeafPoint(aAccessible, aOffset);
|
|
}
|
|
|
|
Maybe<std::pair<int32_t, int32_t>> AccessibleWrap::NavigateText(
|
|
Accessible* aAccessible, int32_t aGranularity, int32_t aStartOffset,
|
|
int32_t aEndOffset, bool aForward, bool aSelect) {
|
|
int32_t startOffset = aStartOffset;
|
|
int32_t endOffset = aEndOffset;
|
|
if (startOffset == -1) {
|
|
MOZ_ASSERT(endOffset == -1,
|
|
"When start offset is unset, end offset should be too");
|
|
startOffset = aForward ? 0 : nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT;
|
|
endOffset = aForward ? 0 : nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT;
|
|
}
|
|
|
|
// If the accessible is an editable, set the virtual cursor position
|
|
// to its caret offset. Otherwise use the document's virtual cursor
|
|
// position as a starting offset.
|
|
if (aAccessible->State() & states::EDITABLE) {
|
|
startOffset = endOffset = aAccessible->AsHyperTextBase()->CaretOffset();
|
|
}
|
|
|
|
TextLeafRange currentRange =
|
|
TextLeafRange(ToTextLeafPoint(aAccessible, startOffset),
|
|
ToTextLeafPoint(aAccessible, endOffset));
|
|
uint16_t startBoundaryType = nsIAccessibleText::BOUNDARY_LINE_START;
|
|
uint16_t endBoundaryType = nsIAccessibleText::BOUNDARY_LINE_END;
|
|
switch (aGranularity) {
|
|
case 1: // MOVEMENT_GRANULARITY_CHARACTER
|
|
startBoundaryType = nsIAccessibleText::BOUNDARY_CHAR;
|
|
endBoundaryType = nsIAccessibleText::BOUNDARY_CHAR;
|
|
break;
|
|
case 2: // MOVEMENT_GRANULARITY_WORD
|
|
startBoundaryType = nsIAccessibleText::BOUNDARY_WORD_START;
|
|
endBoundaryType = nsIAccessibleText::BOUNDARY_WORD_END;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
TextLeafRange resultRange;
|
|
|
|
if (aForward) {
|
|
resultRange.SetEnd(
|
|
currentRange.End().FindBoundary(endBoundaryType, eDirNext));
|
|
resultRange.SetStart(
|
|
resultRange.End().FindBoundary(startBoundaryType, eDirPrevious));
|
|
} else {
|
|
resultRange.SetStart(
|
|
currentRange.Start().FindBoundary(startBoundaryType, eDirPrevious));
|
|
resultRange.SetEnd(
|
|
resultRange.Start().FindBoundary(endBoundaryType, eDirNext));
|
|
}
|
|
|
|
if (!resultRange.Crop(aAccessible)) {
|
|
// If the new range does not intersect at all with the given
|
|
// accessible/container this navigation has failed or reached an edge.
|
|
return Nothing();
|
|
}
|
|
|
|
if (resultRange == currentRange || resultRange.Start() == resultRange.End()) {
|
|
// If the result range equals the current range, or if the result range is
|
|
// collapsed, we failed or reached an edge.
|
|
return Nothing();
|
|
}
|
|
|
|
if (HyperTextAccessibleBase* ht = aAccessible->AsHyperTextBase()) {
|
|
DebugOnly<bool> ok = false;
|
|
std::tie(ok, startOffset) = ht->TransformOffset(
|
|
resultRange.Start().mAcc, resultRange.Start().mOffset, false);
|
|
MOZ_ASSERT(ok, "Accessible of range start should be in container.");
|
|
|
|
std::tie(ok, endOffset) = ht->TransformOffset(
|
|
resultRange.End().mAcc, resultRange.End().mOffset, false);
|
|
MOZ_ASSERT(ok, "Accessible range end should be in container.");
|
|
} else {
|
|
startOffset = resultRange.Start().mOffset;
|
|
endOffset = resultRange.End().mOffset;
|
|
}
|
|
|
|
return Some(std::make_pair(startOffset, endOffset));
|
|
}
|
|
|
|
uint32_t AccessibleWrap::GetFlags(role aRole, uint64_t aState,
|
|
uint8_t aActionCount) {
|
|
uint32_t flags = 0;
|
|
if (aState & states::CHECKABLE) {
|
|
flags |= java::SessionAccessibility::FLAG_CHECKABLE;
|
|
}
|
|
|
|
if (aState & states::CHECKED) {
|
|
flags |= java::SessionAccessibility::FLAG_CHECKED;
|
|
}
|
|
|
|
if (aState & states::INVALID) {
|
|
flags |= java::SessionAccessibility::FLAG_CONTENT_INVALID;
|
|
}
|
|
|
|
if (aState & states::EDITABLE) {
|
|
flags |= java::SessionAccessibility::FLAG_EDITABLE;
|
|
}
|
|
|
|
if (aActionCount && aRole != roles::TEXT_LEAF) {
|
|
flags |= java::SessionAccessibility::FLAG_CLICKABLE;
|
|
}
|
|
|
|
if (aState & states::ENABLED) {
|
|
flags |= java::SessionAccessibility::FLAG_ENABLED;
|
|
}
|
|
|
|
if (aState & states::FOCUSABLE) {
|
|
flags |= java::SessionAccessibility::FLAG_FOCUSABLE;
|
|
}
|
|
|
|
if (aState & states::FOCUSED) {
|
|
flags |= java::SessionAccessibility::FLAG_FOCUSED;
|
|
}
|
|
|
|
if (aState & states::MULTI_LINE) {
|
|
flags |= java::SessionAccessibility::FLAG_MULTI_LINE;
|
|
}
|
|
|
|
if (aState & states::SELECTABLE) {
|
|
flags |= java::SessionAccessibility::FLAG_SELECTABLE;
|
|
}
|
|
|
|
if (aState & states::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) {
|
|
flags |= java::SessionAccessibility::FLAG_VISIBLE_TO_USER;
|
|
}
|
|
|
|
if (aRole == roles::PASSWORD_TEXT) {
|
|
flags |= java::SessionAccessibility::FLAG_PASSWORD;
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
void AccessibleWrap::GetRoleDescription(role aRole, AccAttributes* aAttributes,
|
|
nsAString& aGeckoRole,
|
|
nsAString& aRoleDescription) {
|
|
if (aRole == roles::HEADING && aAttributes) {
|
|
// The heading level is an attribute, so we need that.
|
|
nsAutoString headingLevel;
|
|
if (aAttributes->GetAttribute(nsGkAtoms::level, headingLevel)) {
|
|
nsAutoString token(u"heading-");
|
|
token.Append(headingLevel);
|
|
if (LocalizeString(token, aRoleDescription)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((aRole == roles::LANDMARK || aRole == roles::REGION) && aAttributes) {
|
|
nsAutoString xmlRoles;
|
|
if (aAttributes->GetAttribute(nsGkAtoms::xmlroles, xmlRoles)) {
|
|
nsWhitespaceTokenizer tokenizer(xmlRoles);
|
|
while (tokenizer.hasMoreTokens()) {
|
|
if (LocalizeString(tokenizer.nextToken(), aRoleDescription)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
GetAccService()->GetStringRole(aRole, aGeckoRole);
|
|
LocalizeString(aGeckoRole, aRoleDescription);
|
|
}
|
|
|
|
int32_t AccessibleWrap::AndroidClass(Accessible* aAccessible) {
|
|
return GetVirtualViewID(aAccessible) == SessionAccessibility::kNoID
|
|
? java::SessionAccessibility::CLASSNAME_WEBVIEW
|
|
: GetAndroidClass(aAccessible->Role());
|
|
}
|
|
|
|
int32_t AccessibleWrap::GetVirtualViewID(Accessible* aAccessible) {
|
|
if (aAccessible->IsLocal()) {
|
|
return static_cast<AccessibleWrap*>(aAccessible)->mID;
|
|
}
|
|
|
|
return static_cast<int32_t>(aAccessible->AsRemote()->GetWrapper());
|
|
}
|
|
|
|
void AccessibleWrap::SetVirtualViewID(Accessible* aAccessible,
|
|
int32_t aVirtualViewID) {
|
|
if (aAccessible->IsLocal()) {
|
|
static_cast<AccessibleWrap*>(aAccessible)->mID = aVirtualViewID;
|
|
} else {
|
|
aAccessible->AsRemote()->SetWrapper(static_cast<uintptr_t>(aVirtualViewID));
|
|
}
|
|
}
|
|
|
|
int32_t AccessibleWrap::GetAndroidClass(role aRole) {
|
|
#define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
|
|
msaaRole, ia2Role, androidClass, nameRule) \
|
|
case roles::geckoRole: \
|
|
return androidClass;
|
|
|
|
switch (aRole) {
|
|
#include "RoleMap.h"
|
|
default:
|
|
return java::SessionAccessibility::CLASSNAME_VIEW;
|
|
}
|
|
|
|
#undef ROLE
|
|
}
|
|
|
|
int32_t AccessibleWrap::GetInputType(const nsString& aInputTypeAttr) {
|
|
if (aInputTypeAttr.EqualsIgnoreCase("email")) {
|
|
return java::sdk::InputType::TYPE_CLASS_TEXT |
|
|
java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
|
|
}
|
|
|
|
if (aInputTypeAttr.EqualsIgnoreCase("number")) {
|
|
return java::sdk::InputType::TYPE_CLASS_NUMBER;
|
|
}
|
|
|
|
if (aInputTypeAttr.EqualsIgnoreCase("password")) {
|
|
return java::sdk::InputType::TYPE_CLASS_TEXT |
|
|
java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_PASSWORD;
|
|
}
|
|
|
|
if (aInputTypeAttr.EqualsIgnoreCase("tel")) {
|
|
return java::sdk::InputType::TYPE_CLASS_PHONE;
|
|
}
|
|
|
|
if (aInputTypeAttr.EqualsIgnoreCase("text")) {
|
|
return java::sdk::InputType::TYPE_CLASS_TEXT |
|
|
java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
|
|
}
|
|
|
|
if (aInputTypeAttr.EqualsIgnoreCase("url")) {
|
|
return java::sdk::InputType::TYPE_CLASS_TEXT |
|
|
java::sdk::InputType::TYPE_TEXT_VARIATION_URI;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void AccessibleWrap::GetTextEquiv(nsString& aText) {
|
|
if (nsTextEquivUtils::HasNameRule(this, eNameFromSubtreeIfReqRule)) {
|
|
// This is an accessible that normally doesn't get its name from its
|
|
// subtree, so we collect the text equivalent explicitly.
|
|
nsTextEquivUtils::GetTextEquivFromSubtree(this, aText);
|
|
} else {
|
|
Name(aText);
|
|
}
|
|
}
|
|
|
|
bool AccessibleWrap::HandleLiveRegionEvent(AccEvent* aEvent) {
|
|
auto eventType = aEvent->GetEventType();
|
|
if (eventType != nsIAccessibleEvent::EVENT_TEXT_INSERTED &&
|
|
eventType != nsIAccessibleEvent::EVENT_NAME_CHANGE) {
|
|
// XXX: Right now only announce text inserted events. aria-relevant=removals
|
|
// is potentially on the chopping block[1]. We also don't support editable
|
|
// text because we currently can't descern the source of the change[2].
|
|
// 1. https://github.com/w3c/aria/issues/712
|
|
// 2. https://bugzilla.mozilla.org/show_bug.cgi?id=1531189
|
|
return false;
|
|
}
|
|
|
|
if (aEvent->IsFromUserInput()) {
|
|
return false;
|
|
}
|
|
|
|
RefPtr<AccAttributes> attributes = new AccAttributes();
|
|
nsAccUtils::SetLiveContainerAttributes(attributes, this);
|
|
nsString live;
|
|
if (!attributes->GetAttribute(nsGkAtoms::containerLive, live)) {
|
|
return false;
|
|
}
|
|
|
|
uint16_t priority = live.EqualsIgnoreCase("assertive")
|
|
? nsIAccessibleAnnouncementEvent::ASSERTIVE
|
|
: nsIAccessibleAnnouncementEvent::POLITE;
|
|
|
|
Maybe<bool> atomic =
|
|
attributes->GetAttribute<bool>(nsGkAtoms::containerAtomic);
|
|
LocalAccessible* announcementTarget = this;
|
|
nsAutoString announcement;
|
|
if (atomic && *atomic) {
|
|
LocalAccessible* atomicAncestor = nullptr;
|
|
for (LocalAccessible* parent = announcementTarget; parent;
|
|
parent = parent->LocalParent()) {
|
|
dom::Element* element = parent->Elm();
|
|
if (element &&
|
|
nsAccUtils::ARIAAttrValueIs(element, nsGkAtoms::aria_atomic,
|
|
nsGkAtoms::_true, eCaseMatters)) {
|
|
atomicAncestor = parent;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (atomicAncestor) {
|
|
announcementTarget = atomicAncestor;
|
|
static_cast<AccessibleWrap*>(atomicAncestor)->GetTextEquiv(announcement);
|
|
}
|
|
} else {
|
|
GetTextEquiv(announcement);
|
|
}
|
|
|
|
announcement.CompressWhitespace();
|
|
if (announcement.IsEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
announcementTarget->Announce(announcement, priority);
|
|
return true;
|
|
}
|