Merge "Add support for delegating unhandled drags to SystemUI" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index 302ee5a..91a2f68 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -10045,11 +10045,22 @@
method public CharSequence coerceToText(android.content.Context);
method public String getHtmlText();
method public android.content.Intent getIntent();
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @Nullable public android.app.PendingIntent getPendingIntent();
method public CharSequence getText();
method @Nullable public android.view.textclassifier.TextLinks getTextLinks();
method public android.net.Uri getUri();
}
+ @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final class ClipData.Item.Builder {
+ ctor public ClipData.Item.Builder();
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item build();
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setHtmlText(@Nullable String);
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setIntent(@Nullable android.content.Intent);
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setPendingIntent(@Nullable android.app.PendingIntent);
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setText(@Nullable CharSequence);
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setUri(@Nullable android.net.Uri);
+ }
+
public class ClipDescription implements android.os.Parcelable {
ctor public ClipDescription(CharSequence, String[]);
ctor public ClipDescription(android.content.ClipDescription);
@@ -52897,9 +52908,11 @@
field public static final int DRAG_FLAG_GLOBAL = 256; // 0x100
field public static final int DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION = 64; // 0x40
field public static final int DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION = 128; // 0x80
+ field @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final int DRAG_FLAG_GLOBAL_SAME_APPLICATION = 4096; // 0x1000
field public static final int DRAG_FLAG_GLOBAL_URI_READ = 1; // 0x1
field public static final int DRAG_FLAG_GLOBAL_URI_WRITE = 2; // 0x2
field public static final int DRAG_FLAG_OPAQUE = 512; // 0x200
+ field @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final int DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG = 8192; // 0x2000
field @Deprecated public static final int DRAWING_CACHE_QUALITY_AUTO = 0; // 0x0
field @Deprecated public static final int DRAWING_CACHE_QUALITY_HIGH = 1048576; // 0x100000
field @Deprecated public static final int DRAWING_CACHE_QUALITY_LOW = 524288; // 0x80000
diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java
index 67759f4..eb357fe 100644
--- a/core/java/android/content/ClipData.java
+++ b/core/java/android/content/ClipData.java
@@ -21,7 +21,13 @@
import static android.content.ContentResolver.SCHEME_CONTENT;
import static android.content.ContentResolver.SCHEME_FILE;
+import static com.android.window.flags.Flags.FLAG_DELEGATE_UNHANDLED_DRAGS;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.app.PendingIntent;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.pm.ActivityInfo;
import android.content.res.AssetFileDescriptor;
@@ -207,6 +213,7 @@
final CharSequence mText;
final String mHtmlText;
final Intent mIntent;
+ final PendingIntent mPendingIntent;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
Uri mUri;
private TextLinks mTextLinks;
@@ -214,12 +221,91 @@
// if the data is obtained from {@link #copyForTransferWithActivityInfo}
private ActivityInfo mActivityInfo;
+ /**
+ * A builder for a ClipData Item.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @SuppressLint("PackageLayering")
+ public static final class Builder {
+ private CharSequence mText;
+ private String mHtmlText;
+ private Intent mIntent;
+ private PendingIntent mPendingIntent;
+ private Uri mUri;
+
+ /**
+ * Sets the text for the item to be constructed.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Builder setText(@Nullable CharSequence text) {
+ mText = text;
+ return this;
+ }
+
+ /**
+ * Sets the HTML text for the item to be constructed.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Builder setHtmlText(@Nullable String htmlText) {
+ mHtmlText = htmlText;
+ return this;
+ }
+
+ /**
+ * Sets the Intent for the item to be constructed.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Builder setIntent(@Nullable Intent intent) {
+ mIntent = intent;
+ return this;
+ }
+
+ /**
+ * Sets the PendingIntent for the item to be constructed. To prevent receiving apps from
+ * improperly manipulating the intent to launch another activity as this caller, the
+ * provided PendingIntent must be immutable (see {@link PendingIntent#FLAG_IMMUTABLE}).
+ * The system will clean up the PendingIntent when it is no longer used.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Builder setPendingIntent(@Nullable PendingIntent pendingIntent) {
+ if (pendingIntent != null && !pendingIntent.isImmutable()) {
+ throw new IllegalArgumentException("Expected pending intent to be immutable");
+ }
+ mPendingIntent = pendingIntent;
+ return this;
+ }
+
+ /**
+ * Sets the URI for the item to be constructed.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Builder setUri(@Nullable Uri uri) {
+ mUri = uri;
+ return this;
+ }
+
+ /**
+ * Constructs a new Item with the properties set on this builder.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Item build() {
+ return new Item(mText, mHtmlText, mIntent, mPendingIntent, mUri);
+ }
+ }
+
/** @hide */
public Item(Item other) {
mText = other.mText;
mHtmlText = other.mHtmlText;
mIntent = other.mIntent;
+ mPendingIntent = other.mPendingIntent;
mUri = other.mUri;
mActivityInfo = other.mActivityInfo;
mTextLinks = other.mTextLinks;
@@ -229,10 +315,7 @@
* Create an Item consisting of a single block of (possibly styled) text.
*/
public Item(CharSequence text) {
- mText = text;
- mHtmlText = null;
- mIntent = null;
- mUri = null;
+ this(text, null, null, null, null);
}
/**
@@ -245,30 +328,21 @@
* </p>
*/
public Item(CharSequence text, String htmlText) {
- mText = text;
- mHtmlText = htmlText;
- mIntent = null;
- mUri = null;
+ this(text, htmlText, null, null, null);
}
/**
* Create an Item consisting of an arbitrary Intent.
*/
public Item(Intent intent) {
- mText = null;
- mHtmlText = null;
- mIntent = intent;
- mUri = null;
+ this(null, null, intent, null, null);
}
/**
* Create an Item consisting of an arbitrary URI.
*/
public Item(Uri uri) {
- mText = null;
- mHtmlText = null;
- mIntent = null;
- mUri = uri;
+ this(null, null, null, null, uri);
}
/**
@@ -276,10 +350,7 @@
* text, Intent, and/or URI.
*/
public Item(CharSequence text, Intent intent, Uri uri) {
- mText = text;
- mHtmlText = null;
- mIntent = intent;
- mUri = uri;
+ this(text, null, intent, null, uri);
}
/**
@@ -289,6 +360,14 @@
* will not be done from HTML formatted text into plain text.
*/
public Item(CharSequence text, String htmlText, Intent intent, Uri uri) {
+ this(text, htmlText, intent, null, uri);
+ }
+
+ /**
+ * Builder ctor.
+ */
+ private Item(CharSequence text, String htmlText, Intent intent, PendingIntent pendingIntent,
+ Uri uri) {
if (htmlText != null && text == null) {
throw new IllegalArgumentException(
"Plain text must be supplied if HTML text is supplied");
@@ -296,6 +375,7 @@
mText = text;
mHtmlText = htmlText;
mIntent = intent;
+ mPendingIntent = pendingIntent;
mUri = uri;
}
@@ -321,6 +401,15 @@
}
/**
+ * Returns the pending intent in this Item.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @Nullable
+ public PendingIntent getPendingIntent() {
+ return mPendingIntent;
+ }
+
+ /**
* Retrieve the raw URI contained in this Item.
*/
public Uri getUri() {
@@ -777,7 +866,7 @@
throw new NullPointerException("item is null");
}
mIcon = null;
- mItems = new ArrayList<Item>();
+ mItems = new ArrayList<>();
mItems.add(item);
mClipDescription.setIsStyledText(isStyledText());
}
@@ -794,7 +883,7 @@
throw new NullPointerException("item is null");
}
mIcon = null;
- mItems = new ArrayList<Item>();
+ mItems = new ArrayList<>();
mItems.add(item);
mClipDescription.setIsStyledText(isStyledText());
}
@@ -826,7 +915,7 @@
public ClipData(ClipData other) {
mClipDescription = other.mClipDescription;
mIcon = other.mIcon;
- mItems = new ArrayList<Item>(other.mItems);
+ mItems = new ArrayList<>(other.mItems);
}
/**
@@ -1042,6 +1131,35 @@
}
/**
+ * Checks if this clip data has a pending intent that is an activity type.
+ * @hide
+ */
+ public boolean hasActivityPendingIntents() {
+ final int size = mItems.size();
+ for (int i = 0; i < size; i++) {
+ final Item item = mItems.get(i);
+ if (item.mPendingIntent != null && item.mPendingIntent.isActivity()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Cleans up all pending intents in the ClipData.
+ * @hide
+ */
+ public void cleanUpPendingIntents() {
+ final int size = mItems.size();
+ for (int i = 0; i < size; i++) {
+ final Item item = mItems.get(i);
+ if (item.mPendingIntent != null) {
+ item.mPendingIntent.cancel();
+ }
+ }
+ }
+
+ /**
* Prepare this {@link ClipData} to leave an app process.
*
* @hide
@@ -1243,6 +1361,7 @@
TextUtils.writeToParcel(item.mText, dest, flags);
dest.writeString8(item.mHtmlText);
dest.writeTypedObject(item.mIntent, flags);
+ dest.writeTypedObject(item.mPendingIntent, flags);
dest.writeTypedObject(item.mUri, flags);
dest.writeTypedObject(mParcelItemActivityInfos ? item.mActivityInfo : null, flags);
dest.writeTypedObject(item.mTextLinks, flags);
@@ -1262,10 +1381,11 @@
CharSequence text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
String htmlText = in.readString8();
Intent intent = in.readTypedObject(Intent.CREATOR);
+ PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR);
Uri uri = in.readTypedObject(Uri.CREATOR);
ActivityInfo info = in.readTypedObject(ActivityInfo.CREATOR);
TextLinks textLinks = in.readTypedObject(TextLinks.CREATOR);
- Item item = new Item(text, htmlText, intent, uri);
+ Item item = new Item(text, htmlText, intent, pendingIntent, uri);
item.setActivityInfo(info);
item.setTextLinks(textLinks);
mItems.add(item);
@@ -1273,7 +1393,7 @@
}
public static final @android.annotation.NonNull Parcelable.Creator<ClipData> CREATOR =
- new Parcelable.Creator<ClipData>() {
+ new Parcelable.Creator<>() {
@Override
public ClipData createFromParcel(Parcel source) {
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index b5b81d1..29cc859 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -73,6 +73,7 @@
import android.window.ISurfaceSyncGroupCompletedListener;
import android.window.ITaskFpsCallback;
import android.window.ITrustedPresentationListener;
+import android.window.IUnhandledDragListener;
import android.window.InputTransferToken;
import android.window.ScreenCapture;
import android.window.TrustedPresentationThresholds;
@@ -1091,4 +1092,10 @@
@EnforcePermission("DETECT_SCREEN_RECORDING")
void unregisterScreenRecordingCallback(IScreenRecordingCallback callback);
+
+ /**
+ * Sets the listener to be called back when a cross-window drag and drop operation is unhandled
+ * (ie. not handled by any window which can handle the drag).
+ */
+ void setUnhandledDragListener(IUnhandledDragListener listener);
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index c22986b..e781f3c 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -43,6 +43,7 @@
import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS;
import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP;
import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION;
+import static com.android.window.flags.Flags.FLAG_DELEGATE_UNHANDLED_DRAGS;
import static java.lang.Math.max;
@@ -68,6 +69,7 @@
import android.annotation.TestApi;
import android.annotation.UiContext;
import android.annotation.UiThread;
+import android.app.PendingIntent;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.AutofillOptions;
import android.content.ClipData;
@@ -5329,6 +5331,34 @@
public static final int DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION = 1 << 11;
/**
+ * Flag indicating that a drag can cross window boundaries (within the same application). When
+ * {@link #startDragAndDrop(ClipData, DragShadowBuilder, Object, int)} is called
+ * with this flag set, only visible windows belonging to the same application (ie. share the
+ * same UID) with targetSdkVersion >= {@link android.os.Build.VERSION_CODES#N API 24} will be
+ * able to participate in the drag operation and receive the dragged content.
+ *
+ * If both DRAG_FLAG_GLOBAL_SAME_APPLICATION and DRAG_FLAG_GLOBAL are set, then
+ * DRAG_FLAG_GLOBAL_SAME_APPLICATION takes precedence and the drag will only go to visible
+ * windows from the same application.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ public static final int DRAG_FLAG_GLOBAL_SAME_APPLICATION = 1 << 12;
+
+ /**
+ * Flag indicating that an unhandled drag should be delegated to the system to be started if no
+ * visible window wishes to handle the drop. When using this flag, the caller must provide
+ * ClipData with an Item that contains an immutable PendingIntent to an activity to be launched
+ * (not a broadcast, service, etc). See
+ * {@link ClipData.Item.Builder#setPendingIntent(PendingIntent)}.
+ *
+ * The system can decide to launch the intent or not based on factors like the current screen
+ * size or windowing mode. If the system does not launch the intent, it will be canceled via the
+ * normal drag and drop flow.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ public static final int DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG = 1 << 13;
+
+ /**
* Vertical scroll factor cached by {@link #getVerticalScrollFactor}.
*/
private float mVerticalScrollFactor;
@@ -28496,9 +28526,29 @@
Log.w(VIEW_LOG_TAG, "startDragAndDrop called with an invalid surface.");
return false;
}
+ if ((flags & DRAG_FLAG_GLOBAL) != 0 && ((flags & DRAG_FLAG_GLOBAL_SAME_APPLICATION) != 0)) {
+ Log.w(VIEW_LOG_TAG, "startDragAndDrop called with both DRAG_FLAG_GLOBAL "
+ + "and DRAG_FLAG_GLOBAL_SAME_APPLICATION, the drag will default to "
+ + "DRAG_FLAG_GLOBAL_SAME_APPLICATION");
+ flags &= ~DRAG_FLAG_GLOBAL;
+ }
if (data != null) {
- data.prepareToLeaveProcess((flags & View.DRAG_FLAG_GLOBAL) != 0);
+ if (com.android.window.flags.Flags.delegateUnhandledDrags()) {
+ data.prepareToLeaveProcess(
+ (flags & (DRAG_FLAG_GLOBAL_SAME_APPLICATION | DRAG_FLAG_GLOBAL)) != 0);
+ if ((flags & DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG) != 0) {
+ if (!data.hasActivityPendingIntents()) {
+ // Reset the flag if there is no launchable activity intent
+ flags &= ~DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG;
+ Log.w(VIEW_LOG_TAG, "startDragAndDrop called with "
+ + "DRAG_FLAG_START_INTENT_ON_UNHANDLED_DRAG but the clip data "
+ + "contains non-activity PendingIntents");
+ }
+ }
+ } else {
+ data.prepareToLeaveProcess((flags & DRAG_FLAG_GLOBAL) != 0);
+ }
}
Rect bounds = new Rect();
@@ -28524,6 +28574,7 @@
if (token != null) {
root.setLocalDragState(myLocalState);
mAttachInfo.mDragToken = token;
+ mAttachInfo.mDragData = data;
mAttachInfo.mViewRootImpl.setDragStartedViewForAccessibility(this);
setAccessibilityDragStarted(true);
}
@@ -28601,8 +28652,12 @@
if (mAttachInfo.mDragSurface != null) {
mAttachInfo.mDragSurface.release();
}
+ if (mAttachInfo.mDragData != null) {
+ mAttachInfo.mDragData.cleanUpPendingIntents();
+ }
mAttachInfo.mDragSurface = surface;
mAttachInfo.mDragToken = token;
+ mAttachInfo.mDragData = data;
// Cache the local state object for delivery with DragEvents
root.setLocalDragState(myLocalState);
if (a11yEnabled) {
@@ -31516,11 +31571,15 @@
IBinder mDragToken;
/**
+ * Used to track the data of the current drag operation for cleanup later.
+ */
+ ClipData mDragData;
+
+ /**
* The drag shadow surface for the current drag operation.
*/
public Surface mDragSurface;
-
/**
* The view that currently has a tooltip displayed.
*/
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 07c9795..28a7334 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -8599,6 +8599,10 @@
mAttachInfo.mDragSurface.release();
mAttachInfo.mDragSurface = null;
}
+ if (mAttachInfo.mDragData != null) {
+ mAttachInfo.mDragData.cleanUpPendingIntents();
+ mAttachInfo.mDragData = null;
+ }
}
}
}
diff --git a/core/java/android/window/IUnhandledDragCallback.aidl b/core/java/android/window/IUnhandledDragCallback.aidl
new file mode 100644
index 0000000..7806b1f
--- /dev/null
+++ b/core/java/android/window/IUnhandledDragCallback.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.view.DragEvent;
+
+/**
+ * A callback for notifying the system when the unhandled drop is complete.
+ * {@hide}
+ */
+oneway interface IUnhandledDragCallback {
+ /**
+ * Called when the IUnhandledDropListener has fully handled the drop, and the drag can be
+ * cleaned up. If handled is `true`, then cleanup of the drag and drag surface will be
+ * immediate, otherwise, the system will treat the drag as a cancel back to the start of the
+ * drag.
+ */
+ void notifyUnhandledDropComplete(boolean handled);
+}
diff --git a/core/java/android/window/IUnhandledDragListener.aidl b/core/java/android/window/IUnhandledDragListener.aidl
new file mode 100644
index 0000000..52e9895
--- /dev/null
+++ b/core/java/android/window/IUnhandledDragListener.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.view.DragEvent;
+import android.window.IUnhandledDragCallback;
+
+/**
+ * An interface to a handler for global drags that are not consumed (ie. not handled by any window).
+ * {@hide}
+ */
+oneway interface IUnhandledDragListener {
+ /**
+ * Called when the user finishes the drag gesture but no windows have reported handling the
+ * drop. The DragEvent is populated with the drag surface for the listener to animate. The
+ * listener *MUST* call the provided callback exactly once when it has finished handling the
+ * drop. If the listener calls the callback with `true` then it is responsible for removing
+ * and releasing the drag surface passed through the DragEvent.
+ */
+ void onUnhandledDrop(in DragEvent event, in IUnhandledDragCallback callback);
+}
diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java
index 6a3cf43..a3e2869 100644
--- a/services/core/java/com/android/server/wm/DragDropController.java
+++ b/services/core/java/com/android/server/wm/DragDropController.java
@@ -16,6 +16,9 @@
package com.android.server.wm;
+import static android.view.View.DRAG_FLAG_GLOBAL;
+import static android.view.View.DRAG_FLAG_GLOBAL_SAME_APPLICATION;
+
import static com.android.input.flags.Flags.enablePointerChoreographer;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DRAG;
import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS;
@@ -30,15 +33,20 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
+import android.os.RemoteException;
import android.util.Slog;
import android.view.Display;
+import android.view.DragEvent;
import android.view.IWindow;
import android.view.InputDevice;
import android.view.PointerIcon;
import android.view.SurfaceControl;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
+import android.window.IUnhandledDragCallback;
+import android.window.IUnhandledDragListener;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wm.WindowManagerInternal.IDragDropCallback;
import java.util.Objects;
@@ -59,6 +67,7 @@
static final int MSG_TEAR_DOWN_DRAG_AND_DROP_INPUT = 1;
static final int MSG_ANIMATION_END = 2;
static final int MSG_REMOVE_DRAG_SURFACE_TIMEOUT = 3;
+ static final int MSG_UNHANDLED_DROP_LISTENER_TIMEOUT = 4;
/**
* Drag state per operation.
@@ -72,6 +81,21 @@
private WindowManagerService mService;
private final Handler mHandler;
+ // The unhandled drag listener for handling cross-window drags that end with no target window
+ private IUnhandledDragListener mUnhandledDragListener;
+ private final IBinder.DeathRecipient mUnhandledDragListenerDeathRecipient =
+ new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ synchronized (mService.mGlobalLock) {
+ if (hasPendingUnhandledDropCallback()) {
+ onUnhandledDropCallback(false /* consumedByListeners */);
+ }
+ setUnhandledDragListener(null);
+ }
+ }
+ };
+
/**
* Callback which is used to sync drag state with the vendor-specific code.
*/
@@ -83,10 +107,16 @@
mHandler = new DragHandler(service, looper);
}
+ @VisibleForTesting
+ Handler getHandler() {
+ return mHandler;
+ }
+
boolean dragDropActiveLocked() {
return mDragState != null && !mDragState.isClosing();
}
+ @VisibleForTesting
boolean dragSurfaceRelinquishedToDropTarget() {
return mDragState != null && mDragState.mRelinquishDragSurfaceToDropTarget;
}
@@ -96,6 +126,32 @@
mCallback.set(callback);
}
+ /**
+ * Sets the listener for unhandled cross-window drags.
+ */
+ public void setUnhandledDragListener(IUnhandledDragListener listener) {
+ if (mUnhandledDragListener != null && mUnhandledDragListener.asBinder() != null) {
+ mUnhandledDragListener.asBinder().unlinkToDeath(
+ mUnhandledDragListenerDeathRecipient, 0);
+ }
+ mUnhandledDragListener = listener;
+ if (listener != null && listener.asBinder() != null) {
+ try {
+ mUnhandledDragListener.asBinder().linkToDeath(
+ mUnhandledDragListenerDeathRecipient, 0);
+ } catch (RemoteException e) {
+ mUnhandledDragListener = null;
+ }
+ }
+ }
+
+ /**
+ * Returns whether there is an unhandled drag listener set.
+ */
+ boolean hasUnhandledDragListener() {
+ return mUnhandledDragListener != null;
+ }
+
void sendDragStartedIfNeededLocked(WindowState window) {
mDragState.sendDragStartedIfNeededLocked(window);
}
@@ -247,6 +303,10 @@
}
}
+ /**
+ * This is called from the drop target window that received ACTION_DROP
+ * (see DragState#reportDropWindowLock()).
+ */
void reportDropResult(IWindow window, boolean consumed) {
IBinder token = window.asBinder();
if (DEBUG_DRAG) {
@@ -273,22 +333,89 @@
// so be sure to halt the timeout even if the later WindowState
// lookup fails.
mHandler.removeMessages(MSG_DRAG_END_TIMEOUT, window.asBinder());
+
WindowState callingWin = mService.windowForClientLocked(null, window, false);
if (callingWin == null) {
Slog.w(TAG_WM, "Bad result-reporting window " + window);
return; // !!! TODO: throw here?
}
- mDragState.mDragResult = consumed;
- mDragState.mRelinquishDragSurfaceToDropTarget = consumed
- && mDragState.targetInterceptsGlobalDrag(callingWin);
- mDragState.endDragLocked();
+ // If the drop was not consumed by the target window, then check if it should be
+ // consumed by the system unhandled drag listener
+ if (!consumed && notifyUnhandledDrop(mDragState.mUnhandledDropEvent,
+ "window-drop")) {
+ // If the unhandled drag listener is notified, then defer ending the drag until
+ // the listener calls back
+ return;
+ }
+
+ final boolean relinquishDragSurfaceToDropTarget =
+ consumed && mDragState.targetInterceptsGlobalDrag(callingWin);
+ mDragState.endDragLocked(consumed, relinquishDragSurfaceToDropTarget);
}
} finally {
mCallback.get().postReportDropResult();
}
}
+ /**
+ * Notifies the unhandled drag listener if needed.
+ * @return whether the listener was notified and subsequent drag completion should be deferred
+ * until the listener calls back
+ */
+ boolean notifyUnhandledDrop(DragEvent dropEvent, String reason) {
+ final boolean isLocalDrag =
+ (mDragState.mFlags & (DRAG_FLAG_GLOBAL_SAME_APPLICATION | DRAG_FLAG_GLOBAL)) == 0;
+ if (!com.android.window.flags.Flags.delegateUnhandledDrags()
+ || mUnhandledDragListener == null
+ || isLocalDrag) {
+ // Skip if the flag is disabled, there is no unhandled-drag listener, or if this is a
+ // purely local drag
+ if (DEBUG_DRAG) Slog.d(TAG_WM, "Skipping unhandled listener "
+ + "(listener=" + mUnhandledDragListener + ", flags=" + mDragState.mFlags + ")");
+ return false;
+ }
+ if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DROP to unhandled listener (" + reason + ")");
+ try {
+ // Schedule timeout for the unhandled drag listener to call back
+ sendTimeoutMessage(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT, null, DRAG_TIMEOUT_MS);
+ mUnhandledDragListener.onUnhandledDrop(dropEvent, new IUnhandledDragCallback.Stub() {
+ @Override
+ public void notifyUnhandledDropComplete(boolean consumedByListener) {
+ if (DEBUG_DRAG) Slog.d(TAG_WM, "Unhandled listener finished handling DROP");
+ synchronized (mService.mGlobalLock) {
+ onUnhandledDropCallback(consumedByListener);
+ }
+ }
+ });
+ return true;
+ } catch (RemoteException e) {
+ Slog.e(TAG_WM, "Failed to call unhandled drag listener", e);
+ return false;
+ }
+ }
+
+ /**
+ * Called when the unhandled drag listener has completed handling the drop
+ * (if it was notififed).
+ */
+ @VisibleForTesting
+ void onUnhandledDropCallback(boolean consumedByListener) {
+ mHandler.removeMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT, null);
+ // If handled, then the listeners assume responsibility of cleaning up the drag surface
+ mDragState.mDragResult = consumedByListener;
+ mDragState.mRelinquishDragSurfaceToDropTarget = consumedByListener;
+ mDragState.closeLocked();
+ }
+
+ /**
+ * Returns whether we are currently waiting for the unhandled drag listener to callback after
+ * it was notified of an unhandled drag.
+ */
+ boolean hasPendingUnhandledDropCallback() {
+ return mHandler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT);
+ }
+
void cancelDragAndDrop(IBinder dragToken, boolean skipAnimation) {
if (DEBUG_DRAG) {
Slog.d(TAG_WM, "cancelDragAndDrop");
@@ -436,8 +563,8 @@
synchronized (mService.mGlobalLock) {
// !!! TODO: ANR the drag-receiving app
if (mDragState != null) {
- mDragState.mDragResult = false;
- mDragState.endDragLocked();
+ mDragState.endDragLocked(false /* consumed */,
+ false /* relinquishDragSurfaceToDropTarget */);
}
}
break;
@@ -473,6 +600,13 @@
}
break;
}
+
+ case MSG_UNHANDLED_DROP_LISTENER_TIMEOUT: {
+ synchronized (mService.mGlobalLock) {
+ onUnhandledDropCallback(false /* consumedByListener */);
+ }
+ break;
+ }
}
}
}
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index d302f06..76038b9 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -147,6 +147,11 @@
*/
private boolean mIsClosing;
+ // Stores the last drop event which was reported to a valid drop target window, or null
+ // otherwise. This drop event will contain private info and should only be consumed by the
+ // unhandled drag listener.
+ DragEvent mUnhandledDropEvent;
+
DragState(WindowManagerService service, DragDropController controller, IBinder token,
SurfaceControl surface, int flags, IBinder localWin) {
mService = service;
@@ -287,14 +292,54 @@
mData = null;
mThumbOffsetX = mThumbOffsetY = 0;
mNotifiedWindows = null;
+ if (mUnhandledDropEvent != null) {
+ mUnhandledDropEvent.recycle();
+ mUnhandledDropEvent = null;
+ }
// Notifies the controller that the drag state is closed.
mDragDropController.onDragStateClosedLocked(this);
}
/**
+ * Creates the drop event for this drag gesture. If `touchedWin` is null, then the drop event
+ * will be created for dispatching to the unhandled drag and the drag surface will be provided
+ * as a part of the dispatched event.
+ */
+ private DragEvent createDropEvent(float x, float y, @Nullable WindowState touchedWin,
+ boolean includeDragSurface) {
+ if (touchedWin != null) {
+ final int targetUserId = UserHandle.getUserId(touchedWin.getOwningUid());
+ final DragAndDropPermissionsHandler dragAndDropPermissions;
+ if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 && (mFlags & DRAG_FLAGS_URI_ACCESS) != 0
+ && mData != null) {
+ dragAndDropPermissions = new DragAndDropPermissionsHandler(mService.mGlobalLock,
+ mData,
+ mUid,
+ touchedWin.getOwningPackage(),
+ mFlags & DRAG_FLAGS_URI_PERMISSIONS,
+ mSourceUserId,
+ targetUserId);
+ } else {
+ dragAndDropPermissions = null;
+ }
+ if (mSourceUserId != targetUserId) {
+ if (mData != null) {
+ mData.fixUris(mSourceUserId);
+ }
+ }
+ return obtainDragEvent(DragEvent.ACTION_DROP, x, y, mData,
+ targetInterceptsGlobalDrag(touchedWin), dragAndDropPermissions);
+ } else {
+ return obtainDragEvent(DragEvent.ACTION_DROP, x, y, mData,
+ includeDragSurface /* includeDragSurface */, null /* dragAndDropPermissions */);
+ }
+ }
+
+ /**
* Notify the drop target and tells it about the data. If the drop event is not sent to the
- * target, invokes {@code endDragLocked} immediately.
+ * target, invokes {@code endDragLocked} after the unhandled drag listener gets a chance to
+ * handle the drop.
*/
boolean reportDropWindowLock(IBinder token, float x, float y) {
if (mAnimator != null) {
@@ -302,41 +347,27 @@
}
final WindowState touchedWin = mService.mInputToWindowMap.get(token);
+ final DragEvent unhandledDropEvent = createDropEvent(x, y, null /* touchedWin */,
+ true /* includePrivateInfo */);
if (!isWindowNotified(touchedWin)) {
- // "drop" outside a valid window -- no recipient to apply a
- // timeout to, and we can send the drag-ended message immediately.
- mDragResult = false;
- endDragLocked();
+ // Delegate to the unhandled drag listener as a first pass
+ if (mDragDropController.notifyUnhandledDrop(unhandledDropEvent, "unhandled-drop")) {
+ // The unhandled drag listener will call back to notify whether it has consumed
+ // the drag, so return here
+ return true;
+ }
+
+ // "drop" outside a valid window -- no recipient to apply a timeout to, and we can send
+ // the drag-ended message immediately.
+ endDragLocked(false /* consumed */, false /* relinquishDragSurfaceToDropTarget */);
if (DEBUG_DRAG) Slog.d(TAG_WM, "Drop outside a valid window " + touchedWin);
return false;
}
if (DEBUG_DRAG) Slog.d(TAG_WM, "sending DROP to " + touchedWin);
- final int targetUserId = UserHandle.getUserId(touchedWin.getOwningUid());
-
- final DragAndDropPermissionsHandler dragAndDropPermissions;
- if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 && (mFlags & DRAG_FLAGS_URI_ACCESS) != 0
- && mData != null) {
- dragAndDropPermissions = new DragAndDropPermissionsHandler(mService.mGlobalLock,
- mData,
- mUid,
- touchedWin.getOwningPackage(),
- mFlags & DRAG_FLAGS_URI_PERMISSIONS,
- mSourceUserId,
- targetUserId);
- } else {
- dragAndDropPermissions = null;
- }
- if (mSourceUserId != targetUserId) {
- if (mData != null) {
- mData.fixUris(mSourceUserId);
- }
- }
final IBinder clientToken = touchedWin.mClient.asBinder();
- final DragEvent event = obtainDragEvent(DragEvent.ACTION_DROP, x, y,
- mData, targetInterceptsGlobalDrag(touchedWin),
- dragAndDropPermissions);
+ final DragEvent event = createDropEvent(x, y, touchedWin, false /* includePrivateInfo */);
try {
touchedWin.mClient.dispatchDragEvent(event);
@@ -345,7 +376,7 @@
DragDropController.DRAG_TIMEOUT_MS);
} catch (RemoteException e) {
Slog.w(TAG_WM, "can't send drop notification to win " + touchedWin);
- endDragLocked();
+ endDragLocked(false /* consumed */, false /* relinquishDragSurfaceToDropTarget */);
return false;
} finally {
if (MY_PID != touchedWin.mSession.mPid) {
@@ -353,6 +384,7 @@
}
}
mToken = clientToken;
+ mUnhandledDropEvent = unhandledDropEvent;
return true;
}
@@ -478,6 +510,9 @@
boolean containsAppExtras) {
final boolean interceptsGlobalDrag = targetInterceptsGlobalDrag(newWin);
if (mDragInProgress && isValidDropTarget(newWin, containsAppExtras, interceptsGlobalDrag)) {
+ if (DEBUG_DRAG) {
+ Slog.d(TAG_WM, "Sending DRAG_STARTED to new window " + newWin);
+ }
// Only allow the extras to be dispatched to a global-intercepting drag target
ClipData data = interceptsGlobalDrag ? mData.copyForTransferWithActivityInfo() : null;
DragEvent event = obtainDragEvent(DragEvent.ACTION_DRAG_STARTED,
@@ -523,14 +558,25 @@
return false;
}
if (!targetWin.isPotentialDragTarget(interceptsGlobalDrag)) {
+ // Window should not be a target
return false;
}
- if ((mFlags & View.DRAG_FLAG_GLOBAL) == 0 || !targetWindowSupportsGlobalDrag(targetWin)) {
+ final boolean isGlobalSameAppDrag = (mFlags & View.DRAG_FLAG_GLOBAL_SAME_APPLICATION) != 0;
+ final boolean isGlobalDrag = (mFlags & View.DRAG_FLAG_GLOBAL) != 0;
+ final boolean isAnyGlobalDrag = isGlobalDrag || isGlobalSameAppDrag;
+ if (!isAnyGlobalDrag || !targetWindowSupportsGlobalDrag(targetWin)) {
// Drag is limited to the current window.
if (!isLocalWindow) {
return false;
}
}
+ if (isGlobalSameAppDrag) {
+ // Drag is limited to app windows from the same uid or windows that can intercept global
+ // drag
+ if (!interceptsGlobalDrag && mUid != targetWin.getUid()) {
+ return false;
+ }
+ }
return interceptsGlobalDrag
|| mCrossProfileCopyAllowed
@@ -547,7 +593,10 @@
/**
* @return whether the given window {@param targetWin} can intercept global drags.
*/
- public boolean targetInterceptsGlobalDrag(WindowState targetWin) {
+ public boolean targetInterceptsGlobalDrag(@Nullable WindowState targetWin) {
+ if (targetWin == null) {
+ return false;
+ }
return (targetWin.mAttrs.privateFlags & PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP) != 0;
}
@@ -561,9 +610,6 @@
if (isWindowNotified(newWin)) {
return;
}
- if (DEBUG_DRAG) {
- Slog.d(TAG_WM, "need to send DRAG_STARTED to new window " + newWin);
- }
sendDragStartedLocked(newWin, mCurrentX, mCurrentY,
containsApplicationExtras(mDataDescription));
}
@@ -578,7 +624,13 @@
return false;
}
- void endDragLocked() {
+ /**
+ * Ends the current drag, animating the drag surface back to the source if the drop was not
+ * consumed by the receiving window.
+ */
+ void endDragLocked(boolean dropConsumed, boolean relinquishDragSurfaceToDropTarget) {
+ mDragResult = dropConsumed;
+ mRelinquishDragSurfaceToDropTarget = relinquishDragSurfaceToDropTarget;
if (mAnimator != null) {
return;
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 4ea76e1..de8d9f9 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -310,6 +310,7 @@
import android.window.ISurfaceSyncGroupCompletedListener;
import android.window.ITaskFpsCallback;
import android.window.ITrustedPresentationListener;
+import android.window.IUnhandledDragListener;
import android.window.InputTransferToken;
import android.window.ScreenCapture;
import android.window.SystemPerformanceHinter;
@@ -10026,4 +10027,16 @@
void onProcessActivityVisibilityChanged(int uid, boolean visible) {
mScreenRecordingCallbackController.onProcessActivityVisibilityChanged(uid, visible);
}
+
+ /**
+ * Sets the listener to be called back when a cross-window drag and drop operation is unhandled
+ * (ie. not handled by any window which can handle the drag).
+ */
+ @Override
+ public void setUnhandledDragListener(IUnhandledDragListener listener) throws RemoteException {
+ mAtmService.enforceTaskPermission("setUnhandledDragListener");
+ synchronized (mGlobalLock) {
+ mDragDropController.setUnhandledDragListener(listener);
+ }
+ }
}
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 1fb7cd8..9e2b1ec 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
@@ -32,14 +32,17 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.android.server.wm.DragDropController.MSG_UNHANDLED_DROP_LISTENER_TIMEOUT;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.app.PendingIntent;
@@ -49,9 +52,12 @@
import android.content.pm.ShortcutServiceInternal;
import android.graphics.PixelFormat;
import android.os.Binder;
+import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
+import android.os.Message;
import android.os.Parcelable;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
import android.view.DragEvent;
@@ -61,6 +67,7 @@
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
+import android.window.IUnhandledDragListener;
import androidx.test.filters.SmallTest;
@@ -533,14 +540,98 @@
});
}
+ @Test
+ public void testUnhandledDragListenerNotCalledForNormalDrags() throws RemoteException {
+ assumeTrue(com.android.window.flags.Flags.delegateUnhandledDrags());
+
+ final IUnhandledDragListener listener = mock(IUnhandledDragListener.class);
+ doReturn(mock(Binder.class)).when(listener).asBinder();
+ mTarget.setUnhandledDragListener(listener);
+ doDragAndDrop(0, ClipData.newPlainText("label", "Test"), 0, 0);
+ verify(listener, times(0)).onUnhandledDrop(any(), any());
+ }
+
+ @Test
+ public void testUnhandledDragListenerReceivesUnhandledDropOverWindow() {
+ assumeTrue(com.android.window.flags.Flags.delegateUnhandledDrags());
+
+ final IUnhandledDragListener listener = mock(IUnhandledDragListener.class);
+ doReturn(mock(Binder.class)).when(listener).asBinder();
+ mTarget.setUnhandledDragListener(listener);
+ final int invalidXY = 100_000;
+ startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> {
+ // Notify the unhandled drag listener
+ mTarget.reportDropWindow(mWindow.mInputChannelToken, invalidXY, invalidXY);
+ mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
+ mTarget.reportDropResult(mWindow.mClient, false);
+ mTarget.onUnhandledDropCallback(true);
+ mToken = null;
+ try {
+ verify(listener, times(1)).onUnhandledDrop(any(), any());
+ } catch (RemoteException e) {
+ fail("Failed to verify unhandled drop: " + e);
+ }
+ });
+ }
+
+ @Test
+ public void testUnhandledDragListenerReceivesUnhandledDropOverNoValidWindow() {
+ assumeTrue(com.android.window.flags.Flags.delegateUnhandledDrags());
+
+ final IUnhandledDragListener listener = mock(IUnhandledDragListener.class);
+ doReturn(mock(Binder.class)).when(listener).asBinder();
+ mTarget.setUnhandledDragListener(listener);
+ final int invalidXY = 100_000;
+ startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> {
+ // Notify the unhandled drag listener
+ mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
+ mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
+ mTarget.onUnhandledDropCallback(true);
+ mToken = null;
+ try {
+ verify(listener, times(1)).onUnhandledDrop(any(), any());
+ } catch (RemoteException e) {
+ fail("Failed to verify unhandled drop: " + e);
+ }
+ });
+ }
+
+ @Test
+ public void testUnhandledDragListenerCallbackTimeout() {
+ assumeTrue(com.android.window.flags.Flags.delegateUnhandledDrags());
+
+ final IUnhandledDragListener listener = mock(IUnhandledDragListener.class);
+ doReturn(mock(Binder.class)).when(listener).asBinder();
+ mTarget.setUnhandledDragListener(listener);
+ final int invalidXY = 100_000;
+ startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> {
+ // Notify the unhandled drag listener
+ mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
+ mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
+
+ // Verify that the unhandled drop listener callback timeout has been scheduled
+ final Handler handler = mTarget.getHandler();
+ assertTrue(handler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT));
+
+ // Force trigger the timeout and verify that it actually cleans up the drag & timeout
+ handler.handleMessage(Message.obtain(handler, MSG_UNHANDLED_DROP_LISTENER_TIMEOUT));
+ assertFalse(handler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT));
+ assertFalse(mTarget.dragDropActiveLocked());
+ mToken = null;
+ });
+ }
+
private void doDragAndDrop(int flags, ClipData data, float dropX, float dropY) {
startDrag(flags, data, () -> {
mTarget.reportDropWindow(mWindow.mInputChannelToken, dropX, dropY);
- mTarget.handleMotionEvent(false, dropX, dropY);
+ mTarget.handleMotionEvent(false /* keepHandling */, dropX, dropY);
mToken = mWindow.mClient.asBinder();
});
}
+ /**
+ * Starts a drag with the given parameters, calls Runnable `r` after drag is started.
+ */
private void startDrag(int flag, ClipData data, Runnable r) {
final SurfaceSession appSession = new SurfaceSession();
try {