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