Loading core/java/android/view/View.java +18 −0 Original line number Diff line number Diff line Loading @@ -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}. */ services/core/java/com/android/server/wm/DragDropController.java +4 −4 Original line number Diff line number Diff line Loading @@ -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) { Loading Loading @@ -245,7 +245,7 @@ class DragDropController { } mDragState.mDragResult = consumed; mDragState.mRelinquishDragSurface = consumed mDragState.mRelinquishDragSurfaceToDropTarget = consumed && mDragState.targetInterceptsGlobalDrag(callingWin); mDragState.endDragLocked(); } Loading Loading @@ -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(); Loading services/core/java/com/android/server/wm/DragState.java +14 −4 Original line number Diff line number Diff line Loading @@ -106,7 +106,7 @@ class DragState { ClipDescription mDataDescription; int mTouchSource; boolean mDragResult; boolean mRelinquishDragSurface; boolean mRelinquishDragSurfaceToDropTarget; float mOriginalAlpha; float mOriginalX, mOriginalY; float mCurrentX, mCurrentY; Loading Loading @@ -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); Loading Loading @@ -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, Loading Loading @@ -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. } Loading Loading @@ -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; } } services/core/java/com/android/server/wm/Session.java +21 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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, Loading @@ -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. */ Loading services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java +98 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; } Loading Loading @@ -259,7 +262,7 @@ public class DragDropControllerTests extends WindowTestsBase { } finally { mTarget.mDeferDragStateClosed = false; } assertTrue(mTarget.dragSurfaceRelinquished()); assertTrue(mTarget.dragSurfaceRelinquishedToDropTarget()); }); } Loading Loading @@ -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); Loading Loading
core/java/android/view/View.java +18 −0 Original line number Diff line number Diff line Loading @@ -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}. */
services/core/java/com/android/server/wm/DragDropController.java +4 −4 Original line number Diff line number Diff line Loading @@ -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) { Loading Loading @@ -245,7 +245,7 @@ class DragDropController { } mDragState.mDragResult = consumed; mDragState.mRelinquishDragSurface = consumed mDragState.mRelinquishDragSurfaceToDropTarget = consumed && mDragState.targetInterceptsGlobalDrag(callingWin); mDragState.endDragLocked(); } Loading Loading @@ -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(); Loading
services/core/java/com/android/server/wm/DragState.java +14 −4 Original line number Diff line number Diff line Loading @@ -106,7 +106,7 @@ class DragState { ClipDescription mDataDescription; int mTouchSource; boolean mDragResult; boolean mRelinquishDragSurface; boolean mRelinquishDragSurfaceToDropTarget; float mOriginalAlpha; float mOriginalX, mOriginalY; float mCurrentX, mCurrentY; Loading Loading @@ -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); Loading Loading @@ -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, Loading Loading @@ -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. } Loading Loading @@ -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; } }
services/core/java/com/android/server/wm/Session.java +21 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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, Loading @@ -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. */ Loading
services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java +98 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; } Loading Loading @@ -259,7 +262,7 @@ public class DragDropControllerTests extends WindowTestsBase { } finally { mTarget.mDeferDragStateClosed = false; } assertTrue(mTarget.dragSurfaceRelinquished()); assertTrue(mTarget.dragSurfaceRelinquishedToDropTarget()); }); } Loading Loading @@ -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); Loading