diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
index f2d194275c3d4..41e128ecd63f7 100644
--- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
+++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
@@ -20,6 +20,8 @@
import io.flutter.embedding.engine.FlutterEngine.EngineLifecycleListener;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;
+import io.flutter.plugin.common.StandardMessageCodec;
+import io.flutter.view.AccessibilityBridge;
/**
* Interface between Flutter embedding's Java code and Flutter engine's C/C++ code.
@@ -323,6 +325,22 @@ private native void nativeDispatchPointerDataPacket(long nativePlatformViewId,
ByteBuffer buffer,
int position);
+ public void dispatchSemanticsAction(int id, @NonNull AccessibilityBridge.Action action) {
+ dispatchSemanticsAction(id, action, null);
+ }
+
+ public void dispatchSemanticsAction(int id, @NonNull AccessibilityBridge.Action action, @Nullable Object args) {
+ ensureAttachedToNative();
+
+ ByteBuffer encodedArgs = null;
+ int position = 0;
+ if (args != null) {
+ encodedArgs = StandardMessageCodec.INSTANCE.encodeMessage(args);
+ position = encodedArgs.position();
+ }
+ dispatchSemanticsAction(id, action.value, encodedArgs, position);
+ }
+
@UiThread
public void dispatchSemanticsAction(int id, int action, ByteBuffer args, int argsPosition) {
ensureAttachedToNative();
diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java
index 4bfcd37973b58..7eed2619bebda 100644
--- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java
+++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java
@@ -103,17 +103,17 @@ public interface AccessibilityMessageHandler {
void announce(@NonNull String message);
/**
- * The user has tapped on the artifact with the given {@code nodeId}.
+ * The user has tapped on the widget with the given {@code nodeId}.
*/
void onTap(int nodeId);
/**
- * The user has long pressed on the artifact with the given {@code nodeId}.
+ * The user has long pressed on the widget with the given {@code nodeId}.
*/
void onLongPress(int nodeId);
/**
- * The user has opened a popup window, menu, dialog, etc.
+ * The user has opened a tooltip.
*/
void onTooltip(@NonNull String message);
}
diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java
index add434080c054..812170155ec75 100644
--- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java
+++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java
@@ -6,28 +6,69 @@
import android.annotation.TargetApi;
import android.app.Activity;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
import android.graphics.Rect;
+import android.net.Uri;
import android.opengl.Matrix;
import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
+import android.provider.Settings;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import android.util.Log;
+import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
+import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
import io.flutter.util.Predicate;
import java.nio.ByteBuffer;
import java.util.*;
-class AccessibilityBridge
- extends AccessibilityNodeProvider {
- private static final String TAG = "FlutterView";
+/**
+ * Bridge between Android's OS accessibility system and Flutter's accessibility system.
+ *
+ * An {@code AccessibilityBridge} requires:
+ *
+ * - A real Android {@link View}, called the {@link #rootAccessibilityView}, which contains a
+ * Flutter UI. The {@link #rootAccessibilityView} is required at the time of
+ * {@code AccessibilityBridge}'s instantiation and is held for the duration of
+ * {@code AccessibilityBridge}'s lifespan. {@code AccessibilityBridge} invokes various
+ * accessibility methods on the {@link #rootAccessibilityView}, e.g.,
+ * {@link View#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)}. The
+ * {@link #rootAccessibilityView} is expected to notify the {@code AccessibilityBridge} of
+ * relevant interactions: {@link #onAccessibilityHoverEvent(MotionEvent)}, {@link #reset()},
+ * {@link #updateSemantics(ByteBuffer, String[])}, and {@link #updateCustomAccessibilityActions(ByteBuffer, String[])}
+ * - A {@link FlutterJNI} instance, corresponding to the running Flutter app.
+ * - An {@link AccessibilityChannel} that is connected to the running Flutter app.
+ * - Android's {@link AccessibilityManager} to query and listen for accessibility settings.
+ * - Android's {@link ContentResolver} to listen for changes to system animation settings.
+ *
+ *
+ * The {@code AccessibilityBridge} causes Android to treat Flutter {@code SemanticsNode}s as if
+ * they were accessible Android {@link View}s. Accessibility requests may be sent from
+ * a Flutter widget to the Android OS, as if it were an Android {@link View}, and
+ * accessibility events may be consumed by a Flutter widget, as if it were an Android
+ * {@link View}. {@code AccessibilityBridge} refers to Flutter's accessible widgets as
+ * "virtual views" and identifies them with "virtual view IDs".
+ *
+ * Most communication between the Android OS accessibility system and Flutter's accessibility
+ * system is achieved via the {@link AccessibilityChannel} system channel. However, some
+ * information is exchanged directly between the Android embedding and Flutter framework
+ * via {@link FlutterJNI}.
+ * TODO(mattcarroll): consider moving FlutterJNI calls over to AccessibilityChannel
+ */
+public class AccessibilityBridge extends AccessibilityNodeProvider {
+ private static final String TAG = "AccessibilityBridge";
// Constants from higher API levels.
// TODO(goderbauer): Get these from Android Support Library when
@@ -38,35 +79,160 @@ class AccessibilityBridge
private static final float SCROLL_POSITION_CAP_FOR_INFINITY = 70000.0f;
private static final int ROOT_NODE_ID = 0;
- private final FlutterView owner;
+ /// Value is derived from ACTION_TYPE_MASK in AccessibilityNodeInfo.java
+ private static int FIRST_RESOURCE_ID = 267386881;
+
+ // Real Android View, which internally holds a Flutter UI.
+ @NonNull
+ private final View rootAccessibilityView;
+
+ // Direct interface between Flutter's Android embedding and the Flutter framework.
+ @NonNull
+ private final FlutterJNI flutterJNI;
+
+ // The accessibility communication API between Flutter's Android embedding and
+ // the Flutter framework.
+ @NonNull
private final AccessibilityChannel accessibilityChannel;
+
+ // Android's {@link AccessibilityManager}, which we can query to see if accessibility is
+ // turned on, as well as listen for changes to accessibility's activation.
+ @NonNull
+ private final AccessibilityManager accessibilityManager;
+
+ // Android's {@link ContentResolver}, which is used to observe the global TRANSITION_ANIMATION_SCALE,
+ // which determines whether Flutter's animations should be enabled or disabled for accessibility
+ // purposes.
+ @NonNull
+ private final ContentResolver contentResolver;
+
+ // The top-level Android View within the containing Window.
+ // TODO(mattcarroll): Move communication with the decorView out to FlutterView, or even FlutterActivity.
+ // The reason this is here is because when the device is in reverse-landscape
+ // orientation, Android has a bug where it assumes the OS nav bar is on the
+ // right side of the screen, not the left. As a result, accessibility borders
+ // are drawn too far to the left. The AccessibilityBridge directly adjusts
+ // for this Android bug. We still need to adjust, but this is the wrong place
+ // to access a decorView. What if the FlutterView is only part of the UI
+ // hierarchy, like a list item? We shouldn't touch the decor view.
+ // https://github.com/flutter/flutter/issues/19967
+ @NonNull
private final View decorView;
- private Map objects;
- private Map customAccessibilityActions;
- private boolean accessibilityEnabled = false;
- private SemanticsObject a11yFocusedObject;
- private SemanticsObject inputFocusedObject;
- private SemanticsObject hoveredObject;
+
+ // The entire Flutter semantics tree of the running Flutter app, stored as a Map
+ // from each SemanticsNode's ID to a Java representation of a Flutter SemanticsNode.
+ //
+ // Flutter's semantics tree is cached here because Android might ask for information about
+ // a given SemanticsNode at any moment in time. Caching the tree allows for immediate
+ // response to Android's request.
+ //
+ // The structure of flutterSemanticsTree may be 1 or 2 frames behind the Flutter app
+ // due to the time required to communicate tree changes from Flutter to Android.
+ //
+ // See the Flutter docs on SemanticsNode:
+ // https://docs.flutter.io/flutter/semantics/SemanticsNode-class.html
+ @NonNull
+ private final Map flutterSemanticsTree = new HashMap<>();
+
+ // The set of all custom Flutter accessibility actions that are present in the running
+ // Flutter app, stored as a Map from each action's ID to the definition of the custom accessibility
+ // action.
+ //
+ // Flutter and Android support a number of built-in accessibility actions. However, these
+ // predefined actions are not always sufficient for a desired interaction. Android facilitates
+ // custom accessibility actions, https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction.
+ // Flutter supports custom accessibility actions via {@code customSemanticsActions} within
+ // a {@code Semantics} widget, https://docs.flutter.io/flutter/widgets/Semantics-class.html.
+ // {@code customAccessibilityActions} are an Android-side cache of all custom accessibility
+ // types declared within the running Flutter app.
+ //
+ // Custom accessibility actions are comprised of only a few fields, and therefore it is likely
+ // that a given app may define the same custom accessibility action many times. Identical
+ // custom accessibility actions are de-duped such that {@code customAccessibilityActions} only
+ // caches unique custom accessibility actions.
+ //
+ // See the Android documentation for custom accessibility actions:
+ // https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction
+ //
+ // See the Flutter documentation for the Semantics widget:
+ // https://docs.flutter.io/flutter/widgets/Semantics-class.html
+ @NonNull
+ private final Map customAccessibilityActions = new HashMap<>();
+
+ // The {@code SemanticsNode} within Flutter that currently has the focus of Android's
+ // accessibility system.
+ @Nullable
+ private SemanticsNode accessibilityFocusedSemanticsNode;
+
+ // The accessibility features that should currently be active within Flutter, represented as
+ // a bitmask whose values comes from {@link AccessibilityFeature}.
+ private int accessibilityFeatureFlags = 0;
+
+ // The {@code SemanticsNode} within Flutter that currently has the focus of Android's input
+ // system.
+ //
+ // Input focus is independent of accessibility focus. It is possible that accessibility focus
+ // and input focus target the same {@code SemanticsNode}, but it is also possible that one
+ // {@code SemanticsNode} has input focus while a different {@code SemanticsNode} has
+ // accessibility focus. For example, a user may use a D-Pad to navigate to a text field, giving
+ // it accessibility focus, and then enable input on that text field, giving it input focus. Then
+ // the user moves the accessibility focus to a nearby label to get info about the label, while
+ // maintaining input focus on the original text field.
+ @Nullable
+ private SemanticsNode inputFocusedSemanticsNode;
+
+ // The widget within Flutter that currently sits beneath a cursor, e.g,
+ // beneath a stylus or mouse cursor.
+ @Nullable
+ private SemanticsNode hoveredObject;
+
+ // A Java/Android cached representation of the Flutter app's navigation stack. The Flutter
+ // navigation stack is tracked so that accessibility announcements can be made during Flutter's
+ // navigation changes.
+ // TODO(mattcarroll): take this cache into account for new routing solution so accessibility does
+ // not get left behind.
+ @NonNull
+ private final List flutterNavigationStack = new ArrayList<>();
+
+ // TODO(mattcarroll): why do we need previouseRouteId if we have flutterNavigationStack
private int previousRouteId = ROOT_NODE_ID;
- private List previousRoutes;
+
+ // TODO(mattcarroll): is this for the decor view adjustment?
+ @NonNull
private Integer lastLeftFrameInset = 0;
+ @Nullable
+ private OnAccessibilityChangeListener onAccessibilityChangeListener;
+
+ // Handler for all messages received from Flutter via the {@code accessibilityChannel}
private final AccessibilityChannel.AccessibilityMessageHandler accessibilityMessageHandler = new AccessibilityChannel.AccessibilityMessageHandler() {
+ /**
+ * The Dart application would like the given {@code message} to be announced.
+ */
@Override
public void announce(@NonNull String message) {
- owner.announceForAccessibility(message);
+ rootAccessibilityView.announceForAccessibility(message);
}
+ /**
+ * The user has tapped on the widget with the given {@code nodeId}.
+ */
@Override
public void onTap(int nodeId) {
sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_CLICKED);
}
+ /**
+ * The user has long pressed on the widget with the given {@code nodeId}.
+ */
@Override
public void onLongPress(int nodeId) {
sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
}
+ /**
+ * The user has opened a tooltip.
+ */
@Override
public void onTooltip(@NonNull String message) {
AccessibilityEvent e = obtainAccessibilityEvent(ROOT_NODE_ID, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
@@ -75,164 +241,262 @@ public void onTooltip(@NonNull String message) {
}
};
- enum Action {
- TAP(1 << 0),
- LONG_PRESS(1 << 1),
- SCROLL_LEFT(1 << 2),
- SCROLL_RIGHT(1 << 3),
- SCROLL_UP(1 << 4),
- SCROLL_DOWN(1 << 5),
- INCREASE(1 << 6),
- DECREASE(1 << 7),
- SHOW_ON_SCREEN(1 << 8),
- MOVE_CURSOR_FORWARD_BY_CHARACTER(1 << 9),
- MOVE_CURSOR_BACKWARD_BY_CHARACTER(1 << 10),
- /** These actions are only supported on Android 4.3 and above. */
- SET_SELECTION(1 << 11),
- COPY(1 << 12),
- CUT(1 << 13),
- PASTE(1 << 14),
- /** End 4.3 only supported actions. */
- DID_GAIN_ACCESSIBILITY_FOCUS(1 << 15),
- DID_LOSE_ACCESSIBILITY_FOCUS(1 << 16),
- CUSTOM_ACTION(1 << 17),
- DISMISS(1 << 18),
- MOVE_CURSOR_FORWARD_BY_WORD(1 << 19),
- MOVE_CURSOR_BACKWARD_BY_WORD(1 << 20);
+ // Listener that is notified when accessibility is turned on/off.
+ private final AccessibilityManager.AccessibilityStateChangeListener accessibilityStateChangeListener = new AccessibilityManager.AccessibilityStateChangeListener() {
+ @Override
+ public void onAccessibilityStateChanged(boolean accessibilityEnabled) {
+ if (accessibilityEnabled) {
+ accessibilityChannel.setAccessibilityMessageHandler(accessibilityMessageHandler);
+ flutterJNI.setSemanticsEnabled(true);
+ } else {
+ accessibilityChannel.setAccessibilityMessageHandler(null);
+ flutterJNI.setSemanticsEnabled(false);
+ }
- Action(int value) {
- this.value = value;
+ if (onAccessibilityChangeListener != null) {
+ onAccessibilityChangeListener.onAccessibilityChanged(
+ accessibilityEnabled,
+ accessibilityManager.isTouchExplorationEnabled()
+ );
+ }
}
+ };
- final int value;
- }
+ // Listener that is notified when accessibility touch exploration is turned on/off.
+ // This is guarded at instantiation time.
+ @TargetApi(19)
+ @RequiresApi(19)
+ private final AccessibilityManager.TouchExplorationStateChangeListener touchExplorationStateChangeListener = new AccessibilityManager.TouchExplorationStateChangeListener() {
+ @Override
+ public void onTouchExplorationStateChanged(boolean isTouchExplorationEnabled) {
+ if (isTouchExplorationEnabled) {
+ accessibilityFeatureFlags |= AccessibilityFeature.ACCESSIBLE_NAVIGATION.value;
+ } else {
+ onTouchExplorationExit();
+ accessibilityFeatureFlags &= ~AccessibilityFeature.ACCESSIBLE_NAVIGATION.value;
+ }
+ sendLatestAccessibilityFlagsToFlutter();
- enum Flag {
- HAS_CHECKED_STATE(1 << 0),
- IS_CHECKED(1 << 1),
- IS_SELECTED(1 << 2),
- IS_BUTTON(1 << 3),
- IS_TEXT_FIELD(1 << 4),
- IS_FOCUSED(1 << 5),
- HAS_ENABLED_STATE(1 << 6),
- IS_ENABLED(1 << 7),
- IS_IN_MUTUALLY_EXCLUSIVE_GROUP(1 << 8),
- IS_HEADER(1 << 9),
- IS_OBSCURED(1 << 10),
- SCOPES_ROUTE(1 << 11),
- NAMES_ROUTE(1 << 12),
- IS_HIDDEN(1 << 13),
- IS_IMAGE(1 << 14),
- IS_LIVE_REGION(1 << 15),
- HAS_TOGGLED_STATE(1 << 16),
- IS_TOGGLED(1 << 17),
- HAS_IMPLICIT_SCROLLING(1 << 18);
+ if (onAccessibilityChangeListener != null) {
+ onAccessibilityChangeListener.onAccessibilityChanged(
+ accessibilityManager.isEnabled(),
+ isTouchExplorationEnabled
+ );
+ }
+ }
+ };
- Flag(int value) {
- this.value = value;
+ // Listener that is notified when the global TRANSITION_ANIMATION_SCALE. When this scale goes
+ // to zero, we instruct Flutter to disable animations.
+ private final ContentObserver animationScaleObserver = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ this.onChange(selfChange, null);
}
- final int value;
- }
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ // Retrieve the current value of TRANSITION_ANIMATION_SCALE from the OS.
+ String value = Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 ? null
+ : Settings.Global.getString(
+ contentResolver,
+ Settings.Global.TRANSITION_ANIMATION_SCALE
+ );
+
+ boolean shouldAnimationsBeDisabled = value != null && value.equals("0");
+ if (shouldAnimationsBeDisabled) {
+ accessibilityFeatureFlags |= AccessibilityFeature.DISABLE_ANIMATIONS.value;
+ } else {
+ accessibilityFeatureFlags &= ~AccessibilityFeature.DISABLE_ANIMATIONS.value;
+ }
+ sendLatestAccessibilityFlagsToFlutter();
+ }
+ };
- AccessibilityBridge(@NonNull FlutterView owner, @NonNull AccessibilityChannel accessibilityChannel) {
- this.owner = owner;
+ AccessibilityBridge(
+ @NonNull View rootAccessibilityView,
+ @NonNull FlutterJNI flutterJNI,
+ @NonNull AccessibilityChannel accessibilityChannel,
+ @NonNull AccessibilityManager accessibilityManager,
+ @NonNull ContentResolver contentResolver
+ ) {
+ this.rootAccessibilityView = rootAccessibilityView;
+ this.flutterJNI = flutterJNI;
this.accessibilityChannel = accessibilityChannel;
- decorView = ((Activity) owner.getContext()).getWindow().getDecorView();
- objects = new HashMap<>();
- customAccessibilityActions = new HashMap<>();
- previousRoutes = new ArrayList<>();
- }
+ this.accessibilityManager = accessibilityManager;
+ this.contentResolver = contentResolver;
- void setAccessibilityEnabled(boolean accessibilityEnabled) {
- this.accessibilityEnabled = accessibilityEnabled;
- if (accessibilityEnabled) {
- this.accessibilityChannel.setAccessibilityMessageHandler(accessibilityMessageHandler);
- } else {
- this.accessibilityChannel.setAccessibilityMessageHandler(null);
+ decorView = ((Activity) rootAccessibilityView.getContext()).getWindow().getDecorView();
+
+ // Tell Flutter whether accessibility is initially active or not. Then register a listener
+ // to be notified of changes in the future.
+ accessibilityStateChangeListener.onAccessibilityStateChanged(accessibilityManager.isEnabled());
+ this.accessibilityManager.addAccessibilityStateChangeListener(accessibilityStateChangeListener);
+
+ // Tell Flutter whether touch exploration is initially active or not. Then register a listener
+ // to be notified of changes in the future.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ touchExplorationStateChangeListener.onTouchExplorationStateChanged(accessibilityManager.isTouchExplorationEnabled());
+ this.accessibilityManager.addTouchExplorationStateChangeListener(touchExplorationStateChangeListener);
+ }
+
+ // Tell Flutter whether animations should initially be enabled or disabled. Then register a
+ // listener to be notified of changes in the future.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ animationScaleObserver.onChange(false);
+ Uri transitionUri = Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE);
+ this.contentResolver.registerContentObserver(transitionUri, false, animationScaleObserver);
}
}
- private boolean shouldSetCollectionInfo(final SemanticsObject object) {
+ /**
+ * Disconnects any listeners and/or delegates that were initialized in {@code AccessibilityBridge}'s
+ * constructor, or added after.
+ *
+ * Do not use this instance after invoking {@code release}. The behavior of any method invoked
+ * on this {@code AccessibilityBridge} after invoking {@code release()} is undefined.
+ */
+ public void release() {
+ setOnAccessibilityChangeListener(null);
+ accessibilityManager.removeAccessibilityStateChangeListener(accessibilityStateChangeListener);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ accessibilityManager.removeTouchExplorationStateChangeListener(touchExplorationStateChangeListener);
+ }
+ contentResolver.unregisterContentObserver(animationScaleObserver);
+ }
+
+ /**
+ * Returns true if the Android OS currently has accessibility enabled, false otherwise.
+ */
+ public boolean isAccessibilityEnabled() {
+ return accessibilityManager.isEnabled();
+ }
+
+ /**
+ * Returns true if the Android OS currently has touch exploration enabled, false otherwise.
+ */
+ public boolean isTouchExplorationEnabled() {
+ return accessibilityManager.isTouchExplorationEnabled();
+ }
+
+ /**
+ * Sets a listener on this {@code AccessibilityBridge}, which is notified whenever accessibility
+ * activation, or touch exploration activation changes.
+ */
+ public void setOnAccessibilityChangeListener(@Nullable OnAccessibilityChangeListener listener) {
+ this.onAccessibilityChangeListener = listener;
+ }
+
+ /**
+ * Sends the current value of {@link #accessibilityFeatureFlags} to Flutter via {@link FlutterJNI}.
+ */
+ private void sendLatestAccessibilityFlagsToFlutter() {
+ flutterJNI.setAccessibilityFeatures(accessibilityFeatureFlags);
+ }
+
+ private boolean shouldSetCollectionInfo(final SemanticsNode semanticsNode) {
// TalkBack expects a number of rows and/or columns greater than 0 to announce
// in list and out of list. For an infinite or growing list, you have to
// specify something > 0 to get "in list" announcements.
// TalkBack will also only track one list at a time, so we only want to set this
- // for a list that contains the current a11y focused object - otherwise, if there
+ // for a list that contains the current a11y focused semanticsNode - otherwise, if there
// are two lists or nested lists, we may end up with announcements for only the last
// one that is currently available in the semantics tree. However, we also want
// to set it if we're exiting a list to a non-list, so that we can get the "out of list"
// announcement when A11y focus moves out of a list and not into another list.
- return object.scrollChildren > 0
- && (hasSemanticsObjectAncestor(a11yFocusedObject, o -> o == object)
- || !hasSemanticsObjectAncestor(a11yFocusedObject, o -> o.hasFlag(Flag.HAS_IMPLICIT_SCROLLING)));
+ return semanticsNode.scrollChildren > 0
+ && (SemanticsNode.nullableHasAncestor(accessibilityFocusedSemanticsNode, o -> o == semanticsNode)
+ || !SemanticsNode.nullableHasAncestor(accessibilityFocusedSemanticsNode, o -> o.hasFlag(Flag.HAS_IMPLICIT_SCROLLING)));
}
+ /**
+ * Returns {@link AccessibilityNodeInfo} for the view corresponding to the given {@code virtualViewId}.
+ *
+ * This method is invoked by Android's accessibility system when Android needs accessibility info
+ * for a given view.
+ *
+ * When a {@code virtualViewId} of {@link View#NO_ID} is requested, accessibility node info is
+ * returned for our {@link #rootAccessibilityView}. Otherwise, Flutter's semantics tree,
+ * represented by {@link #flutterSemanticsTree}, is searched for a {@link SemanticsNode} with
+ * the given {@code virtualViewId}. If no such {@link SemanticsNode} is found, then this method
+ * returns null. If the desired {@link SemanticsNode} is found, then an {@link AccessibilityNodeInfo}
+ * is obtained from the {@link #rootAccessibilityView}, filled with appropriate info, and then
+ * returned.
+ *
+ * Depending on the type of Flutter {@code SemanticsNode} that is requested, the returned
+ * {@link AccessibilityNodeInfo} pretends that the {@code SemanticsNode} in question comes from
+ * a specialize Android view, e.g., {@link Flag#IS_TEXT_FIELD} maps to {@code android.widget.EditText},
+ * {@link Flag#IS_BUTTON} maps to {@code android.widget.Button}, and {@link Flag#IS_IMAGE} maps
+ * to {@code android.widget.ImageView}. In the case that no specialized view applies, the
+ * returned {@link AccessibilityNodeInfo} pretends that it represents a {@code android.view.View}.
+ */
@Override
@SuppressWarnings("deprecation")
public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
if (virtualViewId == View.NO_ID) {
- AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(owner);
- owner.onInitializeAccessibilityNodeInfo(result);
- if (objects.containsKey(ROOT_NODE_ID)) {
- result.addChild(owner, ROOT_NODE_ID);
+ AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(rootAccessibilityView);
+ rootAccessibilityView.onInitializeAccessibilityNodeInfo(result);
+ // TODO(mattcarroll): what does it mean for the semantics tree to contain or not contain
+ // the root node ID?
+ if (flutterSemanticsTree.containsKey(ROOT_NODE_ID)) {
+ result.addChild(rootAccessibilityView, ROOT_NODE_ID);
}
return result;
}
- SemanticsObject object = objects.get(virtualViewId);
- if (object == null) {
+ SemanticsNode semanticsNode = flutterSemanticsTree.get(virtualViewId);
+ if (semanticsNode == null) {
return null;
}
- AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(owner, virtualViewId);
+ AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(rootAccessibilityView, virtualViewId);
// Work around for https://github.com/flutter/flutter/issues/2101
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
result.setViewIdResourceName("");
}
- result.setPackageName(owner.getContext().getPackageName());
+ result.setPackageName(rootAccessibilityView.getContext().getPackageName());
result.setClassName("android.view.View");
- result.setSource(owner, virtualViewId);
- result.setFocusable(object.isFocusable());
- if (inputFocusedObject != null) {
- result.setFocused(inputFocusedObject.id == virtualViewId);
+ result.setSource(rootAccessibilityView, virtualViewId);
+ result.setFocusable(semanticsNode.isFocusable());
+ if (inputFocusedSemanticsNode != null) {
+ result.setFocused(inputFocusedSemanticsNode.id == virtualViewId);
}
- if (a11yFocusedObject != null) {
- result.setAccessibilityFocused(a11yFocusedObject.id == virtualViewId);
+ if (accessibilityFocusedSemanticsNode != null) {
+ result.setAccessibilityFocused(accessibilityFocusedSemanticsNode.id == virtualViewId);
}
- if (object.hasFlag(Flag.IS_TEXT_FIELD)) {
- result.setPassword(object.hasFlag(Flag.IS_OBSCURED));
+ if (semanticsNode.hasFlag(Flag.IS_TEXT_FIELD)) {
+ result.setPassword(semanticsNode.hasFlag(Flag.IS_OBSCURED));
result.setClassName("android.widget.EditText");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
result.setEditable(true);
- if (object.textSelectionBase != -1 && object.textSelectionExtent != -1) {
- result.setTextSelection(object.textSelectionBase, object.textSelectionExtent);
+ if (semanticsNode.textSelectionBase != -1 && semanticsNode.textSelectionExtent != -1) {
+ result.setTextSelection(semanticsNode.textSelectionBase, semanticsNode.textSelectionExtent);
}
// Text fields will always be created as a live region when they have input focus,
// so that updates to the label trigger polite announcements. This makes it easy to
// follow a11y guidelines for text fields on Android.
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 && a11yFocusedObject != null && a11yFocusedObject.id == virtualViewId) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 && accessibilityFocusedSemanticsNode != null && accessibilityFocusedSemanticsNode.id == virtualViewId) {
result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
}
}
// Cursor movements
int granularities = 0;
- if (object.hasAction(Action.MOVE_CURSOR_FORWARD_BY_CHARACTER)) {
+ if (semanticsNode.hasAction(Action.MOVE_CURSOR_FORWARD_BY_CHARACTER)) {
result.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER;
}
- if (object.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER)) {
+ if (semanticsNode.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER)) {
result.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER;
}
- if (object.hasAction(Action.MOVE_CURSOR_FORWARD_BY_WORD)) {
+ if (semanticsNode.hasAction(Action.MOVE_CURSOR_FORWARD_BY_WORD)) {
result.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD;
}
- if (object.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_WORD)) {
+ if (semanticsNode.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_WORD)) {
result.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD;
}
@@ -242,44 +506,44 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
// These are non-ops on older devices. Attempting to interact with the text will cause Talkback to read the
// contents of the text box instead.
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
- if (object.hasAction(Action.SET_SELECTION)) {
+ if (semanticsNode.hasAction(Action.SET_SELECTION)) {
result.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
}
- if (object.hasAction(Action.COPY)) {
+ if (semanticsNode.hasAction(Action.COPY)) {
result.addAction(AccessibilityNodeInfo.ACTION_COPY);
}
- if (object.hasAction(Action.CUT)) {
+ if (semanticsNode.hasAction(Action.CUT)) {
result.addAction(AccessibilityNodeInfo.ACTION_CUT);
}
- if (object.hasAction(Action.PASTE)) {
+ if (semanticsNode.hasAction(Action.PASTE)) {
result.addAction(AccessibilityNodeInfo.ACTION_PASTE);
}
}
- if (object.hasFlag(Flag.IS_BUTTON)) {
+ if (semanticsNode.hasFlag(Flag.IS_BUTTON)) {
result.setClassName("android.widget.Button");
}
- if (object.hasFlag(Flag.IS_IMAGE)) {
+ if (semanticsNode.hasFlag(Flag.IS_IMAGE)) {
result.setClassName("android.widget.ImageView");
// TODO(jonahwilliams): Figure out a way conform to the expected id from TalkBack's
// CustomLabelManager. talkback/src/main/java/labeling/CustomLabelManager.java#L525
}
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 && object.hasAction(Action.DISMISS)) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 && semanticsNode.hasAction(Action.DISMISS)) {
result.setDismissable(true);
result.addAction(AccessibilityNodeInfo.ACTION_DISMISS);
}
- if (object.parent != null) {
- assert object.id > ROOT_NODE_ID;
- result.setParent(owner, object.parent.id);
+ if (semanticsNode.parent != null) {
+ assert semanticsNode.id > ROOT_NODE_ID;
+ result.setParent(rootAccessibilityView, semanticsNode.parent.id);
} else {
- assert object.id == ROOT_NODE_ID;
- result.setParent(owner);
+ assert semanticsNode.id == ROOT_NODE_ID;
+ result.setParent(rootAccessibilityView);
}
- Rect bounds = object.getGlobalRect();
- if (object.parent != null) {
- Rect parentBounds = object.parent.getGlobalRect();
+ Rect bounds = semanticsNode.getGlobalRect();
+ if (semanticsNode.parent != null) {
+ Rect parentBounds = semanticsNode.parent.getGlobalRect();
Rect boundsInParent = new Rect(bounds);
boundsInParent.offset(-parentBounds.left, -parentBounds.top);
result.setBoundsInParent(boundsInParent);
@@ -289,30 +553,35 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
result.setBoundsInScreen(bounds);
result.setVisibleToUser(true);
result.setEnabled(
- !object.hasFlag(Flag.HAS_ENABLED_STATE) || object.hasFlag(Flag.IS_ENABLED));
+ !semanticsNode.hasFlag(Flag.HAS_ENABLED_STATE) || semanticsNode.hasFlag(Flag.IS_ENABLED)
+ );
- if (object.hasAction(Action.TAP)) {
- if (Build.VERSION.SDK_INT >= 21 && object.onTapOverride != null) {
+ if (semanticsNode.hasAction(Action.TAP)) {
+ if (Build.VERSION.SDK_INT >= 21 && semanticsNode.onTapOverride != null) {
result.addAction(new AccessibilityNodeInfo.AccessibilityAction(
- AccessibilityNodeInfo.ACTION_CLICK, object.onTapOverride.hint));
+ AccessibilityNodeInfo.ACTION_CLICK,
+ semanticsNode.onTapOverride.hint
+ ));
result.setClickable(true);
} else {
result.addAction(AccessibilityNodeInfo.ACTION_CLICK);
result.setClickable(true);
}
}
- if (object.hasAction(Action.LONG_PRESS)) {
- if (Build.VERSION.SDK_INT >= 21 && object.onLongPressOverride != null) {
- result.addAction(new AccessibilityNodeInfo.AccessibilityAction(AccessibilityNodeInfo.ACTION_LONG_CLICK,
- object.onLongPressOverride.hint));
+ if (semanticsNode.hasAction(Action.LONG_PRESS)) {
+ if (Build.VERSION.SDK_INT >= 21 && semanticsNode.onLongPressOverride != null) {
+ result.addAction(new AccessibilityNodeInfo.AccessibilityAction(
+ AccessibilityNodeInfo.ACTION_LONG_CLICK,
+ semanticsNode.onLongPressOverride.hint
+ ));
result.setLongClickable(true);
} else {
result.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
result.setLongClickable(true);
}
}
- if (object.hasAction(Action.SCROLL_LEFT) || object.hasAction(Action.SCROLL_UP)
- || object.hasAction(Action.SCROLL_RIGHT) || object.hasAction(Action.SCROLL_DOWN)) {
+ if (semanticsNode.hasAction(Action.SCROLL_LEFT) || semanticsNode.hasAction(Action.SCROLL_UP)
+ || semanticsNode.hasAction(Action.SCROLL_RIGHT) || semanticsNode.hasAction(Action.SCROLL_DOWN)) {
result.setScrollable(true);
// This tells Android's a11y to send scroll events when reaching the end of
@@ -327,22 +596,24 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
//
// TODO(dnfield): We should add semantics properties for rows and columns in 2 dimensional lists, e.g.
// GridView. Right now, we're only supporting ListViews and only if they have scroll children.
- if (object.hasFlag(Flag.HAS_IMPLICIT_SCROLLING)) {
- if (object.hasAction(Action.SCROLL_LEFT) || object.hasAction(Action.SCROLL_RIGHT)) {
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT && shouldSetCollectionInfo(object)) {
+ if (semanticsNode.hasFlag(Flag.HAS_IMPLICIT_SCROLLING)) {
+ if (semanticsNode.hasAction(Action.SCROLL_LEFT) || semanticsNode.hasAction(Action.SCROLL_RIGHT)) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT && shouldSetCollectionInfo(semanticsNode)) {
result.setCollectionInfo(AccessibilityNodeInfo.CollectionInfo.obtain(
0, // rows
- object.scrollChildren, // columns
- false)); // hierarchical
+ semanticsNode.scrollChildren, // columns
+ false // hierarchical
+ ));
} else {
result.setClassName("android.widget.HorizontalScrollView");
}
} else {
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 && shouldSetCollectionInfo(object)) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 && shouldSetCollectionInfo(semanticsNode)) {
result.setCollectionInfo(AccessibilityNodeInfo.CollectionInfo.obtain(
- object.scrollChildren, // rows
+ semanticsNode.scrollChildren, // rows
0, // columns
- false)); // hierarchical
+ false // hierarchical
+ ));
} else {
result.setClassName("android.widget.ScrollView");
}
@@ -351,53 +622,54 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
// TODO(ianh): Once we're on SDK v23+, call addAction to
// expose AccessibilityAction.ACTION_SCROLL_LEFT, _RIGHT,
// _UP, and _DOWN when appropriate.
- if (object.hasAction(Action.SCROLL_LEFT) || object.hasAction(Action.SCROLL_UP)) {
+ if (semanticsNode.hasAction(Action.SCROLL_LEFT) || semanticsNode.hasAction(Action.SCROLL_UP)) {
result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
}
- if (object.hasAction(Action.SCROLL_RIGHT) || object.hasAction(Action.SCROLL_DOWN)) {
+ if (semanticsNode.hasAction(Action.SCROLL_RIGHT) || semanticsNode.hasAction(Action.SCROLL_DOWN)) {
result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
}
}
- if (object.hasAction(Action.INCREASE) || object.hasAction(Action.DECREASE)) {
+ if (semanticsNode.hasAction(Action.INCREASE) || semanticsNode.hasAction(Action.DECREASE)) {
// TODO(jonahwilliams): support AccessibilityAction.ACTION_SET_PROGRESS once SDK is
// updated.
result.setClassName("android.widget.SeekBar");
- if (object.hasAction(Action.INCREASE)) {
+ if (semanticsNode.hasAction(Action.INCREASE)) {
result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
}
- if (object.hasAction(Action.DECREASE)) {
+ if (semanticsNode.hasAction(Action.DECREASE)) {
result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
}
}
- if (object.hasFlag(Flag.IS_LIVE_REGION) && Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ if (semanticsNode.hasFlag(Flag.IS_LIVE_REGION) && Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
}
- boolean hasCheckedState = object.hasFlag(Flag.HAS_CHECKED_STATE);
- boolean hasToggledState = object.hasFlag(Flag.HAS_TOGGLED_STATE);
+ boolean hasCheckedState = semanticsNode.hasFlag(Flag.HAS_CHECKED_STATE);
+ boolean hasToggledState = semanticsNode.hasFlag(Flag.HAS_TOGGLED_STATE);
assert !(hasCheckedState && hasToggledState);
result.setCheckable(hasCheckedState || hasToggledState);
if (hasCheckedState) {
- result.setChecked(object.hasFlag(Flag.IS_CHECKED));
- result.setContentDescription(object.getValueLabelHint());
- if (object.hasFlag(Flag.IS_IN_MUTUALLY_EXCLUSIVE_GROUP))
+ result.setChecked(semanticsNode.hasFlag(Flag.IS_CHECKED));
+ result.setContentDescription(semanticsNode.getValueLabelHint());
+ if (semanticsNode.hasFlag(Flag.IS_IN_MUTUALLY_EXCLUSIVE_GROUP)) {
result.setClassName("android.widget.RadioButton");
- else
+ } else {
result.setClassName("android.widget.CheckBox");
+ }
} else if (hasToggledState) {
- result.setChecked(object.hasFlag(Flag.IS_TOGGLED));
+ result.setChecked(semanticsNode.hasFlag(Flag.IS_TOGGLED));
result.setClassName("android.widget.Switch");
- result.setContentDescription(object.getValueLabelHint());
+ result.setContentDescription(semanticsNode.getValueLabelHint());
} else {
// Setting the text directly instead of the content description
// will replace the "checked" or "not-checked" label.
- result.setText(object.getValueLabelHint());
+ result.setText(semanticsNode.getValueLabelHint());
}
- result.setSelected(object.hasFlag(Flag.IS_SELECTED));
+ result.setSelected(semanticsNode.hasFlag(Flag.IS_SELECTED));
// Accessibility Focus
- if (a11yFocusedObject != null && a11yFocusedObject.id == virtualViewId) {
+ if (accessibilityFocusedSemanticsNode != null && accessibilityFocusedSemanticsNode.id == virtualViewId) {
result.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
} else {
result.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
@@ -405,18 +677,20 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
// Actions on the local context menu
if (Build.VERSION.SDK_INT >= 21) {
- if (object.customAccessibilityActions != null) {
- for (CustomAccessibilityAction action : object.customAccessibilityActions) {
+ if (semanticsNode.customAccessibilityActions != null) {
+ for (CustomAccessibilityAction action : semanticsNode.customAccessibilityActions) {
result.addAction(new AccessibilityNodeInfo.AccessibilityAction(
- action.resourceId, action.label));
+ action.resourceId,
+ action.label
+ ));
}
}
}
- if (object.childrenInTraversalOrder != null) {
- for (SemanticsObject child : object.childrenInTraversalOrder) {
+ if (semanticsNode.childrenInTraversalOrder != null) {
+ for (SemanticsNode child : semanticsNode.childrenInTraversalOrder) {
if (!child.hasFlag(Flag.IS_HIDDEN)) {
- result.addChild(owner, child.id);
+ result.addChild(rootAccessibilityView, child.id);
}
}
}
@@ -424,54 +698,68 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
return result;
}
+ /**
+ * Instructs the view represented by {@code virtualViewId} to carry out the desired {@code accessibilityAction},
+ * perhaps configured by additional {@code arguments}.
+ *
+ * This method is invoked by Android's accessibility system. This method returns true if the
+ * desired {@code SemanticsNode} was found and was capable of performing the desired action,
+ * false otherwise.
+ *
+ * In a traditional Android app, the given view ID refers to a {@link View} within an Android
+ * {@link View} hierarchy. Flutter does not have an Android {@link View} hierarchy, therefore
+ * the given view ID is a {@code virtualViewId} that refers to a {@code SemanticsNode} within
+ * a Flutter app. The given arguments of this method are forwarded from Android to Flutter
+ * via {@link FlutterJNI}.
+ */
@Override
- public boolean performAction(int virtualViewId, int action, Bundle arguments) {
- SemanticsObject object = objects.get(virtualViewId);
- if (object == null) {
+ public boolean performAction(int virtualViewId, int accessibilityAction, @Nullable Bundle arguments) {
+ SemanticsNode semanticsNode = flutterSemanticsTree.get(virtualViewId);
+ if (semanticsNode == null) {
return false;
}
- switch (action) {
+ switch (accessibilityAction) {
case AccessibilityNodeInfo.ACTION_CLICK: {
// Note: TalkBack prior to Oreo doesn't use this handler and instead simulates a
// click event at the center of the SemanticsNode. Other a11y services might go
// through this handler though.
- owner.dispatchSemanticsAction(virtualViewId, Action.TAP);
+ flutterJNI.dispatchSemanticsAction(virtualViewId, Action.TAP);
return true;
}
case AccessibilityNodeInfo.ACTION_LONG_CLICK: {
// Note: TalkBack doesn't use this handler and instead simulates a long click event
// at the center of the SemanticsNode. Other a11y services might go through this
// handler though.
- owner.dispatchSemanticsAction(virtualViewId, Action.LONG_PRESS);
+ flutterJNI.dispatchSemanticsAction(virtualViewId, Action.LONG_PRESS);
return true;
}
case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
- if (object.hasAction(Action.SCROLL_UP)) {
- owner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_UP);
- } else if (object.hasAction(Action.SCROLL_LEFT)) {
+ if (semanticsNode.hasAction(Action.SCROLL_UP)) {
+ flutterJNI.dispatchSemanticsAction(virtualViewId, Action.SCROLL_UP);
+ } else if (semanticsNode.hasAction(Action.SCROLL_LEFT)) {
// TODO(ianh): bidi support using textDirection
- owner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_LEFT);
- } else if (object.hasAction(Action.INCREASE)) {
- object.value = object.increasedValue;
+ flutterJNI.dispatchSemanticsAction(virtualViewId, Action.SCROLL_LEFT);
+ } else if (semanticsNode.hasAction(Action.INCREASE)) {
+ semanticsNode.value = semanticsNode.increasedValue;
// Event causes Android to read out the updated value.
sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED);
- owner.dispatchSemanticsAction(virtualViewId, Action.INCREASE);
+ flutterJNI.dispatchSemanticsAction(virtualViewId, Action.INCREASE);
} else {
return false;
}
return true;
}
case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
- if (object.hasAction(Action.SCROLL_DOWN)) {
- owner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_DOWN);
- } else if (object.hasAction(Action.SCROLL_RIGHT)) {
+ if (semanticsNode.hasAction(Action.SCROLL_DOWN)) {
+ flutterJNI.dispatchSemanticsAction(virtualViewId, Action.SCROLL_DOWN);
+ } else if (semanticsNode.hasAction(Action.SCROLL_RIGHT)) {
// TODO(ianh): bidi support using textDirection
- owner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_RIGHT);
- } else if (object.hasAction(Action.DECREASE)) {
- object.value = object.decreasedValue;
+ flutterJNI.dispatchSemanticsAction(virtualViewId, Action.SCROLL_RIGHT);
+ } else if (semanticsNode.hasAction(Action.DECREASE)) {
+ semanticsNode.value = semanticsNode.decreasedValue;
// Event causes Android to read out the updated value.
sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED);
- owner.dispatchSemanticsAction(virtualViewId, Action.DECREASE);
+ flutterJNI.dispatchSemanticsAction(virtualViewId, Action.DECREASE);
} else {
return false;
}
@@ -484,7 +772,7 @@ public boolean performAction(int virtualViewId, int action, Bundle arguments) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
return false;
}
- return performCursorMoveAction(object, virtualViewId, arguments, false);
+ return performCursorMoveAction(semanticsNode, virtualViewId, arguments, false);
}
case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: {
// Text selection APIs aren't available until API 18. We can't handle the case here so return false
@@ -493,29 +781,39 @@ public boolean performAction(int virtualViewId, int action, Bundle arguments) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
return false;
}
- return performCursorMoveAction(object, virtualViewId, arguments, true);
+ return performCursorMoveAction(semanticsNode, virtualViewId, arguments, true);
}
case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
- owner.dispatchSemanticsAction(virtualViewId, Action.DID_LOSE_ACCESSIBILITY_FOCUS);
+ flutterJNI.dispatchSemanticsAction(
+ virtualViewId,
+ Action.DID_LOSE_ACCESSIBILITY_FOCUS
+ );
sendAccessibilityEvent(
- virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
- a11yFocusedObject = null;
+ virtualViewId,
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
+ );
+ accessibilityFocusedSemanticsNode = null;
return true;
}
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
- owner.dispatchSemanticsAction(virtualViewId, Action.DID_GAIN_ACCESSIBILITY_FOCUS);
+ flutterJNI.dispatchSemanticsAction(
+ virtualViewId,
+ Action.DID_GAIN_ACCESSIBILITY_FOCUS
+ );
sendAccessibilityEvent(
- virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+ virtualViewId,
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
+ );
- if (a11yFocusedObject == null) {
+ if (accessibilityFocusedSemanticsNode == null) {
// When Android focuses a node, it doesn't invalidate the view.
// (It does when it sends ACTION_CLEAR_ACCESSIBILITY_FOCUS, so
// we only have to worry about this when the focused node is null.)
- owner.invalidate();
+ rootAccessibilityView.invalidate();
}
- a11yFocusedObject = object;
+ accessibilityFocusedSemanticsNode = semanticsNode;
- if (object.hasAction(Action.INCREASE) || object.hasAction(Action.DECREASE)) {
+ if (semanticsNode.hasAction(Action.INCREASE) || semanticsNode.hasAction(Action.DECREASE)) {
// SeekBars only announce themselves after this event.
sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED);
}
@@ -523,7 +821,7 @@ public boolean performAction(int virtualViewId, int action, Bundle arguments) {
return true;
}
case ACTION_SHOW_ON_SCREEN: {
- owner.dispatchSemanticsAction(virtualViewId, Action.SHOW_ON_SCREEN);
+ flutterJNI.dispatchSemanticsAction(virtualViewId, Action.SHOW_ON_SCREEN);
return true;
}
case AccessibilityNodeInfo.ACTION_SET_SELECTION: {
@@ -535,86 +833,110 @@ public boolean performAction(int virtualViewId, int action, Bundle arguments) {
}
final Map selection = new HashMap<>();
final boolean hasSelection = arguments != null
- && arguments.containsKey(
- AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT)
- && arguments.containsKey(
- AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT);
+ && arguments.containsKey(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT)
+ && arguments.containsKey(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT);
if (hasSelection) {
- selection.put("base",
- arguments.getInt(
- AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT));
- selection.put("extent",
- arguments.getInt(
- AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT));
+ selection.put(
+ "base",
+ arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT)
+ );
+ selection.put(
+ "extent",
+ arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT)
+ );
} else {
// Clear the selection
- selection.put("base", object.textSelectionExtent);
- selection.put("extent", object.textSelectionExtent);
+ selection.put("base", semanticsNode.textSelectionExtent);
+ selection.put("extent", semanticsNode.textSelectionExtent);
}
- owner.dispatchSemanticsAction(virtualViewId, Action.SET_SELECTION, selection);
+ flutterJNI.dispatchSemanticsAction(virtualViewId, Action.SET_SELECTION, selection);
return true;
}
case AccessibilityNodeInfo.ACTION_COPY: {
- owner.dispatchSemanticsAction(virtualViewId, Action.COPY);
+ flutterJNI.dispatchSemanticsAction(virtualViewId, Action.COPY);
return true;
}
case AccessibilityNodeInfo.ACTION_CUT: {
- owner.dispatchSemanticsAction(virtualViewId, Action.CUT);
+ flutterJNI.dispatchSemanticsAction(virtualViewId, Action.CUT);
return true;
}
case AccessibilityNodeInfo.ACTION_PASTE: {
- owner.dispatchSemanticsAction(virtualViewId, Action.PASTE);
+ flutterJNI.dispatchSemanticsAction(virtualViewId, Action.PASTE);
return true;
}
case AccessibilityNodeInfo.ACTION_DISMISS: {
- owner.dispatchSemanticsAction(virtualViewId, Action.DISMISS);
+ flutterJNI.dispatchSemanticsAction(virtualViewId, Action.DISMISS);
return true;
}
default:
- // might be a custom accessibility action.
- final int flutterId = action - firstResourceId;
- CustomAccessibilityAction contextAction =
- customAccessibilityActions.get(flutterId);
+ // might be a custom accessibility accessibilityAction.
+ final int flutterId = accessibilityAction - FIRST_RESOURCE_ID;
+ CustomAccessibilityAction contextAction = customAccessibilityActions.get(flutterId);
if (contextAction != null) {
- owner.dispatchSemanticsAction(
- virtualViewId, Action.CUSTOM_ACTION, contextAction.id);
+ flutterJNI.dispatchSemanticsAction(
+ virtualViewId,
+ Action.CUSTOM_ACTION,
+ contextAction.id
+ );
return true;
}
}
return false;
}
- @RequiresApi(18)
+ /**
+ * Handles the responsibilities of {@link #performAction(int, int, Bundle)} for the specific
+ * scenario of cursor movement.
+ */
@TargetApi(18)
- boolean performCursorMoveAction(
- SemanticsObject object, int virtualViewId, Bundle arguments, boolean forward) {
- final int granularity =
- arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
+ @RequiresApi(18)
+ private boolean performCursorMoveAction(
+ @NonNull SemanticsNode semanticsNode,
+ int virtualViewId,
+ @NonNull Bundle arguments,
+ boolean forward
+ ) {
+ final int granularity = arguments.getInt(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT
+ );
final boolean extendSelection = arguments.getBoolean(
- AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
+ AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
+ );
switch (granularity) {
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER: {
- if (forward && object.hasAction(Action.MOVE_CURSOR_FORWARD_BY_CHARACTER)) {
- owner.dispatchSemanticsAction(virtualViewId,
- Action.MOVE_CURSOR_FORWARD_BY_CHARACTER, extendSelection);
+ if (forward && semanticsNode.hasAction(Action.MOVE_CURSOR_FORWARD_BY_CHARACTER)) {
+ flutterJNI.dispatchSemanticsAction(
+ virtualViewId,
+ Action.MOVE_CURSOR_FORWARD_BY_CHARACTER,
+ extendSelection
+ );
return true;
}
- if (!forward && object.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER)) {
- owner.dispatchSemanticsAction(virtualViewId,
- Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER, extendSelection);
+ if (!forward && semanticsNode.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER)) {
+ flutterJNI.dispatchSemanticsAction(
+ virtualViewId,
+ Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER,
+ extendSelection
+ );
return true;
}
break;
}
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD:
- if (forward && object.hasAction(Action.MOVE_CURSOR_FORWARD_BY_WORD)) {
- owner.dispatchSemanticsAction(virtualViewId,
- Action.MOVE_CURSOR_FORWARD_BY_WORD, extendSelection);
+ if (forward && semanticsNode.hasAction(Action.MOVE_CURSOR_FORWARD_BY_WORD)) {
+ flutterJNI.dispatchSemanticsAction(
+ virtualViewId,
+ Action.MOVE_CURSOR_FORWARD_BY_WORD,
+ extendSelection
+ );
return true;
}
- if (!forward && object.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_WORD)) {
- owner.dispatchSemanticsAction(virtualViewId,
- Action.MOVE_CURSOR_BACKWARD_BY_WORD, extendSelection);
+ if (!forward && semanticsNode.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_WORD)) {
+ flutterJNI.dispatchSemanticsAction(
+ virtualViewId,
+ Action.MOVE_CURSOR_BACKWARD_BY_WORD,
+ extendSelection
+ );
return true;
}
break;
@@ -624,76 +946,164 @@ boolean performCursorMoveAction(
// TODO(ianh): implement findAccessibilityNodeInfosByText()
+ /**
+ * Finds the view in a hierarchy that currently has the given type of {@code focus}.
+ *
+ * This method is invoked by Android's accessibility system.
+ *
+ * Flutter does not have an Android {@link View} hierarchy. Therefore, Flutter conceptually
+ * handles this request by searching its semantics tree for the given {@code focus}, represented
+ * by {@link #flutterSemanticsTree}. In practice, this {@code AccessibilityBridge} always
+ * caches any active {@link #accessibilityFocusedSemanticsNode} and {@link #inputFocusedSemanticsNode}.
+ * Therefore, no searching is necessary. This method directly inspects the given {@code focus}
+ * type to return one of the cached nodes, null if the cached node is null, or null if a different
+ * {@code focus} type is requested.
+ */
@Override
public AccessibilityNodeInfo findFocus(int focus) {
switch (focus) {
case AccessibilityNodeInfo.FOCUS_INPUT: {
- if (inputFocusedObject != null)
- return createAccessibilityNodeInfo(inputFocusedObject.id);
+ if (inputFocusedSemanticsNode != null) {
+ return createAccessibilityNodeInfo(inputFocusedSemanticsNode.id);
+ }
}
// Fall through to check FOCUS_ACCESSIBILITY
case AccessibilityNodeInfo.FOCUS_ACCESSIBILITY: {
- if (a11yFocusedObject != null)
- return createAccessibilityNodeInfo(a11yFocusedObject.id);
+ if (accessibilityFocusedSemanticsNode != null) {
+ return createAccessibilityNodeInfo(accessibilityFocusedSemanticsNode.id);
+ }
}
}
return null;
}
- private SemanticsObject getRootObject() {
- assert objects.containsKey(0);
- return objects.get(0);
+ /**
+ * Returns the {@link SemanticsNode} at the root of Flutter's semantics tree.
+ */
+ private SemanticsNode getRootSemanticsNode() {
+ assert flutterSemanticsTree.containsKey(0);
+ return flutterSemanticsTree.get(0);
}
- private SemanticsObject getOrCreateObject(int id) {
- SemanticsObject object = objects.get(id);
- if (object == null) {
- object = new SemanticsObject();
- object.id = id;
- objects.put(id, object);
- }
- return object;
+ /**
+ * Returns an existing {@link SemanticsNode} with the given {@code id}, if it exists within
+ * {@link #flutterSemanticsTree}, or creates and returns a new {@link SemanticsNode} with the
+ * given {@code id}, adding the new {@link SemanticsNode} to the {@link #flutterSemanticsTree}.
+ *
+ * This method should only be invoked as a result of receiving new information from Flutter.
+ * The {@link #flutterSemanticsTree} is an Android cache of the last known state of a Flutter
+ * app's semantics tree, therefore, invoking this method in any other situation will result in
+ * a corrupt cache of Flutter's semantics tree.
+ */
+ private SemanticsNode getOrCreateSemanticsNode(int id) {
+ SemanticsNode semanticsNode = flutterSemanticsTree.get(id);
+ if (semanticsNode == null) {
+ semanticsNode = new SemanticsNode(this);
+ semanticsNode.id = id;
+ flutterSemanticsTree.put(id, semanticsNode);
+ }
+ return semanticsNode;
}
- private CustomAccessibilityAction getOrCreateAction(int id) {
+ /**
+ * Returns an existing {@link CustomAccessibilityAction} with the given {@code id}, if it exists
+ * within {@link #customAccessibilityActions}, or creates and returns a new {@link CustomAccessibilityAction}
+ * with the given {@code id}, adding the new {@link CustomAccessibilityAction} to the
+ * {@link #customAccessibilityActions}.
+ *
+ * This method should only be invoked as a result of receiving new information from Flutter.
+ * The {@link #customAccessibilityActions} is an Android cache of the last known state of a Flutter
+ * app's registered custom accessibility actions, therefore, invoking this method in any other
+ * situation will result in a corrupt cache of Flutter's accessibility actions.
+ */
+ private CustomAccessibilityAction getOrCreateAccessibilityAction(int id) {
CustomAccessibilityAction action = customAccessibilityActions.get(id);
if (action == null) {
action = new CustomAccessibilityAction();
action.id = id;
- action.resourceId = id + firstResourceId;
+ action.resourceId = id + FIRST_RESOURCE_ID;
customAccessibilityActions.put(id, action);
}
return action;
}
- void handleTouchExplorationExit() {
+ /**
+ * A hover {@link MotionEvent} has occurred in the {@code View} that corresponds to this
+ * {@code AccessibilityBridge}.
+ *
+ * This method returns true if Flutter's accessibility system handled the hover event, false
+ * otherwise.
+ *
+ * This method should be invoked from the corresponding {@code View}'s
+ * {@link View#onHoverEvent(MotionEvent)}.
+ */
+ public boolean onAccessibilityHoverEvent(MotionEvent event) {
+ if (!accessibilityManager.isTouchExplorationEnabled()) {
+ return false;
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER || event.getAction() == MotionEvent.ACTION_HOVER_MOVE) {
+ handleTouchExploration(event.getX(), event.getY());
+ } else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
+ onTouchExplorationExit();
+ } else {
+ Log.d("flutter", "unexpected accessibility hover event: " + event);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * This method should be invoked when a hover interaction has the cursor move off of a
+ * {@code SemanticsNode}.
+ *
+ * This method informs the Android accessibility system that a {@link AccessibilityEvent#TYPE_VIEW_HOVER_EXIT}
+ * has occurred.
+ */
+ private void onTouchExplorationExit() {
if (hoveredObject != null) {
sendAccessibilityEvent(hoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
hoveredObject = null;
}
}
- void handleTouchExploration(float x, float y) {
- if (objects.isEmpty()) {
+ /**
+ * This method should be invoked when a new hover interaction begins with a {@code SemanticsNode},
+ * or when an existing hover interaction sees a movement of the cursor.
+ *
+ * This method checks to see if the cursor has moved from one {@code SemanticsNode} to another.
+ * If it has, this method informs the Android accessibility system of the change by first sending
+ * a {@link AccessibilityEvent#TYPE_VIEW_HOVER_ENTER} event for the new hover node, followed by
+ * a {@link AccessibilityEvent#TYPE_VIEW_HOVER_EXIT} event for the old hover node.
+ */
+ private void handleTouchExploration(float x, float y) {
+ if (flutterSemanticsTree.isEmpty()) {
return;
}
- SemanticsObject newObject = getRootObject().hitTest(new float[] {x, y, 0, 1});
- if (newObject != hoveredObject) {
+ SemanticsNode semanticsNodeUnderCursor = getRootSemanticsNode().hitTest(new float[] {x, y, 0, 1});
+ if (semanticsNodeUnderCursor != hoveredObject) {
// sending ENTER before EXIT is how Android wants it
- if (newObject != null) {
- sendAccessibilityEvent(newObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
+ if (semanticsNodeUnderCursor != null) {
+ sendAccessibilityEvent(semanticsNodeUnderCursor.id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
}
if (hoveredObject != null) {
sendAccessibilityEvent(hoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
}
- hoveredObject = newObject;
+ hoveredObject = semanticsNodeUnderCursor;
}
}
- void updateCustomAccessibilityActions(ByteBuffer buffer, String[] strings) {
+ /**
+ * Updates the Android cache of Flutter's currently registered custom accessibility actions.
+ */
+ // TODO(mattcarroll): Consider introducing ability to delete custom actions because they can
+ // probably come and go in Flutter, so we may want to reflect that here in
+ // the Android cache as well.
+ // TODO(mattcarroll): where is the encoding code for reference?
+ void updateCustomAccessibilityActions(@NonNull ByteBuffer buffer, @NonNull String[] strings) {
while (buffer.hasRemaining()) {
int id = buffer.getInt();
- CustomAccessibilityAction action = getOrCreateAction(id);
+ CustomAccessibilityAction action = getOrCreateAccessibilityAction(id);
action.overrideId = buffer.getInt();
int stringIndex = buffer.getInt();
action.label = stringIndex == -1 ? null : strings[stringIndex];
@@ -702,26 +1112,32 @@ void updateCustomAccessibilityActions(ByteBuffer buffer, String[] strings) {
}
}
- void updateSemantics(ByteBuffer buffer, String[] strings) {
- ArrayList updated = new ArrayList<>();
+ /**
+ * Updates {@link #flutterSemanticsTree} to reflect the latest state of Flutter's semantics tree.
+ *
+ * The latest state of Flutter's semantics tree is encoded in the given {@code buffer}.
+ */
+ // TODO(mattcarroll): where is the encoding code for reference?
+ void updateSemantics(@NonNull ByteBuffer buffer, @NonNull String[] strings) {
+ ArrayList updated = new ArrayList<>();
while (buffer.hasRemaining()) {
int id = buffer.getInt();
- SemanticsObject object = getOrCreateObject(id);
- object.updateWith(buffer, strings);
- if (object.hasFlag(Flag.IS_HIDDEN)) {
+ SemanticsNode semanticsNode = getOrCreateSemanticsNode(id);
+ semanticsNode.updateWith(buffer, strings);
+ if (semanticsNode.hasFlag(Flag.IS_HIDDEN)) {
continue;
}
- if (object.hasFlag(Flag.IS_FOCUSED)) {
- inputFocusedObject = object;
+ if (semanticsNode.hasFlag(Flag.IS_FOCUSED)) {
+ inputFocusedSemanticsNode = semanticsNode;
}
- if (object.hadPreviousConfig) {
- updated.add(object);
+ if (semanticsNode.hadPreviousConfig) {
+ updated.add(semanticsNode);
}
}
- Set visitedObjects = new HashSet<>();
- SemanticsObject rootObject = getRootObject();
- List newRoutes = new ArrayList<>();
+ Set visitedObjects = new HashSet<>();
+ SemanticsNode rootObject = getRootSemanticsNode();
+ List newRoutes = new ArrayList<>();
if (rootObject != null) {
final float[] identity = new float[16];
Matrix.setIdentityM(identity, 0);
@@ -744,10 +1160,10 @@ void updateSemantics(ByteBuffer buffer, String[] strings) {
// Dispatch a TYPE_WINDOW_STATE_CHANGED event if the most recent route id changed from the
// previously cached route id.
- SemanticsObject lastAdded = null;
- for (SemanticsObject semanticsObject : newRoutes) {
- if (!previousRoutes.contains(semanticsObject.id)) {
- lastAdded = semanticsObject;
+ SemanticsNode lastAdded = null;
+ for (SemanticsNode semanticsNode : newRoutes) {
+ if (!flutterNavigationStack.contains(semanticsNode.id)) {
+ lastAdded = semanticsNode;
}
}
if (lastAdded == null && newRoutes.size() > 0) {
@@ -755,19 +1171,19 @@ void updateSemantics(ByteBuffer buffer, String[] strings) {
}
if (lastAdded != null && lastAdded.id != previousRouteId) {
previousRouteId = lastAdded.id;
- createWindowChangeEvent(lastAdded);
+ createAndSendWindowChangeEvent(lastAdded);
}
- previousRoutes.clear();
- for (SemanticsObject semanticsObject : newRoutes) {
- previousRoutes.add(semanticsObject.id);
+ flutterNavigationStack.clear();
+ for (SemanticsNode semanticsNode : newRoutes) {
+ flutterNavigationStack.add(semanticsNode.id);
}
- Iterator> it = objects.entrySet().iterator();
+ Iterator> it = flutterSemanticsTree.entrySet().iterator();
while (it.hasNext()) {
- Map.Entry entry = it.next();
- SemanticsObject object = entry.getValue();
+ Map.Entry entry = it.next();
+ SemanticsNode object = entry.getValue();
if (!visitedObjects.contains(object)) {
- willRemoveSemanticsObject(object);
+ willRemoveSemanticsNode(object);
it.remove();
}
}
@@ -776,7 +1192,7 @@ void updateSemantics(ByteBuffer buffer, String[] strings) {
// see https://github.com/flutter/flutter/issues/14534
sendAccessibilityEvent(0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
- for (SemanticsObject object : updated) {
+ for (SemanticsNode object : updated) {
if (object.didScroll()) {
AccessibilityEvent event =
obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_SCROLLED);
@@ -816,7 +1232,7 @@ void updateSemantics(ByteBuffer buffer, String[] strings) {
event.setFromIndex(object.scrollIndex);
int visibleChildren = 0;
// handle hidden children at the beginning and end of the list.
- for (SemanticsObject child : object.childrenInHitTestOrder) {
+ for (SemanticsNode child : object.childrenInHitTestOrder) {
if (!child.hasFlag(Flag.IS_HIDDEN)) {
visibleChildren += 1;
}
@@ -842,25 +1258,25 @@ void updateSemantics(ByteBuffer buffer, String[] strings) {
sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
}
} else if (object.hasFlag(Flag.IS_TEXT_FIELD) && object.didChangeLabel()
- && inputFocusedObject != null && inputFocusedObject.id == object.id) {
+ && inputFocusedSemanticsNode != null && inputFocusedSemanticsNode.id == object.id) {
// Text fields should announce when their label changes while focused. We use a live
// region tag to do so, and this event triggers that update.
sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
}
- if (a11yFocusedObject != null && a11yFocusedObject.id == object.id
+ if (accessibilityFocusedSemanticsNode != null && accessibilityFocusedSemanticsNode.id == object.id
&& !object.hadFlag(Flag.IS_SELECTED) && object.hasFlag(Flag.IS_SELECTED)) {
AccessibilityEvent event =
obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_SELECTED);
event.getText().add(object.label);
sendAccessibilityEvent(event);
}
- if (inputFocusedObject != null && inputFocusedObject.id == object.id
+ if (inputFocusedSemanticsNode != null && inputFocusedSemanticsNode.id == object.id
&& object.hadFlag(Flag.IS_TEXT_FIELD) && object.hasFlag(Flag.IS_TEXT_FIELD)
// If we have a TextField that has InputFocus, we should avoid announcing it if something
// else we track has a11y focus. This needs to still work when, e.g., IME has a11y focus
// or the "PASTE" popup is used though.
// See more discussion at https://github.com/flutter/flutter/issues/23180
- && (a11yFocusedObject == null || (a11yFocusedObject.id == inputFocusedObject.id))) {
+ && (accessibilityFocusedSemanticsNode == null || (accessibilityFocusedSemanticsNode.id == inputFocusedSemanticsNode.id))) {
String oldValue = object.previousValue != null ? object.previousValue : "";
String newValue = object.value != null ? object.value : "";
AccessibilityEvent event = createTextChangedEvent(object.id, oldValue, newValue);
@@ -915,67 +1331,205 @@ private AccessibilityEvent createTextChangedEvent(int id, String oldValue, Strin
return e;
}
- private AccessibilityEvent obtainAccessibilityEvent(int virtualViewId, int eventType) {
- assert virtualViewId != ROOT_NODE_ID;
- AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
- event.setPackageName(owner.getContext().getPackageName());
- event.setSource(owner, virtualViewId);
- return event;
- }
-
- private void sendAccessibilityEvent(int virtualViewId, int eventType) {
- if (!accessibilityEnabled) {
+ /**
+ * Sends an accessibility event of the given {@code eventType} to Android's accessibility
+ * system with the given {@code viewId} represented as the source of the event.
+ *
+ * The given {@code viewId} may either belong to {@link #rootAccessibilityView}, or any
+ * Flutter {@link SemanticsNode}.
+ */
+ private void sendAccessibilityEvent(int viewId, int eventType) {
+ if (!accessibilityManager.isEnabled()) {
return;
}
- if (virtualViewId == ROOT_NODE_ID) {
- owner.sendAccessibilityEvent(eventType);
+ if (viewId == ROOT_NODE_ID) {
+ rootAccessibilityView.sendAccessibilityEvent(eventType);
} else {
- sendAccessibilityEvent(obtainAccessibilityEvent(virtualViewId, eventType));
+ sendAccessibilityEvent(obtainAccessibilityEvent(viewId, eventType));
}
}
- private void sendAccessibilityEvent(AccessibilityEvent event) {
- if (!accessibilityEnabled) {
+ /**
+ * Sends the given {@link AccessibilityEvent} to Android's accessibility system for a given
+ * Flutter {@link SemanticsNode}.
+ *
+ * This method should only be called for a Flutter {@link SemanticsNode}, not a traditional
+ * Android {@code View}, i.e., {@link #rootAccessibilityView}.
+ */
+ private void sendAccessibilityEvent(@NonNull AccessibilityEvent event) {
+ if (!accessibilityManager.isEnabled()) {
return;
}
- owner.getParent().requestSendAccessibilityEvent(owner, event);
+ // TODO(mattcarroll): why are we explicitly talking to the root view's parent?
+ rootAccessibilityView.getParent().requestSendAccessibilityEvent(rootAccessibilityView, event);
}
- private void createWindowChangeEvent(SemanticsObject route) {
- AccessibilityEvent e =
- obtainAccessibilityEvent(route.id, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ /**
+ * Factory method that creates a {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED} and sends
+ * the event to Android's accessibility system.
+ *
+ * The given {@code route} should be a {@link SemanticsNode} that represents a navigation route
+ * in the Flutter app.
+ */
+ private void createAndSendWindowChangeEvent(@NonNull SemanticsNode route) {
+ AccessibilityEvent event = obtainAccessibilityEvent(
+ route.id,
+ AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+ );
String routeName = route.getRouteName();
- e.getText().add(routeName);
- sendAccessibilityEvent(e);
+ event.getText().add(routeName);
+ sendAccessibilityEvent(event);
}
- private void willRemoveSemanticsObject(SemanticsObject object) {
- assert objects.containsKey(object.id);
- assert objects.get(object.id) == object;
- object.parent = null;
- if (a11yFocusedObject == object) {
- sendAccessibilityEvent(a11yFocusedObject.id,
- AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
- a11yFocusedObject = null;
- }
- if (inputFocusedObject == object) {
- inputFocusedObject = null;
- }
- if (hoveredObject == object) {
+ /**
+ * Factory method that creates a new {@link AccessibilityEvent} that is configured to represent
+ * the Flutter {@link SemanticsNode} represented by the given {@code virtualViewId}, categorized
+ * as the given {@code eventType}.
+ *
+ * This method should *only* be called for Flutter {@link SemanticsNode}s. It should *not* be
+ * invoked to create an {@link AccessibilityEvent} for the {@link #rootAccessibilityView}.
+ */
+ private AccessibilityEvent obtainAccessibilityEvent(int virtualViewId, int eventType) {
+ assert virtualViewId != ROOT_NODE_ID;
+ AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+ event.setPackageName(rootAccessibilityView.getContext().getPackageName());
+ event.setSource(rootAccessibilityView, virtualViewId);
+ return event;
+ }
+
+ /**
+ * Hook called just before a {@link SemanticsNode} is removed from the Android cache of Flutter's
+ * semantics tree.
+ */
+ private void willRemoveSemanticsNode(SemanticsNode semanticsNodeToBeRemoved) {
+ assert flutterSemanticsTree.containsKey(semanticsNodeToBeRemoved.id);
+ assert flutterSemanticsTree.get(semanticsNodeToBeRemoved.id) == semanticsNodeToBeRemoved;
+ // TODO(mattcarroll): should parent be set to "null" here? Changing the parent seems like the
+ // behavior of a method called "removeSemanticsNode()". The same is true
+ // for null'ing accessibilityFocusedSemanticsNode, inputFocusedSemanticsNode,
+ // and hoveredObject. Is this a hook method or a command?
+ semanticsNodeToBeRemoved.parent = null;
+ if (accessibilityFocusedSemanticsNode == semanticsNodeToBeRemoved) {
+ sendAccessibilityEvent(
+ accessibilityFocusedSemanticsNode.id,
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
+ );
+ accessibilityFocusedSemanticsNode = null;
+ }
+ if (inputFocusedSemanticsNode == semanticsNodeToBeRemoved) {
+ inputFocusedSemanticsNode = null;
+ }
+ if (hoveredObject == semanticsNodeToBeRemoved) {
hoveredObject = null;
}
}
- void reset() {
- objects.clear();
- if (a11yFocusedObject != null)
- sendAccessibilityEvent(a11yFocusedObject.id,
- AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
- a11yFocusedObject = null;
+ /**
+ * Resets the {@code AccessibilityBridge}:
+ *
+ * - Clears {@link #flutterSemanticsTree}, the Android cache of Flutter's semantics tree
+ * - Releases focus on any active {@link #accessibilityFocusedSemanticsNode}
+ * - Clears any hovered {@code SemanticsNode}
+ * - Sends a {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} event
+ *
+ */
+ // TODO(mattcarroll): under what conditions is this method expected to be invoked?
+ public void reset() {
+ flutterSemanticsTree.clear();
+ if (accessibilityFocusedSemanticsNode != null) {
+ sendAccessibilityEvent(
+ accessibilityFocusedSemanticsNode.id,
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
+ );
+ }
+ accessibilityFocusedSemanticsNode = null;
hoveredObject = null;
sendAccessibilityEvent(0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
}
+ /**
+ * Listener that can be set on a {@link AccessibilityBridge}, which is invoked any time
+ * accessibility is turned on/off, or touch exploration is turned on/off.
+ */
+ public interface OnAccessibilityChangeListener {
+ void onAccessibilityChanged(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled);
+ }
+
+ // Must match SemanticsActions in semantics.dart
+ // https://github.com/flutter/engine/blob/master/lib/ui/semantics.dart
+ public enum Action {
+ TAP(1 << 0),
+ LONG_PRESS(1 << 1),
+ SCROLL_LEFT(1 << 2),
+ SCROLL_RIGHT(1 << 3),
+ SCROLL_UP(1 << 4),
+ SCROLL_DOWN(1 << 5),
+ INCREASE(1 << 6),
+ DECREASE(1 << 7),
+ SHOW_ON_SCREEN(1 << 8),
+ MOVE_CURSOR_FORWARD_BY_CHARACTER(1 << 9),
+ MOVE_CURSOR_BACKWARD_BY_CHARACTER(1 << 10),
+ SET_SELECTION(1 << 11),
+ COPY(1 << 12),
+ CUT(1 << 13),
+ PASTE(1 << 14),
+ DID_GAIN_ACCESSIBILITY_FOCUS(1 << 15),
+ DID_LOSE_ACCESSIBILITY_FOCUS(1 << 16),
+ CUSTOM_ACTION(1 << 17),
+ DISMISS(1 << 18),
+ MOVE_CURSOR_FORWARD_BY_WORD(1 << 19),
+ MOVE_CURSOR_BACKWARD_BY_WORD(1 << 20);
+
+ public final int value;
+
+ Action(int value) {
+ this.value = value;
+ }
+ }
+
+ // Must match SemanticsFlag in semantics.dart
+ // https://github.com/flutter/engine/blob/master/lib/ui/semantics.dart
+ private enum Flag {
+ HAS_CHECKED_STATE(1 << 0),
+ IS_CHECKED(1 << 1),
+ IS_SELECTED(1 << 2),
+ IS_BUTTON(1 << 3),
+ IS_TEXT_FIELD(1 << 4),
+ IS_FOCUSED(1 << 5),
+ HAS_ENABLED_STATE(1 << 6),
+ IS_ENABLED(1 << 7),
+ IS_IN_MUTUALLY_EXCLUSIVE_GROUP(1 << 8),
+ IS_HEADER(1 << 9),
+ IS_OBSCURED(1 << 10),
+ SCOPES_ROUTE(1 << 11),
+ NAMES_ROUTE(1 << 12),
+ IS_HIDDEN(1 << 13),
+ IS_IMAGE(1 << 14),
+ IS_LIVE_REGION(1 << 15),
+ HAS_TOGGLED_STATE(1 << 16),
+ IS_TOGGLED(1 << 17),
+ HAS_IMPLICIT_SCROLLING(1 << 18);
+
+ final int value;
+
+ Flag(int value) {
+ this.value = value;
+ }
+ }
+
+ // Must match the enum defined in window.dart.
+ private enum AccessibilityFeature {
+ ACCESSIBLE_NAVIGATION(1 << 0),
+ INVERT_COLORS(1 << 1), // NOT SUPPORTED
+ DISABLE_ANIMATIONS(1 << 2);
+
+ final int value;
+
+ AccessibilityFeature(int value) {
+ this.value = value;
+ }
+ }
+
private enum TextDirection {
UNKNOWN,
LTR,
@@ -992,64 +1546,95 @@ public static TextDirection fromInt(int value) {
}
}
- private class CustomAccessibilityAction {
+ /**
+ * Accessibility action that is defined within a given Flutter application, as opposed to the
+ * standard accessibility actions that are available in the Flutter framework.
+ *
+ * Flutter and Android support a number of built-in accessibility actions. However, these
+ * predefined actions are not always sufficient for a desired interaction. Android facilitates
+ * custom accessibility actions, https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction.
+ * Flutter supports custom accessibility actions via {@code customSemanticsActions} within
+ * a {@code Semantics} widget, https://docs.flutter.io/flutter/widgets/Semantics-class.html.
+ *
+ * See the Android documentation for custom accessibility actions:
+ * https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction
+ *
+ * See the Flutter documentation for the Semantics widget:
+ * https://docs.flutter.io/flutter/widgets/Semantics-class.html
+ */
+ private static class CustomAccessibilityAction {
CustomAccessibilityAction() {}
- /// Resource id is the id of the custom action plus a minimum value so that the identifier
- /// does not collide with existing Android accessibility actions.
- int resourceId = -1;
- int id = -1;
- int overrideId = -1;
+ // The ID of the custom action plus a minimum value so that the identifier
+ // does not collide with existing Android accessibility actions. This ID
+ // represents and Android resource ID, not a Flutter ID.
+ private int resourceId = -1;
- /// The label is the user presented value which is displayed in the local context menu.
- String label;
+ // The Flutter ID of this custom accessibility action. See Flutter's Semantics widget for
+ // custom accessibility action definitions: https://docs.flutter.io/flutter/widgets/Semantics-class.html
+ private int id = -1;
- /// The hint is the text used in overriden standard actions.
- String hint;
-
- boolean isStandardAction() {
- return overrideId != -1;
- }
- }
- /// Value is derived from ACTION_TYPE_MASK in AccessibilityNodeInfo.java
- static int firstResourceId = 267386881;
+ // The ID of the standard Flutter accessibility action that this {@code CustomAccessibilityAction}
+ // overrides with a custom {@code label} and/or {@code hint}.
+ private int overrideId = -1;
+ // The user presented value which is displayed in the local context menu.
+ private String label;
- static boolean hasSemanticsObjectAncestor(SemanticsObject target, Predicate tester) {
- return target != null && target.getAncestor(tester) != null;
+ // The text used in overridden standard actions.
+ private String hint;
}
- private class SemanticsObject {
- SemanticsObject() {}
-
- int id = -1;
-
- int flags;
- int actions;
- int textSelectionBase;
- int textSelectionExtent;
- int scrollChildren;
- int scrollIndex;
- float scrollPosition;
- float scrollExtentMax;
- float scrollExtentMin;
- String label;
- String value;
- String increasedValue;
- String decreasedValue;
- String hint;
- TextDirection textDirection;
-
- boolean hadPreviousConfig = false;
- int previousFlags;
- int previousActions;
- int previousTextSelectionBase;
- int previousTextSelectionExtent;
- float previousScrollPosition;
- float previousScrollExtentMax;
- float previousScrollExtentMin;
- String previousValue;
- String previousLabel;
+ /**
+ * Flutter {@code SemanticsNode} represented in Java/Android.
+ *
+ * Flutter maintains a semantics tree that is controlled by, but is independent of Flutter's
+ * element tree, i.e., widgets/elements/render objects. Flutter's semantics tree must be cached
+ * on the Android side so that Android can query any {@code SemanticsNode} at any time. This
+ * class represents a single node in the semantics tree, and it is a Java representation of the
+ * analogous concept within Flutter.
+ *
+ * To see how this {@code SemanticsNode}'s fields correspond to Flutter's semantics system, see
+ * semantics.dart: https://github.com/flutter/engine/blob/master/lib/ui/semantics.dart
+ */
+ private static class SemanticsNode {
+ private static boolean nullableHasAncestor(SemanticsNode target, Predicate tester) {
+ return target != null && target.getAncestor(tester) != null;
+ }
+
+ final AccessibilityBridge accessibilityBridge;
+
+ // Flutter ID of this {@code SemanticsNode}.
+ private int id = -1;
+
+ private int flags;
+ private int actions;
+ private int textSelectionBase;
+ private int textSelectionExtent;
+ private int scrollChildren;
+ private int scrollIndex;
+ private float scrollPosition;
+ private float scrollExtentMax;
+ private float scrollExtentMin;
+ private String label;
+ private String value;
+ private String increasedValue;
+ private String decreasedValue;
+ private String hint;
+
+ // See Flutter's {@code SemanticsNode#textDirection}.
+ private TextDirection textDirection;
+
+ private boolean hadPreviousConfig = false;
+ private int previousFlags;
+ private int previousActions;
+ private int previousTextSelectionBase;
+ private int previousTextSelectionExtent;
+ private float previousScrollPosition;
+ private float previousScrollExtentMax;
+ private float previousScrollExtentMin;
+ private String previousValue;
+ private String previousLabel;
private float left;
private float top;
@@ -1057,12 +1642,12 @@ private class SemanticsObject {
private float bottom;
private float[] transform;
- SemanticsObject parent;
- List childrenInTraversalOrder;
- List childrenInHitTestOrder;
- List customAccessibilityActions;
- CustomAccessibilityAction onTapOverride;
- CustomAccessibilityAction onLongPressOverride;
+ private SemanticsNode parent;
+ private List childrenInTraversalOrder;
+ private List childrenInHitTestOrder;
+ private List customAccessibilityActions;
+ private CustomAccessibilityAction onTapOverride;
+ private CustomAccessibilityAction onLongPressOverride;
private boolean inverseTransformDirty = true;
private float[] inverseTransform;
@@ -1071,8 +1656,16 @@ private class SemanticsObject {
private float[] globalTransform;
private Rect globalRect;
- SemanticsObject getAncestor(Predicate tester) {
- SemanticsObject nextAncestor = parent;
+ SemanticsNode(@NonNull AccessibilityBridge accessibilityBridge) {
+ this.accessibilityBridge = accessibilityBridge;
+ }
+
+ /**
+ * Returns the ancestor of this {@code SemanticsNode} for which {@link Predicate#test(Object)}
+ * returns true, or null if no such ancestor exists.
+ */
+ private SemanticsNode getAncestor(Predicate tester) {
+ SemanticsNode nextAncestor = parent;
while (nextAncestor != null) {
if (tester.test(nextAncestor)) {
return nextAncestor;
@@ -1082,51 +1675,61 @@ SemanticsObject getAncestor(Predicate tester) {
return null;
}
- boolean hasAction(Action action) {
+ /**
+ * Returns true if the given {@code action} is supported by this {@code SemanticsNode}.
+ *
+ * This method only applies to this {@code SemanticsNode} and does not implicitly search
+ * its children.
+ */
+ private boolean hasAction(@NonNull Action action) {
return (actions & action.value) != 0;
}
- boolean hadAction(Action action) {
+ /**
+ * Returns true if the given {@code action} was supported by the immediately previous
+ * version of this {@code SemanticsNode}.
+ */
+ private boolean hadAction(@NonNull Action action) {
return (previousActions & action.value) != 0;
}
- boolean hasFlag(Flag flag) {
+ private boolean hasFlag(@NonNull Flag flag) {
return (flags & flag.value) != 0;
}
- boolean hadFlag(Flag flag) {
+ private boolean hadFlag(@NonNull Flag flag) {
assert hadPreviousConfig;
return (previousFlags & flag.value) != 0;
}
- boolean didScroll() {
+ private boolean didScroll() {
return !Float.isNaN(scrollPosition) && !Float.isNaN(previousScrollPosition)
&& previousScrollPosition != scrollPosition;
}
- boolean didChangeLabel() {
+ private boolean didChangeLabel() {
if (label == null && previousLabel == null) {
return false;
}
return label == null || previousLabel == null || !label.equals(previousLabel);
}
- void log(String indent, boolean recursive) {
+ private void log(@NonNull String indent, boolean recursive) {
Log.i(TAG,
- indent + "SemanticsObject id=" + id + " label=" + label + " actions=" + actions
+ indent + "SemanticsNode id=" + id + " label=" + label + " actions=" + actions
+ " flags=" + flags + "\n" + indent + " +-- textDirection="
+ textDirection + "\n" + indent + " +-- rect.ltrb=(" + left + ", "
+ top + ", " + right + ", " + bottom + ")\n" + indent
+ " +-- transform=" + Arrays.toString(transform) + "\n");
if (childrenInTraversalOrder != null && recursive) {
String childIndent = indent + " ";
- for (SemanticsObject child : childrenInTraversalOrder) {
+ for (SemanticsNode child : childrenInTraversalOrder) {
child.log(childIndent, recursive);
}
}
}
- void updateWith(ByteBuffer buffer, String[] strings) {
+ private void updateWith(@NonNull ByteBuffer buffer, @NonNull String[] strings) {
hadPreviousConfig = true;
previousValue = value;
previousLabel = label;
@@ -1190,7 +1793,7 @@ void updateWith(ByteBuffer buffer, String[] strings) {
childrenInTraversalOrder.clear();
for (int i = 0; i < childCount; ++i) {
- SemanticsObject child = getOrCreateObject(buffer.getInt());
+ SemanticsNode child = accessibilityBridge.getOrCreateSemanticsNode(buffer.getInt());
child.parent = this;
childrenInTraversalOrder.add(child);
}
@@ -1201,7 +1804,7 @@ void updateWith(ByteBuffer buffer, String[] strings) {
childrenInHitTestOrder.clear();
for (int i = 0; i < childCount; ++i) {
- SemanticsObject child = getOrCreateObject(buffer.getInt());
+ SemanticsNode child = accessibilityBridge.getOrCreateSemanticsNode(buffer.getInt());
child.parent = this;
childrenInHitTestOrder.add(child);
}
@@ -1216,7 +1819,7 @@ void updateWith(ByteBuffer buffer, String[] strings) {
customAccessibilityActions.clear();
for (int i = 0; i < actionCount; i++) {
- CustomAccessibilityAction action = getOrCreateAction(buffer.getInt());
+ CustomAccessibilityAction action = accessibilityBridge.getOrCreateAccessibilityAction(buffer.getInt());
if (action.overrideId == Action.TAP.value) {
onTapOverride = action;
} else if (action.overrideId == Action.LONG_PRESS.value) {
@@ -1245,12 +1848,12 @@ private void ensureInverseTransform() {
}
}
- Rect getGlobalRect() {
+ private Rect getGlobalRect() {
assert !globalGeometryDirty;
return globalRect;
}
- SemanticsObject hitTest(float[] point) {
+ private SemanticsNode hitTest(float[] point) {
final float w = point[3];
final float x = point[0] / w;
final float y = point[1] / w;
@@ -1258,13 +1861,13 @@ SemanticsObject hitTest(float[] point) {
if (childrenInHitTestOrder != null) {
final float[] transformedPoint = new float[4];
for (int i = 0; i < childrenInHitTestOrder.size(); i += 1) {
- final SemanticsObject child = childrenInHitTestOrder.get(i);
+ final SemanticsNode child = childrenInHitTestOrder.get(i);
if (child.hasFlag(Flag.IS_HIDDEN)) {
continue;
}
child.ensureInverseTransform();
Matrix.multiplyMV(transformedPoint, 0, child.inverseTransform, 0, point, 0);
- final SemanticsObject result = child.hitTest(transformedPoint);
+ final SemanticsNode result = child.hitTest(transformedPoint);
if (result != null) {
return result;
}
@@ -1275,7 +1878,7 @@ SemanticsObject hitTest(float[] point) {
// TODO(goderbauer): This should be decided by the framework once we have more information
// about focusability there.
- boolean isFocusable() {
+ private boolean isFocusable() {
// We enforce in the framework that no other useful semantics are merged with these
// nodes.
if (hasFlag(Flag.SCOPES_ROUTE)) {
@@ -1288,7 +1891,7 @@ boolean isFocusable() {
|| (hint != null && !hint.isEmpty());
}
- void collectRoutes(List edges) {
+ private void collectRoutes(List edges) {
if (hasFlag(Flag.SCOPES_ROUTE)) {
edges.add(this);
}
@@ -1299,7 +1902,7 @@ void collectRoutes(List edges) {
}
}
- String getRouteName() {
+ private String getRouteName() {
// Returns the first non-null and non-empty semantic label of a child
// with an NamesRoute flag. Otherwise returns null.
if (hasFlag(Flag.NAMES_ROUTE)) {
@@ -1318,7 +1921,7 @@ String getRouteName() {
return null;
}
- void updateRecursively(float[] ancestorTransform, Set visitedObjects,
+ private void updateRecursively(float[] ancestorTransform, Set visitedObjects,
boolean forceUpdate) {
visitedObjects.add(this);
diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java
index 6161a23c5f4af..18434c28924aa 100644
--- a/shell/platform/android/io/flutter/view/FlutterView.java
+++ b/shell/platform/android/io/flutter/view/FlutterView.java
@@ -8,12 +8,10 @@
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
-import android.database.ContentObserver;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
-import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.LocaleList;
@@ -52,8 +50,7 @@
/**
* An Android view containing a Flutter app.
*/
-public class FlutterView extends SurfaceView
- implements BinaryMessenger, TextureRegistry, AccessibilityManager.AccessibilityStateChangeListener {
+public class FlutterView extends SurfaceView implements BinaryMessenger, TextureRegistry {
/**
* Interface for those objects that maintain and expose a reference to a
* {@code FlutterView} (such as a full-screen Flutter activity).
@@ -91,7 +88,6 @@ static final class ViewportMetrics {
}
private final DartExecutor dartExecutor;
- private final AccessibilityChannel accessibilityChannel;
private final NavigationChannel navigationChannel;
private final KeyEventChannel keyEventChannel;
private final LifecycleChannel lifecycleChannel;
@@ -105,14 +101,19 @@ static final class ViewportMetrics {
private AccessibilityBridge mAccessibilityNodeProvider;
private final SurfaceHolder.Callback mSurfaceCallback;
private final ViewportMetrics mMetrics;
- private final AccessibilityManager mAccessibilityManager;
private final List mActivityLifecycleListeners;
private final List mFirstFrameListeners;
private final AtomicLong nextTextureId = new AtomicLong(0L);
private FlutterNativeView mNativeView;
- private final AnimationScaleObserver mAnimationScaleObserver;
private boolean mIsSoftwareRenderingEnabled = false; // using the software renderer or not
+ private final AccessibilityBridge.OnAccessibilityChangeListener onAccessibilityChangeListener = new AccessibilityBridge.OnAccessibilityChangeListener() {
+ @Override
+ public void onAccessibilityChanged(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) {
+ resetWillNotDraw(isAccessibilityEnabled, isTouchExplorationEnabled);
+ }
+ };
+
public FlutterView(Context context) {
this(context, null);
}
@@ -133,7 +134,6 @@ public FlutterView(Context context, AttributeSet attrs, FlutterNativeView native
dartExecutor = mNativeView.getDartExecutor();
mIsSoftwareRenderingEnabled = FlutterJNI.nativeGetIsSoftwareRenderingEnabled();
- mAnimationScaleObserver = new AnimationScaleObserver(new Handler());
mMetrics = new ViewportMetrics();
mMetrics.devicePixelRatio = context.getResources().getDisplayMetrics().density;
setFocusable(true);
@@ -162,13 +162,10 @@ public void surfaceDestroyed(SurfaceHolder holder) {
};
getHolder().addCallback(mSurfaceCallback);
- mAccessibilityManager = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
-
mActivityLifecycleListeners = new ArrayList<>();
mFirstFrameListeners = new ArrayList<>();
// Create all platform channels
- accessibilityChannel = new AccessibilityChannel(dartExecutor);
navigationChannel = new NavigationChannel(dartExecutor);
keyEventChannel = new KeyEventChannel(dartExecutor);
lifecycleChannel = new LifecycleChannel(dartExecutor);
@@ -236,7 +233,6 @@ public void onPause() {
}
public void onPostResume() {
- updateAccessibilityFeatures();
for (ActivityLifecycleListener listener : mActivityLifecycleListeners) {
listener.onPostResume();
}
@@ -573,7 +569,7 @@ public boolean onHoverEvent(MotionEvent event) {
return false;
}
- boolean handled = handleAccessibilityHoverEvent(event);
+ boolean handled = mAccessibilityNodeProvider.onAccessibilityHoverEvent(event);
if (!handled) {
// TODO(ianh): Expose hover events to the platform,
// implementing ADD, REMOVE, etc.
@@ -746,6 +742,12 @@ private void preRun() {
resetAccessibilityTree();
}
+ void resetAccessibilityTree() {
+ if (mAccessibilityNodeProvider != null) {
+ mAccessibilityNodeProvider.reset();
+ }
+ }
+
private void postRun() {
}
@@ -844,206 +846,52 @@ public void onFirstFrame() {
}
}
- // ACCESSIBILITY
-
- private boolean mAccessibilityEnabled = false;
- private boolean mTouchExplorationEnabled = false;
- private int mAccessibilityFeatureFlags = 0;
- private TouchExplorationListener mTouchExplorationListener;
-
- protected void dispatchSemanticsAction(int id, AccessibilityBridge.Action action) {
- dispatchSemanticsAction(id, action, null);
- }
-
- protected void dispatchSemanticsAction(int id, AccessibilityBridge.Action action, Object args) {
- if (!isAttached())
- return;
- ByteBuffer encodedArgs = null;
- int position = 0;
- if (args != null) {
- encodedArgs = StandardMessageCodec.INSTANCE.encodeMessage(args);
- position = encodedArgs.position();
- }
- mNativeView.getFlutterJNI().dispatchSemanticsAction(id, action.value, encodedArgs, position);
- }
-
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
- mAccessibilityEnabled = mAccessibilityManager.isEnabled();
- mTouchExplorationEnabled = mAccessibilityManager.isTouchExplorationEnabled();
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- Uri transitionUri = Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE);
- getContext().getContentResolver().registerContentObserver(transitionUri, false, mAnimationScaleObserver);
- }
- if (mAccessibilityEnabled || mTouchExplorationEnabled) {
- ensureAccessibilityEnabled();
- }
- if (mTouchExplorationEnabled) {
- mAccessibilityFeatureFlags |= AccessibilityFeature.ACCESSIBLE_NAVIGATION.value;
- } else {
- mAccessibilityFeatureFlags &= ~AccessibilityFeature.ACCESSIBLE_NAVIGATION.value;
- }
- // Apply additional accessibility settings
- updateAccessibilityFeatures();
- resetWillNotDraw();
- mAccessibilityManager.addAccessibilityStateChangeListener(this);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- if (mTouchExplorationListener == null) {
- mTouchExplorationListener = new TouchExplorationListener();
- }
- mAccessibilityManager.addTouchExplorationStateChangeListener(mTouchExplorationListener);
- }
- }
+ mAccessibilityNodeProvider = new AccessibilityBridge(
+ this,
+ getFlutterNativeView().getFlutterJNI(),
+ new AccessibilityChannel(dartExecutor),
+ (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE),
+ getContext().getContentResolver()
+ );
+ mAccessibilityNodeProvider.setOnAccessibilityChangeListener(onAccessibilityChangeListener);
- private void updateAccessibilityFeatures() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- String transitionAnimationScale = Settings.Global.getString(getContext().getContentResolver(),
- Settings.Global.TRANSITION_ANIMATION_SCALE);
- if (transitionAnimationScale != null && transitionAnimationScale.equals("0")) {
- mAccessibilityFeatureFlags |= AccessibilityFeature.DISABLE_ANIMATIONS.value;
- } else {
- mAccessibilityFeatureFlags &= ~AccessibilityFeature.DISABLE_ANIMATIONS.value;
- }
- }
- mNativeView.getFlutterJNI().setAccessibilityFeatures(mAccessibilityFeatureFlags);
+ resetWillNotDraw(
+ mAccessibilityNodeProvider.isAccessibilityEnabled(),
+ mAccessibilityNodeProvider.isTouchExplorationEnabled()
+ );
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
- getContext().getContentResolver().unregisterContentObserver(mAnimationScaleObserver);
- mAccessibilityManager.removeAccessibilityStateChangeListener(this);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- mAccessibilityManager.removeTouchExplorationStateChangeListener(mTouchExplorationListener);
- }
+
+ mAccessibilityNodeProvider.release();
+ mAccessibilityNodeProvider = null;
}
- private void resetWillNotDraw() {
+ // TODO(mattcarroll): Confer with Ian as to why we need this method. Delete if possible, otherwise add comments.
+ private void resetWillNotDraw(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) {
if (!mIsSoftwareRenderingEnabled) {
- setWillNotDraw(!(mAccessibilityEnabled || mTouchExplorationEnabled));
+ setWillNotDraw(!(isAccessibilityEnabled || isTouchExplorationEnabled));
} else {
setWillNotDraw(false);
}
}
- @Override
- public void onAccessibilityStateChanged(boolean enabled) {
- if (enabled) {
- ensureAccessibilityEnabled();
- } else {
- mAccessibilityEnabled = false;
- if (mAccessibilityNodeProvider != null) {
- mAccessibilityNodeProvider.setAccessibilityEnabled(false);
- }
- mNativeView.getFlutterJNI().setSemanticsEnabled(false);
- }
- resetWillNotDraw();
- }
-
- /// Must match the enum defined in window.dart.
- private enum AccessibilityFeature {
- ACCESSIBLE_NAVIGATION(1 << 0),
- INVERT_COLORS(1 << 1), // NOT SUPPORTED
- DISABLE_ANIMATIONS(1 << 2);
-
- AccessibilityFeature(int value) {
- this.value = value;
- }
-
- final int value;
- }
-
- // Listens to the global TRANSITION_ANIMATION_SCALE property and notifies us so
- // that we can disable animations in Flutter.
- private class AnimationScaleObserver extends ContentObserver {
- public AnimationScaleObserver(Handler handler) {
- super(handler);
- }
-
- @Override
- public void onChange(boolean selfChange) {
- this.onChange(selfChange, null);
- }
-
- @Override
- public void onChange(boolean selfChange, Uri uri) {
- String value = Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 ? null
- : Settings.Global.getString(getContext().getContentResolver(),
- Settings.Global.TRANSITION_ANIMATION_SCALE);
- if (value != null && value.equals("0")) {
- mAccessibilityFeatureFlags |= AccessibilityFeature.DISABLE_ANIMATIONS.value;
- } else {
- mAccessibilityFeatureFlags &= ~AccessibilityFeature.DISABLE_ANIMATIONS.value;
- }
- mNativeView.getFlutterJNI().setAccessibilityFeatures(mAccessibilityFeatureFlags);
- }
- }
-
- // This is guarded at instantiation time.
- @TargetApi(19)
- @RequiresApi(19)
- class TouchExplorationListener implements AccessibilityManager.TouchExplorationStateChangeListener {
- @Override
- public void onTouchExplorationStateChanged(boolean enabled) {
- if (enabled) {
- mTouchExplorationEnabled = true;
- ensureAccessibilityEnabled();
- mAccessibilityFeatureFlags |= AccessibilityFeature.ACCESSIBLE_NAVIGATION.value;
- mNativeView.getFlutterJNI().setAccessibilityFeatures(mAccessibilityFeatureFlags);
- } else {
- mTouchExplorationEnabled = false;
- if (mAccessibilityNodeProvider != null) {
- mAccessibilityNodeProvider.handleTouchExplorationExit();
- }
- mAccessibilityFeatureFlags &= ~AccessibilityFeature.ACCESSIBLE_NAVIGATION.value;
- mNativeView.getFlutterJNI().setAccessibilityFeatures(mAccessibilityFeatureFlags);
- }
- resetWillNotDraw();
- }
- }
-
@Override
public AccessibilityNodeProvider getAccessibilityNodeProvider() {
- if (mAccessibilityEnabled)
+ if (mAccessibilityNodeProvider.isAccessibilityEnabled()) {
return mAccessibilityNodeProvider;
- // TODO(goderbauer): when a11y is off this should return a one-off snapshot of
- // the a11y
- // tree.
- return null;
- }
-
- void ensureAccessibilityEnabled() {
- if (!isAttached())
- return;
- mAccessibilityEnabled = true;
- if (mAccessibilityNodeProvider == null) {
- mAccessibilityNodeProvider = new AccessibilityBridge(this, accessibilityChannel);
- }
- mNativeView.getFlutterJNI().setSemanticsEnabled(true);
- mAccessibilityNodeProvider.setAccessibilityEnabled(true);
- }
-
- void resetAccessibilityTree() {
- if (mAccessibilityNodeProvider != null) {
- mAccessibilityNodeProvider.reset();
- }
- }
-
- private boolean handleAccessibilityHoverEvent(MotionEvent event) {
- if (!mTouchExplorationEnabled) {
- return false;
- }
- if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER || event.getAction() == MotionEvent.ACTION_HOVER_MOVE) {
- mAccessibilityNodeProvider.handleTouchExploration(event.getX(), event.getY());
- } else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
- mAccessibilityNodeProvider.handleTouchExplorationExit();
} else {
- Log.d("flutter", "unexpected accessibility hover event: " + event);
- return false;
+ // TODO(goderbauer): when a11y is off this should return a one-off snapshot of
+ // the a11y
+ // tree.
+ return null;
}
- return true;
}
@Override
diff --git a/tools/android_lint/baseline.xml b/tools/android_lint/baseline.xml
index 93bc535f5737d..8bf5af53ea8c0 100644
--- a/tools/android_lint/baseline.xml
+++ b/tools/android_lint/baseline.xml
@@ -4,22 +4,22 @@
@@ -30,18 +30,18 @@
errorLine2=" ~~~~~~">
@@ -52,7 +52,7 @@
errorLine2=" ~~~~~~">
@@ -63,7 +63,7 @@
errorLine2=" ~~~~~~">
@@ -74,29 +74,29 @@
errorLine2=" ~~~~~~">
@@ -107,7 +107,7 @@
errorLine2=" ~~~~~~">
@@ -118,7 +118,7 @@
errorLine2=" ~~~~~~">
@@ -129,7 +129,7 @@
errorLine2=" ~~~~~~">
@@ -305,7 +305,7 @@
errorLine2=" ~~~~~~~~~~~">
@@ -327,7 +327,7 @@
errorLine2=" ^">
@@ -360,7 +360,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -371,7 +371,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -432,24 +432,24 @@
+ message="Use `new SparseArray<SemanticsNode>(...)` instead for better performance"
+ errorLine1=" private final Map<Integer, SemanticsNode> flutterSemanticsTree = new HashMap<>();"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ line="135"
+ column="70"/>
+ errorLine1=" private final Map<Integer, CustomAccessibilityAction> customAccessibilityActions = new HashMap<>();"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ line="160"
+ column="88"/>