forked from mirrors/gecko-dev
Actually, `NativeWeakPtr::Detach` may not release JNI ojbect immediately because it depends on JNI object's `OnWeakNonIntrusiveDetach`i implementation. `SessionAccessibility`'s `OnWeakNonIntrusiveDetach` implementation uses the runnable to run on Android UI thread then disposer runs on main thread, so if Detach is finished, JNI object isn't detached yet. If calling `NativeWeakPtrHolder::Attach` immediately with same/recycled Java object after `NettiveWeakPtr::Detach`, it is possible to run disposer for JNI object by `OnWeakNonIntrusiveDetach` after Attach is finished. So it may release newer attached object unfortunately. So I would like to add a way to waiting for detach JNI object using `MozPromise`. Also, `MozPromise.h` includes `Natives.h` header (for `GeckoResult` support). So I cannot modify inline method to use `MozPromise` due to recursive. So I split implementation with `NativesInlines.h` as workaround. Differential Revision: https://phabricator.services.mozilla.com/D175335
826 lines
29 KiB
C++
826 lines
29 KiB
C++
/* -*- 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/jni/NativesInlines.h"
|
|
#include "mozilla/widget/GeckoViewSupport.h"
|
|
#include "mozilla/MouseEvents.h"
|
|
#include "mozilla/dom/MouseEventBinding.h"
|
|
#include "mozilla/StaticPrefs_accessibility.h"
|
|
|
|
#ifdef DEBUG
|
|
# include <android/log.h>
|
|
# 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<AccessibleWrap*>(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<AccessibleWrap*>(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<Settings> {
|
|
public:
|
|
static void ToggleNativeAccessibility(bool aEnable) {
|
|
if (aEnable) {
|
|
GetOrCreateAccService();
|
|
} else {
|
|
MaybeShutdownAccService(nsAccessibilityService::ePlatformAPI);
|
|
}
|
|
}
|
|
};
|
|
|
|
SessionAccessibility::SessionAccessibility(
|
|
jni::NativeWeakPtr<widget::GeckoViewSupport> aWindow,
|
|
java::SessionAccessibility::NativeProvider::Param aSessionAccessibility)
|
|
: mWindow(aWindow), mSessionAccessibility(aSessionAccessibility) {
|
|
SetAttached(true, nullptr);
|
|
}
|
|
|
|
void SessionAccessibility::SetAttached(bool aAttached,
|
|
already_AddRefed<Runnable> aRunnable) {
|
|
if (RefPtr<nsThread> uiThread = GetAndroidUiThread()) {
|
|
uiThread->Dispatch(NS_NewRunnableFunction(
|
|
"SessionAccessibility::Attach",
|
|
[aAttached,
|
|
sa = java::SessionAccessibility::NativeProvider::GlobalRef(
|
|
mSessionAccessibility),
|
|
runnable = RefPtr<Runnable>(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<SessionAccessibility> 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<SessionAccessibility> 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::Pivot(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<SessionAccessibility> 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 && _acc->IsLocal());
|
|
if (LocalAccessible* localAcc = _acc->AsLocal()) {
|
|
static_cast<AccessibleWrap*>(localAcc)->PivotTo(
|
|
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::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> 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 {
|
|
DocAccessibleParent* remoteDoc = aAccessible->AsRemote()->Document();
|
|
if (remoteDoc->mSessionAccessibility) {
|
|
return remoteDoc->mSessionAccessibility;
|
|
}
|
|
dom::CanonicalBrowsingContext* cbc =
|
|
static_cast<dom::BrowserParent*>(remoteDoc->Manager())
|
|
->GetBrowsingContext()
|
|
->Top();
|
|
dom::BrowserParent* bp = cbc->GetBrowserParent();
|
|
if (!bp) {
|
|
bp = static_cast<dom::BrowserParent*>(
|
|
aAccessible->AsRemote()->Document()->Manager());
|
|
}
|
|
if (auto element = bp->GetOwnerElement()) {
|
|
if (auto doc = element->OwnerDoc()) {
|
|
if (nsPresContext* presContext = doc->GetPresContext()) {
|
|
RefPtr<SessionAccessibility> sessionAcc =
|
|
GetInstanceFor(presContext->PresShell());
|
|
remoteDoc->mSessionAccessibility = sessionAcc;
|
|
return sessionAcc;
|
|
}
|
|
} else {
|
|
MOZ_ASSERT_UNREACHABLE(
|
|
"Browser parent's element does not have owner doc.");
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<SessionAccessibility> SessionAccessibility::GetInstanceFor(
|
|
PresShell* aPresShell) {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
if (!aPresShell) {
|
|
return nullptr;
|
|
}
|
|
|
|
nsViewManager* vm = aPresShell->GetViewManager();
|
|
if (!vm) {
|
|
return nullptr;
|
|
}
|
|
|
|
nsCOMPtr<nsIWidget> rootWidget = vm->GetRootWidget();
|
|
// `rootWidget` can be one of several types. Here we make sure it is an
|
|
// android nsWindow.
|
|
if (RefPtr<nsWindow> 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::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 (<a> 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<AccAttributes> 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<int32_t> 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<float>(minValue),
|
|
static_cast<float>(maxValue), static_cast<float>(curValue));
|
|
}
|
|
|
|
if (attributes) {
|
|
Maybe<int32_t> rowIndex =
|
|
attributes->GetAttribute<int32_t>(nsGkAtoms::posinset);
|
|
if (rowIndex) {
|
|
mSessionAccessibility->PopulateNodeCollectionItemInfo(
|
|
aNodeInfo, *rowIndex - 1, 1, 0, 1);
|
|
}
|
|
|
|
Maybe<int32_t> rowCount =
|
|
attributes->GetAttribute<int32_t>(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<SessionAccessibility> sessionAcc = GetInstanceFor(aAccessible);
|
|
if (!sessionAcc) {
|
|
return;
|
|
}
|
|
|
|
bool isTopLevel = false;
|
|
if (aAccessible->IsLocal() && aAccessible->IsDoc()) {
|
|
DocAccessibleWrap* doc =
|
|
static_cast<DocAccessibleWrap*>(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<SessionAccessibility> sessionAcc = GetInstanceFor(aAccessible);
|
|
MOZ_ASSERT(sessionAcc, "Need SessionAccessibility to unregister Accessible!");
|
|
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<SessionAccessibility> sessionAcc = GetInstanceFor(aPresShell);
|
|
if (sessionAcc) {
|
|
sessionAcc->mIDToAccessibleMap.Clear();
|
|
}
|
|
}
|