Bug 1765433 - P2: Add AccessibilityNodeInfo population methods. r=Jamie

We cannot use GeckoBundle anymore because it cannot be constructed in
the UI thread. Instead, have a set of populate methods that take
arguments and set the correct fields in the AccessibilityNodeInfo, or
its optional info objects.

These fields can be called both in the Gecko and UI thread.

Differential Revision: https://phabricator.services.mozilla.com/D144895
This commit is contained in:
Eitan Isaacson 2022-05-04 18:00:47 +00:00
Родитель 920578753f
Коммит 239896a063
3 изменённых файлов: 370 добавлений и 14 удалений

Просмотреть файл

@ -114,18 +114,30 @@ bool SessionAccessibility::IsCacheEnabled() {
return StaticPrefs::accessibility_cache_enabled_AtStartup();
}
mozilla::jni::Object::LocalRef SessionAccessibility::GetNodeInfo(int32_t aID) {
java::GeckoBundle::GlobalRef ret = nullptr;
void SessionAccessibility::GetNodeInfo(int32_t aID,
mozilla::jni::Object::Param aNodeInfo) {
RefPtr<SessionAccessibility> self(this);
nsAppShell::SyncRunEvent([this, self, aID, &ret] {
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);
}
});
}
int SessionAccessibility::GetNodeClassName(int32_t aID) {
MOZ_ASSERT(IsCacheEnabled(), "Cache is enabled");
int32_t classNameEnum = java::SessionAccessibility::CLASSNAME_VIEW;
RefPtr<SessionAccessibility> self(this);
nsAppShell::SyncRunEvent([this, self, aID, &classNameEnum] {
if (Accessible* acc = GetAccessibleByID(aID)) {
ret = ToBundle(acc);
} else {
AALOG("oops, nothing for %d", aID);
classNameEnum = AccessibleWrap::AndroidClass(acc);
}
});
return mozilla::jni::Object::Ref::From(ret);
return classNameEnum;
}
void SessionAccessibility::SetText(int32_t aID, jni::String::Param aText) {
@ -785,6 +797,142 @@ mozilla::java::GeckoBundle::LocalRef SessionAccessibility::ToBundle(
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 (<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("stateRequired", 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, 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));
}
}
}
void SessionAccessibility::RegisterAccessible(Accessible* aAccessible) {
if (IPCAccessibilityActive()) {
// Don't register accessible in content process.

Просмотреть файл

@ -50,7 +50,8 @@ class SessionAccessibility final
using Base::AttachNative;
using Base::DisposeNative;
bool IsCacheEnabled();
jni::Object::LocalRef GetNodeInfo(int32_t aID);
void GetNodeInfo(int32_t aID, mozilla::jni::Object::Param aNodeInfo);
int GetNodeClassName(int32_t aID);
void SetText(int32_t aID, jni::String::Param aText);
void Click(int32_t aID);
void Pivot(int32_t aID, int32_t aGranularity, bool aForward, bool aInclusive);
@ -131,6 +132,9 @@ class SessionAccessibility final
const double& aStep = UnspecifiedNaN<double>(),
AccAttributes* aAttributes = nullptr);
void PopulateNodeInfo(Accessible* aAccessible,
mozilla::jni::Object::Param aNodeInfo);
void SetAttached(bool aAttached, already_AddRefed<Runnable> aRunnable);
jni::NativeWeakPtr<widget::GeckoViewSupport> mWindow; // Parent only

Просмотреть файл

@ -8,8 +8,10 @@ package org.mozilla.geckoview;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.os.Bundle;
import android.text.InputType;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
@ -384,8 +386,21 @@ public class SessionAccessibility {
}
private AccessibilityNodeInfo getNodeFromGecko(final int virtualViewId) {
ThreadUtils.assertOnUiThread();
final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView, virtualViewId);
populateNodeFromBundle(node, nativeProvider.getNodeInfo(virtualViewId), false);
nativeProvider.getNodeInfo(virtualViewId, node);
// We set the bounds in parent here because we need to use the client-to-screen matrix
// and it is only available in the UI thread.
final Rect bounds = new Rect();
node.getBoundsInScreen(bounds);
final Matrix matrix = new Matrix();
mSession.getClientToScreenMatrix(matrix);
final RectF floatBounds = new RectF(bounds);
matrix.mapRect(floatBounds);
floatBounds.roundOut(bounds);
node.setBoundsInParent(bounds);
return node;
}
@ -839,10 +854,7 @@ public class SessionAccessibility {
if (cachedBundle != null) {
eventClassName = cachedBundle.getInt("className");
} else if (nativeProvider.isCacheEnabled()) {
final GeckoBundle bundle = nativeProvider.getNodeInfo(sourceId);
if (bundle != null) {
eventClassName = bundle.getInt("className");
}
eventClassName = nativeProvider.getNodeClassName(sourceId);
}
}
event.setClassName(getClassName(eventClassName));
@ -1022,7 +1034,10 @@ public class SessionAccessibility {
public native boolean isCacheEnabled();
@WrapForJNI(dispatchTo = "current")
public native GeckoBundle getNodeInfo(int id);
public native void getNodeInfo(int id, AccessibilityNodeInfo nodeInfo);
@WrapForJNI(dispatchTo = "current")
public native int getNodeClassName(int id);
@WrapForJNI(dispatchTo = "gecko")
public native void setText(int id, String text);
@ -1119,5 +1134,194 @@ public class SessionAccessibility {
mLastAccessibilityFocusable = lastNode;
}
}
@WrapForJNI
private void populateNodeInfo(
final AccessibilityNodeInfo node,
final int id,
final int parentId,
final int[] children,
final int flags,
final int className,
final int[] bounds,
@Nullable final String text,
@Nullable final String description,
@Nullable final String hint,
@Nullable final String geckoRole,
@Nullable final String roleDescription,
@Nullable final String viewIdResourceName,
final int inputType) {
if (mView == null) {
return;
}
final boolean isRoot = id == View.NO_ID;
if (isRoot) {
if (Build.VERSION.SDK_INT < 17 || mView.getDisplay() != null) {
// When running junit tests we don't have a display
mView.onInitializeAccessibilityNodeInfo(node);
}
node.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
node.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
} else {
node.setParent(mView, parentId);
}
// The basics
node.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
node.setClassName(getClassName(className));
if (text != null) {
node.setText(text);
}
if (description != null) {
node.setContentDescription(description);
}
// Add actions
node.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
node.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
node.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
node.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
node.setMovementGranularities(
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER
| AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD
| AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE
| AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH);
if ((flags & FLAG_CLICKABLE) != 0) {
node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
}
// Set boolean properties
node.setCheckable((flags & FLAG_CHECKABLE) != 0);
node.setChecked((flags & FLAG_CHECKED) != 0);
node.setClickable((flags & FLAG_CLICKABLE) != 0);
node.setEnabled((flags & FLAG_ENABLED) != 0);
node.setFocusable((flags & FLAG_FOCUSABLE) != 0);
node.setLongClickable((flags & FLAG_LONG_CLICKABLE) != 0);
node.setPassword((flags & FLAG_PASSWORD) != 0);
node.setScrollable((flags & FLAG_SCROLLABLE) != 0);
node.setSelected((flags & FLAG_SELECTED) != 0);
node.setVisibleToUser((flags & FLAG_VISIBLE_TO_USER) != 0);
// Other boolean properties to consider later:
// setHeading, setImportantForAccessibility, setScreenReaderFocusable, setShowingHintText,
// setDismissable
if (mAccessibilityFocusedNode == id) {
node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
node.setAccessibilityFocused(true);
} else {
node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
}
node.setFocused(mFocusedNode == id);
final Rect screenBounds = new Rect(bounds[0], bounds[1], bounds[2], bounds[3]);
node.setBoundsInScreen(screenBounds);
for (final int childId : children) {
node.addChild(mView, childId);
}
// SDK 18 and above
if (Build.VERSION.SDK_INT >= 18) {
node.setViewIdResourceName(viewIdResourceName);
if ((flags & FLAG_EDITABLE) != 0) {
node.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
node.addAction(AccessibilityNodeInfo.ACTION_CUT);
node.addAction(AccessibilityNodeInfo.ACTION_COPY);
node.addAction(AccessibilityNodeInfo.ACTION_PASTE);
node.setEditable(true);
}
}
// SDK 19 and above
if (Build.VERSION.SDK_INT >= 19) {
node.setMultiLine((flags & FLAG_MULTI_LINE) != 0);
node.setContentInvalid((flags & FLAG_CONTENT_INVALID) != 0);
// Set bundle keys like role and hint
final Bundle bundle = node.getExtras();
if (hint != null) {
bundle.putCharSequence("AccessibilityNodeInfo.hint", hint);
if (Build.VERSION.SDK_INT >= 26) {
node.setHintText(hint);
}
}
if (geckoRole != null) {
bundle.putCharSequence("AccessibilityNodeInfo.geckoRole", geckoRole);
}
if (roleDescription != null) {
bundle.putCharSequence("AccessibilityNodeInfo.roleDescription", roleDescription);
}
if (isRoot) {
// Argument values for ACTION_NEXT_HTML_ELEMENT/ACTION_PREVIOUS_HTML_ELEMENT.
// This is mostly here to let TalkBack know we are a legit "WebView".
bundle.putCharSequence(
"ACTION_ARGUMENT_HTML_ELEMENT_STRING_VALUES",
TextUtils.join(",", sHtmlGranularities));
}
if (inputType != InputType.TYPE_NULL) {
node.setInputType(inputType);
}
}
// SDK 21 and above
if (Build.VERSION.SDK_INT >= 21) {
if ((flags & FLAG_EXPANDABLE) != 0) {
if ((flags & FLAG_EXPANDED) != 0) {
node.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
node.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE);
} else {
node.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE);
node.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
}
}
}
// SDK 23 and above
if (Build.VERSION.SDK_INT >= 23) {
node.setContextClickable((flags & FLAG_CONTEXT_CLICKABLE) != 0);
}
}
@WrapForJNI
private void populateNodeCollectionItemInfo(
final AccessibilityNodeInfo node,
final int rowIndex,
final int rowSpan,
final int columnIndex,
final int columnSpan) {
final CollectionItemInfo collectionItemInfo =
CollectionItemInfo.obtain(rowIndex, rowSpan, columnIndex, columnSpan, false);
node.setCollectionItemInfo(collectionItemInfo);
}
@WrapForJNI
private void populateNodeCollectionInfo(
final AccessibilityNodeInfo node,
final int rowCount,
final int columnCount,
final int selectionMode,
final boolean isHierarchical) {
final CollectionInfo collectionInfo =
Build.VERSION.SDK_INT >= 21
? CollectionInfo.obtain(rowCount, columnCount, isHierarchical, selectionMode)
: CollectionInfo.obtain(rowCount, columnCount, isHierarchical);
node.setCollectionInfo(collectionInfo);
}
@WrapForJNI
private void populateNodeRangeInfo(
final AccessibilityNodeInfo node,
final int rangeType,
final float min,
final float max,
final float current) {
final RangeInfo rangeInfo = RangeInfo.obtain(rangeType, min, max, current);
node.setRangeInfo(rangeInfo);
}
}
}