Add hidden flag for SysUI to control the return drag animation

- When a drag is not dropped or the drop is not consumed, the system will
  play a canned animation to the origin of the drag. However, there are
  scenarios where this is not sufficient (ie. dragging from a folder or
  all apps in the taskbar, which are closed once global drag starts). As
  a result, the return animation will return to the wrong location.

  The CL adds a hidden flag DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION
  which only SysUI can request (protected by START_TASKS_FROM_RECENTS)
  which indicates to the system that when a return animation is to be
  played, it should defer to the calling app to animate the surface
  instead.

  The surface will be returned with the DRAG_ENDED event only if the
  result is false (a return animation), to ensure that it does not
  collide with the DROP animation by the Shell.  The system will
  also continue to schedule a timeout to clear the surface if the
  caller does not handle it correctly.

Bug: 179390870
Test: atest DragDropControllerTests
Change-Id: I65d749f9fba186a0e43e155bebb136926642a90a
This commit is contained in:
Winson Chung
2022-02-05 04:35:49 +00:00
parent e1b353167b
commit 22e0d342be
5 changed files with 155 additions and 11 deletions

View File

@@ -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}.
*/

View File

@@ -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();

View File

@@ -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;
}
}

View File

@@ -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.
*/

View File

@@ -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<DragEvent> 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<DragEvent> 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);