diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index febd7917dff9f..74303e2fab7ce 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -31,6 +31,7 @@ import android.window.TaskFragmentOrganizer; import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; +import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -93,6 +94,7 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { } /** No longer overrides the animation if the transition is on the given Task. */ + @GuardedBy("mLock") void stopOverrideSplitAnimation(int taskId) { if (mAnimationController != null) { mAnimationController.unregisterRemoteAnimations(taskId); 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 c8ac0fc73ff91..00be5a6e34168 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java @@ -17,8 +17,10 @@ package androidx.window.extensions.embedding; import android.app.Activity; +import android.content.res.Configuration; import android.util.Pair; import android.util.Size; +import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; @@ -32,14 +34,18 @@ class SplitContainer { private final TaskFragmentContainer mSecondaryContainer; @NonNull private final SplitRule mSplitRule; + @NonNull + private SplitAttributes mSplitAttributes; SplitContainer(@NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, @NonNull TaskFragmentContainer secondaryContainer, - @NonNull SplitRule splitRule) { + @NonNull SplitRule splitRule, + @NonNull SplitAttributes splitAttributes) { mPrimaryContainer = primaryContainer; mSecondaryContainer = secondaryContainer; mSplitRule = splitRule; + mSplitAttributes = splitAttributes; if (shouldFinishPrimaryWithSecondary(splitRule)) { if (mPrimaryContainer.getRunningActivityCount() == 1 @@ -72,6 +78,26 @@ class SplitContainer { return mSplitRule; } + @NonNull + SplitAttributes getSplitAttributes() { + return mSplitAttributes; + } + + /** + * Updates the {@link SplitAttributes} to this container. + * It is usually used when there's a folding state change or + * {@link SplitController#onTaskFragmentParentInfoChanged(WindowContainerTransaction, int, + * Configuration)}. + */ + void setSplitAttributes(@NonNull SplitAttributes splitAttributes) { + mSplitAttributes = splitAttributes; + } + + @NonNull + TaskContainer getTaskContainer() { + return getPrimaryContainer().getTaskContainer(); + } + /** Returns the minimum dimension pair of primary container and secondary container. */ @NonNull Pair getMinDimensionsPair() { @@ -141,6 +167,7 @@ class SplitContainer { + " primaryContainer=" + mPrimaryContainer + " secondaryContainer=" + mSecondaryContainer + " splitRule=" + mSplitRule + + " splitAttributes" + mSplitAttributes + "}"; } } 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 138eee41f1093..6468fe87f2001 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -41,7 +41,7 @@ import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAs import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO; import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair; import static androidx.window.extensions.embedding.SplitPresenter.getNonEmbeddedActivityBounds; -import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide; +import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit; import android.app.Activity; import android.app.ActivityClient; @@ -64,6 +64,7 @@ import android.util.Log; import android.util.Pair; import android.util.Size; import android.util.SparseArray; +import android.view.WindowMetrics; import android.window.TaskFragmentInfo; import android.window.TaskFragmentParentInfo; import android.window.TaskFragmentTransaction; @@ -582,6 +583,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Updates if we should override transition animation. We only want to override if the Task * bounds is large enough for at least one split rule. */ + @GuardedBy("mLock") private void updateAnimationOverride(@NonNull TaskContainer taskContainer) { if (ENABLE_SHELL_TRANSITIONS) { // TODO(b/207070762): cleanup with legacy app transition @@ -593,15 +595,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return; } - // We only want to override if it supports split. - if (supportSplit(taskContainer)) { + // We only want to override if the TaskContainer may show split. + if (mayShowSplit(taskContainer)) { mPresenter.startOverrideSplitAnimation(taskContainer.getTaskId()); } else { mPresenter.stopOverrideSplitAnimation(taskContainer.getTaskId()); } } - private boolean supportSplit(@NonNull TaskContainer taskContainer) { + /** Returns whether the given {@link TaskContainer} may show in split. */ + // Suppress GuardedBy warning because lint asks to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") + private boolean mayShowSplit(@NonNull TaskContainer taskContainer) { // No split inside PIP. if (taskContainer.isInPictureInPicture()) { return false; @@ -611,7 +618,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (!(rule instanceof SplitRule)) { continue; } - if (shouldShowSideBySide(taskContainer.getTaskBounds(), (SplitRule) rule)) { + final SplitRule splitRule = (SplitRule) rule; + final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes( + taskContainer.getTaskProperties(), splitRule, null /* minDimensionsPair */); + if (shouldShowSplit(splitAttributes)) { return true; } } @@ -755,14 +765,18 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Starts an activity to side of the launchingActivity with the provided split config. */ + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(container.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") @GuardedBy("mLock") private void startActivityToSide(@NonNull WindowContainerTransaction wct, @NonNull Activity launchingActivity, @NonNull Intent intent, @Nullable Bundle options, @NonNull SplitRule sideRule, - @Nullable Consumer failureCallback, boolean isPlaceholder) { + @NonNull SplitAttributes splitAttributes, @Nullable Consumer failureCallback, + boolean isPlaceholder) { try { mPresenter.startActivityToSide(wct, launchingActivity, intent, options, sideRule, - isPlaceholder); + splitAttributes, isPlaceholder); } catch (Exception e) { if (failureCallback != null) { failureCallback.accept(e); @@ -789,6 +803,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** Whether the given new launched activity is in a split with a rule matched. */ + // Suppress GuardedBy warning because lint asks to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") private boolean isNewActivityInSplitWithRuleMatched(@NonNull Activity launchedActivity) { final TaskFragmentContainer container = getContainerWithActivity(launchedActivity); final SplitContainer splitContainer = getActiveSplitForContainer(container); @@ -882,8 +900,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final TaskFragmentContainer primaryContainer = getContainerWithActivity( primaryActivity); final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer); + final WindowMetrics taskWindowMetrics = mPresenter.getTaskWindowMetrics(primaryActivity); if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer() - && canReuseContainer(splitRule, splitContainer.getSplitRule())) { + && canReuseContainer(splitRule, splitContainer.getSplitRule(), taskWindowMetrics)) { // Can launch in the existing secondary container if the rules share the same // presentation. final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); @@ -1013,6 +1032,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ @VisibleForTesting @Nullable + @GuardedBy("mLock") TaskFragmentContainer resolveStartActivityIntent(@NonNull WindowContainerTransaction wct, int taskId, @NonNull Intent intent, @Nullable Activity launchingActivity) { /* @@ -1117,8 +1137,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } final TaskFragmentContainer existingContainer = getContainerWithActivity(primaryActivity); final SplitContainer splitContainer = getActiveSplitForContainer(existingContainer); + final WindowMetrics taskWindowMetrics = mPresenter.getTaskWindowMetrics(primaryActivity); if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer() - && (canReuseContainer(splitRule, splitContainer.getSplitRule()) + && (canReuseContainer(splitRule, splitContainer.getSplitRule(), taskWindowMetrics) // TODO(b/231845476) we should always respect clearTop. || !respectClearTop) && mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, @@ -1208,12 +1229,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Creates and registers a new split with the provided containers and configuration. Finishes * existing secondary containers if found for the given primary container. */ + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") void registerSplit(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, @NonNull TaskFragmentContainer secondaryContainer, - @NonNull SplitRule splitRule) { + @NonNull SplitRule splitRule, @NonNull SplitAttributes splitAttributes) { final SplitContainer splitContainer = new SplitContainer(primaryContainer, primaryActivity, - secondaryContainer, splitRule); + secondaryContainer, splitRule, splitAttributes); // Remove container later to prevent pinning escaping toast showing in lock task mode. if (splitRule instanceof SplitPairRule && ((SplitPairRule) splitRule).shouldClearTop()) { removeExistingSecondaryContainers(wct, primaryContainer); @@ -1364,6 +1389,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Skip position update - one or both containers are finished. return; } + final TaskContainer taskContainer = splitContainer.getTaskContainer(); + final SplitRule splitRule = splitContainer.getSplitRule(); + final Pair minDimensionsPair = splitContainer.getMinDimensionsPair(); + final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes( + taskContainer.getTaskProperties(), splitRule, minDimensionsPair); + splitContainer.setSplitAttributes(splitAttributes); if (dismissPlaceholderIfNecessary(wct, splitContainer)) { // Placeholder was finished, the positions will be updated when its container is emptied return; @@ -1437,6 +1468,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return launchPlaceholderIfNecessary(wct, topActivity, false /* isOnCreated */); } + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") @GuardedBy("mLock") boolean launchPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct, @NonNull Activity activity, boolean isOnCreated) { @@ -1463,18 +1497,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } + final TaskContainer.TaskProperties taskProperties = mPresenter.getTaskProperties(activity); final Pair minDimensionsPair = getActivityIntentMinDimensionsPair(activity, placeholderRule.getPlaceholderIntent()); - if (!shouldShowSideBySide( - mPresenter.getParentContainerBounds(activity), placeholderRule, - minDimensionsPair)) { + final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes(taskProperties, + placeholderRule, minDimensionsPair); + if (!SplitPresenter.shouldShowSplit(splitAttributes)) { return false; } // TODO(b/190433398): Handle failed request final Bundle options = getPlaceholderOptions(activity, isOnCreated); startActivityToSide(wct, activity, placeholderRule.getPlaceholderIntent(), options, - placeholderRule, null /* failureCallback */, true /* isPlaceholder */); + placeholderRule, splitAttributes, null /* failureCallback */, + true /* isPlaceholder */); return true; } @@ -1499,6 +1535,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return options.toBundle(); } + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") @VisibleForTesting @GuardedBy("mLock") boolean dismissPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct, @@ -1511,11 +1550,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // The placeholder should remain after it was first shown. return false; } - - if (shouldShowSideBySide(splitContainer)) { + final SplitAttributes splitAttributes = splitContainer.getSplitAttributes(); + if (SplitPresenter.shouldShowSplit(splitAttributes)) { return false; } - mPresenter.cleanupContainer(wct, splitContainer.getSecondaryContainer(), false /* shouldFinishDependent */); return true; @@ -1525,6 +1563,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns the rule to launch a placeholder for the activity with the provided component name * if it is configured in the split config. */ + @GuardedBy("mLock") private SplitPlaceholderRule getPlaceholderRule(@NonNull Activity activity) { for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof SplitPlaceholderRule)) { @@ -1541,6 +1580,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Notifies listeners about changes to split states if necessary. */ + @GuardedBy("mLock") private void updateCallbackIfNecessary() { if (mEmbeddingCallback == null) { return; @@ -1562,6 +1602,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * null, that indicates that the active split states are in an intermediate state and should * not be reported. */ + @GuardedBy("mLock") @Nullable private List getActiveSplitStates() { List splitStates = new ArrayList<>(); @@ -1580,20 +1621,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen .toActivityStack(); final ActivityStack secondaryContainer = container.getSecondaryContainer() .toActivityStack(); - final SplitAttributes.SplitType splitType = shouldShowSideBySide(container) - ? new SplitAttributes.SplitType.RatioSplitType( - container.getSplitRule().getSplitRatio()) - : new SplitAttributes.SplitType.ExpandContainersSplitType(); final SplitInfo splitState = new SplitInfo(primaryContainer, secondaryContainer, - // 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. - // TODO(b/241042437): use v2 APIs for splitAttributes - new SplitAttributes.Builder() - .setSplitType(splitType) - .setLayoutDirection(container.getSplitRule().getLayoutDirection()) - .build() - ); + container.getSplitAttributes()); splitStates.add(splitState); } } @@ -1631,6 +1660,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns a split rule for the provided pair of primary activity and secondary activity intent * if available. */ + @GuardedBy("mLock") @Nullable private SplitPairRule getSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryActivityIntent) { @@ -1649,6 +1679,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Returns a split rule for the provided pair of primary and secondary activities if available. */ + @GuardedBy("mLock") @Nullable private SplitPairRule getSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { @@ -1723,6 +1754,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns {@code true} if an Activity with the provided component name should always be * expanded to occupy full task bounds. Such activity must not be put in a split. */ + @GuardedBy("mLock") private boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent) { for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof ActivityRule)) { @@ -1748,6 +1780,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * 'sticky' and the placeholder was finished when fully overlapping the primary container. * @return {@code true} if the associated container should be retained (and not be finished). */ + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") boolean shouldRetainAssociatedContainer(@NonNull TaskFragmentContainer finishingContainer, @NonNull TaskFragmentContainer associatedContainer) { SplitContainer splitContainer = getActiveSplitForContainers(associatedContainer, @@ -1766,7 +1802,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } // Decide whether the associated container should be retained based on the current // presentation mode. - if (shouldShowSideBySide(splitContainer)) { + if (shouldShowSplit(splitContainer)) { return !shouldFinishAssociatedContainerWhenAdjacent(finishBehavior); } else { return !shouldFinishAssociatedContainerWhenStacked(finishBehavior); @@ -1959,23 +1995,24 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * If the two rules have the same presentation, we can reuse the same {@link SplitContainer} if * there is any. */ - private static boolean canReuseContainer(@NonNull SplitRule rule1, @NonNull SplitRule rule2) { + private static boolean canReuseContainer(@NonNull SplitRule rule1, @NonNull SplitRule rule2, + @NonNull WindowMetrics parentWindowMetrics) { if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) { return false; } - return haveSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2); + return haveSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2, + parentWindowMetrics); } /** Whether the two rules have the same presentation. */ private static boolean haveSamePresentation(@NonNull SplitPairRule rule1, - @NonNull SplitPairRule rule2) { + @NonNull SplitPairRule rule2, @NonNull WindowMetrics parentWindowMetrics) { // TODO(b/231655482): add util method to do the comparison in SplitPairRule. - return rule1.getSplitRatio() == rule2.getSplitRatio() - && rule1.getLayoutDirection() == rule2.getLayoutDirection() - && rule1.getFinishPrimaryWithSecondary() - == rule2.getFinishPrimaryWithSecondary() - && rule1.getFinishSecondaryWithPrimary() - == rule2.getFinishSecondaryWithPrimary(); + return rule1.getDefaultSplitAttributes().equals(rule2.getDefaultSplitAttributes()) + && rule1.checkParentMetrics(parentWindowMetrics) + == rule2.checkParentMetrics(parentWindowMetrics) + && rule1.getFinishPrimaryWithSecondary() == rule2.getFinishPrimaryWithSecondary() + && rule1.getFinishSecondaryWithPrimary() == rule2.getFinishSecondaryWithPrimary(); } /** 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 2ef8e4c64855c..c9895239029ed 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -22,11 +22,11 @@ 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.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; @@ -42,9 +42,20 @@ import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.window.extensions.embedding.SplitAttributes.SplitType; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.HingeSplitType; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType; +import androidx.window.extensions.embedding.TaskContainer.TaskProperties; +import androidx.window.extensions.layout.DisplayFeature; +import androidx.window.extensions.layout.FoldingFeature; +import androidx.window.extensions.layout.WindowLayoutInfo; import com.android.internal.annotations.VisibleForTesting; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import java.util.concurrent.Executor; /** @@ -66,11 +77,25 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { }) private @interface Position {} + private static final int CONTAINER_POSITION_LEFT = 0; + private static final int CONTAINER_POSITION_TOP = 1; + private static final int CONTAINER_POSITION_RIGHT = 2; + private static final int CONTAINER_POSITION_BOTTOM = 3; + + @IntDef(value = { + CONTAINER_POSITION_LEFT, + CONTAINER_POSITION_TOP, + CONTAINER_POSITION_RIGHT, + CONTAINER_POSITION_BOTTOM, + }) + private @interface ContainerPosition {} + /** * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, * Activity, Activity, Intent)}. * No need to expand the splitContainer because screen is big enough to - * {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} and minimum dimensions is satisfied. + * {@link #shouldShowSplit(SplitAttributes)} and minimum dimensions is + * satisfied. */ static final int RESULT_NOT_EXPANDED = 0; /** @@ -78,7 +103,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * Activity, Activity, Intent)}. * The splitContainer should be expanded. It is usually because minimum dimensions is not * satisfied. - * @see #shouldShowSideBySide(Rect, SplitRule, Pair) + * @see #shouldShowSplit(SplitAttributes) */ static final int RESULT_EXPANDED = 1; /** @@ -101,6 +126,11 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { }) private @interface ResultCode {} + private static final SplitAttributes EXPAND_CONTAINERS_ATTRIBUTES = + new SplitAttributes.Builder() + .setSplitType(new ExpandContainersSplitType()) + .build(); + private final SplitController mController; SplitPresenter(@NonNull Executor executor, @NonNull SplitController controller) { @@ -129,14 +159,17 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * @return The newly created secondary container. */ @NonNull + @GuardedBy("mController.mLock") TaskFragmentContainer createNewSplitWithEmptySideContainer( @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @NonNull Intent secondaryIntent, @NonNull SplitPairRule rule) { - final Rect parentBounds = getParentContainerBounds(primaryActivity); + final TaskProperties taskProperties = getTaskProperties(primaryActivity); final Pair minDimensionsPair = getActivityIntentMinDimensionsPair( primaryActivity, secondaryIntent); - final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - primaryActivity, minDimensionsPair); + final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule, + minDimensionsPair); + final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties, + splitAttributes); final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, primaryActivity, primaryRectBounds, null); @@ -144,8 +177,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final int taskId = primaryContainer.getTaskId(); final TaskFragmentContainer secondaryContainer = mController.newContainer( secondaryIntent, primaryActivity, taskId); - final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, - rule, primaryActivity, minDimensionsPair); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties, + splitAttributes); final int windowingMode = mController.getTaskContainer(taskId) .getWindowingModeForSplitTaskFragment(secondaryRectBounds); createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(), @@ -154,9 +187,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Set adjacent to each other so that the containers below will be invisible. setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, - minDimensionsPair); + splitAttributes); - mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); + mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule, + splitAttributes); return secondaryContainer; } @@ -176,16 +210,18 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { void createNewSplitContainer(@NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, @NonNull SplitPairRule rule) { - final Rect parentBounds = getParentContainerBounds(primaryActivity); + final TaskProperties taskProperties = getTaskProperties(primaryActivity); final Pair minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity); - final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - primaryActivity, minDimensionsPair); + final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule, + minDimensionsPair); + final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties, + splitAttributes); final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, primaryActivity, primaryRectBounds, null); - final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - primaryActivity, minDimensionsPair); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties, + splitAttributes); final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity( secondaryActivity); TaskFragmentContainer containerToAvoid = primaryContainer; @@ -200,9 +236,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Set adjacent to each other so that the containers below will be invisible. setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, - minDimensionsPair); + splitAttributes); - mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); + mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule, + splitAttributes); } /** @@ -244,16 +281,16 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * @param rule The split rule to be applied to the container. * @param isPlaceholder Whether the launch is a placeholder. */ + @GuardedBy("mController.mLock") void startActivityToSide(@NonNull WindowContainerTransaction wct, @NonNull Activity launchingActivity, @NonNull Intent activityIntent, - @Nullable Bundle activityOptions, @NonNull SplitRule rule, boolean isPlaceholder) { - final Rect parentBounds = getParentContainerBounds(launchingActivity); - final Pair minDimensionsPair = getActivityIntentMinDimensionsPair( - launchingActivity, activityIntent); - final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - launchingActivity, minDimensionsPair); - final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - launchingActivity, minDimensionsPair); + @Nullable Bundle activityOptions, @NonNull SplitRule rule, + @NonNull SplitAttributes splitAttributes, boolean isPlaceholder) { + final TaskProperties taskProperties = getTaskProperties(launchingActivity); + final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties, + splitAttributes); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties, + splitAttributes); TaskFragmentContainer primaryContainer = mController.getContainerWithActivity( launchingActivity); @@ -268,7 +305,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final int windowingMode = mController.getTaskContainer(taskId) .getWindowingModeForSplitTaskFragment(primaryRectBounds); mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer, - rule); + rule, splitAttributes); startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds, launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds, activityIntent, activityOptions, rule, windowingMode); @@ -284,22 +321,24 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * @param updatedContainer The task fragment that was updated and caused this split update. * @param wct WindowContainerTransaction that this update should be performed with. */ + @GuardedBy("mController.mLock") void updateSplitContainer(@NonNull SplitContainer splitContainer, @NonNull TaskFragmentContainer updatedContainer, @NonNull WindowContainerTransaction wct) { - // Getting the parent bounds using the updated container - it will have the recent value. - final Rect parentBounds = getParentContainerBounds(updatedContainer); + // Getting the parent configuration using the updated container - it will have the recent + // value. final SplitRule rule = splitContainer.getSplitRule(); final TaskFragmentContainer primaryContainer = splitContainer.getPrimaryContainer(); final Activity activity = primaryContainer.getTopNonFinishingActivity(); if (activity == null) { return; } - final Pair minDimensionsPair = splitContainer.getMinDimensionsPair(); - final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - activity, minDimensionsPair); - final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - activity, minDimensionsPair); + final TaskProperties taskProperties = getTaskProperties(updatedContainer); + final SplitAttributes splitAttributes = splitContainer.getSplitAttributes(); + final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties, + splitAttributes); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties, + splitAttributes); final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); // Whether the placeholder is becoming side-by-side with the primary from fullscreen. final boolean isPlaceholderBecomingSplit = splitContainer.isPlaceholderContainer() @@ -311,7 +350,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds); resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds); setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, - minDimensionsPair); + splitAttributes); if (isPlaceholderBecomingSplit) { // When placeholder is shown in split, we should keep the focus on the primary. wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken()); @@ -323,14 +362,14 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode); } + @GuardedBy("mController.mLock") private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer, @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule, - @NonNull Pair minDimensionsPair) { - final Rect parentBounds = getParentContainerBounds(primaryContainer); + @NonNull SplitAttributes splitAttributes) { // Clear adjacent TaskFragments if the container is shown in fullscreen, or the // secondaryContainer could not be finished. - if (!shouldShowSideBySide(parentBounds, splitRule, minDimensionsPair)) { + if (!shouldShowSplit(splitAttributes)) { setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(), null /* secondary */, null /* splitRule */); } else { @@ -416,8 +455,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * Expands the split container if the current split bounds are smaller than the Activity or * Intent that is added to the container. * - * @return the {@link ResultCode} based on {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} - * and if {@link android.window.TaskFragmentInfo} has reported to the client side. + * @return the {@link ResultCode} based on + * {@link #shouldShowSplit(SplitAttributes)} and if + * {@link android.window.TaskFragmentInfo} has reported to the client side. */ @ResultCode int expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct, @@ -427,7 +467,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { throw new IllegalArgumentException("Either secondaryActivity or secondaryIntent must be" + " non-null."); } - final Rect taskBounds = getParentContainerBounds(primaryActivity); final Pair minDimensionsPair; if (secondaryActivity != null) { minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity); @@ -436,7 +475,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { secondaryIntent); } // Expand the splitContainer if minimum dimensions are not satisfied. - if (!shouldShowSideBySide(taskBounds, splitContainer.getSplitRule(), minDimensionsPair)) { + final TaskContainer taskContainer = splitContainer.getTaskContainer(); + final SplitAttributes splitAttributes = sanitizeSplitAttributes( + taskContainer.getTaskProperties(), splitContainer.getSplitAttributes(), + minDimensionsPair); + splitContainer.setSplitAttributes(splitAttributes); + if (!shouldShowSplit(splitAttributes)) { // If the client side hasn't received TaskFragmentInfo yet, we can't change TaskFragment // bounds. Return failure to create a new SplitContainer which fills task bounds. if (splitContainer.getPrimaryContainer().getInfo() == null @@ -450,36 +494,52 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { return RESULT_NOT_EXPANDED; } - static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule) { - return shouldShowSideBySide(parentBounds, rule, null /* minimumDimensionPair */); + static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) { + return shouldShowSplit(splitContainer.getSplitAttributes()); } - static boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) { - final Rect parentBounds = getParentContainerBounds(splitContainer.getPrimaryContainer()); - - return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule(), - splitContainer.getMinDimensionsPair()); + static boolean shouldShowSplit(@NonNull SplitAttributes splitAttributes) { + return !(splitAttributes.getSplitType() instanceof ExpandContainersSplitType); } - static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule, + // TODO: expand this method to apply SplitLayoutCalculator#computeSplitAttributesForState + @NonNull + SplitAttributes computeSplitAttributes(@NonNull TaskProperties taskProperties, + @NonNull SplitRule rule, @Nullable Pair minDimensionsPair) { + final WindowMetrics taskWindowMetrics = getTaskWindowMetrics( + taskProperties.getConfiguration()); + final SplitAttributes splitAttributes; + if (rule.checkParentMetrics(taskWindowMetrics)) { + splitAttributes = rule.getDefaultSplitAttributes(); + } else { + splitAttributes = EXPAND_CONTAINERS_ATTRIBUTES; + } + return sanitizeSplitAttributes(taskProperties, splitAttributes, minDimensionsPair); + } + + /** + * Returns {@link #EXPAND_CONTAINERS_ATTRIBUTES} if the passed {@link SplitAttributes} doesn't + * meet the minimum dimensions set in {@link ActivityInfo.WindowLayout}. Otherwise, returns + * the passed {@link SplitAttributes}. + */ + @NonNull + private SplitAttributes sanitizeSplitAttributes(@NonNull TaskProperties taskProperties, + @NonNull SplitAttributes splitAttributes, @Nullable Pair minDimensionsPair) { - // TODO(b/190433398): Supply correct insets. - final WindowMetrics parentMetrics = new WindowMetrics(parentBounds, - new WindowInsets(new Rect())); - // 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 splitAttributes; } - return !boundsSmallerThanMinDimensions(primaryBounds, minDimensionsPair.first) - && !boundsSmallerThanMinDimensions(secondaryBounds, minDimensionsPair.second); + final FoldingFeature foldingFeature = getFoldingFeature(taskProperties); + final Configuration taskConfiguration = taskProperties.getConfiguration(); + final Rect primaryBounds = getPrimaryBounds(taskConfiguration, splitAttributes, + foldingFeature); + final Rect secondaryBounds = getSecondaryBounds(taskConfiguration, splitAttributes, + foldingFeature); + if (boundsSmallerThanMinDimensions(primaryBounds, minDimensionsPair.first) + || boundsSmallerThanMinDimensions(secondaryBounds, minDimensionsPair.second)) { + return EXPAND_CONTAINERS_ATTRIBUTES; + } + return splitAttributes; } @NonNull @@ -541,20 +601,25 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @VisibleForTesting @NonNull - static Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds, - @NonNull SplitRule rule, @NonNull Activity primaryActivity, - @Nullable Pair minDimensionsPair) { - if (!shouldShowSideBySide(parentBounds, rule, minDimensionsPair)) { + Rect getBoundsForPosition(@Position int position, @NonNull TaskProperties taskProperties, + @NonNull SplitAttributes splitAttributes) { + final Configuration taskConfiguration = taskProperties.getConfiguration(); + final FoldingFeature foldingFeature = getFoldingFeature(taskProperties); + final SplitType splitType = computeSplitType(splitAttributes, taskConfiguration, + foldingFeature); + final SplitAttributes computedSplitAttributes = new SplitAttributes.Builder() + .setSplitType(splitType) + .setLayoutDirection(splitAttributes.getLayoutDirection()) + .build(); + if (!shouldShowSplit(computedSplitAttributes)) { return new Rect(); } - final boolean isLtr = isLtr(primaryActivity, rule); - final float splitRatio = rule.getSplitRatio(); - switch (position) { case POSITION_START: - return getPrimaryBounds(parentBounds, splitRatio, isLtr); + return getPrimaryBounds(taskConfiguration, computedSplitAttributes, foldingFeature); case POSITION_END: - return getSecondaryBounds(parentBounds, splitRatio, isLtr); + return getSecondaryBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); case POSITION_FILL: default: return new Rect(); @@ -562,74 +627,303 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } @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, - (int) (parentBounds.left + parentBounds.width() * splitRatio), - parentBounds.bottom); - } - - private static Rect getRightContainerBounds(@NonNull Rect parentBounds, float splitRatio) { - return new Rect( - (int) (parentBounds.left + parentBounds.width() * splitRatio), - parentBounds.top, - parentBounds.right, - parentBounds.bottom); - } - - /** - * 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 static boolean isLtr(@NonNull Context context, @NonNull SplitRule rule) { - switch (rule.getLayoutDirection()) { - case LayoutDirection.LOCALE: - return context.getResources().getConfiguration().getLayoutDirection() + private Rect getPrimaryBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + if (!shouldShowSplit(splitAttributes)) { + return new Rect(); + } + switch (splitAttributes.getLayoutDirection()) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: { + return getLeftContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: { + return getRightContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.LOCALE: { + final boolean isLtr = taskConfiguration.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; - case LayoutDirection.RTL: - return false; - case LayoutDirection.LTR: + return isLtr + ? getLeftContainerBounds(taskConfiguration, splitAttributes, foldingFeature) + : getRightContainerBounds(taskConfiguration, splitAttributes, + foldingFeature); + } + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: { + return getTopContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: { + return getBottomContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } default: - return true; + throw new IllegalArgumentException("Unknown layout direction:" + + splitAttributes.getLayoutDirection()); } } @NonNull - static Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) { - return container.getTaskContainer().getTaskBounds(); + private Rect getSecondaryBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + if (!shouldShowSplit(splitAttributes)) { + return new Rect(); + } + switch (splitAttributes.getLayoutDirection()) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: { + return getRightContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: { + return getLeftContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.LOCALE: { + final boolean isLtr = taskConfiguration.getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; + return isLtr + ? getRightContainerBounds(taskConfiguration, splitAttributes, + foldingFeature) + : getLeftContainerBounds(taskConfiguration, splitAttributes, + foldingFeature); + } + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: { + return getBottomContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: { + return getTopContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + default: + throw new IllegalArgumentException("Unknown layout direction:" + + splitAttributes.getLayoutDirection()); + } } @NonNull - Rect getParentContainerBounds(@NonNull Activity activity) { - final TaskFragmentContainer container = mController.getContainerWithActivity(activity); - if (container != null) { - return getParentContainerBounds(container); - } - // Obtain bounds from Activity instead because the Activity hasn't been embedded yet. - return getNonEmbeddedActivityBounds(activity); + private Rect getLeftContainerBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int right = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, + CONTAINER_POSITION_LEFT, foldingFeature); + final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds(); + return new Rect(taskBounds.left, taskBounds.top, right, taskBounds.bottom); + } + + @NonNull + private Rect getRightContainerBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int left = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, + CONTAINER_POSITION_RIGHT, foldingFeature); + final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); + return new Rect(left, parentBounds.top, parentBounds.right, parentBounds.bottom); + } + + @NonNull + private Rect getTopContainerBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int bottom = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, + CONTAINER_POSITION_TOP, foldingFeature); + final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); + return new Rect(parentBounds.left, parentBounds.top, parentBounds.right, bottom); + } + + @NonNull + private Rect getBottomContainerBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int top = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, + CONTAINER_POSITION_BOTTOM, foldingFeature); + final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); + return new Rect(parentBounds.left, top, parentBounds.right, parentBounds.bottom); } /** - * Obtains the bounds from a non-embedded Activity. - *

- * Note that callers should use {@link #getParentContainerBounds(Activity)} instead for most - * cases unless we want to obtain task bounds before - * {@link TaskContainer#isTaskBoundsInitialized()}. + * Computes the boundary position between the primary and the secondary containers for the given + * {@link ContainerPosition} with {@link SplitAttributes}, current window and device states. + *

    + *
  1. For {@link #CONTAINER_POSITION_TOP}, it computes the boundary with the bottom + * container, which is {@link Rect#bottom} of the top container bounds.
  2. + *
  3. For {@link #CONTAINER_POSITION_BOTTOM}, it computes the boundary with the top + * container, which is {@link Rect#top} of the bottom container bounds.
  4. + *
  5. For {@link #CONTAINER_POSITION_LEFT}, it computes the boundary with the right + * container, which is {@link Rect#right} of the left container bounds.
  6. + *
  7. For {@link #CONTAINER_POSITION_RIGHT}, it computes the boundary with the bottom + * container, which is {@link Rect#left} of the right container bounds.
  8. + *
+ * + * @see #getTopContainerBounds(Configuration, SplitAttributes, FoldingFeature) + * @see #getBottomContainerBounds(Configuration, SplitAttributes, FoldingFeature) + * @see #getLeftContainerBounds(Configuration, SplitAttributes, FoldingFeature) + * @see #getRightContainerBounds(Configuration, SplitAttributes, FoldingFeature) */ + private int computeBoundaryBetweenContainers(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @ContainerPosition int position, + @Nullable FoldingFeature foldingFeature) { + final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); + final int startPoint = shouldSplitHorizontally(splitAttributes) + ? parentBounds.top + : parentBounds.left; + final int dimen = shouldSplitHorizontally(splitAttributes) + ? parentBounds.height() + : parentBounds.width(); + final SplitType splitType = splitAttributes.getSplitType(); + if (splitType instanceof RatioSplitType) { + final RatioSplitType splitRatio = (RatioSplitType) splitType; + return (int) (startPoint + dimen * splitRatio.getRatio()); + } + // At this point, SplitType must be a HingeSplitType and foldingFeature must be + // non-null. RatioSplitType and ExpandContainerSplitType have been handled earlier. + Objects.requireNonNull(foldingFeature); + if (!(splitType instanceof HingeSplitType)) { + throw new IllegalArgumentException("Unknown splitType:" + splitType); + } + final Rect hingeArea = foldingFeature.getBounds(); + switch (position) { + case CONTAINER_POSITION_LEFT: + return hingeArea.left; + case CONTAINER_POSITION_TOP: + return hingeArea.top; + case CONTAINER_POSITION_RIGHT: + return hingeArea.right; + case CONTAINER_POSITION_BOTTOM: + return hingeArea.bottom; + default: + throw new IllegalArgumentException("Unknown position:" + position); + } + } + + @Nullable + private FoldingFeature getFoldingFeature(@NonNull TaskProperties taskProperties) { + final int displayId = taskProperties.getDisplayId(); + final WindowConfiguration windowConfiguration = taskProperties.getConfiguration() + .windowConfiguration; + final WindowLayoutInfo info = mController.mWindowLayoutComponent + .getCurrentWindowLayoutInfo(displayId, windowConfiguration); + final List displayFeatures = info.getDisplayFeatures(); + if (displayFeatures.isEmpty()) { + return null; + } + final List foldingFeatures = new ArrayList<>(); + for (DisplayFeature displayFeature : displayFeatures) { + if (displayFeature instanceof FoldingFeature) { + foldingFeatures.add((FoldingFeature) displayFeature); + } + } + // TODO(b/240219484): Support device with multiple hinges. + if (foldingFeatures.size() != 1) { + return null; + } + return foldingFeatures.get(0); + } + + /** + * Indicates that this {@link SplitAttributes} splits the task horizontally. Returns + * {@code false} if this {@link SplitAttributes} splits the task vertically. + */ + private static boolean shouldSplitHorizontally(SplitAttributes splitAttributes) { + switch (splitAttributes.getLayoutDirection()) { + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: + return true; + default: + return false; + } + } + + /** + * Computes the {@link SplitType} with the {@link SplitAttributes} and the current device and + * window state. + * If passed {@link SplitAttributes#getSplitType} is a {@link RatioSplitType}. It reversed + * the ratio if the computed {@link SplitAttributes#getLayoutDirection} is + * {@link SplitAttributes.LayoutDirection.LEFT_TO_RIGHT} or + * {@link SplitAttributes.LayoutDirection.BOTTOM_TO_TOP} to make the bounds calculation easier. + * If passed {@link SplitAttributes#getSplitType} is a {@link HingeSplitType}, it checks + * the current device and window states to determine whether the split container should split + * by hinge or use {@link HingeSplitType#getFallbackSplitType}. + */ + private SplitType computeSplitType(@NonNull SplitAttributes splitAttributes, + @NonNull Configuration taskConfiguration, @Nullable FoldingFeature foldingFeature) { + final int layoutDirection = splitAttributes.getLayoutDirection(); + final SplitType splitType = splitAttributes.getSplitType(); + if (splitType instanceof ExpandContainersSplitType) { + return splitType; + } else if (splitType instanceof RatioSplitType) { + final RatioSplitType splitRatio = (RatioSplitType) splitType; + // Reverse the ratio for RIGHT_TO_LEFT and BOTTOM_TO_TOP to make the boundary + // computation have the same direction, which is from (top, left) to (bottom, right). + final SplitType reversedSplitType = new RatioSplitType(1 - splitRatio.getRatio()); + switch (layoutDirection) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: + return splitType; + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: + return reversedSplitType; + case LayoutDirection.LOCALE: { + boolean isLtr = taskConfiguration.getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; + return isLtr ? splitType : reversedSplitType; + } + } + } else if (splitType instanceof HingeSplitType) { + final HingeSplitType hinge = (HingeSplitType) splitType; + @WindowingMode + final int windowingMode = taskConfiguration.windowConfiguration.getWindowingMode(); + return shouldSplitByHinge(splitAttributes, foldingFeature, windowingMode) + ? hinge : hinge.getFallbackSplitType(); + } + throw new IllegalArgumentException("Unknown SplitType:" + splitType); + } + + private static boolean shouldSplitByHinge(@NonNull SplitAttributes splitAttributes, + @Nullable FoldingFeature foldingFeature, @WindowingMode int taskWindowingMode) { + // Only HingeSplitType may split the task bounds by hinge. + if (!(splitAttributes.getSplitType() instanceof HingeSplitType)) { + return false; + } + // Device is not foldable, so there's no hinge to match. + if (foldingFeature == null) { + return false; + } + // The task is in multi-window mode. Match hinge doesn't make sense because current task + // bounds may not fit display bounds. + if (WindowConfiguration.inMultiWindowMode(taskWindowingMode)) { + return false; + } + // Return true if how the split attributes split the task bounds matches the orientation of + // folding area orientation. + return shouldSplitHorizontally(splitAttributes) == isFoldingAreaHorizontal(foldingFeature); + } + + private static boolean isFoldingAreaHorizontal(@NonNull FoldingFeature foldingFeature) { + final Rect bounds = foldingFeature.getBounds(); + return bounds.width() > bounds.height(); + } + + @NonNull + static TaskProperties getTaskProperties(@NonNull TaskFragmentContainer container) { + return container.getTaskContainer().getTaskProperties(); + } + + @NonNull + TaskProperties getTaskProperties(@NonNull Activity activity) { + final TaskContainer taskContainer = mController.getTaskContainer( + mController.getTaskId(activity)); + if (taskContainer != null) { + return taskContainer.getTaskProperties(); + } + // Use a copy of configuration because activity's configuration may be updated later, + // or we may get unexpected TaskContainer's configuration if Activity's configuration is + // updated. An example is Activity is going to be in split. + return new TaskProperties(activity.getDisplayId(), + new Configuration(activity.getResources().getConfiguration())); + } + + @NonNull + WindowMetrics getTaskWindowMetrics(@NonNull Activity activity) { + return getTaskWindowMetrics(getTaskProperties(activity).getConfiguration()); + } + + @NonNull + private static WindowMetrics getTaskWindowMetrics(@NonNull Configuration taskConfiguration) { + final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds(); + // TODO(b/190433398): Supply correct insets. + return new WindowMetrics(taskBounds, WindowInsets.CONSUMED); + } + + /** Obtains the bounds from a non-embedded Activity. */ @NonNull static Rect getNonEmbeddedActivityBounds(@NonNull Activity activity) { final WindowConfiguration windowConfiguration = diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 95486fa0e4404..91573ffef568a 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -132,6 +132,11 @@ class TaskContainer { return new Configuration(mConfiguration); } + @NonNull + TaskProperties getTaskProperties() { + return new TaskProperties(mDisplayId, mConfiguration); + } + void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) { mConfiguration.setTo(info.getConfiguration()); mDisplayId = info.getDisplayId(); @@ -215,4 +220,28 @@ class TaskContainer { int indexOf(@NonNull TaskFragmentContainer child) { return mContainers.indexOf(child); } + + /** + * A wrapper class which contains the display ID and {@link Configuration} of a + * {@link TaskContainer} + */ + static final class TaskProperties { + private final int mDisplayId; + @NonNull + private final Configuration mConfiguration; + + TaskProperties(int displayId, @NonNull Configuration configuration) { + mDisplayId = displayId; + mConfiguration = configuration; + } + + int getDisplayId() { + return mDisplayId; + } + + @NonNull + Configuration getConfiguration() { + return mConfiguration; + } + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index d78331737fdc6..c76f568e117f6 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -69,6 +69,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { private final DataProducer> mFoldingFeatureProducer; + private final List mLastReportedFoldingFeatures = new ArrayList<>(); + private final Map mWindowContextConfigListeners = new ArrayMap<>(); @@ -192,6 +194,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } private void onDisplayFeaturesChanged(List storedFeatures) { + mLastReportedFoldingFeatures.clear(); + mLastReportedFoldingFeatures.addAll(storedFeatures); for (Context context : getContextsListeningForLayoutChanges()) { // Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer. Consumer layoutConsumer = mWindowLayoutChangeListeners.get(context); @@ -212,6 +216,27 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { return new WindowLayoutInfo(displayFeatureList); } + /** + * Gets the current {@link WindowLayoutInfo} computed with passed {@link WindowConfiguration}. + * + * @return current {@link WindowLayoutInfo} on the default display. Returns + * empty {@link WindowLayoutInfo} on secondary displays. + */ + @NonNull + public WindowLayoutInfo getCurrentWindowLayoutInfo(int displayId, + @NonNull WindowConfiguration windowConfiguration) { + return getWindowLayoutInfo(displayId, windowConfiguration, mLastReportedFoldingFeatures); + } + + /** @see #getWindowLayoutInfo(Context, List) */ + private WindowLayoutInfo getWindowLayoutInfo(int displayId, + @NonNull WindowConfiguration windowConfiguration, + List storedFeatures) { + List displayFeatureList = getDisplayFeatures(displayId, windowConfiguration, + storedFeatures); + return new WindowLayoutInfo(displayFeatureList); + } + /** * Translate from the {@link CommonFoldingFeature} to * {@link DisplayFeature} for a given {@link Activity}. If a 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 index b835a050e34bf..40f7a273980a3 100644 --- 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 @@ -36,26 +36,61 @@ import android.util.Pair; import android.window.TaskFragmentInfo; import android.window.WindowContainerToken; +import androidx.window.extensions.embedding.SplitAttributes.SplitType; +import androidx.window.extensions.layout.DisplayFeature; +import androidx.window.extensions.layout.FoldingFeature; +import androidx.window.extensions.layout.WindowLayoutInfo; + +import java.util.ArrayList; import java.util.Collections; +import java.util.List; 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; + static final SplitType SPLIT_TYPE = SplitType.RatioSplitType.splitEqually(); + static final SplitAttributes SPLIT_ATTRIBUTES = new SplitAttributes.Builder().build(); + static final String TEST_TAG = "test"; /** 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 static final float SPLIT_RATIO = 0.5f; 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 getSplitBounds(isPrimary, false /* shouldSplitHorizontally */); + } + + /** Gets the bounds of a TaskFragment that is in split. */ + static Rect getSplitBounds(boolean isPrimary, boolean shouldSplitHorizontally) { + final int dimension = (int) ( + (shouldSplitHorizontally ? TASK_BOUNDS.height() : TASK_BOUNDS.width()) + * SPLIT_RATIO); + if (shouldSplitHorizontally) { + return isPrimary + ? new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top, + TASK_BOUNDS.right, + TASK_BOUNDS.top + dimension) + : new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top + dimension, + TASK_BOUNDS.right, + TASK_BOUNDS.bottom); + } return isPrimary - ? new Rect(TASK_BOUNDS.left, TASK_BOUNDS.top, TASK_BOUNDS.left + width, - TASK_BOUNDS.bottom) + ? new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top, + TASK_BOUNDS.left + dimension, + TASK_BOUNDS.bottom) : new Rect( - TASK_BOUNDS.left + width, TASK_BOUNDS.top, TASK_BOUNDS.right, + TASK_BOUNDS.left + dimension, + TASK_BOUNDS.top, + TASK_BOUNDS.right, TASK_BOUNDS.bottom); } @@ -73,10 +108,15 @@ public class EmbeddingTestUtils { activityPair -> false, targetPair::equals, w -> true) - .setSplitRatio(SPLIT_RATIO) + .setDefaultSplitAttributes( + new SplitAttributes.Builder() + .setSplitType(SPLIT_TYPE) + .build() + ) .setShouldClearTop(clearTop) .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setTag(TEST_TAG) .build(); } @@ -105,10 +145,15 @@ public class EmbeddingTestUtils { targetPair::equals, activityIntentPair -> false, w -> true) - .setSplitRatio(SPLIT_RATIO) + .setDefaultSplitAttributes( + new SplitAttributes.Builder() + .setSplitType(SPLIT_TYPE) + .build() + ) .setFinishPrimaryWithSecondary(finishPrimaryWithSecondary) .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary) .setShouldClearTop(clearTop) + .setTag(TEST_TAG) .build(); } @@ -144,4 +189,19 @@ public class EmbeddingTestUtils { return new TaskContainer(TASK_ID, activity); } + + static WindowLayoutInfo createWindowLayoutInfo() { + final FoldingFeature foldingFeature = new FoldingFeature( + new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top + TASK_BOUNDS.height() / 2 - 5, + TASK_BOUNDS.right, + TASK_BOUNDS.top + TASK_BOUNDS.height() / 2 + 5 + ), + FoldingFeature.TYPE_HINGE, + FoldingFeature.STATE_HALF_OPENED); + final List displayFeatures = new ArrayList<>(); + displayFeatures.add(foldingFeature); + return new WindowLayoutInfo(displayFeatures); + } } 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 8a8b5d94e9432..3b52a3d0055cc 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 @@ -28,7 +28,7 @@ import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_I import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT; -import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_RATIO; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_ATTRIBUTES; 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.createActivityInfoWithMinDimensions; @@ -85,6 +85,8 @@ import android.window.WindowContainerTransaction; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.window.extensions.layout.WindowLayoutComponentImpl; +import androidx.window.extensions.layout.WindowLayoutInfo; import org.junit.Before; import org.junit.Test; @@ -120,6 +122,8 @@ public class SplitControllerTest { private WindowContainerTransaction mTransaction; @Mock private Handler mHandler; + @Mock + private WindowLayoutComponentImpl mWindowLayoutComponent; private SplitController mSplitController; private SplitPresenter mSplitPresenter; @@ -127,7 +131,9 @@ public class SplitControllerTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); - mSplitController = new SplitController(); + doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + mSplitController = new SplitController(mWindowLayoutComponent); mSplitPresenter = mSplitController.mPresenter; spyOn(mSplitController); spyOn(mSplitPresenter); @@ -273,6 +279,8 @@ public class SplitControllerTest { final SplitContainer splitContainer = mock(SplitContainer.class); doReturn(tf).when(splitContainer).getPrimaryContainer(); doReturn(tf).when(splitContainer).getSecondaryContainer(); + doReturn(createTestTaskContainer()).when(splitContainer).getTaskContainer(); + doReturn(createSplitRule(mActivity, mActivity)).when(splitContainer).getSplitRule(); final List splitContainers = mSplitController.getTaskContainer(TASK_ID).mSplitContainers; splitContainers.add(splitContainer); @@ -303,7 +311,7 @@ public class SplitControllerTest { // Verify if the top active split is updated if both of its containers are not finished. doReturn(false).when(mSplitController) - .dismissPlaceholderIfNecessary(mTransaction, splitContainer); + .dismissPlaceholderIfNecessary(mTransaction, splitContainer); mSplitController.updateContainer(mTransaction, tf); @@ -613,7 +621,7 @@ public class SplitControllerTest { assertTrue(result); verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT, mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), - placeholderRule, true /* isPlaceholder */); + placeholderRule, SPLIT_ATTRIBUTES, true /* isPlaceholder */); } @Test @@ -629,7 +637,7 @@ public class SplitControllerTest { assertFalse(result); verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(), - anyBoolean()); + any(), anyBoolean()); } @Test @@ -646,7 +654,7 @@ public class SplitControllerTest { assertTrue(result); verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT, mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), - placeholderRule, true /* isPlaceholder */); + placeholderRule, SPLIT_ATTRIBUTES, true /* isPlaceholder */); } @Test @@ -661,7 +669,7 @@ public class SplitControllerTest { assertFalse(result); verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(), - anyBoolean()); + any(), anyBoolean()); } @Test @@ -679,7 +687,7 @@ public class SplitControllerTest { assertTrue(result); verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT, mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), - placeholderRule, true /* isPlaceholder */); + placeholderRule, SPLIT_ATTRIBUTES, true /* isPlaceholder */); } @Test @@ -698,14 +706,15 @@ public class SplitControllerTest { primaryContainer, mActivity, secondaryContainer, - splitRule); + splitRule, + SPLIT_ATTRIBUTES); clearInvocations(mSplitController); final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); - verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); + verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } @Test @@ -725,7 +734,8 @@ public class SplitControllerTest { primaryContainer, mActivity, secondaryContainer, - splitRule); + splitRule, + SPLIT_ATTRIBUTES); final Activity launchedActivity = createMockActivity(); primaryContainer.addPendingAppearedActivity(launchedActivity); @@ -746,7 +756,7 @@ public class SplitControllerTest { assertTrue(result); verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); - verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); + verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } @Test @@ -783,7 +793,8 @@ public class SplitControllerTest { primaryContainer, mActivity, secondaryContainer, - placeholderRule); + placeholderRule, + SPLIT_ATTRIBUTES); final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); @@ -1103,6 +1114,7 @@ public class SplitControllerTest { doReturn(activity).when(mSplitController).getActivity(activityToken); doReturn(TASK_ID).when(activity).getTaskId(); doReturn(new ActivityInfo()).when(activity).getActivityInfo(); + doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId(); return activity; } @@ -1141,7 +1153,7 @@ public class SplitControllerTest { private void setupPlaceholderRule(@NonNull Activity primaryActivity) { final SplitRule placeholderRule = new SplitPlaceholderRule.Builder(PLACEHOLDER_INTENT, primaryActivity::equals, i -> false, w -> true) - .setSplitRatio(SPLIT_RATIO) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) .build(); mSplitController.setEmbeddingRules(Collections.singleton(placeholderRule)); } @@ -1194,7 +1206,8 @@ public class SplitControllerTest { primaryContainer, primaryContainer.getTopNonFinishingActivity(), secondaryContainer, - rule); + rule, + SPLIT_ATTRIBUTES); // We need to set those in case we are not respecting clear top. // TODO(b/231845476) we should always respect clearTop. 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 25f0e25eec75c..78ac91cc2e27d 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 @@ -16,13 +16,17 @@ package androidx.window.extensions.embedding; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.view.Display.DEFAULT_DISPLAY; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_ATTRIBUTES; 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.createActivityInfoWithMinDimensions; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createWindowLayoutInfo; 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; @@ -30,9 +34,7 @@ import static androidx.window.extensions.embedding.SplitPresenter.POSITION_START import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPANDED; import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO; import static androidx.window.extensions.embedding.SplitPresenter.RESULT_NOT_EXPANDED; -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; @@ -58,7 +60,6 @@ import android.content.res.Resources; import android.graphics.Rect; import android.os.IBinder; import android.platform.test.annotations.Presubmit; -import android.util.Pair; import android.util.Size; import android.window.TaskFragmentInfo; import android.window.WindowContainerTransaction; @@ -66,6 +67,8 @@ import android.window.WindowContainerTransaction; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.window.extensions.layout.WindowLayoutComponentImpl; +import androidx.window.extensions.layout.WindowLayoutInfo; import org.junit.Before; import org.junit.Test; @@ -73,6 +76,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; + /** * Test class for {@link SplitPresenter}. * @@ -94,13 +99,17 @@ public class SplitPresenterTest { private TaskFragmentInfo mTaskFragmentInfo; @Mock private WindowContainerTransaction mTransaction; + @Mock + private WindowLayoutComponentImpl mWindowLayoutComponent; private SplitController mController; private SplitPresenter mPresenter; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mController = new SplitController(); + doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + mController = new SplitController(mWindowLayoutComponent); mPresenter = mController.mPresenter; spyOn(mController); spyOn(mPresenter); @@ -162,59 +171,263 @@ public class SplitPresenterTest { @Test public void testShouldShowSideBySide() { - Activity secondaryActivity = createMockActivity(); - final SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); + assertTrue(SplitPresenter.shouldShowSplit(SPLIT_ATTRIBUTES)); - assertTrue(shouldShowSideBySide(TASK_BOUNDS, splitRule)); + final SplitAttributes expandContainers = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType()) + .build(); - // Set minDimensions of primary container to larger than primary bounds. - final Rect primaryBounds = getSplitBounds(true /* isPrimary */); - Pair minDimensionsPair = new Pair<>( - new Size(primaryBounds.width() + 1, primaryBounds.height() + 1), null); - - assertFalse(shouldShowSideBySide(TASK_BOUNDS, splitRule, minDimensionsPair)); + assertFalse(SplitPresenter.shouldShowSplit(expandContainers)); } @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 */); + public void testGetBoundsForPosition_expandContainers() { + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType()) + .build(); + + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetBoundsForPosition_splitVertically() { + final Rect primaryBounds = getSplitBounds(true /* isPrimary */, + false /* splitHorizontally */); + final Rect secondaryBounds = getSplitBounds(false /* isPrimary */, + false /* splitHorizontally */); + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) + .build(); assertEquals("Primary bounds must be reported.", primaryBounds, - getBoundsForPosition(POSITION_START, TASK_BOUNDS, splitRule, - mActivity, null /* miniDimensionsPair */)); + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); assertEquals("Secondary bounds must be reported.", secondaryBounds, - getBoundsForPosition(POSITION_END, TASK_BOUNDS, splitRule, - mActivity, null /* miniDimensionsPair */)); + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); assertEquals("Task bounds must be reported.", new Rect(), - getBoundsForPosition(POSITION_FILL, TASK_BOUNDS, splitRule, - mActivity, null /* miniDimensionsPair */)); + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); - Pair minDimensionsPair = new Pair<>( - new Size(primaryBounds.width() + 1, primaryBounds.height() + 1), null); + splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.RIGHT_TO_LEFT) + .build(); - assertEquals("Fullscreen bounds must be reported because of min dimensions.", + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", new Rect(), - getBoundsForPosition(POSITION_START, TASK_BOUNDS, - splitRule, mActivity, minDimensionsPair)); + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE) + .build(); + // Layout direction should follow screen layout for SplitAttributes.LayoutDirection.LOCALE. + taskProperties.getConfiguration().screenLayout |= Configuration.SCREENLAYOUT_LAYOUTDIR_RTL; + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetBoundsForPosition_splitHorizontally() { + final Rect primaryBounds = getSplitBounds(true /* isPrimary */, + true /* splitHorizontally */); + final Rect secondaryBounds = getSplitBounds(false /* isPrimary */, + true /* splitHorizontally */); + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM) + .build(); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.BOTTOM_TO_TOP) + .build(); + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetBoundsForPosition_useHingeFallback() { + final Rect primaryBounds = getSplitBounds(true /* isPrimary */, + false /* splitHorizontally */); + final Rect secondaryBounds = getSplitBounds(false /* isPrimary */, + false /* splitHorizontally */); + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.HingeSplitType( + SplitAttributes.SplitType.RatioSplitType.splitEqually() + )).setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) + .build(); + + // There's no hinge on the device. Use fallback SplitType. + doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + + assertEquals("PrimaryBounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("SecondaryBounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + // Hinge is reported, but the host task is in multi-window mode. Still use fallback + // splitType. + doReturn(createWindowLayoutInfo()).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + taskProperties.getConfiguration().windowConfiguration + .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + + assertEquals("PrimaryBounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("SecondaryBounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + // Hinge is reported, and the host task is in fullscreen, but layout direction doesn't match + // folding area orientation. Still use fallback splitType. + doReturn(createWindowLayoutInfo()).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + taskProperties.getConfiguration().windowConfiguration + .setWindowingMode(WINDOWING_MODE_FULLSCREEN); + + assertEquals("PrimaryBounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("SecondaryBounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetBoundsForPosition_fallbackToExpandContainers() { + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.HingeSplitType( + new SplitAttributes.SplitType.ExpandContainersSplitType() + )).setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) + .build(); + + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetBoundsForPosition_useHingeSplitType() { + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.HingeSplitType( + new SplitAttributes.SplitType.ExpandContainersSplitType() + )).setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM) + .build(); + final WindowLayoutInfo windowLayoutInfo = createWindowLayoutInfo(); + doReturn(windowLayoutInfo).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + final Rect hingeBounds = windowLayoutInfo.getDisplayFeatures().get(0).getBounds(); + final Rect primaryBounds = new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top, + TASK_BOUNDS.right, + hingeBounds.top + ); + final Rect secondaryBounds = new Rect( + TASK_BOUNDS.left, + hingeBounds.bottom, + TASK_BOUNDS.right, + TASK_BOUNDS.bottom + ); + + assertEquals("PrimaryBounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("SecondaryBounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); } @Test public void testExpandSplitContainerIfNeeded() { - SplitContainer splitContainer = mock(SplitContainer.class); Activity secondaryActivity = createMockActivity(); SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID); TaskFragmentContainer secondaryTf = mController.newContainer(secondaryActivity, TASK_ID); - doReturn(splitRule).when(splitContainer).getSplitRule(); - doReturn(primaryTf).when(splitContainer).getPrimaryContainer(); - doReturn(secondaryTf).when(splitContainer).getSecondaryContainer(); + SplitContainer splitContainer = new SplitContainer(primaryTf, secondaryActivity, + secondaryTf, splitRule, SPLIT_ATTRIBUTES); assertThrows(IllegalArgumentException.class, () -> mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity, @@ -224,11 +437,13 @@ public class SplitPresenterTest { splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); verify(mPresenter, never()).expandTaskFragment(any(), any()); + splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES); doReturn(createActivityInfoWithMinDimensions()).when(secondaryActivity).getActivityInfo(); assertEquals(RESULT_EXPAND_FAILED_NO_TF_INFO, mPresenter.expandSplitContainerIfNeeded( mTransaction, splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); + splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES); primaryTf.setInfo(mTransaction, createMockTaskFragmentInfo(primaryTf, mActivity)); secondaryTf.setInfo(mTransaction, createMockTaskFragmentInfo(secondaryTf, secondaryActivity)); @@ -238,6 +453,7 @@ public class SplitPresenterTest { verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken()); verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken()); + splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES); clearInvocations(mPresenter); assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, @@ -256,6 +472,7 @@ public class SplitPresenterTest { final SplitPairRule rule = new SplitPairRule.Builder(pair -> pair.first == mActivity && pair.second == secondaryActivity, pair -> false, metrics -> true) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) .setShouldClearTop(false) .build(); @@ -279,4 +496,10 @@ public class SplitPresenterTest { doReturn(mock(IBinder.class)).when(activity).getActivityToken(); return activity; } + + private static TaskContainer.TaskProperties getTaskProperty() { + final Configuration configuration = new Configuration(); + configuration.windowConfiguration.setBounds(TASK_BOUNDS); + return new TaskContainer.TaskProperties(DEFAULT_DISPLAY, configuration); + } }