/* -*- 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 "DocAccessibleParent.h" #include "nsThreadUtils.h" #include "AccAttributes.h" #include "AccessibilityEvent.h" #include "HyperTextAccessible.h" #include "JavaBuiltins.h" #include "RootAccessibleWrap.h" #include "nsAccessibilityService.h" #include "nsViewManager.h" #include "mozilla/PresShell.h" #include "mozilla/dom/BrowserParent.h" #include "mozilla/a11y/DocAccessibleParent.h" #include "mozilla/a11y/DocManager.h" #include "mozilla/jni/GeckoBundleUtils.h" #include "mozilla/widget/GeckoViewSupport.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, ...) \ if (RootAccessibleWrap* rootAcc = GetRoot()) { \ AccessibleWrap* acc = rootAcc->FindAccessibleById(aID); \ if (!acc) { \ return; \ } \ \ acc->funcname(__VA_ARGS__); \ } using namespace mozilla::a11y; 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(); } mozilla::jni::Object::LocalRef SessionAccessibility::GetNodeInfo(int32_t aID) { java::GeckoBundle::GlobalRef ret = nullptr; RefPtr self(this); nsAppShell::SyncRunEvent([this, self, aID, &ret] { if (RootAccessibleWrap* rootAcc = GetRoot()) { AccessibleWrap* acc = rootAcc->FindAccessibleById(aID); if (acc) { ret = acc->ToBundle(); } else { AALOG("oops, nothing for %d", aID); } } }); return mozilla::jni::Object::Ref::From(ret); } RootAccessibleWrap* SessionAccessibility::GetRoot() { auto acc(mWindow.Access()); if (!acc) { return nullptr; } nsWindow* gkWindow = acc->GetNsWindow(); if (!gkWindow) { return nullptr; } return static_cast(gkWindow->GetRootAccessible()); } void SessionAccessibility::SetText(int32_t aID, jni::String::Param aText) { FORWARD_ACTION_TO_ACCESSIBLE(SetTextContents, aText->ToString()); } void SessionAccessibility::Click(int32_t aID) { FORWARD_ACTION_TO_ACCESSIBLE(DoAction, 0); } void SessionAccessibility::Pivot(int32_t aID, int32_t aGranularity, bool aForward, bool aInclusive) { FORWARD_ACTION_TO_ACCESSIBLE(PivotTo, aGranularity, aForward, aInclusive); } void SessionAccessibility::ExploreByTouch(int32_t aID, float aX, float aY) { FORWARD_ACTION_TO_ACCESSIBLE(ExploreByTouch, aX, aY); } void SessionAccessibility::NavigateText(int32_t aID, int32_t aGranularity, int32_t aStartOffset, int32_t aEndOffset, bool aForward, bool aSelect) { FORWARD_ACTION_TO_ACCESSIBLE(NavigateText, aGranularity, aStartOffset, aEndOffset, aForward, aSelect); } void SessionAccessibility::SetSelection(int32_t aID, int32_t aStart, int32_t aEnd) { FORWARD_ACTION_TO_ACCESSIBLE(SetSelection, aStart, aEnd); } void SessionAccessibility::Cut(int32_t aID) { FORWARD_ACTION_TO_ACCESSIBLE(Cut); } void SessionAccessibility::Copy(int32_t aID) { FORWARD_ACTION_TO_ACCESSIBLE(Copy); } void SessionAccessibility::Paste(int32_t aID) { FORWARD_ACTION_TO_ACCESSIBLE(Paste); } RefPtr SessionAccessibility::GetInstanceFor( RemoteAccessible* aAccessible) { auto tab = static_cast(aAccessible->Document()->Manager()); dom::Element* frame = tab->GetOwnerElement(); MOZ_ASSERT(frame); if (!frame) { return nullptr; } LocalAccessible* chromeDoc = GetExistingDocAccessible(frame->OwnerDoc()); return chromeDoc ? GetInstanceFor(chromeDoc) : nullptr; } RefPtr SessionAccessibility::GetInstanceFor( LocalAccessible* aAccessible) { RootAccessible* rootAcc = aAccessible->RootAccessible(); nsViewManager* vm = rootAcc->PresShellPtr()->GetViewManager(); if (!vm) { return nullptr; } nsCOMPtr rootWidget; vm->GetRootWidget(getter_AddRefs(rootWidget)); // `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( AccessibleWrap* aAccessible) { mSessionAccessibility->SendEvent( java::sdk::AccessibilityEvent::TYPE_VIEW_ACCESSIBILITY_FOCUSED, aAccessible->VirtualViewID(), aAccessible->AndroidClass(), nullptr); aAccessible->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE); } void SessionAccessibility::SendHoverEnterEvent(AccessibleWrap* aAccessible) { mSessionAccessibility->SendEvent( java::sdk::AccessibilityEvent::TYPE_VIEW_HOVER_ENTER, aAccessible->VirtualViewID(), aAccessible->AndroidClass(), nullptr); } void SessionAccessibility::SendFocusEvent(AccessibleWrap* aAccessible) { // 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, aAccessible->VirtualViewID(), aAccessible->AndroidClass(), nullptr); } void SessionAccessibility::SendScrollingEvent(AccessibleWrap* aAccessible, int32_t aScrollX, int32_t aScrollY, int32_t aMaxScrollX, int32_t aMaxScrollY) { int32_t virtualViewId = aAccessible->VirtualViewID(); if (virtualViewId != AccessibleWrap::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, aAccessible->AndroidClass(), eventInfo); } void SessionAccessibility::SendWindowContentChangedEvent() { mSessionAccessibility->SendEvent( java::sdk::AccessibilityEvent::TYPE_WINDOW_CONTENT_CHANGED, AccessibleWrap::kNoID, java::SessionAccessibility::CLASSNAME_WEBVIEW, nullptr); } void SessionAccessibility::SendWindowStateChangedEvent( AccessibleWrap* aAccessible) { // 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, aAccessible->VirtualViewID(), aAccessible->AndroidClass(), nullptr); } void SessionAccessibility::SendTextSelectionChangedEvent( AccessibleWrap* aAccessible, int32_t aCaretOffset) { int32_t fromIndex = aCaretOffset; int32_t startSel = -1; int32_t endSel = -1; if (aAccessible->GetSelectionBounds(&startSel, &endSel)) { 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, aAccessible->VirtualViewID(), aAccessible->AndroidClass(), eventInfo); } void SessionAccessibility::SendTextChangedEvent(AccessibleWrap* aAccessible, const nsString& aStr, int32_t aStart, uint32_t aLen, bool aIsInsert, bool aFromUser) { if (!aFromUser) { // Only dispatch text change events from users, for now. return; } nsAutoString text; aAccessible->GetTextContents(text); 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, aAccessible->VirtualViewID(), aAccessible->AndroidClass(), eventInfo); } void SessionAccessibility::SendTextTraversedEvent(AccessibleWrap* aAccessible, int32_t aStartOffset, int32_t aEndOffset) { nsAutoString text; aAccessible->GetTextContents(text); 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, aAccessible->VirtualViewID(), aAccessible->AndroidClass(), eventInfo); } void SessionAccessibility::SendClickedEvent(AccessibleWrap* 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, aAccessible->VirtualViewID(), aAccessible->AndroidClass(), eventInfo); } void SessionAccessibility::SendSelectedEvent(AccessibleWrap* aAccessible, bool aSelected) { 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, aAccessible->VirtualViewID(), aAccessible->AndroidClass(), eventInfo); } void SessionAccessibility::SendAnnouncementEvent(AccessibleWrap* aAccessible, const nsString& aAnnouncement, uint16_t aPriority) { 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, AccessibleWrap::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++) { AccessibleWrap* 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 = acc->ToBundle( data.State(), data.Bounds(), data.ActionCount(), data.Name(), data.TextValue(), data.DOMNodeID(), data.Description()); infos->SetElement(i, bundle); } else { infos->SetElement(i, acc->ToBundle(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++) { AccessibleWrap* 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 = acc->ToBundle(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, acc->ToBundle()); } } 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++) { AccessibleWrap* 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 = acc->ToBundle( data.State(), data.Bounds(), data.ActionCount(), data.Name(), data.TextValue(), data.DOMNodeID(), data.Description()); infos->SetElement(i, bundle); } else { infos->SetElement(i, acc->ToBundle(true)); } } mSessionAccessibility->UpdateCachedBounds(infos); SendWindowContentChangedEvent(); } void SessionAccessibility::UpdateAccessibleFocusBoundaries( AccessibleWrap* aFirst, AccessibleWrap* aLast) { mSessionAccessibility->UpdateAccessibleFocusBoundaries( aFirst ? aFirst->VirtualViewID() : AccessibleWrap::kNoID, aLast ? aLast->VirtualViewID() : AccessibleWrap::kNoID); } #undef FORWARD_ACTION_TO_ACCESSIBLE