Bug 1635217 [wpt PR 23388] - Add WPT tests for bug 1074317, a=testonly
[gecko.git] / accessible / android / AccessibleWrap.cpp
blobb1e42b0b141cf411d424fea420b79bc5e77f0a1b
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 "Accessible-inl.h"
10 #include "HyperTextAccessible-inl.h"
11 #include "AccEvent.h"
12 #include "AndroidInputType.h"
13 #include "DocAccessibleWrap.h"
14 #include "IDSet.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 "nsPersistentProperties.h"
23 #include "nsIAccessibleAnnouncementEvent.h"
24 #include "nsAccUtils.h"
25 #include "nsTextEquivUtils.h"
26 #include "nsWhitespaceTokenizer.h"
27 #include "RootAccessible.h"
29 #include "mozilla/a11y/PDocAccessibleChild.h"
30 #include "mozilla/jni/GeckoBundleUtils.h"
32 // icu TRUE conflicting with java::sdk::Boolean::TRUE()
33 // https://searchfox.org/mozilla-central/rev/ce02064d8afc8673cef83c92896ee873bd35e7ae/intl/icu/source/common/unicode/umachine.h#265
34 // https://searchfox.org/mozilla-central/source/__GENERATED__/widget/android/bindings/JavaBuiltins.h#78
35 #ifdef TRUE
36 # undef TRUE
37 #endif
39 using namespace mozilla::a11y;
41 // IDs should be a positive 32bit integer.
42 IDSet sIDSet(31UL);
44 //-----------------------------------------------------
45 // construction
46 //-----------------------------------------------------
47 AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc)
48 : Accessible(aContent, aDoc) {
49 if (aDoc) {
50 mID = AcquireID();
51 DocAccessibleWrap* doc = static_cast<DocAccessibleWrap*>(aDoc);
52 doc->AddID(mID, this);
56 //-----------------------------------------------------
57 // destruction
58 //-----------------------------------------------------
59 AccessibleWrap::~AccessibleWrap() {}
61 nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
62 auto accessible = static_cast<AccessibleWrap*>(aEvent->GetAccessible());
63 NS_ENSURE_TRUE(accessible, NS_ERROR_FAILURE);
64 DocAccessibleWrap* doc =
65 static_cast<DocAccessibleWrap*>(accessible->Document());
66 if (doc) {
67 switch (aEvent->GetEventType()) {
68 case nsIAccessibleEvent::EVENT_FOCUS: {
69 if (DocAccessibleWrap* topContentDoc =
70 doc->GetTopLevelContentDoc(accessible)) {
71 topContentDoc->CacheFocusPath(accessible);
73 break;
75 case nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED: {
76 AccVCChangeEvent* vcEvent = downcast_accEvent(aEvent);
77 auto newPosition =
78 static_cast<AccessibleWrap*>(vcEvent->NewAccessible());
79 if (newPosition) {
80 if (DocAccessibleWrap* topContentDoc =
81 doc->GetTopLevelContentDoc(accessible)) {
82 topContentDoc->CacheFocusPath(newPosition);
85 break;
87 case nsIAccessibleEvent::EVENT_REORDER: {
88 if (DocAccessibleWrap* topContentDoc =
89 doc->GetTopLevelContentDoc(accessible)) {
90 topContentDoc->CacheViewport(true);
92 break;
94 case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
95 if (accessible != aEvent->Document() && !aEvent->IsFromUserInput()) {
96 AccCaretMoveEvent* caretEvent = downcast_accEvent(aEvent);
97 HyperTextAccessible* ht = AsHyperText();
98 // Pivot to the caret's position if it has an expanded selection.
99 // This is used mostly for find in page.
100 if ((ht && ht->SelectionCount())) {
101 DOMPoint point =
102 AsHyperText()->OffsetToDOMPoint(caretEvent->GetCaretOffset());
103 if (Accessible* newPos =
104 doc->GetAccessibleOrContainer(point.node)) {
105 static_cast<AccessibleWrap*>(newPos)->Pivot(
106 java::SessionAccessibility::HTML_GRANULARITY_DEFAULT, true,
107 true);
111 break;
113 case nsIAccessibleEvent::EVENT_SCROLLING_START: {
114 accessible->Pivot(java::SessionAccessibility::HTML_GRANULARITY_DEFAULT,
115 true, true);
116 break;
118 default:
119 break;
123 nsresult rv = Accessible::HandleAccEvent(aEvent);
124 NS_ENSURE_SUCCESS(rv, rv);
126 accessible->HandleLiveRegionEvent(aEvent);
128 if (IPCAccessibilityActive()) {
129 return NS_OK;
132 // The accessible can become defunct if we have an xpcom event listener
133 // which decides it would be fun to change the DOM and flush layout.
134 if (accessible->IsDefunct() || !accessible->IsBoundToParent()) {
135 return NS_OK;
138 if (doc) {
139 if (!nsCoreUtils::IsContentDocument(doc->DocumentNode())) {
140 return NS_OK;
144 RefPtr<SessionAccessibility> sessionAcc =
145 SessionAccessibility::GetInstanceFor(accessible);
146 if (!sessionAcc) {
147 return NS_OK;
150 switch (aEvent->GetEventType()) {
151 case nsIAccessibleEvent::EVENT_FOCUS:
152 sessionAcc->SendFocusEvent(accessible);
153 break;
154 case nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED: {
155 AccVCChangeEvent* vcEvent = downcast_accEvent(aEvent);
156 if (!vcEvent->IsFromUserInput()) {
157 break;
160 RefPtr<AccessibleWrap> newPosition =
161 static_cast<AccessibleWrap*>(vcEvent->NewAccessible());
162 if (sessionAcc && newPosition) {
163 if (vcEvent->Reason() == nsIAccessiblePivot::REASON_POINT) {
164 sessionAcc->SendHoverEnterEvent(newPosition);
165 } else if (vcEvent->BoundaryType() == nsIAccessiblePivot::NO_BOUNDARY) {
166 sessionAcc->SendAccessibilityFocusedEvent(newPosition);
169 if (vcEvent->BoundaryType() != nsIAccessiblePivot::NO_BOUNDARY) {
170 sessionAcc->SendTextTraversedEvent(
171 newPosition, vcEvent->NewStartOffset(), vcEvent->NewEndOffset());
174 break;
176 case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
177 AccCaretMoveEvent* event = downcast_accEvent(aEvent);
178 sessionAcc->SendTextSelectionChangedEvent(accessible,
179 event->GetCaretOffset());
180 break;
182 case nsIAccessibleEvent::EVENT_TEXT_INSERTED:
183 case nsIAccessibleEvent::EVENT_TEXT_REMOVED: {
184 AccTextChangeEvent* event = downcast_accEvent(aEvent);
185 sessionAcc->SendTextChangedEvent(
186 accessible, event->ModifiedText(), event->GetStartOffset(),
187 event->GetLength(), event->IsTextInserted(),
188 event->IsFromUserInput());
189 break;
191 case nsIAccessibleEvent::EVENT_STATE_CHANGE: {
192 AccStateChangeEvent* event = downcast_accEvent(aEvent);
193 auto state = event->GetState();
194 if (state & states::CHECKED) {
195 sessionAcc->SendClickedEvent(
196 accessible, java::SessionAccessibility::FLAG_CHECKABLE |
197 (event->IsStateEnabled()
198 ? java::SessionAccessibility::FLAG_CHECKED
199 : 0));
202 if (state & states::EXPANDED) {
203 sessionAcc->SendClickedEvent(
204 accessible, java::SessionAccessibility::FLAG_EXPANDABLE |
205 (event->IsStateEnabled()
206 ? java::SessionAccessibility::FLAG_EXPANDED
207 : 0));
210 if (state & states::SELECTED) {
211 sessionAcc->SendSelectedEvent(accessible, event->IsStateEnabled());
214 if (state & states::BUSY) {
215 sessionAcc->SendWindowStateChangedEvent(accessible);
217 break;
219 case nsIAccessibleEvent::EVENT_SCROLLING: {
220 AccScrollingEvent* event = downcast_accEvent(aEvent);
221 sessionAcc->SendScrollingEvent(accessible, event->ScrollX(),
222 event->ScrollY(), event->MaxScrollX(),
223 event->MaxScrollY());
224 break;
226 case nsIAccessibleEvent::EVENT_ANNOUNCEMENT: {
227 AccAnnouncementEvent* event = downcast_accEvent(aEvent);
228 sessionAcc->SendAnnouncementEvent(accessible, event->Announcement(),
229 event->Priority());
230 break;
232 default:
233 break;
236 return NS_OK;
239 void AccessibleWrap::Shutdown() {
240 if (mDoc) {
241 if (mID > 0) {
242 if (auto doc = static_cast<DocAccessibleWrap*>(mDoc.get())) {
243 doc->RemoveID(mID);
245 ReleaseID(mID);
246 mID = 0;
250 Accessible::Shutdown();
253 bool AccessibleWrap::DoAction(uint8_t aIndex) const {
254 if (ActionCount()) {
255 return Accessible::DoAction(aIndex);
258 if (mContent) {
259 // We still simulate a click on an accessible even if there is no
260 // known actions. For the sake of bad markup.
261 DoCommand();
262 return true;
265 return false;
268 int32_t AccessibleWrap::AcquireID() { return sIDSet.GetID(); }
270 void AccessibleWrap::ReleaseID(int32_t aID) { sIDSet.ReleaseID(aID); }
272 void AccessibleWrap::SetTextContents(const nsAString& aText) {
273 if (IsHyperText()) {
274 AsHyperText()->ReplaceText(aText);
278 void AccessibleWrap::GetTextContents(nsAString& aText) {
279 // For now it is a simple wrapper for getting entire range of TextSubstring.
280 // In the future this may be smarter and retrieve a flattened string.
281 if (IsHyperText()) {
282 AsHyperText()->TextSubstring(0, -1, aText);
283 } else if (IsTextLeaf()) {
284 aText = AsTextLeaf()->Text();
288 bool AccessibleWrap::GetSelectionBounds(int32_t* aStartOffset,
289 int32_t* aEndOffset) {
290 if (IsHyperText()) {
291 return AsHyperText()->SelectionBoundsAt(0, aStartOffset, aEndOffset);
294 return false;
297 void AccessibleWrap::Pivot(int32_t aGranularity, bool aForward,
298 bool aInclusive) {
299 a11y::Pivot pivot(RootAccessible());
300 TraversalRule rule(aGranularity);
301 Accessible* result = aForward ? pivot.Next(this, rule, aInclusive)
302 : pivot.Prev(this, rule, aInclusive);
303 if (result && (result != this || aInclusive)) {
304 PivotMoveReason reason = aForward ? nsIAccessiblePivot::REASON_NEXT
305 : nsIAccessiblePivot::REASON_PREV;
306 RefPtr<AccEvent> event = new AccVCChangeEvent(
307 result->Document(), this, -1, -1, result, -1, -1, reason,
308 nsIAccessiblePivot::NO_BOUNDARY, eFromUserInput);
309 nsEventShell::FireEvent(event);
313 void AccessibleWrap::ExploreByTouch(float aX, float aY) {
314 a11y::Pivot pivot(RootAccessible());
315 TraversalRule rule;
317 Accessible* result = pivot.AtPoint(aX, aY, rule);
319 if (result && result != this) {
320 RefPtr<AccEvent> event =
321 new AccVCChangeEvent(result->Document(), this, -1, -1, result, -1, -1,
322 nsIAccessiblePivot::REASON_POINT,
323 nsIAccessiblePivot::NO_BOUNDARY, eFromUserInput);
324 nsEventShell::FireEvent(event);
328 void AccessibleWrap::NavigateText(int32_t aGranularity, int32_t aStartOffset,
329 int32_t aEndOffset, bool aForward,
330 bool aSelect) {
331 a11y::Pivot pivot(RootAccessible());
333 HyperTextAccessible* editable =
334 (State() & states::EDITABLE) != 0 ? AsHyperText() : nullptr;
336 int32_t start = aStartOffset, end = aEndOffset;
337 // If the accessible is an editable, set the virtual cursor position
338 // to its caret offset. Otherwise use the document's virtual cursor
339 // position as a starting offset.
340 if (editable) {
341 start = end = editable->CaretOffset();
344 uint16_t pivotGranularity = nsIAccessiblePivot::LINE_BOUNDARY;
345 switch (aGranularity) {
346 case 1: // MOVEMENT_GRANULARITY_CHARACTER
347 pivotGranularity = nsIAccessiblePivot::CHAR_BOUNDARY;
348 break;
349 case 2: // MOVEMENT_GRANULARITY_WORD
350 pivotGranularity = nsIAccessiblePivot::WORD_BOUNDARY;
351 break;
352 default:
353 break;
356 int32_t newOffset;
357 Accessible* newAnchor = nullptr;
358 if (aForward) {
359 newAnchor = pivot.NextText(this, &start, &end, pivotGranularity);
360 newOffset = end;
361 } else {
362 newAnchor = pivot.PrevText(this, &start, &end, pivotGranularity);
363 newOffset = start;
366 if (newAnchor && (start != aStartOffset || end != aEndOffset)) {
367 if (IsTextLeaf() && newAnchor == Parent()) {
368 // For paragraphs, divs, spans, etc., we put a11y focus on the text leaf
369 // node instead of the HyperTextAccessible. However, Pivot will always
370 // return a HyperTextAccessible. Android doesn't support text navigation
371 // landing on an accessible which is different to the originating
372 // accessible. Therefore, if we're still within the same text leaf,
373 // translate the offsets to the text leaf.
374 int32_t thisChild = IndexInParent();
375 HyperTextAccessible* newHyper = newAnchor->AsHyperText();
376 MOZ_ASSERT(newHyper);
377 int32_t startChild = newHyper->GetChildIndexAtOffset(start);
378 // We use end - 1 because the end offset is exclusive, so end itself
379 // might be associated with the next child.
380 int32_t endChild = newHyper->GetChildIndexAtOffset(end - 1);
381 if (startChild == thisChild && endChild == thisChild) {
382 // We've landed within the same text leaf.
383 newAnchor = this;
384 int32_t thisOffset = newHyper->GetChildOffset(thisChild);
385 start -= thisOffset;
386 end -= thisOffset;
389 RefPtr<AccEvent> event = new AccVCChangeEvent(
390 newAnchor->Document(), this, aStartOffset, aEndOffset, newAnchor, start,
391 end, nsIAccessiblePivot::REASON_NONE, pivotGranularity, eFromUserInput);
392 nsEventShell::FireEvent(event);
395 // If we are in an editable, move the caret to the new virtual cursor
396 // offset.
397 if (editable) {
398 if (aSelect) {
399 int32_t anchor = editable->CaretOffset();
400 if (editable->SelectionCount()) {
401 int32_t startSel, endSel;
402 GetSelectionOrCaret(&startSel, &endSel);
403 anchor = startSel == anchor ? endSel : startSel;
405 editable->SetSelectionBoundsAt(0, anchor, newOffset);
406 } else {
407 editable->SetCaretOffset(newOffset);
412 void AccessibleWrap::SetSelection(int32_t aStart, int32_t aEnd) {
413 if (HyperTextAccessible* textAcc = AsHyperText()) {
414 if (aStart == aEnd) {
415 textAcc->SetCaretOffset(aStart);
416 } else {
417 textAcc->SetSelectionBoundsAt(0, aStart, aEnd);
422 void AccessibleWrap::Cut() {
423 if ((State() & states::EDITABLE) == 0) {
424 return;
427 if (HyperTextAccessible* textAcc = AsHyperText()) {
428 int32_t startSel, endSel;
429 GetSelectionOrCaret(&startSel, &endSel);
430 textAcc->CutText(startSel, endSel);
434 void AccessibleWrap::Copy() {
435 if (HyperTextAccessible* textAcc = AsHyperText()) {
436 int32_t startSel, endSel;
437 GetSelectionOrCaret(&startSel, &endSel);
438 textAcc->CopyText(startSel, endSel);
442 void AccessibleWrap::Paste() {
443 if ((State() & states::EDITABLE) == 0) {
444 return;
447 if (IsHyperText()) {
448 RefPtr<HyperTextAccessible> textAcc = AsHyperText();
449 int32_t startSel, endSel;
450 GetSelectionOrCaret(&startSel, &endSel);
451 if (startSel != endSel) {
452 textAcc->DeleteText(startSel, endSel);
454 textAcc->PasteText(startSel);
458 void AccessibleWrap::GetSelectionOrCaret(int32_t* aStartOffset,
459 int32_t* aEndOffset) {
460 *aStartOffset = *aEndOffset = -1;
461 if (HyperTextAccessible* textAcc = AsHyperText()) {
462 if (!textAcc->SelectionBoundsAt(0, aStartOffset, aEndOffset)) {
463 *aStartOffset = *aEndOffset = textAcc->CaretOffset();
468 uint32_t AccessibleWrap::GetFlags(role aRole, uint64_t aState,
469 uint8_t aActionCount) {
470 uint32_t flags = 0;
471 if (aState & states::CHECKABLE) {
472 flags |= java::SessionAccessibility::FLAG_CHECKABLE;
475 if (aState & states::CHECKED) {
476 flags |= java::SessionAccessibility::FLAG_CHECKED;
479 if (aState & states::INVALID) {
480 flags |= java::SessionAccessibility::FLAG_CONTENT_INVALID;
483 if (aState & states::EDITABLE) {
484 flags |= java::SessionAccessibility::FLAG_EDITABLE;
487 if (aActionCount && aRole != roles::TEXT_LEAF) {
488 flags |= java::SessionAccessibility::FLAG_CLICKABLE;
491 if (aState & states::ENABLED) {
492 flags |= java::SessionAccessibility::FLAG_ENABLED;
495 if (aState & states::FOCUSABLE) {
496 flags |= java::SessionAccessibility::FLAG_FOCUSABLE;
499 if (aState & states::FOCUSED) {
500 flags |= java::SessionAccessibility::FLAG_FOCUSED;
503 if (aState & states::MULTI_LINE) {
504 flags |= java::SessionAccessibility::FLAG_MULTI_LINE;
507 if (aState & states::SELECTABLE) {
508 flags |= java::SessionAccessibility::FLAG_SELECTABLE;
511 if (aState & states::SELECTED) {
512 flags |= java::SessionAccessibility::FLAG_SELECTED;
515 if (aState & states::EXPANDABLE) {
516 flags |= java::SessionAccessibility::FLAG_EXPANDABLE;
519 if (aState & states::EXPANDED) {
520 flags |= java::SessionAccessibility::FLAG_EXPANDED;
523 if ((aState & (states::INVISIBLE | states::OFFSCREEN)) == 0) {
524 flags |= java::SessionAccessibility::FLAG_VISIBLE_TO_USER;
527 if (aRole == roles::PASSWORD_TEXT) {
528 flags |= java::SessionAccessibility::FLAG_PASSWORD;
531 return flags;
534 void AccessibleWrap::GetRoleDescription(role aRole,
535 nsIPersistentProperties* aAttributes,
536 nsAString& aGeckoRole,
537 nsAString& aRoleDescription) {
538 if (aRole == roles::HEADING && aAttributes) {
539 // The heading level is an attribute, so we need that.
540 AutoTArray<nsString, 1> formatString;
541 nsresult rv = aAttributes->GetStringProperty(NS_LITERAL_CSTRING("level"),
542 *formatString.AppendElement());
543 if (NS_SUCCEEDED(rv) &&
544 LocalizeString("headingLevel", aRoleDescription, formatString)) {
545 return;
549 if ((aRole == roles::LANDMARK || aRole == roles::REGION) && aAttributes) {
550 nsAutoString xmlRoles;
551 if (NS_SUCCEEDED(aAttributes->GetStringProperty(
552 NS_LITERAL_CSTRING("xml-roles"), xmlRoles))) {
553 nsWhitespaceTokenizer tokenizer(xmlRoles);
554 while (tokenizer.hasMoreTokens()) {
555 if (LocalizeString(NS_ConvertUTF16toUTF8(tokenizer.nextToken()).get(),
556 aRoleDescription)) {
557 return;
563 GetAccService()->GetStringRole(aRole, aGeckoRole);
564 LocalizeString(NS_ConvertUTF16toUTF8(aGeckoRole).get(), aRoleDescription);
567 already_AddRefed<nsIPersistentProperties>
568 AccessibleWrap::AttributeArrayToProperties(
569 const nsTArray<Attribute>& aAttributes) {
570 RefPtr<nsPersistentProperties> props = new nsPersistentProperties();
571 nsAutoString unused;
573 for (size_t i = 0; i < aAttributes.Length(); i++) {
574 props->SetStringProperty(aAttributes.ElementAt(i).Name(),
575 aAttributes.ElementAt(i).Value(), unused);
578 return props.forget();
581 int32_t AccessibleWrap::GetAndroidClass(role aRole) {
582 #define ROLE(geckoRole, stringRole, atkRole, macRole, msaaRole, ia2Role, \
583 androidClass, nameRule) \
584 case roles::geckoRole: \
585 return androidClass;
587 switch (aRole) {
588 #include "RoleMap.h"
589 default:
590 return java::SessionAccessibility::CLASSNAME_VIEW;
593 #undef ROLE
596 int32_t AccessibleWrap::GetInputType(const nsString& aInputTypeAttr) {
597 if (aInputTypeAttr.EqualsIgnoreCase("email")) {
598 return java::sdk::InputType::TYPE_CLASS_TEXT |
599 java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
602 if (aInputTypeAttr.EqualsIgnoreCase("number")) {
603 return java::sdk::InputType::TYPE_CLASS_NUMBER;
606 if (aInputTypeAttr.EqualsIgnoreCase("password")) {
607 return java::sdk::InputType::TYPE_CLASS_TEXT |
608 java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_PASSWORD;
611 if (aInputTypeAttr.EqualsIgnoreCase("tel")) {
612 return java::sdk::InputType::TYPE_CLASS_PHONE;
615 if (aInputTypeAttr.EqualsIgnoreCase("text")) {
616 return java::sdk::InputType::TYPE_CLASS_TEXT |
617 java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
620 if (aInputTypeAttr.EqualsIgnoreCase("url")) {
621 return java::sdk::InputType::TYPE_CLASS_TEXT |
622 java::sdk::InputType::TYPE_TEXT_VARIATION_URI;
625 return 0;
628 void AccessibleWrap::WrapperDOMNodeID(nsString& aDOMNodeID) {
629 if (mContent) {
630 nsAtom* id = mContent->GetID();
631 if (id) {
632 id->ToString(aDOMNodeID);
637 bool AccessibleWrap::WrapperRangeInfo(double* aCurVal, double* aMinVal,
638 double* aMaxVal, double* aStep) {
639 if (HasNumericValue()) {
640 *aCurVal = CurValue();
641 *aMinVal = MinValue();
642 *aMaxVal = MaxValue();
643 *aStep = Step();
644 return true;
647 return false;
650 mozilla::java::GeckoBundle::LocalRef AccessibleWrap::ToBundle(bool aSmall) {
651 nsAutoString name;
652 Name(name);
653 nsAutoString textValue;
654 Value(textValue);
655 nsAutoString nodeID;
656 WrapperDOMNodeID(nodeID);
657 nsAutoString description;
658 Description(description);
660 if (aSmall) {
661 return ToBundle(State(), Bounds(), ActionCount(), name, textValue, nodeID,
662 description);
665 double curValue = UnspecifiedNaN<double>();
666 double minValue = UnspecifiedNaN<double>();
667 double maxValue = UnspecifiedNaN<double>();
668 double step = UnspecifiedNaN<double>();
669 WrapperRangeInfo(&curValue, &minValue, &maxValue, &step);
671 nsCOMPtr<nsIPersistentProperties> attributes = Attributes();
673 return ToBundle(State(), Bounds(), ActionCount(), name, textValue, nodeID,
674 description, curValue, minValue, maxValue, step, attributes);
677 mozilla::java::GeckoBundle::LocalRef AccessibleWrap::ToBundle(
678 const uint64_t aState, const nsIntRect& aBounds, const uint8_t aActionCount,
679 const nsString& aName, const nsString& aTextValue,
680 const nsString& aDOMNodeID, const nsString& aDescription,
681 const double& aCurVal, const double& aMinVal, const double& aMaxVal,
682 const double& aStep, nsIPersistentProperties* aAttributes) {
683 if (!IsProxy() && IsDefunct()) {
684 return nullptr;
687 GECKOBUNDLE_START(nodeInfo);
688 GECKOBUNDLE_PUT(nodeInfo, "id", java::sdk::Integer::ValueOf(VirtualViewID()));
690 AccessibleWrap* parent = WrapperParent();
691 GECKOBUNDLE_PUT(
692 nodeInfo, "parentId",
693 java::sdk::Integer::ValueOf(parent ? parent->VirtualViewID() : 0));
695 role role = WrapperRole();
696 if (role == roles::LINK && !(aState & states::LINKED)) {
697 // A link without the linked state (<a> with no href) shouldn't be presented
698 // as a link.
699 role = roles::TEXT;
702 uint32_t flags = GetFlags(role, aState, aActionCount);
703 GECKOBUNDLE_PUT(nodeInfo, "flags", java::sdk::Integer::ValueOf(flags));
704 GECKOBUNDLE_PUT(nodeInfo, "className",
705 java::sdk::Integer::ValueOf(AndroidClass()));
707 nsAutoString hint;
708 if (aState & states::EDITABLE) {
709 // An editable field's name is populated in the hint.
710 hint.Assign(aName);
711 GECKOBUNDLE_PUT(nodeInfo, "text", jni::StringParam(aTextValue));
712 } else {
713 if (role == roles::LINK || role == roles::HEADING) {
714 GECKOBUNDLE_PUT(nodeInfo, "description", jni::StringParam(aName));
715 } else {
716 GECKOBUNDLE_PUT(nodeInfo, "text", jni::StringParam(aName));
720 if (!aDescription.IsEmpty()) {
721 if (!hint.IsEmpty()) {
722 // If this is an editable, the description is concatenated with a
723 // whitespace directly after the name.
724 hint.AppendLiteral(" ");
726 hint.Append(aDescription);
729 if ((aState & states::REQUIRED) != 0) {
730 nsAutoString requiredString;
731 if (LocalizeString("stateRequired", requiredString)) {
732 if (!hint.IsEmpty()) {
733 // If the hint is non-empty, concatenate with a comma for a brief pause.
734 hint.AppendLiteral(", ");
736 hint.Append(requiredString);
740 if (!hint.IsEmpty()) {
741 GECKOBUNDLE_PUT(nodeInfo, "hint", jni::StringParam(hint));
744 nsAutoString geckoRole;
745 nsAutoString roleDescription;
746 if (VirtualViewID() != kNoID) {
747 GetRoleDescription(role, aAttributes, geckoRole, roleDescription);
750 GECKOBUNDLE_PUT(nodeInfo, "roleDescription",
751 jni::StringParam(roleDescription));
752 GECKOBUNDLE_PUT(nodeInfo, "geckoRole", jni::StringParam(geckoRole));
754 if (!aDOMNodeID.IsEmpty()) {
755 GECKOBUNDLE_PUT(nodeInfo, "viewIdResourceName",
756 jni::StringParam(aDOMNodeID));
759 const int32_t data[4] = {aBounds.x, aBounds.y, aBounds.x + aBounds.width,
760 aBounds.y + aBounds.height};
761 GECKOBUNDLE_PUT(nodeInfo, "bounds", jni::IntArray::New(data, 4));
763 if (HasNumericValue()) {
764 GECKOBUNDLE_START(rangeInfo);
765 if (aMaxVal == 1 && aMinVal == 0) {
766 GECKOBUNDLE_PUT(rangeInfo, "type",
767 java::sdk::Integer::ValueOf(2)); // percent
768 } else if (std::round(aStep) != aStep) {
769 GECKOBUNDLE_PUT(rangeInfo, "type",
770 java::sdk::Integer::ValueOf(1)); // float
771 } else {
772 GECKOBUNDLE_PUT(rangeInfo, "type",
773 java::sdk::Integer::ValueOf(0)); // integer
776 if (!IsNaN(aCurVal)) {
777 GECKOBUNDLE_PUT(rangeInfo, "current", java::sdk::Double::New(aCurVal));
779 if (!IsNaN(aMinVal)) {
780 GECKOBUNDLE_PUT(rangeInfo, "min", java::sdk::Double::New(aMinVal));
782 if (!IsNaN(aMaxVal)) {
783 GECKOBUNDLE_PUT(rangeInfo, "max", java::sdk::Double::New(aMaxVal));
786 GECKOBUNDLE_FINISH(rangeInfo);
787 GECKOBUNDLE_PUT(nodeInfo, "rangeInfo", rangeInfo);
790 if (aAttributes) {
791 nsString inputTypeAttr;
792 nsAccUtils::GetAccAttr(aAttributes, nsGkAtoms::textInputType,
793 inputTypeAttr);
794 int32_t inputType = GetInputType(inputTypeAttr);
795 if (inputType) {
796 GECKOBUNDLE_PUT(nodeInfo, "inputType",
797 java::sdk::Integer::ValueOf(inputType));
800 nsString posinset;
801 nsresult rv = aAttributes->GetStringProperty(NS_LITERAL_CSTRING("posinset"),
802 posinset);
803 if (NS_SUCCEEDED(rv)) {
804 int32_t rowIndex;
805 if (sscanf(NS_ConvertUTF16toUTF8(posinset).get(), "%d", &rowIndex) > 0) {
806 GECKOBUNDLE_START(collectionItemInfo);
807 GECKOBUNDLE_PUT(collectionItemInfo, "rowIndex",
808 java::sdk::Integer::ValueOf(rowIndex));
809 GECKOBUNDLE_PUT(collectionItemInfo, "columnIndex",
810 java::sdk::Integer::ValueOf(0));
811 GECKOBUNDLE_PUT(collectionItemInfo, "rowSpan",
812 java::sdk::Integer::ValueOf(1));
813 GECKOBUNDLE_PUT(collectionItemInfo, "columnSpan",
814 java::sdk::Integer::ValueOf(1));
815 GECKOBUNDLE_FINISH(collectionItemInfo);
817 GECKOBUNDLE_PUT(nodeInfo, "collectionItemInfo", collectionItemInfo);
821 nsString colSize;
822 rv = aAttributes->GetStringProperty(NS_LITERAL_CSTRING("child-item-count"),
823 colSize);
824 if (NS_SUCCEEDED(rv)) {
825 int32_t rowCount;
826 if (sscanf(NS_ConvertUTF16toUTF8(colSize).get(), "%d", &rowCount) > 0) {
827 GECKOBUNDLE_START(collectionInfo);
828 GECKOBUNDLE_PUT(collectionInfo, "rowCount",
829 java::sdk::Integer::ValueOf(rowCount));
830 GECKOBUNDLE_PUT(collectionInfo, "columnCount",
831 java::sdk::Integer::ValueOf(1));
833 nsString unused;
834 rv = aAttributes->GetStringProperty(NS_LITERAL_CSTRING("hierarchical"),
835 unused);
836 if (NS_SUCCEEDED(rv)) {
837 GECKOBUNDLE_PUT(collectionInfo, "isHierarchical",
838 java::sdk::Boolean::TRUE());
841 if (IsSelect()) {
842 int32_t selectionMode = (aState & states::MULTISELECTABLE) ? 2 : 1;
843 GECKOBUNDLE_PUT(collectionInfo, "selectionMode",
844 java::sdk::Integer::ValueOf(selectionMode));
847 GECKOBUNDLE_FINISH(collectionInfo);
848 GECKOBUNDLE_PUT(nodeInfo, "collectionInfo", collectionInfo);
853 bool mustPrune =
854 IsProxy() ? nsAccUtils::MustPrune(Proxy()) : nsAccUtils::MustPrune(this);
855 if (!mustPrune) {
856 auto childCount = ChildCount();
857 nsTArray<int32_t> children(childCount);
858 for (uint32_t i = 0; i < childCount; i++) {
859 auto child = static_cast<AccessibleWrap*>(GetChildAt(i));
860 children.AppendElement(child->VirtualViewID());
863 GECKOBUNDLE_PUT(nodeInfo, "children",
864 jni::IntArray::New(children.Elements(), children.Length()));
867 GECKOBUNDLE_FINISH(nodeInfo);
869 return nodeInfo;
872 void AccessibleWrap::GetTextEquiv(nsString& aText) {
873 if (nsTextEquivUtils::HasNameRule(this, eNameFromSubtreeIfReqRule)) {
874 // This is an accessible that normally doesn't get its name from its
875 // subtree, so we collect the text equivalent explicitly.
876 nsTextEquivUtils::GetTextEquivFromSubtree(this, aText);
877 } else {
878 Name(aText);
882 bool AccessibleWrap::HandleLiveRegionEvent(AccEvent* aEvent) {
883 auto eventType = aEvent->GetEventType();
884 if (eventType != nsIAccessibleEvent::EVENT_TEXT_INSERTED &&
885 eventType != nsIAccessibleEvent::EVENT_NAME_CHANGE) {
886 // XXX: Right now only announce text inserted events. aria-relevant=removals
887 // is potentially on the chopping block[1]. We also don't support editable
888 // text because we currently can't descern the source of the change[2].
889 // 1. https://github.com/w3c/aria/issues/712
890 // 2. https://bugzilla.mozilla.org/show_bug.cgi?id=1531189
891 return false;
894 if (aEvent->IsFromUserInput()) {
895 return false;
898 nsCOMPtr<nsIPersistentProperties> attributes = Attributes();
899 nsString live;
900 nsresult rv =
901 attributes->GetStringProperty(NS_LITERAL_CSTRING("container-live"), live);
902 if (!NS_SUCCEEDED(rv)) {
903 return false;
906 uint16_t priority = live.EqualsIgnoreCase("assertive")
907 ? nsIAccessibleAnnouncementEvent::ASSERTIVE
908 : nsIAccessibleAnnouncementEvent::POLITE;
910 nsString atomic;
911 rv = attributes->GetStringProperty(NS_LITERAL_CSTRING("container-atomic"),
912 atomic);
914 Accessible* announcementTarget = this;
915 nsAutoString announcement;
916 if (atomic.EqualsIgnoreCase("true")) {
917 Accessible* atomicAncestor = nullptr;
918 for (Accessible* parent = announcementTarget; parent;
919 parent = parent->Parent()) {
920 dom::Element* element = parent->Elm();
921 if (element &&
922 element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_atomic,
923 nsGkAtoms::_true, eCaseMatters)) {
924 atomicAncestor = parent;
925 break;
929 if (atomicAncestor) {
930 announcementTarget = atomicAncestor;
931 static_cast<AccessibleWrap*>(atomicAncestor)->GetTextEquiv(announcement);
933 } else {
934 GetTextEquiv(announcement);
937 announcement.CompressWhitespace();
938 if (announcement.IsEmpty()) {
939 return false;
942 announcementTarget->Announce(announcement, priority);
943 return true;