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 "nsIAccessiblePivot.h"
24 #include "nsAccUtils.h"
25 #include "nsTextEquivUtils.h"
26 #include "nsWhitespaceTokenizer.h"
27 #include "RootAccessible.h"
28 #include "TextLeafRange.h"
30 #include "mozilla/a11y/PDocAccessibleChild.h"
31 #include "mozilla/jni/GeckoBundleUtils.h"
32 #include "mozilla/a11y/DocAccessibleParent.h"
33 #include "mozilla/Maybe.h"
35 // icu TRUE conflicting with java::sdk::Boolean::TRUE()
36 // https://searchfox.org/mozilla-central/rev/ce02064d8afc8673cef83c92896ee873bd35e7ae/intl/icu/source/common/unicode/umachine.h#265
37 // https://searchfox.org/mozilla-central/source/__GENERATED__/widget/android/bindings/JavaBuiltins.h#78
42 using namespace mozilla::a11y
;
45 //-----------------------------------------------------
47 //-----------------------------------------------------
48 AccessibleWrap::AccessibleWrap(nsIContent
* aContent
, DocAccessible
* aDoc
)
49 : LocalAccessible(aContent
, aDoc
), mID(SessionAccessibility::kUnsetID
) {
50 if (!IPCAccessibilityActive()) {
51 MonitorAutoLock
mal(nsAccessibilityService::GetAndroidMonitor());
52 SessionAccessibility::RegisterAccessible(this);
56 //-----------------------------------------------------
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
);
65 nsresult rv
= LocalAccessible::HandleAccEvent(aEvent
);
66 NS_ENSURE_SUCCESS(rv
, rv
);
68 accessible
->HandleLiveRegionEvent(aEvent
);
73 void AccessibleWrap::Shutdown() {
74 if (!IPCAccessibilityActive()) {
75 MonitorAutoLock
mal(nsAccessibilityService::GetAndroidMonitor());
76 SessionAccessibility::UnregisterAccessible(this);
78 LocalAccessible::Shutdown();
81 bool AccessibleWrap::DoAction(uint8_t aIndex
) const {
83 return LocalAccessible::DoAction(aIndex
);
87 // We still simulate a click on an accessible even if there is no
88 // known actions. For the sake of bad markup.
96 Accessible
* AccessibleWrap::DoPivot(Accessible
* aAccessible
,
97 int32_t aGranularity
, bool aForward
,
99 Accessible
* pivotRoot
= nullptr;
100 if (aAccessible
->IsRemote()) {
101 // If this is a remote accessible provide the top level
102 // remote doc as the pivot root for thread safety reasons.
103 DocAccessibleParent
* doc
= aAccessible
->AsRemote()->Document();
104 while (doc
&& !doc
->IsTopLevel()) {
105 doc
= doc
->ParentDoc();
107 MOZ_ASSERT(doc
, "Failed to get top level DocAccessibleParent");
110 a11y::Pivot
pivot(pivotRoot
);
111 // Depending on the start accessible, the pivot rule will either traverse
112 // local or remote accessibles exclusively.
113 TraversalRule
rule(aGranularity
, aAccessible
->IsLocal());
114 Accessible
* result
= aForward
? pivot
.Next(aAccessible
, rule
, aInclusive
)
115 : pivot
.Prev(aAccessible
, rule
, aInclusive
);
117 if (result
&& (result
!= aAccessible
|| aInclusive
)) {
124 Accessible
* AccessibleWrap::ExploreByTouch(Accessible
* aAccessible
, float aX
,
127 if (LocalAccessible
* local
= aAccessible
->AsLocal()) {
128 root
= local
->RootAccessible();
130 // If this is a RemoteAccessible, provide the top level
131 // remote doc as the pivot root for thread safety reasons.
132 DocAccessibleParent
* doc
= aAccessible
->AsRemote()->Document();
133 while (doc
&& !doc
->IsTopLevel()) {
134 doc
= doc
->ParentDoc();
136 MOZ_ASSERT(doc
, "Failed to get top level DocAccessibleParent");
139 a11y::Pivot
pivot(root
);
140 TraversalRule
rule(java::SessionAccessibility::HTML_GRANULARITY_DEFAULT
,
141 aAccessible
->IsLocal());
142 Accessible
* result
= pivot
.AtPoint(aX
, aY
, rule
);
143 if (result
== aAccessible
) {
149 static TextLeafPoint
ToTextLeafPoint(Accessible
* aAccessible
, int32_t aOffset
) {
150 if (HyperTextAccessibleBase
* ht
= aAccessible
->AsHyperTextBase()) {
151 return ht
->ToTextLeafPoint(aOffset
);
154 return TextLeafPoint(aAccessible
, aOffset
);
157 Maybe
<std::pair
<int32_t, int32_t>> AccessibleWrap::NavigateText(
158 Accessible
* aAccessible
, int32_t aGranularity
, int32_t aStartOffset
,
159 int32_t aEndOffset
, bool aForward
, bool aSelect
) {
160 int32_t startOffset
= aStartOffset
;
161 int32_t endOffset
= aEndOffset
;
162 if (startOffset
== -1) {
163 MOZ_ASSERT(endOffset
== -1,
164 "When start offset is unset, end offset should be too");
165 startOffset
= aForward
? 0 : nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
;
166 endOffset
= aForward
? 0 : nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
;
169 // If the accessible is an editable, set the virtual cursor position
170 // to its caret offset. Otherwise use the document's virtual cursor
171 // position as a starting offset.
172 if (aAccessible
->State() & states::EDITABLE
) {
173 startOffset
= endOffset
= aAccessible
->AsHyperTextBase()->CaretOffset();
176 TextLeafRange currentRange
=
177 TextLeafRange(ToTextLeafPoint(aAccessible
, startOffset
),
178 ToTextLeafPoint(aAccessible
, endOffset
));
179 uint16_t startBoundaryType
= nsIAccessibleText::BOUNDARY_LINE_START
;
180 uint16_t endBoundaryType
= nsIAccessibleText::BOUNDARY_LINE_END
;
181 switch (aGranularity
) {
182 case 1: // MOVEMENT_GRANULARITY_CHARACTER
183 startBoundaryType
= nsIAccessibleText::BOUNDARY_CHAR
;
184 endBoundaryType
= nsIAccessibleText::BOUNDARY_CHAR
;
186 case 2: // MOVEMENT_GRANULARITY_WORD
187 startBoundaryType
= nsIAccessibleText::BOUNDARY_WORD_START
;
188 endBoundaryType
= nsIAccessibleText::BOUNDARY_WORD_END
;
194 TextLeafRange resultRange
;
198 currentRange
.End().FindBoundary(endBoundaryType
, eDirNext
));
199 resultRange
.SetStart(
200 resultRange
.End().FindBoundary(startBoundaryType
, eDirPrevious
));
202 resultRange
.SetStart(
203 currentRange
.Start().FindBoundary(startBoundaryType
, eDirPrevious
));
205 resultRange
.Start().FindBoundary(endBoundaryType
, eDirNext
));
208 if (!resultRange
.Crop(aAccessible
)) {
209 // If the new range does not intersect at all with the given
210 // accessible/container this navigation has failed or reached an edge.
214 if (resultRange
== currentRange
|| resultRange
.Start() == resultRange
.End()) {
215 // If the result range equals the current range, or if the result range is
216 // collapsed, we failed or reached an edge.
220 if (HyperTextAccessibleBase
* ht
= aAccessible
->AsHyperTextBase()) {
221 DebugOnly
<bool> ok
= false;
222 std::tie(ok
, startOffset
) = ht
->TransformOffset(
223 resultRange
.Start().mAcc
, resultRange
.Start().mOffset
, false);
224 MOZ_ASSERT(ok
, "Accessible of range start should be in container.");
226 std::tie(ok
, endOffset
) = ht
->TransformOffset(
227 resultRange
.End().mAcc
, resultRange
.End().mOffset
, false);
228 MOZ_ASSERT(ok
, "Accessible range end should be in container.");
230 startOffset
= resultRange
.Start().mOffset
;
231 endOffset
= resultRange
.End().mOffset
;
234 return Some(std::make_pair(startOffset
, endOffset
));
237 uint32_t AccessibleWrap::GetFlags(role aRole
, uint64_t aState
,
238 uint8_t aActionCount
) {
240 if (aState
& states::CHECKABLE
) {
241 flags
|= java::SessionAccessibility::FLAG_CHECKABLE
;
244 if (aState
& states::CHECKED
) {
245 flags
|= java::SessionAccessibility::FLAG_CHECKED
;
248 if (aState
& states::INVALID
) {
249 flags
|= java::SessionAccessibility::FLAG_CONTENT_INVALID
;
252 if (aState
& states::EDITABLE
) {
253 flags
|= java::SessionAccessibility::FLAG_EDITABLE
;
256 if (aActionCount
&& aRole
!= roles::TEXT_LEAF
) {
257 flags
|= java::SessionAccessibility::FLAG_CLICKABLE
;
260 if (aState
& states::ENABLED
) {
261 flags
|= java::SessionAccessibility::FLAG_ENABLED
;
264 if (aState
& states::FOCUSABLE
) {
265 flags
|= java::SessionAccessibility::FLAG_FOCUSABLE
;
268 if (aState
& states::FOCUSED
) {
269 flags
|= java::SessionAccessibility::FLAG_FOCUSED
;
272 if (aState
& states::MULTI_LINE
) {
273 flags
|= java::SessionAccessibility::FLAG_MULTI_LINE
;
276 if (aState
& states::SELECTABLE
) {
277 flags
|= java::SessionAccessibility::FLAG_SELECTABLE
;
280 if (aState
& states::SELECTED
) {
281 flags
|= java::SessionAccessibility::FLAG_SELECTED
;
284 if (aState
& states::EXPANDABLE
) {
285 flags
|= java::SessionAccessibility::FLAG_EXPANDABLE
;
288 if (aState
& states::EXPANDED
) {
289 flags
|= java::SessionAccessibility::FLAG_EXPANDED
;
292 if ((aState
& (states::INVISIBLE
| states::OFFSCREEN
)) == 0) {
293 flags
|= java::SessionAccessibility::FLAG_VISIBLE_TO_USER
;
296 if (aRole
== roles::PASSWORD_TEXT
) {
297 flags
|= java::SessionAccessibility::FLAG_PASSWORD
;
303 void AccessibleWrap::GetRoleDescription(role aRole
, AccAttributes
* aAttributes
,
304 nsAString
& aGeckoRole
,
305 nsAString
& aRoleDescription
) {
306 if (aRole
== roles::HEADING
&& aAttributes
) {
307 // The heading level is an attribute, so we need that.
308 nsAutoString headingLevel
;
309 if (aAttributes
->GetAttribute(nsGkAtoms::level
, headingLevel
)) {
310 nsAutoString
token(u
"heading-");
311 token
.Append(headingLevel
);
312 if (LocalizeString(token
, aRoleDescription
)) {
318 if ((aRole
== roles::LANDMARK
|| aRole
== roles::REGION
) && aAttributes
) {
319 nsAutoString xmlRoles
;
320 if (aAttributes
->GetAttribute(nsGkAtoms::xmlroles
, xmlRoles
)) {
321 nsWhitespaceTokenizer
tokenizer(xmlRoles
);
322 while (tokenizer
.hasMoreTokens()) {
323 if (LocalizeString(tokenizer
.nextToken(), aRoleDescription
)) {
330 GetAccService()->GetStringRole(aRole
, aGeckoRole
);
331 LocalizeString(aGeckoRole
, aRoleDescription
);
334 int32_t AccessibleWrap::AndroidClass(Accessible
* aAccessible
) {
335 return GetVirtualViewID(aAccessible
) == SessionAccessibility::kNoID
336 ? java::SessionAccessibility::CLASSNAME_WEBVIEW
337 : GetAndroidClass(aAccessible
->Role());
340 int32_t AccessibleWrap::GetVirtualViewID(Accessible
* aAccessible
) {
341 if (aAccessible
->IsLocal()) {
342 return static_cast<AccessibleWrap
*>(aAccessible
)->mID
;
345 return static_cast<int32_t>(aAccessible
->AsRemote()->GetWrapper());
348 void AccessibleWrap::SetVirtualViewID(Accessible
* aAccessible
,
349 int32_t aVirtualViewID
) {
350 if (aAccessible
->IsLocal()) {
351 static_cast<AccessibleWrap
*>(aAccessible
)->mID
= aVirtualViewID
;
353 aAccessible
->AsRemote()->SetWrapper(static_cast<uintptr_t>(aVirtualViewID
));
357 int32_t AccessibleWrap::GetAndroidClass(role aRole
) {
358 #define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
359 msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \
361 case roles::geckoRole: \
367 return java::SessionAccessibility::CLASSNAME_VIEW
;
373 int32_t AccessibleWrap::GetInputType(const nsString
& aInputTypeAttr
) {
374 if (aInputTypeAttr
.EqualsIgnoreCase("email")) {
375 return java::sdk::InputType::TYPE_CLASS_TEXT
|
376 java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
;
379 if (aInputTypeAttr
.EqualsIgnoreCase("number")) {
380 return java::sdk::InputType::TYPE_CLASS_NUMBER
;
383 if (aInputTypeAttr
.EqualsIgnoreCase("password")) {
384 return java::sdk::InputType::TYPE_CLASS_TEXT
|
385 java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_PASSWORD
;
388 if (aInputTypeAttr
.EqualsIgnoreCase("tel")) {
389 return java::sdk::InputType::TYPE_CLASS_PHONE
;
392 if (aInputTypeAttr
.EqualsIgnoreCase("text")) {
393 return java::sdk::InputType::TYPE_CLASS_TEXT
|
394 java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EDIT_TEXT
;
397 if (aInputTypeAttr
.EqualsIgnoreCase("url")) {
398 return java::sdk::InputType::TYPE_CLASS_TEXT
|
399 java::sdk::InputType::TYPE_TEXT_VARIATION_URI
;
405 void AccessibleWrap::GetTextEquiv(nsString
& aText
) {
406 if (nsTextEquivUtils::HasNameRule(this, eNameFromSubtreeIfReqRule
)) {
407 // This is an accessible that normally doesn't get its name from its
408 // subtree, so we collect the text equivalent explicitly.
409 nsTextEquivUtils::GetTextEquivFromSubtree(this, aText
);
415 bool AccessibleWrap::HandleLiveRegionEvent(AccEvent
* aEvent
) {
416 auto eventType
= aEvent
->GetEventType();
417 if (eventType
!= nsIAccessibleEvent::EVENT_TEXT_INSERTED
&&
418 eventType
!= nsIAccessibleEvent::EVENT_NAME_CHANGE
) {
419 // XXX: Right now only announce text inserted events. aria-relevant=removals
420 // is potentially on the chopping block[1]. We also don't support editable
421 // text because we currently can't descern the source of the change[2].
422 // 1. https://github.com/w3c/aria/issues/712
423 // 2. https://bugzilla.mozilla.org/show_bug.cgi?id=1531189
427 if (aEvent
->IsFromUserInput()) {
431 RefPtr
<AccAttributes
> attributes
= new AccAttributes();
432 nsAccUtils::SetLiveContainerAttributes(attributes
, this);
434 if (!attributes
->GetAttribute(nsGkAtoms::containerLive
, live
)) {
438 uint16_t priority
= live
.EqualsIgnoreCase("assertive")
439 ? nsIAccessibleAnnouncementEvent::ASSERTIVE
440 : nsIAccessibleAnnouncementEvent::POLITE
;
443 attributes
->GetAttribute
<bool>(nsGkAtoms::containerAtomic
);
444 LocalAccessible
* announcementTarget
= this;
445 nsAutoString announcement
;
446 if (atomic
&& *atomic
) {
447 LocalAccessible
* atomicAncestor
= nullptr;
448 for (LocalAccessible
* parent
= announcementTarget
; parent
;
449 parent
= parent
->LocalParent()) {
450 dom::Element
* element
= parent
->Elm();
452 nsAccUtils::ARIAAttrValueIs(element
, nsGkAtoms::aria_atomic
,
453 nsGkAtoms::_true
, eCaseMatters
)) {
454 atomicAncestor
= parent
;
459 if (atomicAncestor
) {
460 announcementTarget
= atomicAncestor
;
461 static_cast<AccessibleWrap
*>(atomicAncestor
)->GetTextEquiv(announcement
);
464 GetTextEquiv(announcement
);
467 announcement
.CompressWhitespace();
468 if (announcement
.IsEmpty()) {
472 announcementTarget
->Announce(announcement
, priority
);