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 final Rect mTmpBounds = new Rect(); 66 LegacySplitDisplayLayout(Context ctx, DisplayLayout dl, LegacySplitScreenTaskListener taskTiles)67 public LegacySplitDisplayLayout(Context ctx, DisplayLayout dl, 68 LegacySplitScreenTaskListener taskTiles) { 69 mTiles = taskTiles; 70 mDisplayLayout = dl; 71 mContext = ctx; 72 } 73 rotateTo(int newRotation)74 void rotateTo(int newRotation) { 75 mDisplayLayout.rotateTo(mContext.getResources(), newRotation); 76 final Configuration config = new Configuration(); 77 config.unset(); 78 config.orientation = mDisplayLayout.getOrientation(); 79 Rect tmpRect = new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); 80 tmpRect.inset(mDisplayLayout.nonDecorInsets()); 81 config.windowConfiguration.setAppBounds(tmpRect); 82 tmpRect.set(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); 83 tmpRect.inset(mDisplayLayout.stableInsets()); 84 config.screenWidthDp = (int) (tmpRect.width() / mDisplayLayout.density()); 85 config.screenHeightDp = (int) (tmpRect.height() / mDisplayLayout.density()); 86 mContext = mContext.createConfigurationContext(config); 87 mSnapAlgorithm = null; 88 mMinimizedSnapAlgorithm = null; 89 mResourcesValid = false; 90 } 91 updateResources()92 private void updateResources() { 93 if (mResourcesValid) { 94 return; 95 } 96 mResourcesValid = true; 97 Resources res = mContext.getResources(); 98 mDividerSize = DockedDividerUtils.getDividerSize(res, 99 DockedDividerUtils.getDividerInsets(res)); 100 mDividerSizeInactive = (int) TypedValue.applyDimension( 101 TypedValue.COMPLEX_UNIT_DIP, DIVIDER_WIDTH_INACTIVE_DP, res.getDisplayMetrics()); 102 } 103 getPrimarySplitSide()104 int getPrimarySplitSide() { 105 switch (mDisplayLayout.getNavigationBarPosition(mContext.getResources())) { 106 case DisplayLayout.NAV_BAR_BOTTOM: 107 return mDisplayLayout.isLandscape() ? DOCKED_LEFT : DOCKED_TOP; 108 case DisplayLayout.NAV_BAR_LEFT: 109 return DOCKED_RIGHT; 110 case DisplayLayout.NAV_BAR_RIGHT: 111 return DOCKED_LEFT; 112 default: 113 return DOCKED_INVALID; 114 } 115 } 116 getSnapAlgorithm()117 DividerSnapAlgorithm getSnapAlgorithm() { 118 if (mSnapAlgorithm == null) { 119 updateResources(); 120 boolean isHorizontalDivision = !mDisplayLayout.isLandscape(); 121 mSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(), 122 mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize, 123 isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide()); 124 } 125 return mSnapAlgorithm; 126 } 127 getMinimizedSnapAlgorithm(boolean homeStackResizable)128 DividerSnapAlgorithm getMinimizedSnapAlgorithm(boolean homeStackResizable) { 129 if (mMinimizedSnapAlgorithm == null) { 130 updateResources(); 131 boolean isHorizontalDivision = !mDisplayLayout.isLandscape(); 132 mMinimizedSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(), 133 mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize, 134 isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide(), 135 true /* isMinimized */, homeStackResizable); 136 } 137 return mMinimizedSnapAlgorithm; 138 } 139 140 /** 141 * Resize primary bounds and secondary bounds by divider position. 142 * 143 * @param position divider position. 144 * @return true if calculated bounds changed. 145 */ resizeSplits(int position)146 boolean resizeSplits(int position) { 147 mPrimary = mPrimary == null ? new Rect() : mPrimary; 148 mSecondary = mSecondary == null ? new Rect() : mSecondary; 149 int dockSide = getPrimarySplitSide(); 150 boolean boundsChanged; 151 152 mTmpBounds.set(mPrimary); 153 DockedDividerUtils.calculateBoundsForPosition(position, dockSide, mPrimary, 154 mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize); 155 boundsChanged = !mPrimary.equals(mTmpBounds); 156 157 mTmpBounds.set(mSecondary); 158 DockedDividerUtils.calculateBoundsForPosition(position, 159 DockedDividerUtils.invertDockSide(dockSide), mSecondary, mDisplayLayout.width(), 160 mDisplayLayout.height(), mDividerSize); 161 boundsChanged |= !mSecondary.equals(mTmpBounds); 162 return boundsChanged; 163 } 164 resizeSplits(int position, WindowContainerTransaction t)165 void resizeSplits(int position, WindowContainerTransaction t) { 166 if (resizeSplits(position)) { 167 t.setBounds(mTiles.mPrimary.token, mPrimary); 168 t.setBounds(mTiles.mSecondary.token, mSecondary); 169 170 t.setSmallestScreenWidthDp(mTiles.mPrimary.token, 171 getSmallestWidthDpForBounds(mContext, mDisplayLayout, mPrimary)); 172 t.setSmallestScreenWidthDp(mTiles.mSecondary.token, 173 getSmallestWidthDpForBounds(mContext, mDisplayLayout, mSecondary)); 174 } 175 } 176 calcResizableMinimizedHomeStackBounds()177 Rect calcResizableMinimizedHomeStackBounds() { 178 DividerSnapAlgorithm.SnapTarget miniMid = 179 getMinimizedSnapAlgorithm(true /* resizable */).getMiddleTarget(); 180 Rect homeBounds = new Rect(); 181 DockedDividerUtils.calculateBoundsForPosition(miniMid.position, 182 DockedDividerUtils.invertDockSide(getPrimarySplitSide()), homeBounds, 183 mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize); 184 return homeBounds; 185 } 186 187 /** 188 * Updates the adjustment depending on it's current state. 189 */ updateAdjustedBounds(int currImeTop, int hiddenTop, int shownTop)190 void updateAdjustedBounds(int currImeTop, int hiddenTop, int shownTop) { 191 adjustForIME(mDisplayLayout, currImeTop, hiddenTop, shownTop, mDividerSize, 192 mDividerSizeInactive, mPrimary, mSecondary); 193 } 194 195 /** 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)196 private void adjustForIME(DisplayLayout dl, int currImeTop, int hiddenTop, int shownTop, 197 int dividerWidth, int dividerWidthInactive, Rect primaryBounds, Rect secondaryBounds) { 198 if (mAdjustedPrimary == null) { 199 mAdjustedPrimary = new Rect(); 200 mAdjustedSecondary = new Rect(); 201 } 202 203 final Rect displayStableRect = new Rect(); 204 dl.getStableBounds(displayStableRect); 205 206 final float shownFraction = ((float) (currImeTop - hiddenTop)) / (shownTop - hiddenTop); 207 final int currDividerWidth = 208 (int) (dividerWidthInactive * shownFraction + dividerWidth * (1.f - shownFraction)); 209 210 // Calculate the highest we can move the bottom of the top stack to keep 30% visible. 211 final int minTopStackBottom = displayStableRect.top 212 + (int) ((mPrimary.bottom - displayStableRect.top) * ADJUSTED_STACK_FRACTION_MIN); 213 // Based on that, calculate the maximum amount we'll allow the ime to shift things. 214 final int maxOffset = mPrimary.bottom - minTopStackBottom; 215 // Calculate how much we would shift things without limits (basically the height of ime). 216 final int desiredOffset = hiddenTop - shownTop; 217 // Calculate an "adjustedTop" which is the currImeTop but restricted by our constraints. 218 // We want an effect where the adjustment only occurs during the "highest" portion of the 219 // ime animation. This is done by shifting the adjustment values by the difference in 220 // offsets (effectively playing the whole adjustment animation some fixed amount of pixels 221 // below the ime top). 222 final int topCorrection = Math.max(0, desiredOffset - maxOffset); 223 final int adjustedTop = currImeTop + topCorrection; 224 // The actual yOffset is the distance between adjustedTop and the bottom of the display. 225 // Since our adjustedTop values are playing "below" the ime, we clamp at 0 so we only 226 // see adjustment upward. 227 final int yOffset = Math.max(0, dl.height() - adjustedTop); 228 229 // TOP 230 // Reduce the offset by an additional small amount to squish the divider bar. 231 mAdjustedPrimary.set(primaryBounds); 232 mAdjustedPrimary.offset(0, -yOffset + (dividerWidth - currDividerWidth)); 233 234 // BOTTOM 235 mAdjustedSecondary.set(secondaryBounds); 236 mAdjustedSecondary.offset(0, -yOffset); 237 } 238 getSmallestWidthDpForBounds(@onNull Context context, DisplayLayout dl, Rect bounds)239 static int getSmallestWidthDpForBounds(@NonNull Context context, DisplayLayout dl, 240 Rect bounds) { 241 int dividerSize = DockedDividerUtils.getDividerSize(context.getResources(), 242 DockedDividerUtils.getDividerInsets(context.getResources())); 243 244 int minWidth = Integer.MAX_VALUE; 245 246 // Go through all screen orientations and find the orientation in which the task has the 247 // smallest width. 248 Rect tmpRect = new Rect(); 249 Rect rotatedDisplayRect = new Rect(); 250 Rect displayRect = new Rect(0, 0, dl.width(), dl.height()); 251 252 DisplayLayout tmpDL = new DisplayLayout(); 253 for (int rotation = 0; rotation < 4; rotation++) { 254 tmpDL.set(dl); 255 tmpDL.rotateTo(context.getResources(), rotation); 256 DividerSnapAlgorithm snap = initSnapAlgorithmForRotation(context, tmpDL, dividerSize); 257 258 tmpRect.set(bounds); 259 rotateBounds(tmpRect, displayRect, dl.rotation(), rotation); 260 rotatedDisplayRect.set(0, 0, tmpDL.width(), tmpDL.height()); 261 final int dockSide = getPrimarySplitSide(tmpRect, rotatedDisplayRect, 262 tmpDL.getOrientation()); 263 final int position = DockedDividerUtils.calculatePositionForBounds(tmpRect, dockSide, 264 dividerSize); 265 266 final int snappedPosition = 267 snap.calculateNonDismissingSnapTarget(position).position; 268 DockedDividerUtils.calculateBoundsForPosition(snappedPosition, dockSide, tmpRect, 269 tmpDL.width(), tmpDL.height(), dividerSize); 270 Rect insettedDisplay = new Rect(rotatedDisplayRect); 271 insettedDisplay.inset(tmpDL.stableInsets()); 272 tmpRect.intersect(insettedDisplay); 273 minWidth = Math.min(tmpRect.width(), minWidth); 274 } 275 return (int) (minWidth / dl.density()); 276 } 277 initSnapAlgorithmForRotation(Context context, DisplayLayout dl, int dividerSize)278 static DividerSnapAlgorithm initSnapAlgorithmForRotation(Context context, DisplayLayout dl, 279 int dividerSize) { 280 final Configuration config = new Configuration(); 281 config.unset(); 282 config.orientation = dl.getOrientation(); 283 Rect tmpRect = new Rect(0, 0, dl.width(), dl.height()); 284 tmpRect.inset(dl.nonDecorInsets()); 285 config.windowConfiguration.setAppBounds(tmpRect); 286 tmpRect.set(0, 0, dl.width(), dl.height()); 287 tmpRect.inset(dl.stableInsets()); 288 config.screenWidthDp = (int) (tmpRect.width() / dl.density()); 289 config.screenHeightDp = (int) (tmpRect.height() / dl.density()); 290 final Context rotationContext = context.createConfigurationContext(config); 291 return new DividerSnapAlgorithm( 292 rotationContext.getResources(), dl.width(), dl.height(), dividerSize, 293 config.orientation == ORIENTATION_PORTRAIT, dl.stableInsets()); 294 } 295 296 /** 297 * Get the current primary-split side. Determined by its location of {@param bounds} within 298 * {@param displayRect} but if both are the same, it will try to dock to each side and determine 299 * if allowed in its respected {@param orientation}. 300 * 301 * @param bounds bounds of the primary split task to get which side is docked 302 * @param displayRect bounds of the display that contains the primary split task 303 * @param orientation the origination of device 304 * @return current primary-split side 305 */ getPrimarySplitSide(Rect bounds, Rect displayRect, int orientation)306 static int getPrimarySplitSide(Rect bounds, Rect displayRect, int orientation) { 307 if (orientation == ORIENTATION_PORTRAIT) { 308 // Portrait mode, docked either at the top or the bottom. 309 final int diff = (displayRect.bottom - bounds.bottom) - (bounds.top - displayRect.top); 310 if (diff < 0) { 311 return DOCKED_BOTTOM; 312 } else { 313 // Top is default 314 return DOCKED_TOP; 315 } 316 } else if (orientation == ORIENTATION_LANDSCAPE) { 317 // Landscape mode, docked either on the left or on the right. 318 final int diff = (displayRect.right - bounds.right) - (bounds.left - displayRect.left); 319 if (diff < 0) { 320 return DOCKED_RIGHT; 321 } 322 return DOCKED_LEFT; 323 } 324 return DOCKED_INVALID; 325 } 326 } 327