diff options
| author | 2022-05-21 05:26:06 +0800 | |
|---|---|---|
| committer | 2022-05-31 23:25:18 +0800 | |
| commit | 482fbfabd7b9ad9e6ae5520234386ece97b29910 (patch) | |
| tree | b9cbf29d0fc00a3a4ce5ee5bec8bd037d0c7c3b3 | |
| parent | 75cec0e82550630e01f74002e603e5ff20eb070d (diff) | |
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
23 files changed, 926 insertions, 248 deletions
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index ae16e01b7788..7141259d7dce 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -8260,6 +8260,11 @@ public class Activity extends ContextThemeWrapper return mMainThread; } + /** @hide */ + public final ActivityInfo getActivityInfo() { + return mActivityInfo; + } + final void performCreate(Bundle icicle) { performCreate(icicle, null); } diff --git a/core/java/android/window/TaskFragmentInfo.java b/core/java/android/window/TaskFragmentInfo.java index f72164e1f53f..56e910769cb5 100644 --- a/core/java/android/window/TaskFragmentInfo.java +++ b/core/java/android/window/TaskFragmentInfo.java @@ -23,8 +23,10 @@ import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; +import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Point; +import android.graphics.Rect; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; @@ -66,7 +68,7 @@ public final class TaskFragmentInfo implements Parcelable { private final List<IBinder> mActivities = new ArrayList<>(); /** 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 @@ -80,21 +82,31 @@ public final class TaskFragmentInfo implements Parcelable { */ 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 */ public TaskFragmentInfo( @NonNull IBinder fragmentToken, @NonNull WindowContainerToken token, @NonNull Configuration configuration, int runningActivityCount, boolean isVisible, @NonNull List<IBinder> activities, @NonNull Point positionInParent, - boolean isTaskClearedForReuse, boolean isTaskFragmentClearedForPip) { + boolean isTaskClearedForReuse, boolean isTaskFragmentClearedForPip, + @NonNull Point minimumDimensions) { mFragmentToken = requireNonNull(fragmentToken); mToken = requireNonNull(token); mConfiguration.setTo(configuration); mRunningActivityCount = runningActivityCount; mIsVisible = isVisible; mActivities.addAll(activities); - mPositionInParent = requireNonNull(positionInParent); + mPositionInParent.set(positionInParent); mIsTaskClearedForReuse = isTaskClearedForReuse; mIsTaskFragmentClearedForPip = isTaskFragmentClearedForPip; + mMinimumDimensions.set(minimumDimensions); } @NonNull @@ -154,6 +166,26 @@ public final class TaskFragmentInfo implements Parcelable { } /** + * 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 * equal between this {@link TaskFragmentInfo} and {@param that}. */ @@ -170,7 +202,8 @@ public final class TaskFragmentInfo implements Parcelable { && mActivities.equals(that.mActivities) && mPositionInParent.equals(that.mPositionInParent) && mIsTaskClearedForReuse == that.mIsTaskClearedForReuse - && mIsTaskFragmentClearedForPip == that.mIsTaskFragmentClearedForPip; + && mIsTaskFragmentClearedForPip == that.mIsTaskFragmentClearedForPip + && mMinimumDimensions.equals(that.mMinimumDimensions); } private TaskFragmentInfo(Parcel in) { @@ -180,9 +213,10 @@ public final class TaskFragmentInfo implements Parcelable { mRunningActivityCount = in.readInt(); mIsVisible = in.readBoolean(); in.readBinderList(mActivities); - mPositionInParent = requireNonNull(in.readTypedObject(Point.CREATOR)); + mPositionInParent.readFromParcel(in); mIsTaskClearedForReuse = in.readBoolean(); mIsTaskFragmentClearedForPip = in.readBoolean(); + mMinimumDimensions.readFromParcel(in); } /** @hide */ @@ -194,9 +228,10 @@ public final class TaskFragmentInfo implements Parcelable { dest.writeInt(mRunningActivityCount); dest.writeBoolean(mIsVisible); dest.writeBinderList(mActivities); - dest.writeTypedObject(mPositionInParent, flags); + mPositionInParent.writeToParcel(dest, flags); dest.writeBoolean(mIsTaskClearedForReuse); dest.writeBoolean(mIsTaskFragmentClearedForPip); + mMinimumDimensions.writeToParcel(dest, flags); } @NonNull @@ -224,6 +259,7 @@ public final class TaskFragmentInfo implements Parcelable { + " positionInParent=" + mPositionInParent + " isTaskClearedForReuse=" + mIsTaskClearedForReuse + " isTaskFragmentClearedForPip" + mIsTaskFragmentClearedForPip + + " minimumDimensions" + mMinimumDimensions + "}"; } diff --git a/graphics/java/android/graphics/Point.java b/graphics/java/android/graphics/Point.java index 25f76f6f6da7..2781ac4bf1da 100644 --- a/graphics/java/android/graphics/Point.java +++ b/graphics/java/android/graphics/Point.java @@ -36,8 +36,7 @@ public class Point implements Parcelable { } public Point(@NonNull Point src) { - this.x = src.x; - this.y = src.y; + set(src); } /** @@ -49,6 +48,15 @@ public class Point implements Parcelable { } /** + * 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 */ public final void negate() { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java index 44af1a9fd780..f09a91018bf0 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java @@ -18,6 +18,8 @@ package androidx.window.extensions.embedding; import android.annotation.NonNull; import android.app.Activity; +import android.util.Pair; +import android.util.Size; /** * Client-side descriptor of a split that holds two containers. @@ -66,6 +68,13 @@ class SplitContainer { 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() { return (mSplitRule instanceof SplitPlaceholderRule); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 575c3f002791..242e9ab6beee 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -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.shouldFinishAssociatedContainerWhenAdjacent; 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.ActivityClient; import android.app.ActivityOptions; @@ -43,11 +45,15 @@ import android.os.IBinder; import android.os.Looper; import android.util.ArraySet; import android.util.Log; +import android.util.Pair; +import android.util.Size; import android.util.SparseArray; import android.window.TaskFragmentInfo; import android.window.WindowContainerTransaction; import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.window.common.EmptyLifecycleCallbacksAdapter; import com.android.internal.annotations.VisibleForTesting; @@ -63,7 +69,7 @@ import java.util.function.Consumer; */ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback, ActivityEmbeddingComponent { - private static final String TAG = "SplitController"; + static final String TAG = "SplitController"; @VisibleForTesting @GuardedBy("mLock") @@ -350,7 +356,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (!(rule instanceof SplitRule)) { continue; } - if (mPresenter.shouldShowSideBySide(taskContainer.getTaskBounds(), (SplitRule) rule)) { + if (shouldShowSideBySide(taskContainer.getTaskBounds(), (SplitRule) rule)) { return true; } } @@ -610,11 +616,15 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen primaryActivity); final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer); 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 // presentation. 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. return true; } @@ -791,9 +801,15 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen && (canReuseContainer(splitRule, splitContainer.getSplitRule()) // TODO(b/231845476) we should always respect clearTop. || !respectClearTop)) { - // Can launch in the existing secondary container if the rules share the same - // presentation. - return splitContainer.getSecondaryContainer(); + 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 + // presentation. + return splitContainer.getSecondaryContainer(); + } } // Create a new TaskFragment to split with the primary activity for the new activity. return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, intent, @@ -1117,8 +1133,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Check if there is enough space for launch 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; } @@ -1161,7 +1185,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } - if (mPresenter.shouldShowSideBySide(splitContainer)) { + if (shouldShowSideBySide(splitContainer)) { 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 // ratio, since by definition in the API the primary container occupies no // width of the split when covered by the secondary. - mPresenter.shouldShowSideBySide(container) + shouldShowSideBySide(container) ? container.getSplitRule().getSplitRatio() : 0.0f); 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 // presentation mode. - if (mPresenter.shouldShowSideBySide(splitContainer)) { + if (shouldShowSideBySide(splitContainer)) { return !shouldFinishAssociatedContainerWhenAdjacent(finishBehavior); } else { return !shouldFinishAssociatedContainerWhenStacked(finishBehavior); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index ac3b05a0e825..1b79ad999435 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -16,15 +16,23 @@ package androidx.window.extensions.embedding; +import static android.content.pm.PackageManager.MATCH_ALL; + import android.app.Activity; +import android.app.ActivityThread; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; import android.util.LayoutDirection; +import android.util.Pair; +import android.util.Size; import android.view.View; import android.view.WindowInsets; import android.view.WindowMetrics; @@ -34,6 +42,8 @@ import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; + import java.util.concurrent.Executor; /** @@ -41,9 +51,12 @@ import java.util.concurrent.Executor; * {@link SplitController}. */ class SplitPresenter extends JetpackTaskFragmentOrganizer { - private static final int POSITION_START = 0; - private static final int POSITION_END = 1; - private static final int POSITION_FILL = 2; + @VisibleForTesting + static final int POSITION_START = 0; + @VisibleForTesting + static final int POSITION_END = 1; + @VisibleForTesting + static final int POSITION_FILL = 2; @IntDef(value = { POSITION_START, @@ -103,8 +116,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @NonNull Intent secondaryIntent, @NonNull SplitPairRule rule) { final Rect parentBounds = getParentContainerBounds(primaryActivity); + final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair( + primaryActivity, secondaryIntent); final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - isLtr(primaryActivity, rule)); + primaryActivity, minDimensionsPair); final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, primaryActivity, primaryRectBounds, null); @@ -113,7 +128,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final TaskFragmentContainer secondaryContainer = mController.newContainer( secondaryIntent, primaryActivity, taskId); final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, - rule, isLtr(primaryActivity, rule)); + rule, primaryActivity, minDimensionsPair); final int windowingMode = mController.getTaskContainer(taskId) .getWindowingModeForSplitTaskFragment(secondaryRectBounds); createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(), @@ -121,7 +136,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { windowingMode); // Set adjacent to each other so that the containers below will be invisible. - setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule); + setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, + minDimensionsPair); mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); @@ -144,13 +160,15 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final WindowContainerTransaction wct = new WindowContainerTransaction(); final Rect parentBounds = getParentContainerBounds(primaryActivity); + final Pair<Size, Size> minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, + secondaryActivity); final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - isLtr(primaryActivity, rule)); + primaryActivity, minDimensionsPair); final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, primaryActivity, primaryRectBounds, null); final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - isLtr(primaryActivity, rule)); + primaryActivity, minDimensionsPair); final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity( secondaryActivity); TaskFragmentContainer containerToAvoid = primaryContainer; @@ -162,7 +180,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { secondaryActivity, secondaryRectBounds, containerToAvoid); // Set adjacent to each other so that the containers below will be invisible. - setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule); + setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, + minDimensionsPair); mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); @@ -211,10 +230,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent, @Nullable Bundle activityOptions, @NonNull SplitRule rule, boolean isPlaceholder) { final Rect parentBounds = getParentContainerBounds(launchingActivity); + final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair( + launchingActivity, activityIntent); final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - isLtr(launchingActivity, rule)); + launchingActivity, minDimensionsPair); final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - isLtr(launchingActivity, rule)); + launchingActivity, minDimensionsPair); TaskFragmentContainer primaryContainer = mController.getContainerWithActivity( launchingActivity); @@ -258,11 +279,11 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { if (activity == null) { return; } - final boolean isLtr = isLtr(activity, rule); + final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair(); final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - isLtr); + activity, minDimensionsPair); final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - isLtr); + activity, minDimensionsPair); final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); // Whether the placeholder is becoming side-by-side with the primary from fullscreen. final boolean isPlaceholderBecomingSplit = splitContainer.isPlaceholderContainer() @@ -273,7 +294,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // are created again. resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds); resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds); - setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule); + setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, + minDimensionsPair); if (isPlaceholderBecomingSplit) { // When placeholder is shown in split, we should keep the focus on the primary. wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken()); @@ -287,11 +309,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer, - @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule) { + @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule, + @NonNull Pair<Size, Size> minDimensionsPair) { final Rect parentBounds = getParentContainerBounds(primaryContainer); // Clear adjacent TaskFragments if the container is shown in fullscreen, or the // secondaryContainer could not be finished. - if (!shouldShowSideBySide(parentBounds, splitRule)) { + if (!shouldShowSideBySide(parentBounds, splitRule, minDimensionsPair)) { setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(), null /* secondary */, null /* splitRule */); } else { @@ -373,41 +396,132 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { super.updateWindowingMode(wct, fragmentToken, windowingMode); } - boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) { + static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule) { + return shouldShowSideBySide(parentBounds, rule, null /* minimumDimensionPair */); + } + + static boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) { final Rect parentBounds = getParentContainerBounds(splitContainer.getPrimaryContainer()); - return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule()); + + return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule(), + splitContainer.getMinDimensionsPair()); } - boolean shouldShowSideBySide(@Nullable Rect parentBounds, @NonNull SplitRule rule) { + static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule, + @Nullable Pair<Size, Size> minDimensionsPair) { // TODO(b/190433398): Supply correct insets. final WindowMetrics parentMetrics = new WindowMetrics(parentBounds, new WindowInsets(new Rect())); - return rule.checkParentMetrics(parentMetrics); + // Don't show side by side if bounds is not qualified. + if (!rule.checkParentMetrics(parentMetrics)) { + return false; + } + final float splitRatio = rule.getSplitRatio(); + // We only care the size of the bounds regardless of its position. + final Rect primaryBounds = getPrimaryBounds(parentBounds, splitRatio, true /* isLtr */); + final Rect secondaryBounds = getSecondaryBounds(parentBounds, splitRatio, true /* isLtr */); + + if (minDimensionsPair == null) { + return true; + } + return !boundsSmallerThanMinDimensions(primaryBounds, minDimensionsPair.first) + && !boundsSmallerThanMinDimensions(secondaryBounds, minDimensionsPair.second); } @NonNull - private Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds, - @NonNull SplitRule rule, boolean isLtr) { - if (!shouldShowSideBySide(parentBounds, rule)) { - return new Rect(); + static Pair<Size, Size> getActivitiesMinDimensionsPair(Activity primaryActivity, + Activity secondaryActivity) { + return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryActivity)); + } + + @NonNull + static Pair<Size, Size> getActivityIntentMinDimensionsPair(Activity primaryActivity, + Intent secondaryIntent) { + return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryIntent)); + } + + @Nullable + static Size getMinDimensions(@Nullable Activity activity) { + if (activity == null) { + return null; + } + final ActivityInfo.WindowLayout windowLayout = activity.getActivityInfo().windowLayout; + if (windowLayout == null) { + return null; } + return new Size(windowLayout.minWidth, windowLayout.minHeight); + } + // TODO(b/232871351): find a light-weight approach for this check. + @Nullable + static Size getMinDimensions(@Nullable Intent intent) { + if (intent == null) { + return null; + } + final PackageManager packageManager = ActivityThread.currentActivityThread() + .getApplication().getPackageManager(); + final ResolveInfo resolveInfo = packageManager.resolveActivity(intent, + PackageManager.ResolveInfoFlags.of(MATCH_ALL)); + if (resolveInfo == null) { + return null; + } + final ActivityInfo activityInfo = resolveInfo.activityInfo; + if (activityInfo == null) { + return null; + } + final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout; + if (windowLayout == null) { + return null; + } + return new Size(windowLayout.minWidth, windowLayout.minHeight); + } + + static boolean boundsSmallerThanMinDimensions(@NonNull Rect bounds, + @Nullable Size minDimensions) { + if (minDimensions == null) { + return false; + } + return bounds.width() < minDimensions.getWidth() + || bounds.height() < minDimensions.getHeight(); + } + + @VisibleForTesting + @NonNull + static Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds, + @NonNull SplitRule rule, @NonNull Activity primaryActivity, + @Nullable Pair<Size, Size> minDimensionsPair) { + if (!shouldShowSideBySide(parentBounds, rule, minDimensionsPair)) { + return new Rect(); + } + final boolean isLtr = isLtr(primaryActivity, rule); final float splitRatio = rule.getSplitRatio(); - final float rtlSplitRatio = 1 - splitRatio; + switch (position) { case POSITION_START: - return isLtr ? getLeftContainerBounds(parentBounds, splitRatio) - : getRightContainerBounds(parentBounds, rtlSplitRatio); + return getPrimaryBounds(parentBounds, splitRatio, isLtr); case POSITION_END: - return isLtr ? getRightContainerBounds(parentBounds, splitRatio) - : getLeftContainerBounds(parentBounds, rtlSplitRatio); + return getSecondaryBounds(parentBounds, splitRatio, isLtr); case POSITION_FILL: - return parentBounds; + default: + return new Rect(); } - return parentBounds; } - private Rect getLeftContainerBounds(@NonNull Rect parentBounds, float splitRatio) { + @NonNull + private static Rect getPrimaryBounds(@NonNull Rect parentBounds, float splitRatio, + boolean isLtr) { + return isLtr ? getLeftContainerBounds(parentBounds, splitRatio) + : getRightContainerBounds(parentBounds, 1 - splitRatio); + } + + @NonNull + private static Rect getSecondaryBounds(@NonNull Rect parentBounds, float splitRatio, + boolean isLtr) { + return isLtr ? getRightContainerBounds(parentBounds, splitRatio) + : getLeftContainerBounds(parentBounds, 1 - splitRatio); + } + + private static Rect getLeftContainerBounds(@NonNull Rect parentBounds, float splitRatio) { return new Rect( parentBounds.left, parentBounds.top, @@ -415,7 +529,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { parentBounds.bottom); } - private Rect getRightContainerBounds(@NonNull Rect parentBounds, float splitRatio) { + private static Rect getRightContainerBounds(@NonNull Rect parentBounds, float splitRatio) { return new Rect( (int) (parentBounds.left + parentBounds.width() * splitRatio), parentBounds.top, @@ -427,7 +541,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * Checks if a split with the provided rule should be displays in left-to-right layout * direction, either always or with the current configuration. */ - private boolean isLtr(@NonNull Context context, @NonNull SplitRule rule) { + private static boolean isLtr(@NonNull Context context, @NonNull SplitRule rule) { switch (rule.getLayoutDirection()) { case LayoutDirection.LOCALE: return context.getResources().getConfiguration().getLayoutDirection() @@ -441,7 +555,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } @NonNull - Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) { + static Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) { return container.getTaskContainer().getTaskBounds(); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index 624cde50ff72..abf32a26efa2 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -26,6 +26,7 @@ import android.content.Intent; import android.graphics.Rect; import android.os.Binder; import android.os.IBinder; +import android.util.Size; import android.window.TaskFragmentInfo; import android.window.WindowContainerTransaction; @@ -414,6 +415,11 @@ class TaskFragmentContainer { } } + @NonNull + Rect getLastRequestedBounds() { + return mLastRequestedBounds; + } + /** * Checks if last requested windowing mode is equal to the provided value. */ @@ -439,6 +445,31 @@ class TaskFragmentContainer { return mTaskContainer; } + @Nullable + Size getMinDimensions() { + if (mInfo == null) { + return null; + } + int maxMinWidth = mInfo.getMinimumWidth(); + int maxMinHeight = mInfo.getMinimumHeight(); + for (Activity activity : mPendingAppearedActivities) { + final Size minDimensions = SplitPresenter.getMinDimensions(activity); + if (minDimensions == null) { + continue; + } + maxMinWidth = Math.max(maxMinWidth, minDimensions.getWidth()); + maxMinHeight = Math.max(maxMinHeight, minDimensions.getHeight()); + } + if (mPendingAppearedIntent != null) { + final Size minDimensions = SplitPresenter.getMinDimensions(mPendingAppearedIntent); + if (minDimensions != null) { + maxMinWidth = Math.max(maxMinWidth, minDimensions.getWidth()); + maxMinHeight = Math.max(maxMinHeight, minDimensions.getHeight()); + } + } + return new Size(maxMinWidth, maxMinHeight); + } + @Override public String toString() { return toString(true /* includeContainersToFinishOnExit */); diff --git a/libs/WindowManager/Jetpack/tests/unittest/AndroidManifest.xml b/libs/WindowManager/Jetpack/tests/unittest/AndroidManifest.xml index b12b6f6f0ef1..c736e9ed971e 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/AndroidManifest.xml +++ b/libs/WindowManager/Jetpack/tests/unittest/AndroidManifest.xml @@ -22,6 +22,11 @@ <application android:debuggable="true" android:largeHeap="true"> <uses-library android:name="android.test.mock" /> <uses-library android:name="android.test.runner" /> + + <activity android:name="androidx.window.extensions.embedding.MinimumDimensionActivity"> + <layout android:minWidth="600px" + android:minHeight="1200px"/> + </activity> </application> <instrumentation diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java new file mode 100644 index 000000000000..835c40365cda --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2022 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 androidx.window.extensions.embedding; + +import static androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS; +import static androidx.window.extensions.embedding.SplitRule.FINISH_NEVER; + +import static org.mockito.Mockito.mock; + +import android.annotation.NonNull; +import android.app.Activity; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.Pair; +import android.window.TaskFragmentInfo; +import android.window.WindowContainerToken; + +import java.util.Collections; + +public class EmbeddingTestUtils { + static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200); + static final int TASK_ID = 10; + static final float SPLIT_RATIO = 0.5f; + /** Default finish behavior in Jetpack. */ + static final int DEFAULT_FINISH_PRIMARY_WITH_SECONDARY = FINISH_NEVER; + static final int DEFAULT_FINISH_SECONDARY_WITH_PRIMARY = FINISH_ALWAYS; + + private EmbeddingTestUtils() {} + + /** Gets the bounds of a TaskFragment that is in split. */ + static Rect getSplitBounds(boolean isPrimary) { + final int width = (int) (TASK_BOUNDS.width() * SPLIT_RATIO); + return isPrimary + ? new Rect(TASK_BOUNDS.left, TASK_BOUNDS.top, TASK_BOUNDS.left + width, + TASK_BOUNDS.bottom) + : new Rect( + TASK_BOUNDS.left + width, TASK_BOUNDS.top, TASK_BOUNDS.right, + TASK_BOUNDS.bottom); + } + + /** Creates a rule to always split the given activity and the given intent. */ + static SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent) { + final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent); + return new SplitPairRule.Builder( + activityPair -> false, + targetPair::equals, + w -> true) + .setSplitRatio(SPLIT_RATIO) + .setShouldClearTop(true) + .build(); + } + + /** Creates a rule to always split the given activities. */ + static SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + return createSplitRule(primaryActivity, secondaryActivity, + DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY, + true /* clearTop */); + } + + /** Creates a rule to always split the given activities with the given finish behaviors. */ + static SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, int finishPrimaryWithSecondary, + int finishSecondaryWithPrimary, boolean clearTop) { + final Pair<Activity, Activity> targetPair = new Pair<>(primaryActivity, secondaryActivity); + return new SplitPairRule.Builder( + targetPair::equals, + activityIntentPair -> false, + w -> true) + .setSplitRatio(SPLIT_RATIO) + .setFinishPrimaryWithSecondary(finishPrimaryWithSecondary) + .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary) + .setShouldClearTop(clearTop) + .build(); + } + + /** Creates a mock TaskFragmentInfo for the given TaskFragment. */ + static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container, + @NonNull Activity activity) { + return new TaskFragmentInfo(container.getTaskFragmentToken(), + mock(WindowContainerToken.class), + new Configuration(), + 1, + true /* isVisible */, + Collections.singletonList(activity.getActivityToken()), + new Point(), + false /* isTaskClearedForReuse */, + false /* isTaskFragmentClearedForPip */, + new Point()); + } +} diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java index a191e685f651..4d2595275f20 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java @@ -18,6 +18,8 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -58,8 +60,6 @@ import java.util.ArrayList; @SmallTest @RunWith(AndroidJUnit4.class) public class JetpackTaskFragmentOrganizerTest { - private static final int TASK_ID = 10; - @Mock private WindowContainerTransaction mTransaction; @Mock @@ -131,6 +131,7 @@ public class JetpackTaskFragmentOrganizerTest { return new TaskFragmentInfo(container.getTaskFragmentToken(), mock(WindowContainerToken.class), new Configuration(), 0 /* runningActivityCount */, false /* isVisible */, new ArrayList<>(), new Point(), - false /* isTaskClearedForReuse */, false /* isTaskFragmentClearedForPip */); + false /* isTaskClearedForReuse */, false /* isTaskFragmentClearedForPip */, + new Point()); } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/MinimumDimensionActivity.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/MinimumDimensionActivity.java new file mode 100644 index 000000000000..ffcaf3e6f546 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/MinimumDimensionActivity.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 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 androidx.window.extensions.embedding; + +import android.app.Activity; + +/** + * Activity that declares minWidth and minHeight in + * {@link android.content.pm.ActivityInfo.WindowLayout} + */ +public class MinimumDimensionActivity extends Activity {} diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 60390eb2b3d2..ef7728cec387 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -19,8 +19,13 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_RATIO; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; import static androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS; -import static androidx.window.extensions.embedding.SplitRule.FINISH_NEVER; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -49,20 +54,19 @@ import android.app.Activity; import android.app.ActivityOptions; import android.content.ComponentName; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.Point; import android.graphics.Rect; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.platform.test.annotations.Presubmit; -import android.util.Pair; import android.window.TaskFragmentInfo; -import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -86,16 +90,9 @@ import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) public class SplitControllerTest { - private static final int TASK_ID = 10; - private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200); - private static final float SPLIT_RATIO = 0.5f; private static final Intent PLACEHOLDER_INTENT = new Intent().setComponent( new ComponentName("test", "placeholder")); - /** Default finish behavior in Jetpack. */ - private static final int DEFAULT_FINISH_PRIMARY_WITH_SECONDARY = FINISH_NEVER; - private static final int DEFAULT_FINISH_SECONDARY_WITH_PRIMARY = FINISH_ALWAYS; - private Activity mActivity; @Mock private Resources mActivityResources; @@ -420,6 +417,25 @@ public class SplitControllerTest { } @Test + public void testResolveStartActivityIntent_shouldLaunchInFullscreen() { + final Intent intent = new Intent().setComponent( + new ComponentName(ApplicationProvider.getApplicationContext(), + MinimumDimensionActivity.class)); + setupSplitRule(mActivity, intent); + final Activity primaryActivity = createMockActivity(); + addSplitTaskFragments(primaryActivity, mActivity); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, null /* launchingActivity */); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container)); + assertTrue(primaryContainer.areLastRequestedBoundsEqual(null)); + assertTrue(container.areLastRequestedBoundsEqual(null)); + } + + @Test public void testPlaceActivityInTopContainer() { mSplitController.placeActivityInTopContainer(mActivity); @@ -767,6 +783,52 @@ public class SplitControllerTest { } @Test + public void testResolveActivityToContainer_primaryActivityMinDimensionsNotSatisfied() { + final Activity activityBelow = createMockActivity(); + setupSplitRule(mActivity, activityBelow); + + ActivityInfo aInfo = new ActivityInfo(); + final Rect primaryBounds = getSplitBounds(true /* isPrimary */); + aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, + primaryBounds.width() + 1, primaryBounds.height() + 1); + doReturn(aInfo).when(mActivity).getActivityInfo(); + + final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, + TASK_ID); + container.addPendingAppearedActivity(mActivity); + + // Allow to split as primary. + boolean result = mSplitController.resolveActivityToContainer(mActivity, + true /* isOnReparent */); + + assertTrue(result); + assertSplitPair(mActivity, activityBelow, true /* matchParentBounds */); + } + + @Test + public void testResolveActivityToContainer_secondaryActivityMinDimensionsNotSatisfied() { + final Activity activityBelow = createMockActivity(); + setupSplitRule(activityBelow, mActivity); + + ActivityInfo aInfo = new ActivityInfo(); + final Rect secondaryBounds = getSplitBounds(false /* isPrimary */); + aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, + secondaryBounds.width() + 1, secondaryBounds.height() + 1); + doReturn(aInfo).when(mActivity).getActivityInfo(); + + final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, + TASK_ID); + container.addPendingAppearedActivity(mActivity); + + // Allow to split as primary. + boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + assertSplitPair(activityBelow, mActivity, true /* matchParentBounds */); + } + + @Test public void testResolveActivityToContainer_inUnknownTaskFragment() { doReturn(new Binder()).when(mSplitController).getInitialTaskFragmentToken(mActivity); @@ -835,23 +897,10 @@ public class SplitControllerTest { doReturn(activityToken).when(activity).getActivityToken(); doReturn(activity).when(mSplitController).getActivity(activityToken); doReturn(TASK_ID).when(activity).getTaskId(); + doReturn(new ActivityInfo()).when(activity).getActivityInfo(); return activity; } - /** Creates a mock TaskFragmentInfo for the given TaskFragment. */ - private TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container, - @NonNull Activity activity) { - return new TaskFragmentInfo(container.getTaskFragmentToken(), - mock(WindowContainerToken.class), - new Configuration(), - 1, - true /* isVisible */, - Collections.singletonList(activity.getActivityToken()), - new Point(), - false /* isTaskClearedForReuse */, - false /* isTaskFragmentClearedForPip */); - } - /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) { final TaskFragmentContainer container = mSplitController.newContainer(activity, TASK_ID); @@ -902,49 +951,10 @@ public class SplitControllerTest { /** Setups a rule to always split the given activities. */ private void setupSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { - final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity, - DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY, - true /* clearTop */); + final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity); mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); } - /** Creates a rule to always split the given activity and the given intent. */ - private SplitRule createSplitRule(@NonNull Activity primaryActivity, - @NonNull Intent secondaryIntent) { - final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent); - return new SplitPairRule.Builder( - activityPair -> false, - targetPair::equals, - w -> true) - .setSplitRatio(SPLIT_RATIO) - .setShouldClearTop(true) - .build(); - } - - /** Creates a rule to always split the given activities. */ - private SplitRule createSplitRule(@NonNull Activity primaryActivity, - @NonNull Activity secondaryActivity) { - return createSplitRule(primaryActivity, secondaryActivity, - DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY, - true /* clearTop */); - } - - /** Creates a rule to always split the given activities with the given finish behaviors. */ - private SplitRule createSplitRule(@NonNull Activity primaryActivity, - @NonNull Activity secondaryActivity, int finishPrimaryWithSecondary, - int finishSecondaryWithPrimary, boolean clearTop) { - final Pair<Activity, Activity> targetPair = new Pair<>(primaryActivity, secondaryActivity); - return new SplitPairRule.Builder( - targetPair::equals, - activityIntentPair -> false, - w -> true) - .setSplitRatio(SPLIT_RATIO) - .setFinishPrimaryWithSecondary(finishPrimaryWithSecondary) - .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary) - .setShouldClearTop(clearTop) - .build(); - } - /** Adds a pair of TaskFragments as split for the given activities. */ private void addSplitTaskFragments(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { @@ -973,39 +983,42 @@ public class SplitControllerTest { secondaryContainer.setLastRequestedBounds(getSplitBounds(false /* isPrimary */)); } - /** Gets the bounds of a TaskFragment that is in split. */ - private Rect getSplitBounds(boolean isPrimary) { - final int width = (int) (TASK_BOUNDS.width() * SPLIT_RATIO); - return isPrimary - ? new Rect(TASK_BOUNDS.left, TASK_BOUNDS.top, TASK_BOUNDS.left + width, - TASK_BOUNDS.bottom) - : new Rect(TASK_BOUNDS.left + width, TASK_BOUNDS.top, TASK_BOUNDS.right, - TASK_BOUNDS.bottom); + /** Asserts that the two given activities are in split. */ + private void assertSplitPair(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + assertSplitPair(primaryActivity, secondaryActivity, false /* matchParentBounds */); } /** Asserts that the two given activities are in split. */ private void assertSplitPair(@NonNull Activity primaryActivity, - @NonNull Activity secondaryActivity) { + @NonNull Activity secondaryActivity, boolean matchParentBounds) { assertSplitPair(mSplitController.getContainerWithActivity(primaryActivity), - mSplitController.getContainerWithActivity(secondaryActivity)); + mSplitController.getContainerWithActivity(secondaryActivity), matchParentBounds); } - /** Asserts that the two given TaskFragments are in split. */ private void assertSplitPair(@NonNull TaskFragmentContainer primaryContainer, @NonNull TaskFragmentContainer secondaryContainer) { + assertSplitPair(primaryContainer, secondaryContainer, false /* matchParentBounds*/); + } + + /** Asserts that the two given TaskFragments are in split. */ + private void assertSplitPair(@NonNull TaskFragmentContainer primaryContainer, + @NonNull TaskFragmentContainer secondaryContainer, boolean matchParentBounds) { assertNotNull(primaryContainer); assertNotNull(secondaryContainer); assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, secondaryContainer)); if (primaryContainer.mInfo != null) { - assertTrue(primaryContainer.areLastRequestedBoundsEqual( - getSplitBounds(true /* isPrimary */))); + final Rect primaryBounds = matchParentBounds ? new Rect() + : getSplitBounds(true /* isPrimary */); + assertTrue(primaryContainer.areLastRequestedBoundsEqual(primaryBounds)); assertTrue(primaryContainer.isLastRequestedWindowingModeEqual( WINDOWING_MODE_MULTI_WINDOW)); } if (secondaryContainer.mInfo != null) { - assertTrue(secondaryContainer.areLastRequestedBoundsEqual( - getSplitBounds(false /* isPrimary */))); + final Rect secondaryBounds = matchParentBounds ? new Rect() + : getSplitBounds(false /* isPrimary */); + assertTrue(secondaryContainer.areLastRequestedBoundsEqual(secondaryBounds)); assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual( WINDOWING_MODE_MULTI_WINDOW)); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java index 906e9904566f..acc398a27baf 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java @@ -18,25 +18,44 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; +import static androidx.window.extensions.embedding.SplitPresenter.POSITION_END; +import static androidx.window.extensions.embedding.SplitPresenter.POSITION_FILL; +import static androidx.window.extensions.embedding.SplitPresenter.POSITION_START; +import static androidx.window.extensions.embedding.SplitPresenter.getBoundsForPosition; +import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions; +import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import android.app.Activity; +import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; +import android.util.Pair; +import android.util.Size; import android.window.TaskFragmentInfo; import android.window.WindowContainerTransaction; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -56,8 +75,6 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public class SplitPresenterTest { - private static final int TASK_ID = 10; - private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200); @Mock private Activity mActivity; @@ -77,11 +94,7 @@ public class SplitPresenterTest { mPresenter = mController.mPresenter; spyOn(mController); spyOn(mPresenter); - final Configuration activityConfig = new Configuration(); - activityConfig.windowConfiguration.setBounds(TASK_BOUNDS); - activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS); - doReturn(mActivityResources).when(mActivity).getResources(); - doReturn(activityConfig).when(mActivityResources).getConfiguration(); + mActivity = createMockActivity(); } @Test @@ -129,4 +142,67 @@ public class SplitPresenterTest { verify(mTransaction, never()).setWindowingMode(any(), anyInt()); } + + @Test + public void testGetMinDimensionsForIntent() { + final Intent intent = new Intent(ApplicationProvider.getApplicationContext(), + MinimumDimensionActivity.class); + assertEquals(new Size(600, 1200), getMinDimensions(intent)); + } + + @Test + public void testShouldShowSideBySide() { + Activity secondaryActivity = createMockActivity(); + final SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); + + assertTrue(shouldShowSideBySide(TASK_BOUNDS, splitRule)); + + // Set minDimensions of primary container to larger than primary bounds. + final Rect primaryBounds = getSplitBounds(true /* isPrimary */); + Pair<Size, Size> minDimensionsPair = new Pair<>( + new Size(primaryBounds.width() + 1, primaryBounds.height() + 1), null); + + assertFalse(shouldShowSideBySide(TASK_BOUNDS, splitRule, minDimensionsPair)); + } + + @Test + public void testGetBoundsForPosition() { + Activity secondaryActivity = createMockActivity(); + final SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); + final Rect primaryBounds = getSplitBounds(true /* isPrimary */); + final Rect secondaryBounds = getSplitBounds(false /* isPrimary */); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + getBoundsForPosition(POSITION_START, TASK_BOUNDS, splitRule, + mActivity, null /* miniDimensionsPair */)); + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + getBoundsForPosition(POSITION_END, TASK_BOUNDS, splitRule, + mActivity, null /* miniDimensionsPair */)); + assertEquals("Task bounds must be reported.", + new Rect(), + getBoundsForPosition(POSITION_FILL, TASK_BOUNDS, splitRule, + mActivity, null /* miniDimensionsPair */)); + + Pair<Size, Size> minDimensionsPair = new Pair<>( + new Size(primaryBounds.width() + 1, primaryBounds.height() + 1), null); + + assertEquals("Fullscreen bounds must be reported because of min dimensions.", + new Rect(), + getBoundsForPosition(POSITION_START, TASK_BOUNDS, + splitRule, mActivity, minDimensionsPair)); + } + + private Activity createMockActivity() { + final Activity activity = mock(Activity.class); + final Configuration activityConfig = new Configuration(); + activityConfig.windowConfiguration.setBounds(TASK_BOUNDS); + activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS); + doReturn(mActivityResources).when(activity).getResources(); + doReturn(activityConfig).when(mActivityResources).getConfiguration(); + doReturn(new ActivityInfo()).when(activity).getActivityInfo(); + return activity; + } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java index ebe202db4e54..dd67e48ef353 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java @@ -22,6 +22,9 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -53,9 +56,6 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public class TaskContainerTest { - private static final int TASK_ID = 10; - private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200); - @Mock private SplitController mController; diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java index af3ad70c04db..d31342bfb309 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java @@ -16,6 +16,8 @@ package androidx.window.extensions.embedding; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.mockito.ArgumentMatchers.anyInt; @@ -43,8 +45,6 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public class TaskFragmentAnimationControllerTest { - private static final int TASK_ID = 10; - @Mock private TaskFragmentOrganizer mOrganizer; private TaskFragmentAnimationController mAnimationController; diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java index fcbd8a3ac020..28c2773e25cb 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -16,6 +16,9 @@ package androidx.window.extensions.embedding; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertEquals; @@ -30,17 +33,13 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import android.annotation.NonNull; import android.app.Activity; import android.content.Intent; -import android.content.res.Configuration; -import android.graphics.Point; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.window.TaskFragmentInfo; -import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -55,7 +54,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -68,8 +66,6 @@ import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) public class TaskFragmentContainerTest { - private static final int TASK_ID = 10; - @Mock private SplitPresenter mPresenter; @Mock @@ -311,18 +307,4 @@ public class TaskFragmentContainerTest { doReturn(activity).when(mController).getActivity(activityToken); return activity; } - - /** Creates a mock TaskFragmentInfo for the given TaskFragment. */ - private TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container, - @NonNull Activity activity) { - return new TaskFragmentInfo(container.getTaskFragmentToken(), - mock(WindowContainerToken.class), - new Configuration(), - 1, - true /* isVisible */, - Collections.singletonList(activity.getActivityToken()), - new Point(), - false /* isTaskClearedForReuse */, - false /* isTaskFragmentClearedForPip */); - } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 4060ec596218..a324042d1457 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -432,6 +432,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A private static final int DESTROY_TIMEOUT = 10 * 1000; final ActivityTaskManagerService mAtmService; + @NonNull final ActivityInfo info; // activity info provided by developer in AndroidManifest // Which user is this running for? final int mUserId; @@ -9706,6 +9707,15 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return true; } + @Nullable + Point getMinDimensions() { + final ActivityInfo.WindowLayout windowLayout = info.windowLayout; + if (windowLayout == null) { + return null; + } + return new Point(windowLayout.minWidth, windowLayout.minHeight); + } + static class Builder { private final ActivityTaskManagerService mAtmService; private WindowProcessController mCallerApp; diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java index 4e2d1aa803af..a78dbd6bbe60 100644 --- a/services/core/java/com/android/server/wm/ActivityStartController.java +++ b/services/core/java/com/android/server/wm/ActivityStartController.java @@ -518,7 +518,8 @@ public class ActivityStartController { */ int startActivityInTaskFragment(@NonNull TaskFragment taskFragment, @NonNull Intent activityIntent, @Nullable Bundle activityOptions, - @Nullable IBinder resultTo, int callingUid, int callingPid) { + @Nullable IBinder resultTo, int callingUid, int callingPid, + @Nullable IBinder errorCallbackToken) { final ActivityRecord caller = resultTo != null ? ActivityRecord.forTokenLocked(resultTo) : null; return obtainStarter(activityIntent, "startActivityInTaskFragment") @@ -531,6 +532,7 @@ public class ActivityStartController { .setRealCallingUid(callingUid) .setRealCallingPid(callingPid) .setUserId(caller != null ? caller.mUserId : mService.getCurrentUserId()) + .setErrorCallbackToken(errorCallbackToken) .execute(); } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index f4aafe096f37..61776d6aa4db 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -89,7 +89,6 @@ import android.app.PendingIntent; import android.app.ProfilerInfo; import android.app.WaitResult; import android.app.WindowConfiguration; -import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.ComponentName; @@ -369,6 +368,13 @@ class ActivityStarter { int filterCallingUid; PendingIntentRecord originatingPendingIntent; boolean allowBackgroundActivityStart; + /** + * The error callback token passed in {@link android.window.WindowContainerTransaction} + * for TaskFragment operation error handling via + * {@link android.window.TaskFragmentOrganizer#onTaskFragmentError(IBinder, Throwable)}. + */ + @Nullable + IBinder errorCallbackToken; /** * If set to {@code true}, allows this activity start to look into @@ -422,6 +428,7 @@ class ActivityStarter { filterCallingUid = UserHandle.USER_NULL; originatingPendingIntent = null; allowBackgroundActivityStart = false; + errorCallbackToken = null; } /** @@ -464,6 +471,7 @@ class ActivityStarter { filterCallingUid = request.filterCallingUid; originatingPendingIntent = request.originatingPendingIntent; allowBackgroundActivityStart = request.allowBackgroundActivityStart; + errorCallbackToken = request.errorCallbackToken; } /** @@ -2926,6 +2934,7 @@ class ActivityStarter { private void addOrReparentStartingActivity(@NonNull Task task, String reason) { TaskFragment newParent = task; if (mInTaskFragment != null) { + // TODO(b/234351413): remove remaining embedded Task logic. // mInTaskFragment is created and added to the leaf task by task fragment organizer's // request. If the task was resolved and different than mInTaskFragment, reparent the // task to mInTaskFragment for embedding. @@ -2947,7 +2956,14 @@ class ActivityStarter { newParent = top.getTaskFragment(); } } - + // Start Activity to the Task if mStartActivity's min dimensions are not satisfied. + if (newParent.isEmbedded() && newParent.smallerThanMinDimension(mStartActivity)) { + reason += " - MinimumDimensionViolation"; + mService.mWindowOrganizerController.sendMinimumDimensionViolation( + newParent, mStartActivity.getMinDimensions(), mRequest.errorCallbackToken, + reason); + newParent = task; + } if (mStartActivity.getTaskFragment() == null || mStartActivity.getTaskFragment() == newParent) { newParent.addChild(mStartActivity, POSITION_TOP); @@ -3232,6 +3248,11 @@ class ActivityStarter { return this; } + ActivityStarter setErrorCallbackToken(@Nullable IBinder errorCallbackToken) { + mRequest.errorCallbackToken = errorCallbackToken; + return this; + } + void dump(PrintWriter pw, String prefix) { pw.print(prefix); pw.print("mCurrentUser="); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index b95c25b69444..9b12a922740b 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -151,6 +151,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { final RootWindowContainer mRootWindowContainer; private final TaskFragmentOrganizerController mTaskFragmentOrganizerController; + // TODO(b/233177466): Move mMinWidth and mMinHeight to Task and remove usages in TaskFragment /** * Minimal width of this task fragment when it's resizeable. {@link #INVALID_MIN_SIZE} means it * should use the default minimal width. @@ -523,6 +524,25 @@ class TaskFragment extends WindowContainer<WindowContainer> { || isAllowedToEmbedActivityInTrustedMode(a, uid); } + boolean smallerThanMinDimension(@NonNull ActivityRecord activity) { + final Rect taskFragBounds = getBounds(); + final Task task = getTask(); + // Don't need to check if the bounds match parent Task bounds because the fallback mechanism + // is to reparent the Activity to parent if minimum dimensions are not satisfied. + if (task == null || taskFragBounds.equals(task.getBounds())) { + return false; + } + final Point minDimensions = activity.getMinDimensions(); + if (minDimensions == null) { + return false; + } + final int minWidth = minDimensions.x; + final int minHeight = minDimensions.y; + final boolean smaller = taskFragBounds.width() < minWidth + || taskFragBounds.height() < minHeight; + return smaller; + } + /** * Checks if the organized task fragment is allowed to embed activity in untrusted mode. */ @@ -1745,7 +1765,8 @@ class TaskFragment extends WindowContainer<WindowContainer> { mClearedTaskForReuse = false; mClearedTaskFragmentForPip = false; - boolean isAddingActivity = child.asActivityRecord() != null; + final ActivityRecord addingActivity = child.asActivityRecord(); + final boolean isAddingActivity = addingActivity != null; final Task task = isAddingActivity ? getTask() : null; // If this task had any activity before we added this one. @@ -1770,7 +1791,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { mBackScreenshots.put(r.mActivityComponent.flattenToString(), backBuffer); } child.asActivityRecord().inHistory = true; - task.onDescendantActivityAdded(taskHadActivity, activityType, child.asActivityRecord()); + task.onDescendantActivityAdded(taskHadActivity, activityType, addingActivity); } } @@ -2279,7 +2300,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { TaskFragmentInfo getTaskFragmentInfo() { List<IBinder> childActivities = new ArrayList<>(); for (int i = 0; i < getChildCount(); i++) { - final WindowContainer wc = getChildAt(i); + final WindowContainer<?> wc = getChildAt(i); final ActivityRecord ar = wc.asActivityRecord(); if (mTaskFragmentOrganizerUid != INVALID_UID && ar != null && ar.info.processName.equals(mTaskFragmentOrganizerProcessName) @@ -2299,7 +2320,31 @@ class TaskFragment extends WindowContainer<WindowContainer> { childActivities, positionInParent, mClearedTaskForReuse, - mClearedTaskFragmentForPip); + mClearedTaskFragmentForPip, + calculateMinDimension()); + } + + /** + * Calculates the minimum dimensions that this TaskFragment can be resized. + * @see TaskFragmentInfo#getMinimumWidth() + * @see TaskFragmentInfo#getMinimumHeight() + */ + Point calculateMinDimension() { + final int[] maxMinWidth = new int[1]; + final int[] maxMinHeight = new int[1]; + + forAllActivities(a -> { + if (a.finishing) { + return; + } + final Point minDimensions = a.getMinDimensions(); + if (minDimensions == null) { + return; + } + maxMinWidth[0] = Math.max(maxMinWidth[0], minDimensions.x); + maxMinHeight[0] = Math.max(maxMinHeight[0], minDimensions.y); + }); + return new Point(maxMinWidth[0], maxMinHeight[0]); } @Nullable diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 8652bc8b7eb9..efa607e04e15 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static android.Manifest.permission.START_TASKS_FROM_RECENTS; import static android.app.ActivityManager.isStartResultSuccessful; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS; import static android.view.Display.DEFAULT_DISPLAY; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_ADD_RECT_INSETS_PROVIDER; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT; @@ -58,6 +59,7 @@ import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.Configuration; +import android.graphics.Point; import android.graphics.Rect; import android.os.Binder; import android.os.Bundle; @@ -117,7 +119,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub static final int CONTROLLABLE_CONFIGS = ActivityInfo.CONFIG_WINDOW_CONFIGURATION | ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE | ActivityInfo.CONFIG_SCREEN_SIZE | ActivityInfo.CONFIG_LAYOUT_DIRECTION; - static final int CONTROLLABLE_WINDOW_CONFIGS = WindowConfiguration.WINDOW_CONFIG_BOUNDS + static final int CONTROLLABLE_WINDOW_CONFIGS = WINDOW_CONFIG_BOUNDS | WindowConfiguration.WINDOW_CONFIG_APP_BOUNDS; private final ActivityTaskManagerService mService; @@ -446,7 +448,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub wc.asTask().setForceHidden(FLAG_FORCE_HIDDEN_FOR_PINNED_TASK, true /* set */); } - int containerEffect = applyWindowContainerChange(wc, entry.getValue()); + int containerEffect = applyWindowContainerChange(wc, entry.getValue(), + t.getErrorCallbackToken()); effects |= containerEffect; if (forceHiddenForPip) { @@ -530,7 +533,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } } - private int applyChanges(WindowContainer container, WindowContainerTransaction.Change change) { + private int applyChanges(WindowContainer<?> container, + WindowContainerTransaction.Change change, @Nullable IBinder errorCallbackToken) { // The "client"-facing API should prevent bad changes; however, just in case, sanitize // masks here. final int configMask = change.getConfigSetMask() & CONTROLLABLE_CONFIGS; @@ -538,6 +542,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub int effects = 0; final int windowingMode = change.getWindowingMode(); if (configMask != 0) { + + adjustBoundsForMinDimensionsIfNeeded(container, change, errorCallbackToken); + if (windowingMode > -1 && windowingMode != container.getWindowingMode()) { // Special handling for when we are setting a windowingMode in the same transaction. // Setting the windowingMode is going to call onConfigurationChanged so we don't @@ -590,6 +597,26 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return effects; } + private void adjustBoundsForMinDimensionsIfNeeded(WindowContainer<?> container, + WindowContainerTransaction.Change change, @Nullable IBinder errorCallbackToken) { + final TaskFragment taskFragment = container.asTaskFragment(); + if (taskFragment == null || !taskFragment.isEmbedded()) { + return; + } + if ((change.getWindowSetMask() & WINDOW_CONFIG_BOUNDS) == 0) { + return; + } + final WindowConfiguration winConfig = change.getConfiguration().windowConfiguration; + final Rect bounds = winConfig.getBounds(); + final Point minDimensions = taskFragment.calculateMinDimension(); + if (bounds.width() < minDimensions.x || bounds.height() < minDimensions.y) { + sendMinimumDimensionViolation(taskFragment, minDimensions, errorCallbackToken, + "setBounds:" + bounds); + // Sets the bounds to match parent bounds. + winConfig.setBounds(new Rect()); + } + } + private int applyTaskChanges(Task tr, WindowContainerTransaction.Change c) { int effects = 0; final SurfaceControl.Transaction t = c.getBoundsChangeTransaction(); @@ -752,7 +779,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final Bundle activityOptions = hop.getLaunchOptions(); final int result = mService.getActivityStartController() .startActivityInTaskFragment(tf, activityIntent, activityOptions, - hop.getCallingActivity(), caller.mUid, caller.mPid); + hop.getCallingActivity(), caller.mUid, caller.mPid, + errorCallbackToken); if (!isStartResultSuccessful(result)) { sendTaskFragmentOperationFailure(organizer, errorCallbackToken, convertStartFailureToThrowable(result, activityIntent)); @@ -796,6 +824,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception); break; } + if (parent.smallerThanMinDimension(activity)) { + sendMinimumDimensionViolation(parent, activity.getMinDimensions(), + errorCallbackToken, "reparentActivityToTask"); + break; + } + activity.reparent(parent, POSITION_TOP); effects |= TRANSACT_EFFECTS_LIFECYCLE; break; @@ -1009,6 +1043,19 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return effects; } + /** A helper method to send minimum dimension violation error to the client. */ + void sendMinimumDimensionViolation(TaskFragment taskFragment, Point minDimensions, + IBinder errorCallbackToken, String reason) { + if (taskFragment == null || taskFragment.getTaskFragmentOrganizer() == null) { + return; + } + final Throwable exception = new SecurityException("The task fragment's bounds:" + + taskFragment.getBounds() + " does not satisfy minimum dimensions:" + + minDimensions + " " + reason); + sendTaskFragmentOperationFailure(taskFragment.getTaskFragmentOrganizer(), + errorCallbackToken, exception); + } + /** * Post and wait for the result of the activity start to prevent potential deadlock against * {@link WindowManagerGlobalLock}. @@ -1220,14 +1267,14 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } private int applyWindowContainerChange(WindowContainer wc, - WindowContainerTransaction.Change c) { + WindowContainerTransaction.Change c, @Nullable IBinder errorCallbackToken) { sanitizeWindowContainer(wc); if (wc.asTaskFragment() != null && wc.asTaskFragment().isEmbeddedTaskFragmentInPip()) { // No override from organizer for embedded TaskFragment in a PIP Task. return 0; } - int effects = applyChanges(wc, c); + int effects = applyChanges(wc, c, errorCallbackToken); if (wc instanceof DisplayArea) { effects |= applyDisplayAreaChanges(wc.asDisplayArea(), c); @@ -1571,8 +1618,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub mLaunchTaskFragments.put(creationParams.getFragmentToken(), taskFragment); } - void reparentTaskFragment(@NonNull TaskFragment oldParent, @Nullable WindowContainer newParent, - @Nullable ITaskFragmentOrganizer organizer, @Nullable IBinder errorCallbackToken) { + void reparentTaskFragment(@NonNull TaskFragment oldParent, + @Nullable WindowContainer<?> newParent, @Nullable ITaskFragmentOrganizer organizer, + @Nullable IBinder errorCallbackToken) { final TaskFragment newParentTF; if (newParent == null) { // Use the old parent's parent if the caller doesn't specify the new parent. @@ -1610,6 +1658,14 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception); return; } + final Point minDimensions = oldParent.calculateMinDimension(); + final Rect newParentBounds = newParentTF.getBounds(); + if (newParentBounds.width() < minDimensions.x + || newParentBounds.height() < minDimensions.y) { + sendMinimumDimensionViolation(newParentTF, minDimensions, errorCallbackToken, + "reparentTaskFragment"); + return; + } while (oldParent.hasChild()) { oldParent.getChildAt(0).reparent(newParentTF, POSITION_TOP); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 7cdf5a8629cb..3ec24b76960f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -24,6 +24,8 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.server.wm.WindowContainer.POSITION_TOP; import static com.android.server.wm.testing.Assert.assertThrows; +import static com.google.common.truth.Truth.assertWithMessage; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; @@ -42,6 +44,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Rect; import android.os.Binder; @@ -77,6 +80,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { private static final int TASK_ID = 10; private TaskFragmentOrganizerController mController; + private WindowOrganizerController mWindowOrganizerController; private TaskFragmentOrganizer mOrganizer; private TaskFragmentOrganizerToken mOrganizerToken; private ITaskFragmentOrganizer mIOrganizer; @@ -86,10 +90,13 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { private WindowContainerTransaction mTransaction; private WindowContainerToken mFragmentWindowToken; private RemoteAnimationDefinition mDefinition; + private IBinder mErrorToken; + private Rect mTaskFragBounds; @Before public void setup() { - mController = mAtm.mWindowOrganizerController.mTaskFragmentOrganizerController; + mWindowOrganizerController = mAtm.mWindowOrganizerController; + mController = mWindowOrganizerController.mTaskFragmentOrganizerController; mOrganizer = new TaskFragmentOrganizer(Runnable::run); mOrganizerToken = mOrganizer.getOrganizerToken(); mIOrganizer = ITaskFragmentOrganizer.Stub.asInterface(mOrganizerToken.asBinder()); @@ -100,6 +107,10 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { mTransaction = new WindowContainerTransaction(); mFragmentWindowToken = mTaskFragment.mRemoteToken.toWindowContainerToken(); mDefinition = new RemoteAnimationDefinition(); + mErrorToken = new Binder(); + final Rect displayBounds = mDisplayContent.getBounds(); + mTaskFragBounds = new Rect(displayBounds.left, displayBounds.top, displayBounds.centerX(), + displayBounds.centerY()); spyOn(mController); spyOn(mOrganizer); @@ -220,16 +231,15 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test - public void testOnTaskFragmentError() throws RemoteException { - final IBinder errorCallbackToken = new Binder(); + public void testOnTaskFragmentError() { final Throwable exception = new IllegalArgumentException("Test exception"); mController.registerOrganizer(mIOrganizer); mController.onTaskFragmentError(mTaskFragment.getTaskFragmentOrganizer(), - errorCallbackToken, exception); + mErrorToken, exception); mController.dispatchPendingEvents(); - verify(mOrganizer).onTaskFragmentError(eq(errorCallbackToken), eq(exception)); + verify(mOrganizer).onTaskFragmentError(eq(mErrorToken), eq(exception)); } @Test @@ -279,7 +289,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final int uid = Binder.getCallingUid(); mTaskFragment.setTaskFragmentOrganizer(mOrganizer.getOrganizerToken(), uid, DEFAULT_TASK_FRAGMENT_ORGANIZER_PROCESS_NAME); - mAtm.mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); mController.registerOrganizer(mIOrganizer); mOrganizer.applyTransaction(mTransaction); final Task task = createTask(mDisplayContent); @@ -304,7 +314,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final IBinder temporaryToken = token.getValue(); assertNotEquals(activity.token, temporaryToken); mTransaction.reparentActivityToTaskFragment(mFragmentToken, temporaryToken); - mAtm.mWindowOrganizerController.applyTransaction(mTransaction); + mWindowOrganizerController.applyTransaction(mTransaction); assertEquals(mTaskFragment, activity.getTaskFragment()); // The temporary token can only be used once. @@ -394,8 +404,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // No lifecycle update when the TaskFragment is not recorded. verify(mAtm.mRootWindowContainer, never()).resumeFocusedTasksTopActivities(); - mAtm.mWindowOrganizerController.mLaunchTaskFragments - .put(mFragmentToken, mTaskFragment); + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); assertApplyTransactionAllowed(mTransaction); verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities(); @@ -465,23 +474,23 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // Fail to create TaskFragment when the task uid is different from caller. activity.info.applicationInfo.uid = uid; activity.getTask().effectiveUid = uid + 1; - mAtm.getWindowOrganizerController().applyTransaction(mTransaction); + mWindowOrganizerController.applyTransaction(mTransaction); - assertNull(mAtm.mWindowOrganizerController.getTaskFragment(fragmentToken)); + assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken)); // Fail to create TaskFragment when the task uid is different from owner activity. activity.info.applicationInfo.uid = uid + 1; activity.getTask().effectiveUid = uid; - mAtm.getWindowOrganizerController().applyTransaction(mTransaction); + mWindowOrganizerController.applyTransaction(mTransaction); - assertNull(mAtm.mWindowOrganizerController.getTaskFragment(fragmentToken)); + assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken)); // Successfully created a TaskFragment for same uid. activity.info.applicationInfo.uid = uid; activity.getTask().effectiveUid = uid; - mAtm.getWindowOrganizerController().applyTransaction(mTransaction); + mWindowOrganizerController.applyTransaction(mTransaction); - assertNotNull(mAtm.mWindowOrganizerController.getTaskFragment(fragmentToken)); + assertNotNull(mWindowOrganizerController.getTaskFragment(fragmentToken)); } @Test @@ -518,7 +527,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { .setParentTask(task) .setFragmentToken(mFragmentToken) .build(); - mAtm.mWindowOrganizerController.mLaunchTaskFragments + mWindowOrganizerController.mLaunchTaskFragments .put(mFragmentToken, mTaskFragment); mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.token); doReturn(true).when(mTaskFragment).isAllowedToEmbedActivity(activity); @@ -548,8 +557,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { .setOrganizer(mOrganizer) .createActivityCount(1) .build(); - mAtm.mWindowOrganizerController.mLaunchTaskFragments.put(token0, tf0); - mAtm.mWindowOrganizerController.mLaunchTaskFragments.put(token1, tf1); + mWindowOrganizerController.mLaunchTaskFragments.put(token0, tf0); + mWindowOrganizerController.mLaunchTaskFragments.put(token1, tf1); final ActivityRecord activity0 = tf0.getTopMostActivity(); final ActivityRecord activity1 = tf1.getTopMostActivity(); @@ -557,7 +566,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final ActivityRecord activityInOtherTask = createActivityRecord(mDefaultDisplay); mDisplayContent.setFocusedApp(activityInOtherTask); mTransaction.requestFocusOnTaskFragment(token0); - mAtm.mWindowOrganizerController.applyTransaction(mTransaction); + mWindowOrganizerController.applyTransaction(mTransaction); assertEquals(activityInOtherTask, mDisplayContent.mFocusedApp); @@ -565,7 +574,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { activity0.setState(ActivityRecord.State.PAUSED, "test"); activity1.setState(ActivityRecord.State.RESUMED, "test"); mDisplayContent.setFocusedApp(activity1); - mAtm.mWindowOrganizerController.applyTransaction(mTransaction); + mWindowOrganizerController.applyTransaction(mTransaction); assertEquals(activity1, mDisplayContent.mFocusedApp); @@ -573,7 +582,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // has a resumed activity. activity0.setState(ActivityRecord.State.RESUMED, "test"); mDisplayContent.setFocusedApp(activity1); - mAtm.mWindowOrganizerController.applyTransaction(mTransaction); + mWindowOrganizerController.applyTransaction(mTransaction); assertEquals(activity0, mDisplayContent.mFocusedApp); } @@ -582,53 +591,50 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { public void testTaskFragmentInPip_startActivityInTaskFragment() { setupTaskFragmentInPip(); final ActivityRecord activity = mTaskFragment.getTopMostActivity(); - final IBinder errorToken = new Binder(); spyOn(mAtm.getActivityStartController()); - spyOn(mAtm.mWindowOrganizerController); + spyOn(mWindowOrganizerController); // Not allow to start activity in a TaskFragment that is in a PIP Task. mTransaction.startActivityInTaskFragment( mFragmentToken, activity.token, new Intent(), null /* activityOptions */) - .setErrorCallbackToken(errorToken); - mAtm.mWindowOrganizerController.applyTransaction(mTransaction); + .setErrorCallbackToken(mErrorToken); + mWindowOrganizerController.applyTransaction(mTransaction); verify(mAtm.getActivityStartController(), never()).startActivityInTaskFragment(any(), any(), - any(), any(), anyInt(), anyInt()); + any(), any(), anyInt(), anyInt(), any()); verify(mAtm.mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), - eq(errorToken), any(IllegalArgumentException.class)); + eq(mErrorToken), any(IllegalArgumentException.class)); } @Test public void testTaskFragmentInPip_reparentActivityToTaskFragment() { setupTaskFragmentInPip(); final ActivityRecord activity = createActivityRecord(mDisplayContent); - final IBinder errorToken = new Binder(); - spyOn(mAtm.mWindowOrganizerController); + spyOn(mWindowOrganizerController); // Not allow to reparent activity to a TaskFragment that is in a PIP Task. mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.token) - .setErrorCallbackToken(errorToken); - mAtm.mWindowOrganizerController.applyTransaction(mTransaction); + .setErrorCallbackToken(mErrorToken); + mWindowOrganizerController.applyTransaction(mTransaction); - verify(mAtm.mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), - eq(errorToken), any(IllegalArgumentException.class)); + verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), + eq(mErrorToken), any(IllegalArgumentException.class)); assertNull(activity.getOrganizedTaskFragment()); } @Test public void testTaskFragmentInPip_setAdjacentTaskFragment() { setupTaskFragmentInPip(); - final IBinder errorToken = new Binder(); - spyOn(mAtm.mWindowOrganizerController); + spyOn(mWindowOrganizerController); // Not allow to set adjacent on a TaskFragment that is in a PIP Task. mTransaction.setAdjacentTaskFragments(mFragmentToken, null /* fragmentToken2 */, null /* options */) - .setErrorCallbackToken(errorToken); - mAtm.mWindowOrganizerController.applyTransaction(mTransaction); + .setErrorCallbackToken(mErrorToken); + mWindowOrganizerController.applyTransaction(mTransaction); - verify(mAtm.mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), - eq(errorToken), any(IllegalArgumentException.class)); + verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), + eq(mErrorToken), any(IllegalArgumentException.class)); verify(mTaskFragment, never()).setAdjacentTaskFragment(any()); } @@ -639,47 +645,45 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { ACTIVITY_TYPE_STANDARD); final ActivityRecord activity = createActivityRecord(pipTask); final IBinder fragmentToken = new Binder(); - final IBinder errorToken = new Binder(); - spyOn(mAtm.mWindowOrganizerController); + spyOn(mWindowOrganizerController); // Not allow to create TaskFragment in a PIP Task. createTaskFragmentFromOrganizer(mTransaction, activity, fragmentToken); - mTransaction.setErrorCallbackToken(errorToken); - mAtm.mWindowOrganizerController.applyTransaction(mTransaction); + mTransaction.setErrorCallbackToken(mErrorToken); + mWindowOrganizerController.applyTransaction(mTransaction); - verify(mAtm.mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), - eq(errorToken), any(IllegalArgumentException.class)); - assertNull(mAtm.mWindowOrganizerController.getTaskFragment(fragmentToken)); + verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), + eq(mErrorToken), any(IllegalArgumentException.class)); + assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken)); } @Test public void testTaskFragmentInPip_deleteTaskFragment() { setupTaskFragmentInPip(); - final IBinder errorToken = new Binder(); - spyOn(mAtm.mWindowOrganizerController); + spyOn(mWindowOrganizerController); // Not allow to delete a TaskFragment that is in a PIP Task. mTransaction.deleteTaskFragment(mFragmentWindowToken) - .setErrorCallbackToken(errorToken); - mAtm.mWindowOrganizerController.applyTransaction(mTransaction); + .setErrorCallbackToken(mErrorToken); + mWindowOrganizerController.applyTransaction(mTransaction); - verify(mAtm.mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), - eq(errorToken), any(IllegalArgumentException.class)); - assertNotNull(mAtm.mWindowOrganizerController.getTaskFragment(mFragmentToken)); + verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), + eq(mErrorToken), any(IllegalArgumentException.class)); + assertNotNull(mWindowOrganizerController.getTaskFragment(mFragmentToken)); // Allow organizer to delete empty TaskFragment for cleanup. final Task task = mTaskFragment.getTask(); mTaskFragment.removeChild(mTaskFragment.getTopMostActivity()); - mAtm.mWindowOrganizerController.applyTransaction(mTransaction); + mWindowOrganizerController.applyTransaction(mTransaction); - assertNull(mAtm.mWindowOrganizerController.getTaskFragment(mFragmentToken)); + assertNull(mWindowOrganizerController.getTaskFragment(mFragmentToken)); assertNull(task.getTopChild()); } @Test public void testTaskFragmentInPip_setConfig() { setupTaskFragmentInPip(); - spyOn(mAtm.mWindowOrganizerController); + spyOn(mWindowOrganizerController); // Set bounds is ignored on a TaskFragment that is in a PIP Task. mTransaction.setBounds(mFragmentWindowToken, new Rect(0, 0, 100, 100)); @@ -831,14 +835,13 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final IBinder fragmentToken = new Binder(); createTaskFragmentFromOrganizer(mTransaction, ownerActivity, fragmentToken); mAtm.getWindowOrganizerController().applyTransaction(mTransaction); - final TaskFragment taskFragment = mAtm.mWindowOrganizerController - .getTaskFragment(fragmentToken); + final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment(fragmentToken); assertNotNull(taskFragment); taskFragment.removeImmediately(); - assertNull(mAtm.mWindowOrganizerController.getTaskFragment(fragmentToken)); + assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken)); } /** @@ -901,6 +904,101 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { assertApplyTransactionDisallowed(mTransaction); } + // TODO(b/232871351): add test for minimum dimension violation in startActivityInTaskFragment + @Test + public void testMinDimensionViolation_ReparentActivityToTaskFragment() { + final Task task = createTask(mDisplayContent); + final ActivityRecord activity = createActivityRecord(task); + // Make minWidth/minHeight exceeds the TaskFragment bounds. + activity.info.windowLayout = new ActivityInfo.WindowLayout( + 0, 0, 0, 0, 0, mTaskFragBounds.width() + 10, mTaskFragBounds.height() + 10); + mOrganizer.applyTransaction(mTransaction); + mController.registerOrganizer(mIOrganizer); + mTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .setFragmentToken(mFragmentToken) + .setOrganizer(mOrganizer) + .setBounds(mTaskFragBounds) + .build(); + doReturn(true).when(mTaskFragment).isAllowedToEmbedActivity(activity); + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); + clearInvocations(mAtm.mRootWindowContainer); + + // Reparent activity to mTaskFragment, which is smaller than activity's + // minimum dimensions. + mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.token) + .setErrorCallbackToken(mErrorToken); + mWindowOrganizerController.applyTransaction(mTransaction); + + verify(mOrganizer).onTaskFragmentError(eq(mErrorToken), any(SecurityException.class)); + } + + @Test + public void testMinDimensionViolation_ReparentChildren() { + final Task task = createTask(mDisplayContent); + mOrganizer.applyTransaction(mTransaction); + mController.registerOrganizer(mIOrganizer); + final IBinder oldFragToken = new Binder(); + final TaskFragment oldTaskFrag = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .createActivityCount(1) + .setFragmentToken(oldFragToken) + .setOrganizer(mOrganizer) + .build(); + final ActivityRecord activity = oldTaskFrag.getTopMostActivity(); + // Make minWidth/minHeight exceeds mTaskFragment bounds. + activity.info.windowLayout = new ActivityInfo.WindowLayout( + 0, 0, 0, 0, 0, mTaskFragBounds.width() + 10, mTaskFragBounds.height() + 10); + mTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .setFragmentToken(mFragmentToken) + .setOrganizer(mOrganizer) + .setBounds(mTaskFragBounds) + .build(); + doReturn(true).when(mTaskFragment).isAllowedToEmbedActivity(activity); + mWindowOrganizerController.mLaunchTaskFragments.put(oldFragToken, oldTaskFrag); + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); + clearInvocations(mAtm.mRootWindowContainer); + + // Reparent oldTaskFrag's children to mTaskFragment, which is smaller than activity's + // minimum dimensions. + mTransaction.reparentChildren(oldTaskFrag.mRemoteToken.toWindowContainerToken(), + mTaskFragment.mRemoteToken.toWindowContainerToken()) + .setErrorCallbackToken(mErrorToken); + mWindowOrganizerController.applyTransaction(mTransaction); + + verify(mOrganizer).onTaskFragmentError(eq(mErrorToken), any(SecurityException.class)); + } + + @Test + public void testMinDimensionViolation_SetBounds() { + final Task task = createTask(mDisplayContent); + mOrganizer.applyTransaction(mTransaction); + mController.registerOrganizer(mIOrganizer); + mTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .createActivityCount(1) + .setFragmentToken(mFragmentToken) + .setOrganizer(mOrganizer) + .setBounds(new Rect(0, 0, mTaskFragBounds.right * 2, mTaskFragBounds.bottom * 2)) + .build(); + final ActivityRecord activity = mTaskFragment.getTopMostActivity(); + // Make minWidth/minHeight exceeds the TaskFragment bounds. + activity.info.windowLayout = new ActivityInfo.WindowLayout( + 0, 0, 0, 0, 0, mTaskFragBounds.width() + 10, mTaskFragBounds.height() + 10); + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); + clearInvocations(mAtm.mRootWindowContainer); + + // Shrink the TaskFragment to mTaskFragBounds to make its bounds smaller than activity's + // minimum dimensions. + mTransaction.setBounds(mTaskFragment.mRemoteToken.toWindowContainerToken(), mTaskFragBounds) + .setErrorCallbackToken(mErrorToken); + mWindowOrganizerController.applyTransaction(mTransaction); + + assertWithMessage("setBounds must not be performed.") + .that(mTaskFragment.getBounds()).isEqualTo(task.getBounds()); + } + /** * Creates a {@link TaskFragment} with the {@link WindowContainerTransaction}. Calls * {@link WindowOrganizerController#applyTransaction} to apply the transaction, diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index d299a86c0ee6..50fa4cc9eb7c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -1217,6 +1217,7 @@ class WindowTestsBase extends SystemServiceTestsBase { @Nullable private TaskFragmentOrganizer mOrganizer; private IBinder mFragmentToken; + private Rect mBounds; TaskFragmentBuilder(ActivityTaskManagerService service) { mAtm = service; @@ -1253,6 +1254,11 @@ class WindowTestsBase extends SystemServiceTestsBase { return this; } + TaskFragmentBuilder setBounds(@Nullable Rect bounds) { + mBounds = bounds; + return this; + } + TaskFragment build() { SystemServicesTestRule.checkHoldsLock(mAtm.mGlobalLock); @@ -1280,6 +1286,9 @@ class WindowTestsBase extends SystemServiceTestsBase { mOrganizer.getOrganizerToken(), DEFAULT_TASK_FRAGMENT_ORGANIZER_UID, DEFAULT_TASK_FRAGMENT_ORGANIZER_PROCESS_NAME); } + if (mBounds != null && !mBounds.isEmpty()) { + taskFragment.setBounds(mBounds); + } spyOn(taskFragment); return taskFragment; } |