зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
920578753f
Коммит
239896a063
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче