Bug 1814798 - pt 1. Add bool to enable/disable PHC at runtime r=glandium
[gecko.git] / accessible / android / AccessibleWrap.cpp
blobf378bd83e4261a2006cff8838437a358218e1536
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);
64 DocAccessibleWrap* doc =
65 static_cast<DocAccessibleWrap*>(accessible->Document());
66 if (doc) {
67 switch (aEvent->GetEventType()) {
68 case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
69 if (accessible != aEvent->Document() && !aEvent->IsFromUserInput()) {
70 AccCaretMoveEvent* caretEvent = downcast_accEvent(aEvent);
71 HyperTextAccessible* ht = AsHyperText();
72 // Pivot to the caret's position if it has an expanded selection.
73 // This is used mostly for find in page.
74 if ((ht && ht->SelectionCount())) {
75 DOMPoint point =
76 AsHyperText()->OffsetToDOMPoint(caretEvent->GetCaretOffset());
77 if (LocalAccessible* newPos =
78 doc->GetAccessibleOrContainer(point.node)) {
79 static_cast<AccessibleWrap*>(newPos)->PivotTo(
80 java::SessionAccessibility::HTML_GRANULARITY_DEFAULT, true,
81 true);
85 break;
87 case nsIAccessibleEvent::EVENT_SCROLLING_START: {
88 accessible->PivotTo(
89 java::SessionAccessibility::HTML_GRANULARITY_DEFAULT, true, true);
90 break;
92 default:
93 break;
97 nsresult rv = LocalAccessible::HandleAccEvent(aEvent);
98 NS_ENSURE_SUCCESS(rv, rv);
100 accessible->HandleLiveRegionEvent(aEvent);
102 return NS_OK;
105 void AccessibleWrap::Shutdown() {
106 if (!IPCAccessibilityActive()) {
107 MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
108 SessionAccessibility::UnregisterAccessible(this);
110 LocalAccessible::Shutdown();
113 bool AccessibleWrap::DoAction(uint8_t aIndex) const {
114 if (ActionCount()) {
115 return LocalAccessible::DoAction(aIndex);
118 if (mContent) {
119 // We still simulate a click on an accessible even if there is no
120 // known actions. For the sake of bad markup.
121 DoCommand();
122 return true;
125 return false;
128 Accessible* AccessibleWrap::DoPivot(Accessible* aAccessible,
129 int32_t aGranularity, bool aForward,
130 bool aInclusive) {
131 Accessible* pivotRoot = nullptr;
132 if (aAccessible->IsRemote()) {
133 // If this is a remote accessible provide the top level
134 // remote doc as the pivot root for thread safety reasons.
135 DocAccessibleParent* doc = aAccessible->AsRemote()->Document();
136 while (doc && !doc->IsTopLevel()) {
137 doc = doc->ParentDoc();
139 MOZ_ASSERT(doc, "Failed to get top level DocAccessibleParent");
140 pivotRoot = doc;
142 a11y::Pivot pivot(pivotRoot);
143 // Depending on the start accessible, the pivot rule will either traverse
144 // local or remote accessibles exclusively.
145 TraversalRule rule(aGranularity, aAccessible->IsLocal());
146 Accessible* result = aForward ? pivot.Next(aAccessible, rule, aInclusive)
147 : pivot.Prev(aAccessible, rule, aInclusive);
149 if (result && (result != aAccessible || aInclusive)) {
150 return result;
153 return nullptr;
156 bool AccessibleWrap::PivotTo(int32_t aGranularity, bool aForward,
157 bool aInclusive) {
158 Accessible* result = DoPivot(this, aGranularity, aForward, aInclusive);
159 if (result) {
160 MOZ_ASSERT(result->IsLocal());
161 // Dispatch a virtual cursor change event that will be turned into an
162 // android accessibility focused changed event in the parent.
163 PivotMoveReason reason = aForward ? nsIAccessiblePivot::REASON_NEXT
164 : nsIAccessiblePivot::REASON_PREV;
165 LocalAccessible* localResult = result->AsLocal();
166 RefPtr<AccEvent> event = new AccVCChangeEvent(
167 localResult->Document(), this, localResult, reason, eFromUserInput);
168 nsEventShell::FireEvent(event);
170 return true;
173 return false;
176 void AccessibleWrap::ExploreByTouch(float aX, float aY) {
177 a11y::Pivot pivot(RootAccessible());
178 TraversalRule rule;
180 Accessible* maybeResult = pivot.AtPoint(aX, aY, rule);
181 LocalAccessible* result = maybeResult ? maybeResult->AsLocal() : nullptr;
183 if (result && result != this) {
184 RefPtr<AccEvent> event =
185 new AccVCChangeEvent(result->Document(), this, result,
186 nsIAccessiblePivot::REASON_POINT, eFromUserInput);
187 nsEventShell::FireEvent(event);
191 static TextLeafPoint ToTextLeafPoint(Accessible* aAccessible, int32_t aOffset) {
192 if (HyperTextAccessibleBase* ht = aAccessible->AsHyperTextBase()) {
193 return ht->ToTextLeafPoint(aOffset);
196 return TextLeafPoint(aAccessible, aOffset);
199 Maybe<std::pair<int32_t, int32_t>> AccessibleWrap::NavigateText(
200 Accessible* aAccessible, int32_t aGranularity, int32_t aStartOffset,
201 int32_t aEndOffset, bool aForward, bool aSelect) {
202 int32_t startOffset = aStartOffset;
203 int32_t endOffset = aEndOffset;
204 if (startOffset == -1) {
205 MOZ_ASSERT(endOffset == -1,
206 "When start offset is unset, end offset should be too");
207 startOffset = aForward ? 0 : nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT;
208 endOffset = aForward ? 0 : nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT;
211 // If the accessible is an editable, set the virtual cursor position
212 // to its caret offset. Otherwise use the document's virtual cursor
213 // position as a starting offset.
214 if (aAccessible->State() & states::EDITABLE) {
215 startOffset = endOffset = aAccessible->AsHyperTextBase()->CaretOffset();
218 TextLeafRange currentRange =
219 TextLeafRange(ToTextLeafPoint(aAccessible, startOffset),
220 ToTextLeafPoint(aAccessible, endOffset));
221 uint16_t startBoundaryType = nsIAccessibleText::BOUNDARY_LINE_START;
222 uint16_t endBoundaryType = nsIAccessibleText::BOUNDARY_LINE_END;
223 switch (aGranularity) {
224 case 1: // MOVEMENT_GRANULARITY_CHARACTER
225 startBoundaryType = nsIAccessibleText::BOUNDARY_CHAR;
226 endBoundaryType = nsIAccessibleText::BOUNDARY_CHAR;
227 break;
228 case 2: // MOVEMENT_GRANULARITY_WORD
229 startBoundaryType = nsIAccessibleText::BOUNDARY_WORD_START;
230 endBoundaryType = nsIAccessibleText::BOUNDARY_WORD_END;
231 break;
232 default:
233 break;
236 TextLeafRange resultRange;
238 if (aForward) {
239 resultRange.SetEnd(
240 currentRange.End().FindBoundary(endBoundaryType, eDirNext));
241 resultRange.SetStart(
242 resultRange.End().FindBoundary(startBoundaryType, eDirPrevious));
243 } else {
244 resultRange.SetStart(
245 currentRange.Start().FindBoundary(startBoundaryType, eDirPrevious));
246 resultRange.SetEnd(
247 resultRange.Start().FindBoundary(endBoundaryType, eDirNext));
250 if (!resultRange.Crop(aAccessible)) {
251 // If the new range does not intersect at all with the given
252 // accessible/container this navigation has failed or reached an edge.
253 return Nothing();
256 if (resultRange == currentRange || resultRange.Start() == resultRange.End()) {
257 // If the result range equals the current range, or if the result range is
258 // collapsed, we failed or reached an edge.
259 return Nothing();
262 if (HyperTextAccessibleBase* ht = aAccessible->AsHyperTextBase()) {
263 DebugOnly<bool> ok = false;
264 std::tie(ok, startOffset) = ht->TransformOffset(
265 resultRange.Start().mAcc, resultRange.Start().mOffset, false);
266 MOZ_ASSERT(ok, "Accessible of range start should be in container.");
268 std::tie(ok, endOffset) = ht->TransformOffset(
269 resultRange.End().mAcc, resultRange.End().mOffset, false);
270 MOZ_ASSERT(ok, "Accessible range end should be in container.");
271 } else {
272 startOffset = resultRange.Start().mOffset;
273 endOffset = resultRange.End().mOffset;
276 return Some(std::make_pair(startOffset, endOffset));
279 uint32_t AccessibleWrap::GetFlags(role aRole, uint64_t aState,
280 uint8_t aActionCount) {
281 uint32_t flags = 0;
282 if (aState & states::CHECKABLE) {
283 flags |= java::SessionAccessibility::FLAG_CHECKABLE;
286 if (aState & states::CHECKED) {
287 flags |= java::SessionAccessibility::FLAG_CHECKED;
290 if (aState & states::INVALID) {
291 flags |= java::SessionAccessibility::FLAG_CONTENT_INVALID;
294 if (aState & states::EDITABLE) {
295 flags |= java::SessionAccessibility::FLAG_EDITABLE;
298 if (aActionCount && aRole != roles::TEXT_LEAF) {
299 flags |= java::SessionAccessibility::FLAG_CLICKABLE;
302 if (aState & states::ENABLED) {
303 flags |= java::SessionAccessibility::FLAG_ENABLED;
306 if (aState & states::FOCUSABLE) {
307 flags |= java::SessionAccessibility::FLAG_FOCUSABLE;
310 if (aState & states::FOCUSED) {
311 flags |= java::SessionAccessibility::FLAG_FOCUSED;
314 if (aState & states::MULTI_LINE) {
315 flags |= java::SessionAccessibility::FLAG_MULTI_LINE;
318 if (aState & states::SELECTABLE) {
319 flags |= java::SessionAccessibility::FLAG_SELECTABLE;
322 if (aState & states::SELECTED) {
323 flags |= java::SessionAccessibility::FLAG_SELECTED;
326 if (aState & states::EXPANDABLE) {
327 flags |= java::SessionAccessibility::FLAG_EXPANDABLE;
330 if (aState & states::EXPANDED) {
331 flags |= java::SessionAccessibility::FLAG_EXPANDED;
334 if ((aState & (states::INVISIBLE | states::OFFSCREEN)) == 0) {
335 flags |= java::SessionAccessibility::FLAG_VISIBLE_TO_USER;
338 if (aRole == roles::PASSWORD_TEXT) {
339 flags |= java::SessionAccessibility::FLAG_PASSWORD;
342 return flags;
345 void AccessibleWrap::GetRoleDescription(role aRole, AccAttributes* aAttributes,
346 nsAString& aGeckoRole,
347 nsAString& aRoleDescription) {
348 if (aRole == roles::HEADING && aAttributes) {
349 // The heading level is an attribute, so we need that.
350 nsAutoString headingLevel;
351 if (aAttributes->GetAttribute(nsGkAtoms::level, headingLevel)) {
352 nsAutoString token(u"heading-");
353 token.Append(headingLevel);
354 if (LocalizeString(token, aRoleDescription)) {
355 return;
360 if ((aRole == roles::LANDMARK || aRole == roles::REGION) && aAttributes) {
361 nsAutoString xmlRoles;
362 if (aAttributes->GetAttribute(nsGkAtoms::xmlroles, xmlRoles)) {
363 nsWhitespaceTokenizer tokenizer(xmlRoles);
364 while (tokenizer.hasMoreTokens()) {
365 if (LocalizeString(tokenizer.nextToken(), aRoleDescription)) {
366 return;
372 GetAccService()->GetStringRole(aRole, aGeckoRole);
373 LocalizeString(aGeckoRole, aRoleDescription);
376 int32_t AccessibleWrap::AndroidClass(Accessible* aAccessible) {
377 return GetVirtualViewID(aAccessible) == SessionAccessibility::kNoID
378 ? java::SessionAccessibility::CLASSNAME_WEBVIEW
379 : GetAndroidClass(aAccessible->Role());
382 int32_t AccessibleWrap::GetVirtualViewID(Accessible* aAccessible) {
383 if (aAccessible->IsLocal()) {
384 return static_cast<AccessibleWrap*>(aAccessible)->mID;
387 return static_cast<int32_t>(aAccessible->AsRemote()->GetWrapper());
390 void AccessibleWrap::SetVirtualViewID(Accessible* aAccessible,
391 int32_t aVirtualViewID) {
392 if (aAccessible->IsLocal()) {
393 static_cast<AccessibleWrap*>(aAccessible)->mID = aVirtualViewID;
394 } else {
395 aAccessible->AsRemote()->SetWrapper(static_cast<uintptr_t>(aVirtualViewID));
399 int32_t AccessibleWrap::GetAndroidClass(role aRole) {
400 #define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
401 msaaRole, ia2Role, androidClass, nameRule) \
402 case roles::geckoRole: \
403 return androidClass;
405 switch (aRole) {
406 #include "RoleMap.h"
407 default:
408 return java::SessionAccessibility::CLASSNAME_VIEW;
411 #undef ROLE
414 int32_t AccessibleWrap::GetInputType(const nsString& aInputTypeAttr) {
415 if (aInputTypeAttr.EqualsIgnoreCase("email")) {
416 return java::sdk::InputType::TYPE_CLASS_TEXT |
417 java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
420 if (aInputTypeAttr.EqualsIgnoreCase("number")) {
421 return java::sdk::InputType::TYPE_CLASS_NUMBER;
424 if (aInputTypeAttr.EqualsIgnoreCase("password")) {
425 return java::sdk::InputType::TYPE_CLASS_TEXT |
426 java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_PASSWORD;
429 if (aInputTypeAttr.EqualsIgnoreCase("tel")) {
430 return java::sdk::InputType::TYPE_CLASS_PHONE;
433 if (aInputTypeAttr.EqualsIgnoreCase("text")) {
434 return java::sdk::InputType::TYPE_CLASS_TEXT |
435 java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
438 if (aInputTypeAttr.EqualsIgnoreCase("url")) {
439 return java::sdk::InputType::TYPE_CLASS_TEXT |
440 java::sdk::InputType::TYPE_TEXT_VARIATION_URI;
443 return 0;
446 void AccessibleWrap::GetTextEquiv(nsString& aText) {
447 if (nsTextEquivUtils::HasNameRule(this, eNameFromSubtreeIfReqRule)) {
448 // This is an accessible that normally doesn't get its name from its
449 // subtree, so we collect the text equivalent explicitly.
450 nsTextEquivUtils::GetTextEquivFromSubtree(this, aText);
451 } else {
452 Name(aText);
456 bool AccessibleWrap::HandleLiveRegionEvent(AccEvent* aEvent) {
457 auto eventType = aEvent->GetEventType();
458 if (eventType != nsIAccessibleEvent::EVENT_TEXT_INSERTED &&
459 eventType != nsIAccessibleEvent::EVENT_NAME_CHANGE) {
460 // XXX: Right now only announce text inserted events. aria-relevant=removals
461 // is potentially on the chopping block[1]. We also don't support editable
462 // text because we currently can't descern the source of the change[2].
463 // 1. https://github.com/w3c/aria/issues/712
464 // 2. https://bugzilla.mozilla.org/show_bug.cgi?id=1531189
465 return false;
468 if (aEvent->IsFromUserInput()) {
469 return false;
472 RefPtr<AccAttributes> attributes = new AccAttributes();
473 nsAccUtils::SetLiveContainerAttributes(attributes, this);
474 nsString live;
475 if (!attributes->GetAttribute(nsGkAtoms::containerLive, live)) {
476 return false;
479 uint16_t priority = live.EqualsIgnoreCase("assertive")
480 ? nsIAccessibleAnnouncementEvent::ASSERTIVE
481 : nsIAccessibleAnnouncementEvent::POLITE;
483 Maybe<bool> atomic =
484 attributes->GetAttribute<bool>(nsGkAtoms::containerAtomic);
485 LocalAccessible* announcementTarget = this;
486 nsAutoString announcement;
487 if (atomic && *atomic) {
488 LocalAccessible* atomicAncestor = nullptr;
489 for (LocalAccessible* parent = announcementTarget; parent;
490 parent = parent->LocalParent()) {
491 dom::Element* element = parent->Elm();
492 if (element &&
493 nsAccUtils::ARIAAttrValueIs(element, nsGkAtoms::aria_atomic,
494 nsGkAtoms::_true, eCaseMatters)) {
495 atomicAncestor = parent;
496 break;
500 if (atomicAncestor) {
501 announcementTarget = atomicAncestor;
502 static_cast<AccessibleWrap*>(atomicAncestor)->GetTextEquiv(announcement);
504 } else {
505 GetTextEquiv(announcement);
508 announcement.CompressWhitespace();
509 if (announcement.IsEmpty()) {
510 return false;
513 announcementTarget->Announce(announcement, priority);
514 return true;