Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ded133c4 authored by Svetoslav's avatar Svetoslav Committed by Svetoslav Ganov
Browse files

Fix broken activation of the selected view in accessibility mode.

We were using an approximation to determine where to send a pair of down
and up events to click on the view that has accessibility focus. We were
doing reverse computation to figuring out which portion of the view is
not covered by interactive views and get a point in this region. However,
determining whether a view is interactive is not feasible in general since
for example may override onTouchEvent. This results in views not being
activated or which is worse wrong views being activated.

This change swithes to a new approach to activate views in accessibility
mode which is guaranteed to always work except the very rare case of a
view that overrides dispatchTouchEvent (which developers shouldn't be
doing). The new approach is to flag the down and up events pair sent
by the touch explorer as targeting the accessibility focused view. Such
events are dispatched such that views predecessors of the accessibility
focus do not handle them guaranteeing that these events reach the accessibiliy
focused view. Once the accessibiliy focused view gets such an event it clears
the flag and the event is dispatched following the normal event dispatch
semantics.

The new approach is semantically equivalent to requesting the view to perform
a click accessiblitiy action but is more generic as it is not affected by
views not implementing click action support correctly.

bug:18986806
bug:18889611

Change-Id: Id4b7b886c9fd34f7eb11e606636d8e3bab122869
parent 175ddbcf
Loading
Loading
Loading
Loading
+0 −4
Original line number Diff line number Diff line
@@ -54,10 +54,6 @@ interface IAccessibilityServiceConnection {
        int action, in Bundle arguments, int interactionId,
        IAccessibilityInteractionConnectionCallback callback, long threadId);

    boolean computeClickPointInScreen(int accessibilityWindowId, long accessibilityNodeId,
        int interactionId, IAccessibilityInteractionConnectionCallback callback,
        long threadId);

    AccessibilityWindowInfo getWindow(int windowId);

    List<AccessibilityWindowInfo> getWindows();
+0 −95
Original line number Diff line number Diff line
@@ -636,95 +636,6 @@ final class AccessibilityInteractionController {
        }
    }

    public void computeClickPointInScreenClientThread(long accessibilityNodeId,
            Region interactiveRegion, int interactionId,
            IAccessibilityInteractionConnectionCallback callback, int interrogatingPid,
            long interrogatingTid, MagnificationSpec spec) {
        Message message = mHandler.obtainMessage();
        message.what = PrivateHandler.MSG_COMPUTE_CLICK_POINT_IN_SCREEN;

        SomeArgs args = SomeArgs.obtain();
        args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId);
        args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId);
        args.argi3 = interactionId;
        args.arg1 = callback;
        args.arg2 = spec;
        args.arg3 = interactiveRegion;

        message.obj = args;

        // If the interrogation is performed by the same thread as the main UI
        // thread in this process, set the message as a static reference so
        // after this call completes the same thread but in the interrogating
        // client can handle the message to generate the result.
        if (interrogatingPid == mMyProcessId && interrogatingTid == mMyLooperThreadId) {
            AccessibilityInteractionClient.getInstanceForThread(
                    interrogatingTid).setSameThreadMessage(message);
        } else {
            mHandler.sendMessage(message);
        }
    }

    private void computeClickPointInScreenUiThread(Message message) {
        SomeArgs args = (SomeArgs) message.obj;
        final int accessibilityViewId = args.argi1;
        final int virtualDescendantId = args.argi2;
        final int interactionId = args.argi3;
        final IAccessibilityInteractionConnectionCallback callback =
                (IAccessibilityInteractionConnectionCallback) args.arg1;
        final MagnificationSpec spec = (MagnificationSpec) args.arg2;
        final Region interactiveRegion = (Region) args.arg3;
        args.recycle();

        boolean succeeded = false;
        Point point = mTempPoint;
        try {
            if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) {
                return;
            }
            View target = null;
            if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
                target = findViewByAccessibilityId(accessibilityViewId);
            } else {
                target = mViewRootImpl.mView;
            }
            if (target != null && isShown(target)) {
                AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider();
                if (provider != null) {
                    // For virtual views just use the center of the bounds in screen.
                    AccessibilityNodeInfo node = null;
                    if (virtualDescendantId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
                        node = provider.createAccessibilityNodeInfo(virtualDescendantId);
                    } else {
                        node = provider.createAccessibilityNodeInfo(
                                AccessibilityNodeProvider.HOST_VIEW_ID);
                    }
                    if (node != null) {
                        succeeded = true;
                        Rect boundsInScreen = mTempRect;
                        node.getBoundsInScreen(boundsInScreen);
                        point.set(boundsInScreen.centerX(), boundsInScreen.centerY());
                    }
                } else if (virtualDescendantId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
                    // For a real view, ask the view to compute the click point.
                    succeeded = target.computeClickPointInScreenForAccessibility(
                            interactiveRegion, point);
                }
            }
        } finally {
            try {
                Point result = null;
                if (succeeded) {
                    applyAppScaleAndMagnificationSpecIfNeeded(point, spec);
                    result = point;
                }
                callback.setComputeClickPointInScreenActionResult(result, interactionId);
            } catch (RemoteException re) {
                /* ignore - the other side will time out */
            }
        }
    }

    private View findViewByAccessibilityId(int accessibilityId) {
        View root = mViewRootImpl.mView;
        if (root == null) {
@@ -1201,7 +1112,6 @@ final class AccessibilityInteractionController {
        private final static int MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_TEXT = 4;
        private final static int MSG_FIND_FOCUS = 5;
        private final static int MSG_FOCUS_SEARCH = 6;
        private final static int MSG_COMPUTE_CLICK_POINT_IN_SCREEN = 7;

        public PrivateHandler(Looper looper) {
            super(looper);
@@ -1223,8 +1133,6 @@ final class AccessibilityInteractionController {
                    return "MSG_FIND_FOCUS";
                case MSG_FOCUS_SEARCH:
                    return "MSG_FOCUS_SEARCH";
                case MSG_COMPUTE_CLICK_POINT_IN_SCREEN:
                    return "MSG_COMPUTE_CLICK_POINT_IN_SCREEN";
                default:
                    throw new IllegalArgumentException("Unknown message type: " + type);
            }
@@ -1252,9 +1160,6 @@ final class AccessibilityInteractionController {
                case MSG_FOCUS_SEARCH: {
                    focusSearchUiThread(message);
                } break;
                case MSG_COMPUTE_CLICK_POINT_IN_SCREEN: {
                    computeClickPointInScreenUiThread(message);
                } break;
                default:
                    throw new IllegalArgumentException("Unknown message type: " + type);
            }
+31 −0
Original line number Diff line number Diff line
@@ -401,6 +401,23 @@ public final class MotionEvent extends InputEvent implements Parcelable {
     */
    public static final int FLAG_TAINTED = 0x80000000;

    /**
     * Private flag indicating that this event was synthesized by the system and
     * should be delivered to the accessibility focused view first. When being
     * dispatched such an event is not handled by predecessors of the accessibility
     * focused view and after the event reaches that view the flag is cleared and
     * normal event dispatch is performed. This ensures that the platform can click
     * on any view that has accessibility focus which is semantically equivalent to
     * asking the view to perform a click accessibility action but more generic as
     * views not implementing click action correctly can still be activated.
     *
     * @hide
     * @see #isTargetAccessibilityFocus()
     * @see #setTargetAccessibilityFocus(boolean)
     */
    public static final int FLAG_TARGET_ACCESSIBILITY_FOCUS = 0x40000000;


    /**
     * Flag indicating the motion event intersected the top edge of the screen.
     */
@@ -1766,6 +1783,20 @@ public final class MotionEvent extends InputEvent implements Parcelable {
        nativeSetFlags(mNativePtr, tainted ? flags | FLAG_TAINTED : flags & ~FLAG_TAINTED);
    }

    /** @hide */
    public final boolean isTargetAccessibilityFocus() {
        final int flags = getFlags();
        return (flags & FLAG_TARGET_ACCESSIBILITY_FOCUS) != 0;
    }

    /** @hide */
    public final void setTargetAccessibilityFocus(boolean targetsFocus) {
        final int flags = getFlags();
        nativeSetFlags(mNativePtr, targetsFocus
                ? flags | FLAG_TARGET_ACCESSIBILITY_FOCUS
                : flags & ~FLAG_TARGET_ACCESSIBILITY_FOCUS);
    }

    /**
     * Returns the time (in ms) when the user originally pressed down to start
     * a stream of position events.
+31 −138
Original line number Diff line number Diff line
@@ -5553,12 +5553,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
    }
    /**
     * Gets the location of this view in screen coordintates.
     * Gets the location of this view in screen coordinates.
     *
     * @param outRect The output location
     * @hide
     */
    public void getBoundsOnScreen(Rect outRect) {
        getBoundsOnScreen(outRect, false);
    }
    /**
     * Gets the location of this view in screen coordinates.
     *
     * @param outRect The output location
     * @param clipToParent Whether to clip child bounds to the parent ones.
     * @hide
     */
    public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
        if (mAttachInfo == null) {
            return;
        }
@@ -5578,6 +5589,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
            position.offset(-parentView.mScrollX, -parentView.mScrollY);
            if (clipToParent) {
                position.left = Math.max(position.left, 0);
                position.top = Math.max(position.top, 0);
                position.right = Math.min(position.right, parentView.getWidth());
                position.bottom = Math.min(position.bottom, parentView.getHeight());
            }
            if (!parentView.hasIdentityMatrix()) {
                parentView.getMatrix().mapRect(position);
            }
@@ -5609,7 +5627,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
        getDrawingRect(bounds);
        info.setBoundsInParent(bounds);
        getBoundsOnScreen(bounds);
        getBoundsOnScreen(bounds, true);
        info.setBoundsInScreen(bounds);
        ViewParent parent = getParentForAccessibility();
@@ -5804,142 +5822,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
        return false;
    }
    /**
     * Computes a point on which a sequence of a down/up event can be sent to
     * trigger clicking this view. This method is for the exclusive use by the
     * accessibility layer to determine where to send a click event in explore
     * by touch mode.
     *
     * @param interactiveRegion The interactive portion of this window.
     * @param outPoint The point to populate.
     * @return True of such a point exists.
     */
    boolean computeClickPointInScreenForAccessibility(Region interactiveRegion,
            Point outPoint) {
        // Since the interactive portion of the view is a region but as a view
        // may have a transformation matrix which cannot be applied to a
        // region we compute the view bounds rectangle and all interactive
        // predecessor's and sibling's (siblings of predecessors included)
        // rectangles that intersect the view bounds. At the
        // end if the view was partially covered by another interactive
        // view we compute the view's interactive region and pick a point
        // on its boundary path as regions do not offer APIs to get inner
        // points. Note that the the code is optimized to fail early and
        // avoid unnecessary allocations plus computations.
        // The current approach has edge cases that may produce false
        // positives or false negatives. For example, a portion of the
        // view may be covered by an interactive descendant of a
        // predecessor, which we do not compute. Also a view may be handling
        // raw touch events instead registering click listeners, which
        // we cannot compute. Despite these limitations this approach will
        // work most of the time and it is a huge improvement over just
        // blindly sending the down and up events in the center of the
        // view.
        // Cannot click on an unattached view.
        if (mAttachInfo == null) {
            return false;
        }
        // Attached to an invisible window means this view is not visible.
        if (mAttachInfo.mWindowVisibility != View.VISIBLE) {
            return false;
        }
        RectF bounds = mAttachInfo.mTmpTransformRect;
        bounds.set(0, 0, getWidth(), getHeight());
        List<RectF> intersections = mAttachInfo.mTmpRectList;
        intersections.clear();
        if (mParent instanceof ViewGroup) {
            ViewGroup parentGroup = (ViewGroup) mParent;
            if (!parentGroup.translateBoundsAndIntersectionsInWindowCoordinates(
                    this, bounds, intersections)) {
                intersections.clear();
                return false;
            }
        }
        // Take into account the window location.
        final int dx = mAttachInfo.mWindowLeft;
        final int dy = mAttachInfo.mWindowTop;
        bounds.offset(dx, dy);
        offsetRects(intersections, dx, dy);
        if (intersections.isEmpty() && interactiveRegion == null) {
            outPoint.set((int) bounds.centerX(), (int) bounds.centerY());
        } else {
            // This view is partially covered by other views, then compute
            // the not covered region and pick a point on its boundary.
            Region region = new Region();
            region.set((int) bounds.left, (int) bounds.top,
                    (int) bounds.right, (int) bounds.bottom);
            final int intersectionCount = intersections.size();
            for (int i = intersectionCount - 1; i >= 0; i--) {
                RectF intersection = intersections.remove(i);
                region.op((int) intersection.left, (int) intersection.top,
                        (int) intersection.right, (int) intersection.bottom,
                        Region.Op.DIFFERENCE);
            }
            // If the view is completely covered, done.
            if (region.isEmpty()) {
                return false;
            }
            // Take into account the interactive portion of the window
            // as the rest is covered by other windows. If no such a region
            // then the whole window is interactive.
            if (interactiveRegion != null) {
                region.op(interactiveRegion, Region.Op.INTERSECT);
            }
            // Take into account the window bounds.
            final View root = getRootView();
            if (root != null) {
                region.op(dx, dy, root.getWidth() + dx, root.getHeight() + dy, Region.Op.INTERSECT);
            }
            // If the view is completely covered, done.
            if (region.isEmpty()) {
                return false;
            }
            // Try a shortcut here.
            if (region.isRect()) {
                Rect regionBounds = mAttachInfo.mTmpInvalRect;
                region.getBounds(regionBounds);
                outPoint.set(regionBounds.centerX(), regionBounds.centerY());
                return true;
            }
            // Get the a point on the region boundary path.
            Path path = region.getBoundaryPath();
            PathMeasure pathMeasure = new PathMeasure(path, false);
            final float[] coordinates = mAttachInfo.mTmpTransformLocation;
            // Without loss of generality pick a point.
            final float point = pathMeasure.getLength() * 0.01f;
            if (!pathMeasure.getPosTan(point, coordinates, null)) {
                return false;
            }
            outPoint.set(Math.round(coordinates[0]), Math.round(coordinates[1]));
        }
        return true;
    }
    static void offsetRects(List<RectF> rects, float offsetX, float offsetY) {
        final int rectCount = rects.size();
        for (int i = 0; i < rectCount; i++) {
            RectF intersection = rects.get(i);
            intersection.offset(offsetX, offsetY);
        }
    }
    /**
     * Returns the delegate for implementing accessibility support via
     * composition. For more details see {@link AccessibilityDelegate}.
@@ -8555,6 +8437,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocused() && !(getViewRootImpl() != null && getViewRootImpl()
                    .getAccessibilityFocusedHost() == this)) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }
        boolean result = false;
        if (mInputEventConsistencyVerifier != null) {
+20 −205

File changed.

Preview size limit exceeded, changes collapsed.

Loading