From f163deb788a78f5346c45ceab322f314789f2193 Mon Sep 17 00:00:00 2001 From: sallyyuen Date: Tue, 24 Aug 2021 09:10:18 -0700 Subject: [PATCH] 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 --- core/java/android/view/IWindowSession.aidl | 5 + core/java/android/view/View.java | 137 +++++++++++++++++- core/java/android/view/ViewParent.java | 3 + core/java/android/view/ViewRootImpl.java | 23 +++ .../android/view/WindowlessWindowManager.java | 5 + .../android/server/wm/DragDropController.java | 97 +++++++++---- .../java/com/android/server/wm/DragState.java | 27 ++-- .../java/com/android/server/wm/Session.java | 11 ++ .../server/wm/DragDropControllerTests.java | 40 ++++- 9 files changed, 301 insertions(+), 47 deletions(-) diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index a6abed0209591..9da50889e43f9 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -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, diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 1ecdb900316d0..afdb755179a6b 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -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. *

@@ -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. diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index 775c15e77d5dd..49f5229d3c09c 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -412,6 +412,9 @@ public interface ViewParent { *

  • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE} *
  • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT} *
  • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED} + *
  • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_DRAG_STARTED} + *
  • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_DRAG_CANCELLED} + *
  • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_DRAG_DROPPED} * */ public void notifySubtreeAccessibilityStateChanged( diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 4c36fc939c34c..d53286885f854 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -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"); diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index c413a9ba7a272..ffb7efa672432 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -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; + } } diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java index d12d07ab74481..cc6a8807bb9d2 100644 --- a/services/core/java/com/android/server/wm/DragDropController.java +++ b/services/core/java/com/android/server/wm/DragDropController.java @@ -22,6 +22,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.annotation.NonNull; import android.content.ClipData; +import android.content.Context; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -32,6 +33,7 @@ import android.view.Display; import android.view.IWindow; import android.view.SurfaceControl; import android.view.View; +import android.view.accessibility.AccessibilityManager; import com.android.server.wm.WindowManagerInternal.IDragDropCallback; @@ -43,7 +45,8 @@ import java.util.concurrent.atomic.AtomicReference; */ class DragDropController { private static final float DRAG_SHADOW_ALPHA_TRANSPARENT = .7071f; - private static final long DRAG_TIMEOUT_MS = 5000; + static final long DRAG_TIMEOUT_MS = 5000; + private static final int A11Y_DRAG_TIMEOUT_DEFAULT_MS = 60000; // Messages for Handler. static final int MSG_DRAG_END_TIMEOUT = 0; @@ -151,36 +154,48 @@ class DragDropController { mDragState.mOriginalAlpha = alpha; mDragState.mToken = dragToken; mDragState.mDisplayContent = displayContent; - - final Display display = displayContent.getDisplay(); - if (!mCallback.get().registerInputChannel( - mDragState, display, mService.mInputManager, - callingWin.mInputChannel)) { - Slog.e(TAG_WM, "Unable to transfer touch focus"); - return null; - } - - final SurfaceControl surfaceControl = mDragState.mSurfaceControl; mDragState.mData = data; - mDragState.broadcastDragStartedLocked(touchX, touchY); - mDragState.overridePointerIconLocked(touchSource); - // remember the thumb offsets for later - mDragState.mThumbOffsetX = thumbCenterX; - mDragState.mThumbOffsetY = thumbCenterY; - // Make the surface visible at the proper location - if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG_WM, ">>> OPEN TRANSACTION performDrag"); + if ((flags & View.DRAG_FLAG_ACCESSIBILITY_ACTION) == 0) { + final Display display = displayContent.getDisplay(); + if (!mCallback.get().registerInputChannel( + mDragState, display, mService.mInputManager, + callingWin.mInputChannel)) { + Slog.e(TAG_WM, "Unable to transfer touch focus"); + return null; + } - final SurfaceControl.Transaction transaction = mDragState.mTransaction; - transaction.setAlpha(surfaceControl, mDragState.mOriginalAlpha); - transaction.setPosition( - surfaceControl, touchX - thumbCenterX, touchY - thumbCenterY); - transaction.show(surfaceControl); - displayContent.reparentToOverlay(transaction, surfaceControl); - callingWin.scheduleAnimation(); + final SurfaceControl surfaceControl = mDragState.mSurfaceControl; + mDragState.broadcastDragStartedLocked(touchX, touchY); + mDragState.overridePointerIconLocked(touchSource); + // remember the thumb offsets for later + mDragState.mThumbOffsetX = thumbCenterX; + mDragState.mThumbOffsetY = thumbCenterY; - if (SHOW_LIGHT_TRANSACTIONS) { - Slog.i(TAG_WM, "<<< CLOSE TRANSACTION performDrag"); + // Make the surface visible at the proper location + if (SHOW_LIGHT_TRANSACTIONS) { + Slog.i(TAG_WM, ">>> OPEN TRANSACTION performDrag"); + } + + final SurfaceControl.Transaction transaction = mDragState.mTransaction; + transaction.setAlpha(surfaceControl, mDragState.mOriginalAlpha); + transaction.setPosition( + surfaceControl, touchX - thumbCenterX, touchY - thumbCenterY); + transaction.show(surfaceControl); + displayContent.reparentToOverlay(transaction, surfaceControl); + callingWin.scheduleAnimation(); + if (SHOW_LIGHT_TRANSACTIONS) { + Slog.i(TAG_WM, "<<< CLOSE TRANSACTION performDrag"); + } + } else { + // Skip surface logic for a drag triggered by an AccessibilityAction + mDragState.broadcastDragStartedLocked(touchX, touchY); + + // Timeout for the user to drop the content + sendTimeoutMessage(MSG_DRAG_END_TIMEOUT, callingWin.mClient.asBinder(), + getAccessibilityManager().getRecommendedTimeoutMillis( + A11Y_DRAG_TIMEOUT_DEFAULT_MS, + AccessibilityManager.FLAG_CONTENT_CONTROLS)); } } finally { if (surface != null) { @@ -309,10 +324,10 @@ class DragDropController { /** * Sends a timeout message to the Handler managed by DragDropController. */ - void sendTimeoutMessage(int what, Object arg) { + void sendTimeoutMessage(int what, Object arg, long timeoutMs) { mHandler.removeMessages(what, arg); final Message msg = mHandler.obtainMessage(what, arg); - mHandler.sendMessageDelayed(msg, DRAG_TIMEOUT_MS); + mHandler.sendMessageDelayed(msg, timeoutMs); } /** @@ -332,6 +347,30 @@ class DragDropController { } } + boolean dropForAccessibility(IWindow window, float x, float y) { + synchronized (mService.mGlobalLock) { + final boolean isA11yEnabled = getAccessibilityManager().isEnabled(); + if (!dragDropActiveLocked()) { + return false; + } + if (mDragState.isAccessibilityDragDrop() && isA11yEnabled) { + final WindowState winState = mService.windowForClientLocked( + null, window, false); + if (!mDragState.isWindowNotified(winState)) { + return false; + } + IBinder token = winState.mInputChannelToken; + return mDragState.reportDropWindowLock(token, x, y); + } + return false; + } + } + + AccessibilityManager getAccessibilityManager() { + return (AccessibilityManager) mService.mContext.getSystemService( + Context.ACCESSIBILITY_SERVICE); + } + private class DragHandler extends Handler { /** * Lock for window manager. diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java index 18ea738b08ce1..e92de67377cd4 100644 --- a/services/core/java/com/android/server/wm/DragState.java +++ b/services/core/java/com/android/server/wm/DragState.java @@ -253,7 +253,7 @@ class DragState { mTransaction.reparent(mSurfaceControl, null).apply(); } else { mDragDropController.sendTimeoutMessage(MSG_REMOVE_DRAG_SURFACE_TIMEOUT, - mSurfaceControl); + mSurfaceControl, DragDropController.DRAG_TIMEOUT_MS); } mSurfaceControl = null; } @@ -276,9 +276,9 @@ class DragState { * Notify the drop target and tells it about the data. If the drop event is not sent to the * target, invokes {@code endDragLocked} immediately. */ - void reportDropWindowLock(IBinder token, float x, float y) { + boolean reportDropWindowLock(IBinder token, float x, float y) { if (mAnimator != null) { - return; + return false; } final WindowState touchedWin = mService.mInputToWindowMap.get(token); @@ -288,7 +288,7 @@ class DragState { mDragResult = false; endDragLocked(); if (DEBUG_DRAG) Slog.d(TAG_WM, "Drop outside a valid window " + touchedWin); - return; + return false; } if (DEBUG_DRAG) Slog.d(TAG_WM, "sending DROP to " + touchedWin); @@ -322,16 +322,19 @@ class DragState { touchedWin.mClient.dispatchDragEvent(event); // 5 second timeout for this window to respond to the drop - mDragDropController.sendTimeoutMessage(MSG_DRAG_END_TIMEOUT, clientToken); + mDragDropController.sendTimeoutMessage(MSG_DRAG_END_TIMEOUT, clientToken, + DragDropController.DRAG_TIMEOUT_MS); } catch (RemoteException e) { Slog.w(TAG_WM, "can't send drop notification to win " + touchedWin); endDragLocked(); + return false; } finally { if (myPid != touchedWin.mSession.mPid) { event.recycle(); } } mToken = clientToken; + return true; } class InputInterceptor { @@ -552,7 +555,7 @@ class DragState { } } - private boolean isWindowNotified(WindowState newWin) { + boolean isWindowNotified(WindowState newWin) { for (WindowState ws : mNotifiedWindows) { if (ws == newWin) { return true; @@ -566,8 +569,10 @@ class DragState { return; } if (!mDragResult) { - mAnimator = createReturnAnimationLocked(); - return; // Will call closeLocked() when the animation is done. + if (!isAccessibilityDragDrop()) { + mAnimator = createReturnAnimationLocked(); + return; // Will call closeLocked() when the animation is done. + } } closeLocked(); } @@ -576,7 +581,7 @@ class DragState { if (mAnimator != null) { return; } - if (!mDragInProgress || skipAnimation) { + if (!mDragInProgress || skipAnimation || isAccessibilityDragDrop()) { // mDragInProgress is false if an app invokes Session#cancelDragAndDrop before // Session#performDrag. Reset the drag state without playing the cancel animation // because H.DRAG_START_TIMEOUT may be sent to WindowManagerService, which will cause @@ -721,4 +726,8 @@ class DragState { mDragDropController.sendHandlerMessage(MSG_ANIMATION_END, null); } } + + boolean isAccessibilityDragDrop() { + return (mFlags & View.DRAG_FLAG_ACCESSIBILITY_ACTION) != 0; + } } diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 0b5677724fc35..8c056b2a43b39 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -299,6 +299,17 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { } } + + @Override + public boolean dropForAccessibility(IWindow window, int x, int y) { + final long ident = Binder.clearCallingIdentity(); + try { + return mDragDropController.dropForAccessibility(window, x, y); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + /** * Validates the given drag data. */ diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java index 223dc3121cd6e..a1e8ca4aa84b2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java @@ -58,6 +58,7 @@ import android.view.SurfaceControl; import android.view.SurfaceSession; import android.view.View; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; @@ -70,6 +71,7 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import java.util.ArrayList; import java.util.concurrent.CountDownLatch; @@ -97,14 +99,20 @@ public class DragDropControllerTests extends WindowTestsBase { static class TestDragDropController extends DragDropController { private Runnable mCloseCallback; boolean mDeferDragStateClosed; + boolean mIsAccessibilityDrag; TestDragDropController(WindowManagerService service, Looper looper) { super(service, looper); } void setOnClosedCallbackLocked(Runnable runnable) { - assertTrue(dragDropActiveLocked()); - mCloseCallback = runnable; + if (mIsAccessibilityDrag) { + // Accessibility does not use animation + assertTrue(!dragDropActiveLocked()); + } else { + assertTrue(dragDropActiveLocked()); + mCloseCallback = runnable; + } } @Override @@ -171,6 +179,10 @@ public class DragDropControllerTests extends WindowTestsBase { } latch = new CountDownLatch(1); mTarget.setOnClosedCallbackLocked(latch::countDown); + if (mTarget.mIsAccessibilityDrag) { + mTarget.mIsAccessibilityDrag = false; + return; + } assertTrue(awaitInWmLock(() -> latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS))); } @@ -179,6 +191,12 @@ public class DragDropControllerTests extends WindowTestsBase { doDragAndDrop(0, ClipData.newPlainText("label", "Test"), 0, 0); } + @Test + public void testA11yDragFlow() { + mTarget.mIsAccessibilityDrag = true; + doA11yDragAndDrop(0, ClipData.newPlainText("label", "Test"), 0, 0); + } + @Test public void testPerformDrag_NullDataWithGrantUri() { doDragAndDrop(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, null, 0, 0); @@ -436,4 +454,22 @@ public class DragDropControllerTests extends WindowTestsBase { appSession.kill(); } } + + private void doA11yDragAndDrop(int flags, ClipData data, float dropX, float dropY) { + spyOn(mTarget); + AccessibilityManager accessibilityManager = Mockito.mock(AccessibilityManager.class); + when(accessibilityManager.isEnabled()).thenReturn(true); + doReturn(accessibilityManager).when(mTarget).getAccessibilityManager(); + startA11yDrag(flags, data, () -> { + boolean dropped = mTarget.dropForAccessibility(mWindow.mClient, dropX, dropY); + mToken = mWindow.mClient.asBinder(); + }); + } + + private void startA11yDrag(int flags, ClipData data, Runnable r) { + mToken = mTarget.performDrag(0, 0, mWindow.mClient, + flags | View.DRAG_FLAG_ACCESSIBILITY_ACTION, null, 0, 0, 0, 0, 0, data); + assertNotNull(mToken); + r.run(); + } }