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

Commit 54400760 authored by Hongwei Wang's avatar Hongwei Wang
Browse files

Add Content PiP support with launch-into-pip API

Terms
- Host activity, the activity that the content-pip is originated from.
  There should be some UI affordance on Host activity user can tap to
  enter content-pip
- Container activity, the activity that carries the content from Host
  Activity and transits to PiP mode

Workflow in brief

- Container activity is defined in the same application as Host activity
- A new ActivityOptions#makeLaunchIntoPip API is added, Host activity
  uses this API to construct ActivityOptions to launch the Container
  activity directly into pip mode.
- ActivityStarter sets a reference on Container activity pointing to
  Host activity upon creation and moves the activity to pinned task
- WMShell transits Container activity to pinned mode
- When user taps on `Expand` button in PiP menu, WMShell calls
  WCT#startTask with the task id to bring the Host task to front

Content hand-off

- Since Host activity is the one starts Container activity, it can
  replace its content with some placeholder right after startActivity
- This CL also makes the assumption that Host activity can establish a
  communication channel with the Container activity, such as using the
  ResultReceiver parcelable. Therefore it's out of scope to provide
  extra callback to Host activity on lifecycle events of Container activity.

See also the design doc go/content-pip-v2 and the end-to-end demo in
ApiDemos `App > Activity > Picture in Picture`

Bug: 165793661
Video: http://recall/-/aaaaaabFQoRHlzixHdtY/e9Mp5rvxYBrNOdGSUoSXHO
Test: manual, see Video
Test: atest WmTests:ActivityOptionsTest \
            WmTests:ActivityRecordTests \
            WmTests:ActivityStarterTests \
            PinnedStackTests
Change-Id: I2bebfea8d1930b0b7e495d91cfd5ddc7c347da68
parent ef65f3d9
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -4550,6 +4550,7 @@ package android.app {
    method public static android.app.ActivityOptions makeClipRevealAnimation(android.view.View, int, int, int, int);
    method public static android.app.ActivityOptions makeCustomAnimation(android.content.Context, int, int);
    method @NonNull public static android.app.ActivityOptions makeCustomAnimation(@NonNull android.content.Context, int, int, int);
    method @NonNull public static android.app.ActivityOptions makeLaunchIntoPip(@NonNull android.app.PictureInPictureParams);
    method public static android.app.ActivityOptions makeScaleUpAnimation(android.view.View, int, int, int, int);
    method public static android.app.ActivityOptions makeSceneTransitionAnimation(android.app.Activity, android.view.View, String);
    method @java.lang.SafeVarargs public static android.app.ActivityOptions makeSceneTransitionAnimation(android.app.Activity, android.util.Pair<android.view.View,java.lang.String>...);
@@ -6635,6 +6636,7 @@ package android.app {
  public static class PictureInPictureParams.Builder {
    ctor public PictureInPictureParams.Builder();
    ctor public PictureInPictureParams.Builder(@NonNull android.app.PictureInPictureParams);
    method public android.app.PictureInPictureParams build();
    method public android.app.PictureInPictureParams.Builder setActions(java.util.List<android.app.RemoteAction>);
    method public android.app.PictureInPictureParams.Builder setAspectRatio(android.util.Rational);
+45 −0
Original line number Diff line number Diff line
@@ -350,6 +350,10 @@ public class ActivityOptions extends ComponentOptions {
    /** See {@link #setTransientLaunch()}. */
    private static final String KEY_TRANSIENT_LAUNCH = "android.activity.transientLaunch";

    /** see {@link #makeLaunchIntoPip(PictureInPictureParams)}. */
    private static final String KEY_LAUNCH_INTO_PIP_PARAMS =
            "android.activity.launchIntoPipParams";

    /**
     * @see #setLaunchCookie
     * @hide
@@ -444,6 +448,7 @@ public class ActivityOptions extends ComponentOptions {
    private boolean mRemoveWithTaskOrganizer;
    private boolean mLaunchedFromBubble;
    private boolean mTransientLaunch;
    private PictureInPictureParams mLaunchIntoPipParams;

    /**
     * Create an ActivityOptions specifying a custom animation to run when
@@ -1106,6 +1111,24 @@ public class ActivityOptions extends ComponentOptions {
        return opts;
    }

    /**
     * Creates an {@link ActivityOptions} instance that launch into picture-in-picture.
     * This is normally used by a Host activity to start another activity that will directly enter
     * picture-in-picture upon its creation.
     * @param pictureInPictureParams {@link PictureInPictureParams} for launching the Activity to
     *                               picture-in-picture mode.
     */
    @NonNull
    public static ActivityOptions makeLaunchIntoPip(
            @NonNull PictureInPictureParams pictureInPictureParams) {
        final ActivityOptions opts = new ActivityOptions();
        opts.mLaunchIntoPipParams = new PictureInPictureParams.Builder(pictureInPictureParams)
                .setIsLaunchIntoPip(true)
                .build();
        opts.mLaunchBounds = new Rect(pictureInPictureParams.getSourceRectHint());
        return opts;
    }

    /** @hide */
    public boolean getLaunchTaskBehind() {
        return mAnimationType == ANIM_LAUNCH_TASK_BEHIND;
@@ -1219,6 +1242,7 @@ public class ActivityOptions extends ComponentOptions {
        mLaunchedFromBubble = opts.getBoolean(KEY_LAUNCHED_FROM_BUBBLE);
        mTransientLaunch = opts.getBoolean(KEY_TRANSIENT_LAUNCH);
        mSplashScreenStyle = opts.getInt(KEY_SPLASH_SCREEN_STYLE);
        mLaunchIntoPipParams = opts.getParcelable(KEY_LAUNCH_INTO_PIP_PARAMS);
    }

    /**
@@ -1556,6 +1580,23 @@ public class ActivityOptions extends ComponentOptions {
        mLaunchWindowingMode = windowingMode;
    }

    /**
     * @return {@link PictureInPictureParams} used to launch into PiP mode.
     * @hide
     */
    public PictureInPictureParams getLaunchIntoPipParams() {
        return mLaunchIntoPipParams;
    }

    /**
     * @return {@code true} if this instance is used to launch into PiP mode.
     * @hide
     */
    public boolean isLaunchIntoPip() {
        return mLaunchIntoPipParams != null
                && mLaunchIntoPipParams.isLaunchIntoPip();
    }

    /** @hide */
    public int getLaunchActivityType() {
        return mLaunchActivityType;
@@ -1867,6 +1908,7 @@ public class ActivityOptions extends ComponentOptions {
        mAnimationFinishedListener = otherOptions.mAnimationFinishedListener;
        mSpecsFuture = otherOptions.mSpecsFuture;
        mRemoteAnimationAdapter = otherOptions.mRemoteAnimationAdapter;
        mLaunchIntoPipParams = otherOptions.mLaunchIntoPipParams;
    }

    /**
@@ -2039,6 +2081,9 @@ public class ActivityOptions extends ComponentOptions {
        if (mSplashScreenStyle != 0) {
            b.putInt(KEY_SPLASH_SCREEN_STYLE, mSplashScreenStyle);
        }
        if (mLaunchIntoPipParams != null) {
            b.putParcelable(KEY_LAUNCH_INTO_PIP_PARAMS, mLaunchIntoPipParams);
        }
        return b;
    }

+76 −9
Original line number Diff line number Diff line
@@ -64,6 +64,27 @@ public final class PictureInPictureParams implements Parcelable {

        private CharSequence mSubtitle;

        private Boolean mIsLaunchIntoPip;

        /** Default constructor */
        public Builder() {}

        /**
         * Copy constructor
         * @param original {@link PictureInPictureParams} instance this builder is built upon.
         */
        public Builder(@NonNull PictureInPictureParams original) {
            mAspectRatio = original.mAspectRatio;
            mUserActions = original.mUserActions;
            mCloseAction = original.mCloseAction;
            mSourceRectHint = original.mSourceRectHint;
            mAutoEnterEnabled = original.mAutoEnterEnabled;
            mSeamlessResizeEnabled = original.mSeamlessResizeEnabled;
            mTitle = original.mTitle;
            mSubtitle = original.mSubtitle;
            mIsLaunchIntoPip = original.mIsLaunchIntoPip;
        }

        /**
         * Sets the aspect ratio.  This aspect ratio is defined as the desired width / height, and
         * does not change upon device rotation.
@@ -221,6 +242,20 @@ public final class PictureInPictureParams implements Parcelable {
            return this;
        }

        /**
         * Sets whether the built {@link PictureInPictureParams} represents a launch into
         * picture-in-picture request.
         *
         * This property is {@code false} by default.
         * @param isLaunchIntoPip {@code true} if the built instance represents a launch into
         *                                 picture-in-picture request
         * @return this builder instance.
         */
        @NonNull
        Builder setIsLaunchIntoPip(boolean isLaunchIntoPip) {
            mIsLaunchIntoPip = isLaunchIntoPip;
            return this;
        }

        /**
         * @return an immutable {@link PictureInPictureParams} to be used when entering or updating
@@ -232,7 +267,8 @@ public final class PictureInPictureParams implements Parcelable {
        public PictureInPictureParams build() {
            PictureInPictureParams params = new PictureInPictureParams(mAspectRatio,
                    mExpandedAspectRatio, mUserActions, mCloseAction, mSourceRectHint,
                    mAutoEnterEnabled, mSeamlessResizeEnabled, mTitle, mSubtitle);
                    mAutoEnterEnabled, mSeamlessResizeEnabled, mTitle, mSubtitle,
                    mIsLaunchIntoPip);
            return params;
        }
    }
@@ -294,6 +330,13 @@ public final class PictureInPictureParams implements Parcelable {
    @Nullable
    private CharSequence mSubtitle;

    /**
     * Whether this {@link PictureInPictureParams} represents a launch into
     * picture-in-picture request.
     * {@link #isLaunchIntoPip()} defaults to {@code false} is this is not set.
     */
    private Boolean mIsLaunchIntoPip;

    /** {@hide} */
    PictureInPictureParams() {
    }
@@ -322,13 +365,16 @@ public final class PictureInPictureParams implements Parcelable {
        if (in.readInt() != 0) {
            mSubtitle = in.readCharSequence();
        }
        if (in.readInt() != 0) {
            mIsLaunchIntoPip = in.readBoolean();
        }
    }

    /** {@hide} */
    PictureInPictureParams(Rational aspectRatio, Rational expandedAspectRatio,
            List<RemoteAction> actions, RemoteAction closeAction, Rect sourceRectHint,
            Boolean autoEnterEnabled, Boolean seamlessResizeEnabled, CharSequence title,
            CharSequence subtitle) {
            CharSequence subtitle, Boolean isLaunchIntoPip) {
        mAspectRatio = aspectRatio;
        mExpandedAspectRatio = expandedAspectRatio;
        mUserActions = actions;
@@ -338,6 +384,7 @@ public final class PictureInPictureParams implements Parcelable {
        mSeamlessResizeEnabled = seamlessResizeEnabled;
        mTitle = title;
        mSubtitle = subtitle;
        mIsLaunchIntoPip = isLaunchIntoPip;
    }

    /**
@@ -347,8 +394,8 @@ public final class PictureInPictureParams implements Parcelable {
    public PictureInPictureParams(PictureInPictureParams other) {
        this(other.mAspectRatio, other.mExpandedAspectRatio, other.mUserActions, other.mCloseAction,
                other.hasSourceBoundsHint() ? new Rect(other.getSourceRectHint()) : null,
                other.mAutoEnterEnabled, other.mSeamlessResizeEnabled, other.mTitle,
                other.mSubtitle);
                other.mAutoEnterEnabled, other.mSeamlessResizeEnabled,
                other.mTitle, other.mSubtitle, other.mIsLaunchIntoPip);
    }

    /**
@@ -384,6 +431,9 @@ public final class PictureInPictureParams implements Parcelable {
        if (otherArgs.hasSetSubtitle()) {
            mSubtitle = otherArgs.mSubtitle;
        }
        if (otherArgs.mIsLaunchIntoPip != null) {
            mIsLaunchIntoPip = otherArgs.mIsLaunchIntoPip;
        }
    }

    /**
@@ -547,15 +597,23 @@ public final class PictureInPictureParams implements Parcelable {
        return mSubtitle;
    }

    /**
     * @return whether this {@link PictureInPictureParams} represents a launch into pip request.
     * @hide
     */
    public boolean isLaunchIntoPip() {
        return mIsLaunchIntoPip == null ? false : mIsLaunchIntoPip;
    }

    /**
     * @return True if no parameters are set
     * @hide
     */
    public boolean empty() {
        return !hasSourceBoundsHint() && !hasSetActions() && !hasSetCloseAction()
                && !hasSetAspectRatio() && !hasSetExpandedAspectRatio() && mAutoEnterEnabled != null
                && mSeamlessResizeEnabled != null && !hasSetTitle()
                && !hasSetSubtitle();
                && !hasSetAspectRatio() && !hasSetExpandedAspectRatio() && mAutoEnterEnabled == null
                && mSeamlessResizeEnabled == null && !hasSetTitle()
                && !hasSetSubtitle() && mIsLaunchIntoPip == null;
    }

    @Override
@@ -571,13 +629,15 @@ public final class PictureInPictureParams implements Parcelable {
                && Objects.equals(mCloseAction, that.mCloseAction)
                && Objects.equals(mSourceRectHint, that.mSourceRectHint)
                && Objects.equals(mTitle, that.mTitle)
                && Objects.equals(mSubtitle, that.mSubtitle);
                && Objects.equals(mSubtitle, that.mSubtitle)
                && Objects.equals(mIsLaunchIntoPip, that.mIsLaunchIntoPip);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mAspectRatio, mExpandedAspectRatio, mUserActions, mCloseAction,
                mSourceRectHint, mAutoEnterEnabled, mSeamlessResizeEnabled, mTitle, mSubtitle);
                mSourceRectHint, mAutoEnterEnabled, mSeamlessResizeEnabled, mTitle, mSubtitle,
                mIsLaunchIntoPip);
    }

    @Override
@@ -628,6 +688,12 @@ public final class PictureInPictureParams implements Parcelable {
        } else {
            out.writeInt(0);
        }
        if (mIsLaunchIntoPip != null) {
            out.writeInt(1);
            out.writeBoolean(mIsLaunchIntoPip);
        } else {
            out.writeInt(0);
        }
    }

    private void writeRationalToParcel(Rational rational, Parcel out) {
@@ -659,6 +725,7 @@ public final class PictureInPictureParams implements Parcelable {
                + " isSeamlessResizeEnabled=" + isSeamlessResizeEnabled()
                + " title=" + getTitle()
                + " subtitle=" + getSubtitle()
                + " isLaunchIntoPip=" + isLaunchIntoPip()
                + ")";
    }

+10 −0
Original line number Diff line number Diff line
@@ -190,6 +190,13 @@ public class TaskInfo {
     */
    public boolean preferDockBigOverlays;

    /**
     * The task id of the host Task of the launch-into-pip Activity, i.e., it points to the Task
     * the launch-into-pip Activity is originated from.
     * @hide
     */
    public int launchIntoPipHostTaskId;

    /**
     * The {@link Rect} copied from {@link DisplayCutout#getSafeInsets()} if the cutout is not of
     * (LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS),
@@ -516,6 +523,7 @@ public class TaskInfo {
        topActivityType = source.readInt();
        pictureInPictureParams = source.readTypedObject(PictureInPictureParams.CREATOR);
        preferDockBigOverlays = source.readBoolean();
        launchIntoPipHostTaskId = source.readInt();
        displayCutoutInsets = source.readTypedObject(Rect.CREATOR);
        topActivityInfo = source.readTypedObject(ActivityInfo.CREATOR);
        isResizeable = source.readBoolean();
@@ -562,6 +570,7 @@ public class TaskInfo {
        dest.writeInt(topActivityType);
        dest.writeTypedObject(pictureInPictureParams, flags);
        dest.writeBoolean(preferDockBigOverlays);
        dest.writeInt(launchIntoPipHostTaskId);
        dest.writeTypedObject(displayCutoutInsets, flags);
        dest.writeTypedObject(topActivityInfo, flags);
        dest.writeBoolean(isResizeable);
@@ -602,6 +611,7 @@ public class TaskInfo {
                + " topActivityType=" + topActivityType
                + " pictureInPictureParams=" + pictureInPictureParams
                + " preferDockBigOverlays=" + preferDockBigOverlays
                + " launchIntoPipHostTaskId=" + launchIntoPipHostTaskId
                + " displayCutoutSafeInsets=" + displayCutoutInsets
                + " topActivityInfo=" + topActivityInfo
                + " launchCookies=" + launchCookies
+17 −1
Original line number Diff line number Diff line
@@ -310,6 +310,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
        return mPipTransitionState.isInPip();
    }

    private boolean isLaunchIntoPipTask() {
        return mPictureInPictureParams != null && mPictureInPictureParams.isLaunchIntoPip();
    }

    /**
     * Returns whether the entry animation is waiting to be started.
     */
@@ -397,6 +401,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
        }

        final WindowContainerTransaction wct = new WindowContainerTransaction();
        if (isLaunchIntoPipTask()) {
            exitLaunchIntoPipTask(wct);
            return;
        }

        if (ENABLE_SHELL_TRANSITIONS) {
            if (requestEnterSplit && mSplitScreenOptional.isPresent()) {
@@ -468,6 +476,14 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
        });
    }

    private void exitLaunchIntoPipTask(WindowContainerTransaction wct) {
        wct.startTask(mTaskInfo.launchIntoPipHostTaskId, null /* ActivityOptions */);
        mTaskOrganizer.applyTransaction(wct);

        // Remove the PiP with fade-out animation right after the host Task is brought to front.
        removePip();
    }

    private void applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction) {
        // Reset the final windowing mode.
        wct.setWindowingMode(mToken, getOutPipWindowingMode());
@@ -729,7 +745,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
    }

    /**
     * Note that dismissing PiP is now originated from SystemUI, see {@link #exitPip(int)}.
     * Note that dismissing PiP is now originated from SystemUI, see {@link #exitPip(int, boolean)}.
     * Meanwhile this callback is invoked whenever the task is removed. For instance:
     *   - as a result of removeRootTasksInWindowingModes from WM
     *   - activity itself is died
Loading