no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / accessible / android / AccessibleWrap.cpp
blob5b69866c05037722a6a08ecc9a990b9904ffc1f7
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 "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
38 #ifdef TRUE
39 # undef TRUE
40 #endif
42 using namespace mozilla::a11y;
43 using mozilla::Maybe;
45 //-----------------------------------------------------
46 // construction
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 //-----------------------------------------------------
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);
65 nsresult rv = LocalAccessible::HandleAccEvent(aEvent);
66 NS_ENSURE_SUCCESS(rv, rv);
68 accessible->HandleLiveRegionEvent(aEvent);
70 return NS_OK;
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 {
82 if (ActionCount()) {
83 return LocalAccessible::DoAction(aIndex);
86 if (mContent) {
87 // We still simulate a click on an accessible even if there is no
88 // known actions. For the sake of bad markup.
89 DoCommand();
90 return true;
93 return false;
96 Accessible* AccessibleWrap::DoPivot(Accessible* aAccessible,
97 int32_t aGranularity, bool aForward,
98 bool aInclusive) {
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");
108 pivotRoot = doc;
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)) {
118 return result;
121 return nullptr;
124 Accessible* AccessibleWrap::ExploreByTouch(Accessible* aAccessible, float aX,
125 float aY) {
126 Accessible* root;
127 if (LocalAccessible* local = aAccessible->AsLocal()) {
128 root = local->RootAccessible();
129 } else {
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");
137 root = doc;
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) {
144 return nullptr;
146 return result;
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;
185 break;
186 case 2: // MOVEMENT_GRANULARITY_WORD
187 startBoundaryType = nsIAccessibleText::BOUNDARY_WORD_START;
188 endBoundaryType = nsIAccessibleText::BOUNDARY_WORD_END;
189 break;
190 default:
191 break;
194 TextLeafRange resultRange;
196 if (aForward) {
197 resultRange.SetEnd(
198 currentRange.End().FindBoundary(endBoundaryType, eDirNext));
199 resultRange.SetStart(
200 resultRange.End().FindBoundary(startBoundaryType, eDirPrevious));
201 } else {
202 resultRange.SetStart(
203 currentRange.Start().FindBoundary(startBoundaryType, eDirPrevious));
204 resultRange.SetEnd(
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.
211 return Nothing();
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.
217 return Nothing();
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.");
229 } else {
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) {
239 uint32_t flags = 0;
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;
300 return flags;
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)) {
313 return;
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)) {
324 return;
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;
352 } else {
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, \
360 nameRule) \
361 case roles::geckoRole: \
362 return androidClass;
364 switch (aRole) {
365 #include "RoleMap.h"
366 default:
367 return java::SessionAccessibility::CLASSNAME_VIEW;
370 #undef ROLE
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;
402 return 0;
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);
410 } else {
411 Name(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
424 return false;
427 if (aEvent->IsFromUserInput()) {
428 return false;
431 RefPtr<AccAttributes> attributes = new AccAttributes();
432 nsAccUtils::SetLiveContainerAttributes(attributes, this);
433 nsString live;
434 if (!attributes->GetAttribute(nsGkAtoms::containerLive, live)) {
435 return false;
438 uint16_t priority = live.EqualsIgnoreCase("assertive")
439 ? nsIAccessibleAnnouncementEvent::ASSERTIVE
440 : nsIAccessibleAnnouncementEvent::POLITE;
442 Maybe<bool> atomic =
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();
451 if (element &&
452 nsAccUtils::ARIAAttrValueIs(element, nsGkAtoms::aria_atomic,
453 nsGkAtoms::_true, eCaseMatters)) {
454 atomicAncestor = parent;
455 break;
459 if (atomicAncestor) {
460 announcementTarget = atomicAncestor;
461 static_cast<AccessibleWrap*>(atomicAncestor)->GetTextEquiv(announcement);
463 } else {
464 GetTextEquiv(announcement);
467 announcement.CompressWhitespace();
468 if (announcement.IsEmpty()) {
469 return false;
472 announcementTarget->Announce(announcement, priority);
473 return true;