AccessibleWrap.cpp (16128B)
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/. */ 5 6 #include "AccessibleWrap.h" 7 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" 29 30 #include "mozilla/a11y/PDocAccessibleChild.h" 31 #include "mozilla/jni/GeckoBundleUtils.h" 32 #include "mozilla/a11y/DocAccessibleParent.h" 33 #include "mozilla/Maybe.h" 34 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 41 42 using namespace mozilla::a11y; 43 using mozilla::Maybe; 44 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); 53 } 54 } 55 56 //----------------------------------------------------- 57 // destruction 58 //----------------------------------------------------- 59 AccessibleWrap::~AccessibleWrap() {} 60 61 nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) { 62 auto accessible = static_cast<AccessibleWrap*>(aEvent->GetAccessible()); 63 NS_ENSURE_TRUE(accessible, NS_ERROR_FAILURE); 64 65 nsresult rv = LocalAccessible::HandleAccEvent(aEvent); 66 NS_ENSURE_SUCCESS(rv, rv); 67 68 accessible->HandleLiveRegionEvent(aEvent); 69 70 return NS_OK; 71 } 72 73 void AccessibleWrap::Shutdown() { 74 if (!IPCAccessibilityActive()) { 75 MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); 76 SessionAccessibility::UnregisterAccessible(this); 77 } 78 LocalAccessible::Shutdown(); 79 } 80 81 bool AccessibleWrap::DoAction(uint8_t aIndex) const { 82 if (ActionCount()) { 83 return LocalAccessible::DoAction(aIndex); 84 } 85 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; 91 } 92 93 return false; 94 } 95 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(); 106 } 107 MOZ_ASSERT(doc, "Failed to get top level DocAccessibleParent"); 108 pivotRoot = doc; 109 } 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); 116 117 if (result && (result != aAccessible || aInclusive)) { 118 return result; 119 } 120 121 return nullptr; 122 } 123 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(); 135 } 136 MOZ_ASSERT(doc, "Failed to get top level DocAccessibleParent"); 137 root = doc; 138 } 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; 145 } 146 return result; 147 } 148 149 static TextLeafPoint ToTextLeafPoint(Accessible* aAccessible, int32_t aOffset) { 150 if (HyperTextAccessibleBase* ht = aAccessible->AsHyperTextBase()) { 151 return ht->ToTextLeafPoint(aOffset); 152 } 153 154 return TextLeafPoint(aAccessible, aOffset); 155 } 156 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; 167 } 168 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(); 174 } 175 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; 192 } 193 194 TextLeafRange resultRange; 195 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)); 206 } 207 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(); 212 } 213 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(); 218 } 219 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."); 225 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; 232 } 233 234 return Some(std::make_pair(startOffset, endOffset)); 235 } 236 237 uint32_t AccessibleWrap::GetFlags(Accessible* aAccessible) { 238 uint32_t flags = 0; 239 uint64_t state = aAccessible->State(); 240 role role = aAccessible->Role(); 241 if (aAccessible->IsScrollable()) { 242 flags |= java::SessionAccessibility::FLAG_SCROLLABLE; 243 } 244 245 if (state & states::CHECKABLE) { 246 flags |= java::SessionAccessibility::FLAG_CHECKABLE; 247 } 248 249 if (state & states::CHECKED) { 250 flags |= java::SessionAccessibility::FLAG_CHECKED; 251 } 252 253 if (state & states::INVALID) { 254 flags |= java::SessionAccessibility::FLAG_CONTENT_INVALID; 255 } 256 257 if (state & states::EDITABLE) { 258 flags |= java::SessionAccessibility::FLAG_EDITABLE; 259 } 260 261 if (aAccessible->ActionCount() && role != roles::TEXT_LEAF) { 262 flags |= java::SessionAccessibility::FLAG_CLICKABLE; 263 } 264 265 if (state & states::ENABLED) { 266 flags |= java::SessionAccessibility::FLAG_ENABLED; 267 } 268 269 if (state & states::FOCUSABLE) { 270 flags |= java::SessionAccessibility::FLAG_FOCUSABLE; 271 } 272 273 if (state & states::FOCUSED) { 274 flags |= java::SessionAccessibility::FLAG_FOCUSED; 275 } 276 277 if (state & states::MULTI_LINE) { 278 flags |= java::SessionAccessibility::FLAG_MULTI_LINE; 279 } 280 281 if (state & states::SELECTABLE) { 282 flags |= java::SessionAccessibility::FLAG_SELECTABLE; 283 } 284 285 if (state & states::SELECTED) { 286 flags |= java::SessionAccessibility::FLAG_SELECTED; 287 } 288 289 if (state & states::EXPANDABLE) { 290 flags |= java::SessionAccessibility::FLAG_EXPANDABLE; 291 } 292 293 if (state & states::EXPANDED) { 294 flags |= java::SessionAccessibility::FLAG_EXPANDED; 295 } 296 297 if ((state & (states::INVISIBLE | states::OFFSCREEN)) == 0) { 298 flags |= java::SessionAccessibility::FLAG_VISIBLE_TO_USER; 299 } 300 301 if (role == roles::PASSWORD_TEXT) { 302 flags |= java::SessionAccessibility::FLAG_PASSWORD; 303 } 304 305 return flags; 306 } 307 308 void AccessibleWrap::GetRoleDescription(role aRole, AccAttributes* aAttributes, 309 nsAString& aGeckoRole, 310 nsAString& aRoleDescription) { 311 if (aRole == roles::HEADING && aAttributes) { 312 // The heading level is an attribute, so we need that. 313 nsAutoString headingLevel; 314 if (aAttributes->GetAttribute(nsGkAtoms::level, headingLevel)) { 315 nsAutoString token(u"heading-"); 316 token.Append(headingLevel); 317 if (LocalizeString(token, aRoleDescription)) { 318 return; 319 } 320 } 321 } 322 323 if ((aRole == roles::LANDMARK || aRole == roles::REGION) && aAttributes) { 324 nsAutoString xmlRoles; 325 if (aAttributes->GetAttribute(nsGkAtoms::xmlroles, xmlRoles)) { 326 nsWhitespaceTokenizer tokenizer(xmlRoles); 327 while (tokenizer.hasMoreTokens()) { 328 if (LocalizeString(tokenizer.nextToken(), aRoleDescription)) { 329 return; 330 } 331 } 332 } 333 } 334 335 GetAccService()->GetStringRole(aRole, aGeckoRole); 336 LocalizeString(aGeckoRole, aRoleDescription); 337 } 338 339 int32_t AccessibleWrap::AndroidClass(Accessible* aAccessible) { 340 return GetVirtualViewID(aAccessible) == SessionAccessibility::kNoID 341 ? java::SessionAccessibility::CLASSNAME_WEBVIEW 342 : GetAndroidClass(aAccessible->Role()); 343 } 344 345 int32_t AccessibleWrap::GetVirtualViewID(Accessible* aAccessible) { 346 if (aAccessible->IsLocal()) { 347 return static_cast<AccessibleWrap*>(aAccessible)->mID; 348 } 349 350 return static_cast<int32_t>(aAccessible->AsRemote()->GetWrapper()); 351 } 352 353 void AccessibleWrap::SetVirtualViewID(Accessible* aAccessible, 354 int32_t aVirtualViewID) { 355 if (aAccessible->IsLocal()) { 356 static_cast<AccessibleWrap*>(aAccessible)->mID = aVirtualViewID; 357 } else { 358 aAccessible->AsRemote()->SetWrapper(static_cast<uintptr_t>(aVirtualViewID)); 359 } 360 } 361 362 int32_t AccessibleWrap::GetAndroidClass(role aRole) { 363 #define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \ 364 msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \ 365 nameRule) \ 366 case roles::geckoRole: \ 367 return androidClass; 368 369 switch (aRole) { 370 #include "RoleMap.h" 371 default: 372 return java::SessionAccessibility::CLASSNAME_VIEW; 373 } 374 375 #undef ROLE 376 } 377 378 int32_t AccessibleWrap::GetInputType(const nsString& aInputTypeAttr) { 379 if (aInputTypeAttr.EqualsIgnoreCase("email")) { 380 return java::sdk::InputType::TYPE_CLASS_TEXT | 381 java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; 382 } 383 384 if (aInputTypeAttr.EqualsIgnoreCase("number")) { 385 return java::sdk::InputType::TYPE_CLASS_NUMBER; 386 } 387 388 if (aInputTypeAttr.EqualsIgnoreCase("password")) { 389 return java::sdk::InputType::TYPE_CLASS_TEXT | 390 java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_PASSWORD; 391 } 392 393 if (aInputTypeAttr.EqualsIgnoreCase("tel")) { 394 return java::sdk::InputType::TYPE_CLASS_PHONE; 395 } 396 397 if (aInputTypeAttr.EqualsIgnoreCase("text")) { 398 return java::sdk::InputType::TYPE_CLASS_TEXT | 399 java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; 400 } 401 402 if (aInputTypeAttr.EqualsIgnoreCase("url")) { 403 return java::sdk::InputType::TYPE_CLASS_TEXT | 404 java::sdk::InputType::TYPE_TEXT_VARIATION_URI; 405 } 406 407 return 0; 408 } 409 410 void AccessibleWrap::GetTextEquiv(nsString& aText) { 411 // 1. Start with the name, since it might have been explicitly specified. 412 if (Name(aText) != eNameFromSubtree) { 413 // 2. If the name didn't come from the subtree, add the text from the 414 // subtree. 415 if (aText.IsEmpty()) { 416 nsTextEquivUtils::GetTextEquivFromSubtree(this, aText); 417 } else { 418 nsAutoString subtree; 419 nsTextEquivUtils::GetTextEquivFromSubtree(this, subtree); 420 if (!subtree.IsEmpty()) { 421 aText.Append(' '); 422 aText.Append(subtree); 423 } 424 } 425 } 426 } 427 428 bool AccessibleWrap::HandleLiveRegionEvent(AccEvent* aEvent) { 429 auto eventType = aEvent->GetEventType(); 430 if (eventType != nsIAccessibleEvent::EVENT_TEXT_INSERTED && 431 eventType != nsIAccessibleEvent::EVENT_NAME_CHANGE) { 432 // XXX: Right now only announce text inserted events. aria-relevant=removals 433 // is potentially on the chopping block[1]. We also don't support editable 434 // text because we currently can't descern the source of the change[2]. 435 // 1. https://github.com/w3c/aria/issues/712 436 // 2. https://bugzilla.mozilla.org/show_bug.cgi?id=1531189 437 return false; 438 } 439 440 if (aEvent->IsFromUserInput()) { 441 return false; 442 } 443 444 RefPtr<AccAttributes> attributes = new AccAttributes(); 445 nsAccUtils::SetLiveContainerAttributes(attributes, this); 446 nsString live; 447 if (!attributes->GetAttribute(nsGkAtoms::containerLive, live)) { 448 return false; 449 } 450 451 uint16_t priority = live.EqualsIgnoreCase("assertive") 452 ? nsIAccessibleAnnouncementEvent::ASSERTIVE 453 : nsIAccessibleAnnouncementEvent::POLITE; 454 455 Maybe<bool> atomic = 456 attributes->GetAttribute<bool>(nsGkAtoms::containerAtomic); 457 LocalAccessible* announcementTarget = this; 458 nsAutoString announcement; 459 if (atomic && *atomic) { 460 LocalAccessible* atomicAncestor = nullptr; 461 for (LocalAccessible* parent = announcementTarget; parent; 462 parent = parent->LocalParent()) { 463 dom::Element* element = parent->Elm(); 464 if (element && 465 nsAccUtils::ARIAAttrValueIs(element, nsGkAtoms::aria_atomic, 466 nsGkAtoms::_true, eCaseMatters)) { 467 atomicAncestor = parent; 468 break; 469 } 470 } 471 472 if (atomicAncestor) { 473 announcementTarget = atomicAncestor; 474 static_cast<AccessibleWrap*>(atomicAncestor)->GetTextEquiv(announcement); 475 } 476 } else { 477 GetTextEquiv(announcement); 478 } 479 480 announcement.CompressWhitespace(); 481 if (announcement.IsEmpty()) { 482 return false; 483 } 484 485 announcementTarget->Announce(announcement, priority); 486 return true; 487 }