/* -*- 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 "HyperTextAccessible.h" #include "HyperTextAccessible-inl.h" #include "JavaBuiltins.h" #include "RootAccessibleWrap.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/DocAccessiblePlatformExtParent.h" #include "mozilla/a11y/DocManager.h" #include "mozilla/jni/GeckoBundleUtils.h" #include "mozilla/widget/GeckoViewSupport.h" #include "mozilla/MouseEvents.h" #include "mozilla/dom/MouseEventBinding.h" #include "mozilla/StaticPrefs_accessibility.h" #ifdef DEBUG # include # define AALOG(args...) \ __android_log_print(ANDROID_LOG_INFO, "GeckoAccessibilityNative", ##args) #else # define AALOG(args...) \ do { \ } while (0) #endif #define FORWARD_ACTION_TO_ACCESSIBLE(funcname, ...) \ MOZ_ASSERT(NS_IsMainThread()); \ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); \ if (Accessible* acc = GetAccessibleByID(aID)) { \ if (acc->IsRemote()) { \ acc->AsRemote()->funcname(__VA_ARGS__); \ } else { \ static_cast(acc->AsLocal())->funcname(__VA_ARGS__); \ } \ } #define FORWARD_EXT_ACTION_TO_ACCESSIBLE(funcname, ...) \ MOZ_ASSERT(NS_IsMainThread()); \ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); \ if (Accessible* acc = GetAccessibleByID(aID)) { \ if (RemoteAccessible* remote = acc->AsRemote()) { \ Unused << remote->Document()->GetPlatformExtension()->Send##funcname( \ remote->ID(), ##__VA_ARGS__); \ } else { \ static_cast(acc->AsLocal())->funcname(__VA_ARGS__); \ } \ } 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(); } bool SessionAccessibility::IsCacheEnabled() { return StaticPrefs::accessibility_cache_enabled_AtStartup(); } 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() || !IsCacheEnabled()) { 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()); MOZ_ASSERT(IsCacheEnabled(), "Cache is enabled"); 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) { FORWARD_ACTION_TO_ACCESSIBLE(DoAction, 0); } bool SessionAccessibility::CachedPivot(int32_t aID, int32_t aGranularity, bool aForward, bool aInclusive) { MOZ_ASSERT(IsCacheEnabled(), "Cache is enabled"); 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] { Pivot(aID, aGranularity, aForward, aInclusive); }); 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); } }); return true; } } return false; } void SessionAccessibility::Pivot(int32_t aID, int32_t aGranularity, bool aForward, bool aInclusive) { FORWARD_EXT_ACTION_TO_ACCESSIBLE(PivotTo, aGranularity, aForward, aInclusive); } void SessionAccessibility::ExploreByTouch(int32_t aID, float aX, float aY) { auto gvAccessor(mWindow.Access()); if (gvAccessor) { if (nsWindow* gkWindow = gvAccessor->GetNsWindow()) { WidgetMouseEvent hittest(true, eMouseExploreByTouch, gkWindow, WidgetMouseEvent::eReal); hittest.mRefPoint = LayoutDeviceIntPoint::Floor(aX, aY); hittest.mInputSource = dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH; hittest.mFlags.mOnlyChromeDispatch = true; gkWindow->DispatchInputEvent(&hittest); } } } void SessionAccessibility::NavigateText(int32_t aID, int32_t aGranularity, int32_t aStartOffset, int32_t aEndOffset, bool aForward, bool aSelect) { FORWARD_EXT_ACTION_TO_ACCESSIBLE(NavigateText, aGranularity, aStartOffset, aEndOffset, aForward, aSelect); } void SessionAccessibility::SetSelection(int32_t aID, int32_t aStart, int32_t aEnd) { FORWARD_EXT_ACTION_TO_ACCESSIBLE(SetSelection, aStart, aEnd); } void SessionAccessibility::Cut(int32_t aID) { FORWARD_EXT_ACTION_TO_ACCESSIBLE(Cut); } void SessionAccessibility::Copy(int32_t aID) { FORWARD_EXT_ACTION_TO_ACCESSIBLE(Copy); } void SessionAccessibility::Paste(int32_t aID) { FORWARD_EXT_ACTION_TO_ACCESSIBLE(Paste); } #undef FORWARD_ACTION_TO_ACCESSIBLE #undef FORWARD_EXT_ACTION_TO_ACCESSIBLE 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) { MOZ_ASSERT(NS_IsMainThread()); mSessionAccessibility->SendEvent( java::sdk::AccessibilityEvent::TYPE_VIEW_ACCESSIBILITY_FOCUSED, AccessibleWrap::GetVirtualViewID(aAccessible), AccessibleWrap::AndroidClass(aAccessible), nullptr); 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); if (IsCacheEnabled()) { 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 = false; if (aAccessible->IsRemote() && !IsCacheEnabled()) { nsAutoString unused; hasSelection = aAccessible->AsRemote()->SelectionBoundsAt( 0, unused, &startSel, &endSel); } else { hasSelection = aAccessible->AsHyperTextBase()->SelectionBoundsAt( 0, &startSel, &endSel); } if (hasSelection) { fromIndex = startSel == aCaretOffset ? endSel : startSel; } GECKOBUNDLE_START(eventInfo); 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()) { if (aAccessible->IsRemote() && !IsCacheEnabled()) { // XXX: AppendTextTo is not implemented in the IPDL and only // works when cache is enabled. aAccessible->Name(text); } else { 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, "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()) { if (aAccessible->IsRemote() && !IsCacheEnabled()) { // XXX: AppendTextTo is not implemented in the IPDL and only // works when cache is enabled. aAccessible->Name(text); } else { 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::ReplaceViewportCache( const nsTArray& aAccessibles, const nsTArray& aData) { auto infos = jni::ObjectArray::New(aAccessibles.Length()); for (size_t i = 0; i < aAccessibles.Length(); i++) { Accessible* acc = aAccessibles.ElementAt(i); if (!acc) { MOZ_ASSERT_UNREACHABLE("Updated accessible is gone."); continue; } if (aData.Length() == aAccessibles.Length()) { const BatchData& data = aData.ElementAt(i); auto bundle = ToBundle(acc, data.State(), data.Bounds(), data.ActionCount(), data.Name(), data.TextValue(), data.DOMNodeID(), data.Description()); infos->SetElement(i, bundle); } else { infos->SetElement(i, ToBundle(acc, true)); } } mSessionAccessibility->ReplaceViewportCache(infos); SendWindowContentChangedEvent(); } void SessionAccessibility::ReplaceFocusPathCache( const nsTArray& aAccessibles, const nsTArray& aData) { auto infos = jni::ObjectArray::New(aAccessibles.Length()); for (size_t i = 0; i < aAccessibles.Length(); i++) { Accessible* acc = aAccessibles.ElementAt(i); if (!acc) { MOZ_ASSERT_UNREACHABLE("Updated accessible is gone."); continue; } if (aData.Length() == aAccessibles.Length()) { const BatchData& data = aData.ElementAt(i); auto bundle = ToBundle(acc, data.State(), data.Bounds(), data.ActionCount(), data.Name(), data.TextValue(), data.DOMNodeID(), data.Description(), data.CurValue(), data.MinValue(), data.MaxValue(), data.Step(), data.Attributes()); infos->SetElement(i, bundle); } else { infos->SetElement(i, ToBundle(acc)); } } mSessionAccessibility->ReplaceFocusPathCache(infos); } void SessionAccessibility::UpdateCachedBounds( const nsTArray& aAccessibles, const nsTArray& aData) { auto infos = jni::ObjectArray::New(aAccessibles.Length()); for (size_t i = 0; i < aAccessibles.Length(); i++) { Accessible* acc = aAccessibles.ElementAt(i); if (!acc) { MOZ_ASSERT_UNREACHABLE("Updated accessible is gone."); continue; } if (aData.Length() == aAccessibles.Length()) { const BatchData& data = aData.ElementAt(i); auto bundle = ToBundle(acc, data.State(), data.Bounds(), data.ActionCount(), data.Name(), data.TextValue(), data.DOMNodeID(), data.Description()); infos->SetElement(i, bundle); } else { infos->SetElement(i, ToBundle(acc, true)); } } mSessionAccessibility->UpdateCachedBounds(infos); } void SessionAccessibility::UpdateAccessibleFocusBoundaries(Accessible* aFirst, Accessible* aLast) { mSessionAccessibility->UpdateAccessibleFocusBoundaries( aFirst ? AccessibleWrap::GetVirtualViewID(aFirst) : kNoID, aLast ? AccessibleWrap::GetVirtualViewID(aLast) : kNoID); } mozilla::java::GeckoBundle::LocalRef SessionAccessibility::ToBundle( Accessible* aAccessible, bool aSmall) { nsAutoString name; aAccessible->Name(name); nsAutoString textValue; aAccessible->Value(textValue); nsAutoString nodeID; aAccessible->DOMNodeID(nodeID); nsAutoString description; aAccessible->Description(description); uint64_t state = aAccessible->State(); LayoutDeviceIntRect bounds = aAccessible->Bounds(); uint8_t actionCount = aAccessible->ActionCount(); if (aSmall) { return ToBundle(aAccessible, state, bounds, actionCount, name, textValue, nodeID, description); } double curValue = UnspecifiedNaN(); double minValue = UnspecifiedNaN(); double maxValue = UnspecifiedNaN(); double step = UnspecifiedNaN(); if (aAccessible->HasNumericValue()) { curValue = aAccessible->CurValue(); minValue = aAccessible->MinValue(); maxValue = aAccessible->MaxValue(); step = aAccessible->Step(); } RefPtr attributes = aAccessible->Attributes(); return ToBundle(aAccessible, state, bounds, actionCount, name, textValue, nodeID, description, curValue, minValue, maxValue, step, attributes); } mozilla::java::GeckoBundle::LocalRef SessionAccessibility::ToBundle( Accessible* aAccessible, const uint64_t aState, const LayoutDeviceIntRect& aBounds, const uint8_t aActionCount, const nsString& aName, const nsString& aTextValue, const nsString& aDOMNodeID, const nsString& aDescription, const double& aCurVal, const double& aMinVal, const double& aMaxVal, const double& aStep, AccAttributes* aAttributes) { MOZ_ASSERT(NS_IsMainThread()); int32_t virtualViewID = AccessibleWrap::GetVirtualViewID(aAccessible); GECKOBUNDLE_START(nodeInfo); GECKOBUNDLE_PUT(nodeInfo, "id", java::sdk::Integer::ValueOf(virtualViewID)); Accessible* parent = virtualViewID != kNoID ? aAccessible->Parent() : nullptr; GECKOBUNDLE_PUT(nodeInfo, "parentId", java::sdk::Integer::ValueOf( parent ? AccessibleWrap::GetVirtualViewID(parent) : 0)); role role = aAccessible->Role(); if (role == roles::LINK && !(aState & 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, aState, aActionCount); GECKOBUNDLE_PUT(nodeInfo, "flags", java::sdk::Integer::ValueOf(flags)); GECKOBUNDLE_PUT( nodeInfo, "className", java::sdk::Integer::ValueOf(AccessibleWrap::AndroidClass(aAccessible))); nsAutoString hint; if (aState & states::EDITABLE) { // An editable field's name is populated in the hint. hint.Assign(aName); GECKOBUNDLE_PUT(nodeInfo, "text", jni::StringParam(aTextValue)); } else { if (role == roles::LINK || role == roles::HEADING) { GECKOBUNDLE_PUT(nodeInfo, "description", jni::StringParam(aName)); } else { GECKOBUNDLE_PUT(nodeInfo, "text", jni::StringParam(aName)); } } if (!aDescription.IsEmpty()) { if (!hint.IsEmpty()) { // If this is an editable, the description is concatenated with a // whitespace directly after the name. hint.AppendLiteral(" "); } hint.Append(aDescription); } if ((aState & 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); } } if (!hint.IsEmpty()) { GECKOBUNDLE_PUT(nodeInfo, "hint", jni::StringParam(hint)); } nsAutoString geckoRole; nsAutoString roleDescription; if (virtualViewID != kNoID) { AccessibleWrap::GetRoleDescription(role, aAttributes, geckoRole, roleDescription); } GECKOBUNDLE_PUT(nodeInfo, "roleDescription", jni::StringParam(roleDescription)); GECKOBUNDLE_PUT(nodeInfo, "geckoRole", jni::StringParam(geckoRole)); if (!aDOMNodeID.IsEmpty()) { GECKOBUNDLE_PUT(nodeInfo, "viewIdResourceName", jni::StringParam(aDOMNodeID)); } const int32_t data[4] = {aBounds.x, aBounds.y, aBounds.x + aBounds.width, aBounds.y + aBounds.height}; GECKOBUNDLE_PUT(nodeInfo, "bounds", jni::IntArray::New(data, 4)); if (aAccessible->HasNumericValue()) { GECKOBUNDLE_START(rangeInfo); if (aMaxVal == 1 && aMinVal == 0) { GECKOBUNDLE_PUT(rangeInfo, "type", java::sdk::Integer::ValueOf(2)); // percent } else if (std::round(aStep) != aStep) { GECKOBUNDLE_PUT(rangeInfo, "type", java::sdk::Integer::ValueOf(1)); // float } else { GECKOBUNDLE_PUT(rangeInfo, "type", java::sdk::Integer::ValueOf(0)); // integer } if (!IsNaN(aCurVal)) { GECKOBUNDLE_PUT(rangeInfo, "current", java::sdk::Double::New(aCurVal)); } if (!IsNaN(aMinVal)) { GECKOBUNDLE_PUT(rangeInfo, "min", java::sdk::Double::New(aMinVal)); } if (!IsNaN(aMaxVal)) { GECKOBUNDLE_PUT(rangeInfo, "max", java::sdk::Double::New(aMaxVal)); } GECKOBUNDLE_FINISH(rangeInfo); GECKOBUNDLE_PUT(nodeInfo, "rangeInfo", rangeInfo); } if (aAttributes) { nsString inputTypeAttr; aAttributes->GetAttribute(nsGkAtoms::textInputType, inputTypeAttr); int32_t inputType = AccessibleWrap::GetInputType(inputTypeAttr); if (inputType) { GECKOBUNDLE_PUT(nodeInfo, "inputType", java::sdk::Integer::ValueOf(inputType)); } Maybe rowIndex = aAttributes->GetAttribute(nsGkAtoms::posinset); if (rowIndex) { GECKOBUNDLE_START(collectionItemInfo); GECKOBUNDLE_PUT(collectionItemInfo, "rowIndex", java::sdk::Integer::ValueOf(*rowIndex)); GECKOBUNDLE_PUT(collectionItemInfo, "columnIndex", java::sdk::Integer::ValueOf(0)); GECKOBUNDLE_PUT(collectionItemInfo, "rowSpan", java::sdk::Integer::ValueOf(1)); GECKOBUNDLE_PUT(collectionItemInfo, "columnSpan", java::sdk::Integer::ValueOf(1)); GECKOBUNDLE_FINISH(collectionItemInfo); GECKOBUNDLE_PUT(nodeInfo, "collectionItemInfo", collectionItemInfo); } Maybe rowCount = aAttributes->GetAttribute(nsGkAtoms::child_item_count); if (rowCount) { GECKOBUNDLE_START(collectionInfo); GECKOBUNDLE_PUT(collectionInfo, "rowCount", java::sdk::Integer::ValueOf(*rowCount)); GECKOBUNDLE_PUT(collectionInfo, "columnCount", java::sdk::Integer::ValueOf(1)); if (aAttributes->HasAttribute(nsGkAtoms::tree)) { GECKOBUNDLE_PUT(collectionInfo, "isHierarchical", java::sdk::Boolean::TRUE()); } if (aAccessible->IsSelect()) { int32_t selectionMode = (aState & states::MULTISELECTABLE) ? 2 : 1; GECKOBUNDLE_PUT(collectionInfo, "selectionMode", java::sdk::Integer::ValueOf(selectionMode)); } GECKOBUNDLE_FINISH(collectionInfo); GECKOBUNDLE_PUT(nodeInfo, "collectionInfo", collectionInfo); } } if (!nsAccUtils::MustPrune(aAccessible)) { auto childCount = aAccessible->ChildCount(); nsTArray children(childCount); for (uint32_t i = 0; i < childCount; i++) { auto child = aAccessible->ChildAt(i); children.AppendElement(AccessibleWrap::GetVirtualViewID(child)); } GECKOBUNDLE_PUT(nodeInfo, "children", jni::IntArray::New(children.Elements(), children.Length())); } GECKOBUNDLE_FINISH(nodeInfo); return nodeInfo; } 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, 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 { Accessible* accessible = mIDToAccessibleMap.Get(aID); if (accessible && accessible->IsLocal() && accessible->AsLocal()->IsDefunct()) { MOZ_ASSERT_UNREACHABLE("Registered accessible is defunct!"); return nullptr; } return accessible; } #ifdef DEBUG static bool IsDetachedDoc(Accessible* aAccessible) { if (!aAccessible->IsRemote() || !aAccessible->AsRemote()->IsDoc()) { return false; } return !aAccessible->Parent() || aAccessible->Parent()->FirstChild() != aAccessible; } #endif 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(); } }