blob: 1b79ad999435ecafed5cad7777e99ee31c37894a [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.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
import android.util.LayoutDirection;
import android.util.Pair;
import android.util.Size;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowMetrics;
import android.window.WindowContainerTransaction;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
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 final SplitController mController;
SplitPresenter(@NonNull Executor executor, SplitController controller) {
super(executor, controller);
mController = controller;
registerOrganizer();
}
/**
* Updates the presentation of the provided container.
*/
void updateContainer(@NonNull TaskFragmentContainer container) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
mController.updateContainer(wct, container);
applyTransaction(wct);
}
/**
* Deletes the specified container and all other associated and dependent containers in the same
* transaction.
*/
void cleanupContainer(@NonNull TaskFragmentContainer container, boolean shouldFinishDependent) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
cleanupContainer(container, shouldFinishDependent, wct);
applyTransaction(wct);
}
/**
* Deletes the specified container and all other associated and dependent containers in the same
* transaction.
*/
void cleanupContainer(@NonNull TaskFragmentContainer container, boolean shouldFinishDependent,
@NonNull WindowContainerTransaction wct) {
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
TaskFragmentContainer createNewSplitWithEmptySideContainer(
@NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity,
@NonNull Intent secondaryIntent, @NonNull SplitPairRule rule) {
final Rect parentBounds = getParentContainerBounds(primaryActivity);
final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(
primaryActivity, secondaryIntent);
final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
primaryActivity, minDimensionsPair);
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, parentBounds,
rule, primaryActivity, minDimensionsPair);
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,
minDimensionsPair);
mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
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.
*/
void createNewSplitContainer(@NonNull Activity primaryActivity,
@NonNull Activity secondaryActivity, @NonNull SplitPairRule rule) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
final Rect parentBounds = getParentContainerBounds(primaryActivity);
final Pair<Size, Size> minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity,
secondaryActivity);
final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
primaryActivity, minDimensionsPair);
final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
primaryActivity, primaryRectBounds, null);
final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
primaryActivity, minDimensionsPair);
final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity(
secondaryActivity);
TaskFragmentContainer containerToAvoid = primaryContainer;
if (rule.shouldClearTop() && curSecondaryContainer != null) {
// Do not reuse the current TaskFragment if the rule is to clear top.
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,
minDimensionsPair);
mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
applyTransaction(wct);
}
/**
* 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.
*/
void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent,
@Nullable Bundle activityOptions, @NonNull SplitRule rule, boolean isPlaceholder) {
final Rect parentBounds = getParentContainerBounds(launchingActivity);
final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(
launchingActivity, activityIntent);
final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
launchingActivity, minDimensionsPair);
final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
launchingActivity, minDimensionsPair);
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);
final WindowContainerTransaction wct = new WindowContainerTransaction();
mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer,
rule);
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());
}
applyTransaction(wct);
}
/**
* 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.
*/
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);
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 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,
minDimensionsPair);
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);
}
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);
// Clear adjacent TaskFragments if the container is shown in fullscreen, or the
// secondaryContainer could not be finished.
if (!shouldShowSideBySide(parentBounds, splitRule, minDimensionsPair)) {
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);
}
static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule) {
return shouldShowSideBySide(parentBounds, rule, null /* minimumDimensionPair */);
}
static boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) {
final Rect parentBounds = getParentContainerBounds(splitContainer.getPrimaryContainer());
return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule(),
splitContainer.getMinDimensionsPair());
}
static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule,
@Nullable Pair<Size, Size> minDimensionsPair) {
// TODO(b/190433398): Supply correct insets.
final WindowMetrics parentMetrics = new WindowMetrics(parentBounds,
new WindowInsets(new Rect()));
// Don't show side by side if bounds is not qualified.
if (!rule.checkParentMetrics(parentMetrics)) {
return false;
}
final float splitRatio = rule.getSplitRatio();
// We only care the size of the bounds regardless of its position.
final Rect primaryBounds = getPrimaryBounds(parentBounds, splitRatio, true /* isLtr */);
final Rect secondaryBounds = getSecondaryBounds(parentBounds, splitRatio, true /* isLtr */);
if (minDimensionsPair == null) {
return true;
}
return !boundsSmallerThanMinDimensions(primaryBounds, minDimensionsPair.first)
&& !boundsSmallerThanMinDimensions(secondaryBounds, minDimensionsPair.second);
}
@NonNull
static Pair<Size, Size> getActivitiesMinDimensionsPair(Activity primaryActivity,
Activity secondaryActivity) {
return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryActivity));
}
@NonNull
static Pair<Size, Size> getActivityIntentMinDimensionsPair(Activity primaryActivity,
Intent secondaryIntent) {
return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryIntent));
}
@Nullable
static Size getMinDimensions(@Nullable Activity activity) {
if (activity == null) {
return null;
}
final ActivityInfo.WindowLayout windowLayout = activity.getActivityInfo().windowLayout;
if (windowLayout == null) {
return null;
}
return new Size(windowLayout.minWidth, windowLayout.minHeight);
}
// TODO(b/232871351): find a light-weight approach for this check.
@Nullable
static Size getMinDimensions(@Nullable Intent intent) {
if (intent == null) {
return null;
}
final PackageManager packageManager = ActivityThread.currentActivityThread()
.getApplication().getPackageManager();
final ResolveInfo resolveInfo = packageManager.resolveActivity(intent,
PackageManager.ResolveInfoFlags.of(MATCH_ALL));
if (resolveInfo == null) {
return null;
}
final ActivityInfo activityInfo = resolveInfo.activityInfo;
if (activityInfo == null) {
return null;
}
final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout;
if (windowLayout == null) {
return null;
}
return new Size(windowLayout.minWidth, windowLayout.minHeight);
}
static boolean boundsSmallerThanMinDimensions(@NonNull Rect bounds,
@Nullable Size minDimensions) {
if (minDimensions == null) {
return false;
}
return bounds.width() < minDimensions.getWidth()
|| bounds.height() < minDimensions.getHeight();
}
@VisibleForTesting
@NonNull
static Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds,
@NonNull SplitRule rule, @NonNull Activity primaryActivity,
@Nullable Pair<Size, Size> minDimensionsPair) {
if (!shouldShowSideBySide(parentBounds, rule, minDimensionsPair)) {
return new Rect();
}
final boolean isLtr = isLtr(primaryActivity, rule);
final float splitRatio = rule.getSplitRatio();
switch (position) {
case POSITION_START:
return getPrimaryBounds(parentBounds, splitRatio, isLtr);
case POSITION_END:
return getSecondaryBounds(parentBounds, splitRatio, isLtr);
case POSITION_FILL:
default:
return new Rect();
}
}
@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()
== View.LAYOUT_DIRECTION_LTR;
case LayoutDirection.RTL:
return false;
case LayoutDirection.LTR:
default:
return true;
}
}
@NonNull
static Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) {
return container.getTaskContainer().getTaskBounds();
}
@NonNull
Rect getParentContainerBounds(@NonNull Activity activity) {
final TaskFragmentContainer container = mController.getContainerWithActivity(activity);
if (container != null) {
return getParentContainerBounds(container);
}
return getTaskBoundsFromActivity(activity);
}
@NonNull
static Rect getTaskBoundsFromActivity(@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();
}
}