diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 70689bdfb117f..f74b5995c1294 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -5058,6 +5058,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public static final int DRAG_FLAG_ACCESSIBILITY_ACTION = 1 << 10; + /** + * Flag indicating that the caller desires to take ownership of the drag surface for handling + * the animation associated with an unhandled drag. It is mainly useful if the view starting + * a global drag changes visibility during the gesture and the default animation of animating + * the surface back to the origin is not sufficient. + * + * The calling app must hold the {@link android.Manifest.permission#START_TASKS_FROM_RECENTS} + * permission and will receive the drag surface as a part of + * {@link action.view.DragEvent#ACTION_DRAG_ENDED} only if the drag event's + * {@link action.view.DragEvent#getDragResult()} is {@code false}. The caller is responsible + * for removing the surface after its animation. + * + * This flag has no effect if the system decides that a cancel-drag animation does not need to + * occur. + * @hide + */ + public static final int DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION = 1 << 11; + /** * Vertical scroll factor cached by {@link #getVerticalScrollFactor}. */ diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java index 17a11dca003be..e0887e1f19706 100644 --- a/services/core/java/com/android/server/wm/DragDropController.java +++ b/services/core/java/com/android/server/wm/DragDropController.java @@ -81,8 +81,8 @@ class DragDropController { return mDragState != null && !mDragState.isClosing(); } - boolean dragSurfaceRelinquished() { - return mDragState != null && mDragState.mRelinquishDragSurface; + boolean dragSurfaceRelinquishedToDropTarget() { + return mDragState != null && mDragState.mRelinquishDragSurfaceToDropTarget; } void registerCallback(IDragDropCallback callback) { @@ -245,7 +245,7 @@ class DragDropController { } mDragState.mDragResult = consumed; - mDragState.mRelinquishDragSurface = consumed + mDragState.mRelinquishDragSurfaceToDropTarget = consumed && mDragState.targetInterceptsGlobalDrag(callingWin); mDragState.endDragLocked(); } @@ -419,7 +419,7 @@ class DragDropController { synchronized (mService.mGlobalLock) { if (mDragState == null) { Slog.wtf(TAG_WM, "mDragState unexpectedly became null while " + - "plyaing animation"); + "playing animation"); return; } mDragState.closeLocked(); diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java index 4fc123d3f68a5..00380bb6e84e1 100644 --- a/services/core/java/com/android/server/wm/DragState.java +++ b/services/core/java/com/android/server/wm/DragState.java @@ -106,7 +106,7 @@ class DragState { ClipDescription mDataDescription; int mTouchSource; boolean mDragResult; - boolean mRelinquishDragSurface; + boolean mRelinquishDragSurfaceToDropTarget; float mOriginalAlpha; float mOriginalX, mOriginalY; float mCurrentX, mCurrentY; @@ -214,13 +214,19 @@ class DragState { for (WindowState ws : mNotifiedWindows) { float x = 0; float y = 0; + SurfaceControl dragSurface = null; if (!mDragResult && (ws.mSession.mPid == mPid)) { // Report unconsumed drop location back to the app that started the drag. x = mCurrentX; y = mCurrentY; + if (relinquishDragSurfaceToDragSource()) { + // If requested (and allowed), report the drag surface back to the app + // starting the drag to handle the return animation + dragSurface = mSurfaceControl; + } } DragEvent event = DragEvent.obtain(DragEvent.ACTION_DRAG_ENDED, - x, y, mThumbOffsetX, mThumbOffsetY, null, null, null, null, null, + x, y, mThumbOffsetX, mThumbOffsetY, null, null, null, dragSurface, null, mDragResult); try { ws.mClient.dispatchDragEvent(event); @@ -249,7 +255,7 @@ class DragState { mInputSurface = null; } if (mSurfaceControl != null) { - if (!mRelinquishDragSurface) { + if (!mRelinquishDragSurfaceToDropTarget && !relinquishDragSurfaceToDragSource()) { mTransaction.reparent(mSurfaceControl, null).apply(); } else { mDragDropController.sendTimeoutMessage(MSG_REMOVE_DRAG_SURFACE_TIMEOUT, @@ -570,7 +576,7 @@ class DragState { return; } if (!mDragResult) { - if (!isAccessibilityDragDrop()) { + if (!isAccessibilityDragDrop() && !relinquishDragSurfaceToDragSource()) { mAnimator = createReturnAnimationLocked(); return; // Will call closeLocked() when the animation is done. } @@ -731,4 +737,8 @@ class DragState { boolean isAccessibilityDragDrop() { return (mFlags & View.DRAG_FLAG_ACCESSIBILITY_ACTION) != 0; } + + private boolean relinquishDragSurfaceToDragSource() { + return (mFlags & View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION) != 0; + } } diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index f26c539380774..7c5144e3892a5 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -71,6 +71,7 @@ import android.view.InsetsState; import android.view.InsetsVisibilities; import android.view.SurfaceControl; import android.view.SurfaceSession; +import android.view.View; import android.view.WindowManager; import android.window.ClientWindowFrames; import android.window.IOnBackInvokedCallback; @@ -293,9 +294,11 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { @Override public IBinder performDrag(IWindow window, int flags, SurfaceControl surface, int touchSource, float touchX, float touchY, float thumbCenterX, float thumbCenterY, ClipData data) { + final int callingUid = Binder.getCallingUid(); + final int callingPid = Binder.getCallingPid(); // Validate and resolve ClipDescription data before clearing the calling identity - validateAndResolveDragMimeTypeExtras(data, Binder.getCallingUid(), Binder.getCallingPid(), - mPackageName); + validateAndResolveDragMimeTypeExtras(data, callingUid, callingPid, mPackageName); + validateDragFlags(flags, callingUid); final long ident = Binder.clearCallingIdentity(); try { return mDragDropController.performDrag(mPid, mUid, window, flags, surface, touchSource, @@ -316,6 +319,22 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { } } + /** + * Validates the given drag flags. + */ + @VisibleForTesting + void validateDragFlags(int flags, int callingUid) { + if (callingUid == Process.SYSTEM_UID) { + throw new IllegalStateException("Need to validate before calling identify is cleared"); + } + + if ((flags & View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION) != 0) { + if (!mCanStartTasksFromRecents) { + throw new SecurityException("Requires START_TASKS_FROM_RECENTS permission"); + } + } + } + /** * 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 32cca47b991c0..28fc352e2f741 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java @@ -21,6 +21,7 @@ import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY; import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.view.DragEvent.ACTION_DRAG_ENDED; import static android.view.DragEvent.ACTION_DRAG_STARTED; import static android.view.DragEvent.ACTION_DROP; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP; @@ -145,7 +146,9 @@ public class DragDropControllerTests extends WindowTestsBase { final WindowState window = createWindow( null, TYPE_BASE_APPLICATION, activity, name, ownerId, false, new TestIWindow()); window.mInputChannel = new InputChannel(); + window.mInputChannelToken = window.mInputChannel.getToken(); window.mHasSurface = true; + mWm.mWindowMap.put(window.mClient.asBinder(), window); mWm.mInputToWindowMap.put(window.mInputChannelToken, window); return window; } @@ -259,7 +262,7 @@ public class DragDropControllerTests extends WindowTestsBase { } finally { mTarget.mDeferDragStateClosed = false; } - assertTrue(mTarget.dragSurfaceRelinquished()); + assertTrue(mTarget.dragSurfaceRelinquishedToDropTarget()); }); } @@ -457,6 +460,100 @@ public class DragDropControllerTests extends WindowTestsBase { } } + @Test + public void testValidateFlags() { + final Session session = new Session(mWm, new IWindowSessionCallback.Stub() { + @Override + public void onAnimatorScaleChanged(float scale) {} + }); + try { + session.validateDragFlags(View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION, + TEST_UID); + fail("Expected failure without permission"); + } catch (SecurityException e) { + // Expected failure + } + } + + @Test + public void testValidateFlagsWithPermission() { + doReturn(PERMISSION_GRANTED).when(mWm.mContext) + .checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS)); + final Session session = new Session(mWm, new IWindowSessionCallback.Stub() { + @Override + public void onAnimatorScaleChanged(float scale) {} + }); + try { + session.validateDragFlags(View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION, + TEST_UID); + // Expected pass + } catch (SecurityException e) { + fail("Expected no failure with permission"); + } + } + + @Test + public void testRequestSurfaceForReturnAnimationFlag_dropSuccessful() { + WindowState otherWindow = createDropTargetWindow("App drag test window", 0); + TestIWindow otherIWindow = (TestIWindow) otherWindow.mClient; + + // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events + // immediately after dispatching, which is a problem when using mockito arguments captor + // because it returns and modifies the same drag event + TestIWindow iwindow = (TestIWindow) mWindow.mClient; + final ArrayList dragEvents = new ArrayList<>(); + iwindow.setDragEventJournal(dragEvents); + + startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ + | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION, + ClipData.newPlainText("label", "text"), () -> { + assertTrue(dragEvents.get(0).getAction() == ACTION_DRAG_STARTED); + + // Verify after consuming that the drag surface is relinquished + mTarget.reportDropWindow(otherWindow.mInputChannelToken, 0, 0); + mTarget.handleMotionEvent(false, 0, 0); + mToken = otherWindow.mClient.asBinder(); + mTarget.reportDropResult(otherIWindow, true); + + // Verify the DRAG_ENDED event does NOT include the drag surface + final DragEvent dropEvent = dragEvents.get(dragEvents.size() - 1); + assertTrue(dragEvents.get(dragEvents.size() - 1).getAction() + == ACTION_DRAG_ENDED); + assertTrue(dropEvent.getDragSurface() == null); + }); + } + + @Test + public void testRequestSurfaceForReturnAnimationFlag_dropUnsuccessful() { + WindowState otherWindow = createDropTargetWindow("App drag test window", 0); + TestIWindow otherIWindow = (TestIWindow) otherWindow.mClient; + + // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events + // immediately after dispatching, which is a problem when using mockito arguments captor + // because it returns and modifies the same drag event + TestIWindow iwindow = (TestIWindow) mWindow.mClient; + final ArrayList dragEvents = new ArrayList<>(); + iwindow.setDragEventJournal(dragEvents); + + startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ + | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION, + ClipData.newPlainText("label", "text"), () -> { + assertTrue(dragEvents.get(0).getAction() == ACTION_DRAG_STARTED); + + // Verify after consuming that the drag surface is relinquished + mTarget.reportDropWindow(otherWindow.mInputChannelToken, 0, 0); + mTarget.handleMotionEvent(false, 0, 0); + mToken = otherWindow.mClient.asBinder(); + mTarget.reportDropResult(otherIWindow, false); + + // Verify the DRAG_ENDED event includes the drag surface + final DragEvent dropEvent = dragEvents.get(dragEvents.size() - 1); + assertTrue(dragEvents.get(dragEvents.size() - 1).getAction() + == ACTION_DRAG_ENDED); + assertTrue(dropEvent.getDragSurface() != null); + }); + } + private void doDragAndDrop(int flags, ClipData data, float dropX, float dropY) { startDrag(flags, data, () -> { mTarget.reportDropWindow(mWindow.mInputChannelToken, dropX, dropY);