blob: 40244fbb450373aa5ff4cd50574a379d846a5dba [file] [log] [blame]
/*
* Copyright (C) 2020 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 com.android.wm.shell.legacysplitscreen;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.util.RotationUtils.rotateBounds;
import static android.view.WindowManager.DOCKED_BOTTOM;
import static android.view.WindowManager.DOCKED_INVALID;
import static android.view.WindowManager.DOCKED_LEFT;
import static android.view.WindowManager.DOCKED_RIGHT;
import static android.view.WindowManager.DOCKED_TOP;
import android.annotation.NonNull;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
import android.util.TypedValue;
import android.window.WindowContainerTransaction;
import com.android.internal.policy.DividerSnapAlgorithm;
import com.android.internal.policy.DockedDividerUtils;
import com.android.wm.shell.common.DisplayLayout;
/**
* Handles split-screen related internal display layout. In general, this represents the
* WM-facing understanding of the splits.
*/
public class LegacySplitDisplayLayout {
/** Minimum size of an adjusted stack bounds relative to original stack bounds. Used to
* restrict IME adjustment so that a min portion of top stack remains visible.*/
private static final float ADJUSTED_STACK_FRACTION_MIN = 0.3f;
private static final int DIVIDER_WIDTH_INACTIVE_DP = 4;
LegacySplitScreenTaskListener mTiles;
DisplayLayout mDisplayLayout;
Context mContext;
// Lazy stuff
boolean mResourcesValid = false;
int mDividerSize;
int mDividerSizeInactive;
private DividerSnapAlgorithm mSnapAlgorithm = null;
private DividerSnapAlgorithm mMinimizedSnapAlgorithm = null;
Rect mPrimary = null;
Rect mSecondary = null;
Rect mAdjustedPrimary = null;
Rect mAdjustedSecondary = null;
public LegacySplitDisplayLayout(Context ctx, DisplayLayout dl,
LegacySplitScreenTaskListener taskTiles) {
mTiles = taskTiles;
mDisplayLayout = dl;
mContext = ctx;
}
void rotateTo(int newRotation) {
mDisplayLayout.rotateTo(mContext.getResources(), newRotation);
final Configuration config = new Configuration();
config.unset();
config.orientation = mDisplayLayout.getOrientation();
Rect tmpRect = new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height());
tmpRect.inset(mDisplayLayout.nonDecorInsets());
config.windowConfiguration.setAppBounds(tmpRect);
tmpRect.set(0, 0, mDisplayLayout.width(), mDisplayLayout.height());
tmpRect.inset(mDisplayLayout.stableInsets());
config.screenWidthDp = (int) (tmpRect.width() / mDisplayLayout.density());
config.screenHeightDp = (int) (tmpRect.height() / mDisplayLayout.density());
mContext = mContext.createConfigurationContext(config);
mSnapAlgorithm = null;
mMinimizedSnapAlgorithm = null;
mResourcesValid = false;
}
private void updateResources() {
if (mResourcesValid) {
return;
}
mResourcesValid = true;
Resources res = mContext.getResources();
mDividerSize = DockedDividerUtils.getDividerSize(res,
DockedDividerUtils.getDividerInsets(res));
mDividerSizeInactive = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, DIVIDER_WIDTH_INACTIVE_DP, res.getDisplayMetrics());
}
int getPrimarySplitSide() {
switch (mDisplayLayout.getNavigationBarPosition(mContext.getResources())) {
case DisplayLayout.NAV_BAR_BOTTOM:
return mDisplayLayout.isLandscape() ? DOCKED_LEFT : DOCKED_TOP;
case DisplayLayout.NAV_BAR_LEFT:
return DOCKED_RIGHT;
case DisplayLayout.NAV_BAR_RIGHT:
return DOCKED_LEFT;
default:
return DOCKED_INVALID;
}
}
DividerSnapAlgorithm getSnapAlgorithm() {
if (mSnapAlgorithm == null) {
updateResources();
boolean isHorizontalDivision = !mDisplayLayout.isLandscape();
mSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(),
mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize,
isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide());
}
return mSnapAlgorithm;
}
DividerSnapAlgorithm getMinimizedSnapAlgorithm(boolean homeStackResizable) {
if (mMinimizedSnapAlgorithm == null) {
updateResources();
boolean isHorizontalDivision = !mDisplayLayout.isLandscape();
mMinimizedSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(),
mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize,
isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide(),
true /* isMinimized */, homeStackResizable);
}
return mMinimizedSnapAlgorithm;
}
void resizeSplits(int position) {
mPrimary = mPrimary == null ? new Rect() : mPrimary;
mSecondary = mSecondary == null ? new Rect() : mSecondary;
calcSplitBounds(position, mPrimary, mSecondary);
}
void resizeSplits(int position, WindowContainerTransaction t) {
resizeSplits(position);
t.setBounds(mTiles.mPrimary.token, mPrimary);
t.setBounds(mTiles.mSecondary.token, mSecondary);
t.setSmallestScreenWidthDp(mTiles.mPrimary.token,
getSmallestWidthDpForBounds(mContext, mDisplayLayout, mPrimary));
t.setSmallestScreenWidthDp(mTiles.mSecondary.token,
getSmallestWidthDpForBounds(mContext, mDisplayLayout, mSecondary));
}
void calcSplitBounds(int position, @NonNull Rect outPrimary, @NonNull Rect outSecondary) {
int dockSide = getPrimarySplitSide();
DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outPrimary,
mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize);
DockedDividerUtils.calculateBoundsForPosition(position,
DockedDividerUtils.invertDockSide(dockSide), outSecondary, mDisplayLayout.width(),
mDisplayLayout.height(), mDividerSize);
}
Rect calcResizableMinimizedHomeStackBounds() {
DividerSnapAlgorithm.SnapTarget miniMid =
getMinimizedSnapAlgorithm(true /* resizable */).getMiddleTarget();
Rect homeBounds = new Rect();
DockedDividerUtils.calculateBoundsForPosition(miniMid.position,
DockedDividerUtils.invertDockSide(getPrimarySplitSide()), homeBounds,
mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize);
return homeBounds;
}
/**
* Updates the adjustment depending on it's current state.
*/
void updateAdjustedBounds(int currImeTop, int hiddenTop, int shownTop) {
adjustForIME(mDisplayLayout, currImeTop, hiddenTop, shownTop, mDividerSize,
mDividerSizeInactive, mPrimary, mSecondary);
}
/** Assumes top/bottom split. Splits are not adjusted for left/right splits. */
private void adjustForIME(DisplayLayout dl, int currImeTop, int hiddenTop, int shownTop,
int dividerWidth, int dividerWidthInactive, Rect primaryBounds, Rect secondaryBounds) {
if (mAdjustedPrimary == null) {
mAdjustedPrimary = new Rect();
mAdjustedSecondary = new Rect();
}
final Rect displayStableRect = new Rect();
dl.getStableBounds(displayStableRect);
final float shownFraction = ((float) (currImeTop - hiddenTop)) / (shownTop - hiddenTop);
final int currDividerWidth =
(int) (dividerWidthInactive * shownFraction + dividerWidth * (1.f - shownFraction));
// Calculate the highest we can move the bottom of the top stack to keep 30% visible.
final int minTopStackBottom = displayStableRect.top
+ (int) ((mPrimary.bottom - displayStableRect.top) * ADJUSTED_STACK_FRACTION_MIN);
// Based on that, calculate the maximum amount we'll allow the ime to shift things.
final int maxOffset = mPrimary.bottom - minTopStackBottom;
// Calculate how much we would shift things without limits (basically the height of ime).
final int desiredOffset = hiddenTop - shownTop;
// Calculate an "adjustedTop" which is the currImeTop but restricted by our constraints.
// We want an effect where the adjustment only occurs during the "highest" portion of the
// ime animation. This is done by shifting the adjustment values by the difference in
// offsets (effectively playing the whole adjustment animation some fixed amount of pixels
// below the ime top).
final int topCorrection = Math.max(0, desiredOffset - maxOffset);
final int adjustedTop = currImeTop + topCorrection;
// The actual yOffset is the distance between adjustedTop and the bottom of the display.
// Since our adjustedTop values are playing "below" the ime, we clamp at 0 so we only
// see adjustment upward.
final int yOffset = Math.max(0, dl.height() - adjustedTop);
// TOP
// Reduce the offset by an additional small amount to squish the divider bar.
mAdjustedPrimary.set(primaryBounds);
mAdjustedPrimary.offset(0, -yOffset + (dividerWidth - currDividerWidth));
// BOTTOM
mAdjustedSecondary.set(secondaryBounds);
mAdjustedSecondary.offset(0, -yOffset);
}
static int getSmallestWidthDpForBounds(@NonNull Context context, DisplayLayout dl,
Rect bounds) {
int dividerSize = DockedDividerUtils.getDividerSize(context.getResources(),
DockedDividerUtils.getDividerInsets(context.getResources()));
int minWidth = Integer.MAX_VALUE;
// Go through all screen orientations and find the orientation in which the task has the
// smallest width.
Rect tmpRect = new Rect();
Rect rotatedDisplayRect = new Rect();
Rect displayRect = new Rect(0, 0, dl.width(), dl.height());
DisplayLayout tmpDL = new DisplayLayout();
for (int rotation = 0; rotation < 4; rotation++) {
tmpDL.set(dl);
tmpDL.rotateTo(context.getResources(), rotation);
DividerSnapAlgorithm snap = initSnapAlgorithmForRotation(context, tmpDL, dividerSize);
tmpRect.set(bounds);
rotateBounds(tmpRect, displayRect, dl.rotation(), rotation);
rotatedDisplayRect.set(0, 0, tmpDL.width(), tmpDL.height());
final int dockSide = getPrimarySplitSide(tmpRect, rotatedDisplayRect,
tmpDL.getOrientation());
final int position = DockedDividerUtils.calculatePositionForBounds(tmpRect, dockSide,
dividerSize);
final int snappedPosition =
snap.calculateNonDismissingSnapTarget(position).position;
DockedDividerUtils.calculateBoundsForPosition(snappedPosition, dockSide, tmpRect,
tmpDL.width(), tmpDL.height(), dividerSize);
Rect insettedDisplay = new Rect(rotatedDisplayRect);
insettedDisplay.inset(tmpDL.stableInsets());
tmpRect.intersect(insettedDisplay);
minWidth = Math.min(tmpRect.width(), minWidth);
}
return (int) (minWidth / dl.density());
}
static DividerSnapAlgorithm initSnapAlgorithmForRotation(Context context, DisplayLayout dl,
int dividerSize) {
final Configuration config = new Configuration();
config.unset();
config.orientation = dl.getOrientation();
Rect tmpRect = new Rect(0, 0, dl.width(), dl.height());
tmpRect.inset(dl.nonDecorInsets());
config.windowConfiguration.setAppBounds(tmpRect);
tmpRect.set(0, 0, dl.width(), dl.height());
tmpRect.inset(dl.stableInsets());
config.screenWidthDp = (int) (tmpRect.width() / dl.density());
config.screenHeightDp = (int) (tmpRect.height() / dl.density());
final Context rotationContext = context.createConfigurationContext(config);
return new DividerSnapAlgorithm(
rotationContext.getResources(), dl.width(), dl.height(), dividerSize,
config.orientation == ORIENTATION_PORTRAIT, dl.stableInsets());
}
/**
* Get the current primary-split side. Determined by its location of {@param bounds} within
* {@param displayRect} but if both are the same, it will try to dock to each side and determine
* if allowed in its respected {@param orientation}.
*
* @param bounds bounds of the primary split task to get which side is docked
* @param displayRect bounds of the display that contains the primary split task
* @param orientation the origination of device
* @return current primary-split side
*/
static int getPrimarySplitSide(Rect bounds, Rect displayRect, int orientation) {
if (orientation == ORIENTATION_PORTRAIT) {
// Portrait mode, docked either at the top or the bottom.
final int diff = (displayRect.bottom - bounds.bottom) - (bounds.top - displayRect.top);
if (diff < 0) {
return DOCKED_BOTTOM;
} else {
// Top is default
return DOCKED_TOP;
}
} else if (orientation == ORIENTATION_LANDSCAPE) {
// Landscape mode, docked either on the left or on the right.
final int diff = (displayRect.right - bounds.right) - (bounds.left - displayRect.left);
if (diff < 0) {
return DOCKED_RIGHT;
}
return DOCKED_LEFT;
}
return DOCKED_INVALID;
}
}