blob: 362f1fa096cceb63dedd1420c86efabee7d3996f [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.window.extensions.embedding;
import static android.content.pm.PackageManager.MATCH_ALL;
import android.app.Activity;
import android.app.ActivityThread;
import android.app.WindowConfiguration;
import android.app.WindowConfiguration.WindowingMode;
import android.content.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;
import android.util.LayoutDirection;
import android.util.Pair;
import android.util.Size;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowMetrics;
import android.window.WindowContainerTransaction;
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.SplitAttributesCalculator.SplitAttributesCalculatorParams;
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;
/**
* Controls the visual presentation of the splits according to the containers formed by
* {@link SplitController}.
*/
class SplitPresenter extends JetpackTaskFragmentOrganizer {
@VisibleForTesting
static final int POSITION_START = 0;
@VisibleForTesting
static final int POSITION_END = 1;
@VisibleForTesting
static final int POSITION_FILL = 2;
@IntDef(value = {
POSITION_START,
POSITION_END,
POSITION_FILL,
})
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 #shouldShowSplit(SplitAttributes)} and minimum dimensions is
* satisfied.
*/
static final int RESULT_NOT_EXPANDED = 0;
/**
* Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
* Activity, Activity, Intent)}.
* The splitContainer should be expanded. It is usually because minimum dimensions is not
* satisfied.
* @see #shouldShowSplit(SplitAttributes)
*/
static final int RESULT_EXPANDED = 1;
/**
* Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
* Activity, Activity, Intent)}.
* The splitContainer should be expanded, but the client side hasn't received
* {@link android.window.TaskFragmentInfo} yet. Fallback to create new expanded SplitContainer
* instead.
*/
static final int RESULT_EXPAND_FAILED_NO_TF_INFO = 2;
/**
* Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
* Activity, Activity, Intent)}
*/
@IntDef(value = {
RESULT_NOT_EXPANDED,
RESULT_EXPANDED,
RESULT_EXPAND_FAILED_NO_TF_INFO,
})
private @interface ResultCode {}
@VisibleForTesting
static final SplitAttributes EXPAND_CONTAINERS_ATTRIBUTES =
new SplitAttributes.Builder()
.setSplitType(new ExpandContainersSplitType())
.build();
private final SplitController mController;
SplitPresenter(@NonNull Executor executor, @NonNull SplitController controller) {
super(executor, controller);
mController = controller;
registerOrganizer();
}
/**
* Deletes the specified container and all other associated and dependent containers in the same
* transaction.
*/
void cleanupContainer(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container, boolean shouldFinishDependent) {
container.finish(shouldFinishDependent, this, wct, mController);
final TaskFragmentContainer newTopContainer = mController.getTopActiveContainer(
container.getTaskId());
if (newTopContainer != null) {
mController.updateContainer(wct, newTopContainer);
}
}
/**
* Creates a new split with the primary activity and an empty secondary container.
* @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 TaskProperties taskProperties = getTaskProperties(primaryActivity);
final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(
primaryActivity, secondaryIntent);
final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule,
minDimensionsPair);
final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties,
splitAttributes);
final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
primaryActivity, primaryRectBounds, null);
// Create new empty task fragment
final int taskId = primaryContainer.getTaskId();
final TaskFragmentContainer secondaryContainer = mController.newContainer(
secondaryIntent, primaryActivity, taskId);
final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties,
splitAttributes);
final int windowingMode = mController.getTaskContainer(taskId)
.getWindowingModeForSplitTaskFragment(secondaryRectBounds);
createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(),
primaryActivity.getActivityToken(), secondaryRectBounds,
windowingMode);
// Set adjacent to each other so that the containers below will be invisible.
setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule,
splitAttributes);
mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule,
splitAttributes);
return secondaryContainer;
}
/**
* Creates a new split container with the two provided activities.
* @param primaryActivity An activity that should be in the primary container. If it is not
* currently in an existing container, a new one will be created and the
* activity will be re-parented to it.
* @param secondaryActivity An activity that should be in the secondary container. If it is not
* currently in an existing container, or if it is currently in the
* same container as the primary activity, a new container will be
* created and the activity will be re-parented to it.
* @param rule The split rule to be applied to the container.
*/
@GuardedBy("mController.mLock")
void createNewSplitContainer(@NonNull WindowContainerTransaction wct,
@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity,
@NonNull SplitPairRule rule) {
final TaskProperties taskProperties = getTaskProperties(primaryActivity);
final Pair<Size, Size> minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity,
secondaryActivity);
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, taskProperties,
splitAttributes);
final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity(
secondaryActivity);
TaskFragmentContainer containerToAvoid = primaryContainer;
if (curSecondaryContainer != null && curSecondaryContainer != primaryContainer
&& (rule.shouldClearTop() || primaryContainer.isAbove(curSecondaryContainer))) {
// Do not reuse the current TaskFragment if the rule is to clear top, or if it is below
// the primary TaskFragment.
containerToAvoid = curSecondaryContainer;
}
final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct,
secondaryActivity, secondaryRectBounds, containerToAvoid);
// Set adjacent to each other so that the containers below will be invisible.
setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule,
splitAttributes);
mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule,
splitAttributes);
}
/**
* Creates a new container or resizes an existing container for activity to the provided bounds.
* @param activity The activity to be re-parented to the container if necessary.
* @param containerToAvoid Re-parent from this container if an activity is already in it.
*/
private TaskFragmentContainer prepareContainerForActivity(
@NonNull WindowContainerTransaction wct, @NonNull Activity activity,
@NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid) {
TaskFragmentContainer container = mController.getContainerWithActivity(activity);
final int taskId = container != null ? container.getTaskId() : activity.getTaskId();
if (container == null || container == containerToAvoid) {
container = mController.newContainer(activity, taskId);
final int windowingMode = mController.getTaskContainer(taskId)
.getWindowingModeForSplitTaskFragment(bounds);
createTaskFragment(wct, container.getTaskFragmentToken(), activity.getActivityToken(),
bounds, windowingMode);
wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(),
activity.getActivityToken());
} else {
resizeTaskFragmentIfRegistered(wct, container, bounds);
final int windowingMode = mController.getTaskContainer(taskId)
.getWindowingModeForSplitTaskFragment(bounds);
updateTaskFragmentWindowingModeIfRegistered(wct, container, windowingMode);
}
return container;
}
/**
* Starts a new activity to the side, creating a new split container. A new container will be
* created for the activity that will be started.
* @param launchingActivity An activity that should be in the primary container. If it is not
* currently in an existing container, a new one will be created and
* the activity will be re-parented to it.
* @param activityIntent The intent to start the new activity.
* @param activityOptions The options to apply to new activity start.
* @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,
@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);
if (primaryContainer == null) {
primaryContainer = mController.newContainer(launchingActivity,
launchingActivity.getTaskId());
}
final int taskId = primaryContainer.getTaskId();
final TaskFragmentContainer secondaryContainer = mController.newContainer(activityIntent,
launchingActivity, taskId);
final int windowingMode = mController.getTaskContainer(taskId)
.getWindowingModeForSplitTaskFragment(primaryRectBounds);
mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer,
rule, splitAttributes);
startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds,
launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds,
activityIntent, activityOptions, rule, windowingMode);
if (isPlaceholder) {
// When placeholder is launched in split, we should keep the focus on the primary.
wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken());
}
}
/**
* Updates the positions of containers in an existing split.
* @param splitContainer The split container to be updated.
* @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 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 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()
&& secondaryContainer.areLastRequestedBoundsEqual(null /* bounds */)
&& !secondaryRectBounds.isEmpty();
// If the task fragments are not registered yet, the positions will be updated after they
// are created again.
resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds);
resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds);
setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule,
splitAttributes);
if (isPlaceholderBecomingSplit) {
// When placeholder is shown in split, we should keep the focus on the primary.
wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken());
}
final TaskContainer taskContainer = updatedContainer.getTaskContainer();
final int windowingMode = taskContainer.getWindowingModeForSplitTaskFragment(
primaryRectBounds);
updateTaskFragmentWindowingModeIfRegistered(wct, primaryContainer, windowingMode);
updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode);
}
@GuardedBy("mController.mLock")
private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer primaryContainer,
@NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule,
@NonNull SplitAttributes splitAttributes) {
// Clear adjacent TaskFragments if the container is shown in fullscreen, or the
// secondaryContainer could not be finished.
if (!shouldShowSplit(splitAttributes)) {
setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
null /* secondary */, null /* splitRule */);
} else {
setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
secondaryContainer.getTaskFragmentToken(), splitRule);
}
}
/**
* Resizes the task fragment if it was already registered. Skips the operation if the container
* creation has not been reported from the server yet.
*/
// TODO(b/190433398): Handle resize if the fragment hasn't appeared yet.
void resizeTaskFragmentIfRegistered(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container,
@Nullable Rect bounds) {
if (container.getInfo() == null) {
return;
}
resizeTaskFragment(wct, container.getTaskFragmentToken(), bounds);
}
private void updateTaskFragmentWindowingModeIfRegistered(
@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container,
@WindowingMode int windowingMode) {
if (container.getInfo() != null) {
updateWindowingMode(wct, container.getTaskFragmentToken(), windowingMode);
}
}
@Override
void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
@NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
final TaskFragmentContainer container = mController.getContainer(fragmentToken);
if (container == null) {
throw new IllegalStateException(
"Creating a task fragment that is not registered with controller.");
}
container.setLastRequestedBounds(bounds);
container.setLastRequestedWindowingMode(windowingMode);
super.createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode);
}
@Override
void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
@Nullable Rect bounds) {
TaskFragmentContainer container = mController.getContainer(fragmentToken);
if (container == null) {
throw new IllegalStateException(
"Resizing a task fragment that is not registered with controller.");
}
if (container.areLastRequestedBoundsEqual(bounds)) {
// Return early if the provided bounds were already requested
return;
}
container.setLastRequestedBounds(bounds);
super.resizeTaskFragment(wct, fragmentToken, bounds);
}
@Override
void updateWindowingMode(@NonNull WindowContainerTransaction wct,
@NonNull IBinder fragmentToken, @WindowingMode int windowingMode) {
final TaskFragmentContainer container = mController.getContainer(fragmentToken);
if (container == null) {
throw new IllegalStateException("Setting windowing mode for a task fragment that is"
+ " not registered with controller.");
}
if (container.isLastRequestedWindowingModeEqual(windowingMode)) {
// Return early if the windowing mode were already requested
return;
}
container.setLastRequestedWindowingMode(windowingMode);
super.updateWindowingMode(wct, fragmentToken, windowingMode);
}
/**
* 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 #shouldShowSplit(SplitAttributes)} and if
* {@link android.window.TaskFragmentInfo} has reported to the client side.
*/
@ResultCode
int expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct,
@NonNull SplitContainer splitContainer, @NonNull Activity primaryActivity,
@Nullable Activity secondaryActivity, @Nullable Intent secondaryIntent) {
if (secondaryActivity == null && secondaryIntent == null) {
throw new IllegalArgumentException("Either secondaryActivity or secondaryIntent must be"
+ " non-null.");
}
final Pair<Size, Size> minDimensionsPair;
if (secondaryActivity != null) {
minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity);
} else {
minDimensionsPair = getActivityIntentMinDimensionsPair(primaryActivity,
secondaryIntent);
}
// Expand the splitContainer if minimum dimensions are not satisfied.
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
|| splitContainer.getSecondaryContainer().getInfo() == null) {
return RESULT_EXPAND_FAILED_NO_TF_INFO;
}
expandTaskFragment(wct, splitContainer.getPrimaryContainer().getTaskFragmentToken());
expandTaskFragment(wct, splitContainer.getSecondaryContainer().getTaskFragmentToken());
return RESULT_EXPANDED;
}
return RESULT_NOT_EXPANDED;
}
static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) {
return shouldShowSplit(splitContainer.getSplitAttributes());
}
static boolean shouldShowSplit(@NonNull SplitAttributes splitAttributes) {
return !(splitAttributes.getSplitType() instanceof ExpandContainersSplitType);
}
@GuardedBy("mController.mLock")
@NonNull
SplitAttributes computeSplitAttributes(@NonNull TaskProperties taskProperties,
@NonNull SplitRule rule, @Nullable Pair<Size, Size> minDimensionsPair) {
final Configuration taskConfiguration = taskProperties.getConfiguration();
final WindowMetrics taskWindowMetrics = getTaskWindowMetrics(taskConfiguration);
final SplitAttributesCalculator calculator = mController.getSplitAttributesCalculator();
final SplitAttributes defaultSplitAttributes = rule.getDefaultSplitAttributes();
final boolean isDefaultMinSizeSatisfied = rule.checkParentMetrics(taskWindowMetrics);
if (calculator == null) {
if (!isDefaultMinSizeSatisfied) {
return EXPAND_CONTAINERS_ATTRIBUTES;
}
return sanitizeSplitAttributes(taskProperties, defaultSplitAttributes,
minDimensionsPair);
}
final WindowLayoutInfo windowLayoutInfo = mController.mWindowLayoutComponent
.getCurrentWindowLayoutInfo(taskProperties.getDisplayId(),
taskConfiguration.windowConfiguration);
final SplitAttributesCalculatorParams params = new SplitAttributesCalculatorParams(
taskWindowMetrics, taskConfiguration, defaultSplitAttributes,
isDefaultMinSizeSatisfied, windowLayoutInfo, rule.getTag());
final SplitAttributes splitAttributes = calculator.computeSplitAttributesForParams(params);
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) {
if (minDimensionsPair == null) {
return splitAttributes;
}
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
static Pair<Size, Size> getActivitiesMinDimensionsPair(@NonNull Activity primaryActivity,
@NonNull Activity secondaryActivity) {
return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryActivity));
}
@NonNull
static Pair<Size, Size> getActivityIntentMinDimensionsPair(@NonNull Activity primaryActivity,
@NonNull Intent secondaryIntent) {
return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryIntent));
}
@Nullable
static Size getMinDimensions(@Nullable Activity activity) {
if (activity == null) {
return null;
}
final ActivityInfo.WindowLayout windowLayout = activity.getActivityInfo().windowLayout;
if (windowLayout == null) {
return null;
}
return new Size(windowLayout.minWidth, windowLayout.minHeight);
}
// TODO(b/232871351): find a light-weight approach for this check.
@Nullable
static Size getMinDimensions(@Nullable Intent intent) {
if (intent == null) {
return null;
}
final PackageManager packageManager = ActivityThread.currentActivityThread()
.getApplication().getPackageManager();
final ResolveInfo resolveInfo = packageManager.resolveActivity(intent,
PackageManager.ResolveInfoFlags.of(MATCH_ALL));
if (resolveInfo == null) {
return null;
}
final ActivityInfo activityInfo = resolveInfo.activityInfo;
if (activityInfo == null) {
return null;
}
final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout;
if (windowLayout == null) {
return null;
}
return new Size(windowLayout.minWidth, windowLayout.minHeight);
}
static boolean boundsSmallerThanMinDimensions(@NonNull Rect bounds,
@Nullable Size minDimensions) {
if (minDimensions == null) {
return false;
}
return bounds.width() < minDimensions.getWidth()
|| bounds.height() < minDimensions.getHeight();
}
@VisibleForTesting
@NonNull
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();
}
switch (position) {
case POSITION_START:
return getPrimaryBounds(taskConfiguration, computedSplitAttributes, foldingFeature);
case POSITION_END:
return getSecondaryBounds(taskConfiguration, computedSplitAttributes,
foldingFeature);
case POSITION_FILL:
default:
return new Rect();
}
}
@NonNull
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;
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:
throw new IllegalArgumentException("Unknown layout direction:"
+ splitAttributes.getLayoutDirection());
}
}
@NonNull
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
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);
}
/**
* 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 =
activity.getResources().getConfiguration().windowConfiguration;
if (!activity.isInMultiWindowMode()) {
// In fullscreen mode the max bounds should correspond to the task bounds.
return windowConfiguration.getMaxBounds();
}
return windowConfiguration.getBounds();
}
}