summaryrefslogtreecommitdiffstats
path: root/accessible/android
diff options
context:
space:
mode:
Diffstat (limited to 'accessible/android')
-rw-r--r--accessible/android/AccessibleWrap.cpp708
-rw-r--r--accessible/android/AccessibleWrap.h80
-rw-r--r--accessible/android/ApplicationAccessibleWrap.h20
-rw-r--r--accessible/android/DocAccessibleWrap.cpp325
-rw-r--r--accessible/android/DocAccessibleWrap.h59
-rw-r--r--accessible/android/HyperTextAccessibleWrap.h19
-rw-r--r--accessible/android/Platform.cpp267
-rw-r--r--accessible/android/RootAccessibleWrap.cpp69
-rw-r--r--accessible/android/RootAccessibleWrap.h37
-rw-r--r--accessible/android/SessionAccessibility.cpp1120
-rw-r--r--accessible/android/SessionAccessibility.h154
-rw-r--r--accessible/android/TraversalRule.cpp296
-rw-r--r--accessible/android/TraversalRule.h58
-rw-r--r--accessible/android/moz.build38
14 files changed, 3250 insertions, 0 deletions
diff --git a/accessible/android/AccessibleWrap.cpp b/accessible/android/AccessibleWrap.cpp
new file mode 100644
index 0000000000..50fe4a9792
--- /dev/null
+++ b/accessible/android/AccessibleWrap.cpp
@@ -0,0 +1,708 @@
+/* -*- 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 "nsAccUtils.h"
+#include "nsTextEquivUtils.h"
+#include "nsWhitespaceTokenizer.h"
+#include "RootAccessible.h"
+
+#include "mozilla/a11y/PDocAccessibleChild.h"
+#include "mozilla/jni/GeckoBundleUtils.h"
+#include "mozilla/StaticPrefs_accessibility.h"
+#include "mozilla/a11y/DocAccessibleParent.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;
+
+//-----------------------------------------------------
+// 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_FOCUS: {
+ if (DocAccessibleWrap* topContentDoc =
+ doc->GetTopLevelContentDoc(accessible)) {
+ topContentDoc->CacheFocusPath(accessible);
+ }
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED: {
+ AccVCChangeEvent* vcEvent = downcast_accEvent(aEvent);
+ auto newPosition =
+ static_cast<AccessibleWrap*>(vcEvent->NewAccessible());
+ if (newPosition) {
+ if (DocAccessibleWrap* topContentDoc =
+ doc->GetTopLevelContentDoc(accessible)) {
+ topContentDoc->CacheFocusPath(newPosition);
+ }
+ }
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_REORDER: {
+ if (DocAccessibleWrap* topContentDoc =
+ doc->GetTopLevelContentDoc(accessible)) {
+ topContentDoc->CacheViewport(true);
+ }
+ break;
+ }
+ 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);
+
+ if (IPCAccessibilityActive()) {
+ return NS_OK;
+ }
+
+ // The accessible can become defunct if we have an xpcom event listener
+ // which decides it would be fun to change the DOM and flush layout.
+ if (accessible->IsDefunct() || !accessible->IsBoundToParent()) {
+ return NS_OK;
+ }
+
+ if (doc) {
+ if (!doc->DocumentNode()->IsContentDocument()) {
+ return NS_OK;
+ }
+ }
+
+ RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(accessible);
+ if (!sessionAcc) {
+ return NS_OK;
+ }
+
+ switch (aEvent->GetEventType()) {
+ case nsIAccessibleEvent::EVENT_FOCUS:
+ sessionAcc->SendFocusEvent(accessible);
+ break;
+ case nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED: {
+ AccVCChangeEvent* vcEvent = downcast_accEvent(aEvent);
+ if (!vcEvent->IsFromUserInput()) {
+ break;
+ }
+
+ RefPtr<AccessibleWrap> newPosition =
+ static_cast<AccessibleWrap*>(vcEvent->NewAccessible());
+ if (sessionAcc && newPosition) {
+ if (vcEvent->Reason() == nsIAccessiblePivot::REASON_POINT) {
+ sessionAcc->SendHoverEnterEvent(newPosition);
+ } else if (vcEvent->BoundaryType() == nsIAccessiblePivot::NO_BOUNDARY) {
+ sessionAcc->SendAccessibilityFocusedEvent(newPosition);
+ }
+
+ if (vcEvent->BoundaryType() != nsIAccessiblePivot::NO_BOUNDARY) {
+ sessionAcc->SendTextTraversedEvent(
+ newPosition, vcEvent->NewStartOffset(), vcEvent->NewEndOffset());
+ }
+ }
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
+ AccCaretMoveEvent* event = downcast_accEvent(aEvent);
+ sessionAcc->SendTextSelectionChangedEvent(accessible,
+ event->GetCaretOffset());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_TEXT_INSERTED:
+ case nsIAccessibleEvent::EVENT_TEXT_REMOVED: {
+ AccTextChangeEvent* event = downcast_accEvent(aEvent);
+ sessionAcc->SendTextChangedEvent(
+ accessible, event->ModifiedText(), event->GetStartOffset(),
+ event->GetLength(), event->IsTextInserted(),
+ event->IsFromUserInput());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_STATE_CHANGE: {
+ AccStateChangeEvent* event = downcast_accEvent(aEvent);
+ auto state = event->GetState();
+ if (state & states::CHECKED) {
+ 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) {
+ sessionAcc->SendSelectedEvent(accessible, event->IsStateEnabled());
+ }
+
+ if (state & states::BUSY) {
+ sessionAcc->SendWindowStateChangedEvent(accessible);
+ }
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_SCROLLING: {
+ AccScrollingEvent* event = downcast_accEvent(aEvent);
+ sessionAcc->SendScrollingEvent(accessible, event->ScrollX(),
+ event->ScrollY(), event->MaxScrollX(),
+ event->MaxScrollY());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_ANNOUNCEMENT: {
+ AccAnnouncementEvent* event = downcast_accEvent(aEvent);
+ sessionAcc->SendAnnouncementEvent(accessible, event->Announcement(),
+ event->Priority());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_REORDER: {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ sessionAcc->SendWindowContentChangedEvent();
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ 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, -1, -1, localResult, -1, -1, reason,
+ nsIAccessiblePivot::NO_BOUNDARY, 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, -1, -1, result, -1, -1,
+ nsIAccessiblePivot::REASON_POINT,
+ nsIAccessiblePivot::NO_BOUNDARY, eFromUserInput);
+ nsEventShell::FireEvent(event);
+ }
+}
+
+void AccessibleWrap::NavigateText(int32_t aGranularity, int32_t aStartOffset,
+ int32_t aEndOffset, bool aForward,
+ bool aSelect) {
+ a11y::Pivot pivot(RootAccessible());
+
+ HyperTextAccessible* editable =
+ (State() & states::EDITABLE) != 0 ? AsHyperText() : nullptr;
+
+ int32_t start = aStartOffset, end = aEndOffset;
+ // 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 (editable) {
+ start = end = editable->CaretOffset();
+ }
+
+ uint16_t pivotGranularity = nsIAccessiblePivot::LINE_BOUNDARY;
+ switch (aGranularity) {
+ case 1: // MOVEMENT_GRANULARITY_CHARACTER
+ pivotGranularity = nsIAccessiblePivot::CHAR_BOUNDARY;
+ break;
+ case 2: // MOVEMENT_GRANULARITY_WORD
+ pivotGranularity = nsIAccessiblePivot::WORD_BOUNDARY;
+ break;
+ default:
+ break;
+ }
+
+ int32_t newOffset;
+ Accessible* newAnchorBase = nullptr;
+ if (aForward) {
+ newAnchorBase = pivot.NextText(this, &start, &end, pivotGranularity);
+ newOffset = end;
+ } else {
+ newAnchorBase = pivot.PrevText(this, &start, &end, pivotGranularity);
+ newOffset = start;
+ }
+ LocalAccessible* newAnchor =
+ newAnchorBase ? newAnchorBase->AsLocal() : nullptr;
+
+ if (newAnchor && (start != aStartOffset || end != aEndOffset)) {
+ if (IsTextLeaf() && newAnchor == LocalParent()) {
+ // For paragraphs, divs, spans, etc., we put a11y focus on the text leaf
+ // node instead of the HyperTextAccessible. However, Pivot will always
+ // return a HyperTextAccessible. Android doesn't support text navigation
+ // landing on an accessible which is different to the originating
+ // accessible. Therefore, if we're still within the same text leaf,
+ // translate the offsets to the text leaf.
+ int32_t thisChild = IndexInParent();
+ HyperTextAccessible* newHyper = newAnchor->AsHyperText();
+ MOZ_ASSERT(newHyper);
+ int32_t startChild = newHyper->GetChildIndexAtOffset(start);
+ // We use end - 1 because the end offset is exclusive, so end itself
+ // might be associated with the next child.
+ int32_t endChild = newHyper->GetChildIndexAtOffset(end - 1);
+ if (startChild == thisChild && endChild == thisChild) {
+ // We've landed within the same text leaf.
+ newAnchor = this;
+ int32_t thisOffset = newHyper->GetChildOffset(thisChild);
+ start -= thisOffset;
+ end -= thisOffset;
+ }
+ }
+ RefPtr<AccEvent> event = new AccVCChangeEvent(
+ newAnchor->Document(), this, aStartOffset, aEndOffset, newAnchor, start,
+ end, nsIAccessiblePivot::REASON_NONE, pivotGranularity, eFromUserInput);
+ nsEventShell::FireEvent(event);
+ }
+
+ // If we are in an editable, move the caret to the new virtual cursor
+ // offset.
+ if (editable) {
+ if (aSelect) {
+ int32_t anchor = editable->CaretOffset();
+ if (editable->SelectionCount()) {
+ int32_t startSel, endSel;
+ GetSelectionOrCaret(&startSel, &endSel);
+ anchor = startSel == anchor ? endSel : startSel;
+ }
+ editable->SetSelectionBoundsAt(0, anchor, newOffset);
+ } else {
+ editable->SetCaretOffset(newOffset);
+ }
+ }
+}
+
+void AccessibleWrap::SetSelection(int32_t aStart, int32_t aEnd) {
+ if (HyperTextAccessible* textAcc = AsHyperText()) {
+ if (aStart == aEnd) {
+ textAcc->SetCaretOffset(aStart);
+ } else {
+ textAcc->SetSelectionBoundsAt(0, aStart, aEnd);
+ }
+ }
+}
+
+void AccessibleWrap::Cut() {
+ if ((State() & states::EDITABLE) == 0) {
+ return;
+ }
+
+ if (HyperTextAccessible* textAcc = AsHyperText()) {
+ int32_t startSel, endSel;
+ GetSelectionOrCaret(&startSel, &endSel);
+ textAcc->CutText(startSel, endSel);
+ }
+}
+
+void AccessibleWrap::Copy() {
+ if (HyperTextAccessible* textAcc = AsHyperText()) {
+ int32_t startSel, endSel;
+ GetSelectionOrCaret(&startSel, &endSel);
+ textAcc->CopyText(startSel, endSel);
+ }
+}
+
+void AccessibleWrap::Paste() {
+ if ((State() & states::EDITABLE) == 0) {
+ return;
+ }
+
+ if (IsHyperText()) {
+ RefPtr<HyperTextAccessible> textAcc = AsHyperText();
+ int32_t startSel, endSel;
+ GetSelectionOrCaret(&startSel, &endSel);
+ if (startSel != endSel) {
+ textAcc->DeleteText(startSel, endSel);
+ }
+ textAcc->PasteText(startSel);
+ }
+}
+
+void AccessibleWrap::GetSelectionOrCaret(int32_t* aStartOffset,
+ int32_t* aEndOffset) {
+ *aStartOffset = *aEndOffset = -1;
+ if (HyperTextAccessible* textAcc = AsHyperText()) {
+ if (!textAcc->SelectionBoundsAt(0, aStartOffset, aEndOffset)) {
+ *aStartOffset = *aEndOffset = textAcc->CaretOffset();
+ }
+ }
+}
+
+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, 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;
+}
diff --git a/accessible/android/AccessibleWrap.h b/accessible/android/AccessibleWrap.h
new file mode 100644
index 0000000000..3b5ef43f13
--- /dev/null
+++ b/accessible/android/AccessibleWrap.h
@@ -0,0 +1,80 @@
+/* -*- 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/. */
+
+#ifndef mozilla_a11y_AccessibleWrap_h_
+#define mozilla_a11y_AccessibleWrap_h_
+
+#include "LocalAccessible.h"
+#include "mozilla/a11y/RemoteAccessible.h"
+#include "mozilla/java/GeckoBundleWrappers.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla {
+namespace a11y {
+
+class AccessibleWrap : public LocalAccessible {
+ public:
+ AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc);
+ virtual ~AccessibleWrap();
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY // TODO: Mark this as MOZ_CAN_RUN_SCRIPT
+ virtual nsresult
+ HandleAccEvent(AccEvent* aEvent) override;
+
+ virtual void Shutdown() override;
+
+ virtual bool DoAction(uint8_t aIndex) const override;
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ virtual bool PivotTo(int32_t aGranularity, bool aForward, bool aInclusive);
+
+ virtual void NavigateText(int32_t aGranularity, int32_t aStartOffset,
+ int32_t aEndOffset, bool aForward, bool aSelect);
+
+ virtual void SetSelection(int32_t aStart, int32_t aEnd);
+
+ virtual void Cut();
+
+ virtual void Copy();
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ virtual void Paste();
+
+ void ExploreByTouch(float aX, float aY);
+
+ static uint32_t GetFlags(role aRole, uint64_t aState, uint8_t aActionCount);
+
+ static int32_t GetInputType(const nsString& aInputTypeAttr);
+
+ static int32_t GetAndroidClass(role aRole);
+
+ static void GetRoleDescription(role aRole, AccAttributes* aAttributes,
+ nsAString& aGeckoRole,
+ nsAString& aRoleDescription);
+
+ static int32_t AndroidClass(Accessible* aAccessible);
+
+ static int32_t GetVirtualViewID(Accessible* aAccessible);
+
+ static void SetVirtualViewID(Accessible* aAccessible, int32_t aVirtualViewID);
+
+ static Accessible* DoPivot(Accessible* aAccessible, int32_t aGranularity,
+ bool aForward, bool aInclusive);
+
+ protected:
+ int32_t mID;
+
+ private:
+ void GetTextEquiv(nsString& aText);
+
+ bool HandleLiveRegionEvent(AccEvent* aEvent);
+
+ void GetSelectionOrCaret(int32_t* aStartOffset, int32_t* aEndOffset);
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/android/ApplicationAccessibleWrap.h b/accessible/android/ApplicationAccessibleWrap.h
new file mode 100644
index 0000000000..89b07916c9
--- /dev/null
+++ b/accessible/android/ApplicationAccessibleWrap.h
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef mozilla_a11y_ApplicationAccessibleWrap_h__
+#define mozilla_a11y_ApplicationAccessibleWrap_h__
+
+#include "ApplicationAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+typedef ApplicationAccessible ApplicationAccessibleWrap;
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/android/DocAccessibleWrap.cpp b/accessible/android/DocAccessibleWrap.cpp
new file mode 100644
index 0000000000..269ab85cb9
--- /dev/null
+++ b/accessible/android/DocAccessibleWrap.cpp
@@ -0,0 +1,325 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "LocalAccessible-inl.h"
+#include "AccAttributes.h"
+#include "DocAccessibleChild.h"
+#include "DocAccessibleWrap.h"
+#include "nsIDocShell.h"
+#include "nsIScrollableFrame.h"
+#include "nsLayoutUtils.h"
+#include "nsAccessibilityService.h"
+#include "nsAccUtils.h"
+#include "Pivot.h"
+#include "SessionAccessibility.h"
+#include "TraversalRule.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/a11y/DocAccessiblePlatformExtChild.h"
+#include "mozilla/StaticPrefs_accessibility.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+const uint32_t kCacheRefreshInterval = 500;
+
+#define UNIQUE_ID(acc) \
+ !acc || (acc->IsDoc() && acc->AsDoc()->IPCDoc()) \
+ ? 0 \
+ : reinterpret_cast<uint64_t>(acc->UniqueID())
+
+////////////////////////////////////////////////////////////////////////////////
+// DocAccessibleWrap
+////////////////////////////////////////////////////////////////////////////////
+
+DocAccessibleWrap::DocAccessibleWrap(Document* aDocument, PresShell* aPresShell)
+ : DocAccessible(aDocument, aPresShell) {
+ // We need an nsINode associated with this accessible to register it with the
+ // right SessionAccessibility instance. When the base AccessibleWrap
+ // constructor is called we don't have one yet because null is passed as the
+ // content node. So we do it here after a Document is associated with the
+ // accessible.
+ if (!IPCAccessibilityActive()) {
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ SessionAccessibility::RegisterAccessible(this);
+ }
+}
+
+DocAccessibleWrap::~DocAccessibleWrap() {}
+
+void DocAccessibleWrap::Shutdown() {
+ // Unregister here before disconnecting from PresShell.
+ if (!IPCAccessibilityActive()) {
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ if (IsRoot()) {
+ SessionAccessibility::UnregisterAll(PresShellPtr());
+ } else {
+ SessionAccessibility::UnregisterAccessible(this);
+ }
+ }
+ DocAccessible::Shutdown();
+}
+
+void DocAccessibleWrap::DoInitialUpdate() {
+ DocAccessible::DoInitialUpdate();
+ CacheViewport(true);
+}
+
+nsresult DocAccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
+ switch (aEvent->GetEventType()) {
+ case nsIAccessibleEvent::EVENT_SCROLLING_END:
+ CacheViewport(false);
+ break;
+ case nsIAccessibleEvent::EVENT_SCROLLING:
+ UpdateFocusPathBounds();
+ break;
+ default:
+ break;
+ }
+
+ return DocAccessible::HandleAccEvent(aEvent);
+}
+
+void DocAccessibleWrap::CacheViewportCallback(nsITimer* aTimer,
+ void* aDocAccParam) {
+ RefPtr<DocAccessibleWrap> docAcc(
+ dont_AddRef(reinterpret_cast<DocAccessibleWrap*>(aDocAccParam)));
+ if (!docAcc || docAcc->HasShutdown() ||
+ (IPCAccessibilityActive() && !docAcc->IPCDoc())) {
+ return;
+ }
+
+ PresShell* presShell = docAcc->PresShellPtr();
+ nsIFrame* rootFrame = presShell->GetRootFrame();
+ if (!rootFrame) {
+ return;
+ }
+
+ nsTArray<nsIFrame*> frames;
+ nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollable();
+ nsRect scrollPort = sf ? sf->GetScrollPortRect() : rootFrame->GetRect();
+
+ nsLayoutUtils::GetFramesForArea(
+ RelativeTo{presShell->GetRootFrame()}, scrollPort, frames,
+ {nsLayoutUtils::FrameForPointOption::OnlyVisible});
+ AccessibleHashtable inViewAccs;
+ for (size_t i = 0; i < frames.Length(); i++) {
+ nsIContent* content = frames.ElementAt(i)->GetContent();
+ if (!content) {
+ continue;
+ }
+
+ LocalAccessible* visibleAcc = docAcc->GetAccessibleOrContainer(content);
+ if (!visibleAcc) {
+ continue;
+ }
+
+ for (LocalAccessible* acc = visibleAcc; acc && acc != docAcc->LocalParent();
+ acc = acc->LocalParent()) {
+ const bool alreadyPresent =
+ inViewAccs.WithEntryHandle(acc->UniqueID(), [&](auto&& entry) {
+ if (entry) {
+ return true;
+ }
+
+ entry.Insert(RefPtr{acc});
+ return false;
+ });
+ if (alreadyPresent) {
+ break;
+ }
+ }
+ }
+
+ if (IPCAccessibilityActive()) {
+ DocAccessibleChild* ipcDoc = docAcc->IPCDoc();
+ nsTArray<BatchData> cacheData(inViewAccs.Count());
+ for (auto iter = inViewAccs.Iter(); !iter.Done(); iter.Next()) {
+ LocalAccessible* accessible = iter.Data();
+ nsAutoString name;
+ accessible->Name(name);
+ nsAutoString textValue;
+ accessible->Value(textValue);
+ nsAutoString nodeID;
+ accessible->DOMNodeID(nodeID);
+ nsAutoString description;
+ accessible->Description(description);
+
+ cacheData.AppendElement(BatchData(
+ accessible->Document()->IPCDoc(), UNIQUE_ID(accessible),
+ accessible->State(), accessible->Bounds(), accessible->ActionCount(),
+ name, textValue, nodeID, description, UnspecifiedNaN<double>(),
+ UnspecifiedNaN<double>(), UnspecifiedNaN<double>(),
+ UnspecifiedNaN<double>(), nullptr));
+ }
+
+ ipcDoc->SendBatch(eBatch_Viewport, cacheData);
+ } else if (RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(docAcc)) {
+ nsTArray<Accessible*> accessibles(inViewAccs.Count());
+ for (const auto& entry : inViewAccs) {
+ accessibles.AppendElement(entry.GetWeak());
+ }
+
+ sessionAcc->ReplaceViewportCache(accessibles);
+ }
+
+ if (docAcc->mCachePivotBoundaries) {
+ a11y::Pivot pivot(docAcc);
+ TraversalRule rule(java::SessionAccessibility::HTML_GRANULARITY_DEFAULT,
+ true);
+ Accessible* maybeFirst = pivot.First(rule);
+ Accessible* maybeLast = pivot.Last(rule);
+ LocalAccessible* first = maybeFirst ? maybeFirst->AsLocal() : nullptr;
+ LocalAccessible* last = maybeLast ? maybeLast->AsLocal() : nullptr;
+
+ // If first/last are null, pass the root document as pivot boundary.
+ if (IPCAccessibilityActive()) {
+ DocAccessibleChild* ipcDoc = docAcc->IPCDoc();
+ DocAccessibleChild* firstDoc =
+ first ? first->Document()->IPCDoc() : ipcDoc;
+ DocAccessibleChild* lastDoc = last ? last->Document()->IPCDoc() : ipcDoc;
+ if (ipcDoc && firstDoc && lastDoc) {
+ // One or more of the documents may not have recieved an IPC doc yet.
+ // In that case, just throw away this update. We will get a new one soon
+ // enough.
+ ipcDoc->GetPlatformExtension()->SendSetPivotBoundaries(
+ firstDoc, UNIQUE_ID(first), lastDoc, UNIQUE_ID(last));
+ }
+ } else if (RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(docAcc)) {
+ sessionAcc->UpdateAccessibleFocusBoundaries(
+ first ? static_cast<AccessibleWrap*>(first) : docAcc,
+ last ? static_cast<AccessibleWrap*>(last) : docAcc);
+ }
+
+ docAcc->mCachePivotBoundaries = false;
+ }
+
+ if (docAcc->mCacheRefreshTimer) {
+ docAcc->mCacheRefreshTimer = nullptr;
+ }
+}
+
+void DocAccessibleWrap::CacheViewport(bool aCachePivotBoundaries) {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ return;
+ }
+ mCachePivotBoundaries |= aCachePivotBoundaries;
+ if (IsTopLevelContentDoc() && !mCacheRefreshTimer) {
+ NS_NewTimerWithFuncCallback(getter_AddRefs(mCacheRefreshTimer),
+ CacheViewportCallback, this,
+ kCacheRefreshInterval, nsITimer::TYPE_ONE_SHOT,
+ "a11y::DocAccessibleWrap::CacheViewport");
+ if (mCacheRefreshTimer) {
+ NS_ADDREF_THIS(); // Kung fu death grip
+ }
+ }
+}
+
+DocAccessibleWrap* DocAccessibleWrap::GetTopLevelContentDoc(
+ AccessibleWrap* aAccessible) {
+ DocAccessibleWrap* doc =
+ static_cast<DocAccessibleWrap*>(aAccessible->Document());
+ while (doc && !doc->IsTopLevelContentDoc()) {
+ doc = static_cast<DocAccessibleWrap*>(doc->ParentDocument());
+ }
+
+ return doc;
+}
+
+bool DocAccessibleWrap::IsTopLevelContentDoc() {
+ DocAccessible* parentDoc = ParentDocument();
+ return DocumentNode()->IsContentDocument() &&
+ (!parentDoc || !parentDoc->DocumentNode()->IsContentDocument());
+}
+
+void DocAccessibleWrap::CacheFocusPath(AccessibleWrap* aAccessible) {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ return;
+ }
+
+ mFocusPath.Clear();
+ if (IPCAccessibilityActive()) {
+ DocAccessibleChild* ipcDoc = IPCDoc();
+ nsTArray<BatchData> cacheData;
+ for (AccessibleWrap* acc = aAccessible; acc && acc != this->LocalParent();
+ acc = static_cast<AccessibleWrap*>(acc->LocalParent())) {
+ nsAutoString name;
+ acc->Name(name);
+ nsAutoString textValue;
+ acc->Value(textValue);
+ nsAutoString nodeID;
+ acc->DOMNodeID(nodeID);
+ nsAutoString description;
+ acc->Description(description);
+ RefPtr<AccAttributes> attributes = acc->Attributes();
+ cacheData.AppendElement(
+ BatchData(acc->Document()->IPCDoc(), UNIQUE_ID(acc), acc->State(),
+ acc->Bounds(), acc->ActionCount(), name, textValue, nodeID,
+ description, acc->CurValue(), acc->MinValue(),
+ acc->MaxValue(), acc->Step(), attributes));
+ mFocusPath.InsertOrUpdate(acc->UniqueID(), RefPtr{acc});
+ }
+
+ ipcDoc->SendBatch(eBatch_FocusPath, cacheData);
+ } else if (RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(this)) {
+ nsTArray<Accessible*> accessibles;
+ for (LocalAccessible* acc = aAccessible; acc && acc != this->LocalParent();
+ acc = acc->LocalParent()) {
+ accessibles.AppendElement(acc);
+ mFocusPath.InsertOrUpdate(acc->UniqueID(), RefPtr{acc});
+ }
+
+ sessionAcc->ReplaceFocusPathCache(accessibles);
+ }
+}
+
+void DocAccessibleWrap::UpdateFocusPathBounds() {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ return;
+ }
+
+ if (!mFocusPath.Count()) {
+ return;
+ }
+
+ if (IPCAccessibilityActive()) {
+ DocAccessibleChild* ipcDoc = IPCDoc();
+ nsTArray<BatchData> boundsData(mFocusPath.Count());
+ for (auto iter = mFocusPath.Iter(); !iter.Done(); iter.Next()) {
+ LocalAccessible* accessible = iter.Data();
+ if (!accessible || accessible->IsDefunct()) {
+ MOZ_ASSERT_UNREACHABLE("Focus path cached accessible is gone.");
+ continue;
+ }
+
+ boundsData.AppendElement(BatchData(
+ accessible->Document()->IPCDoc(), UNIQUE_ID(accessible), 0,
+ accessible->Bounds(), 0, nsString(), nsString(), nsString(),
+ nsString(), UnspecifiedNaN<double>(), UnspecifiedNaN<double>(),
+ UnspecifiedNaN<double>(), UnspecifiedNaN<double>(), nullptr));
+ }
+
+ ipcDoc->SendBatch(eBatch_BoundsUpdate, boundsData);
+ } else if (RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(this)) {
+ nsTArray<Accessible*> accessibles(mFocusPath.Count());
+ for (auto iter = mFocusPath.Iter(); !iter.Done(); iter.Next()) {
+ LocalAccessible* accessible = iter.Data();
+ if (!accessible || accessible->IsDefunct()) {
+ MOZ_ASSERT_UNREACHABLE("Focus path cached accessible is gone.");
+ continue;
+ }
+
+ accessibles.AppendElement(accessible);
+ }
+
+ sessionAcc->UpdateCachedBounds(accessibles);
+ }
+}
+
+#undef UNIQUE_ID
diff --git a/accessible/android/DocAccessibleWrap.h b/accessible/android/DocAccessibleWrap.h
new file mode 100644
index 0000000000..3e562090c1
--- /dev/null
+++ b/accessible/android/DocAccessibleWrap.h
@@ -0,0 +1,59 @@
+/* -*- 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/. */
+
+#ifndef mozilla_a11y_DocAccessibleWrap_h__
+#define mozilla_a11y_DocAccessibleWrap_h__
+
+#include "DocAccessible.h"
+#include "nsITimer.h"
+
+namespace mozilla {
+
+class PresShell;
+
+namespace a11y {
+
+class DocAccessibleWrap : public DocAccessible {
+ public:
+ DocAccessibleWrap(Document* aDocument, PresShell* aPresShell);
+ virtual ~DocAccessibleWrap();
+
+ virtual void Shutdown() override;
+
+ virtual nsresult HandleAccEvent(AccEvent* aEvent) override;
+
+ DocAccessibleWrap* GetTopLevelContentDoc(AccessibleWrap* aAccessible);
+
+ bool IsTopLevelContentDoc();
+
+ void CacheFocusPath(AccessibleWrap* aAccessible);
+
+ void CacheViewport(bool aCachePivotBoundaries);
+
+ enum {
+ eBatch_Viewport = 0,
+ eBatch_FocusPath = 1,
+ eBatch_BoundsUpdate = 2,
+ };
+
+ protected:
+ virtual void DoInitialUpdate() override;
+
+ private:
+ void UpdateFocusPathBounds();
+
+ static void CacheViewportCallback(nsITimer* aTimer, void* aDocAccParam);
+
+ nsCOMPtr<nsITimer> mCacheRefreshTimer;
+
+ bool mCachePivotBoundaries;
+
+ AccessibleHashtable mFocusPath;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/android/HyperTextAccessibleWrap.h b/accessible/android/HyperTextAccessibleWrap.h
new file mode 100644
index 0000000000..da569c8216
--- /dev/null
+++ b/accessible/android/HyperTextAccessibleWrap.h
@@ -0,0 +1,19 @@
+/* -*- 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/. */
+
+#ifndef mozilla_a11y_HyperTextAccessibleWrap_h__
+#define mozilla_a11y_HyperTextAccessibleWrap_h__
+
+#include "HyperTextAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+typedef class HyperTextAccessible HyperTextAccessibleWrap;
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/android/Platform.cpp b/accessible/android/Platform.cpp
new file mode 100644
index 0000000000..da7d7f7cab
--- /dev/null
+++ b/accessible/android/Platform.cpp
@@ -0,0 +1,267 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "Platform.h"
+#include "DocAccessibleWrap.h"
+#include "SessionAccessibility.h"
+#include "mozilla/a11y/RemoteAccessible.h"
+#include "mozilla/Components.h"
+#include "mozilla/StaticPrefs_accessibility.h"
+#include "nsIAccessibleEvent.h"
+#include "nsIAccessiblePivot.h"
+#include "nsIStringBundle.h"
+
+#define ROLE_STRINGS_URL "chrome://global/locale/AccessFu.properties"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+static nsTHashMap<nsStringHashKey, nsString> sLocalizedStrings;
+
+void a11y::PlatformInit() {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIStringBundleService> stringBundleService =
+ components::StringBundle::Service();
+ if (!stringBundleService) return;
+
+ nsCOMPtr<nsIStringBundle> stringBundle;
+ nsCOMPtr<nsIStringBundleService> sbs = components::StringBundle::Service();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get string bundle service");
+ return;
+ }
+
+ rv = sbs->CreateBundle(ROLE_STRINGS_URL, getter_AddRefs(stringBundle));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get string bundle");
+ return;
+ }
+
+ nsString localizedStr;
+ // Preload the state required localized string.
+ rv = stringBundle->GetStringFromName("stateRequired", localizedStr);
+ if (NS_SUCCEEDED(rv)) {
+ sLocalizedStrings.InsertOrUpdate(u"stateRequired"_ns, localizedStr);
+ }
+
+ // Preload heading level localized descriptions 1 thru 6.
+ for (int32_t level = 1; level <= 6; level++) {
+ nsAutoString token;
+ token.AppendPrintf("heading-%d", level);
+
+ nsAutoString formatString;
+ formatString.AppendInt(level);
+ AutoTArray<nsString, 1> formatParams;
+ formatParams.AppendElement(formatString);
+ rv = stringBundle->FormatStringFromName("headingLevel", formatParams,
+ localizedStr);
+ if (NS_SUCCEEDED(rv)) {
+ sLocalizedStrings.InsertOrUpdate(token, localizedStr);
+ }
+ }
+
+ // Preload any roles that have localized versions
+#define ROLE(geckoRole, stringRole, atkRole, macRole, macSubrole, msaaRole, \
+ ia2Role, androidClass, nameRule) \
+ rv = stringBundle->GetStringFromName(stringRole, localizedStr); \
+ if (NS_SUCCEEDED(rv)) { \
+ sLocalizedStrings.InsertOrUpdate(u##stringRole##_ns, localizedStr); \
+ }
+
+#include "RoleMap.h"
+#undef ROLE
+}
+
+void a11y::PlatformShutdown() { sLocalizedStrings.Clear(); }
+
+void a11y::ProxyCreated(RemoteAccessible* aProxy) {
+ SessionAccessibility::RegisterAccessible(aProxy);
+}
+
+void a11y::ProxyDestroyed(RemoteAccessible* aProxy) {
+ SessionAccessibility::UnregisterAccessible(aProxy);
+}
+
+void a11y::ProxyEvent(RemoteAccessible* aTarget, uint32_t aEventType) {
+ RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aTarget);
+ if (!sessionAcc) {
+ return;
+ }
+
+ switch (aEventType) {
+ case nsIAccessibleEvent::EVENT_FOCUS:
+ sessionAcc->SendFocusEvent(aTarget);
+ break;
+ case nsIAccessibleEvent::EVENT_REORDER:
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ sessionAcc->SendWindowContentChangedEvent();
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+void a11y::ProxyStateChangeEvent(RemoteAccessible* aTarget, uint64_t aState,
+ bool aEnabled) {
+ RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aTarget);
+
+ if (!sessionAcc) {
+ return;
+ }
+
+ if (aState & states::CHECKED) {
+ sessionAcc->SendClickedEvent(
+ aTarget, java::SessionAccessibility::FLAG_CHECKABLE |
+ (aEnabled ? java::SessionAccessibility::FLAG_CHECKED : 0));
+ }
+
+ if (aState & states::EXPANDED) {
+ sessionAcc->SendClickedEvent(
+ aTarget,
+ java::SessionAccessibility::FLAG_EXPANDABLE |
+ (aEnabled ? java::SessionAccessibility::FLAG_EXPANDED : 0));
+ }
+
+ if (aState & states::SELECTED) {
+ sessionAcc->SendSelectedEvent(aTarget, aEnabled);
+ }
+
+ if (aState & states::BUSY) {
+ sessionAcc->SendWindowStateChangedEvent(aTarget);
+ }
+}
+
+void a11y::ProxyCaretMoveEvent(RemoteAccessible* aTarget, int32_t aOffset,
+ bool aIsSelectionCollapsed,
+ int32_t aGranularity) {
+ RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aTarget);
+
+ if (sessionAcc) {
+ sessionAcc->SendTextSelectionChangedEvent(aTarget, aOffset);
+ }
+}
+
+void a11y::ProxyTextChangeEvent(RemoteAccessible* aTarget,
+ const nsAString& aStr, int32_t aStart,
+ uint32_t aLen, bool aIsInsert, bool aFromUser) {
+ RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aTarget);
+
+ if (sessionAcc) {
+ sessionAcc->SendTextChangedEvent(aTarget, aStr, aStart, aLen, aIsInsert,
+ aFromUser);
+ }
+}
+
+void a11y::ProxyShowHideEvent(RemoteAccessible* aTarget,
+ RemoteAccessible* aParent, bool aInsert,
+ bool aFromUser) {
+ // We rely on the window content changed events to be dispatched
+ // after the viewport cache is refreshed.
+}
+
+void a11y::ProxySelectionEvent(RemoteAccessible*, RemoteAccessible*, uint32_t) {
+}
+
+void a11y::ProxyVirtualCursorChangeEvent(
+ RemoteAccessible* aTarget, RemoteAccessible* aOldPosition,
+ int32_t aOldStartOffset, int32_t aOldEndOffset,
+ RemoteAccessible* aNewPosition, int32_t aNewStartOffset,
+ int32_t aNewEndOffset, int16_t aReason, int16_t aBoundaryType,
+ bool aFromUser) {
+ if (!aNewPosition || !aFromUser) {
+ return;
+ }
+
+ RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aTarget);
+
+ if (!sessionAcc) {
+ return;
+ }
+
+ if (aReason == nsIAccessiblePivot::REASON_POINT) {
+ sessionAcc->SendHoverEnterEvent(aNewPosition);
+ } else if (aBoundaryType == nsIAccessiblePivot::NO_BOUNDARY) {
+ sessionAcc->SendAccessibilityFocusedEvent(aNewPosition);
+ }
+
+ if (aBoundaryType != nsIAccessiblePivot::NO_BOUNDARY) {
+ sessionAcc->SendTextTraversedEvent(aNewPosition, aNewStartOffset,
+ aNewEndOffset);
+ }
+}
+
+void a11y::ProxyScrollingEvent(RemoteAccessible* aTarget, uint32_t aEventType,
+ uint32_t aScrollX, uint32_t aScrollY,
+ uint32_t aMaxScrollX, uint32_t aMaxScrollY) {
+ if (aEventType == nsIAccessibleEvent::EVENT_SCROLLING) {
+ RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aTarget);
+
+ if (sessionAcc) {
+ sessionAcc->SendScrollingEvent(aTarget, aScrollX, aScrollY, aMaxScrollX,
+ aMaxScrollY);
+ }
+ }
+}
+
+void a11y::ProxyAnnouncementEvent(RemoteAccessible* aTarget,
+ const nsAString& aAnnouncement,
+ uint16_t aPriority) {
+ RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aTarget);
+
+ if (sessionAcc) {
+ sessionAcc->SendAnnouncementEvent(aTarget, aAnnouncement, aPriority);
+ }
+}
+
+void a11y::ProxyBatch(RemoteAccessible* aDocument, const uint64_t aBatchType,
+ const nsTArray<RemoteAccessible*>& aAccessibles,
+ const nsTArray<BatchData>& aData) {
+ RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aDocument);
+ if (!sessionAcc) {
+ return;
+ }
+
+ nsTArray<Accessible*> accessibles(aAccessibles.Length());
+ for (size_t i = 0; i < aAccessibles.Length(); i++) {
+ accessibles.AppendElement(aAccessibles.ElementAt(i));
+ }
+
+ switch (aBatchType) {
+ case DocAccessibleWrap::eBatch_Viewport:
+ sessionAcc->ReplaceViewportCache(accessibles, aData);
+ break;
+ case DocAccessibleWrap::eBatch_FocusPath:
+ sessionAcc->ReplaceFocusPathCache(accessibles, aData);
+ break;
+ case DocAccessibleWrap::eBatch_BoundsUpdate:
+ sessionAcc->UpdateCachedBounds(accessibles, aData);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown batch type.");
+ break;
+ }
+}
+
+bool a11y::LocalizeString(const nsAString& aToken, nsAString& aLocalized) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ auto str = sLocalizedStrings.Lookup(aToken);
+ if (str) {
+ aLocalized.Assign(*str);
+ } else {
+ }
+
+ return !!str;
+}
diff --git a/accessible/android/RootAccessibleWrap.cpp b/accessible/android/RootAccessibleWrap.cpp
new file mode 100644
index 0000000000..d21aed39ba
--- /dev/null
+++ b/accessible/android/RootAccessibleWrap.cpp
@@ -0,0 +1,69 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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 "RootAccessibleWrap.h"
+
+#include "LocalAccessible-inl.h"
+
+#include "DocAccessibleParent.h"
+#include "DocAccessible-inl.h"
+#include "SessionAccessibility.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/EventTarget.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/MouseEvent.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+using namespace mozilla::dom;
+
+RootAccessibleWrap::RootAccessibleWrap(dom::Document* aDoc,
+ PresShell* aPresShell)
+ : RootAccessible(aDoc, aPresShell) {}
+
+RootAccessibleWrap::~RootAccessibleWrap() {}
+
+nsresult RootAccessibleWrap::AddEventListeners() {
+ nsPIDOMWindowOuter* window = mDocumentNode->GetWindow();
+ nsCOMPtr<EventTarget> nstarget = window ? window->GetParentTarget() : nullptr;
+
+ if (nstarget) {
+ nstarget->AddEventListener(u"MozMouseExploreByTouch"_ns, this, false, true);
+ }
+
+ return RootAccessible::AddEventListeners();
+}
+
+nsresult RootAccessibleWrap::RemoveEventListeners() {
+ nsPIDOMWindowOuter* window = mDocumentNode->GetWindow();
+ nsCOMPtr<EventTarget> nstarget = window ? window->GetParentTarget() : nullptr;
+ if (nstarget) {
+ nstarget->RemoveEventListener(u"MozMouseExploreByTouch"_ns, this, true);
+ }
+
+ return RootAccessible::RemoveEventListeners();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIDOMEventListener
+
+NS_IMETHODIMP
+RootAccessibleWrap::HandleEvent(Event* aDOMEvent) {
+ WidgetMouseEvent* widgetEvent = aDOMEvent->WidgetEventPtr()->AsMouseEvent();
+ if (widgetEvent && widgetEvent->mMessage == eMouseExploreByTouch) {
+ if (HasShutdown()) {
+ return NS_OK;
+ }
+
+ if (MouseEvent* mouseEvent = aDOMEvent->AsMouseEvent()) {
+ LayoutDeviceIntPoint point = mouseEvent->ScreenPointLayoutDevicePix();
+ ExploreByTouch(point.x, point.y);
+ }
+
+ return NS_OK;
+ }
+
+ return RootAccessible::HandleEvent(aDOMEvent);
+}
diff --git a/accessible/android/RootAccessibleWrap.h b/accessible/android/RootAccessibleWrap.h
new file mode 100644
index 0000000000..5c5ba95f61
--- /dev/null
+++ b/accessible/android/RootAccessibleWrap.h
@@ -0,0 +1,37 @@
+/* -*- 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/. */
+
+#ifndef mozilla_a11y_RootAccessibleWrap_h__
+#define mozilla_a11y_RootAccessibleWrap_h__
+
+#include "RootAccessible.h"
+
+namespace mozilla {
+
+class PresShell;
+
+namespace a11y {
+
+/**
+ * Android specific functionality for the node at a root of the accessibility
+ * tree: see the RootAccessible superclass for further details.
+ */
+class RootAccessibleWrap : public RootAccessible {
+ public:
+ RootAccessibleWrap(dom::Document* aDocument, PresShell* aPresShell);
+ virtual ~RootAccessibleWrap();
+
+ // nsIDOMEventListener
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ protected:
+ virtual nsresult AddEventListeners() override;
+ virtual nsresult RemoveEventListeners() override;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/android/SessionAccessibility.cpp b/accessible/android/SessionAccessibility.cpp
new file mode 100644
index 0000000000..12c426ed95
--- /dev/null
+++ b/accessible/android/SessionAccessibility.cpp
@@ -0,0 +1,1120 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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 "SessionAccessibility.h"
+#include "LocalAccessible-inl.h"
+#include "AndroidUiThread.h"
+#include "AndroidBridge.h"
+#include "DocAccessibleParent.h"
+#include "IDSet.h"
+#include "nsThreadUtils.h"
+#include "AccAttributes.h"
+#include "AccessibilityEvent.h"
+#include "HyperTextAccessible.h"
+#include "HyperTextAccessible-inl.h"
+#include "JavaBuiltins.h"
+#include "RootAccessibleWrap.h"
+#include "nsAccessibilityService.h"
+#include "nsAccUtils.h"
+#include "nsViewManager.h"
+
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/a11y/Accessible.h"
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "mozilla/a11y/DocAccessiblePlatformExtParent.h"
+#include "mozilla/a11y/DocManager.h"
+#include "mozilla/jni/GeckoBundleUtils.h"
+#include "mozilla/widget/GeckoViewSupport.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/StaticPrefs_accessibility.h"
+
+#ifdef DEBUG
+# include <android/log.h>
+# define AALOG(args...) \
+ __android_log_print(ANDROID_LOG_INFO, "GeckoAccessibilityNative", ##args)
+#else
+# define AALOG(args...) \
+ do { \
+ } while (0)
+#endif
+
+#define FORWARD_ACTION_TO_ACCESSIBLE(funcname, ...) \
+ MOZ_ASSERT(NS_IsMainThread()); \
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); \
+ if (Accessible* acc = GetAccessibleByID(aID)) { \
+ if (acc->IsRemote()) { \
+ acc->AsRemote()->funcname(__VA_ARGS__); \
+ } else { \
+ static_cast<AccessibleWrap*>(acc->AsLocal())->funcname(__VA_ARGS__); \
+ } \
+ }
+
+#define FORWARD_EXT_ACTION_TO_ACCESSIBLE(funcname, ...) \
+ MOZ_ASSERT(NS_IsMainThread()); \
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); \
+ if (Accessible* acc = GetAccessibleByID(aID)) { \
+ if (RemoteAccessible* remote = acc->AsRemote()) { \
+ Unused << remote->Document()->GetPlatformExtension()->Send##funcname( \
+ remote->ID(), ##__VA_ARGS__); \
+ } else { \
+ static_cast<AccessibleWrap*>(acc->AsLocal())->funcname(__VA_ARGS__); \
+ } \
+ }
+
+using namespace mozilla::a11y;
+
+// IDs should be a positive 32bit integer.
+IDSet sIDSet(31UL);
+
+class Settings final
+ : public mozilla::java::SessionAccessibility::Settings::Natives<Settings> {
+ public:
+ static void ToggleNativeAccessibility(bool aEnable) {
+ if (aEnable) {
+ GetOrCreateAccService();
+ } else {
+ MaybeShutdownAccService(nsAccessibilityService::ePlatformAPI);
+ }
+ }
+};
+
+SessionAccessibility::SessionAccessibility(
+ jni::NativeWeakPtr<widget::GeckoViewSupport> aWindow,
+ java::SessionAccessibility::NativeProvider::Param aSessionAccessibility)
+ : mWindow(aWindow), mSessionAccessibility(aSessionAccessibility) {
+ SetAttached(true, nullptr);
+}
+
+void SessionAccessibility::SetAttached(bool aAttached,
+ already_AddRefed<Runnable> aRunnable) {
+ if (RefPtr<nsThread> uiThread = GetAndroidUiThread()) {
+ uiThread->Dispatch(NS_NewRunnableFunction(
+ "SessionAccessibility::Attach",
+ [aAttached,
+ sa = java::SessionAccessibility::NativeProvider::GlobalRef(
+ mSessionAccessibility),
+ runnable = RefPtr<Runnable>(aRunnable)] {
+ sa->SetAttached(aAttached);
+ if (runnable) {
+ runnable->Run();
+ }
+ }));
+ }
+}
+
+void SessionAccessibility::Init() {
+ java::SessionAccessibility::NativeProvider::Natives<
+ SessionAccessibility>::Init();
+ Settings::Init();
+}
+
+bool SessionAccessibility::IsCacheEnabled() {
+ return StaticPrefs::accessibility_cache_enabled_AtStartup();
+}
+
+void SessionAccessibility::GetNodeInfo(int32_t aID,
+ mozilla::jni::Object::Param aNodeInfo) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+ ReleasableMonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ java::GeckoBundle::GlobalRef ret = nullptr;
+ RefPtr<SessionAccessibility> self(this);
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ if (acc->IsLocal() || !IsCacheEnabled()) {
+ mal.Unlock();
+ nsAppShell::SyncRunEvent(
+ [this, self, aID, aNodeInfo = jni::Object::GlobalRef(aNodeInfo)] {
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ PopulateNodeInfo(acc, aNodeInfo);
+ } else {
+ AALOG("oops, nothing for %d", aID);
+ }
+ });
+ } else {
+ PopulateNodeInfo(acc, aNodeInfo);
+ }
+ } else {
+ AALOG("oops, nothing for %d", aID);
+ }
+}
+
+int SessionAccessibility::GetNodeClassName(int32_t aID) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+ MOZ_ASSERT(IsCacheEnabled(), "Cache is enabled");
+ ReleasableMonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ int32_t classNameEnum = java::SessionAccessibility::CLASSNAME_VIEW;
+ RefPtr<SessionAccessibility> self(this);
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ if (acc->IsLocal()) {
+ mal.Unlock();
+ nsAppShell::SyncRunEvent([this, self, aID, &classNameEnum] {
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ classNameEnum = AccessibleWrap::AndroidClass(acc);
+ }
+ });
+ } else {
+ classNameEnum = AccessibleWrap::AndroidClass(acc);
+ }
+ }
+
+ return classNameEnum;
+}
+
+void SessionAccessibility::SetText(int32_t aID, jni::String::Param aText) {
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ if (acc->IsRemote()) {
+ acc->AsRemote()->ReplaceText(PromiseFlatString(aText->ToString()));
+ } else if (acc->AsLocal()->IsHyperText()) {
+ acc->AsLocal()->AsHyperText()->ReplaceText(aText->ToString());
+ }
+ }
+}
+
+void SessionAccessibility::Click(int32_t aID) {
+ FORWARD_ACTION_TO_ACCESSIBLE(DoAction, 0);
+}
+
+bool SessionAccessibility::CachedPivot(int32_t aID, int32_t aGranularity,
+ bool aForward, bool aInclusive) {
+ MOZ_ASSERT(IsCacheEnabled(), "Cache is enabled");
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ RefPtr<SessionAccessibility> self(this);
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ if (acc->IsLocal()) {
+ nsAppShell::PostEvent(
+ [this, self, aID, aGranularity, aForward, aInclusive] {
+ Pivot(aID, aGranularity, aForward, aInclusive);
+ });
+ return true;
+ }
+ Accessible* result =
+ AccessibleWrap::DoPivot(acc, aGranularity, aForward, aInclusive);
+ if (result) {
+ int32_t virtualViewID = AccessibleWrap::GetVirtualViewID(result);
+ nsAppShell::PostEvent([this, self, virtualViewID] {
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ if (Accessible* acc = GetAccessibleByID(virtualViewID)) {
+ SendAccessibilityFocusedEvent(acc);
+ }
+ });
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void SessionAccessibility::Pivot(int32_t aID, int32_t aGranularity,
+ bool aForward, bool aInclusive) {
+ FORWARD_EXT_ACTION_TO_ACCESSIBLE(PivotTo, aGranularity, aForward, aInclusive);
+}
+
+void SessionAccessibility::ExploreByTouch(int32_t aID, float aX, float aY) {
+ auto gvAccessor(mWindow.Access());
+ if (gvAccessor) {
+ if (nsWindow* gkWindow = gvAccessor->GetNsWindow()) {
+ WidgetMouseEvent hittest(true, eMouseExploreByTouch, gkWindow,
+ WidgetMouseEvent::eReal);
+ hittest.mRefPoint = LayoutDeviceIntPoint::Floor(aX, aY);
+ hittest.mInputSource = dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH;
+ hittest.mFlags.mOnlyChromeDispatch = true;
+ gkWindow->DispatchInputEvent(&hittest);
+ }
+ }
+}
+
+void SessionAccessibility::NavigateText(int32_t aID, int32_t aGranularity,
+ int32_t aStartOffset,
+ int32_t aEndOffset, bool aForward,
+ bool aSelect) {
+ FORWARD_EXT_ACTION_TO_ACCESSIBLE(NavigateText, aGranularity, aStartOffset,
+ aEndOffset, aForward, aSelect);
+}
+
+void SessionAccessibility::SetSelection(int32_t aID, int32_t aStart,
+ int32_t aEnd) {
+ FORWARD_EXT_ACTION_TO_ACCESSIBLE(SetSelection, aStart, aEnd);
+}
+
+void SessionAccessibility::Cut(int32_t aID) {
+ FORWARD_EXT_ACTION_TO_ACCESSIBLE(Cut);
+}
+
+void SessionAccessibility::Copy(int32_t aID) {
+ FORWARD_EXT_ACTION_TO_ACCESSIBLE(Copy);
+}
+
+void SessionAccessibility::Paste(int32_t aID) {
+ FORWARD_EXT_ACTION_TO_ACCESSIBLE(Paste);
+}
+
+#undef FORWARD_ACTION_TO_ACCESSIBLE
+#undef FORWARD_EXT_ACTION_TO_ACCESSIBLE
+
+RefPtr<SessionAccessibility> SessionAccessibility::GetInstanceFor(
+ Accessible* aAccessible) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (LocalAccessible* localAcc = aAccessible->AsLocal()) {
+ DocAccessible* docAcc = localAcc->Document();
+ // If the accessible is being shutdown from the doc's shutdown
+ // the doc accessible won't have a ref to a presshell anymore,
+ // but we should have a ref to the DOM document node, and the DOM doc
+ // has a ref to the presshell.
+ dom::Document* doc = docAcc ? docAcc->DocumentNode() : nullptr;
+ if (doc && doc->IsContentDocument()) {
+ // Only content accessibles should have an associated SessionAccessible.
+ return GetInstanceFor(doc->GetPresShell());
+ }
+ } else {
+ dom::CanonicalBrowsingContext* cbc =
+ static_cast<dom::BrowserParent*>(
+ aAccessible->AsRemote()->Document()->Manager())
+ ->GetBrowsingContext()
+ ->Top();
+ dom::BrowserParent* bp = cbc->GetBrowserParent();
+ if (!bp) {
+ bp = static_cast<dom::BrowserParent*>(
+ aAccessible->AsRemote()->Document()->Manager());
+ }
+ if (auto element = bp->GetOwnerElement()) {
+ if (auto doc = element->OwnerDoc()) {
+ if (nsPresContext* presContext = doc->GetPresContext()) {
+ return GetInstanceFor(presContext->PresShell());
+ }
+ } else {
+ MOZ_ASSERT_UNREACHABLE(
+ "Browser parent's element does not have owner doc.");
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+RefPtr<SessionAccessibility> SessionAccessibility::GetInstanceFor(
+ PresShell* aPresShell) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!aPresShell) {
+ return nullptr;
+ }
+
+ nsViewManager* vm = aPresShell->GetViewManager();
+ if (!vm) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIWidget> rootWidget = vm->GetRootWidget();
+ // `rootWidget` can be one of several types. Here we make sure it is an
+ // android nsWindow.
+ if (RefPtr<nsWindow> window = nsWindow::From(rootWidget)) {
+ return window->GetSessionAccessibility();
+ }
+
+ return nullptr;
+}
+
+void SessionAccessibility::SendAccessibilityFocusedEvent(
+ Accessible* aAccessible) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSessionAccessibility->SendEvent(
+ java::sdk::AccessibilityEvent::TYPE_VIEW_ACCESSIBILITY_FOCUSED,
+ AccessibleWrap::GetVirtualViewID(aAccessible),
+ AccessibleWrap::AndroidClass(aAccessible), nullptr);
+ aAccessible->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE);
+}
+
+void SessionAccessibility::SendHoverEnterEvent(Accessible* aAccessible) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSessionAccessibility->SendEvent(
+ java::sdk::AccessibilityEvent::TYPE_VIEW_HOVER_ENTER,
+ AccessibleWrap::GetVirtualViewID(aAccessible),
+ AccessibleWrap::AndroidClass(aAccessible), nullptr);
+}
+
+void SessionAccessibility::SendFocusEvent(Accessible* aAccessible) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Suppress focus events from about:blank pages.
+ // This is important for tests.
+ if (aAccessible->IsDoc() && aAccessible->ChildCount() == 0) {
+ return;
+ }
+
+ mSessionAccessibility->SendEvent(
+ java::sdk::AccessibilityEvent::TYPE_VIEW_FOCUSED,
+ AccessibleWrap::GetVirtualViewID(aAccessible),
+ AccessibleWrap::AndroidClass(aAccessible), nullptr);
+}
+
+void SessionAccessibility::SendScrollingEvent(Accessible* aAccessible,
+ int32_t aScrollX,
+ int32_t aScrollY,
+ int32_t aMaxScrollX,
+ int32_t aMaxScrollY) {
+ MOZ_ASSERT(NS_IsMainThread());
+ int32_t virtualViewId = AccessibleWrap::GetVirtualViewID(aAccessible);
+
+ if (virtualViewId != kNoID) {
+ // XXX: Support scrolling in subframes
+ return;
+ }
+
+ GECKOBUNDLE_START(eventInfo);
+ GECKOBUNDLE_PUT(eventInfo, "scrollX", java::sdk::Integer::ValueOf(aScrollX));
+ GECKOBUNDLE_PUT(eventInfo, "scrollY", java::sdk::Integer::ValueOf(aScrollY));
+ GECKOBUNDLE_PUT(eventInfo, "maxScrollX",
+ java::sdk::Integer::ValueOf(aMaxScrollX));
+ GECKOBUNDLE_PUT(eventInfo, "maxScrollY",
+ java::sdk::Integer::ValueOf(aMaxScrollY));
+ GECKOBUNDLE_FINISH(eventInfo);
+
+ mSessionAccessibility->SendEvent(
+ java::sdk::AccessibilityEvent::TYPE_VIEW_SCROLLED, virtualViewId,
+ AccessibleWrap::AndroidClass(aAccessible), eventInfo);
+ SendWindowContentChangedEvent();
+}
+
+void SessionAccessibility::SendWindowContentChangedEvent() {
+ mSessionAccessibility->SendEvent(
+ java::sdk::AccessibilityEvent::TYPE_WINDOW_CONTENT_CHANGED, kNoID,
+ java::SessionAccessibility::CLASSNAME_WEBVIEW, nullptr);
+}
+
+void SessionAccessibility::SendWindowStateChangedEvent(
+ Accessible* aAccessible) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Suppress window state changed events from about:blank pages.
+ // This is important for tests.
+ if (aAccessible->IsDoc() && aAccessible->ChildCount() == 0) {
+ return;
+ }
+
+ mSessionAccessibility->SendEvent(
+ java::sdk::AccessibilityEvent::TYPE_WINDOW_STATE_CHANGED,
+ AccessibleWrap::GetVirtualViewID(aAccessible),
+ AccessibleWrap::AndroidClass(aAccessible), nullptr);
+
+ if (IsCacheEnabled()) {
+ SendWindowContentChangedEvent();
+ }
+}
+
+void SessionAccessibility::SendTextSelectionChangedEvent(
+ Accessible* aAccessible, int32_t aCaretOffset) {
+ MOZ_ASSERT(NS_IsMainThread());
+ int32_t fromIndex = aCaretOffset;
+ int32_t startSel = -1;
+ int32_t endSel = -1;
+ bool hasSelection = false;
+ if (aAccessible->IsRemote() && !IsCacheEnabled()) {
+ nsAutoString unused;
+ hasSelection = aAccessible->AsRemote()->SelectionBoundsAt(
+ 0, unused, &startSel, &endSel);
+ } else {
+ hasSelection = aAccessible->AsHyperTextBase()->SelectionBoundsAt(
+ 0, &startSel, &endSel);
+ }
+
+ if (hasSelection) {
+ fromIndex = startSel == aCaretOffset ? endSel : startSel;
+ }
+
+ GECKOBUNDLE_START(eventInfo);
+ GECKOBUNDLE_PUT(eventInfo, "fromIndex",
+ java::sdk::Integer::ValueOf(fromIndex));
+ GECKOBUNDLE_PUT(eventInfo, "toIndex",
+ java::sdk::Integer::ValueOf(aCaretOffset));
+ GECKOBUNDLE_FINISH(eventInfo);
+
+ mSessionAccessibility->SendEvent(
+ java::sdk::AccessibilityEvent::TYPE_VIEW_TEXT_SELECTION_CHANGED,
+ AccessibleWrap::GetVirtualViewID(aAccessible),
+ AccessibleWrap::AndroidClass(aAccessible), eventInfo);
+}
+
+void SessionAccessibility::SendTextChangedEvent(Accessible* aAccessible,
+ const nsAString& aStr,
+ int32_t aStart, uint32_t aLen,
+ bool aIsInsert,
+ bool aFromUser) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!aFromUser) {
+ // Only dispatch text change events from users, for now.
+ return;
+ }
+
+ nsAutoString text;
+ if (aAccessible->IsHyperText()) {
+ aAccessible->AsHyperTextBase()->TextSubstring(0, -1, text);
+ } else if (aAccessible->IsText()) {
+ if (aAccessible->IsRemote() && !IsCacheEnabled()) {
+ // XXX: AppendTextTo is not implemented in the IPDL and only
+ // works when cache is enabled.
+ aAccessible->Name(text);
+ } else {
+ aAccessible->AppendTextTo(text, 0, -1);
+ }
+ }
+ nsAutoString beforeText(text);
+ if (aIsInsert) {
+ beforeText.Cut(aStart, aLen);
+ } else {
+ beforeText.Insert(aStr, aStart);
+ }
+
+ GECKOBUNDLE_START(eventInfo);
+ GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(text));
+ GECKOBUNDLE_PUT(eventInfo, "beforeText", jni::StringParam(beforeText));
+ GECKOBUNDLE_PUT(eventInfo, "addedCount",
+ java::sdk::Integer::ValueOf(aIsInsert ? aLen : 0));
+ GECKOBUNDLE_PUT(eventInfo, "removedCount",
+ java::sdk::Integer::ValueOf(aIsInsert ? 0 : aLen));
+ GECKOBUNDLE_FINISH(eventInfo);
+
+ mSessionAccessibility->SendEvent(
+ java::sdk::AccessibilityEvent::TYPE_VIEW_TEXT_CHANGED,
+ AccessibleWrap::GetVirtualViewID(aAccessible),
+ AccessibleWrap::AndroidClass(aAccessible), eventInfo);
+}
+
+void SessionAccessibility::SendTextTraversedEvent(Accessible* aAccessible,
+ int32_t aStartOffset,
+ int32_t aEndOffset) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsAutoString text;
+ if (aAccessible->IsHyperText()) {
+ aAccessible->AsHyperTextBase()->TextSubstring(0, -1, text);
+ } else if (aAccessible->IsText()) {
+ if (aAccessible->IsRemote() && !IsCacheEnabled()) {
+ // XXX: AppendTextTo is not implemented in the IPDL and only
+ // works when cache is enabled.
+ aAccessible->Name(text);
+ } else {
+ aAccessible->AppendTextTo(text, 0, -1);
+ }
+ }
+
+ GECKOBUNDLE_START(eventInfo);
+ GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(text));
+ GECKOBUNDLE_PUT(eventInfo, "fromIndex",
+ java::sdk::Integer::ValueOf(aStartOffset));
+ GECKOBUNDLE_PUT(eventInfo, "toIndex",
+ java::sdk::Integer::ValueOf(aEndOffset));
+ GECKOBUNDLE_FINISH(eventInfo);
+
+ mSessionAccessibility->SendEvent(
+ java::sdk::AccessibilityEvent::
+ TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+ AccessibleWrap::GetVirtualViewID(aAccessible),
+ AccessibleWrap::AndroidClass(aAccessible), eventInfo);
+}
+
+void SessionAccessibility::SendClickedEvent(Accessible* aAccessible,
+ uint32_t aFlags) {
+ GECKOBUNDLE_START(eventInfo);
+ GECKOBUNDLE_PUT(eventInfo, "flags", java::sdk::Integer::ValueOf(aFlags));
+ GECKOBUNDLE_FINISH(eventInfo);
+
+ mSessionAccessibility->SendEvent(
+ java::sdk::AccessibilityEvent::TYPE_VIEW_CLICKED,
+ AccessibleWrap::GetVirtualViewID(aAccessible),
+ AccessibleWrap::AndroidClass(aAccessible), eventInfo);
+}
+
+void SessionAccessibility::SendSelectedEvent(Accessible* aAccessible,
+ bool aSelected) {
+ MOZ_ASSERT(NS_IsMainThread());
+ GECKOBUNDLE_START(eventInfo);
+ // Boolean::FALSE/TRUE gets clobbered by a macro, so ugh.
+ GECKOBUNDLE_PUT(eventInfo, "selected",
+ java::sdk::Integer::ValueOf(aSelected ? 1 : 0));
+ GECKOBUNDLE_FINISH(eventInfo);
+
+ mSessionAccessibility->SendEvent(
+ java::sdk::AccessibilityEvent::TYPE_VIEW_SELECTED,
+ AccessibleWrap::GetVirtualViewID(aAccessible),
+ AccessibleWrap::AndroidClass(aAccessible), eventInfo);
+}
+
+void SessionAccessibility::SendAnnouncementEvent(Accessible* aAccessible,
+ const nsAString& aAnnouncement,
+ uint16_t aPriority) {
+ MOZ_ASSERT(NS_IsMainThread());
+ GECKOBUNDLE_START(eventInfo);
+ GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(aAnnouncement));
+ GECKOBUNDLE_FINISH(eventInfo);
+
+ // Announcements should have the root as their source, so we ignore the
+ // accessible of the event.
+ mSessionAccessibility->SendEvent(
+ java::sdk::AccessibilityEvent::TYPE_ANNOUNCEMENT, kNoID,
+ java::SessionAccessibility::CLASSNAME_WEBVIEW, eventInfo);
+}
+
+void SessionAccessibility::ReplaceViewportCache(
+ const nsTArray<Accessible*>& aAccessibles,
+ const nsTArray<BatchData>& aData) {
+ auto infos = jni::ObjectArray::New<java::GeckoBundle>(aAccessibles.Length());
+ for (size_t i = 0; i < aAccessibles.Length(); i++) {
+ Accessible* acc = aAccessibles.ElementAt(i);
+ if (!acc) {
+ MOZ_ASSERT_UNREACHABLE("Updated accessible is gone.");
+ continue;
+ }
+
+ if (aData.Length() == aAccessibles.Length()) {
+ const BatchData& data = aData.ElementAt(i);
+ auto bundle = ToBundle(acc, data.State(), data.Bounds(),
+ data.ActionCount(), data.Name(), data.TextValue(),
+ data.DOMNodeID(), data.Description());
+ infos->SetElement(i, bundle);
+ } else {
+ infos->SetElement(i, ToBundle(acc, true));
+ }
+ }
+
+ mSessionAccessibility->ReplaceViewportCache(infos);
+ SendWindowContentChangedEvent();
+}
+
+void SessionAccessibility::ReplaceFocusPathCache(
+ const nsTArray<Accessible*>& aAccessibles,
+ const nsTArray<BatchData>& aData) {
+ auto infos = jni::ObjectArray::New<java::GeckoBundle>(aAccessibles.Length());
+ for (size_t i = 0; i < aAccessibles.Length(); i++) {
+ Accessible* acc = aAccessibles.ElementAt(i);
+ if (!acc) {
+ MOZ_ASSERT_UNREACHABLE("Updated accessible is gone.");
+ continue;
+ }
+
+ if (aData.Length() == aAccessibles.Length()) {
+ const BatchData& data = aData.ElementAt(i);
+ auto bundle =
+ ToBundle(acc, data.State(), data.Bounds(), data.ActionCount(),
+ data.Name(), data.TextValue(), data.DOMNodeID(),
+ data.Description(), data.CurValue(), data.MinValue(),
+ data.MaxValue(), data.Step(), data.Attributes());
+ infos->SetElement(i, bundle);
+ } else {
+ infos->SetElement(i, ToBundle(acc));
+ }
+ }
+
+ mSessionAccessibility->ReplaceFocusPathCache(infos);
+}
+
+void SessionAccessibility::UpdateCachedBounds(
+ const nsTArray<Accessible*>& aAccessibles,
+ const nsTArray<BatchData>& aData) {
+ auto infos = jni::ObjectArray::New<java::GeckoBundle>(aAccessibles.Length());
+ for (size_t i = 0; i < aAccessibles.Length(); i++) {
+ Accessible* acc = aAccessibles.ElementAt(i);
+ if (!acc) {
+ MOZ_ASSERT_UNREACHABLE("Updated accessible is gone.");
+ continue;
+ }
+
+ if (aData.Length() == aAccessibles.Length()) {
+ const BatchData& data = aData.ElementAt(i);
+ auto bundle = ToBundle(acc, data.State(), data.Bounds(),
+ data.ActionCount(), data.Name(), data.TextValue(),
+ data.DOMNodeID(), data.Description());
+ infos->SetElement(i, bundle);
+ } else {
+ infos->SetElement(i, ToBundle(acc, true));
+ }
+ }
+
+ mSessionAccessibility->UpdateCachedBounds(infos);
+}
+
+void SessionAccessibility::UpdateAccessibleFocusBoundaries(Accessible* aFirst,
+ Accessible* aLast) {
+ mSessionAccessibility->UpdateAccessibleFocusBoundaries(
+ aFirst ? AccessibleWrap::GetVirtualViewID(aFirst) : kNoID,
+ aLast ? AccessibleWrap::GetVirtualViewID(aLast) : kNoID);
+}
+
+mozilla::java::GeckoBundle::LocalRef SessionAccessibility::ToBundle(
+ Accessible* aAccessible, bool aSmall) {
+ nsAutoString name;
+ aAccessible->Name(name);
+ nsAutoString textValue;
+ aAccessible->Value(textValue);
+ nsAutoString nodeID;
+ aAccessible->DOMNodeID(nodeID);
+ nsAutoString description;
+ aAccessible->Description(description);
+ uint64_t state = aAccessible->State();
+ LayoutDeviceIntRect bounds = aAccessible->Bounds();
+ uint8_t actionCount = aAccessible->ActionCount();
+
+ if (aSmall) {
+ return ToBundle(aAccessible, state, bounds, actionCount, name, textValue,
+ nodeID, description);
+ }
+
+ double curValue = UnspecifiedNaN<double>();
+ double minValue = UnspecifiedNaN<double>();
+ double maxValue = UnspecifiedNaN<double>();
+ double step = UnspecifiedNaN<double>();
+ if (aAccessible->HasNumericValue()) {
+ curValue = aAccessible->CurValue();
+ minValue = aAccessible->MinValue();
+ maxValue = aAccessible->MaxValue();
+ step = aAccessible->Step();
+ }
+
+ RefPtr<AccAttributes> attributes = aAccessible->Attributes();
+
+ return ToBundle(aAccessible, state, bounds, actionCount, name, textValue,
+ nodeID, description, curValue, minValue, maxValue, step,
+ attributes);
+}
+
+mozilla::java::GeckoBundle::LocalRef SessionAccessibility::ToBundle(
+ Accessible* aAccessible, const uint64_t aState,
+ const LayoutDeviceIntRect& aBounds, const uint8_t aActionCount,
+ const nsString& aName, const nsString& aTextValue,
+ const nsString& aDOMNodeID, const nsString& aDescription,
+ const double& aCurVal, const double& aMinVal, const double& aMaxVal,
+ const double& aStep, AccAttributes* aAttributes) {
+ MOZ_ASSERT(NS_IsMainThread());
+ int32_t virtualViewID = AccessibleWrap::GetVirtualViewID(aAccessible);
+ GECKOBUNDLE_START(nodeInfo);
+ GECKOBUNDLE_PUT(nodeInfo, "id", java::sdk::Integer::ValueOf(virtualViewID));
+
+ Accessible* parent = virtualViewID != kNoID ? aAccessible->Parent() : nullptr;
+ GECKOBUNDLE_PUT(nodeInfo, "parentId",
+ java::sdk::Integer::ValueOf(
+ parent ? AccessibleWrap::GetVirtualViewID(parent) : 0));
+
+ role role = aAccessible->Role();
+ if (role == roles::LINK && !(aState & states::LINKED)) {
+ // A link without the linked state (<a> with no href) shouldn't be presented
+ // as a link.
+ role = roles::TEXT;
+ }
+
+ uint32_t flags = AccessibleWrap::GetFlags(role, aState, aActionCount);
+ GECKOBUNDLE_PUT(nodeInfo, "flags", java::sdk::Integer::ValueOf(flags));
+ GECKOBUNDLE_PUT(
+ nodeInfo, "className",
+ java::sdk::Integer::ValueOf(AccessibleWrap::AndroidClass(aAccessible)));
+
+ nsAutoString hint;
+ if (aState & states::EDITABLE) {
+ // An editable field's name is populated in the hint.
+ hint.Assign(aName);
+ GECKOBUNDLE_PUT(nodeInfo, "text", jni::StringParam(aTextValue));
+ } else {
+ if (role == roles::LINK || role == roles::HEADING) {
+ GECKOBUNDLE_PUT(nodeInfo, "description", jni::StringParam(aName));
+ } else {
+ GECKOBUNDLE_PUT(nodeInfo, "text", jni::StringParam(aName));
+ }
+ }
+
+ if (!aDescription.IsEmpty()) {
+ if (!hint.IsEmpty()) {
+ // If this is an editable, the description is concatenated with a
+ // whitespace directly after the name.
+ hint.AppendLiteral(" ");
+ }
+ hint.Append(aDescription);
+ }
+
+ if ((aState & states::REQUIRED) != 0) {
+ nsAutoString requiredString;
+ if (LocalizeString(u"stateRequired"_ns, requiredString)) {
+ if (!hint.IsEmpty()) {
+ // If the hint is non-empty, concatenate with a comma for a brief pause.
+ hint.AppendLiteral(", ");
+ }
+ hint.Append(requiredString);
+ }
+ }
+
+ if (!hint.IsEmpty()) {
+ GECKOBUNDLE_PUT(nodeInfo, "hint", jni::StringParam(hint));
+ }
+
+ nsAutoString geckoRole;
+ nsAutoString roleDescription;
+ if (virtualViewID != kNoID) {
+ AccessibleWrap::GetRoleDescription(role, aAttributes, geckoRole,
+ roleDescription);
+ }
+
+ GECKOBUNDLE_PUT(nodeInfo, "roleDescription",
+ jni::StringParam(roleDescription));
+ GECKOBUNDLE_PUT(nodeInfo, "geckoRole", jni::StringParam(geckoRole));
+
+ if (!aDOMNodeID.IsEmpty()) {
+ GECKOBUNDLE_PUT(nodeInfo, "viewIdResourceName",
+ jni::StringParam(aDOMNodeID));
+ }
+
+ const int32_t data[4] = {aBounds.x, aBounds.y, aBounds.x + aBounds.width,
+ aBounds.y + aBounds.height};
+ GECKOBUNDLE_PUT(nodeInfo, "bounds", jni::IntArray::New(data, 4));
+
+ if (aAccessible->HasNumericValue()) {
+ GECKOBUNDLE_START(rangeInfo);
+ if (aMaxVal == 1 && aMinVal == 0) {
+ GECKOBUNDLE_PUT(rangeInfo, "type",
+ java::sdk::Integer::ValueOf(2)); // percent
+ } else if (std::round(aStep) != aStep) {
+ GECKOBUNDLE_PUT(rangeInfo, "type",
+ java::sdk::Integer::ValueOf(1)); // float
+ } else {
+ GECKOBUNDLE_PUT(rangeInfo, "type",
+ java::sdk::Integer::ValueOf(0)); // integer
+ }
+
+ if (!IsNaN(aCurVal)) {
+ GECKOBUNDLE_PUT(rangeInfo, "current", java::sdk::Double::New(aCurVal));
+ }
+ if (!IsNaN(aMinVal)) {
+ GECKOBUNDLE_PUT(rangeInfo, "min", java::sdk::Double::New(aMinVal));
+ }
+ if (!IsNaN(aMaxVal)) {
+ GECKOBUNDLE_PUT(rangeInfo, "max", java::sdk::Double::New(aMaxVal));
+ }
+
+ GECKOBUNDLE_FINISH(rangeInfo);
+ GECKOBUNDLE_PUT(nodeInfo, "rangeInfo", rangeInfo);
+ }
+
+ if (aAttributes) {
+ nsString inputTypeAttr;
+ aAttributes->GetAttribute(nsGkAtoms::textInputType, inputTypeAttr);
+ int32_t inputType = AccessibleWrap::GetInputType(inputTypeAttr);
+ if (inputType) {
+ GECKOBUNDLE_PUT(nodeInfo, "inputType",
+ java::sdk::Integer::ValueOf(inputType));
+ }
+
+ Maybe<int32_t> rowIndex =
+ aAttributes->GetAttribute<int32_t>(nsGkAtoms::posinset);
+ if (rowIndex) {
+ GECKOBUNDLE_START(collectionItemInfo);
+ GECKOBUNDLE_PUT(collectionItemInfo, "rowIndex",
+ java::sdk::Integer::ValueOf(*rowIndex));
+ GECKOBUNDLE_PUT(collectionItemInfo, "columnIndex",
+ java::sdk::Integer::ValueOf(0));
+ GECKOBUNDLE_PUT(collectionItemInfo, "rowSpan",
+ java::sdk::Integer::ValueOf(1));
+ GECKOBUNDLE_PUT(collectionItemInfo, "columnSpan",
+ java::sdk::Integer::ValueOf(1));
+ GECKOBUNDLE_FINISH(collectionItemInfo);
+
+ GECKOBUNDLE_PUT(nodeInfo, "collectionItemInfo", collectionItemInfo);
+ }
+
+ Maybe<int32_t> rowCount =
+ aAttributes->GetAttribute<int32_t>(nsGkAtoms::child_item_count);
+ if (rowCount) {
+ GECKOBUNDLE_START(collectionInfo);
+ GECKOBUNDLE_PUT(collectionInfo, "rowCount",
+ java::sdk::Integer::ValueOf(*rowCount));
+ GECKOBUNDLE_PUT(collectionInfo, "columnCount",
+ java::sdk::Integer::ValueOf(1));
+
+ if (aAttributes->HasAttribute(nsGkAtoms::tree)) {
+ GECKOBUNDLE_PUT(collectionInfo, "isHierarchical",
+ java::sdk::Boolean::TRUE());
+ }
+
+ if (aAccessible->IsSelect()) {
+ int32_t selectionMode = (aState & states::MULTISELECTABLE) ? 2 : 1;
+ GECKOBUNDLE_PUT(collectionInfo, "selectionMode",
+ java::sdk::Integer::ValueOf(selectionMode));
+ }
+
+ GECKOBUNDLE_FINISH(collectionInfo);
+ GECKOBUNDLE_PUT(nodeInfo, "collectionInfo", collectionInfo);
+ }
+ }
+
+ if (!nsAccUtils::MustPrune(aAccessible)) {
+ auto childCount = aAccessible->ChildCount();
+ nsTArray<int32_t> children(childCount);
+ for (uint32_t i = 0; i < childCount; i++) {
+ auto child = aAccessible->ChildAt(i);
+ children.AppendElement(AccessibleWrap::GetVirtualViewID(child));
+ }
+
+ GECKOBUNDLE_PUT(nodeInfo, "children",
+ jni::IntArray::New(children.Elements(), children.Length()));
+ }
+
+ GECKOBUNDLE_FINISH(nodeInfo);
+
+ return nodeInfo;
+}
+
+void SessionAccessibility::PopulateNodeInfo(
+ Accessible* aAccessible, mozilla::jni::Object::Param aNodeInfo) {
+ nsAutoString name;
+ aAccessible->Name(name);
+ nsAutoString textValue;
+ aAccessible->Value(textValue);
+ nsAutoString nodeID;
+ aAccessible->DOMNodeID(nodeID);
+ nsAutoString accDesc;
+ aAccessible->Description(accDesc);
+ uint64_t state = aAccessible->State();
+ LayoutDeviceIntRect bounds = aAccessible->Bounds();
+ uint8_t actionCount = aAccessible->ActionCount();
+ int32_t virtualViewID = AccessibleWrap::GetVirtualViewID(aAccessible);
+ Accessible* parent = virtualViewID != kNoID ? aAccessible->Parent() : nullptr;
+ int32_t parentID = parent ? AccessibleWrap::GetVirtualViewID(parent) : 0;
+ role role = aAccessible->Role();
+ if (role == roles::LINK && !(state & states::LINKED)) {
+ // A link without the linked state (<a> with no href) shouldn't be presented
+ // as a link.
+ role = roles::TEXT;
+ }
+
+ uint32_t flags = AccessibleWrap::GetFlags(role, state, actionCount);
+ int32_t className = AccessibleWrap::AndroidClass(aAccessible);
+
+ nsAutoString hint;
+ nsAutoString text;
+ nsAutoString description;
+ if (state & states::EDITABLE) {
+ // An editable field's name is populated in the hint.
+ hint.Assign(name);
+ text.Assign(textValue);
+ } else {
+ if (role == roles::LINK || role == roles::HEADING) {
+ description.Assign(name);
+ } else {
+ text.Assign(name);
+ }
+ }
+
+ if (!accDesc.IsEmpty()) {
+ if (!hint.IsEmpty()) {
+ // If this is an editable, the description is concatenated with a
+ // whitespace directly after the name.
+ hint.AppendLiteral(" ");
+ }
+ hint.Append(accDesc);
+ }
+
+ if ((state & states::REQUIRED) != 0) {
+ nsAutoString requiredString;
+ if (LocalizeString(u"stateRequired"_ns, requiredString)) {
+ if (!hint.IsEmpty()) {
+ // If the hint is non-empty, concatenate with a comma for a brief pause.
+ hint.AppendLiteral(", ");
+ }
+ hint.Append(requiredString);
+ }
+ }
+
+ RefPtr<AccAttributes> attributes = aAccessible->Attributes();
+
+ nsAutoString geckoRole;
+ nsAutoString roleDescription;
+ if (virtualViewID != kNoID) {
+ AccessibleWrap::GetRoleDescription(role, attributes, geckoRole,
+ roleDescription);
+ }
+
+ int32_t inputType = 0;
+ if (attributes) {
+ nsString inputTypeAttr;
+ attributes->GetAttribute(nsGkAtoms::textInputType, inputTypeAttr);
+ inputType = AccessibleWrap::GetInputType(inputTypeAttr);
+ }
+
+ auto childCount = aAccessible->ChildCount();
+ nsTArray<int32_t> children(childCount);
+ if (!nsAccUtils::MustPrune(aAccessible)) {
+ for (uint32_t i = 0; i < childCount; i++) {
+ auto child = aAccessible->ChildAt(i);
+ children.AppendElement(AccessibleWrap::GetVirtualViewID(child));
+ }
+ }
+
+ const int32_t boundsArray[4] = {bounds.x, bounds.y, bounds.x + bounds.width,
+ bounds.y + bounds.height};
+
+ mSessionAccessibility->PopulateNodeInfo(
+ aNodeInfo, virtualViewID, parentID, jni::IntArray::From(children), flags,
+ className, jni::IntArray::New(boundsArray, 4), jni::StringParam(text),
+ jni::StringParam(description), jni::StringParam(hint),
+ jni::StringParam(geckoRole), jni::StringParam(roleDescription),
+ jni::StringParam(nodeID), inputType);
+
+ if (aAccessible->HasNumericValue()) {
+ double curValue = aAccessible->CurValue();
+ double minValue = aAccessible->MinValue();
+ double maxValue = aAccessible->MaxValue();
+ double step = aAccessible->Step();
+
+ int32_t rangeType = 0; // integer
+ if (maxValue == 1 && minValue == 0) {
+ rangeType = 2; // percent
+ } else if (std::round(step) != step) {
+ rangeType = 1; // float;
+ }
+
+ mSessionAccessibility->PopulateNodeRangeInfo(
+ aNodeInfo, rangeType, static_cast<float>(minValue),
+ static_cast<float>(maxValue), static_cast<float>(curValue));
+ }
+
+ if (attributes) {
+ Maybe<int32_t> rowIndex =
+ attributes->GetAttribute<int32_t>(nsGkAtoms::posinset);
+ if (rowIndex) {
+ mSessionAccessibility->PopulateNodeCollectionItemInfo(aNodeInfo,
+ *rowIndex, 1, 0, 1);
+ }
+
+ Maybe<int32_t> rowCount =
+ attributes->GetAttribute<int32_t>(nsGkAtoms::child_item_count);
+ if (rowCount) {
+ int32_t selectionMode = 0;
+ if (aAccessible->IsSelect()) {
+ selectionMode = (state & states::MULTISELECTABLE) ? 2 : 1;
+ }
+ mSessionAccessibility->PopulateNodeCollectionInfo(
+ aNodeInfo, *rowCount, 1, selectionMode,
+ attributes->HasAttribute(nsGkAtoms::tree));
+ }
+ }
+}
+
+Accessible* SessionAccessibility::GetAccessibleByID(int32_t aID) const {
+ Accessible* accessible = mIDToAccessibleMap.Get(aID);
+ if (accessible && accessible->IsLocal() &&
+ accessible->AsLocal()->IsDefunct()) {
+ MOZ_ASSERT_UNREACHABLE("Registered accessible is defunct!");
+ return nullptr;
+ }
+
+ return accessible;
+}
+
+#ifdef DEBUG
+static bool IsDetachedDoc(Accessible* aAccessible) {
+ if (!aAccessible->IsRemote() || !aAccessible->AsRemote()->IsDoc()) {
+ return false;
+ }
+
+ return !aAccessible->Parent() ||
+ aAccessible->Parent()->FirstChild() != aAccessible;
+}
+#endif
+
+void SessionAccessibility::RegisterAccessible(Accessible* aAccessible) {
+ if (IPCAccessibilityActive()) {
+ // Don't register accessible in content process.
+ return;
+ }
+
+ nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns();
+ RefPtr<SessionAccessibility> sessionAcc = GetInstanceFor(aAccessible);
+ if (!sessionAcc) {
+ return;
+ }
+
+ bool isTopLevel = false;
+ if (aAccessible->IsLocal() && aAccessible->IsDoc()) {
+ DocAccessibleWrap* doc =
+ static_cast<DocAccessibleWrap*>(aAccessible->AsLocal()->AsDoc());
+ isTopLevel = doc->IsTopLevelContentDoc();
+ } else if (aAccessible->IsRemote() && aAccessible->IsDoc()) {
+ isTopLevel = aAccessible->AsRemote()->AsDoc()->IsTopLevel();
+ }
+
+ int32_t virtualViewID = kNoID;
+ if (!isTopLevel) {
+ if (sessionAcc->mIDToAccessibleMap.IsEmpty()) {
+ // We expect there to already be at least one accessible
+ // registered (the top-level one). If it isn't we are
+ // probably in a shutdown process where it was already
+ // unregistered. So we don't register this accessible.
+ return;
+ }
+ // Don't use the special "unset" value (0).
+ while ((virtualViewID = sIDSet.GetID()) == kUnsetID) {
+ }
+ }
+ AccessibleWrap::SetVirtualViewID(aAccessible, virtualViewID);
+
+ Accessible* oldAcc = sessionAcc->mIDToAccessibleMap.Get(virtualViewID);
+ if (oldAcc) {
+ // About to overwrite mapping of registered accessible. This should
+ // only happen when the registered accessible is a detached document.
+ MOZ_ASSERT(IsDetachedDoc(oldAcc),
+ "ID already registered to non-detached document");
+ AccessibleWrap::SetVirtualViewID(oldAcc, kUnsetID);
+ }
+
+ sessionAcc->mIDToAccessibleMap.InsertOrUpdate(virtualViewID, aAccessible);
+}
+
+void SessionAccessibility::UnregisterAccessible(Accessible* aAccessible) {
+ if (IPCAccessibilityActive()) {
+ // Don't unregister accessible in content process.
+ return;
+ }
+
+ nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns();
+ int32_t virtualViewID = AccessibleWrap::GetVirtualViewID(aAccessible);
+ if (virtualViewID == kUnsetID) {
+ return;
+ }
+
+ RefPtr<SessionAccessibility> sessionAcc = GetInstanceFor(aAccessible);
+ if (sessionAcc) {
+ Accessible* registeredAcc =
+ sessionAcc->mIDToAccessibleMap.Get(virtualViewID);
+ if (registeredAcc != aAccessible) {
+ // Attempting to unregister an accessible that is not mapped to
+ // its virtual view ID. This probably means it is a detached document
+ // and a more recent document overwrote its '-1' mapping.
+ // We set its own virtual view ID to `kUnsetID` and return early.
+ MOZ_ASSERT(!registeredAcc || IsDetachedDoc(aAccessible),
+ "Accessible is detached document");
+ AccessibleWrap::SetVirtualViewID(aAccessible, kUnsetID);
+ return;
+ }
+
+ MOZ_ASSERT(registeredAcc, "Unregistering unregistered accessible");
+ MOZ_ASSERT(registeredAcc == aAccessible, "Unregistering wrong accessible");
+ sessionAcc->mIDToAccessibleMap.Remove(virtualViewID);
+ }
+
+ if (virtualViewID > kNoID) {
+ sIDSet.ReleaseID(virtualViewID);
+ }
+
+ AccessibleWrap::SetVirtualViewID(aAccessible, kUnsetID);
+}
+
+void SessionAccessibility::UnregisterAll(PresShell* aPresShell) {
+ if (IPCAccessibilityActive()) {
+ // Don't unregister accessible in content process.
+ return;
+ }
+
+ nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns();
+ RefPtr<SessionAccessibility> sessionAcc = GetInstanceFor(aPresShell);
+ if (sessionAcc) {
+ sessionAcc->mIDToAccessibleMap.Clear();
+ }
+}
diff --git a/accessible/android/SessionAccessibility.h b/accessible/android/SessionAccessibility.h
new file mode 100644
index 0000000000..ffd8eb6ed8
--- /dev/null
+++ b/accessible/android/SessionAccessibility.h
@@ -0,0 +1,154 @@
+/* -*- 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/. */
+
+#ifndef mozilla_a11y_SessionAccessibility_h_
+#define mozilla_a11y_SessionAccessibility_h_
+
+#include "mozilla/java/SessionAccessibilityNatives.h"
+#include "mozilla/widget/GeckoViewSupport.h"
+#include "nsAppShell.h"
+#include "nsThreadUtils.h"
+#include "nsWindow.h"
+#include "AccessibleWrap.h"
+
+namespace mozilla {
+namespace a11y {
+
+class AccessibleWrap;
+class AccAttributes;
+class Accessible;
+class RemoteAccessible;
+class RootAccessibleWrap;
+class BatchData;
+
+class SessionAccessibility final
+ : public java::SessionAccessibility::NativeProvider::Natives<
+ SessionAccessibility> {
+ public:
+ typedef java::SessionAccessibility::NativeProvider::Natives<
+ SessionAccessibility>
+ Base;
+
+ SessionAccessibility(
+ jni::NativeWeakPtr<widget::GeckoViewSupport> aWindow,
+ java::SessionAccessibility::NativeProvider::Param aSessionAccessibility);
+
+ void OnWeakNonIntrusiveDetach(already_AddRefed<Runnable> aDisposer) {
+ SetAttached(false, std::move(aDisposer));
+ }
+
+ const java::SessionAccessibility::NativeProvider::Ref&
+ GetJavaAccessibility() {
+ return mSessionAccessibility;
+ }
+
+ static void Init();
+ static RefPtr<SessionAccessibility> GetInstanceFor(Accessible* aAccessible);
+ static RefPtr<SessionAccessibility> GetInstanceFor(PresShell* aPresShell);
+
+ // Native implementations
+ using Base::AttachNative;
+ using Base::DisposeNative;
+ bool IsCacheEnabled();
+ void GetNodeInfo(int32_t aID, mozilla::jni::Object::Param aNodeInfo);
+ int GetNodeClassName(int32_t aID);
+ void SetText(int32_t aID, jni::String::Param aText);
+ void Click(int32_t aID);
+ void Pivot(int32_t aID, int32_t aGranularity, bool aForward, bool aInclusive);
+ bool CachedPivot(int32_t aID, int32_t aGranularity, bool aForward,
+ bool aInclusive);
+ void ExploreByTouch(int32_t aID, float aX, float aY);
+ void NavigateText(int32_t aID, int32_t aGranularity, int32_t aStartOffset,
+ int32_t aEndOffset, bool aForward, bool aSelect);
+ void SetSelection(int32_t aID, int32_t aStart, int32_t aEnd);
+ void Cut(int32_t aID);
+ void Copy(int32_t aID);
+ void Paste(int32_t aID);
+ void StartNativeAccessibility();
+
+ // Event methods
+ void SendFocusEvent(Accessible* aAccessible);
+ void SendScrollingEvent(Accessible* aAccessible, int32_t aScrollX,
+ int32_t aScrollY, int32_t aMaxScrollX,
+ int32_t aMaxScrollY);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void SendAccessibilityFocusedEvent(Accessible* aAccessible);
+ void SendHoverEnterEvent(Accessible* aAccessible);
+ void SendTextSelectionChangedEvent(Accessible* aAccessible,
+ int32_t aCaretOffset);
+ void SendTextTraversedEvent(Accessible* aAccessible, int32_t aStartOffset,
+ int32_t aEndOffset);
+ void SendTextChangedEvent(Accessible* aAccessible, const nsAString& aStr,
+ int32_t aStart, uint32_t aLen, bool aIsInsert,
+ bool aFromUser);
+ void SendSelectedEvent(Accessible* aAccessible, bool aSelected);
+ void SendClickedEvent(Accessible* aAccessible, uint32_t aFlags);
+ void SendWindowContentChangedEvent();
+ void SendWindowStateChangedEvent(Accessible* aAccessible);
+ void SendAnnouncementEvent(Accessible* aAccessible,
+ const nsAString& aAnnouncement,
+ uint16_t aPriority);
+
+ // Cache methods
+ void ReplaceViewportCache(
+ const nsTArray<Accessible*>& aAccessibles,
+ const nsTArray<BatchData>& aData = nsTArray<BatchData>());
+
+ void ReplaceFocusPathCache(
+ const nsTArray<Accessible*>& aAccessibles,
+ const nsTArray<BatchData>& aData = nsTArray<BatchData>());
+
+ void UpdateCachedBounds(
+ const nsTArray<Accessible*>& aAccessibles,
+ const nsTArray<BatchData>& aData = nsTArray<BatchData>());
+
+ void UpdateAccessibleFocusBoundaries(Accessible* aFirst, Accessible* aLast);
+
+ Accessible* GetAccessibleByID(int32_t aID) const;
+
+ static const int32_t kNoID = -1;
+ static const int32_t kUnsetID = 0;
+
+ static void RegisterAccessible(Accessible* aAccessible);
+ static void UnregisterAccessible(Accessible* aAccessible);
+ static void UnregisterAll(PresShell* aPresShell);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SessionAccessibility)
+
+ private:
+ ~SessionAccessibility() {}
+
+ mozilla::java::GeckoBundle::LocalRef ToBundle(Accessible* aAccessible,
+ bool aSmall = false);
+
+ mozilla::java::GeckoBundle::LocalRef ToBundle(
+ Accessible* aAccessible, const uint64_t aState,
+ const LayoutDeviceIntRect& aBounds, const uint8_t aActionCount,
+ const nsString& aName, const nsString& aTextValue,
+ const nsString& aDOMNodeID, const nsString& aDescription,
+ const double& aCurVal = UnspecifiedNaN<double>(),
+ const double& aMinVal = UnspecifiedNaN<double>(),
+ const double& aMaxVal = UnspecifiedNaN<double>(),
+ const double& aStep = UnspecifiedNaN<double>(),
+ AccAttributes* aAttributes = nullptr);
+
+ void PopulateNodeInfo(Accessible* aAccessible,
+ mozilla::jni::Object::Param aNodeInfo);
+
+ void SetAttached(bool aAttached, already_AddRefed<Runnable> aRunnable);
+
+ jni::NativeWeakPtr<widget::GeckoViewSupport> mWindow; // Parent only
+ java::SessionAccessibility::NativeProvider::GlobalRef mSessionAccessibility;
+
+ /*
+ * This provides a mapping from 32 bit id to accessible objects.
+ */
+ nsTHashMap<nsUint32HashKey, Accessible*> mIDToAccessibleMap;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/android/TraversalRule.cpp b/accessible/android/TraversalRule.cpp
new file mode 100644
index 0000000000..2156c3c0ea
--- /dev/null
+++ b/accessible/android/TraversalRule.cpp
@@ -0,0 +1,296 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "TraversalRule.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/a11y/Accessible.h"
+
+#include "Role.h"
+#include "HTMLListAccessible.h"
+#include "SessionAccessibility.h"
+#include "nsAccUtils.h"
+#include "nsIAccessiblePivot.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+TraversalRule::TraversalRule()
+ : TraversalRule(java::SessionAccessibility::HTML_GRANULARITY_DEFAULT,
+ true) {}
+
+TraversalRule::TraversalRule(int32_t aGranularity, bool aIsLocal)
+ : mGranularity(aGranularity), mIsLocal(aIsLocal) {}
+
+uint16_t TraversalRule::Match(Accessible* aAcc) {
+ MOZ_ASSERT(aAcc);
+
+ if (mIsLocal && aAcc->IsRemote()) {
+ // If we encounter a remote accessible in a local rule, we should
+ // ignore the subtree because we won't encounter anymore local accessibles
+ // in it.
+ return nsIAccessibleTraversalRule::FILTER_IGNORE |
+ nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ } else if (!mIsLocal && aAcc->IsLocal()) {
+ // If we encounter a local accessible in a remote rule we are likely
+ // traversing backwards/upwards, we don't ignore its subtree because it is
+ // likely the outer doc root of the remote tree.
+ return nsIAccessibleTraversalRule::FILTER_IGNORE;
+ }
+
+ uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE;
+
+ if (nsAccUtils::MustPrune(aAcc)) {
+ result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+
+ uint64_t state = aAcc->State();
+
+ if ((state & states::INVISIBLE) != 0) {
+ return result;
+ }
+
+ if (aAcc->Opacity() == 0.0f) {
+ return result | nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+
+ switch (mGranularity) {
+ case java::SessionAccessibility::HTML_GRANULARITY_LINK:
+ result |= LinkMatch(aAcc);
+ break;
+ case java::SessionAccessibility::HTML_GRANULARITY_CONTROL:
+ result |= ControlMatch(aAcc);
+ break;
+ case java::SessionAccessibility::HTML_GRANULARITY_SECTION:
+ result |= SectionMatch(aAcc);
+ break;
+ case java::SessionAccessibility::HTML_GRANULARITY_HEADING:
+ result |= HeadingMatch(aAcc);
+ break;
+ case java::SessionAccessibility::HTML_GRANULARITY_LANDMARK:
+ result |= LandmarkMatch(aAcc);
+ break;
+ default:
+ result |= DefaultMatch(aAcc);
+ break;
+ }
+
+ return result;
+}
+
+bool TraversalRule::IsSingleLineage(Accessible* aAccessible) {
+ Accessible* child = aAccessible;
+ while (child) {
+ switch (child->ChildCount()) {
+ case 0:
+ return true;
+ case 1:
+ child = child->FirstChild();
+ break;
+ case 2:
+ if (IsListItemBullet(child->FirstChild())) {
+ child = child->LastChild();
+ } else {
+ return false;
+ }
+ break;
+ default:
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool TraversalRule::IsListItemBullet(const Accessible* aAccessible) {
+ return aAccessible->Role() == roles::LISTITEM_MARKER;
+}
+
+bool TraversalRule::IsFlatSubtree(const Accessible* aAccessible) {
+ for (auto child = aAccessible->FirstChild(); child;
+ child = child->NextSibling()) {
+ roles::Role role = child->Role();
+ if (role == roles::TEXT_LEAF || role == roles::STATICTEXT) {
+ continue;
+ }
+
+ if (child->ChildCount() > 0 || child->ActionCount() > 0) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool TraversalRule::HasName(const Accessible* aAccessible) {
+ nsAutoString name;
+ aAccessible->Name(name);
+ name.CompressWhitespace();
+ return !name.IsEmpty();
+}
+
+uint16_t TraversalRule::LinkMatch(Accessible* aAccessible) {
+ if (aAccessible->Role() == roles::LINK &&
+ (aAccessible->State() & states::LINKED) != 0) {
+ return nsIAccessibleTraversalRule::FILTER_MATCH |
+ nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+
+ return nsIAccessibleTraversalRule::FILTER_IGNORE;
+}
+
+uint16_t TraversalRule::HeadingMatch(Accessible* aAccessible) {
+ if (aAccessible->Role() == roles::HEADING && aAccessible->ChildCount()) {
+ return nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+
+ return nsIAccessibleTraversalRule::FILTER_IGNORE;
+}
+
+uint16_t TraversalRule::SectionMatch(Accessible* aAccessible) {
+ roles::Role role = aAccessible->Role();
+ if (role == roles::HEADING || role == roles::LANDMARK ||
+ aAccessible->LandmarkRole()) {
+ return nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+
+ return nsIAccessibleTraversalRule::FILTER_IGNORE;
+}
+
+uint16_t TraversalRule::LandmarkMatch(Accessible* aAccessible) {
+ if (aAccessible->LandmarkRole()) {
+ return nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+
+ return nsIAccessibleTraversalRule::FILTER_IGNORE;
+}
+
+uint16_t TraversalRule::ControlMatch(Accessible* aAccessible) {
+ switch (aAccessible->Role()) {
+ case roles::PUSHBUTTON:
+ case roles::SPINBUTTON:
+ case roles::TOGGLE_BUTTON:
+ case roles::BUTTONDROPDOWN:
+ case roles::BUTTONDROPDOWNGRID:
+ case roles::COMBOBOX:
+ case roles::LISTBOX:
+ case roles::ENTRY:
+ case roles::PASSWORD_TEXT:
+ case roles::PAGETAB:
+ case roles::RADIOBUTTON:
+ case roles::RADIO_MENU_ITEM:
+ case roles::SLIDER:
+ case roles::CHECKBUTTON:
+ case roles::CHECK_MENU_ITEM:
+ case roles::SWITCH:
+ case roles::MENUITEM:
+ return nsIAccessibleTraversalRule::FILTER_MATCH |
+ nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ case roles::LINK:
+ return LinkMatch(aAccessible);
+ case roles::EDITCOMBOBOX:
+ if (aAccessible->State() & states::EDITABLE) {
+ // Only match ARIA 1.0 comboboxes; i.e. where the combobox itself is
+ // editable. If it's a 1.1 combobox, the combobox is just a container;
+ // we want to stop on the textbox inside it, not the container.
+ return nsIAccessibleTraversalRule::FILTER_MATCH |
+ nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+ break;
+ default:
+ break;
+ }
+
+ return nsIAccessibleTraversalRule::FILTER_IGNORE;
+}
+
+uint16_t TraversalRule::DefaultMatch(Accessible* aAccessible) {
+ switch (aAccessible->Role()) {
+ case roles::COMBOBOX:
+ // We don't want to ignore the subtree because this is often
+ // where the list box hangs out.
+ return nsIAccessibleTraversalRule::FILTER_MATCH;
+ case roles::EDITCOMBOBOX:
+ if (aAccessible->State() & states::EDITABLE) {
+ // Only match ARIA 1.0 comboboxes; i.e. where the combobox itself is
+ // editable. If it's a 1.1 combobox, the combobox is just a container;
+ // we want to stop on the textbox inside it.
+ return nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ break;
+ case roles::TEXT_LEAF:
+ case roles::GRAPHIC:
+ // Nameless text leaves are boring, skip them.
+ if (HasName(aAccessible)) {
+ return nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ break;
+ case roles::STATICTEXT:
+ // Ignore list bullets
+ if (!IsListItemBullet(aAccessible)) {
+ return nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ break;
+ case roles::HEADER:
+ case roles::HEADING:
+ case roles::COLUMNHEADER:
+ case roles::ROWHEADER:
+ case roles::STATUSBAR:
+ if ((aAccessible->ChildCount() > 0 || HasName(aAccessible)) &&
+ (IsSingleLineage(aAccessible) || IsFlatSubtree(aAccessible))) {
+ return nsIAccessibleTraversalRule::FILTER_MATCH |
+ nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+ break;
+ case roles::GRID_CELL:
+ if (IsSingleLineage(aAccessible) || IsFlatSubtree(aAccessible)) {
+ return nsIAccessibleTraversalRule::FILTER_MATCH |
+ nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+ break;
+ case roles::LISTITEM:
+ if (IsFlatSubtree(aAccessible) || IsSingleLineage(aAccessible)) {
+ return nsIAccessibleTraversalRule::FILTER_MATCH |
+ nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+ break;
+ case roles::LABEL:
+ if (IsFlatSubtree(aAccessible)) {
+ // Match if this is a label with text but no nested controls.
+ return nsIAccessibleTraversalRule::FILTER_MATCH |
+ nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+ break;
+ case roles::MENUITEM:
+ case roles::LINK:
+ case roles::PAGETAB:
+ case roles::PUSHBUTTON:
+ case roles::CHECKBUTTON:
+ case roles::RADIOBUTTON:
+ case roles::PROGRESSBAR:
+ case roles::BUTTONDROPDOWN:
+ case roles::BUTTONMENU:
+ case roles::CHECK_MENU_ITEM:
+ case roles::PASSWORD_TEXT:
+ case roles::RADIO_MENU_ITEM:
+ case roles::TOGGLE_BUTTON:
+ case roles::ENTRY:
+ case roles::KEY:
+ case roles::SLIDER:
+ case roles::SPINBUTTON:
+ case roles::OPTION:
+ case roles::SWITCH:
+ case roles::MATHML_MATH:
+ // Ignore the subtree, if there is one. So that we don't land on
+ // the same content that was already presented by its parent.
+ return nsIAccessibleTraversalRule::FILTER_MATCH |
+ nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ default:
+ break;
+ }
+
+ return nsIAccessibleTraversalRule::FILTER_IGNORE;
+}
diff --git a/accessible/android/TraversalRule.h b/accessible/android/TraversalRule.h
new file mode 100644
index 0000000000..27b2b2f5fb
--- /dev/null
+++ b/accessible/android/TraversalRule.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef _TraversalRule_H_
+#define _TraversalRule_H_
+
+#include "Pivot.h"
+
+namespace mozilla {
+namespace a11y {
+
+class Accessible;
+
+/**
+ * Class represents a simple traversal rule.
+ */
+class TraversalRule : public PivotRule {
+ public:
+ TraversalRule();
+ explicit TraversalRule(int32_t aGranularity, bool aIsLocal);
+
+ ~TraversalRule() = default;
+
+ virtual uint16_t Match(Accessible* aAcc) override;
+
+ private:
+ bool IsSingleLineage(Accessible* aAccessible);
+
+ bool IsFlatSubtree(const Accessible* aAccessible);
+
+ bool IsListItemBullet(const Accessible* aAccessible);
+
+ bool HasName(const Accessible* aAccessible);
+
+ uint16_t DefaultMatch(Accessible* aAccessible);
+
+ uint16_t LinkMatch(Accessible* aAccessible);
+
+ uint16_t HeadingMatch(Accessible* aAccessible);
+
+ uint16_t ControlMatch(Accessible* aAccessible);
+
+ uint16_t SectionMatch(Accessible* aAccessible);
+
+ uint16_t LandmarkMatch(Accessible* aAccessible);
+
+ int32_t mGranularity;
+
+ bool mIsLocal;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/android/moz.build b/accessible/android/moz.build
new file mode 100644
index 0000000000..2370d9a6f5
--- /dev/null
+++ b/accessible/android/moz.build
@@ -0,0 +1,38 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXPORTS.mozilla.a11y += [
+ "AccessibleWrap.h",
+ "HyperTextAccessibleWrap.h",
+ "SessionAccessibility.h",
+ "TraversalRule.h",
+]
+
+SOURCES += [
+ "AccessibleWrap.cpp",
+ "DocAccessibleWrap.cpp",
+ "Platform.cpp",
+ "RootAccessibleWrap.cpp",
+ "SessionAccessibility.cpp",
+ "TraversalRule.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/base",
+ "/accessible/generic",
+ "/accessible/html",
+ "/accessible/ipc",
+ "/accessible/ipc/other",
+ "/accessible/xpcom",
+ "/accessible/xul",
+ "/dom/base",
+ "/widget",
+ "/widget/android",
+]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")