summaryrefslogtreecommitdiffstats
path: root/accessible/android
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /accessible/android
parentInitial commit. (diff)
downloadfirefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.tar.xz
firefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--accessible/android/AccessibleWrap.cpp473
-rw-r--r--accessible/android/AccessibleWrap.h68
-rw-r--r--accessible/android/ApplicationAccessibleWrap.h20
-rw-r--r--accessible/android/DocAccessibleWrap.cpp78
-rw-r--r--accessible/android/DocAccessibleWrap.h33
-rw-r--r--accessible/android/Platform.cpp233
-rw-r--r--accessible/android/RootAccessibleWrap.h22
-rw-r--r--accessible/android/SessionAccessibility.cpp941
-rw-r--r--accessible/android/SessionAccessibility.h145
-rw-r--r--accessible/android/TraversalRule.cpp288
-rw-r--r--accessible/android/TraversalRule.h58
-rw-r--r--accessible/android/moz.build35
12 files changed, 2394 insertions, 0 deletions
diff --git a/accessible/android/AccessibleWrap.cpp b/accessible/android/AccessibleWrap.cpp
new file mode 100644
index 0000000000..4bccc2dddd
--- /dev/null
+++ b/accessible/android/AccessibleWrap.cpp
@@ -0,0 +1,473 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AccessibleWrap.h"
+
+#include "JavaBuiltins.h"
+#include "LocalAccessible-inl.h"
+#include "HyperTextAccessible-inl.h"
+#include "AccAttributes.h"
+#include "AccEvent.h"
+#include "AndroidInputType.h"
+#include "DocAccessibleWrap.h"
+#include "SessionAccessibility.h"
+#include "TextLeafAccessible.h"
+#include "TraversalRule.h"
+#include "Pivot.h"
+#include "Platform.h"
+#include "nsAccessibilityService.h"
+#include "nsEventShell.h"
+#include "nsIAccessibleAnnouncementEvent.h"
+#include "nsIAccessiblePivot.h"
+#include "nsAccUtils.h"
+#include "nsTextEquivUtils.h"
+#include "nsWhitespaceTokenizer.h"
+#include "RootAccessible.h"
+#include "TextLeafRange.h"
+
+#include "mozilla/a11y/PDocAccessibleChild.h"
+#include "mozilla/jni/GeckoBundleUtils.h"
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "mozilla/Maybe.h"
+
+// icu TRUE conflicting with java::sdk::Boolean::TRUE()
+// https://searchfox.org/mozilla-central/rev/ce02064d8afc8673cef83c92896ee873bd35e7ae/intl/icu/source/common/unicode/umachine.h#265
+// https://searchfox.org/mozilla-central/source/__GENERATED__/widget/android/bindings/JavaBuiltins.h#78
+#ifdef TRUE
+# undef TRUE
+#endif
+
+using namespace mozilla::a11y;
+using mozilla::Maybe;
+
+//-----------------------------------------------------
+// construction
+//-----------------------------------------------------
+AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc)
+ : LocalAccessible(aContent, aDoc), mID(SessionAccessibility::kUnsetID) {
+ if (!IPCAccessibilityActive()) {
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ SessionAccessibility::RegisterAccessible(this);
+ }
+}
+
+//-----------------------------------------------------
+// destruction
+//-----------------------------------------------------
+AccessibleWrap::~AccessibleWrap() {}
+
+nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
+ auto accessible = static_cast<AccessibleWrap*>(aEvent->GetAccessible());
+ NS_ENSURE_TRUE(accessible, NS_ERROR_FAILURE);
+
+ nsresult rv = LocalAccessible::HandleAccEvent(aEvent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ accessible->HandleLiveRegionEvent(aEvent);
+
+ return NS_OK;
+}
+
+void AccessibleWrap::Shutdown() {
+ if (!IPCAccessibilityActive()) {
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ SessionAccessibility::UnregisterAccessible(this);
+ }
+ LocalAccessible::Shutdown();
+}
+
+bool AccessibleWrap::DoAction(uint8_t aIndex) const {
+ if (ActionCount()) {
+ return LocalAccessible::DoAction(aIndex);
+ }
+
+ if (mContent) {
+ // We still simulate a click on an accessible even if there is no
+ // known actions. For the sake of bad markup.
+ DoCommand();
+ return true;
+ }
+
+ return false;
+}
+
+Accessible* AccessibleWrap::DoPivot(Accessible* aAccessible,
+ int32_t aGranularity, bool aForward,
+ bool aInclusive) {
+ Accessible* pivotRoot = nullptr;
+ if (aAccessible->IsRemote()) {
+ // If this is a remote accessible provide the top level
+ // remote doc as the pivot root for thread safety reasons.
+ DocAccessibleParent* doc = aAccessible->AsRemote()->Document();
+ while (doc && !doc->IsTopLevel()) {
+ doc = doc->ParentDoc();
+ }
+ MOZ_ASSERT(doc, "Failed to get top level DocAccessibleParent");
+ pivotRoot = doc;
+ }
+ a11y::Pivot pivot(pivotRoot);
+ // Depending on the start accessible, the pivot rule will either traverse
+ // local or remote accessibles exclusively.
+ TraversalRule rule(aGranularity, aAccessible->IsLocal());
+ Accessible* result = aForward ? pivot.Next(aAccessible, rule, aInclusive)
+ : pivot.Prev(aAccessible, rule, aInclusive);
+
+ if (result && (result != aAccessible || aInclusive)) {
+ return result;
+ }
+
+ return nullptr;
+}
+
+Accessible* AccessibleWrap::ExploreByTouch(Accessible* aAccessible, float aX,
+ float aY) {
+ Accessible* root;
+ if (LocalAccessible* local = aAccessible->AsLocal()) {
+ root = local->RootAccessible();
+ } else {
+ // If this is a RemoteAccessible, 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");
+ root = doc;
+ }
+ a11y::Pivot pivot(root);
+ TraversalRule rule(java::SessionAccessibility::HTML_GRANULARITY_DEFAULT,
+ aAccessible->IsLocal());
+ Accessible* result = pivot.AtPoint(aX, aY, rule);
+ if (result == aAccessible) {
+ return nullptr;
+ }
+ return result;
+}
+
+static TextLeafPoint ToTextLeafPoint(Accessible* aAccessible, int32_t aOffset) {
+ if (HyperTextAccessibleBase* ht = aAccessible->AsHyperTextBase()) {
+ return ht->ToTextLeafPoint(aOffset);
+ }
+
+ return TextLeafPoint(aAccessible, aOffset);
+}
+
+Maybe<std::pair<int32_t, int32_t>> AccessibleWrap::NavigateText(
+ Accessible* aAccessible, int32_t aGranularity, int32_t aStartOffset,
+ int32_t aEndOffset, bool aForward, bool aSelect) {
+ int32_t startOffset = aStartOffset;
+ int32_t endOffset = aEndOffset;
+ if (startOffset == -1) {
+ MOZ_ASSERT(endOffset == -1,
+ "When start offset is unset, end offset should be too");
+ startOffset = aForward ? 0 : nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT;
+ endOffset = aForward ? 0 : nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT;
+ }
+
+ // If the accessible is an editable, set the virtual cursor position
+ // to its caret offset. Otherwise use the document's virtual cursor
+ // position as a starting offset.
+ if (aAccessible->State() & states::EDITABLE) {
+ startOffset = endOffset = aAccessible->AsHyperTextBase()->CaretOffset();
+ }
+
+ TextLeafRange currentRange =
+ TextLeafRange(ToTextLeafPoint(aAccessible, startOffset),
+ ToTextLeafPoint(aAccessible, endOffset));
+ uint16_t startBoundaryType = nsIAccessibleText::BOUNDARY_LINE_START;
+ uint16_t endBoundaryType = nsIAccessibleText::BOUNDARY_LINE_END;
+ switch (aGranularity) {
+ case 1: // MOVEMENT_GRANULARITY_CHARACTER
+ startBoundaryType = nsIAccessibleText::BOUNDARY_CHAR;
+ endBoundaryType = nsIAccessibleText::BOUNDARY_CHAR;
+ break;
+ case 2: // MOVEMENT_GRANULARITY_WORD
+ startBoundaryType = nsIAccessibleText::BOUNDARY_WORD_START;
+ endBoundaryType = nsIAccessibleText::BOUNDARY_WORD_END;
+ break;
+ default:
+ break;
+ }
+
+ TextLeafRange resultRange;
+
+ if (aForward) {
+ resultRange.SetEnd(
+ currentRange.End().FindBoundary(endBoundaryType, eDirNext));
+ resultRange.SetStart(
+ resultRange.End().FindBoundary(startBoundaryType, eDirPrevious));
+ } else {
+ resultRange.SetStart(
+ currentRange.Start().FindBoundary(startBoundaryType, eDirPrevious));
+ resultRange.SetEnd(
+ resultRange.Start().FindBoundary(endBoundaryType, eDirNext));
+ }
+
+ if (!resultRange.Crop(aAccessible)) {
+ // If the new range does not intersect at all with the given
+ // accessible/container this navigation has failed or reached an edge.
+ return Nothing();
+ }
+
+ if (resultRange == currentRange || resultRange.Start() == resultRange.End()) {
+ // If the result range equals the current range, or if the result range is
+ // collapsed, we failed or reached an edge.
+ return Nothing();
+ }
+
+ if (HyperTextAccessibleBase* ht = aAccessible->AsHyperTextBase()) {
+ DebugOnly<bool> ok = false;
+ std::tie(ok, startOffset) = ht->TransformOffset(
+ resultRange.Start().mAcc, resultRange.Start().mOffset, false);
+ MOZ_ASSERT(ok, "Accessible of range start should be in container.");
+
+ std::tie(ok, endOffset) = ht->TransformOffset(
+ resultRange.End().mAcc, resultRange.End().mOffset, false);
+ MOZ_ASSERT(ok, "Accessible range end should be in container.");
+ } else {
+ startOffset = resultRange.Start().mOffset;
+ endOffset = resultRange.End().mOffset;
+ }
+
+ return Some(std::make_pair(startOffset, endOffset));
+}
+
+uint32_t AccessibleWrap::GetFlags(role aRole, uint64_t aState,
+ uint8_t aActionCount) {
+ uint32_t flags = 0;
+ if (aState & states::CHECKABLE) {
+ flags |= java::SessionAccessibility::FLAG_CHECKABLE;
+ }
+
+ if (aState & states::CHECKED) {
+ flags |= java::SessionAccessibility::FLAG_CHECKED;
+ }
+
+ if (aState & states::INVALID) {
+ flags |= java::SessionAccessibility::FLAG_CONTENT_INVALID;
+ }
+
+ if (aState & states::EDITABLE) {
+ flags |= java::SessionAccessibility::FLAG_EDITABLE;
+ }
+
+ if (aActionCount && aRole != roles::TEXT_LEAF) {
+ flags |= java::SessionAccessibility::FLAG_CLICKABLE;
+ }
+
+ if (aState & states::ENABLED) {
+ flags |= java::SessionAccessibility::FLAG_ENABLED;
+ }
+
+ if (aState & states::FOCUSABLE) {
+ flags |= java::SessionAccessibility::FLAG_FOCUSABLE;
+ }
+
+ if (aState & states::FOCUSED) {
+ flags |= java::SessionAccessibility::FLAG_FOCUSED;
+ }
+
+ if (aState & states::MULTI_LINE) {
+ flags |= java::SessionAccessibility::FLAG_MULTI_LINE;
+ }
+
+ if (aState & states::SELECTABLE) {
+ flags |= java::SessionAccessibility::FLAG_SELECTABLE;
+ }
+
+ if (aState & states::SELECTED) {
+ flags |= java::SessionAccessibility::FLAG_SELECTED;
+ }
+
+ if (aState & states::EXPANDABLE) {
+ flags |= java::SessionAccessibility::FLAG_EXPANDABLE;
+ }
+
+ if (aState & states::EXPANDED) {
+ flags |= java::SessionAccessibility::FLAG_EXPANDED;
+ }
+
+ if ((aState & (states::INVISIBLE | states::OFFSCREEN)) == 0) {
+ flags |= java::SessionAccessibility::FLAG_VISIBLE_TO_USER;
+ }
+
+ if (aRole == roles::PASSWORD_TEXT) {
+ flags |= java::SessionAccessibility::FLAG_PASSWORD;
+ }
+
+ return flags;
+}
+
+void AccessibleWrap::GetRoleDescription(role aRole, AccAttributes* aAttributes,
+ nsAString& aGeckoRole,
+ nsAString& aRoleDescription) {
+ if (aRole == roles::HEADING && aAttributes) {
+ // The heading level is an attribute, so we need that.
+ nsAutoString headingLevel;
+ if (aAttributes->GetAttribute(nsGkAtoms::level, headingLevel)) {
+ nsAutoString token(u"heading-");
+ token.Append(headingLevel);
+ if (LocalizeString(token, aRoleDescription)) {
+ return;
+ }
+ }
+ }
+
+ if ((aRole == roles::LANDMARK || aRole == roles::REGION) && aAttributes) {
+ nsAutoString xmlRoles;
+ if (aAttributes->GetAttribute(nsGkAtoms::xmlroles, xmlRoles)) {
+ nsWhitespaceTokenizer tokenizer(xmlRoles);
+ while (tokenizer.hasMoreTokens()) {
+ if (LocalizeString(tokenizer.nextToken(), aRoleDescription)) {
+ return;
+ }
+ }
+ }
+ }
+
+ GetAccService()->GetStringRole(aRole, aGeckoRole);
+ LocalizeString(aGeckoRole, aRoleDescription);
+}
+
+int32_t AccessibleWrap::AndroidClass(Accessible* aAccessible) {
+ return GetVirtualViewID(aAccessible) == SessionAccessibility::kNoID
+ ? java::SessionAccessibility::CLASSNAME_WEBVIEW
+ : GetAndroidClass(aAccessible->Role());
+}
+
+int32_t AccessibleWrap::GetVirtualViewID(Accessible* aAccessible) {
+ if (aAccessible->IsLocal()) {
+ return static_cast<AccessibleWrap*>(aAccessible)->mID;
+ }
+
+ return static_cast<int32_t>(aAccessible->AsRemote()->GetWrapper());
+}
+
+void AccessibleWrap::SetVirtualViewID(Accessible* aAccessible,
+ int32_t aVirtualViewID) {
+ if (aAccessible->IsLocal()) {
+ static_cast<AccessibleWrap*>(aAccessible)->mID = aVirtualViewID;
+ } else {
+ aAccessible->AsRemote()->SetWrapper(static_cast<uintptr_t>(aVirtualViewID));
+ }
+}
+
+int32_t AccessibleWrap::GetAndroidClass(role aRole) {
+#define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
+ msaaRole, ia2Role, androidClass, nameRule) \
+ case roles::geckoRole: \
+ return androidClass;
+
+ switch (aRole) {
+#include "RoleMap.h"
+ default:
+ return java::SessionAccessibility::CLASSNAME_VIEW;
+ }
+
+#undef ROLE
+}
+
+int32_t AccessibleWrap::GetInputType(const nsString& aInputTypeAttr) {
+ if (aInputTypeAttr.EqualsIgnoreCase("email")) {
+ return java::sdk::InputType::TYPE_CLASS_TEXT |
+ java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
+ }
+
+ if (aInputTypeAttr.EqualsIgnoreCase("number")) {
+ return java::sdk::InputType::TYPE_CLASS_NUMBER;
+ }
+
+ if (aInputTypeAttr.EqualsIgnoreCase("password")) {
+ return java::sdk::InputType::TYPE_CLASS_TEXT |
+ java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_PASSWORD;
+ }
+
+ if (aInputTypeAttr.EqualsIgnoreCase("tel")) {
+ return java::sdk::InputType::TYPE_CLASS_PHONE;
+ }
+
+ if (aInputTypeAttr.EqualsIgnoreCase("text")) {
+ return java::sdk::InputType::TYPE_CLASS_TEXT |
+ java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
+ }
+
+ if (aInputTypeAttr.EqualsIgnoreCase("url")) {
+ return java::sdk::InputType::TYPE_CLASS_TEXT |
+ java::sdk::InputType::TYPE_TEXT_VARIATION_URI;
+ }
+
+ return 0;
+}
+
+void AccessibleWrap::GetTextEquiv(nsString& aText) {
+ if (nsTextEquivUtils::HasNameRule(this, eNameFromSubtreeIfReqRule)) {
+ // This is an accessible that normally doesn't get its name from its
+ // subtree, so we collect the text equivalent explicitly.
+ nsTextEquivUtils::GetTextEquivFromSubtree(this, aText);
+ } else {
+ Name(aText);
+ }
+}
+
+bool AccessibleWrap::HandleLiveRegionEvent(AccEvent* aEvent) {
+ auto eventType = aEvent->GetEventType();
+ if (eventType != nsIAccessibleEvent::EVENT_TEXT_INSERTED &&
+ eventType != nsIAccessibleEvent::EVENT_NAME_CHANGE) {
+ // XXX: Right now only announce text inserted events. aria-relevant=removals
+ // is potentially on the chopping block[1]. We also don't support editable
+ // text because we currently can't descern the source of the change[2].
+ // 1. https://github.com/w3c/aria/issues/712
+ // 2. https://bugzilla.mozilla.org/show_bug.cgi?id=1531189
+ return false;
+ }
+
+ if (aEvent->IsFromUserInput()) {
+ return false;
+ }
+
+ RefPtr<AccAttributes> attributes = new AccAttributes();
+ nsAccUtils::SetLiveContainerAttributes(attributes, this);
+ nsString live;
+ if (!attributes->GetAttribute(nsGkAtoms::containerLive, live)) {
+ return false;
+ }
+
+ uint16_t priority = live.EqualsIgnoreCase("assertive")
+ ? nsIAccessibleAnnouncementEvent::ASSERTIVE
+ : nsIAccessibleAnnouncementEvent::POLITE;
+
+ Maybe<bool> atomic =
+ attributes->GetAttribute<bool>(nsGkAtoms::containerAtomic);
+ LocalAccessible* announcementTarget = this;
+ nsAutoString announcement;
+ if (atomic && *atomic) {
+ LocalAccessible* atomicAncestor = nullptr;
+ for (LocalAccessible* parent = announcementTarget; parent;
+ parent = parent->LocalParent()) {
+ dom::Element* element = parent->Elm();
+ if (element &&
+ nsAccUtils::ARIAAttrValueIs(element, nsGkAtoms::aria_atomic,
+ nsGkAtoms::_true, eCaseMatters)) {
+ atomicAncestor = parent;
+ break;
+ }
+ }
+
+ if (atomicAncestor) {
+ announcementTarget = atomicAncestor;
+ static_cast<AccessibleWrap*>(atomicAncestor)->GetTextEquiv(announcement);
+ }
+ } else {
+ GetTextEquiv(announcement);
+ }
+
+ announcement.CompressWhitespace();
+ if (announcement.IsEmpty()) {
+ return false;
+ }
+
+ announcementTarget->Announce(announcement, priority);
+ return true;
+}
diff --git a/accessible/android/AccessibleWrap.h b/accessible/android/AccessibleWrap.h
new file mode 100644
index 0000000000..249c5dc14e
--- /dev/null
+++ b/accessible/android/AccessibleWrap.h
@@ -0,0 +1,68 @@
+/* -*- 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;
+
+ static Accessible* ExploreByTouch(Accessible* aAccessible, 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);
+
+ static Maybe<std::pair<int32_t, int32_t>> NavigateText(
+ Accessible* aAccessible, int32_t aGranularity, int32_t aStartOffset,
+ int32_t aEndOffset, bool aForward, bool aSelect);
+
+ protected:
+ int32_t mID;
+
+ private:
+ void GetTextEquiv(nsString& aText);
+
+ bool HandleLiveRegionEvent(AccEvent* aEvent);
+};
+
+} // 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..0e46b56649
--- /dev/null
+++ b/accessible/android/DocAccessibleWrap.cpp
@@ -0,0 +1,78 @@
+/* -*- 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"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+#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();
+}
+
+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());
+}
+
+#undef UNIQUE_ID
diff --git a/accessible/android/DocAccessibleWrap.h b/accessible/android/DocAccessibleWrap.h
new file mode 100644
index 0000000000..c4408cdf41
--- /dev/null
+++ b/accessible/android/DocAccessibleWrap.h
@@ -0,0 +1,33 @@
+/* -*- 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;
+
+ DocAccessibleWrap* GetTopLevelContentDoc(AccessibleWrap* aAccessible);
+
+ bool IsTopLevelContentDoc();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/android/Platform.cpp b/accessible/android/Platform.cpp
new file mode 100644
index 0000000000..02f808f8bc
--- /dev/null
+++ b/accessible/android/Platform.cpp
@@ -0,0 +1,233 @@
+/* -*- 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 "nsIAccessibleEvent.h"
+#include "nsIAccessiblePivot.h"
+#include "nsIStringBundle.h"
+#include "TextLeafRange.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, ariaRole, 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::PlatformEvent(Accessible* aTarget, uint32_t aEventType) {
+ RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aTarget);
+ if (!sessionAcc) {
+ return;
+ }
+
+ switch (aEventType) {
+ case nsIAccessibleEvent::EVENT_REORDER:
+ sessionAcc->SendWindowContentChangedEvent();
+ break;
+ case nsIAccessibleEvent::EVENT_SCROLLING_START:
+ if (Accessible* result = AccessibleWrap::DoPivot(
+ aTarget, java::SessionAccessibility::HTML_GRANULARITY_DEFAULT,
+ true, true)) {
+ sessionAcc->SendAccessibilityFocusedEvent(result, false);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+void a11y::PlatformStateChangeEvent(Accessible* 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::PlatformFocusEvent(Accessible* aTarget,
+ const LayoutDeviceIntRect& aCaretRect) {
+ if (RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aTarget)) {
+ sessionAcc->SendFocusEvent(aTarget);
+ }
+}
+
+void a11y::PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset,
+ bool aIsSelectionCollapsed,
+ int32_t aGranularity,
+ const LayoutDeviceIntRect& aCaretRect,
+ bool aFromUser) {
+ RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aTarget);
+ if (!sessionAcc) {
+ return;
+ }
+
+ if (!aTarget->IsDoc() && !aFromUser && !aIsSelectionCollapsed) {
+ // Pivot to the caret's position if it has an expanded selection.
+ // This is used mostly for find in page.
+ Accessible* leaf = TextLeafPoint::GetCaret(aTarget).ActualizeCaret().mAcc;
+ MOZ_ASSERT(leaf);
+ if (leaf) {
+ if (Accessible* result = AccessibleWrap::DoPivot(
+ leaf, java::SessionAccessibility::HTML_GRANULARITY_DEFAULT, true,
+ true)) {
+ sessionAcc->SendAccessibilityFocusedEvent(result, false);
+ }
+ }
+ }
+
+ sessionAcc->SendTextSelectionChangedEvent(aTarget, aOffset);
+}
+
+void a11y::PlatformTextChangeEvent(Accessible* 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::PlatformShowHideEvent(Accessible* aTarget, Accessible* aParent,
+ bool aInsert, bool aFromUser) {
+ // We rely on the window content changed events to be dispatched
+ // after the viewport cache is refreshed.
+}
+
+void a11y::PlatformSelectionEvent(Accessible*, Accessible*, uint32_t) {}
+
+void a11y::PlatformScrollingEvent(Accessible* 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::PlatformAnnouncementEvent(Accessible* aTarget,
+ const nsAString& aAnnouncement,
+ uint16_t aPriority) {
+ RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aTarget);
+
+ if (sessionAcc) {
+ sessionAcc->SendAnnouncementEvent(aTarget, aAnnouncement, aPriority);
+ }
+}
+
+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.h b/accessible/android/RootAccessibleWrap.h
new file mode 100644
index 0000000000..4198239bad
--- /dev/null
+++ b/accessible/android/RootAccessibleWrap.h
@@ -0,0 +1,22 @@
+/* -*- 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 {
+
+using RootAccessibleWrap = RootAccessible;
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/android/SessionAccessibility.cpp b/accessible/android/SessionAccessibility.cpp
new file mode 100644
index 0000000000..aab9b7da69
--- /dev/null
+++ b/accessible/android/SessionAccessibility.cpp
@@ -0,0 +1,941 @@
+/* -*- 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 "DocAccessibleWrap.h"
+#include "JavaBuiltins.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/DocManager.h"
+#include "mozilla/a11y/HyperTextAccessibleBase.h"
+#include "mozilla/jni/GeckoBundleUtils.h"
+#include "mozilla/jni/NativesInlines.h"
+#include "mozilla/widget/GeckoViewSupport.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/dom/MouseEventBinding.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
+
+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();
+}
+
+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()) {
+ 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());
+ 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) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ acc->DoAction(0);
+ }
+}
+
+bool SessionAccessibility::Pivot(int32_t aID, int32_t aGranularity,
+ bool aForward, bool aInclusive) {
+ 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] {
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ if (Accessible* _acc = GetAccessibleByID(aID)) {
+ MOZ_ASSERT(_acc->IsLocal());
+ if (Accessible* result = AccessibleWrap::DoPivot(
+ _acc, aGranularity, aForward, aInclusive)) {
+ SendAccessibilityFocusedEvent(result, true);
+ }
+ }
+ });
+ 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, true);
+ }
+ });
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void SessionAccessibility::ExploreByTouch(int32_t aID, float aX, float aY) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ RefPtr<SessionAccessibility> self(this);
+ if (Accessible* origin = GetAccessibleByID(aID)) {
+ if (origin->IsLocal()) {
+ nsAppShell::PostEvent([this, self, aID, aX, aY] {
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ if (Accessible* origin = GetAccessibleByID(aID)) {
+ if (Accessible* result =
+ AccessibleWrap::ExploreByTouch(origin, aX, aY)) {
+ SendHoverEnterEvent(result);
+ }
+ }
+ });
+ } else {
+ if (Accessible* result = AccessibleWrap::ExploreByTouch(origin, aX, aY)) {
+ int32_t resultID = AccessibleWrap::GetVirtualViewID(result);
+ nsAppShell::PostEvent([this, self, resultID] {
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ if (Accessible* result = GetAccessibleByID(resultID)) {
+ SendHoverEnterEvent(result);
+ }
+ });
+ }
+ }
+ }
+}
+
+static void GetSelectionOrCaret(HyperTextAccessibleBase* aHyperTextAcc,
+ int32_t* aStartOffset, int32_t* aEndOffset) {
+ if (!aHyperTextAcc->SelectionBoundsAt(0, aStartOffset, aEndOffset)) {
+ *aStartOffset = *aEndOffset = aHyperTextAcc->CaretOffset();
+ }
+}
+
+static void AdjustCaretToTextNavigation(Accessible* aAccessible,
+ int32_t aStartOffset,
+ int32_t aEndOffset, bool aForward,
+ bool aSelect) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!(aAccessible->State() & states::EDITABLE)) {
+ return;
+ }
+
+ HyperTextAccessibleBase* editable = aAccessible->AsHyperTextBase();
+ MOZ_ASSERT(editable);
+ if (!editable) {
+ return;
+ }
+
+ int32_t newOffset = aForward ? aEndOffset : aStartOffset;
+ if (aSelect) {
+ int32_t anchor = editable->CaretOffset();
+ if (editable->SelectionCount()) {
+ int32_t startSel, endSel;
+ GetSelectionOrCaret(editable, &startSel, &endSel);
+ anchor = startSel == anchor ? endSel : startSel;
+ }
+ editable->SetSelectionBoundsAt(0, anchor, newOffset);
+ } else {
+ editable->SetCaretOffset(newOffset);
+ }
+}
+
+bool SessionAccessibility::NavigateText(int32_t aID, int32_t aGranularity,
+ int32_t aStartOffset,
+ int32_t aEndOffset, bool aForward,
+ bool aSelect) {
+ 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, aStartOffset,
+ aEndOffset, aForward, aSelect] {
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ if (Accessible* _acc = GetAccessibleByID(aID)) {
+ auto result = AccessibleWrap::NavigateText(
+ _acc, aGranularity, aStartOffset, aEndOffset, aForward, aSelect);
+
+ if (result) {
+ SendTextTraversedEvent(_acc, result->first, result->second);
+ AdjustCaretToTextNavigation(_acc, result->first, result->second,
+ aForward, aSelect);
+ }
+ }
+ });
+ return true;
+ } else {
+ auto result = AccessibleWrap::NavigateText(
+ acc, aGranularity, aStartOffset, aEndOffset, aForward, aSelect);
+ if (result) {
+ nsAppShell::PostEvent([this, self, aID, result, aForward, aSelect] {
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ if (Accessible* _acc = GetAccessibleByID(aID)) {
+ SendTextTraversedEvent(_acc, result->first, result->second);
+ AdjustCaretToTextNavigation(_acc, result->first, result->second,
+ aForward, aSelect);
+ }
+ });
+ }
+
+ return !!result;
+ }
+ }
+
+ return false;
+}
+
+void SessionAccessibility::SetSelection(int32_t aID, int32_t aStart,
+ int32_t aEnd) {
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ if (auto* textAcc = acc->AsHyperTextBase()) {
+ if (aStart == aEnd) {
+ textAcc->SetCaretOffset(aStart);
+ } else {
+ textAcc->SetSelectionBoundsAt(0, aStart, aEnd);
+ }
+ }
+ }
+}
+
+void SessionAccessibility::Cut(int32_t aID) {
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ if (auto* textAcc = acc->AsHyperTextBase()) {
+ int32_t startSel, endSel;
+ if (textAcc->SelectionBoundsAt(0, &startSel, &endSel)) {
+ textAcc->CutText(startSel, endSel);
+ }
+ }
+ }
+}
+
+void SessionAccessibility::Copy(int32_t aID) {
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ if (auto* textAcc = acc->AsHyperTextBase()) {
+ int32_t startSel, endSel;
+ GetSelectionOrCaret(textAcc, &startSel, &endSel);
+ textAcc->CopyText(startSel, endSel);
+ }
+ }
+}
+
+void SessionAccessibility::Paste(int32_t aID) {
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ if (auto* textAcc = acc->AsHyperTextBase()) {
+ int32_t startSel, endSel;
+ GetSelectionOrCaret(textAcc, &startSel, &endSel);
+ if (startSel != endSel) {
+ textAcc->DeleteText(startSel, endSel);
+ }
+ textAcc->PasteText(startSel);
+ }
+ }
+}
+
+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, bool aScrollIntoView) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSessionAccessibility->SendEvent(
+ java::sdk::AccessibilityEvent::TYPE_VIEW_ACCESSIBILITY_FOCUSED,
+ AccessibleWrap::GetVirtualViewID(aAccessible),
+ AccessibleWrap::AndroidClass(aAccessible), nullptr);
+ if (aScrollIntoView) {
+ 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);
+
+ 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 =
+ aAccessible->AsHyperTextBase()->SelectionBoundsAt(0, &startSel, &endSel);
+
+ if (hasSelection) {
+ fromIndex = startSel == aCaretOffset ? endSel : startSel;
+ }
+
+ nsAutoString text;
+ if (aAccessible->IsHyperText()) {
+ aAccessible->AsHyperTextBase()->TextSubstring(0, -1, text);
+ } else if (aAccessible->IsText()) {
+ aAccessible->AppendTextTo(text, 0, -1);
+ }
+
+ GECKOBUNDLE_START(eventInfo);
+ GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(text));
+ 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()) {
+ 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, "fromIndex", java::sdk::Integer::ValueOf(aStart));
+ 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()) {
+ 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::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, 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 {
+ return mIDToAccessibleMap.Get(aID);
+}
+
+#ifdef DEBUG
+static bool IsDetachedDoc(Accessible* aAccessible) {
+ if (!aAccessible->IsRemote() || !aAccessible->AsRemote()->IsDoc()) {
+ return false;
+ }
+
+ return !aAccessible->Parent() ||
+ aAccessible->Parent()->FirstChild() != aAccessible;
+}
+#endif
+
+SessionAccessibility::IDMappingEntry::IDMappingEntry(Accessible* aAccessible)
+ : mInternalID(0) {
+ *this = aAccessible;
+}
+
+SessionAccessibility::IDMappingEntry&
+SessionAccessibility::IDMappingEntry::operator=(Accessible* aAccessible) {
+ mInternalID = aAccessible->ID();
+ MOZ_ASSERT(!(mInternalID & IS_REMOTE), "First bit is used in accessible ID!");
+ if (aAccessible->IsRemote()) {
+ mInternalID |= IS_REMOTE;
+ }
+
+ Accessible* docAcc = nsAccUtils::DocumentFor(aAccessible);
+ MOZ_ASSERT(docAcc);
+ if (docAcc) {
+ MOZ_ASSERT(docAcc->IsRemote() == aAccessible->IsRemote());
+ if (docAcc->IsRemote()) {
+ mDoc = docAcc->AsRemote()->AsDoc();
+ } else {
+ mDoc = docAcc->AsLocal();
+ }
+ }
+
+ return *this;
+}
+
+SessionAccessibility::IDMappingEntry::operator Accessible*() const {
+ if (mInternalID == 0) {
+ return static_cast<LocalAccessible*>(mDoc.get());
+ }
+
+ if (mInternalID == IS_REMOTE) {
+ return static_cast<DocAccessibleParent*>(mDoc.get());
+ }
+
+ if (mInternalID & IS_REMOTE) {
+ return static_cast<DocAccessibleParent*>(mDoc.get())
+ ->GetAccessible(mInternalID & ~IS_REMOTE);
+ }
+
+ Accessible* accessible =
+ static_cast<LocalAccessible*>(mDoc.get())
+ ->AsDoc()
+ ->GetAccessibleByUniqueID(reinterpret_cast<void*>(mInternalID));
+ // If the accessible is retrievable from the DocAccessible, it can't be
+ // defunct.
+ MOZ_ASSERT(!accessible->AsLocal()->IsDefunct());
+
+ return accessible;
+}
+
+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..deacc57150
--- /dev/null
+++ b/accessible/android/SessionAccessibility.h
@@ -0,0 +1,145 @@
+/* -*- 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"
+
+namespace mozilla {
+namespace a11y {
+
+class Accessible;
+
+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;
+ 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);
+ bool Pivot(int32_t aID, int32_t aGranularity, bool aForward, bool aInclusive);
+ void ExploreByTouch(int32_t aID, float aX, float aY);
+ bool 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);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY 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,
+ bool aScrollIntoView);
+ 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);
+
+ 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() {}
+
+ void PopulateNodeInfo(Accessible* aAccessible,
+ mozilla::jni::Object::Param aNodeInfo);
+
+ void SetAttached(bool aAttached, already_AddRefed<Runnable> aRunnable);
+
+ bool DoNavigateText(Accessible* aAccessible, int32_t aGranularity,
+ int32_t aStartOffset, int32_t aEndOffset, bool aForward,
+ bool aSelect);
+
+ jni::NativeWeakPtr<widget::GeckoViewSupport> mWindow; // Parent only
+ java::SessionAccessibility::NativeProvider::GlobalRef mSessionAccessibility;
+
+ class IDMappingEntry {
+ public:
+ explicit IDMappingEntry(Accessible* aAccessible);
+
+ IDMappingEntry& operator=(Accessible* aAccessible);
+
+ operator Accessible*() const;
+
+ private:
+ // A strong reference to a DocAccessible or DocAccessibleParent. They don't
+ // share any useful base class except nsISupports, so we use that.
+ // When we retrieve the document from this reference we cast it to
+ // LocalAccessible in the DocAccessible case because DocAccessible has
+ // multiple inheritance paths for nsISupports.
+ RefPtr<nsISupports> mDoc;
+ // The ID of the accessible as used in the internal doc mapping.
+ // We rely on this ID being pointer derived and therefore divisible by two
+ // so we can use the first bit to mark if it is remote or not.
+ uint64_t mInternalID;
+
+ static const uintptr_t IS_REMOTE = 0x1;
+ };
+
+ /*
+ * This provides a mapping from 32 bit id to accessible objects.
+ */
+ nsBaseHashtable<nsUint32HashKey, IDMappingEntry, 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..4203fd48c6
--- /dev/null
+++ b/accessible/android/TraversalRule.cpp
@@ -0,0 +1,288 @@
+/* -*- 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 "mozilla/a11y/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::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::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::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..f8a185da48
--- /dev/null
+++ b/accessible/android/moz.build
@@ -0,0 +1,35 @@
+# -*- 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",
+ "SessionAccessibility.h",
+ "TraversalRule.h",
+]
+
+SOURCES += [
+ "AccessibleWrap.cpp",
+ "DocAccessibleWrap.cpp",
+ "Platform.cpp",
+ "SessionAccessibility.cpp",
+ "TraversalRule.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/base",
+ "/accessible/generic",
+ "/accessible/html",
+ "/accessible/ipc",
+ "/accessible/xpcom",
+ "/accessible/xul",
+ "/dom/base",
+ "/widget",
+ "/widget/android",
+]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")