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:
@@ -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}.
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user