| /* |
| * 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.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; |
| |
| import android.app.Activity; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.res.Configuration; |
| import android.graphics.Rect; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.util.LayoutDirection; |
| import android.view.View; |
| import android.view.WindowInsets; |
| import android.view.WindowMetrics; |
| import android.window.TaskFragmentCreationParams; |
| import android.window.WindowContainerTransaction; |
| |
| import androidx.annotation.IntDef; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import java.util.concurrent.Executor; |
| |
| /** |
| * Controls the visual presentation of the splits according to the containers formed by |
| * {@link SplitController}. |
| */ |
| class SplitPresenter extends JetpackTaskFragmentOrganizer { |
| private static final int POSITION_START = 0; |
| private static final int POSITION_END = 1; |
| private static final int POSITION_FILL = 2; |
| |
| @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(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(); |
| |
| container.finish(shouldFinishDependent, this, wct, mController); |
| |
| final TaskFragmentContainer newTopContainer = mController.getTopActiveContainer(); |
| if (newTopContainer != null) { |
| mController.updateContainer(wct, newTopContainer); |
| } |
| |
| applyTransaction(wct); |
| } |
| |
| /** |
| * Creates a new split with the primary activity and an empty secondary container. |
| * @return The newly created secondary container. |
| */ |
| TaskFragmentContainer createNewSplitWithEmptySideContainer(@NonNull Activity primaryActivity, |
| @NonNull SplitPairRule rule) { |
| final WindowContainerTransaction wct = new WindowContainerTransaction(); |
| |
| final Rect parentBounds = getParentContainerBounds(primaryActivity); |
| final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, |
| isLtr(primaryActivity, rule)); |
| final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, |
| primaryActivity, primaryRectBounds, null); |
| |
| // Create new empty task fragment |
| final TaskFragmentContainer secondaryContainer = mController.newContainer(null); |
| final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, |
| rule, isLtr(primaryActivity, rule)); |
| createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(), |
| primaryActivity.getActivityToken(), secondaryRectBounds, |
| WINDOWING_MODE_MULTI_WINDOW); |
| secondaryContainer.setLastRequestedBounds(secondaryRectBounds); |
| |
| // Set adjacent to each other so that the containers below will be invisible. |
| setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule); |
| |
| mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); |
| |
| applyTransaction(wct); |
| |
| 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 Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, |
| isLtr(primaryActivity, rule)); |
| final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, |
| primaryActivity, primaryRectBounds, null); |
| |
| final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, |
| isLtr(primaryActivity, rule)); |
| final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct, |
| secondaryActivity, secondaryRectBounds, primaryContainer); |
| |
| // Set adjacent to each other so that the containers below will be invisible. |
| setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule); |
| |
| mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); |
| |
| applyTransaction(wct); |
| } |
| |
| /** |
| * Creates a new expanded container. |
| */ |
| TaskFragmentContainer createNewExpandedContainer(@NonNull Activity launchingActivity) { |
| final TaskFragmentContainer newContainer = mController.newContainer(null); |
| |
| final WindowContainerTransaction wct = new WindowContainerTransaction(); |
| createTaskFragment(wct, newContainer.getTaskFragmentToken(), |
| launchingActivity.getActivityToken(), new Rect(), WINDOWING_MODE_MULTI_WINDOW); |
| |
| applyTransaction(wct); |
| return newContainer; |
| } |
| |
| /** |
| * 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.getActivityToken()); |
| if (container == null || container == containerToAvoid) { |
| container = mController.newContainer(activity); |
| |
| final TaskFragmentCreationParams fragmentOptions = |
| createFragmentOptions( |
| container.getTaskFragmentToken(), |
| activity.getActivityToken(), |
| bounds, |
| WINDOWING_MODE_MULTI_WINDOW); |
| wct.createTaskFragment(fragmentOptions); |
| |
| wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(), |
| activity.getActivityToken()); |
| |
| container.setLastRequestedBounds(bounds); |
| } else { |
| resizeTaskFragmentIfRegistered(wct, container, bounds); |
| } |
| |
| 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. |
| */ |
| void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent, |
| @Nullable Bundle activityOptions, @NonNull SplitRule rule) { |
| final Rect parentBounds = getParentContainerBounds(launchingActivity); |
| final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, |
| isLtr(launchingActivity, rule)); |
| final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, |
| isLtr(launchingActivity, rule)); |
| |
| TaskFragmentContainer primaryContainer = mController.getContainerWithActivity( |
| launchingActivity.getActivityToken()); |
| if (primaryContainer == null) { |
| primaryContainer = mController.newContainer(launchingActivity); |
| } |
| |
| TaskFragmentContainer secondaryContainer = mController.newContainer(null); |
| final WindowContainerTransaction wct = new WindowContainerTransaction(); |
| mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer, |
| rule); |
| startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds, |
| launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds, |
| activityIntent, activityOptions, rule); |
| applyTransaction(wct); |
| |
| primaryContainer.setLastRequestedBounds(primaryRectBounds); |
| secondaryContainer.setLastRequestedBounds(secondaryRectBounds); |
| } |
| |
| /** |
| * 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 boolean isLtr = isLtr(activity, rule); |
| final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, |
| isLtr); |
| final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, |
| isLtr); |
| |
| // If the task fragments are not registered yet, the positions will be updated after they |
| // are created again. |
| resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds); |
| final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); |
| resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds); |
| |
| setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule); |
| } |
| |
| private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, |
| @NonNull TaskFragmentContainer primaryContainer, |
| @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule) { |
| 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)) { |
| 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); |
| } |
| |
| @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); |
| } |
| |
| boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) { |
| final Rect parentBounds = getParentContainerBounds(splitContainer.getPrimaryContainer()); |
| return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule()); |
| } |
| |
| boolean shouldShowSideBySide(@Nullable Rect parentBounds, @NonNull SplitRule rule) { |
| // TODO(b/190433398): Supply correct insets. |
| final WindowMetrics parentMetrics = new WindowMetrics(parentBounds, |
| new WindowInsets(new Rect())); |
| return rule.checkParentMetrics(parentMetrics); |
| } |
| |
| @NonNull |
| private Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds, |
| @NonNull SplitRule rule, boolean isLtr) { |
| if (!shouldShowSideBySide(parentBounds, rule)) { |
| return new Rect(); |
| } |
| |
| final float splitRatio = rule.getSplitRatio(); |
| final float rtlSplitRatio = 1 - splitRatio; |
| switch (position) { |
| case POSITION_START: |
| return isLtr ? getLeftContainerBounds(parentBounds, splitRatio) |
| : getRightContainerBounds(parentBounds, rtlSplitRatio); |
| case POSITION_END: |
| return isLtr ? getRightContainerBounds(parentBounds, splitRatio) |
| : getLeftContainerBounds(parentBounds, rtlSplitRatio); |
| case POSITION_FILL: |
| return parentBounds; |
| } |
| return parentBounds; |
| } |
| |
| private Rect getLeftContainerBounds(@NonNull Rect parentBounds, float splitRatio) { |
| return new Rect( |
| parentBounds.left, |
| parentBounds.top, |
| (int) (parentBounds.left + parentBounds.width() * splitRatio), |
| parentBounds.bottom); |
| } |
| |
| private 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 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 |
| Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) { |
| final Configuration parentConfig = mFragmentParentConfigs.get( |
| container.getTaskFragmentToken()); |
| if (parentConfig != null) { |
| return parentConfig.windowConfiguration.getBounds(); |
| } |
| |
| // If there is no parent yet - then assuming that activities are running in full task bounds |
| final Activity topActivity = container.getTopNonFinishingActivity(); |
| final Rect bounds = topActivity != null ? getParentContainerBounds(topActivity) : null; |
| |
| if (bounds == null) { |
| throw new IllegalStateException("Unknown parent bounds"); |
| } |
| return bounds; |
| } |
| |
| @NonNull |
| Rect getParentContainerBounds(@NonNull Activity activity) { |
| final TaskFragmentContainer container = mController.getContainerWithActivity( |
| activity.getActivityToken()); |
| if (container != null) { |
| final Configuration parentConfig = mFragmentParentConfigs.get( |
| container.getTaskFragmentToken()); |
| if (parentConfig != null) { |
| return parentConfig.windowConfiguration.getBounds(); |
| } |
| } |
| |
| // TODO(b/190433398): Check if the client-side available info about parent bounds is enough. |
| if (!activity.isInMultiWindowMode()) { |
| // In fullscreen mode the max bounds should correspond to the task bounds. |
| return activity.getResources().getConfiguration().windowConfiguration.getMaxBounds(); |
| } |
| return activity.getResources().getConfiguration().windowConfiguration.getBounds(); |
| } |
| } |