blob: 271faed54bcab0a7744a14bd079f45e46d866cc8 [file] [log] [blame]
Evan Roskyaf9f27c2020-02-18 18:58:35 +00001/*
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
17package com.android.systemui.stackdivider;
18
19import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
20import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
21import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
22import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
23import static android.view.WindowManager.DOCKED_BOTTOM;
24import static android.view.WindowManager.DOCKED_INVALID;
25import static android.view.WindowManager.DOCKED_LEFT;
26import static android.view.WindowManager.DOCKED_RIGHT;
27import static android.view.WindowManager.DOCKED_TOP;
28
29import android.annotation.NonNull;
30import android.content.Context;
31import android.content.res.Configuration;
32import android.content.res.Resources;
33import android.graphics.Rect;
34import android.util.TypedValue;
35import android.view.WindowContainerTransaction;
36
37import com.android.internal.policy.DividerSnapAlgorithm;
38import com.android.internal.policy.DockedDividerUtils;
39import 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 */
45public 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 Rosky95729202020-02-21 10:16:08 -0800174 void updateAdjustedBounds(int currImeTop, int hiddenTop, int shownTop) {
175 adjustForIME(mDisplayLayout, currImeTop, hiddenTop, shownTop, mDividerSize,
Evan Roskyaf9f27c2020-02-18 18:58:35 +0000176 mDividerSizeInactive, mPrimary, mSecondary);
177 }
178
Evan Roskyaf9f27c2020-02-18 18:58:35 +0000179 /** Assumes top/bottom split. Splits are not adjusted for left/right splits. */
Evan Rosky95729202020-02-21 10:16:08 -0800180 private void adjustForIME(DisplayLayout dl, int currImeTop, int hiddenTop, int shownTop,
Evan Roskyaf9f27c2020-02-18 18:58:35 +0000181 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 Rosky95729202020-02-21 10:16:08 -0800190 final float shownFraction = ((float) (currImeTop - hiddenTop)) / (shownTop - hiddenTop);
Evan Roskyaf9f27c2020-02-18 18:58:35 +0000191 final int currDividerWidth =
Evan Rosky95729202020-02-21 10:16:08 -0800192 (int) (dividerWidthInactive * shownFraction + dividerWidth * (1.f - shownFraction));
Evan Roskyaf9f27c2020-02-18 18:58:35 +0000193
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}