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"
13 #include "AndroidInputType.h"
14 #include "DocAccessibleWrap.h"
15 #include "SessionAccessibility.h"
16 #include "TextLeafAccessible.h"
17 #include "TraversalRule.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
41 using namespace mozilla::a11y
;
44 //-----------------------------------------------------
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 //-----------------------------------------------------
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());
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())) {
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,
86 case nsIAccessibleEvent::EVENT_SCROLLING_START
: {
88 java::SessionAccessibility::HTML_GRANULARITY_DEFAULT
, true, true);
96 nsresult rv
= LocalAccessible::HandleAccEvent(aEvent
);
97 NS_ENSURE_SUCCESS(rv
, rv
);
99 accessible
->HandleLiveRegionEvent(aEvent
);
101 if (IPCAccessibilityActive()) {
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()) {
112 if (!doc
->DocumentNode()->IsContentDocument()) {
117 RefPtr
<SessionAccessibility
> sessionAcc
=
118 SessionAccessibility::GetInstanceFor(accessible
);
123 switch (aEvent
->GetEventType()) {
124 case nsIAccessibleEvent::EVENT_FOCUS
:
125 sessionAcc
->SendFocusEvent(accessible
);
127 case nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED
: {
128 AccVCChangeEvent
* vcEvent
= downcast_accEvent(aEvent
);
129 if (!vcEvent
->IsFromUserInput()) {
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
);
139 sessionAcc
->SendAccessibilityFocusedEvent(newPosition
);
144 case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED
: {
145 AccCaretMoveEvent
* event
= downcast_accEvent(aEvent
);
146 sessionAcc
->SendTextSelectionChangedEvent(accessible
,
147 event
->GetCaretOffset());
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());
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
170 if (state
& states::EXPANDED
) {
171 sessionAcc
->SendClickedEvent(
172 accessible
, java::SessionAccessibility::FLAG_EXPANDABLE
|
173 (event
->IsStateEnabled()
174 ? java::SessionAccessibility::FLAG_EXPANDED
178 if (state
& states::SELECTED
) {
179 sessionAcc
->SendSelectedEvent(accessible
, event
->IsStateEnabled());
182 if (state
& states::BUSY
) {
183 sessionAcc
->SendWindowStateChangedEvent(accessible
);
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());
194 case nsIAccessibleEvent::EVENT_ANNOUNCEMENT
: {
195 AccAnnouncementEvent
* event
= downcast_accEvent(aEvent
);
196 sessionAcc
->SendAnnouncementEvent(accessible
, event
->Announcement(),
200 case nsIAccessibleEvent::EVENT_REORDER
: {
201 sessionAcc
->SendWindowContentChangedEvent();
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 {
221 return LocalAccessible::DoAction(aIndex
);
225 // We still simulate a click on an accessible even if there is no
226 // known actions. For the sake of bad markup.
234 Accessible
* AccessibleWrap::DoPivot(Accessible
* aAccessible
,
235 int32_t aGranularity
, bool aForward
,
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");
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
)) {
262 bool AccessibleWrap::PivotTo(int32_t aGranularity
, bool aForward
,
264 Accessible
* result
= DoPivot(this, aGranularity
, aForward
, aInclusive
);
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
);
282 void AccessibleWrap::ExploreByTouch(float aX
, float aY
) {
283 a11y::Pivot
pivot(RootAccessible());
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
;
334 case 2: // MOVEMENT_GRANULARITY_WORD
335 startBoundaryType
= nsIAccessibleText::BOUNDARY_WORD_START
;
336 endBoundaryType
= nsIAccessibleText::BOUNDARY_WORD_END
;
342 TextLeafRange resultRange
;
346 currentRange
.End().FindBoundary(endBoundaryType
, eDirNext
));
347 resultRange
.SetStart(
348 resultRange
.End().FindBoundary(startBoundaryType
, eDirPrevious
));
350 resultRange
.SetStart(
351 currentRange
.Start().FindBoundary(startBoundaryType
, eDirPrevious
));
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.
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.
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.");
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
) {
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
;
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
)) {
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
)) {
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
;
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: \
514 return java::SessionAccessibility::CLASSNAME_VIEW
;
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
;
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
);
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
574 if (aEvent
->IsFromUserInput()) {
578 RefPtr
<AccAttributes
> attributes
= new AccAttributes();
579 nsAccUtils::SetLiveContainerAttributes(attributes
, this);
581 if (!attributes
->GetAttribute(nsGkAtoms::containerLive
, live
)) {
585 uint16_t priority
= live
.EqualsIgnoreCase("assertive")
586 ? nsIAccessibleAnnouncementEvent::ASSERTIVE
587 : nsIAccessibleAnnouncementEvent::POLITE
;
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();
599 nsAccUtils::ARIAAttrValueIs(element
, nsGkAtoms::aria_atomic
,
600 nsGkAtoms::_true
, eCaseMatters
)) {
601 atomicAncestor
= parent
;
606 if (atomicAncestor
) {
607 announcementTarget
= atomicAncestor
;
608 static_cast<AccessibleWrap
*>(atomicAncestor
)->GetTextEquiv(announcement
);
611 GetTextEquiv(announcement
);
614 announcement
.CompressWhitespace();
615 if (announcement
.IsEmpty()) {
619 announcementTarget
->Announce(announcement
, priority
);