Evan Rosky | af9f27c | 2020-02-18 18:58:35 +0000 | [diff] [blame] | 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.systemui.stackdivider; |
| 18 | |
| 19 | import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; |
| 20 | import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; |
| 21 | import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; |
| 22 | import static android.content.res.Configuration.ORIENTATION_PORTRAIT; |
| 23 | import static android.view.WindowManager.DOCKED_BOTTOM; |
| 24 | import static android.view.WindowManager.DOCKED_INVALID; |
| 25 | import static android.view.WindowManager.DOCKED_LEFT; |
| 26 | import static android.view.WindowManager.DOCKED_RIGHT; |
| 27 | import static android.view.WindowManager.DOCKED_TOP; |
| 28 | |
| 29 | import android.annotation.NonNull; |
| 30 | import android.content.Context; |
| 31 | import android.content.res.Configuration; |
| 32 | import android.content.res.Resources; |
| 33 | import android.graphics.Rect; |
| 34 | import android.util.TypedValue; |
| 35 | import android.view.WindowContainerTransaction; |
| 36 | |
| 37 | import com.android.internal.policy.DividerSnapAlgorithm; |
| 38 | import com.android.internal.policy.DockedDividerUtils; |
| 39 | import com.android.systemui.wm.DisplayLayout; |
| 40 | |
| 41 | /** |
| 42 | * Handles split-screen related internal display layout. In general, this represents the |
| 43 | * WM-facing understanding of the splits. |
| 44 | */ |
| 45 | public class SplitDisplayLayout { |
| 46 | /** Minimum size of an adjusted stack bounds relative to original stack bounds. Used to |
| 47 | * restrict IME adjustment so that a min portion of top stack remains visible.*/ |
| 48 | private static final float ADJUSTED_STACK_FRACTION_MIN = 0.3f; |
| 49 | |
| 50 | private static final int DIVIDER_WIDTH_INACTIVE_DP = 4; |
| 51 | |
| 52 | SplitScreenTaskOrganizer mTiles; |
| 53 | DisplayLayout mDisplayLayout; |
| 54 | Context mContext; |
| 55 | |
| 56 | // Lazy stuff |
| 57 | boolean mResourcesValid = false; |
| 58 | int mDividerSize; |
| 59 | int mDividerSizeInactive; |
| 60 | private DividerSnapAlgorithm mSnapAlgorithm = null; |
| 61 | private DividerSnapAlgorithm mMinimizedSnapAlgorithm = null; |
| 62 | Rect mPrimary = null; |
| 63 | Rect mSecondary = null; |
| 64 | Rect mAdjustedPrimary = null; |
| 65 | Rect mAdjustedSecondary = null; |
| 66 | |
| 67 | public SplitDisplayLayout(Context ctx, DisplayLayout dl, SplitScreenTaskOrganizer taskTiles) { |
| 68 | mTiles = taskTiles; |
| 69 | mDisplayLayout = dl; |
| 70 | mContext = ctx; |
| 71 | } |
| 72 | |
| 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 | |
| 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 | |
| 103 | int getPrimarySplitSide() { |
| 104 | return mDisplayLayout.isLandscape() ? DOCKED_LEFT : DOCKED_TOP; |
| 105 | } |
| 106 | |
| 107 | boolean isMinimized() { |
| 108 | return mTiles.mSecondary.topActivityType == ACTIVITY_TYPE_HOME |
| 109 | || mTiles.mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS; |
| 110 | } |
| 111 | |
| 112 | DividerSnapAlgorithm getSnapAlgorithm() { |
| 113 | if (mSnapAlgorithm == null) { |
| 114 | updateResources(); |
| 115 | boolean isHorizontalDivision = !mDisplayLayout.isLandscape(); |
| 116 | mSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(), |
| 117 | mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize, |
| 118 | isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide()); |
| 119 | } |
| 120 | return mSnapAlgorithm; |
| 121 | } |
| 122 | |
| 123 | DividerSnapAlgorithm getMinimizedSnapAlgorithm() { |
| 124 | if (mMinimizedSnapAlgorithm == null) { |
| 125 | updateResources(); |
| 126 | boolean isHorizontalDivision = !mDisplayLayout.isLandscape(); |
| 127 | mMinimizedSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(), |
| 128 | mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize, |
| 129 | isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide(), |
| 130 | true /* isMinimized */); |
| 131 | } |
| 132 | return mMinimizedSnapAlgorithm; |
| 133 | } |
| 134 | |
| 135 | void resizeSplits(int position) { |
| 136 | mPrimary = mPrimary == null ? new Rect() : mPrimary; |
| 137 | mSecondary = mSecondary == null ? new Rect() : mSecondary; |
| 138 | calcSplitBounds(position, mPrimary, mSecondary); |
| 139 | } |
| 140 | |
| 141 | void resizeSplits(int position, WindowContainerTransaction t) { |
| 142 | resizeSplits(position); |
| 143 | t.setBounds(mTiles.mPrimary.token, mPrimary); |
| 144 | t.setBounds(mTiles.mSecondary.token, mSecondary); |
| 145 | |
| 146 | t.setSmallestScreenWidthDp(mTiles.mPrimary.token, |
| 147 | getSmallestWidthDpForBounds(mContext, mDisplayLayout, mPrimary)); |
| 148 | t.setSmallestScreenWidthDp(mTiles.mSecondary.token, |
| 149 | getSmallestWidthDpForBounds(mContext, mDisplayLayout, mSecondary)); |
| 150 | } |
| 151 | |
| 152 | void calcSplitBounds(int position, @NonNull Rect outPrimary, @NonNull Rect outSecondary) { |
| 153 | int dockSide = getPrimarySplitSide(); |
| 154 | DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outPrimary, |
| 155 | mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize); |
| 156 | |
| 157 | DockedDividerUtils.calculateBoundsForPosition(position, |
| 158 | DockedDividerUtils.invertDockSide(dockSide), outSecondary, mDisplayLayout.width(), |
| 159 | mDisplayLayout.height(), mDividerSize); |
| 160 | } |
| 161 | |
| 162 | Rect calcMinimizedHomeStackBounds() { |
| 163 | DividerSnapAlgorithm.SnapTarget miniMid = getMinimizedSnapAlgorithm().getMiddleTarget(); |
| 164 | Rect homeBounds = new Rect(); |
| 165 | DockedDividerUtils.calculateBoundsForPosition(miniMid.position, |
| 166 | DockedDividerUtils.invertDockSide(getPrimarySplitSide()), homeBounds, |
| 167 | mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize); |
| 168 | return homeBounds; |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | * Updates the adjustment depending on it's current state. |
| 173 | */ |
Evan Rosky | 9572920 | 2020-02-21 10:16:08 -0800 | [diff] [blame] | 174 | void updateAdjustedBounds(int currImeTop, int hiddenTop, int shownTop) { |
| 175 | adjustForIME(mDisplayLayout, currImeTop, hiddenTop, shownTop, mDividerSize, |
Evan Rosky | af9f27c | 2020-02-18 18:58:35 +0000 | [diff] [blame] | 176 | mDividerSizeInactive, mPrimary, mSecondary); |
| 177 | } |
| 178 | |
Evan Rosky | af9f27c | 2020-02-18 18:58:35 +0000 | [diff] [blame] | 179 | /** Assumes top/bottom split. Splits are not adjusted for left/right splits. */ |
Evan Rosky | 9572920 | 2020-02-21 10:16:08 -0800 | [diff] [blame] | 180 | private void adjustForIME(DisplayLayout dl, int currImeTop, int hiddenTop, int shownTop, |
Evan Rosky | af9f27c | 2020-02-18 18:58:35 +0000 | [diff] [blame] | 181 | int dividerWidth, int dividerWidthInactive, Rect primaryBounds, Rect secondaryBounds) { |
| 182 | if (mAdjustedPrimary == null) { |
| 183 | mAdjustedPrimary = new Rect(); |
| 184 | mAdjustedSecondary = new Rect(); |
| 185 | } |
| 186 | |
| 187 | final Rect displayStableRect = new Rect(); |
| 188 | dl.getStableBounds(displayStableRect); |
| 189 | |
Evan Rosky | 9572920 | 2020-02-21 10:16:08 -0800 | [diff] [blame] | 190 | final float shownFraction = ((float) (currImeTop - hiddenTop)) / (shownTop - hiddenTop); |
Evan Rosky | af9f27c | 2020-02-18 18:58:35 +0000 | [diff] [blame] | 191 | final int currDividerWidth = |
Evan Rosky | 9572920 | 2020-02-21 10:16:08 -0800 | [diff] [blame] | 192 | (int) (dividerWidthInactive * shownFraction + dividerWidth * (1.f - shownFraction)); |
Evan Rosky | af9f27c | 2020-02-18 18:58:35 +0000 | [diff] [blame] | 193 | |
| 194 | final int minTopStackBottom = displayStableRect.top |
| 195 | + (int) ((mPrimary.bottom - displayStableRect.top) * ADJUSTED_STACK_FRACTION_MIN); |
| 196 | final int minImeTop = minTopStackBottom + currDividerWidth; |
| 197 | |
| 198 | // Calculate an offset which shifts the stacks up by the height of the IME, but still |
| 199 | // leaves at least 30% of the top stack visible. |
| 200 | final int yOffset = Math.max(0, dl.height() - Math.max(currImeTop, minImeTop)); |
| 201 | |
| 202 | // TOP |
| 203 | // Reduce the offset by an additional small amount to squish the divider bar. |
| 204 | mAdjustedPrimary.set(primaryBounds); |
| 205 | mAdjustedPrimary.offset(0, -yOffset + (dividerWidth - currDividerWidth)); |
| 206 | |
| 207 | // BOTTOM |
| 208 | mAdjustedSecondary.set(secondaryBounds); |
| 209 | mAdjustedSecondary.offset(0, -yOffset); |
| 210 | } |
| 211 | |
| 212 | static int getSmallestWidthDpForBounds(@NonNull Context context, DisplayLayout dl, |
| 213 | Rect bounds) { |
| 214 | int dividerSize = DockedDividerUtils.getDividerSize(context.getResources(), |
| 215 | DockedDividerUtils.getDividerInsets(context.getResources())); |
| 216 | |
| 217 | int minWidth = Integer.MAX_VALUE; |
| 218 | |
| 219 | // Go through all screen orientations and find the orientation in which the task has the |
| 220 | // smallest width. |
| 221 | Rect tmpRect = new Rect(); |
| 222 | Rect rotatedDisplayRect = new Rect(); |
| 223 | Rect displayRect = new Rect(0, 0, dl.width(), dl.height()); |
| 224 | |
| 225 | DisplayLayout tmpDL = new DisplayLayout(); |
| 226 | for (int rotation = 0; rotation < 4; rotation++) { |
| 227 | tmpDL.set(dl); |
| 228 | tmpDL.rotateTo(context.getResources(), rotation); |
| 229 | DividerSnapAlgorithm snap = initSnapAlgorithmForRotation(context, tmpDL, dividerSize); |
| 230 | |
| 231 | tmpRect.set(bounds); |
| 232 | DisplayLayout.rotateBounds(tmpRect, displayRect, rotation - dl.rotation()); |
| 233 | rotatedDisplayRect.set(0, 0, tmpDL.width(), tmpDL.height()); |
| 234 | final int dockSide = getPrimarySplitSide(tmpRect, rotatedDisplayRect, |
| 235 | tmpDL.getOrientation()); |
| 236 | final int position = DockedDividerUtils.calculatePositionForBounds(tmpRect, dockSide, |
| 237 | dividerSize); |
| 238 | |
| 239 | final int snappedPosition = |
| 240 | snap.calculateNonDismissingSnapTarget(position).position; |
| 241 | DockedDividerUtils.calculateBoundsForPosition(snappedPosition, dockSide, tmpRect, |
| 242 | tmpDL.width(), tmpDL.height(), dividerSize); |
| 243 | Rect insettedDisplay = new Rect(rotatedDisplayRect); |
| 244 | insettedDisplay.inset(tmpDL.stableInsets()); |
| 245 | tmpRect.intersect(insettedDisplay); |
| 246 | minWidth = Math.min(tmpRect.width(), minWidth); |
| 247 | } |
| 248 | return (int) (minWidth / dl.density()); |
| 249 | } |
| 250 | |
| 251 | static DividerSnapAlgorithm initSnapAlgorithmForRotation(Context context, DisplayLayout dl, |
| 252 | int dividerSize) { |
| 253 | final Configuration config = new Configuration(); |
| 254 | config.unset(); |
| 255 | config.orientation = dl.getOrientation(); |
| 256 | Rect tmpRect = new Rect(0, 0, dl.width(), dl.height()); |
| 257 | tmpRect.inset(dl.nonDecorInsets()); |
| 258 | config.windowConfiguration.setAppBounds(tmpRect); |
| 259 | tmpRect.set(0, 0, dl.width(), dl.height()); |
| 260 | tmpRect.inset(dl.stableInsets()); |
| 261 | config.screenWidthDp = (int) (tmpRect.width() / dl.density()); |
| 262 | config.screenHeightDp = (int) (tmpRect.height() / dl.density()); |
| 263 | final Context rotationContext = context.createConfigurationContext(config); |
| 264 | return new DividerSnapAlgorithm( |
| 265 | rotationContext.getResources(), dl.width(), dl.height(), dividerSize, |
| 266 | config.orientation == ORIENTATION_PORTRAIT, dl.stableInsets()); |
| 267 | } |
| 268 | |
| 269 | /** |
| 270 | * Get the current primary-split side. Determined by its location of {@param bounds} within |
| 271 | * {@param displayRect} but if both are the same, it will try to dock to each side and determine |
| 272 | * if allowed in its respected {@param orientation}. |
| 273 | * |
| 274 | * @param bounds bounds of the primary split task to get which side is docked |
| 275 | * @param displayRect bounds of the display that contains the primary split task |
| 276 | * @param orientation the origination of device |
| 277 | * @return current primary-split side |
| 278 | */ |
| 279 | static int getPrimarySplitSide(Rect bounds, Rect displayRect, int orientation) { |
| 280 | if (orientation == ORIENTATION_PORTRAIT) { |
| 281 | // Portrait mode, docked either at the top or the bottom. |
| 282 | final int diff = (displayRect.bottom - bounds.bottom) - (bounds.top - displayRect.top); |
| 283 | if (diff < 0) { |
| 284 | return DOCKED_BOTTOM; |
| 285 | } else { |
| 286 | // Top is default |
| 287 | return DOCKED_TOP; |
| 288 | } |
| 289 | } else if (orientation == ORIENTATION_LANDSCAPE) { |
| 290 | // Landscape mode, docked either on the left or on the right. |
| 291 | final int diff = (displayRect.right - bounds.right) - (bounds.left - displayRect.left); |
| 292 | if (diff < 0) { |
| 293 | return DOCKED_RIGHT; |
| 294 | } |
| 295 | return DOCKED_LEFT; |
| 296 | } |
| 297 | return DOCKED_INVALID; |
| 298 | } |
| 299 | } |