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