diff options
| author | 2021-08-24 09:10:18 -0700 | |
|---|---|---|
| committer | 2021-09-17 11:26:18 -0700 | |
| commit | f163deb788a78f5346c45ceab322f314789f2193 (patch) | |
| tree | 8e151fa3e71a1804b288d8b1a5951cfccc20b0fb | |
| parent | cd9481d29abd982eb21a9e681a140685a66394ab (diff) | |
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
| -rw-r--r-- | core/java/android/view/IWindowSession.aidl | 5 | ||||
| -rw-r--r-- | core/java/android/view/View.java | 137 | ||||
| -rw-r--r-- | core/java/android/view/ViewParent.java | 3 | ||||
| -rw-r--r-- | core/java/android/view/ViewRootImpl.java | 23 | ||||
| -rw-r--r-- | core/java/android/view/WindowlessWindowManager.java | 5 | ||||
| -rw-r--r-- | services/core/java/com/android/server/wm/DragDropController.java | 101 | ||||
| -rw-r--r-- | services/core/java/com/android/server/wm/DragState.java | 27 | ||||
| -rw-r--r-- | services/core/java/com/android/server/wm/Session.java | 11 | ||||
| -rw-r--r-- | services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java | 40 |
9 files changed, 303 insertions, 49 deletions
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index a6abed020959..9da50889e43f 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -175,6 +175,11 @@ interface IWindowSession { 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, * 'false' otherwise. diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 1ecdb900316d..afdb755179a6 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. * <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. diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index 775c15e77d5d..49f5229d3c09 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -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( diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 4c36fc939c34..d53286885f85 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 c413a9ba7a27..ffb7efa67243 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 d12d07ab7448..cc6a8807bb9d 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; + mDragState.mData = data; - 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; - } + 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 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"); - - 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"); + final SurfaceControl surfaceControl = mDragState.mSurfaceControl; + 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"); + } + + 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 18ea738b08ce..e92de67377cd 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 0b5677724fc3..8c056b2a43b3 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 223dc3121cd6..a1e8ca4aa84b 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))); } @@ -180,6 +192,12 @@ public class DragDropControllerTests extends WindowTestsBase { } @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(); + } } |