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