Extend bounds computation to support horizontal layout

This CL introduces SplitLayout and update bounds computation methods
to support horizontal layout. It also introduces #computeSplitLayout
to compute the current split layout, which always returns the default
splitLayout currently. #computeSplitLayout will extend to consider
SplitLayoutCalculator in later CL.

fixes: 241042437
Bug: 241043028
Bug: 207494880
Test: atest SplitPresenterTest
Change-Id: I037a2b2fa38d05c4fe97e4734cc044148ad15806
This commit is contained in:
Charles Chen
2022-08-08 15:57:19 +08:00
parent fe23ec48f5
commit eaeeaa5608
9 changed files with 935 additions and 225 deletions

View File

@@ -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);

View File

@@ -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<Size, Size> getMinDimensionsPair() {
@@ -141,6 +167,7 @@ class SplitContainer {
+ " primaryContainer=" + mPrimaryContainer
+ " secondaryContainer=" + mSecondaryContainer
+ " splitRule=" + mSplitRule
+ " splitAttributes" + mSplitAttributes
+ "}";
}
}

View File

@@ -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<Exception> failureCallback, boolean isPlaceholder) {
@NonNull SplitAttributes splitAttributes, @Nullable Consumer<Exception> 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<Size, Size> 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<Size, Size> 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<SplitInfo> getActiveSplitStates() {
List<SplitInfo> 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();
}
/**

View File

@@ -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<Size, Size> 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<Size, Size> 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<Size, Size> 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<Size, Size> 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<Size, Size> 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<Size, Size> 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<Size, Size> 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<Size, Size> 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<Size, Size> 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.
* <p>
* 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.
* <ol>
* <li>For {@link #CONTAINER_POSITION_TOP}, it computes the boundary with the bottom
* container, which is {@link Rect#bottom} of the top container bounds.</li>
* <li>For {@link #CONTAINER_POSITION_BOTTOM}, it computes the boundary with the top
* container, which is {@link Rect#top} of the bottom container bounds.</li>
* <li>For {@link #CONTAINER_POSITION_LEFT}, it computes the boundary with the right
* container, which is {@link Rect#right} of the left container bounds.</li>
* <li>For {@link #CONTAINER_POSITION_RIGHT}, it computes the boundary with the bottom
* container, which is {@link Rect#left} of the right container bounds.</li>
* </ol>
*
* @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<DisplayFeature> displayFeatures = info.getDisplayFeatures();
if (displayFeatures.isEmpty()) {
return null;
}
final List<FoldingFeature> 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 =

View File

@@ -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;
}
}
}

View File

@@ -69,6 +69,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer;
private final List<CommonFoldingFeature> mLastReportedFoldingFeatures = new ArrayList<>();
private final Map<IBinder, WindowContextConfigListener> mWindowContextConfigListeners =
new ArrayMap<>();
@@ -192,6 +194,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
}
private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) {
mLastReportedFoldingFeatures.clear();
mLastReportedFoldingFeatures.addAll(storedFeatures);
for (Context context : getContextsListeningForLayoutChanges()) {
// Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer.
Consumer<WindowLayoutInfo> 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<CommonFoldingFeature> storedFeatures) {
List<DisplayFeature> displayFeatureList = getDisplayFeatures(displayId, windowConfiguration,
storedFeatures);
return new WindowLayoutInfo(displayFeatureList);
}
/**
* Translate from the {@link CommonFoldingFeature} to
* {@link DisplayFeature} for a given {@link Activity}. If a

View File

@@ -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<DisplayFeature> displayFeatures = new ArrayList<>();
displayFeatures.add(foldingFeature);
return new WindowLayoutInfo(displayFeatures);
}
}

View File

@@ -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<SplitContainer> 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.

View File

@@ -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<Size, Size> 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<Size, Size> 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);
}
}