Merge "Add hidden flag for SysUI to control the return drag animation"

This commit is contained in:
Winson Chung
2022-02-08 00:33:18 +00:00
committed by Android (Google) Code Review
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);