SessionAccessibility.cpp (33036B)
1 /* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- 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 "SessionAccessibility.h" 7 #include "LocalAccessible-inl.h" 8 #include "AndroidUiThread.h" 9 #include "AndroidBridge.h" 10 #include "DocAccessibleParent.h" 11 #include "IDSet.h" 12 #include "nsThreadUtils.h" 13 #include "AccAttributes.h" 14 #include "AccessibilityEvent.h" 15 #include "DocAccessibleWrap.h" 16 #include "JavaBuiltins.h" 17 #include "nsAccessibilityService.h" 18 #include "nsAccUtils.h" 19 20 #include "mozilla/PresShell.h" 21 #include "mozilla/dom/BrowserParent.h" 22 #include "mozilla/dom/CanonicalBrowsingContext.h" 23 #include "mozilla/dom/Document.h" 24 #include "mozilla/dom/DocumentInlines.h" 25 #include "mozilla/a11y/Accessible.h" 26 #include "mozilla/a11y/DocAccessibleParent.h" 27 #include "mozilla/a11y/DocManager.h" 28 #include "mozilla/a11y/HyperTextAccessibleBase.h" 29 #include "mozilla/jni/GeckoBundleUtils.h" 30 #include "mozilla/jni/NativesInlines.h" 31 #include "mozilla/widget/GeckoViewSupport.h" 32 #include "mozilla/MouseEvents.h" 33 #include "mozilla/dom/MouseEventBinding.h" 34 35 #ifdef DEBUG 36 # include <android/log.h> 37 # define AALOG(args...) \ 38 __android_log_print(ANDROID_LOG_INFO, "GeckoAccessibilityNative", ##args) 39 #else 40 # define AALOG(args...) \ 41 do { \ 42 } while (0) 43 #endif 44 45 using namespace mozilla::a11y; 46 47 // IDs should be a positive 32bit integer. 48 IDSet sIDSet(31UL); 49 50 class Settings final 51 : public mozilla::java::SessionAccessibility::Settings::Natives<Settings> { 52 public: 53 static void ToggleNativeAccessibility(bool aEnable) { 54 if (aEnable) { 55 GetOrCreateAccService(); 56 } else { 57 MaybeShutdownAccService(nsAccessibilityService::ePlatformAPI); 58 } 59 } 60 }; 61 62 SessionAccessibility::SessionAccessibility( 63 jni::NativeWeakPtr<widget::GeckoViewSupport> aWindow, 64 java::SessionAccessibility::NativeProvider::Param aSessionAccessibility) 65 : mWindow(aWindow), mSessionAccessibility(aSessionAccessibility) {} 66 67 void SessionAccessibility::SetAttached(bool aAttached, 68 already_AddRefed<Runnable> aRunnable) { 69 if (RefPtr<nsThread> uiThread = GetAndroidUiThread()) { 70 uiThread->Dispatch(NS_NewRunnableFunction( 71 "SessionAccessibility::Attach", 72 [aAttached, 73 sa = java::SessionAccessibility::NativeProvider::GlobalRef( 74 mSessionAccessibility), 75 runnable = RefPtr<Runnable>(aRunnable)] { 76 sa->SetAttached(aAttached); 77 if (runnable) { 78 runnable->Run(); 79 } 80 })); 81 } 82 } 83 84 void SessionAccessibility::Init() { 85 java::SessionAccessibility::NativeProvider::Natives< 86 SessionAccessibility>::Init(); 87 Settings::Init(); 88 } 89 90 void SessionAccessibility::GetNodeInfo(int32_t aID, 91 mozilla::jni::Object::Param aNodeInfo) { 92 MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); 93 ReleasableMonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); 94 java::GeckoBundle::GlobalRef ret = nullptr; 95 RefPtr<SessionAccessibility> self(this); 96 if (Accessible* acc = GetAccessibleByID(aID)) { 97 if (acc->IsLocal()) { 98 mal.Unlock(); 99 nsAppShell::SyncRunEvent( 100 [this, self, aID, aNodeInfo = jni::Object::GlobalRef(aNodeInfo)] { 101 if (Accessible* acc = GetAccessibleByID(aID)) { 102 PopulateNodeInfo(acc, aNodeInfo); 103 } else { 104 AALOG("oops, nothing for %d", aID); 105 } 106 }); 107 } else { 108 PopulateNodeInfo(acc, aNodeInfo); 109 } 110 } else { 111 AALOG("oops, nothing for %d", aID); 112 } 113 } 114 115 int SessionAccessibility::GetNodeClassName(int32_t aID) { 116 MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); 117 ReleasableMonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); 118 int32_t classNameEnum = java::SessionAccessibility::CLASSNAME_VIEW; 119 RefPtr<SessionAccessibility> self(this); 120 if (Accessible* acc = GetAccessibleByID(aID)) { 121 if (acc->IsLocal()) { 122 mal.Unlock(); 123 nsAppShell::SyncRunEvent([this, self, aID, &classNameEnum] { 124 if (Accessible* acc = GetAccessibleByID(aID)) { 125 classNameEnum = AccessibleWrap::AndroidClass(acc); 126 } 127 }); 128 } else { 129 classNameEnum = AccessibleWrap::AndroidClass(acc); 130 } 131 } 132 133 return classNameEnum; 134 } 135 136 void SessionAccessibility::SetText(int32_t aID, jni::String::Param aText) { 137 if (Accessible* acc = GetAccessibleByID(aID)) { 138 if (acc->IsRemote()) { 139 acc->AsRemote()->ReplaceText(PromiseFlatString(aText->ToString())); 140 } else if (acc->AsLocal()->IsHyperText()) { 141 acc->AsLocal()->AsHyperText()->ReplaceText(aText->ToString()); 142 } 143 } 144 } 145 146 void SessionAccessibility::Click(int32_t aID) { 147 MOZ_ASSERT(NS_IsMainThread()); 148 MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); 149 if (Accessible* acc = GetAccessibleByID(aID)) { 150 acc->DoAction(0); 151 } 152 } 153 154 bool SessionAccessibility::Pivot(int32_t aID, int32_t aGranularity, 155 bool aForward, bool aInclusive) { 156 MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); 157 MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); 158 RefPtr<SessionAccessibility> self(this); 159 if (Accessible* acc = GetAccessibleByID(aID)) { 160 if (acc->IsLocal()) { 161 nsAppShell::PostEvent( 162 [this, self, aID, aGranularity, aForward, aInclusive] { 163 MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); 164 if (Accessible* _acc = GetAccessibleByID(aID)) { 165 MOZ_ASSERT(_acc->IsLocal()); 166 if (Accessible* result = AccessibleWrap::DoPivot( 167 _acc, aGranularity, aForward, aInclusive)) { 168 SendAccessibilityFocusedEvent(result, true); 169 } 170 } 171 }); 172 return true; 173 } 174 Accessible* result = 175 AccessibleWrap::DoPivot(acc, aGranularity, aForward, aInclusive); 176 if (result) { 177 int32_t virtualViewID = AccessibleWrap::GetVirtualViewID(result); 178 nsAppShell::PostEvent([this, self, virtualViewID] { 179 MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); 180 if (Accessible* acc = GetAccessibleByID(virtualViewID)) { 181 SendAccessibilityFocusedEvent(acc, true); 182 } 183 }); 184 return true; 185 } 186 } 187 188 return false; 189 } 190 191 void SessionAccessibility::ExploreByTouch(int32_t aID, float aX, float aY) { 192 MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); 193 MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); 194 RefPtr<SessionAccessibility> self(this); 195 if (Accessible* origin = GetAccessibleByID(aID)) { 196 if (origin->IsLocal()) { 197 nsAppShell::PostEvent([this, self, aID, aX, aY] { 198 MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); 199 if (Accessible* origin = GetAccessibleByID(aID)) { 200 if (Accessible* result = 201 AccessibleWrap::ExploreByTouch(origin, aX, aY)) { 202 SendHoverEnterEvent(result); 203 } 204 } 205 }); 206 } else { 207 if (Accessible* result = AccessibleWrap::ExploreByTouch(origin, aX, aY)) { 208 int32_t resultID = AccessibleWrap::GetVirtualViewID(result); 209 nsAppShell::PostEvent([this, self, resultID] { 210 MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); 211 if (Accessible* result = GetAccessibleByID(resultID)) { 212 SendHoverEnterEvent(result); 213 } 214 }); 215 } 216 } 217 } 218 } 219 220 static void GetSelectionOrCaret(HyperTextAccessibleBase* aHyperTextAcc, 221 int32_t* aStartOffset, int32_t* aEndOffset) { 222 if (!aHyperTextAcc->SelectionBoundsAt(0, aStartOffset, aEndOffset)) { 223 *aStartOffset = *aEndOffset = aHyperTextAcc->CaretOffset(); 224 } 225 } 226 227 static void AdjustCaretToTextNavigation(Accessible* aAccessible, 228 int32_t aStartOffset, 229 int32_t aEndOffset, bool aForward, 230 bool aSelect) { 231 MOZ_ASSERT(NS_IsMainThread()); 232 if (!(aAccessible->State() & states::EDITABLE)) { 233 return; 234 } 235 236 HyperTextAccessibleBase* editable = aAccessible->AsHyperTextBase(); 237 MOZ_ASSERT(editable); 238 if (!editable) { 239 return; 240 } 241 242 int32_t newOffset = aForward ? aEndOffset : aStartOffset; 243 if (aSelect) { 244 int32_t anchor = editable->CaretOffset(); 245 if (editable->SelectionCount()) { 246 int32_t startSel, endSel; 247 GetSelectionOrCaret(editable, &startSel, &endSel); 248 anchor = startSel == anchor ? endSel : startSel; 249 } 250 editable->SetSelectionBoundsAt(0, anchor, newOffset); 251 } else { 252 editable->SetCaretOffset(newOffset); 253 } 254 } 255 256 bool SessionAccessibility::NavigateText(int32_t aID, int32_t aGranularity, 257 int32_t aStartOffset, 258 int32_t aEndOffset, bool aForward, 259 bool aSelect) { 260 MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); 261 MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); 262 RefPtr<SessionAccessibility> self(this); 263 if (Accessible* acc = GetAccessibleByID(aID)) { 264 if (acc->IsLocal()) { 265 nsAppShell::PostEvent([this, self, aID, aGranularity, aStartOffset, 266 aEndOffset, aForward, aSelect] { 267 MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); 268 if (Accessible* _acc = GetAccessibleByID(aID)) { 269 auto result = AccessibleWrap::NavigateText( 270 _acc, aGranularity, aStartOffset, aEndOffset, aForward, aSelect); 271 272 if (result) { 273 SendTextTraversedEvent(_acc, result->first, result->second); 274 AdjustCaretToTextNavigation(_acc, result->first, result->second, 275 aForward, aSelect); 276 } 277 } 278 }); 279 return true; 280 } else { 281 auto result = AccessibleWrap::NavigateText( 282 acc, aGranularity, aStartOffset, aEndOffset, aForward, aSelect); 283 if (result) { 284 nsAppShell::PostEvent([this, self, aID, result, aForward, aSelect] { 285 MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); 286 if (Accessible* _acc = GetAccessibleByID(aID)) { 287 SendTextTraversedEvent(_acc, result->first, result->second); 288 AdjustCaretToTextNavigation(_acc, result->first, result->second, 289 aForward, aSelect); 290 } 291 }); 292 } 293 294 return !!result; 295 } 296 } 297 298 return false; 299 } 300 301 void SessionAccessibility::SetSelection(int32_t aID, int32_t aStart, 302 int32_t aEnd) { 303 if (Accessible* acc = GetAccessibleByID(aID)) { 304 if (auto* textAcc = acc->AsHyperTextBase()) { 305 if (aStart == aEnd) { 306 textAcc->SetCaretOffset(aStart); 307 } else { 308 textAcc->SetSelectionBoundsAt(0, aStart, aEnd); 309 } 310 } 311 } 312 } 313 314 void SessionAccessibility::Cut(int32_t aID) { 315 if (Accessible* acc = GetAccessibleByID(aID)) { 316 if (auto* textAcc = acc->AsHyperTextBase()) { 317 int32_t startSel, endSel; 318 if (textAcc->SelectionBoundsAt(0, &startSel, &endSel)) { 319 textAcc->CutText(startSel, endSel); 320 } 321 } 322 } 323 } 324 325 void SessionAccessibility::Copy(int32_t aID) { 326 if (Accessible* acc = GetAccessibleByID(aID)) { 327 if (auto* textAcc = acc->AsHyperTextBase()) { 328 int32_t startSel, endSel; 329 GetSelectionOrCaret(textAcc, &startSel, &endSel); 330 textAcc->CopyText(startSel, endSel); 331 } 332 } 333 } 334 335 void SessionAccessibility::Paste(int32_t aID) { 336 if (Accessible* acc = GetAccessibleByID(aID)) { 337 if (auto* textAcc = acc->AsHyperTextBase()) { 338 textAcc->PasteText(nsIAccessibleText::TEXT_OFFSET_CARET); 339 } 340 } 341 } 342 343 RefPtr<SessionAccessibility> SessionAccessibility::GetInstanceFor( 344 Accessible* aAccessible) { 345 MOZ_ASSERT(NS_IsMainThread()); 346 if (LocalAccessible* localAcc = aAccessible->AsLocal()) { 347 DocAccessible* docAcc = localAcc->Document(); 348 // If the accessible is being shutdown from the doc's shutdown 349 // the doc accessible won't have a ref to a presshell anymore, 350 // but we should have a ref to the DOM document node, and the DOM doc 351 // has a ref to the presshell. 352 dom::Document* doc = docAcc ? docAcc->DocumentNode() : nullptr; 353 if (doc && doc->IsContentDocument()) { 354 // Only content accessibles should have an associated SessionAccessible. 355 return GetInstanceFor(doc->GetPresShell()); 356 } 357 } else { 358 dom::CanonicalBrowsingContext* cbc = 359 static_cast<dom::BrowserParent*>( 360 aAccessible->AsRemote()->Document()->Manager()) 361 ->GetBrowsingContext() 362 ->Top(); 363 dom::BrowserParent* bp = cbc->GetBrowserParent(); 364 if (!bp) { 365 bp = static_cast<dom::BrowserParent*>( 366 aAccessible->AsRemote()->Document()->Manager()); 367 } 368 if (auto element = bp->GetOwnerElement()) { 369 if (auto doc = element->OwnerDoc()) { 370 if (nsPresContext* presContext = doc->GetPresContext()) { 371 return GetInstanceFor(presContext->PresShell()); 372 } 373 } else { 374 MOZ_ASSERT_UNREACHABLE( 375 "Browser parent's element does not have owner doc."); 376 } 377 } 378 } 379 380 return nullptr; 381 } 382 383 RefPtr<SessionAccessibility> SessionAccessibility::GetInstanceFor( 384 PresShell* aPresShell) { 385 MOZ_ASSERT(NS_IsMainThread()); 386 if (!aPresShell) { 387 return nullptr; 388 } 389 390 nsCOMPtr<nsIWidget> rootWidget = aPresShell->GetRootWidget(); 391 // `rootWidget` can be one of several types. Here we make sure it is an 392 // android nsWindow. 393 if (RefPtr<nsWindow> window = nsWindow::From(rootWidget)) { 394 return window->GetSessionAccessibility(); 395 } 396 397 return nullptr; 398 } 399 400 void SessionAccessibility::SendAccessibilityFocusedEvent( 401 Accessible* aAccessible, bool aScrollIntoView) { 402 MOZ_ASSERT(NS_IsMainThread()); 403 mSessionAccessibility->SendEvent( 404 java::sdk::AccessibilityEvent::TYPE_VIEW_ACCESSIBILITY_FOCUSED, 405 AccessibleWrap::GetVirtualViewID(aAccessible), 406 AccessibleWrap::AndroidClass(aAccessible), nullptr); 407 if (aScrollIntoView) { 408 aAccessible->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE); 409 } 410 } 411 412 void SessionAccessibility::SendHoverEnterEvent(Accessible* aAccessible) { 413 MOZ_ASSERT(NS_IsMainThread()); 414 mSessionAccessibility->SendEvent( 415 java::sdk::AccessibilityEvent::TYPE_VIEW_HOVER_ENTER, 416 AccessibleWrap::GetVirtualViewID(aAccessible), 417 AccessibleWrap::AndroidClass(aAccessible), nullptr); 418 } 419 420 void SessionAccessibility::SendFocusEvent(Accessible* aAccessible) { 421 MOZ_ASSERT(NS_IsMainThread()); 422 // Suppress focus events from about:blank pages. 423 // This is important for tests. 424 if (aAccessible->IsDoc() && aAccessible->ChildCount() == 0) { 425 return; 426 } 427 428 mSessionAccessibility->SendEvent( 429 java::sdk::AccessibilityEvent::TYPE_VIEW_FOCUSED, 430 AccessibleWrap::GetVirtualViewID(aAccessible), 431 AccessibleWrap::AndroidClass(aAccessible), nullptr); 432 } 433 434 void SessionAccessibility::SendScrollingEvent(Accessible* aAccessible, 435 int32_t aScrollX, 436 int32_t aScrollY, 437 int32_t aMaxScrollX, 438 int32_t aMaxScrollY) { 439 MOZ_ASSERT(NS_IsMainThread()); 440 int32_t virtualViewId = AccessibleWrap::GetVirtualViewID(aAccessible); 441 442 if (virtualViewId != kNoID) { 443 // XXX: Support scrolling in subframes 444 return; 445 } 446 447 GECKOBUNDLE_START(eventInfo); 448 GECKOBUNDLE_PUT(eventInfo, "scrollX", java::sdk::Integer::ValueOf(aScrollX)); 449 GECKOBUNDLE_PUT(eventInfo, "scrollY", java::sdk::Integer::ValueOf(aScrollY)); 450 GECKOBUNDLE_PUT(eventInfo, "maxScrollX", 451 java::sdk::Integer::ValueOf(aMaxScrollX)); 452 GECKOBUNDLE_PUT(eventInfo, "maxScrollY", 453 java::sdk::Integer::ValueOf(aMaxScrollY)); 454 GECKOBUNDLE_FINISH(eventInfo); 455 456 mSessionAccessibility->SendEvent( 457 java::sdk::AccessibilityEvent::TYPE_VIEW_SCROLLED, virtualViewId, 458 AccessibleWrap::AndroidClass(aAccessible), eventInfo); 459 SendWindowContentChangedEvent(); 460 } 461 462 void SessionAccessibility::SendWindowContentChangedEvent() { 463 mSessionAccessibility->SendEvent( 464 java::sdk::AccessibilityEvent::TYPE_WINDOW_CONTENT_CHANGED, kNoID, 465 java::SessionAccessibility::CLASSNAME_WEBVIEW, nullptr); 466 } 467 468 void SessionAccessibility::SendWindowStateChangedEvent( 469 Accessible* aAccessible) { 470 MOZ_ASSERT(NS_IsMainThread()); 471 // Suppress window state changed events from about:blank pages. 472 // This is important for tests. 473 if (aAccessible->IsDoc() && aAccessible->ChildCount() == 0) { 474 return; 475 } 476 477 mSessionAccessibility->SendEvent( 478 java::sdk::AccessibilityEvent::TYPE_WINDOW_STATE_CHANGED, 479 AccessibleWrap::GetVirtualViewID(aAccessible), 480 AccessibleWrap::AndroidClass(aAccessible), nullptr); 481 482 SendWindowContentChangedEvent(); 483 } 484 485 void SessionAccessibility::SendTextSelectionChangedEvent( 486 Accessible* aAccessible, int32_t aCaretOffset) { 487 MOZ_ASSERT(NS_IsMainThread()); 488 int32_t fromIndex = aCaretOffset; 489 int32_t startSel = -1; 490 int32_t endSel = -1; 491 bool hasSelection = 492 aAccessible->AsHyperTextBase()->SelectionBoundsAt(0, &startSel, &endSel); 493 494 if (hasSelection) { 495 fromIndex = startSel == aCaretOffset ? endSel : startSel; 496 } 497 498 nsAutoString text; 499 if (aAccessible->IsHyperText()) { 500 aAccessible->AsHyperTextBase()->TextSubstring(0, -1, text); 501 } else if (aAccessible->IsText()) { 502 aAccessible->AppendTextTo(text, 0, -1); 503 } 504 505 GECKOBUNDLE_START(eventInfo); 506 GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(text)); 507 GECKOBUNDLE_PUT(eventInfo, "fromIndex", 508 java::sdk::Integer::ValueOf(fromIndex)); 509 GECKOBUNDLE_PUT(eventInfo, "toIndex", 510 java::sdk::Integer::ValueOf(aCaretOffset)); 511 GECKOBUNDLE_FINISH(eventInfo); 512 513 mSessionAccessibility->SendEvent( 514 java::sdk::AccessibilityEvent::TYPE_VIEW_TEXT_SELECTION_CHANGED, 515 AccessibleWrap::GetVirtualViewID(aAccessible), 516 AccessibleWrap::AndroidClass(aAccessible), eventInfo); 517 } 518 519 void SessionAccessibility::SendTextChangedEvent(Accessible* aAccessible, 520 const nsAString& aStr, 521 int32_t aStart, uint32_t aLen, 522 bool aIsInsert, 523 bool aFromUser) { 524 MOZ_ASSERT(NS_IsMainThread()); 525 if (!aFromUser) { 526 // Only dispatch text change events from users, for now. 527 return; 528 } 529 530 nsAutoString text; 531 if (aAccessible->IsHyperText()) { 532 aAccessible->AsHyperTextBase()->TextSubstring(0, -1, text); 533 } else if (aAccessible->IsText()) { 534 aAccessible->AppendTextTo(text, 0, -1); 535 } 536 nsAutoString beforeText(text); 537 if (aIsInsert) { 538 beforeText.Cut(aStart, aLen); 539 } else { 540 beforeText.Insert(aStr, aStart); 541 } 542 543 GECKOBUNDLE_START(eventInfo); 544 GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(text)); 545 GECKOBUNDLE_PUT(eventInfo, "beforeText", jni::StringParam(beforeText)); 546 GECKOBUNDLE_PUT(eventInfo, "fromIndex", java::sdk::Integer::ValueOf(aStart)); 547 GECKOBUNDLE_PUT(eventInfo, "addedCount", 548 java::sdk::Integer::ValueOf(aIsInsert ? aLen : 0)); 549 GECKOBUNDLE_PUT(eventInfo, "removedCount", 550 java::sdk::Integer::ValueOf(aIsInsert ? 0 : aLen)); 551 GECKOBUNDLE_FINISH(eventInfo); 552 553 mSessionAccessibility->SendEvent( 554 java::sdk::AccessibilityEvent::TYPE_VIEW_TEXT_CHANGED, 555 AccessibleWrap::GetVirtualViewID(aAccessible), 556 AccessibleWrap::AndroidClass(aAccessible), eventInfo); 557 } 558 559 void SessionAccessibility::SendTextTraversedEvent(Accessible* aAccessible, 560 int32_t aStartOffset, 561 int32_t aEndOffset) { 562 MOZ_ASSERT(NS_IsMainThread()); 563 nsAutoString text; 564 if (aAccessible->IsHyperText()) { 565 aAccessible->AsHyperTextBase()->TextSubstring(0, -1, text); 566 } else if (aAccessible->IsText()) { 567 aAccessible->AppendTextTo(text, 0, -1); 568 } 569 570 GECKOBUNDLE_START(eventInfo); 571 GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(text)); 572 GECKOBUNDLE_PUT(eventInfo, "fromIndex", 573 java::sdk::Integer::ValueOf(aStartOffset)); 574 GECKOBUNDLE_PUT(eventInfo, "toIndex", 575 java::sdk::Integer::ValueOf(aEndOffset)); 576 GECKOBUNDLE_FINISH(eventInfo); 577 578 mSessionAccessibility->SendEvent( 579 java::sdk::AccessibilityEvent:: 580 TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY, 581 AccessibleWrap::GetVirtualViewID(aAccessible), 582 AccessibleWrap::AndroidClass(aAccessible), eventInfo); 583 } 584 585 void SessionAccessibility::SendClickedEvent(Accessible* aAccessible, 586 uint32_t aFlags) { 587 GECKOBUNDLE_START(eventInfo); 588 GECKOBUNDLE_PUT(eventInfo, "flags", java::sdk::Integer::ValueOf(aFlags)); 589 GECKOBUNDLE_FINISH(eventInfo); 590 591 mSessionAccessibility->SendEvent( 592 java::sdk::AccessibilityEvent::TYPE_VIEW_CLICKED, 593 AccessibleWrap::GetVirtualViewID(aAccessible), 594 AccessibleWrap::AndroidClass(aAccessible), eventInfo); 595 } 596 597 void SessionAccessibility::SendSelectedEvent(Accessible* aAccessible, 598 bool aSelected) { 599 MOZ_ASSERT(NS_IsMainThread()); 600 GECKOBUNDLE_START(eventInfo); 601 // Boolean::FALSE/TRUE gets clobbered by a macro, so ugh. 602 GECKOBUNDLE_PUT(eventInfo, "selected", 603 java::sdk::Integer::ValueOf(aSelected ? 1 : 0)); 604 GECKOBUNDLE_FINISH(eventInfo); 605 606 mSessionAccessibility->SendEvent( 607 java::sdk::AccessibilityEvent::TYPE_VIEW_SELECTED, 608 AccessibleWrap::GetVirtualViewID(aAccessible), 609 AccessibleWrap::AndroidClass(aAccessible), eventInfo); 610 } 611 612 void SessionAccessibility::SendAnnouncementEvent(Accessible* aAccessible, 613 const nsAString& aAnnouncement, 614 uint16_t aPriority) { 615 MOZ_ASSERT(NS_IsMainThread()); 616 GECKOBUNDLE_START(eventInfo); 617 GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(aAnnouncement)); 618 GECKOBUNDLE_FINISH(eventInfo); 619 620 // Announcements should have the root as their source, so we ignore the 621 // accessible of the event. 622 mSessionAccessibility->SendEvent( 623 java::sdk::AccessibilityEvent::TYPE_ANNOUNCEMENT, kNoID, 624 java::SessionAccessibility::CLASSNAME_WEBVIEW, eventInfo); 625 } 626 627 void SessionAccessibility::PopulateNodeInfo( 628 Accessible* aAccessible, mozilla::jni::Object::Param aNodeInfo) { 629 nsAutoString name; 630 ENameValueFlag nameFlag = aAccessible->Name(name); 631 nsAutoString textValue; 632 aAccessible->Value(textValue); 633 nsAutoString nodeID; 634 aAccessible->DOMNodeID(nodeID); 635 nsAutoString accDesc; 636 aAccessible->Description(accDesc); 637 uint64_t state = aAccessible->State(); 638 LayoutDeviceIntRect bounds = aAccessible->Bounds(); 639 int32_t virtualViewID = AccessibleWrap::GetVirtualViewID(aAccessible); 640 Accessible* parent = virtualViewID != kNoID ? aAccessible->Parent() : nullptr; 641 int32_t parentID = parent ? AccessibleWrap::GetVirtualViewID(parent) : 0; 642 role role = aAccessible->Role(); 643 if (role == roles::LINK && !(state & states::LINKED)) { 644 // A link without the linked state (<a> with no href) shouldn't be presented 645 // as a link. 646 role = roles::TEXT; 647 } 648 649 uint32_t flags = AccessibleWrap::GetFlags(aAccessible); 650 651 int32_t className = AccessibleWrap::AndroidClass(aAccessible); 652 653 nsAutoString hint; 654 nsAutoString text; 655 nsAutoString description; 656 if (state & states::EDITABLE) { 657 // An editable field's name is populated in the hint. 658 hint.Assign(name); 659 text.Assign(textValue); 660 } else { 661 if (role == roles::LINK || role == roles::HEADING) { 662 description.Assign(name); 663 } else if (role != roles::CELL || nameFlag != eNameFromSubtree) { 664 // In most cases, use the name as the text. We discard the name completely 665 // for a table cell where the name is computed from the subtree because 666 // we don't want UI Automator to find the cell instead of a link inside 667 // it. TraversalRule ignores table cells anyway, so this is only relevant 668 // to UI Automator. 669 text.Assign(name); 670 } 671 } 672 673 if (!accDesc.IsEmpty()) { 674 if (!hint.IsEmpty()) { 675 // If this is an editable, the description is concatenated with a 676 // whitespace directly after the name. 677 hint.AppendLiteral(" "); 678 } 679 hint.Append(accDesc); 680 } 681 682 if ((state & states::REQUIRED) != 0) { 683 nsAutoString requiredString; 684 if (LocalizeString(u"stateRequired"_ns, requiredString)) { 685 if (!hint.IsEmpty()) { 686 // If the hint is non-empty, concatenate with a comma for a brief pause. 687 hint.AppendLiteral(", "); 688 } 689 hint.Append(requiredString); 690 } 691 } 692 693 RefPtr<AccAttributes> attributes = aAccessible->Attributes(); 694 nsAccUtils::SetAccGroupAttrs(attributes, aAccessible); 695 696 nsAutoString geckoRole; 697 nsAutoString roleDescription; 698 if (virtualViewID != kNoID) { 699 AccessibleWrap::GetRoleDescription(role, attributes, geckoRole, 700 roleDescription); 701 } 702 703 int32_t inputType = 0; 704 if (attributes) { 705 nsString inputTypeAttr; 706 attributes->GetAttribute(nsGkAtoms::textInputType, inputTypeAttr); 707 inputType = AccessibleWrap::GetInputType(inputTypeAttr); 708 } 709 710 auto childCount = aAccessible->ChildCount(); 711 nsTArray<int32_t> children(childCount); 712 if (!nsAccUtils::MustPrune(aAccessible)) { 713 for (uint32_t i = 0; i < childCount; i++) { 714 auto child = aAccessible->ChildAt(i); 715 children.AppendElement(AccessibleWrap::GetVirtualViewID(child)); 716 } 717 } 718 719 const int32_t boundsArray[4] = {bounds.x, bounds.y, bounds.x + bounds.width, 720 bounds.y + bounds.height}; 721 722 mSessionAccessibility->PopulateNodeInfo( 723 aNodeInfo, virtualViewID, parentID, jni::IntArray::From(children), flags, 724 className, jni::IntArray::New(boundsArray, 4), jni::StringParam(text), 725 jni::StringParam(description), jni::StringParam(hint), 726 jni::StringParam(geckoRole), jni::StringParam(roleDescription), 727 jni::StringParam(nodeID), inputType); 728 729 if (aAccessible->HasNumericValue()) { 730 double curValue = aAccessible->CurValue(); 731 double minValue = aAccessible->MinValue(); 732 double maxValue = aAccessible->MaxValue(); 733 double step = aAccessible->Step(); 734 735 int32_t rangeType = 0; // integer 736 if (maxValue == 1 && minValue == 0) { 737 rangeType = 2; // percent 738 } else if (std::round(step) != step) { 739 rangeType = 1; // float; 740 } 741 742 mSessionAccessibility->PopulateNodeRangeInfo( 743 aNodeInfo, rangeType, static_cast<float>(minValue), 744 static_cast<float>(maxValue), static_cast<float>(curValue)); 745 } 746 747 if (attributes) { 748 Maybe<int32_t> rowIndex = 749 attributes->GetAttribute<int32_t>(nsGkAtoms::posinset); 750 if (rowIndex) { 751 mSessionAccessibility->PopulateNodeCollectionItemInfo( 752 aNodeInfo, *rowIndex - 1, 1, 0, 1); 753 } 754 755 Maybe<int32_t> rowCount = 756 attributes->GetAttribute<int32_t>(nsGkAtoms::child_item_count); 757 if (rowCount) { 758 int32_t selectionMode = 0; 759 if (aAccessible->IsSelect()) { 760 selectionMode = (state & states::MULTISELECTABLE) ? 2 : 1; 761 } 762 mSessionAccessibility->PopulateNodeCollectionInfo( 763 aNodeInfo, *rowCount, 1, selectionMode, 764 attributes->HasAttribute(nsGkAtoms::tree)); 765 } 766 } 767 } 768 769 Accessible* SessionAccessibility::GetAccessibleByID(int32_t aID) const { 770 return mIDToAccessibleMap.Get(aID); 771 } 772 773 #ifdef DEBUG 774 static bool IsDetachedDoc(Accessible* aAccessible) { 775 if (!aAccessible->IsRemote() || !aAccessible->AsRemote()->IsDoc()) { 776 return false; 777 } 778 779 return !aAccessible->Parent() || 780 aAccessible->Parent()->FirstChild() != aAccessible; 781 } 782 #endif 783 784 SessionAccessibility::IDMappingEntry::IDMappingEntry(Accessible* aAccessible) 785 : mInternalID(0) { 786 *this = aAccessible; 787 } 788 789 SessionAccessibility::IDMappingEntry& 790 SessionAccessibility::IDMappingEntry::operator=(Accessible* aAccessible) { 791 mInternalID = aAccessible->ID(); 792 MOZ_ASSERT(!(mInternalID & IS_REMOTE), "First bit is used in accessible ID!"); 793 if (aAccessible->IsRemote()) { 794 mInternalID |= IS_REMOTE; 795 } 796 797 Accessible* docAcc = nsAccUtils::DocumentFor(aAccessible); 798 MOZ_ASSERT(docAcc); 799 if (docAcc) { 800 MOZ_ASSERT(docAcc->IsRemote() == aAccessible->IsRemote()); 801 if (docAcc->IsRemote()) { 802 mDoc = docAcc->AsRemote()->AsDoc(); 803 } else { 804 mDoc = docAcc->AsLocal(); 805 } 806 } 807 808 return *this; 809 } 810 811 SessionAccessibility::IDMappingEntry::operator Accessible*() const { 812 if (mInternalID == 0) { 813 return static_cast<LocalAccessible*>(mDoc.get()); 814 } 815 816 if (mInternalID == IS_REMOTE) { 817 return static_cast<DocAccessibleParent*>(mDoc.get()); 818 } 819 820 if (mInternalID & IS_REMOTE) { 821 return static_cast<DocAccessibleParent*>(mDoc.get()) 822 ->GetAccessible(mInternalID & ~IS_REMOTE); 823 } 824 825 Accessible* accessible = 826 static_cast<LocalAccessible*>(mDoc.get()) 827 ->AsDoc() 828 ->GetAccessibleByUniqueID(reinterpret_cast<void*>(mInternalID)); 829 // If the accessible is retrievable from the DocAccessible, it can't be 830 // defunct. 831 MOZ_ASSERT(!accessible->AsLocal()->IsDefunct()); 832 833 return accessible; 834 } 835 836 void SessionAccessibility::RegisterAccessible(Accessible* aAccessible) { 837 if (IPCAccessibilityActive()) { 838 // Don't register accessible in content process. 839 return; 840 } 841 842 nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns(); 843 RefPtr<SessionAccessibility> sessionAcc = GetInstanceFor(aAccessible); 844 if (!sessionAcc) { 845 return; 846 } 847 848 bool isTopLevel = false; 849 if (aAccessible->IsLocal() && aAccessible->IsDoc()) { 850 DocAccessibleWrap* doc = 851 static_cast<DocAccessibleWrap*>(aAccessible->AsLocal()->AsDoc()); 852 isTopLevel = doc->IsTopLevelContentDoc(); 853 } else if (aAccessible->IsRemote() && aAccessible->IsDoc()) { 854 isTopLevel = aAccessible->AsRemote()->AsDoc()->IsTopLevel(); 855 } 856 857 int32_t virtualViewID = kNoID; 858 if (!isTopLevel) { 859 if (sessionAcc->mIDToAccessibleMap.IsEmpty()) { 860 // We expect there to already be at least one accessible 861 // registered (the top-level one). If it isn't we are 862 // probably in a shutdown process where it was already 863 // unregistered. So we don't register this accessible. 864 return; 865 } 866 // Don't use the special "unset" value (0). 867 while ((virtualViewID = sIDSet.GetID()) == kUnsetID) { 868 } 869 } 870 AccessibleWrap::SetVirtualViewID(aAccessible, virtualViewID); 871 872 Accessible* oldAcc = sessionAcc->mIDToAccessibleMap.Get(virtualViewID); 873 if (oldAcc) { 874 // About to overwrite mapping of registered accessible. This should 875 // only happen when the registered accessible is a detached document. 876 MOZ_ASSERT(IsDetachedDoc(oldAcc), 877 "ID already registered to non-detached document"); 878 AccessibleWrap::SetVirtualViewID(oldAcc, kUnsetID); 879 } 880 881 sessionAcc->mIDToAccessibleMap.InsertOrUpdate(virtualViewID, aAccessible); 882 } 883 884 void SessionAccessibility::UnregisterAccessible(Accessible* aAccessible) { 885 if (IPCAccessibilityActive()) { 886 // Don't unregister accessible in content process. 887 return; 888 } 889 890 nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns(); 891 int32_t virtualViewID = AccessibleWrap::GetVirtualViewID(aAccessible); 892 if (virtualViewID == kUnsetID) { 893 return; 894 } 895 896 RefPtr<SessionAccessibility> sessionAcc = GetInstanceFor(aAccessible); 897 if (sessionAcc) { 898 Accessible* registeredAcc = 899 sessionAcc->mIDToAccessibleMap.Get(virtualViewID); 900 if (registeredAcc != aAccessible) { 901 // Attempting to unregister an accessible that is not mapped to 902 // its virtual view ID. This probably means it is a detached document 903 // and a more recent document overwrote its '-1' mapping. 904 // We set its own virtual view ID to `kUnsetID` and return early. 905 MOZ_ASSERT(!registeredAcc || IsDetachedDoc(aAccessible), 906 "Accessible is detached document"); 907 AccessibleWrap::SetVirtualViewID(aAccessible, kUnsetID); 908 return; 909 } 910 911 MOZ_ASSERT(registeredAcc, "Unregistering unregistered accessible"); 912 MOZ_ASSERT(registeredAcc == aAccessible, "Unregistering wrong accessible"); 913 sessionAcc->mIDToAccessibleMap.Remove(virtualViewID); 914 } 915 916 if (virtualViewID > kNoID) { 917 sIDSet.ReleaseID(virtualViewID); 918 } 919 920 AccessibleWrap::SetVirtualViewID(aAccessible, kUnsetID); 921 } 922 923 void SessionAccessibility::UnregisterAll(PresShell* aPresShell) { 924 if (IPCAccessibilityActive()) { 925 // Don't unregister accessible in content process. 926 return; 927 } 928 929 nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns(); 930 RefPtr<SessionAccessibility> sessionAcc = GetInstanceFor(aPresShell); 931 if (sessionAcc) { 932 sessionAcc->mIDToAccessibleMap.Clear(); 933 } 934 }