1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.legacysplitscreen; 18 19 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; 20 import static android.content.res.Configuration.ORIENTATION_PORTRAIT; 21 import static android.util.RotationUtils.rotateBounds; 22 import static android.view.WindowManager.DOCKED_BOTTOM; 23 import static android.view.WindowManager.DOCKED_INVALID; 24 import static android.view.WindowManager.DOCKED_LEFT; 25 import static android.view.WindowManager.DOCKED_RIGHT; 26 import static android.view.WindowManager.DOCKED_TOP; 27 28 import android.annotation.NonNull; 29 import android.content.Context; 30 import android.content.res.Configuration; 31 import android.content.res.Resources; 32 import android.graphics.Rect; 33 import android.util.TypedValue; 34 import android.window.WindowContainerTransaction; 35 36 import com.android.internal.policy.DividerSnapAlgorithm; 37 import com.android.internal.policy.DockedDividerUtils; 38 import com.android.wm.shell.common.DisplayLayout; 39 40 /** 41 * Handles split-screen related internal display layout. In general, this represents the 42 * WM-facing understanding of the splits. 43 */ 44 public class LegacySplitDisplayLayout { 45 /** Minimum size of an adjusted stack bounds relative to original stack bounds. Used to 46 * restrict IME adjustment so that a min portion of top stack remains visible.*/ 47 private static final float ADJUSTED_STACK_FRACTION_MIN = 0.3f; 48 49 private static final int DIVIDER_WIDTH_INACTIVE_DP = 4; 50 51 LegacySplitScreenTaskListener mTiles; 52 DisplayLayout mDisplayLayout; 53 Context mContext; 54 55 // Lazy stuff 56 boolean mResourcesValid = false; 57 int mDividerSize; 58 int mDividerSizeInactive; 59 private DividerSnapAlgorithm mSnapAlgorithm = null; 60 private DividerSnapAlgorithm mMinimizedSnapAlgorithm = null; 61 Rect mPrimary = null; 62 Rect mSecondary = null; 63 Rect mAdjustedPrimary = null; 64 Rect mAdjustedSecondary = null; 65 LegacySplitDisplayLayout(Context ctx, DisplayLayout dl, LegacySplitScreenTaskListener taskTiles)66 public LegacySplitDisplayLayout(Context ctx, DisplayLayout dl, 67 LegacySplitScreenTaskListener taskTiles) { 68 mTiles = taskTiles; 69 mDisplayLayout = dl; 70 mContext = ctx; 71 } 72 rotateTo(int newRotation)73 void rotateTo(int newRotation) { 74 mDisplayLayout.rotateTo(mContext.getResources(), newRotation); 75 final Configuration config = new Configuration(); 76 config.unset(); 77 config.orientation = mDisplayLayout.getOrientation(); 78 Rect tmpRect = new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); 79 tmpRect.inset(mDisplayLayout.nonDecorInsets()); 80 config.windowConfiguration.setAppBounds(tmpRect); 81 tmpRect.set(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); 82 tmpRect.inset(mDisplayLayout.stableInsets()); 83 config.screenWidthDp = (int) (tmpRect.width() / mDisplayLayout.density()); 84 config.screenHeightDp = (int) (tmpRect.height() / mDisplayLayout.density()); 85 mContext = mContext.createConfigurationContext(config); 86 mSnapAlgorithm = null; 87 mMinimizedSnapAlgorithm = null; 88 mResourcesValid = false; 89 } 90 updateResources()91 private void updateResources() { 92 if (mResourcesValid) { 93 return; 94 } 95 mResourcesValid = true; 96 Resources res = mContext.getResources(); 97 mDividerSize = DockedDividerUtils.getDividerSize(res, 98 DockedDividerUtils.getDividerInsets(res)); 99 mDividerSizeInactive = (int) TypedValue.applyDimension( 100 TypedValue.COMPLEX_UNIT_DIP, DIVIDER_WIDTH_INACTIVE_DP, res.getDisplayMetrics()); 101 } 102 getPrimarySplitSide()103 int getPrimarySplitSide() { 104 switch (mDisplayLayout.getNavigationBarPosition(mContext.getResources())) { 105 case DisplayLayout.NAV_BAR_BOTTOM: 106 return mDisplayLayout.isLandscape() ? DOCKED_LEFT : DOCKED_TOP; 107 case DisplayLayout.NAV_BAR_LEFT: 108 return DOCKED_RIGHT; 109 case DisplayLayout.NAV_BAR_RIGHT: 110 return DOCKED_LEFT; 111 default: 112 return DOCKED_INVALID; 113 } 114 } 115 getSnapAlgorithm()116 DividerSnapAlgorithm getSnapAlgorithm() { 117 if (mSnapAlgorithm == null) { 118 updateResources(); 119 boolean isHorizontalDivision = !mDisplayLayout.isLandscape(); 120 mSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(), 121 mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize, 122 isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide()); 123 } 124 return mSnapAlgorithm; 125 } 126 getMinimizedSnapAlgorithm(boolean homeStackResizable)127 DividerSnapAlgorithm getMinimizedSnapAlgorithm(boolean homeStackResizable) { 128 if (mMinimizedSnapAlgorithm == null) { 129 updateResources(); 130 boolean isHorizontalDivision = !mDisplayLayout.isLandscape(); 131 mMinimizedSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(), 132 mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize, 133 isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide(), 134 true /* isMinimized */, homeStackResizable); 135 } 136 return mMinimizedSnapAlgorithm; 137 } 138 resizeSplits(int position)139 void resizeSplits(int position) { 140 mPrimary = mPrimary == null ? new Rect() : mPrimary; 141 mSecondary = mSecondary == null ? new Rect() : mSecondary; 142 calcSplitBounds(position, mPrimary, mSecondary); 143 } 144 resizeSplits(int position, WindowContainerTransaction t)145 void resizeSplits(int position, WindowContainerTransaction t) { 146 resizeSplits(position); 147 t.setBounds(mTiles.mPrimary.token, mPrimary); 148 t.setBounds(mTiles.mSecondary.token, mSecondary); 149 150 t.setSmallestScreenWidthDp(mTiles.mPrimary.token, 151 getSmallestWidthDpForBounds(mContext, mDisplayLayout, mPrimary)); 152 t.setSmallestScreenWidthDp(mTiles.mSecondary.token, 153 getSmallestWidthDpForBounds(mContext, mDisplayLayout, mSecondary)); 154 } 155 calcSplitBounds(int position, @NonNull Rect outPrimary, @NonNull Rect outSecondary)156 void calcSplitBounds(int position, @NonNull Rect outPrimary, @NonNull Rect outSecondary) { 157 int dockSide = getPrimarySplitSide(); 158 DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outPrimary, 159 mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize); 160 161 DockedDividerUtils.calculateBoundsForPosition(position, 162 DockedDividerUtils.invertDockSide(dockSide), outSecondary, mDisplayLayout.width(), 163 mDisplayLayout.height(), mDividerSize); 164 } 165 calcResizableMinimizedHomeStackBounds()166 Rect calcResizableMinimizedHomeStackBounds() { 167 DividerSnapAlgorithm.SnapTarget miniMid = 168 getMinimizedSnapAlgorithm(true /* resizable */).getMiddleTarget(); 169 Rect homeBounds = new Rect(); 170 DockedDividerUtils.calculateBoundsForPosition(miniMid.position, 171 DockedDividerUtils.invertDockSide(getPrimarySplitSide()), homeBounds, 172 mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize); 173 return homeBounds; 174 } 175 176 /** 177 * Updates the adjustment depending on it's current state. 178 */ updateAdjustedBounds(int currImeTop, int hiddenTop, int shownTop)179 void updateAdjustedBounds(int currImeTop, int hiddenTop, int shownTop) { 180 adjustForIME(mDisplayLayout, currImeTop, hiddenTop, shownTop, mDividerSize, 181 mDividerSizeInactive, mPrimary, mSecondary); 182 } 183 184 /** Assumes top/bottom split. Splits are not adjusted for left/right splits. */ adjustForIME(DisplayLayout dl, int currImeTop, int hiddenTop, int shownTop, int dividerWidth, int dividerWidthInactive, Rect primaryBounds, Rect secondaryBounds)185 private void adjustForIME(DisplayLayout dl, int currImeTop, int hiddenTop, int shownTop, 186 int dividerWidth, int dividerWidthInactive, Rect primaryBounds, Rect secondaryBounds) { 187 if (mAdjustedPrimary == null) { 188 mAdjustedPrimary = new Rect(); 189 mAdjustedSecondary = new Rect(); 190 } 191 192 final Rect displayStableRect = new Rect(); 193 dl.getStableBounds(displayStableRect); 194 195 final float shownFraction = ((float) (currImeTop - hiddenTop)) / (shownTop - hiddenTop); 196 final int currDividerWidth = 197 (int) (dividerWidthInactive * shownFraction + dividerWidth * (1.f - shownFraction)); 198 199 // Calculate the highest we can move the bottom of the top stack to keep 30% visible. 200 final int minTopStackBottom = displayStableRect.top 201 + (int) ((mPrimary.bottom - displayStableRect.top) * ADJUSTED_STACK_FRACTION_MIN); 202 // Based on that, calculate the maximum amount we'll allow the ime to shift things. 203 final int maxOffset = mPrimary.bottom - minTopStackBottom; 204 // Calculate how much we would shift things without limits (basically the height of ime). 205 final int desiredOffset = hiddenTop - shownTop; 206 // Calculate an "adjustedTop" which is the currImeTop but restricted by our constraints. 207 // We want an effect where the adjustment only occurs during the "highest" portion of the 208 // ime animation. This is done by shifting the adjustment values by the difference in 209 // offsets (effectively playing the whole adjustment animation some fixed amount of pixels 210 // below the ime top). 211 final int topCorrection = Math.max(0, desiredOffset - maxOffset); 212 final int adjustedTop = currImeTop + topCorrection; 213 // The actual yOffset is the distance between adjustedTop and the bottom of the display. 214 // Since our adjustedTop values are playing "below" the ime, we clamp at 0 so we only 215 // see adjustment upward. 216 final int yOffset = Math.max(0, dl.height() - adjustedTop); 217 218 // TOP 219 // Reduce the offset by an additional small amount to squish the divider bar. 220 mAdjustedPrimary.set(primaryBounds); 221 mAdjustedPrimary.offset(0, -yOffset + (dividerWidth - currDividerWidth)); 222 223 // BOTTOM 224 mAdjustedSecondary.set(secondaryBounds); 225 mAdjustedSecondary.offset(0, -yOffset); 226 } 227 getSmallestWidthDpForBounds(@onNull Context context, DisplayLayout dl, Rect bounds)228 static int getSmallestWidthDpForBounds(@NonNull Context context, DisplayLayout dl, 229 Rect bounds) { 230 int dividerSize = DockedDividerUtils.getDividerSize(context.getResources(), 231 DockedDividerUtils.getDividerInsets(context.getResources())); 232 233 int minWidth = Integer.MAX_VALUE; 234 235 // Go through all screen orientations and find the orientation in which the task has the 236 // smallest width. 237 Rect tmpRect = new Rect(); 238 Rect rotatedDisplayRect = new Rect(); 239 Rect displayRect = new Rect(0, 0, dl.width(), dl.height()); 240 241 DisplayLayout tmpDL = new DisplayLayout(); 242 for (int rotation = 0; rotation < 4; rotation++) { 243 tmpDL.set(dl); 244 tmpDL.rotateTo(context.getResources(), rotation); 245 DividerSnapAlgorithm snap = initSnapAlgorithmForRotation(context, tmpDL, dividerSize); 246 247 tmpRect.set(bounds); 248 rotateBounds(tmpRect, displayRect, dl.rotation(), rotation); 249 rotatedDisplayRect.set(0, 0, tmpDL.width(), tmpDL.height()); 250 final int dockSide = getPrimarySplitSide(tmpRect, rotatedDisplayRect, 251 tmpDL.getOrientation()); 252 final int position = DockedDividerUtils.calculatePositionForBounds(tmpRect, dockSide, 253 dividerSize); 254 255 final int snappedPosition = 256 snap.calculateNonDismissingSnapTarget(position).position; 257 DockedDividerUtils.calculateBoundsForPosition(snappedPosition, dockSide, tmpRect, 258 tmpDL.width(), tmpDL.height(), dividerSize); 259 Rect insettedDisplay = new Rect(rotatedDisplayRect); 260 insettedDisplay.inset(tmpDL.stableInsets()); 261 tmpRect.intersect(insettedDisplay); 262 minWidth = Math.min(tmpRect.width(), minWidth); 263 } 264 return (int) (minWidth / dl.density()); 265 } 266 initSnapAlgorithmForRotation(Context context, DisplayLayout dl, int dividerSize)267 static DividerSnapAlgorithm initSnapAlgorithmForRotation(Context context, DisplayLayout dl, 268 int dividerSize) { 269 final Configuration config = new Configuration(); 270 config.unset(); 271 config.orientation = dl.getOrientation(); 272 Rect tmpRect = new Rect(0, 0, dl.width(), dl.height()); 273 tmpRect.inset(dl.nonDecorInsets()); 274 config.windowConfiguration.setAppBounds(tmpRect); 275 tmpRect.set(0, 0, dl.width(), dl.height()); 276 tmpRect.inset(dl.stableInsets()); 277 config.screenWidthDp = (int) (tmpRect.width() / dl.density()); 278 config.screenHeightDp = (int) (tmpRect.height() / dl.density()); 279 final Context rotationContext = context.createConfigurationContext(config); 280 return new DividerSnapAlgorithm( 281 rotationContext.getResources(), dl.width(), dl.height(), dividerSize, 282 config.orientation == ORIENTATION_PORTRAIT, dl.stableInsets()); 283 } 284 285 /** 286 * Get the current primary-split side. Determined by its location of {@param bounds} within 287 * {@param displayRect} but if both are the same, it will try to dock to each side and determine 288 * if allowed in its respected {@param orientation}. 289 * 290 * @param bounds bounds of the primary split task to get which side is docked 291 * @param displayRect bounds of the display that contains the primary split task 292 * @param orientation the origination of device 293 * @return current primary-split side 294 */ getPrimarySplitSide(Rect bounds, Rect displayRect, int orientation)295 static int getPrimarySplitSide(Rect bounds, Rect displayRect, int orientation) { 296 if (orientation == ORIENTATION_PORTRAIT) { 297 // Portrait mode, docked either at the top or the bottom. 298 final int diff = (displayRect.bottom - bounds.bottom) - (bounds.top - displayRect.top); 299 if (diff < 0) { 300 return DOCKED_BOTTOM; 301 } else { 302 // Top is default 303 return DOCKED_TOP; 304 } 305 } else if (orientation == ORIENTATION_LANDSCAPE) { 306 // Landscape mode, docked either on the left or on the right. 307 final int diff = (displayRect.right - bounds.right) - (bounds.left - displayRect.left); 308 if (diff < 0) { 309 return DOCKED_RIGHT; 310 } 311 return DOCKED_LEFT; 312 } 313 return DOCKED_INVALID; 314 } 315 } 316