diff options
| -rw-r--r-- | core/api/current.txt | 13 | ||||
| -rw-r--r-- | core/java/android/content/ClipData.java | 170 | ||||
| -rw-r--r-- | core/java/android/view/IWindowManager.aidl | 7 | ||||
| -rw-r--r-- | core/java/android/view/View.java | 63 | ||||
| -rw-r--r-- | core/java/android/view/ViewRootImpl.java | 4 | ||||
| -rw-r--r-- | core/java/android/window/IUnhandledDragCallback.aidl | 33 | ||||
| -rw-r--r-- | core/java/android/window/IUnhandledDragListener.aidl | 35 | ||||
| -rw-r--r-- | services/core/java/com/android/server/wm/DragDropController.java | 146 | ||||
| -rw-r--r-- | services/core/java/com/android/server/wm/DragState.java | 122 | ||||
| -rw-r--r-- | services/core/java/com/android/server/wm/WindowManagerService.java | 13 | ||||
| -rw-r--r-- | services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java | 93 |
11 files changed, 630 insertions, 69 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 302ee5abb7db..91a2f68782a2 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -10045,11 +10045,22 @@ package android.content { 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 @@ package android.view { 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 67759f4aa76d..eb357fe09a31 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_ANDROID_RESOURCE; 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 @@ public class ClipData implements Parcelable { 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 @@ public class ClipData implements Parcelable { // 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 @@ public class ClipData implements Parcelable { * 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 @@ public class ClipData implements Parcelable { * </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 @@ public class ClipData implements Parcelable { * 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 @@ public class ClipData implements Parcelable { * 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 @@ public class ClipData implements Parcelable { mText = text; mHtmlText = htmlText; mIntent = intent; + mPendingIntent = pendingIntent; mUri = uri; } @@ -321,6 +401,15 @@ public class ClipData implements Parcelable { } /** + * 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 @@ public class ClipData implements Parcelable { 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 @@ public class ClipData implements Parcelable { 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 class ClipData implements Parcelable { 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 @@ public class ClipData implements Parcelable { } /** + * 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 @@ public class ClipData implements Parcelable { 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 @@ public class ClipData implements Parcelable { 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 class ClipData implements Parcelable { } 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 b5b81d17013a..29cc8594deec 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -73,6 +73,7 @@ import android.window.IScreenRecordingCallback; 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 @@ interface IWindowManager @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 c22986b3473b..e781f3cdee2f 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_CLASSIFI 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.SystemApi; 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 class View implements Drawable.Callback, KeyEvent.Callback, 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 @@ public class View implements Drawable.Callback, KeyEvent.Callback, 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 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (token != null) { root.setLocalDragState(myLocalState); mAttachInfo.mDragToken = token; + mAttachInfo.mDragData = data; mAttachInfo.mViewRootImpl.setDragStartedViewForAccessibility(this); setAccessibilityDragStarted(true); } @@ -28601,8 +28652,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, 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 @@ public class View implements Drawable.Callback, KeyEvent.Callback, 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 07c97950e9bb..28a73344b731 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -8599,6 +8599,10 @@ public final class ViewRootImpl implements ViewParent, 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 000000000000..7806b1fe771a --- /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 000000000000..52e98952971d --- /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 6a3cf43438fd..a3e28693cb21 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.Handler; 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 @@ class DragDropController { 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 @@ class DragDropController { 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 @@ class DragDropController { 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 @@ class DragDropController { 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 @@ class DragDropController { } } + /** + * 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 @@ class DragDropController { // 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 @@ class DragDropController { 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 @@ class DragDropController { } 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 d302f0641b58..76038b945288 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 @@ class DragState { */ 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 @@ class DragState { 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 @@ class DragState { } 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 @@ class DragState { 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 @@ class DragState { } } mToken = clientToken; + mUnhandledDropEvent = unhandledDropEvent; return true; } @@ -478,6 +510,9 @@ class DragState { 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 @@ class DragState { 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 @@ class DragState { /** * @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 @@ class DragState { 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 @@ class DragState { 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 4ea76e128584..de8d9f96453d 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.IScreenRecordingCallback; 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 @@ public class WindowManagerService extends IWindowManager.Stub 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 1fb7cd8e6e1c..9e2b1eccc3b2 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.doReturn; 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.Intent; 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.SurfaceSession; 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 @@ public class DragDropControllerTests extends WindowTestsBase { }); } + @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 { |