Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 78de7961 authored by Winson Chung's avatar Winson Chung
Browse files

Add support for delegating unhandled drags to SystemUI

- This CL adds two public flags that are used with drag and drop:
  DRAG_FLAG_GLOBAL_SAME_APPLICATION which allows cross-window drag and
  drop between windows of the same app (by uid), and
  DRAG_FLAG_START_INTENT_ON_UNHANDLED_DRAG which allows the system to
  delegate drags that are not handled by any app window to the SystemUI
  for handling (if the current state of the device supports it)
- On the system side, it exposes a way for SysUI to register itself
  as an unhandled drag listener, and modifies the drag and drop flow
  to allow for WM to call the listener when an unhandled drag is
  detected.  In this case in particular, ACTION_DRAG_ENDED will now be
  deferred until the unhandled listener calls back.

  ie.
  For a normal drop over a window that handles it:
  ... -> Input reports drop over window
      -> ACTION_DROP sent to target window
      -> Window reports drop consumed to WM
      -> DRAG_ENDED (result=true) to all notified windows

  For a normal drop over a window that does not handle it:
  ... -> Input reports drop over window
      -> ACTION_DROP to target window
      -> Window reports drop not consumed to WM
      -> Unhandled drag listener notified of drop, reports consumed to WM
      -> DRAG_ENDED (result=unhandled consumed) to all notified windows

  For an unhandled drag over no window
  ... -> Input reports drop over window (but window was not valid target)
      -> Unhandled drag listener notified of drop, reports consumed to WM
      -> DRAG_ENDED (result=unhandled consumed) to all notified windows

  All other existing drag and drop behavior is the same as before

Bug: 320797628
API-Coverage-Bug: 324480328
Test: atest DragDropTest DragDropControllerTests CrossAppDragAndDropTests
Change-Id: Icc42f28e30cf976e5068c12e071942fcc61ee965
parent 35292e51
Loading
Loading
Loading
Loading
+13 −0
Original line number Diff line number Diff line
@@ -10020,11 +10020,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);
@@ -52773,9 +52784,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
+145 −25
Original line number Diff line number Diff line
@@ -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;
        }

@@ -320,6 +400,15 @@ public class ClipData implements Parcelable {
            return mIntent;
        }

        /**
         * 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.
         */
@@ -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);
    }

    /**
@@ -1041,6 +1130,35 @@ public class ClipData implements Parcelable {
        prepareToLeaveProcess(leavingPackage, Intent.FLAG_GRANT_READ_URI_PERMISSION);
    }

    /**
     * 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.
     *
@@ -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) {
+7 −0
Original line number Diff line number Diff line
@@ -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);
}
+61 −2
Original line number Diff line number Diff line
@@ -42,6 +42,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;
@@ -67,6 +68,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;
@@ -5282,6 +5284,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}.
     */
@@ -28402,9 +28432,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();
@@ -28430,6 +28480,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);
                }
@@ -28507,8 +28558,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) {
@@ -31421,12 +31476,16 @@ 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.
         */
+4 −0
Original line number Diff line number Diff line
@@ -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;
                    }
                }
            }
        }
Loading