diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /accessible/android | |
parent | Initial commit. (diff) | |
download | firefox-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.cpp | 473 | ||||
-rw-r--r-- | accessible/android/AccessibleWrap.h | 68 | ||||
-rw-r--r-- | accessible/android/ApplicationAccessibleWrap.h | 20 | ||||
-rw-r--r-- | accessible/android/DocAccessibleWrap.cpp | 78 | ||||
-rw-r--r-- | accessible/android/DocAccessibleWrap.h | 33 | ||||
-rw-r--r-- | accessible/android/Platform.cpp | 233 | ||||
-rw-r--r-- | accessible/android/RootAccessibleWrap.h | 22 | ||||
-rw-r--r-- | accessible/android/SessionAccessibility.cpp | 941 | ||||
-rw-r--r-- | accessible/android/SessionAccessibility.h | 145 | ||||
-rw-r--r-- | accessible/android/TraversalRule.cpp | 288 | ||||
-rw-r--r-- | accessible/android/TraversalRule.h | 58 | ||||
-rw-r--r-- | accessible/android/moz.build | 35 |
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") |