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