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

Commit 482fbfab authored by Charles Chen's avatar Charles Chen
Browse files

Respect minimum dimensions for embedded Activities

Before this CL, minimum dimensions of Activity wasn't respected,
that said, an Activity could be embedded in a TaskFragment of which
bounds are smaller its minimum dimensions.
This CL add the minimum dimensions on several places:

WM core:
1. Verify minimum dimension requirement before adding an Activity to
   a TaskFragment. It'll be early return if the requirement is not
   satisfied.
2. Propagate the minimum dimensions to the client side through
   TaskFragmentInfo to notify the requirement.
3. If TaskFragmentOrganizer tries to shrink a TaskFragment to
   the bounds that smaller than minimum dimensions of its children
   Activity, switch to match the parent bounds.
AndroidX Window extensions:
1. Early return if TaskFragment is resized to the bounds that smaller
   than minimum dimensions which dispatched from the server side.
2. When organizer tries to show Activities side-by-side, verify if
   minimum dimensions requirement of the primary Activiy. If the
   requirement is not satisfied, show Activities in fullscreen
   instead.
TODO: Add an API to check if an Activity intent is allowed to embed
   in a TaskFragment.

Bug: 232871351
Test: atest TaskFragmentOrganizerControllerTest
Test: atest TaskFragmentOrganizerTest TaskFragmentOrganizerPolicyTest
Test: atest SplitActivityLifecycleTest
Test: atest CtsWindowManagerJetpackTestCases
Test: atest WmJetpackUnitTests

Change-Id: Ib46c2cec2a0735b9e3f3420f2cb94754801b86b9
parent 75cec0e8
Loading
Loading
Loading
Loading
+5 −0
Original line number Original line Diff line number Diff line
@@ -8260,6 +8260,11 @@ public class Activity extends ContextThemeWrapper
        return mMainThread;
        return mMainThread;
    }
    }


    /** @hide */
    public final ActivityInfo getActivityInfo() {
        return mActivityInfo;
    }

    final void performCreate(Bundle icicle) {
    final void performCreate(Bundle icicle) {
        performCreate(icicle, null);
        performCreate(icicle, null);
    }
    }
+42 −6
Original line number Original line Diff line number Diff line
@@ -23,8 +23,10 @@ import static java.util.Objects.requireNonNull;
import android.annotation.NonNull;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Nullable;
import android.annotation.TestApi;
import android.annotation.TestApi;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.Parcelable;
@@ -66,7 +68,7 @@ public final class TaskFragmentInfo implements Parcelable {
    private final List<IBinder> mActivities = new ArrayList<>();
    private final List<IBinder> mActivities = new ArrayList<>();


    /** Relative position of the fragment's top left corner in the parent container. */
    /** Relative position of the fragment's top left corner in the parent container. */
    private final Point mPositionInParent;
    private final Point mPositionInParent = new Point();


    /**
    /**
     * Whether the last running activity in the TaskFragment was finished due to clearing task while
     * Whether the last running activity in the TaskFragment was finished due to clearing task while
@@ -80,21 +82,31 @@ public final class TaskFragmentInfo implements Parcelable {
     */
     */
    private final boolean mIsTaskFragmentClearedForPip;
    private final boolean mIsTaskFragmentClearedForPip;


    /**
     * The maximum {@link ActivityInfo.WindowLayout#minWidth} and
     * {@link ActivityInfo.WindowLayout#minHeight} aggregated from the TaskFragment's child
     * activities.
     */
    @NonNull
    private final Point mMinimumDimensions = new Point();

    /** @hide */
    /** @hide */
    public TaskFragmentInfo(
    public TaskFragmentInfo(
            @NonNull IBinder fragmentToken, @NonNull WindowContainerToken token,
            @NonNull IBinder fragmentToken, @NonNull WindowContainerToken token,
            @NonNull Configuration configuration, int runningActivityCount,
            @NonNull Configuration configuration, int runningActivityCount,
            boolean isVisible, @NonNull List<IBinder> activities, @NonNull Point positionInParent,
            boolean isVisible, @NonNull List<IBinder> activities, @NonNull Point positionInParent,
            boolean isTaskClearedForReuse, boolean isTaskFragmentClearedForPip) {
            boolean isTaskClearedForReuse, boolean isTaskFragmentClearedForPip,
            @NonNull Point minimumDimensions) {
        mFragmentToken = requireNonNull(fragmentToken);
        mFragmentToken = requireNonNull(fragmentToken);
        mToken = requireNonNull(token);
        mToken = requireNonNull(token);
        mConfiguration.setTo(configuration);
        mConfiguration.setTo(configuration);
        mRunningActivityCount = runningActivityCount;
        mRunningActivityCount = runningActivityCount;
        mIsVisible = isVisible;
        mIsVisible = isVisible;
        mActivities.addAll(activities);
        mActivities.addAll(activities);
        mPositionInParent = requireNonNull(positionInParent);
        mPositionInParent.set(positionInParent);
        mIsTaskClearedForReuse = isTaskClearedForReuse;
        mIsTaskClearedForReuse = isTaskClearedForReuse;
        mIsTaskFragmentClearedForPip = isTaskFragmentClearedForPip;
        mIsTaskFragmentClearedForPip = isTaskFragmentClearedForPip;
        mMinimumDimensions.set(minimumDimensions);
    }
    }


    @NonNull
    @NonNull
@@ -153,6 +165,26 @@ public final class TaskFragmentInfo implements Parcelable {
        return mConfiguration.windowConfiguration.getWindowingMode();
        return mConfiguration.windowConfiguration.getWindowingMode();
    }
    }


    /**
     * Returns the minimum width this TaskFragment can be resized to.
     * Client side must not {@link WindowContainerTransaction#setBounds(WindowContainerToken, Rect)}
     * that {@link Rect#width()} is shorter than the reported value.
     * @hide pending unhide
     */
    public int getMinimumWidth() {
        return mMinimumDimensions.x;
    }

    /**
     * Returns the minimum width this TaskFragment can be resized to.
     * Client side must not {@link WindowContainerTransaction#setBounds(WindowContainerToken, Rect)}
     * that {@link Rect#height()} is shorter than the reported value.
     * @hide pending unhide
     */
    public int getMinimumHeight() {
        return mMinimumDimensions.y;
    }

    /**
    /**
     * Returns {@code true} if the parameters that are important for task fragment organizers are
     * Returns {@code true} if the parameters that are important for task fragment organizers are
     * equal between this {@link TaskFragmentInfo} and {@param that}.
     * equal between this {@link TaskFragmentInfo} and {@param that}.
@@ -170,7 +202,8 @@ public final class TaskFragmentInfo implements Parcelable {
                && mActivities.equals(that.mActivities)
                && mActivities.equals(that.mActivities)
                && mPositionInParent.equals(that.mPositionInParent)
                && mPositionInParent.equals(that.mPositionInParent)
                && mIsTaskClearedForReuse == that.mIsTaskClearedForReuse
                && mIsTaskClearedForReuse == that.mIsTaskClearedForReuse
                && mIsTaskFragmentClearedForPip == that.mIsTaskFragmentClearedForPip;
                && mIsTaskFragmentClearedForPip == that.mIsTaskFragmentClearedForPip
                && mMinimumDimensions.equals(that.mMinimumDimensions);
    }
    }


    private TaskFragmentInfo(Parcel in) {
    private TaskFragmentInfo(Parcel in) {
@@ -180,9 +213,10 @@ public final class TaskFragmentInfo implements Parcelable {
        mRunningActivityCount = in.readInt();
        mRunningActivityCount = in.readInt();
        mIsVisible = in.readBoolean();
        mIsVisible = in.readBoolean();
        in.readBinderList(mActivities);
        in.readBinderList(mActivities);
        mPositionInParent = requireNonNull(in.readTypedObject(Point.CREATOR));
        mPositionInParent.readFromParcel(in);
        mIsTaskClearedForReuse = in.readBoolean();
        mIsTaskClearedForReuse = in.readBoolean();
        mIsTaskFragmentClearedForPip = in.readBoolean();
        mIsTaskFragmentClearedForPip = in.readBoolean();
        mMinimumDimensions.readFromParcel(in);
    }
    }


    /** @hide */
    /** @hide */
@@ -194,9 +228,10 @@ public final class TaskFragmentInfo implements Parcelable {
        dest.writeInt(mRunningActivityCount);
        dest.writeInt(mRunningActivityCount);
        dest.writeBoolean(mIsVisible);
        dest.writeBoolean(mIsVisible);
        dest.writeBinderList(mActivities);
        dest.writeBinderList(mActivities);
        dest.writeTypedObject(mPositionInParent, flags);
        mPositionInParent.writeToParcel(dest, flags);
        dest.writeBoolean(mIsTaskClearedForReuse);
        dest.writeBoolean(mIsTaskClearedForReuse);
        dest.writeBoolean(mIsTaskFragmentClearedForPip);
        dest.writeBoolean(mIsTaskFragmentClearedForPip);
        mMinimumDimensions.writeToParcel(dest, flags);
    }
    }


    @NonNull
    @NonNull
@@ -224,6 +259,7 @@ public final class TaskFragmentInfo implements Parcelable {
                + " positionInParent=" + mPositionInParent
                + " positionInParent=" + mPositionInParent
                + " isTaskClearedForReuse=" + mIsTaskClearedForReuse
                + " isTaskClearedForReuse=" + mIsTaskClearedForReuse
                + " isTaskFragmentClearedForPip" + mIsTaskFragmentClearedForPip
                + " isTaskFragmentClearedForPip" + mIsTaskFragmentClearedForPip
                + " minimumDimensions" + mMinimumDimensions
                + "}";
                + "}";
    }
    }


+10 −2
Original line number Original line Diff line number Diff line
@@ -36,8 +36,7 @@ public class Point implements Parcelable {
    }
    }


    public Point(@NonNull Point src) {
    public Point(@NonNull Point src) {
        this.x = src.x;
        set(src);
        this.y = src.y;
    }
    }


    /**
    /**
@@ -48,6 +47,15 @@ public class Point implements Parcelable {
        this.y = y;
        this.y = y;
    }
    }


    /**
     * Sets the point's from {@code src}'s coordinates
     * @hide
     */
    public void set(@NonNull Point src) {
        this.x = src.x;
        this.y = src.y;
    }

    /**
    /**
     * Negate the point's coordinates
     * Negate the point's coordinates
     */
     */
+9 −0
Original line number Original line Diff line number Diff line
@@ -18,6 +18,8 @@ package androidx.window.extensions.embedding;


import android.annotation.NonNull;
import android.annotation.NonNull;
import android.app.Activity;
import android.app.Activity;
import android.util.Pair;
import android.util.Size;


/**
/**
 * Client-side descriptor of a split that holds two containers.
 * Client-side descriptor of a split that holds two containers.
@@ -66,6 +68,13 @@ class SplitContainer {
        return mSplitRule;
        return mSplitRule;
    }
    }


    /** Returns the minimum dimension pair of primary container and secondary container. */
    @NonNull
    Pair<Size, Size> getMinDimensionsPair() {
        return new Pair<>(mPrimaryContainer.getMinDimensions(),
                mSecondaryContainer.getMinDimensions());
    }

    boolean isPlaceholderContainer() {
    boolean isPlaceholderContainer() {
        return (mSplitRule instanceof SplitPlaceholderRule);
        return (mSplitRule instanceof SplitPlaceholderRule);
    }
    }
+38 −14
Original line number Original line Diff line number Diff line
@@ -24,9 +24,11 @@ import static androidx.window.extensions.embedding.SplitContainer.getFinishSecon
import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceholderRule;
import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceholderRule;
import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent;
import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent;
import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked;
import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked;
import static androidx.window.extensions.embedding.SplitPresenter.boundsSmallerThanMinDimensions;
import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair;
import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions;
import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide;


import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.Activity;
import android.app.ActivityClient;
import android.app.ActivityClient;
import android.app.ActivityOptions;
import android.app.ActivityOptions;
@@ -43,11 +45,15 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.Looper;
import android.util.ArraySet;
import android.util.ArraySet;
import android.util.Log;
import android.util.Log;
import android.util.Pair;
import android.util.Size;
import android.util.SparseArray;
import android.util.SparseArray;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerTransaction;
import android.window.WindowContainerTransaction;


import androidx.annotation.GuardedBy;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.window.common.EmptyLifecycleCallbacksAdapter;
import androidx.window.common.EmptyLifecycleCallbacksAdapter;


import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting;
@@ -63,7 +69,7 @@ import java.util.function.Consumer;
 */
 */
public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback,
public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback,
        ActivityEmbeddingComponent {
        ActivityEmbeddingComponent {
    private static final String TAG = "SplitController";
    static final String TAG = "SplitController";


    @VisibleForTesting
    @VisibleForTesting
    @GuardedBy("mLock")
    @GuardedBy("mLock")
@@ -350,7 +356,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
            if (!(rule instanceof SplitRule)) {
            if (!(rule instanceof SplitRule)) {
                continue;
                continue;
            }
            }
            if (mPresenter.shouldShowSideBySide(taskContainer.getTaskBounds(), (SplitRule) rule)) {
            if (shouldShowSideBySide(taskContainer.getTaskBounds(), (SplitRule) rule)) {
                return true;
                return true;
            }
            }
        }
        }
@@ -610,11 +616,15 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
                primaryActivity);
                primaryActivity);
        final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer);
        final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer);
        if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer()
        if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer()
                && canReuseContainer(splitRule, splitContainer.getSplitRule())) {
                && canReuseContainer(splitRule, splitContainer.getSplitRule())
                && !boundsSmallerThanMinDimensions(primaryContainer.getLastRequestedBounds(),
                        getMinDimensions(primaryActivity))) {
            // Can launch in the existing secondary container if the rules share the same
            // Can launch in the existing secondary container if the rules share the same
            // presentation.
            // presentation.
            final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer();
            final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer();
            if (secondaryContainer == getContainerWithActivity(secondaryActivity)) {
            if (secondaryContainer == getContainerWithActivity(secondaryActivity)
                    && !boundsSmallerThanMinDimensions(secondaryContainer.getLastRequestedBounds(),
                            getMinDimensions(secondaryActivity))) {
                // The activity is already in the target TaskFragment.
                // The activity is already in the target TaskFragment.
                return true;
                return true;
            }
            }
@@ -791,10 +801,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
                && (canReuseContainer(splitRule, splitContainer.getSplitRule())
                && (canReuseContainer(splitRule, splitContainer.getSplitRule())
                // TODO(b/231845476) we should always respect clearTop.
                // TODO(b/231845476) we should always respect clearTop.
                || !respectClearTop)) {
                || !respectClearTop)) {
            final Rect secondaryBounds = splitContainer.getSecondaryContainer()
                    .getLastRequestedBounds();
            if (secondaryBounds.isEmpty()
                    || !boundsSmallerThanMinDimensions(secondaryBounds,
                            getMinDimensions(intent))) {
                // Can launch in the existing secondary container if the rules share the same
                // Can launch in the existing secondary container if the rules share the same
                // presentation.
                // presentation.
                return splitContainer.getSecondaryContainer();
                return splitContainer.getSecondaryContainer();
            }
            }
        }
        // Create a new TaskFragment to split with the primary activity for the new activity.
        // Create a new TaskFragment to split with the primary activity for the new activity.
        return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, intent,
        return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, intent,
                splitRule);
                splitRule);
@@ -1117,8 +1133,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen


        // Check if there is enough space for launch
        // Check if there is enough space for launch
        final SplitPlaceholderRule placeholderRule = getPlaceholderRule(activity);
        final SplitPlaceholderRule placeholderRule = getPlaceholderRule(activity);
        if (placeholderRule == null || !mPresenter.shouldShowSideBySide(

                mPresenter.getParentContainerBounds(activity), placeholderRule)) {
        if (placeholderRule == null) {
            return false;
        }

        final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(activity,
                placeholderRule.getPlaceholderIntent());
        if (!shouldShowSideBySide(
                mPresenter.getParentContainerBounds(activity), placeholderRule,
                minDimensionsPair)) {
            return false;
            return false;
        }
        }


@@ -1161,7 +1185,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
            return false;
            return false;
        }
        }


        if (mPresenter.shouldShowSideBySide(splitContainer)) {
        if (shouldShowSideBySide(splitContainer)) {
            return false;
            return false;
        }
        }


@@ -1233,7 +1257,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
                        // Splits that are not showing side-by-side are reported as having 0 split
                        // Splits that are not showing side-by-side are reported as having 0 split
                        // ratio, since by definition in the API the primary container occupies no
                        // ratio, since by definition in the API the primary container occupies no
                        // width of the split when covered by the secondary.
                        // width of the split when covered by the secondary.
                        mPresenter.shouldShowSideBySide(container)
                        shouldShowSideBySide(container)
                                ? container.getSplitRule().getSplitRatio()
                                ? container.getSplitRule().getSplitRatio()
                                : 0.0f);
                                : 0.0f);
                splitStates.add(splitState);
                splitStates.add(splitState);
@@ -1402,7 +1426,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
        }
        }
        // Decide whether the associated container should be retained based on the current
        // Decide whether the associated container should be retained based on the current
        // presentation mode.
        // presentation mode.
        if (mPresenter.shouldShowSideBySide(splitContainer)) {
        if (shouldShowSideBySide(splitContainer)) {
            return !shouldFinishAssociatedContainerWhenAdjacent(finishBehavior);
            return !shouldFinishAssociatedContainerWhenAdjacent(finishBehavior);
        } else {
        } else {
            return !shouldFinishAssociatedContainerWhenStacked(finishBehavior);
            return !shouldFinishAssociatedContainerWhenStacked(finishBehavior);
Loading