/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "SessionAccessibility.h" #include "LocalAccessible-inl.h" #include "AndroidUiThread.h" #include "AndroidBridge.h" #include "DocAccessibleParent.h" #include "IDSet.h" #include "nsThreadUtils.h" #include "AccAttributes.h" #include "AccessibilityEvent.h" #include "DocAccessibleWrap.h" #include "JavaBuiltins.h" #include "nsAccessibilityService.h" #include "nsAccUtils.h" #include "nsViewManager.h" #include "mozilla/PresShell.h" #include "mozilla/dom/BrowserParent.h" #include "mozilla/dom/CanonicalBrowsingContext.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/DocumentInlines.h" #include "mozilla/a11y/Accessible.h" #include "mozilla/a11y/DocAccessibleParent.h" #include "mozilla/a11y/DocManager.h" #include "mozilla/a11y/HyperTextAccessibleBase.h" #include "mozilla/jni/GeckoBundleUtils.h" #include "mozilla/jni/NativesInlines.h" #include "mozilla/widget/GeckoViewSupport.h" #include "mozilla/MouseEvents.h" #include "mozilla/dom/MouseEventBinding.h" #ifdef DEBUG # include # define AALOG(args...) \ __android_log_print(ANDROID_LOG_INFO, "GeckoAccessibilityNative", ##args) #else # define AALOG(args...) \ do { \ } while (0) #endif using namespace mozilla::a11y; // IDs should be a positive 32bit integer. IDSet sIDSet(31UL); class Settings final : public mozilla::java::SessionAccessibility::Settings::Natives { public: static void ToggleNativeAccessibility(bool aEnable) { if (aEnable) { GetOrCreateAccService(); } else { MaybeShutdownAccService(nsAccessibilityService::ePlatformAPI); } } }; SessionAccessibility::SessionAccessibility( jni::NativeWeakPtr aWindow, java::SessionAccessibility::NativeProvider::Param aSessionAccessibility) : mWindow(aWindow), mSessionAccessibility(aSessionAccessibility) { SetAttached(true, nullptr); } void SessionAccessibility::SetAttached(bool aAttached, already_AddRefed aRunnable) { if (RefPtr uiThread = GetAndroidUiThread()) { uiThread->Dispatch(NS_NewRunnableFunction( "SessionAccessibility::Attach", [aAttached, sa = java::SessionAccessibility::NativeProvider::GlobalRef( mSessionAccessibility), runnable = RefPtr(aRunnable)] { sa->SetAttached(aAttached); if (runnable) { runnable->Run(); } })); } } void SessionAccessibility::Init() { java::SessionAccessibility::NativeProvider::Natives< SessionAccessibility>::Init(); Settings::Init(); } void SessionAccessibility::GetNodeInfo(int32_t aID, mozilla::jni::Object::Param aNodeInfo) { MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); ReleasableMonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); java::GeckoBundle::GlobalRef ret = nullptr; RefPtr self(this); if (Accessible* acc = GetAccessibleByID(aID)) { if (acc->IsLocal()) { mal.Unlock(); nsAppShell::SyncRunEvent( [this, self, aID, aNodeInfo = jni::Object::GlobalRef(aNodeInfo)] { if (Accessible* acc = GetAccessibleByID(aID)) { PopulateNodeInfo(acc, aNodeInfo); } else { AALOG("oops, nothing for %d", aID); } }); } else { PopulateNodeInfo(acc, aNodeInfo); } } else { AALOG("oops, nothing for %d", aID); } } int SessionAccessibility::GetNodeClassName(int32_t aID) { MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); ReleasableMonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); int32_t classNameEnum = java::SessionAccessibility::CLASSNAME_VIEW; RefPtr self(this); if (Accessible* acc = GetAccessibleByID(aID)) { if (acc->IsLocal()) { mal.Unlock(); nsAppShell::SyncRunEvent([this, self, aID, &classNameEnum] { if (Accessible* acc = GetAccessibleByID(aID)) { classNameEnum = AccessibleWrap::AndroidClass(acc); } }); } else { classNameEnum = AccessibleWrap::AndroidClass(acc); } } return classNameEnum; } void SessionAccessibility::SetText(int32_t aID, jni::String::Param aText) { if (Accessible* acc = GetAccessibleByID(aID)) { if (acc->IsRemote()) { acc->AsRemote()->ReplaceText(PromiseFlatString(aText->ToString())); } else if (acc->AsLocal()->IsHyperText()) { acc->AsLocal()->AsHyperText()->ReplaceText(aText->ToString()); } } } void SessionAccessibility::Click(int32_t aID) { MOZ_ASSERT(NS_IsMainThread()); MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); if (Accessible* acc = GetAccessibleByID(aID)) { acc->DoAction(0); } } bool SessionAccessibility::Pivot(int32_t aID, int32_t aGranularity, bool aForward, bool aInclusive) { MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); RefPtr self(this); if (Accessible* acc = GetAccessibleByID(aID)) { if (acc->IsLocal()) { nsAppShell::PostEvent( [this, self, aID, aGranularity, aForward, aInclusive] { MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); if (Accessible* _acc = GetAccessibleByID(aID)) { MOZ_ASSERT(_acc->IsLocal()); if (Accessible* result = AccessibleWrap::DoPivot( _acc, aGranularity, aForward, aInclusive)) { SendAccessibilityFocusedEvent(result, true); } } }); return true; } Accessible* result = AccessibleWrap::DoPivot(acc, aGranularity, aForward, aInclusive); if (result) { int32_t virtualViewID = AccessibleWrap::GetVirtualViewID(result); nsAppShell::PostEvent([this, self, virtualViewID] { MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); if (Accessible* acc = GetAccessibleByID(virtualViewID)) { SendAccessibilityFocusedEvent(acc, true); } }); return true; } } return false; } void SessionAccessibility::ExploreByTouch(int32_t aID, float aX, float aY) { MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); RefPtr self(this); if (Accessible* origin = GetAccessibleByID(aID)) { if (origin->IsLocal()) { nsAppShell::PostEvent([this, self, aID, aX, aY] { MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); if (Accessible* origin = GetAccessibleByID(aID)) { if (Accessible* result = AccessibleWrap::ExploreByTouch(origin, aX, aY)) { SendHoverEnterEvent(result); } } }); } else { if (Accessible* result = AccessibleWrap::ExploreByTouch(origin, aX, aY)) { int32_t resultID = AccessibleWrap::GetVirtualViewID(result); nsAppShell::PostEvent([this, self, resultID] { MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); if (Accessible* result = GetAccessibleByID(resultID)) { SendHoverEnterEvent(result); } }); } } } } static void GetSelectionOrCaret(HyperTextAccessibleBase* aHyperTextAcc, int32_t* aStartOffset, int32_t* aEndOffset) { if (!aHyperTextAcc->SelectionBoundsAt(0, aStartOffset, aEndOffset)) { *aStartOffset = *aEndOffset = aHyperTextAcc->CaretOffset(); } } static void AdjustCaretToTextNavigation(Accessible* aAccessible, int32_t aStartOffset, int32_t aEndOffset, bool aForward, bool aSelect) { MOZ_ASSERT(NS_IsMainThread()); if (!(aAccessible->State() & states::EDITABLE)) { return; } HyperTextAccessibleBase* editable = aAccessible->AsHyperTextBase(); MOZ_ASSERT(editable); if (!editable) { return; } int32_t newOffset = aForward ? aEndOffset : aStartOffset; if (aSelect) { int32_t anchor = editable->CaretOffset(); if (editable->SelectionCount()) { int32_t startSel, endSel; GetSelectionOrCaret(editable, &startSel, &endSel); anchor = startSel == anchor ? endSel : startSel; } editable->SetSelectionBoundsAt(0, anchor, newOffset); } else { editable->SetCaretOffset(newOffset); } } bool SessionAccessibility::NavigateText(int32_t aID, int32_t aGranularity, int32_t aStartOffset, int32_t aEndOffset, bool aForward, bool aSelect) { MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); RefPtr self(this); if (Accessible* acc = GetAccessibleByID(aID)) { if (acc->IsLocal()) { nsAppShell::PostEvent([this, self, aID, aGranularity, aStartOffset, aEndOffset, aForward, aSelect] { MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); if (Accessible* _acc = GetAccessibleByID(aID)) { auto result = AccessibleWrap::NavigateText( _acc, aGranularity, aStartOffset, aEndOffset, aForward, aSelect); if (result) { SendTextTraversedEvent(_acc, result->first, result->second); AdjustCaretToTextNavigation(_acc, result->first, result->second, aForward, aSelect); } } }); return true; } else { auto result = AccessibleWrap::NavigateText( acc, aGranularity, aStartOffset, aEndOffset, aForward, aSelect); if (result) { nsAppShell::PostEvent([this, self, aID, result, aForward, aSelect] { MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); if (Accessible* _acc = GetAccessibleByID(aID)) { SendTextTraversedEvent(_acc, result->first, result->second); AdjustCaretToTextNavigation(_acc, result->first, result->second, aForward, aSelect); } }); } return !!result; } } return false; } void SessionAccessibility::SetSelection(int32_t aID, int32_t aStart, int32_t aEnd) { if (Accessible* acc = GetAccessibleByID(aID)) { if (auto* textAcc = acc->AsHyperTextBase()) { if (aStart == aEnd) { textAcc->SetCaretOffset(aStart); } else { textAcc->SetSelectionBoundsAt(0, aStart, aEnd); } } } } void SessionAccessibility::Cut(int32_t aID) { if (Accessible* acc = GetAccessibleByID(aID)) { if (auto* textAcc = acc->AsHyperTextBase()) { int32_t startSel, endSel; if (textAcc->SelectionBoundsAt(0, &startSel, &endSel)) { textAcc->CutText(startSel, endSel); } } } } void SessionAccessibility::Copy(int32_t aID) { if (Accessible* acc = GetAccessibleByID(aID)) { if (auto* textAcc = acc->AsHyperTextBase()) { int32_t startSel, endSel; GetSelectionOrCaret(textAcc, &startSel, &endSel); textAcc->CopyText(startSel, endSel); } } } void SessionAccessibility::Paste(int32_t aID) { if (Accessible* acc = GetAccessibleByID(aID)) { if (auto* textAcc = acc->AsHyperTextBase()) { int32_t startSel, endSel; GetSelectionOrCaret(textAcc, &startSel, &endSel); if (startSel != endSel) { textAcc->DeleteText(startSel, endSel); } textAcc->PasteText(startSel); } } } RefPtr SessionAccessibility::GetInstanceFor( Accessible* aAccessible) { MOZ_ASSERT(NS_IsMainThread()); if (LocalAccessible* localAcc = aAccessible->AsLocal()) { DocAccessible* docAcc = localAcc->Document(); // If the accessible is being shutdown from the doc's shutdown // the doc accessible won't have a ref to a presshell anymore, // but we should have a ref to the DOM document node, and the DOM doc // has a ref to the presshell. dom::Document* doc = docAcc ? docAcc->DocumentNode() : nullptr; if (doc && doc->IsContentDocument()) { // Only content accessibles should have an associated SessionAccessible. return GetInstanceFor(doc->GetPresShell()); } } else { dom::CanonicalBrowsingContext* cbc = static_cast( aAccessible->AsRemote()->Document()->Manager()) ->GetBrowsingContext() ->Top(); dom::BrowserParent* bp = cbc->GetBrowserParent(); if (!bp) { bp = static_cast( aAccessible->AsRemote()->Document()->Manager()); } if (auto element = bp->GetOwnerElement()) { if (auto doc = element->OwnerDoc()) { if (nsPresContext* presContext = doc->GetPresContext()) { return GetInstanceFor(presContext->PresShell()); } } else { MOZ_ASSERT_UNREACHABLE( "Browser parent's element does not have owner doc."); } } } return nullptr; } RefPtr SessionAccessibility::GetInstanceFor( PresShell* aPresShell) { MOZ_ASSERT(NS_IsMainThread()); if (!aPresShell) { return nullptr; } nsViewManager* vm = aPresShell->GetViewManager(); if (!vm) { return nullptr; } nsCOMPtr rootWidget = vm->GetRootWidget(); // `rootWidget` can be one of several types. Here we make sure it is an // android nsWindow. if (RefPtr window = nsWindow::From(rootWidget)) { return window->GetSessionAccessibility(); } return nullptr; } void SessionAccessibility::SendAccessibilityFocusedEvent( Accessible* aAccessible, bool aScrollIntoView) { MOZ_ASSERT(NS_IsMainThread()); mSessionAccessibility->SendEvent( java::sdk::AccessibilityEvent::TYPE_VIEW_ACCESSIBILITY_FOCUSED, AccessibleWrap::GetVirtualViewID(aAccessible), AccessibleWrap::AndroidClass(aAccessible), nullptr); if (aScrollIntoView) { aAccessible->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE); } } void SessionAccessibility::SendHoverEnterEvent(Accessible* aAccessible) { MOZ_ASSERT(NS_IsMainThread()); mSessionAccessibility->SendEvent( java::sdk::AccessibilityEvent::TYPE_VIEW_HOVER_ENTER, AccessibleWrap::GetVirtualViewID(aAccessible), AccessibleWrap::AndroidClass(aAccessible), nullptr); } void SessionAccessibility::SendFocusEvent(Accessible* aAccessible) { MOZ_ASSERT(NS_IsMainThread()); // Suppress focus events from about:blank pages. // This is important for tests. if (aAccessible->IsDoc() && aAccessible->ChildCount() == 0) { return; } mSessionAccessibility->SendEvent( java::sdk::AccessibilityEvent::TYPE_VIEW_FOCUSED, AccessibleWrap::GetVirtualViewID(aAccessible), AccessibleWrap::AndroidClass(aAccessible), nullptr); } void SessionAccessibility::SendScrollingEvent(Accessible* aAccessible, int32_t aScrollX, int32_t aScrollY, int32_t aMaxScrollX, int32_t aMaxScrollY) { MOZ_ASSERT(NS_IsMainThread()); int32_t virtualViewId = AccessibleWrap::GetVirtualViewID(aAccessible); if (virtualViewId != kNoID) { // XXX: Support scrolling in subframes return; } GECKOBUNDLE_START(eventInfo); GECKOBUNDLE_PUT(eventInfo, "scrollX", java::sdk::Integer::ValueOf(aScrollX)); GECKOBUNDLE_PUT(eventInfo, "scrollY", java::sdk::Integer::ValueOf(aScrollY)); GECKOBUNDLE_PUT(eventInfo, "maxScrollX", java::sdk::Integer::ValueOf(aMaxScrollX)); GECKOBUNDLE_PUT(eventInfo, "maxScrollY", java::sdk::Integer::ValueOf(aMaxScrollY)); GECKOBUNDLE_FINISH(eventInfo); mSessionAccessibility->SendEvent( java::sdk::AccessibilityEvent::TYPE_VIEW_SCROLLED, virtualViewId, AccessibleWrap::AndroidClass(aAccessible), eventInfo); SendWindowContentChangedEvent(); } void SessionAccessibility::SendWindowContentChangedEvent() { mSessionAccessibility->SendEvent( java::sdk::AccessibilityEvent::TYPE_WINDOW_CONTENT_CHANGED, kNoID, java::SessionAccessibility::CLASSNAME_WEBVIEW, nullptr); } void SessionAccessibility::SendWindowStateChangedEvent( Accessible* aAccessible) { MOZ_ASSERT(NS_IsMainThread()); // Suppress window state changed events from about:blank pages. // This is important for tests. if (aAccessible->IsDoc() && aAccessible->ChildCount() == 0) { return; } mSessionAccessibility->SendEvent( java::sdk::AccessibilityEvent::TYPE_WINDOW_STATE_CHANGED, AccessibleWrap::GetVirtualViewID(aAccessible), AccessibleWrap::AndroidClass(aAccessible), nullptr); SendWindowContentChangedEvent(); } void SessionAccessibility::SendTextSelectionChangedEvent( Accessible* aAccessible, int32_t aCaretOffset) { MOZ_ASSERT(NS_IsMainThread()); int32_t fromIndex = aCaretOffset; int32_t startSel = -1; int32_t endSel = -1; bool hasSelection = aAccessible->AsHyperTextBase()->SelectionBoundsAt(0, &startSel, &endSel); if (hasSelection) { fromIndex = startSel == aCaretOffset ? endSel : startSel; } nsAutoString text; if (aAccessible->IsHyperText()) { aAccessible->AsHyperTextBase()->TextSubstring(0, -1, text); } else if (aAccessible->IsText()) { aAccessible->AppendTextTo(text, 0, -1); } GECKOBUNDLE_START(eventInfo); GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(text)); GECKOBUNDLE_PUT(eventInfo, "fromIndex", java::sdk::Integer::ValueOf(fromIndex)); GECKOBUNDLE_PUT(eventInfo, "toIndex", java::sdk::Integer::ValueOf(aCaretOffset)); GECKOBUNDLE_FINISH(eventInfo); mSessionAccessibility->SendEvent( java::sdk::AccessibilityEvent::TYPE_VIEW_TEXT_SELECTION_CHANGED, AccessibleWrap::GetVirtualViewID(aAccessible), AccessibleWrap::AndroidClass(aAccessible), eventInfo); } void SessionAccessibility::SendTextChangedEvent(Accessible* aAccessible, const nsAString& aStr, int32_t aStart, uint32_t aLen, bool aIsInsert, bool aFromUser) { MOZ_ASSERT(NS_IsMainThread()); if (!aFromUser) { // Only dispatch text change events from users, for now. return; } nsAutoString text; if (aAccessible->IsHyperText()) { aAccessible->AsHyperTextBase()->TextSubstring(0, -1, text); } else if (aAccessible->IsText()) { aAccessible->AppendTextTo(text, 0, -1); } nsAutoString beforeText(text); if (aIsInsert) { beforeText.Cut(aStart, aLen); } else { beforeText.Insert(aStr, aStart); } GECKOBUNDLE_START(eventInfo); GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(text)); GECKOBUNDLE_PUT(eventInfo, "beforeText", jni::StringParam(beforeText)); GECKOBUNDLE_PUT(eventInfo, "fromIndex", java::sdk::Integer::ValueOf(aStart)); GECKOBUNDLE_PUT(eventInfo, "addedCount", java::sdk::Integer::ValueOf(aIsInsert ? aLen : 0)); GECKOBUNDLE_PUT(eventInfo, "removedCount", java::sdk::Integer::ValueOf(aIsInsert ? 0 : aLen)); GECKOBUNDLE_FINISH(eventInfo); mSessionAccessibility->SendEvent( java::sdk::AccessibilityEvent::TYPE_VIEW_TEXT_CHANGED, AccessibleWrap::GetVirtualViewID(aAccessible), AccessibleWrap::AndroidClass(aAccessible), eventInfo); } void SessionAccessibility::SendTextTraversedEvent(Accessible* aAccessible, int32_t aStartOffset, int32_t aEndOffset) { MOZ_ASSERT(NS_IsMainThread()); nsAutoString text; if (aAccessible->IsHyperText()) { aAccessible->AsHyperTextBase()->TextSubstring(0, -1, text); } else if (aAccessible->IsText()) { aAccessible->AppendTextTo(text, 0, -1); } GECKOBUNDLE_START(eventInfo); GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(text)); GECKOBUNDLE_PUT(eventInfo, "fromIndex", java::sdk::Integer::ValueOf(aStartOffset)); GECKOBUNDLE_PUT(eventInfo, "toIndex", java::sdk::Integer::ValueOf(aEndOffset)); GECKOBUNDLE_FINISH(eventInfo); mSessionAccessibility->SendEvent( java::sdk::AccessibilityEvent:: TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY, AccessibleWrap::GetVirtualViewID(aAccessible), AccessibleWrap::AndroidClass(aAccessible), eventInfo); } void SessionAccessibility::SendClickedEvent(Accessible* aAccessible, uint32_t aFlags) { GECKOBUNDLE_START(eventInfo); GECKOBUNDLE_PUT(eventInfo, "flags", java::sdk::Integer::ValueOf(aFlags)); GECKOBUNDLE_FINISH(eventInfo); mSessionAccessibility->SendEvent( java::sdk::AccessibilityEvent::TYPE_VIEW_CLICKED, AccessibleWrap::GetVirtualViewID(aAccessible), AccessibleWrap::AndroidClass(aAccessible), eventInfo); } void SessionAccessibility::SendSelectedEvent(Accessible* aAccessible, bool aSelected) { MOZ_ASSERT(NS_IsMainThread()); GECKOBUNDLE_START(eventInfo); // Boolean::FALSE/TRUE gets clobbered by a macro, so ugh. GECKOBUNDLE_PUT(eventInfo, "selected", java::sdk::Integer::ValueOf(aSelected ? 1 : 0)); GECKOBUNDLE_FINISH(eventInfo); mSessionAccessibility->SendEvent( java::sdk::AccessibilityEvent::TYPE_VIEW_SELECTED, AccessibleWrap::GetVirtualViewID(aAccessible), AccessibleWrap::AndroidClass(aAccessible), eventInfo); } void SessionAccessibility::SendAnnouncementEvent(Accessible* aAccessible, const nsAString& aAnnouncement, uint16_t aPriority) { MOZ_ASSERT(NS_IsMainThread()); GECKOBUNDLE_START(eventInfo); GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(aAnnouncement)); GECKOBUNDLE_FINISH(eventInfo); // Announcements should have the root as their source, so we ignore the // accessible of the event. mSessionAccessibility->SendEvent( java::sdk::AccessibilityEvent::TYPE_ANNOUNCEMENT, kNoID, java::SessionAccessibility::CLASSNAME_WEBVIEW, eventInfo); } void SessionAccessibility::PopulateNodeInfo( Accessible* aAccessible, mozilla::jni::Object::Param aNodeInfo) { nsAutoString name; aAccessible->Name(name); nsAutoString textValue; aAccessible->Value(textValue); nsAutoString nodeID; aAccessible->DOMNodeID(nodeID); nsAutoString accDesc; aAccessible->Description(accDesc); uint64_t state = aAccessible->State(); LayoutDeviceIntRect bounds = aAccessible->Bounds(); uint8_t actionCount = aAccessible->ActionCount(); int32_t virtualViewID = AccessibleWrap::GetVirtualViewID(aAccessible); Accessible* parent = virtualViewID != kNoID ? aAccessible->Parent() : nullptr; int32_t parentID = parent ? AccessibleWrap::GetVirtualViewID(parent) : 0; role role = aAccessible->Role(); if (role == roles::LINK && !(state & states::LINKED)) { // A link without the linked state ( with no href) shouldn't be presented // as a link. role = roles::TEXT; } uint32_t flags = AccessibleWrap::GetFlags(role, state, actionCount); int32_t className = AccessibleWrap::AndroidClass(aAccessible); nsAutoString hint; nsAutoString text; nsAutoString description; if (state & states::EDITABLE) { // An editable field's name is populated in the hint. hint.Assign(name); text.Assign(textValue); } else { if (role == roles::LINK || role == roles::HEADING) { description.Assign(name); } else { text.Assign(name); } } if (!accDesc.IsEmpty()) { if (!hint.IsEmpty()) { // If this is an editable, the description is concatenated with a // whitespace directly after the name. hint.AppendLiteral(" "); } hint.Append(accDesc); } if ((state & states::REQUIRED) != 0) { nsAutoString requiredString; if (LocalizeString(u"stateRequired"_ns, requiredString)) { if (!hint.IsEmpty()) { // If the hint is non-empty, concatenate with a comma for a brief pause. hint.AppendLiteral(", "); } hint.Append(requiredString); } } RefPtr attributes = aAccessible->Attributes(); nsAutoString geckoRole; nsAutoString roleDescription; if (virtualViewID != kNoID) { AccessibleWrap::GetRoleDescription(role, attributes, geckoRole, roleDescription); } int32_t inputType = 0; if (attributes) { nsString inputTypeAttr; attributes->GetAttribute(nsGkAtoms::textInputType, inputTypeAttr); inputType = AccessibleWrap::GetInputType(inputTypeAttr); } auto childCount = aAccessible->ChildCount(); nsTArray children(childCount); if (!nsAccUtils::MustPrune(aAccessible)) { for (uint32_t i = 0; i < childCount; i++) { auto child = aAccessible->ChildAt(i); children.AppendElement(AccessibleWrap::GetVirtualViewID(child)); } } const int32_t boundsArray[4] = {bounds.x, bounds.y, bounds.x + bounds.width, bounds.y + bounds.height}; mSessionAccessibility->PopulateNodeInfo( aNodeInfo, virtualViewID, parentID, jni::IntArray::From(children), flags, className, jni::IntArray::New(boundsArray, 4), jni::StringParam(text), jni::StringParam(description), jni::StringParam(hint), jni::StringParam(geckoRole), jni::StringParam(roleDescription), jni::StringParam(nodeID), inputType); if (aAccessible->HasNumericValue()) { double curValue = aAccessible->CurValue(); double minValue = aAccessible->MinValue(); double maxValue = aAccessible->MaxValue(); double step = aAccessible->Step(); int32_t rangeType = 0; // integer if (maxValue == 1 && minValue == 0) { rangeType = 2; // percent } else if (std::round(step) != step) { rangeType = 1; // float; } mSessionAccessibility->PopulateNodeRangeInfo( aNodeInfo, rangeType, static_cast(minValue), static_cast(maxValue), static_cast(curValue)); } if (attributes) { Maybe rowIndex = attributes->GetAttribute(nsGkAtoms::posinset); if (rowIndex) { mSessionAccessibility->PopulateNodeCollectionItemInfo( aNodeInfo, *rowIndex - 1, 1, 0, 1); } Maybe rowCount = attributes->GetAttribute(nsGkAtoms::child_item_count); if (rowCount) { int32_t selectionMode = 0; if (aAccessible->IsSelect()) { selectionMode = (state & states::MULTISELECTABLE) ? 2 : 1; } mSessionAccessibility->PopulateNodeCollectionInfo( aNodeInfo, *rowCount, 1, selectionMode, attributes->HasAttribute(nsGkAtoms::tree)); } } } Accessible* SessionAccessibility::GetAccessibleByID(int32_t aID) const { return mIDToAccessibleMap.Get(aID); } #ifdef DEBUG static bool IsDetachedDoc(Accessible* aAccessible) { if (!aAccessible->IsRemote() || !aAccessible->AsRemote()->IsDoc()) { return false; } return !aAccessible->Parent() || aAccessible->Parent()->FirstChild() != aAccessible; } #endif SessionAccessibility::IDMappingEntry::IDMappingEntry(Accessible* aAccessible) : mInternalID(0) { *this = aAccessible; } SessionAccessibility::IDMappingEntry& SessionAccessibility::IDMappingEntry::operator=(Accessible* aAccessible) { mInternalID = aAccessible->ID(); MOZ_ASSERT(!(mInternalID & IS_REMOTE), "First bit is used in accessible ID!"); if (aAccessible->IsRemote()) { mInternalID |= IS_REMOTE; } Accessible* docAcc = nsAccUtils::DocumentFor(aAccessible); MOZ_ASSERT(docAcc); if (docAcc) { MOZ_ASSERT(docAcc->IsRemote() == aAccessible->IsRemote()); if (docAcc->IsRemote()) { mDoc = docAcc->AsRemote()->AsDoc(); } else { mDoc = docAcc->AsLocal(); } } return *this; } SessionAccessibility::IDMappingEntry::operator Accessible*() const { if (mInternalID == 0) { return static_cast(mDoc.get()); } if (mInternalID == IS_REMOTE) { return static_cast(mDoc.get()); } if (mInternalID & IS_REMOTE) { return static_cast(mDoc.get()) ->GetAccessible(mInternalID & ~IS_REMOTE); } Accessible* accessible = static_cast(mDoc.get()) ->AsDoc() ->GetAccessibleByUniqueID(reinterpret_cast(mInternalID)); // If the accessible is retrievable from the DocAccessible, it can't be // defunct. MOZ_ASSERT(!accessible->AsLocal()->IsDefunct()); return accessible; } void SessionAccessibility::RegisterAccessible(Accessible* aAccessible) { if (IPCAccessibilityActive()) { // Don't register accessible in content process. return; } nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns(); RefPtr sessionAcc = GetInstanceFor(aAccessible); if (!sessionAcc) { return; } bool isTopLevel = false; if (aAccessible->IsLocal() && aAccessible->IsDoc()) { DocAccessibleWrap* doc = static_cast(aAccessible->AsLocal()->AsDoc()); isTopLevel = doc->IsTopLevelContentDoc(); } else if (aAccessible->IsRemote() && aAccessible->IsDoc()) { isTopLevel = aAccessible->AsRemote()->AsDoc()->IsTopLevel(); } int32_t virtualViewID = kNoID; if (!isTopLevel) { if (sessionAcc->mIDToAccessibleMap.IsEmpty()) { // We expect there to already be at least one accessible // registered (the top-level one). If it isn't we are // probably in a shutdown process where it was already // unregistered. So we don't register this accessible. return; } // Don't use the special "unset" value (0). while ((virtualViewID = sIDSet.GetID()) == kUnsetID) { } } AccessibleWrap::SetVirtualViewID(aAccessible, virtualViewID); Accessible* oldAcc = sessionAcc->mIDToAccessibleMap.Get(virtualViewID); if (oldAcc) { // About to overwrite mapping of registered accessible. This should // only happen when the registered accessible is a detached document. MOZ_ASSERT(IsDetachedDoc(oldAcc), "ID already registered to non-detached document"); AccessibleWrap::SetVirtualViewID(oldAcc, kUnsetID); } sessionAcc->mIDToAccessibleMap.InsertOrUpdate(virtualViewID, aAccessible); } void SessionAccessibility::UnregisterAccessible(Accessible* aAccessible) { if (IPCAccessibilityActive()) { // Don't unregister accessible in content process. return; } nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns(); int32_t virtualViewID = AccessibleWrap::GetVirtualViewID(aAccessible); if (virtualViewID == kUnsetID) { return; } RefPtr sessionAcc = GetInstanceFor(aAccessible); if (sessionAcc) { Accessible* registeredAcc = sessionAcc->mIDToAccessibleMap.Get(virtualViewID); if (registeredAcc != aAccessible) { // Attempting to unregister an accessible that is not mapped to // its virtual view ID. This probably means it is a detached document // and a more recent document overwrote its '-1' mapping. // We set its own virtual view ID to `kUnsetID` and return early. MOZ_ASSERT(!registeredAcc || IsDetachedDoc(aAccessible), "Accessible is detached document"); AccessibleWrap::SetVirtualViewID(aAccessible, kUnsetID); return; } MOZ_ASSERT(registeredAcc, "Unregistering unregistered accessible"); MOZ_ASSERT(registeredAcc == aAccessible, "Unregistering wrong accessible"); sessionAcc->mIDToAccessibleMap.Remove(virtualViewID); } if (virtualViewID > kNoID) { sIDSet.ReleaseID(virtualViewID); } AccessibleWrap::SetVirtualViewID(aAccessible, kUnsetID); } void SessionAccessibility::UnregisterAll(PresShell* aPresShell) { if (IPCAccessibilityActive()) { // Don't unregister accessible in content process. return; } nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns(); RefPtr sessionAcc = GetInstanceFor(aPresShell); if (sessionAcc) { sessionAcc->mIDToAccessibleMap.Clear(); } }