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

Commit f163deb7 authored by sallyyuen's avatar sallyyuen
Browse files

Add accessibility support for Drag & Drop

go/a11yDragAndDrop

Add AccessibilityAction#ACTION_DROP to potential drop targets.
Add A11yAction#ACTION_CANCEL to the source of the drag.
Developers will be required add the A11yAction#ACTION_DRAG action
(androidX can do this)

Send AccessibilityEvents when appropriate. These events will be
sent when a drag is performed via a11y actions, or via touch events
with a11y enabled.

Drop and cancel events are mutuall exclusive.

To avoid a scenario where a user starts a drag via an a11yAction but
doesn't drop or cancel, the system will cancel the event after a minute
timeout.

Corner cases:
1) A user cannot start a drag on another view without first cancelling
the current one

2) An ACTION_DRAG on a View won't be successful if the last touch point
was in that View

For a11y, we send window events directly from the View to avoid source
merging in ViewRootImpl. (Specifically for the start events, where the
event gets sent from the common ancestor of the source and target
instead of the source)

Bug: 26871588
Test: atest DragDropControllerTests CtsWindowManagerDeviceTestCases:CrossAppDragAndDropTests AccessibilityDragAndDropTest
Use Android's official sample drag and drop apps. Drag within window and across split screens
Use ReceiveContentDemo target app with sample app

Change-Id: Ie8554d25ab6c43b08d2dacb8c8907b1636bd72b4
parent cd9481d2
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -174,6 +174,11 @@ interface IWindowSession {
    IBinder performDrag(IWindow window, int flags, in SurfaceControl surface, int touchSource,
            float touchX, float touchY, float thumbCenterX, float thumbCenterY, in ClipData data);

    /**
     * Drops the content of the current drag operation for accessibility
     */
    boolean dropForAccessibility(IWindow window, int x, int y);

    /**
     * Report the result of a drop action targeted to the given window.
     * consumed is 'true' when the drop was accepted by a valid recipient,
+130 −7
Original line number Diff line number Diff line
@@ -3515,6 +3515,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     *                    1             PFLAG4_ALLOW_CLICK_WHEN_DISABLED
     *                   1              PFLAG4_DETACHED
     *                  1               PFLAG4_HAS_TRANSLATION_TRANSIENT_STATE
     *                 1                PFLAG4_DRAG_A11Y_STARTED
     * |-------|-------|-------|-------|
     */
@@ -3586,6 +3587,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     */
    private static final int PFLAG4_HAS_TRANSLATION_TRANSIENT_STATE = 0x000004000;
    /**
     * Indicates that the view has started a drag with {@link AccessibilityAction#ACTION_DRAG_START}
     */
    private static final int PFLAG4_DRAG_A11Y_STARTED = 0x000008000;
    /* End of masks for mPrivateFlags4 */
    /** @hide */
@@ -10384,8 +10390,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
        if (mTouchDelegate != null) {
            info.setTouchDelegateInfo(mTouchDelegate.getTouchDelegateInfo());
        }
        if (startedSystemDragForAccessibility()) {
            info.addAction(AccessibilityAction.ACTION_DRAG_CANCEL);
        }
        if (canAcceptAccessibilityDrop()) {
            info.addAction(AccessibilityAction.ACTION_DRAG_DROP);
        }
    }
    /**
     * Adds extra data to an {@link AccessibilityNodeInfo} based on an explicit request for the
     * additional data.
@@ -14222,9 +14237,45 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                return true;
            }
        }
        if (action == R.id.accessibilityActionDragDrop) {
            if (!canAcceptAccessibilityDrop()) {
                return false;
            }
            try {
                if (mAttachInfo != null && mAttachInfo.mSession != null) {
                    final int[] location = new int[2];
                    getLocationInWindow(location);
                    final int centerX = location[0] + getWidth() / 2;
                    final int centerY = location[1] + getHeight() / 2;
                    return mAttachInfo.mSession.dropForAccessibility(mAttachInfo.mWindow,
                            centerX, centerY);
                }
            } catch (RemoteException e) {
                Log.e(VIEW_LOG_TAG, "Unable to drop for accessibility", e);
            }
            return false;
        } else if (action == R.id.accessibilityActionDragCancel) {
            if (!startedSystemDragForAccessibility()) {
                return false;
            }
            if (mAttachInfo != null && mAttachInfo.mDragToken != null) {
                cancelDragAndDrop();
                return true;
            }
            return false;
        }
        return false;
    }
    private boolean canAcceptAccessibilityDrop() {
        if (!canAcceptDrag()) {
            return false;
        }
        ListenerInfo li = mListenerInfo;
        return (li != null) && (li.mOnDragListener != null || li.mOnReceiveContentListener != null);
    }
    private boolean traverseAtGranularity(int granularity, boolean forward,
            boolean extendSelection) {
        CharSequence text = getIterableTextForAccessibility();
@@ -26664,6 +26715,37 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
            data.prepareToLeaveProcess((flags & View.DRAG_FLAG_GLOBAL) != 0);
        }
        Rect bounds = new Rect();
        getBoundsOnScreen(bounds, true);
        Point lastTouchPoint = new Point();
        mAttachInfo.mViewRootImpl.getLastTouchPoint(lastTouchPoint);
        final ViewRootImpl root = mAttachInfo.mViewRootImpl;
        // Skip surface logic since shadows and animation are not required during the a11y drag
        final boolean a11yEnabled = AccessibilityManager.getInstance(mContext).isEnabled();
        if (a11yEnabled && (flags & View.DRAG_FLAG_ACCESSIBILITY_ACTION) != 0) {
            try {
                IBinder token = mAttachInfo.mSession.performDrag(
                        mAttachInfo.mWindow, flags, null,
                        mAttachInfo.mViewRootImpl.getLastTouchSource(),
                        0f, 0f, 0f, 0f, data);
                if (ViewDebug.DEBUG_DRAG) {
                    Log.d(VIEW_LOG_TAG, "startDragAndDrop via a11y action returned " + token);
                }
                if (token != null) {
                    root.setLocalDragState(myLocalState);
                    mAttachInfo.mDragToken = token;
                    mAttachInfo.mViewRootImpl.setDragStartedViewForAccessibility(this);
                    setAccessibilityDragStarted(true);
                }
                return token != null;
            } catch (Exception e) {
                Log.e(VIEW_LOG_TAG, "Unable to initiate a11y drag", e);
                return false;
            }
        }
        Point shadowSize = new Point();
        Point shadowTouchPoint = new Point();
        shadowBuilder.onProvideShadowMetrics(shadowSize, shadowTouchPoint);
@@ -26688,7 +26770,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                    + " shadowX=" + shadowTouchPoint.x + " shadowY=" + shadowTouchPoint.y);
        }
        final ViewRootImpl root = mAttachInfo.mViewRootImpl;
        final SurfaceSession session = new SurfaceSession();
        final SurfaceControl surfaceControl = new SurfaceControl.Builder(session)
                .setName("drag surface")
@@ -26711,12 +26792,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                surface.unlockCanvasAndPost(canvas);
            }
            // repurpose 'shadowSize' for the last touch point
            root.getLastTouchPoint(shadowSize);
            token = mAttachInfo.mSession.performDrag(
                    mAttachInfo.mWindow, flags, surfaceControl, root.getLastTouchSource(),
                    shadowSize.x, shadowSize.y, shadowTouchPoint.x, shadowTouchPoint.y, data);
            token = mAttachInfo.mSession.performDrag(mAttachInfo.mWindow, flags, surfaceControl,
                    root.getLastTouchSource(), lastTouchPoint.x, lastTouchPoint.y,
                    shadowTouchPoint.x, shadowTouchPoint.y, data);
            if (ViewDebug.DEBUG_DRAG) {
                Log.d(VIEW_LOG_TAG, "performDrag returned " + token);
            }
@@ -26728,6 +26806,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                mAttachInfo.mDragToken = token;
                // Cache the local state object for delivery with DragEvents
                root.setLocalDragState(myLocalState);
                if (a11yEnabled) {
                    // Set for AccessibilityEvents
                    mAttachInfo.mViewRootImpl.setDragStartedViewForAccessibility(this);
                }
            }
            return token != null;
        } catch (Exception e) {
@@ -26741,6 +26823,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
        }
    }
    void setAccessibilityDragStarted(boolean started) {
        int pflags4 = mPrivateFlags4;
        if (started) {
            pflags4 |= PFLAG4_DRAG_A11Y_STARTED;
        } else {
            pflags4 &= ~PFLAG4_DRAG_A11Y_STARTED;
        }
        if (pflags4 != mPrivateFlags4) {
            mPrivateFlags4 = pflags4;
            sendWindowContentChangedAccessibilityEvent(CONTENT_CHANGE_TYPE_UNDEFINED);
        }
    }
    private boolean startedSystemDragForAccessibility() {
        return (mPrivateFlags4 & PFLAG4_DRAG_A11Y_STARTED) != 0;
    }
    /**
     * Cancels an ongoing drag and drop operation.
     * <p>
@@ -26958,6 +27058,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
        }
        switch (event.mAction) {
            case DragEvent.ACTION_DRAG_STARTED: {
                if (result && li.mOnDragListener != null) {
                    sendWindowContentChangedAccessibilityEvent(
                            AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
                }
            } break;
            case DragEvent.ACTION_DRAG_ENTERED: {
                mPrivateFlags2 |= View.PFLAG2_DRAG_HOVERED;
                refreshDrawableState();
@@ -26966,7 +27072,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                mPrivateFlags2 &= ~View.PFLAG2_DRAG_HOVERED;
                refreshDrawableState();
            } break;
            case DragEvent.ACTION_DROP: {
                if (result && (li.mOnDragListener != null | li.mOnReceiveContentListener != null)) {
                    sendWindowContentChangedAccessibilityEvent(
                            AccessibilityEvent.CONTENT_CHANGE_TYPE_DRAG_DROPPED);
                }
            } break;
            case DragEvent.ACTION_DRAG_ENDED: {
                sendWindowContentChangedAccessibilityEvent(
                        AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
                mPrivateFlags2 &= ~View.DRAG_MASK;
                refreshDrawableState();
            } break;
@@ -26979,6 +27093,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
        return (mPrivateFlags2 & PFLAG2_DRAG_CAN_ACCEPT) != 0;
    }
    void sendWindowContentChangedAccessibilityEvent(int changeType) {
        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
            AccessibilityEvent event = AccessibilityEvent.obtain();
            event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
            event.setContentChangeTypes(changeType);
            sendAccessibilityEventUnchecked(event);
        }
    }
    /**
     * This needs to be a better API (NOT ON VIEW) before it is exposed.  If
     * it is ever exposed at all.
+3 −0
Original line number Diff line number Diff line
@@ -412,6 +412,9 @@ public interface ViewParent {
     *            <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE}
     *            <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT}
     *            <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED}
     *            <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_DRAG_STARTED}
     *            <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_DRAG_CANCELLED}
     *            <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_DRAG_DROPPED}
     *            </ul>
     */
    public void notifySubtreeAccessibilityStateChanged(
+23 −0
Original line number Diff line number Diff line
@@ -648,6 +648,7 @@ public final class ViewRootImpl implements ViewParent,
    /* Drag/drop */
    ClipDescription mDragDescription;
    View mCurrentDragView;
    View mStartedDragViewForA11y;
    volatile Object mLocalDragState;
    final PointF mDragPoint = new PointF();
    final PointF mLastTouchPoint = new PointF();
@@ -7573,6 +7574,11 @@ public final class ViewRootImpl implements ViewParent,
            if (what == DragEvent.ACTION_DRAG_STARTED) {
                mCurrentDragView = null;    // Start the current-recipient tracking
                mDragDescription = event.mClipDescription;
                if (mStartedDragViewForA11y != null) {
                    // Send a drag started a11y event
                    mStartedDragViewForA11y.sendWindowContentChangedAccessibilityEvent(
                            AccessibilityEvent.CONTENT_CHANGE_TYPE_DRAG_STARTED);
                }
            } else {
                if (what == DragEvent.ACTION_DRAG_ENDED) {
                    mDragDescription = null;
@@ -7647,6 +7653,16 @@ public final class ViewRootImpl implements ViewParent,

                // When the drag operation ends, reset drag-related state
                if (what == DragEvent.ACTION_DRAG_ENDED) {
                    if (mStartedDragViewForA11y != null) {
                        // If the drag failed, send a cancelled event from the source. Otherwise,
                        // the View that accepted the drop sends CONTENT_CHANGE_TYPE_DRAG_DROPPED
                        if (!event.getResult()) {
                            mStartedDragViewForA11y.sendWindowContentChangedAccessibilityEvent(
                                    AccessibilityEvent.CONTENT_CHANGE_TYPE_DRAG_CANCELLED);
                        }
                        mStartedDragViewForA11y.setAccessibilityDragStarted(false);
                    }
                    mStartedDragViewForA11y = null;
                    mCurrentDragView = null;
                    setLocalDragState(null);
                    mAttachInfo.mDragToken = null;
@@ -7726,6 +7742,13 @@ public final class ViewRootImpl implements ViewParent,
        mCurrentDragView = newDragTarget;
    }

    /** Sets the view that started drag and drop for the purpose of sending AccessibilityEvents */
    void setDragStartedViewForAccessibility(View view) {
        if (mStartedDragViewForA11y == null) {
            mStartedDragViewForA11y = view;
        }
    }

    private AudioManager getAudioManager() {
        if (mView == null) {
            throw new IllegalStateException("getAudioManager called when there is no mView");
+5 −0
Original line number Diff line number Diff line
@@ -498,4 +498,9 @@ public class WindowlessWindowManager implements IWindowSession {
    public void generateDisplayHash(IWindow window, Rect boundsInWindow, String hashAlgorithm,
            RemoteCallback callback) {
    }

    @Override
    public boolean dropForAccessibility(IWindow window, int x, int y) {
        return false;
    }
}
Loading