Backed out changeset 8be4693aecbb (bug 1754905) for causing bustages in xpcAccessible...
[gecko.git] / accessible / android / AccessibleWrap.cpp
blob20543ed470ad8a5b324f6abc6dc452ebd0f4b637
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "AccessibleWrap.h"
8 #include "JavaBuiltins.h"
9 #include "LocalAccessible-inl.h"
10 #include "HyperTextAccessible-inl.h"
11 #include "AccAttributes.h"
12 #include "AccEvent.h"
13 #include "AndroidInputType.h"
14 #include "DocAccessibleWrap.h"
15 #include "SessionAccessibility.h"
16 #include "TextLeafAccessible.h"
17 #include "TraversalRule.h"
18 #include "Pivot.h"
19 #include "Platform.h"
20 #include "nsAccessibilityService.h"
21 #include "nsEventShell.h"
22 #include "nsIAccessibleAnnouncementEvent.h"
23 #include "nsAccUtils.h"
24 #include "nsTextEquivUtils.h"
25 #include "nsWhitespaceTokenizer.h"
26 #include "RootAccessible.h"
27 #include "TextLeafRange.h"
29 #include "mozilla/a11y/PDocAccessibleChild.h"
30 #include "mozilla/jni/GeckoBundleUtils.h"
31 #include "mozilla/a11y/DocAccessibleParent.h"
32 #include "mozilla/Maybe.h"
34 // icu TRUE conflicting with java::sdk::Boolean::TRUE()
35 // https://searchfox.org/mozilla-central/rev/ce02064d8afc8673cef83c92896ee873bd35e7ae/intl/icu/source/common/unicode/umachine.h#265
36 // https://searchfox.org/mozilla-central/source/__GENERATED__/widget/android/bindings/JavaBuiltins.h#78
37 #ifdef TRUE
38 # undef TRUE
39 #endif
41 using namespace mozilla::a11y;
42 using mozilla::Maybe;
44 //-----------------------------------------------------
45 // construction
46 //-----------------------------------------------------
47 AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc)
48 : LocalAccessible(aContent, aDoc), mID(SessionAccessibility::kUnsetID) {
49 if (!IPCAccessibilityActive()) {
50 MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
51 SessionAccessibility::RegisterAccessible(this);
55 //-----------------------------------------------------
56 // destruction
57 //-----------------------------------------------------
58 AccessibleWrap::~AccessibleWrap() {}
60 nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
61 auto accessible = static_cast<AccessibleWrap*>(aEvent->GetAccessible());
62 NS_ENSURE_TRUE(accessible, NS_ERROR_FAILURE);
63 DocAccessibleWrap* doc =
64 static_cast<DocAccessibleWrap*>(accessible->Document());
65 if (doc) {
66 switch (aEvent->GetEventType()) {
67 case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
68 if (accessible != aEvent->Document() && !aEvent->IsFromUserInput()) {
69 AccCaretMoveEvent* caretEvent = downcast_accEvent(aEvent);
70 HyperTextAccessible* ht = AsHyperText();
71 // Pivot to the caret's position if it has an expanded selection.
72 // This is used mostly for find in page.
73 if ((ht && ht->SelectionCount())) {
74 DOMPoint point =
75 AsHyperText()->OffsetToDOMPoint(caretEvent->GetCaretOffset());
76 if (LocalAccessible* newPos =
77 doc->GetAccessibleOrContainer(point.node)) {
78 static_cast<AccessibleWrap*>(newPos)->PivotTo(
79 java::SessionAccessibility::HTML_GRANULARITY_DEFAULT, true,
80 true);
84 break;
86 case nsIAccessibleEvent::EVENT_SCROLLING_START: {
87 accessible->PivotTo(
88 java::SessionAccessibility::HTML_GRANULARITY_DEFAULT, true, true);
89 break;
91 default:
92 break;
96 nsresult rv = LocalAccessible::HandleAccEvent(aEvent);
97 NS_ENSURE_SUCCESS(rv, rv);
99 accessible->HandleLiveRegionEvent(aEvent);
101 if (IPCAccessibilityActive()) {
102 return NS_OK;
105 // The accessible can become defunct if we have an xpcom event listener
106 // which decides it would be fun to change the DOM and flush layout.
107 if (accessible->IsDefunct() || !accessible->IsBoundToParent()) {
108 return NS_OK;
111 if (doc) {
112 if (!doc->DocumentNode()->IsContentDocument()) {
113 return NS_OK;
117 RefPtr<SessionAccessibility> sessionAcc =
118 SessionAccessibility::GetInstanceFor(accessible);
119 if (!sessionAcc) {
120 return NS_OK;
123 switch (aEvent->GetEventType()) {
124 case nsIAccessibleEvent::EVENT_FOCUS:
125 sessionAcc->SendFocusEvent(accessible);
126 break;
127 case nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED: {
128 AccVCChangeEvent* vcEvent = downcast_accEvent(aEvent);
129 if (!vcEvent->IsFromUserInput()) {
130 break;
133 RefPtr<AccessibleWrap> newPosition =
134 static_cast<AccessibleWrap*>(vcEvent->NewAccessible());
135 if (sessionAcc && newPosition) {
136 if (vcEvent->Reason() == nsIAccessiblePivot::REASON_POINT) {
137 sessionAcc->SendHoverEnterEvent(newPosition);
138 } else {
139 sessionAcc->SendAccessibilityFocusedEvent(newPosition);
142 break;
144 case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
145 AccCaretMoveEvent* event = downcast_accEvent(aEvent);
146 sessionAcc->SendTextSelectionChangedEvent(accessible,
147 event->GetCaretOffset());
148 break;
150 case nsIAccessibleEvent::EVENT_TEXT_INSERTED:
151 case nsIAccessibleEvent::EVENT_TEXT_REMOVED: {
152 AccTextChangeEvent* event = downcast_accEvent(aEvent);
153 sessionAcc->SendTextChangedEvent(
154 accessible, event->ModifiedText(), event->GetStartOffset(),
155 event->GetLength(), event->IsTextInserted(),
156 event->IsFromUserInput());
157 break;
159 case nsIAccessibleEvent::EVENT_STATE_CHANGE: {
160 AccStateChangeEvent* event = downcast_accEvent(aEvent);
161 auto state = event->GetState();
162 if (state & states::CHECKED) {
163 sessionAcc->SendClickedEvent(
164 accessible, java::SessionAccessibility::FLAG_CHECKABLE |
165 (event->IsStateEnabled()
166 ? java::SessionAccessibility::FLAG_CHECKED
167 : 0));
170 if (state & states::EXPANDED) {
171 sessionAcc->SendClickedEvent(
172 accessible, java::SessionAccessibility::FLAG_EXPANDABLE |
173 (event->IsStateEnabled()
174 ? java::SessionAccessibility::FLAG_EXPANDED
175 : 0));
178 if (state & states::SELECTED) {
179 sessionAcc->SendSelectedEvent(accessible, event->IsStateEnabled());
182 if (state & states::BUSY) {
183 sessionAcc->SendWindowStateChangedEvent(accessible);
185 break;
187 case nsIAccessibleEvent::EVENT_SCROLLING: {
188 AccScrollingEvent* event = downcast_accEvent(aEvent);
189 sessionAcc->SendScrollingEvent(accessible, event->ScrollX(),
190 event->ScrollY(), event->MaxScrollX(),
191 event->MaxScrollY());
192 break;
194 case nsIAccessibleEvent::EVENT_ANNOUNCEMENT: {
195 AccAnnouncementEvent* event = downcast_accEvent(aEvent);
196 sessionAcc->SendAnnouncementEvent(accessible, event->Announcement(),
197 event->Priority());
198 break;
200 case nsIAccessibleEvent::EVENT_REORDER: {
201 sessionAcc->SendWindowContentChangedEvent();
202 break;
204 default:
205 break;
208 return NS_OK;
211 void AccessibleWrap::Shutdown() {
212 if (!IPCAccessibilityActive()) {
213 MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
214 SessionAccessibility::UnregisterAccessible(this);
216 LocalAccessible::Shutdown();
219 bool AccessibleWrap::DoAction(uint8_t aIndex) const {
220 if (ActionCount()) {
221 return LocalAccessible::DoAction(aIndex);
224 if (mContent) {
225 // We still simulate a click on an accessible even if there is no
226 // known actions. For the sake of bad markup.
227 DoCommand();
228 return true;
231 return false;
234 Accessible* AccessibleWrap::DoPivot(Accessible* aAccessible,
235 int32_t aGranularity, bool aForward,
236 bool aInclusive) {
237 Accessible* pivotRoot = nullptr;
238 if (aAccessible->IsRemote()) {
239 // If this is a remote accessible provide the top level
240 // remote doc as the pivot root for thread safety reasons.
241 DocAccessibleParent* doc = aAccessible->AsRemote()->Document();
242 while (doc && !doc->IsTopLevel()) {
243 doc = doc->ParentDoc();
245 MOZ_ASSERT(doc, "Failed to get top level DocAccessibleParent");
246 pivotRoot = doc;
248 a11y::Pivot pivot(pivotRoot);
249 // Depending on the start accessible, the pivot rule will either traverse
250 // local or remote accessibles exclusively.
251 TraversalRule rule(aGranularity, aAccessible->IsLocal());
252 Accessible* result = aForward ? pivot.Next(aAccessible, rule, aInclusive)
253 : pivot.Prev(aAccessible, rule, aInclusive);
255 if (result && (result != aAccessible || aInclusive)) {
256 return result;
259 return nullptr;
262 bool AccessibleWrap::PivotTo(int32_t aGranularity, bool aForward,
263 bool aInclusive) {
264 Accessible* result = DoPivot(this, aGranularity, aForward, aInclusive);
265 if (result) {
266 MOZ_ASSERT(result->IsLocal());
267 // Dispatch a virtual cursor change event that will be turned into an
268 // android accessibility focused changed event in the parent.
269 PivotMoveReason reason = aForward ? nsIAccessiblePivot::REASON_NEXT
270 : nsIAccessiblePivot::REASON_PREV;
271 LocalAccessible* localResult = result->AsLocal();
272 RefPtr<AccEvent> event = new AccVCChangeEvent(
273 localResult->Document(), this, localResult, reason, eFromUserInput);
274 nsEventShell::FireEvent(event);
276 return true;
279 return false;
282 void AccessibleWrap::ExploreByTouch(float aX, float aY) {
283 a11y::Pivot pivot(RootAccessible());
284 TraversalRule rule;
286 Accessible* maybeResult = pivot.AtPoint(aX, aY, rule);
287 LocalAccessible* result = maybeResult ? maybeResult->AsLocal() : nullptr;
289 if (result && result != this) {
290 RefPtr<AccEvent> event =
291 new AccVCChangeEvent(result->Document(), this, result,
292 nsIAccessiblePivot::REASON_POINT, eFromUserInput);
293 nsEventShell::FireEvent(event);
297 static TextLeafPoint ToTextLeafPoint(Accessible* aAccessible, int32_t aOffset) {
298 if (HyperTextAccessibleBase* ht = aAccessible->AsHyperTextBase()) {
299 return ht->ToTextLeafPoint(aOffset);
302 return TextLeafPoint(aAccessible, aOffset);
305 Maybe<std::pair<int32_t, int32_t>> AccessibleWrap::NavigateText(
306 Accessible* aAccessible, int32_t aGranularity, int32_t aStartOffset,
307 int32_t aEndOffset, bool aForward, bool aSelect) {
308 int32_t startOffset = aStartOffset;
309 int32_t endOffset = aEndOffset;
310 if (startOffset == -1) {
311 MOZ_ASSERT(endOffset == -1,
312 "When start offset is unset, end offset should be too");
313 startOffset = aForward ? 0 : nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT;
314 endOffset = aForward ? 0 : nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT;
317 // If the accessible is an editable, set the virtual cursor position
318 // to its caret offset. Otherwise use the document's virtual cursor
319 // position as a starting offset.
320 if (aAccessible->State() & states::EDITABLE) {
321 startOffset = endOffset = aAccessible->AsHyperTextBase()->CaretOffset();
324 TextLeafRange currentRange =
325 TextLeafRange(ToTextLeafPoint(aAccessible, startOffset),
326 ToTextLeafPoint(aAccessible, endOffset));
327 uint16_t startBoundaryType = nsIAccessibleText::BOUNDARY_LINE_START;
328 uint16_t endBoundaryType = nsIAccessibleText::BOUNDARY_LINE_END;
329 switch (aGranularity) {
330 case 1: // MOVEMENT_GRANULARITY_CHARACTER
331 startBoundaryType = nsIAccessibleText::BOUNDARY_CHAR;
332 endBoundaryType = nsIAccessibleText::BOUNDARY_CHAR;
333 break;
334 case 2: // MOVEMENT_GRANULARITY_WORD
335 startBoundaryType = nsIAccessibleText::BOUNDARY_WORD_START;
336 endBoundaryType = nsIAccessibleText::BOUNDARY_WORD_END;
337 break;
338 default:
339 break;
342 TextLeafRange resultRange;
344 if (aForward) {
345 resultRange.SetEnd(
346 currentRange.End().FindBoundary(endBoundaryType, eDirNext));
347 resultRange.SetStart(
348 resultRange.End().FindBoundary(startBoundaryType, eDirPrevious));
349 } else {
350 resultRange.SetStart(
351 currentRange.Start().FindBoundary(startBoundaryType, eDirPrevious));
352 resultRange.SetEnd(
353 resultRange.Start().FindBoundary(endBoundaryType, eDirNext));
356 if (!resultRange.Crop(aAccessible)) {
357 // If the new range does not intersect at all with the given
358 // accessible/container this navigation has failed or reached an edge.
359 return Nothing();
362 if (resultRange == currentRange || resultRange.Start() == resultRange.End()) {
363 // If the result range equals the current range, or if the result range is
364 // collapsed, we failed or reached an edge.
365 return Nothing();
368 if (HyperTextAccessibleBase* ht = aAccessible->AsHyperTextBase()) {
369 DebugOnly<bool> ok = false;
370 std::tie(ok, startOffset) = ht->TransformOffset(
371 resultRange.Start().mAcc, resultRange.Start().mOffset, false);
372 MOZ_ASSERT(ok, "Accessible of range start should be in container.");
374 std::tie(ok, endOffset) = ht->TransformOffset(
375 resultRange.End().mAcc, resultRange.End().mOffset, false);
376 MOZ_ASSERT(ok, "Accessible range end should be in container.");
377 } else {
378 startOffset = resultRange.Start().mOffset;
379 endOffset = resultRange.End().mOffset;
382 return Some(std::make_pair(startOffset, endOffset));
385 uint32_t AccessibleWrap::GetFlags(role aRole, uint64_t aState,
386 uint8_t aActionCount) {
387 uint32_t flags = 0;
388 if (aState & states::CHECKABLE) {
389 flags |= java::SessionAccessibility::FLAG_CHECKABLE;
392 if (aState & states::CHECKED) {
393 flags |= java::SessionAccessibility::FLAG_CHECKED;
396 if (aState & states::INVALID) {
397 flags |= java::SessionAccessibility::FLAG_CONTENT_INVALID;
400 if (aState & states::EDITABLE) {
401 flags |= java::SessionAccessibility::FLAG_EDITABLE;
404 if (aActionCount && aRole != roles::TEXT_LEAF) {
405 flags |= java::SessionAccessibility::FLAG_CLICKABLE;
408 if (aState & states::ENABLED) {
409 flags |= java::SessionAccessibility::FLAG_ENABLED;
412 if (aState & states::FOCUSABLE) {
413 flags |= java::SessionAccessibility::FLAG_FOCUSABLE;
416 if (aState & states::FOCUSED) {
417 flags |= java::SessionAccessibility::FLAG_FOCUSED;
420 if (aState & states::MULTI_LINE) {
421 flags |= java::SessionAccessibility::FLAG_MULTI_LINE;
424 if (aState & states::SELECTABLE) {
425 flags |= java::SessionAccessibility::FLAG_SELECTABLE;
428 if (aState & states::SELECTED) {
429 flags |= java::SessionAccessibility::FLAG_SELECTED;
432 if (aState & states::EXPANDABLE) {
433 flags |= java::SessionAccessibility::FLAG_EXPANDABLE;
436 if (aState & states::EXPANDED) {
437 flags |= java::SessionAccessibility::FLAG_EXPANDED;
440 if ((aState & (states::INVISIBLE | states::OFFSCREEN)) == 0) {
441 flags |= java::SessionAccessibility::FLAG_VISIBLE_TO_USER;
444 if (aRole == roles::PASSWORD_TEXT) {
445 flags |= java::SessionAccessibility::FLAG_PASSWORD;
448 return flags;
451 void AccessibleWrap::GetRoleDescription(role aRole, AccAttributes* aAttributes,
452 nsAString& aGeckoRole,
453 nsAString& aRoleDescription) {
454 if (aRole == roles::HEADING && aAttributes) {
455 // The heading level is an attribute, so we need that.
456 nsAutoString headingLevel;
457 if (aAttributes->GetAttribute(nsGkAtoms::level, headingLevel)) {
458 nsAutoString token(u"heading-");
459 token.Append(headingLevel);
460 if (LocalizeString(token, aRoleDescription)) {
461 return;
466 if ((aRole == roles::LANDMARK || aRole == roles::REGION) && aAttributes) {
467 nsAutoString xmlRoles;
468 if (aAttributes->GetAttribute(nsGkAtoms::xmlroles, xmlRoles)) {
469 nsWhitespaceTokenizer tokenizer(xmlRoles);
470 while (tokenizer.hasMoreTokens()) {
471 if (LocalizeString(tokenizer.nextToken(), aRoleDescription)) {
472 return;
478 GetAccService()->GetStringRole(aRole, aGeckoRole);
479 LocalizeString(aGeckoRole, aRoleDescription);
482 int32_t AccessibleWrap::AndroidClass(Accessible* aAccessible) {
483 return GetVirtualViewID(aAccessible) == SessionAccessibility::kNoID
484 ? java::SessionAccessibility::CLASSNAME_WEBVIEW
485 : GetAndroidClass(aAccessible->Role());
488 int32_t AccessibleWrap::GetVirtualViewID(Accessible* aAccessible) {
489 if (aAccessible->IsLocal()) {
490 return static_cast<AccessibleWrap*>(aAccessible)->mID;
493 return static_cast<int32_t>(aAccessible->AsRemote()->GetWrapper());
496 void AccessibleWrap::SetVirtualViewID(Accessible* aAccessible,
497 int32_t aVirtualViewID) {
498 if (aAccessible->IsLocal()) {
499 static_cast<AccessibleWrap*>(aAccessible)->mID = aVirtualViewID;
500 } else {
501 aAccessible->AsRemote()->SetWrapper(static_cast<uintptr_t>(aVirtualViewID));
505 int32_t AccessibleWrap::GetAndroidClass(role aRole) {
506 #define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
507 msaaRole, ia2Role, androidClass, nameRule) \
508 case roles::geckoRole: \
509 return androidClass;
511 switch (aRole) {
512 #include "RoleMap.h"
513 default:
514 return java::SessionAccessibility::CLASSNAME_VIEW;
517 #undef ROLE
520 int32_t AccessibleWrap::GetInputType(const nsString& aInputTypeAttr) {
521 if (aInputTypeAttr.EqualsIgnoreCase("email")) {
522 return java::sdk::InputType::TYPE_CLASS_TEXT |
523 java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
526 if (aInputTypeAttr.EqualsIgnoreCase("number")) {
527 return java::sdk::InputType::TYPE_CLASS_NUMBER;
530 if (aInputTypeAttr.EqualsIgnoreCase("password")) {
531 return java::sdk::InputType::TYPE_CLASS_TEXT |
532 java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_PASSWORD;
535 if (aInputTypeAttr.EqualsIgnoreCase("tel")) {
536 return java::sdk::InputType::TYPE_CLASS_PHONE;
539 if (aInputTypeAttr.EqualsIgnoreCase("text")) {
540 return java::sdk::InputType::TYPE_CLASS_TEXT |
541 java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
544 if (aInputTypeAttr.EqualsIgnoreCase("url")) {
545 return java::sdk::InputType::TYPE_CLASS_TEXT |
546 java::sdk::InputType::TYPE_TEXT_VARIATION_URI;
549 return 0;
552 void AccessibleWrap::GetTextEquiv(nsString& aText) {
553 if (nsTextEquivUtils::HasNameRule(this, eNameFromSubtreeIfReqRule)) {
554 // This is an accessible that normally doesn't get its name from its
555 // subtree, so we collect the text equivalent explicitly.
556 nsTextEquivUtils::GetTextEquivFromSubtree(this, aText);
557 } else {
558 Name(aText);
562 bool AccessibleWrap::HandleLiveRegionEvent(AccEvent* aEvent) {
563 auto eventType = aEvent->GetEventType();
564 if (eventType != nsIAccessibleEvent::EVENT_TEXT_INSERTED &&
565 eventType != nsIAccessibleEvent::EVENT_NAME_CHANGE) {
566 // XXX: Right now only announce text inserted events. aria-relevant=removals
567 // is potentially on the chopping block[1]. We also don't support editable
568 // text because we currently can't descern the source of the change[2].
569 // 1. https://github.com/w3c/aria/issues/712
570 // 2. https://bugzilla.mozilla.org/show_bug.cgi?id=1531189
571 return false;
574 if (aEvent->IsFromUserInput()) {
575 return false;
578 RefPtr<AccAttributes> attributes = new AccAttributes();
579 nsAccUtils::SetLiveContainerAttributes(attributes, this);
580 nsString live;
581 if (!attributes->GetAttribute(nsGkAtoms::containerLive, live)) {
582 return false;
585 uint16_t priority = live.EqualsIgnoreCase("assertive")
586 ? nsIAccessibleAnnouncementEvent::ASSERTIVE
587 : nsIAccessibleAnnouncementEvent::POLITE;
589 Maybe<bool> atomic =
590 attributes->GetAttribute<bool>(nsGkAtoms::containerAtomic);
591 LocalAccessible* announcementTarget = this;
592 nsAutoString announcement;
593 if (atomic && *atomic) {
594 LocalAccessible* atomicAncestor = nullptr;
595 for (LocalAccessible* parent = announcementTarget; parent;
596 parent = parent->LocalParent()) {
597 dom::Element* element = parent->Elm();
598 if (element &&
599 nsAccUtils::ARIAAttrValueIs(element, nsGkAtoms::aria_atomic,
600 nsGkAtoms::_true, eCaseMatters)) {
601 atomicAncestor = parent;
602 break;
606 if (atomicAncestor) {
607 announcementTarget = atomicAncestor;
608 static_cast<AccessibleWrap*>(atomicAncestor)->GetTextEquiv(announcement);
610 } else {
611 GetTextEquiv(announcement);
614 announcement.CompressWhitespace();
615 if (announcement.IsEmpty()) {
616 return false;
619 announcementTarget->Announce(announcement, priority);
620 return true;