• 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.common.split;
18 
19 import static android.view.WindowManager.DOCKED_LEFT;
20 import static android.view.WindowManager.DOCKED_TOP;
21 
22 import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END;
23 import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START;
24 
25 import android.animation.Animator;
26 import android.animation.AnimatorListenerAdapter;
27 import android.animation.ValueAnimator;
28 import android.annotation.IntDef;
29 import android.app.ActivityManager;
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.view.SurfaceControl;
35 import android.view.WindowInsets;
36 import android.view.WindowManager;
37 import android.window.WindowContainerToken;
38 import android.window.WindowContainerTransaction;
39 
40 import androidx.annotation.Nullable;
41 
42 import com.android.internal.policy.DividerSnapAlgorithm;
43 import com.android.wm.shell.ShellTaskOrganizer;
44 import com.android.wm.shell.animation.Interpolators;
45 import com.android.wm.shell.common.DisplayImeController;
46 
47 /**
48  * Records and handles layout of splits. Helps to calculate proper bounds when configuration or
49  * divide position changes.
50  */
51 public final class SplitLayout {
52     /**
53      * Split position isn't specified normally meaning to use what ever it is currently set to.
54      */
55     public static final int SPLIT_POSITION_UNDEFINED = -1;
56 
57     /**
58      * Specifies that a split is positioned at the top half of the screen if
59      * in portrait mode or at the left half of the screen if in landscape mode.
60      */
61     public static final int SPLIT_POSITION_TOP_OR_LEFT = 0;
62 
63     /**
64      * Specifies that a split is positioned at the bottom half of the screen if
65      * in portrait mode or at the right half of the screen if in landscape mode.
66      */
67     public static final int SPLIT_POSITION_BOTTOM_OR_RIGHT = 1;
68 
69     @IntDef(prefix = {"SPLIT_POSITION_"}, value = {
70             SPLIT_POSITION_UNDEFINED,
71             SPLIT_POSITION_TOP_OR_LEFT,
72             SPLIT_POSITION_BOTTOM_OR_RIGHT
73     })
74     public @interface SplitPosition {
75     }
76 
77     private final int mDividerWindowWidth;
78     private final int mDividerInsets;
79     private final int mDividerSize;
80 
81     private final Rect mRootBounds = new Rect();
82     private final Rect mDividerBounds = new Rect();
83     private final Rect mBounds1 = new Rect();
84     private final Rect mBounds2 = new Rect();
85     private final SplitLayoutHandler mSplitLayoutHandler;
86     private final SplitWindowManager mSplitWindowManager;
87     private final DisplayImeController mDisplayImeController;
88     private final ImePositionProcessor mImePositionProcessor;
89     private final ShellTaskOrganizer mTaskOrganizer;
90 
91     private Context mContext;
92     private DividerSnapAlgorithm mDividerSnapAlgorithm;
93     private int mDividePosition;
94     private boolean mInitialized = false;
95 
SplitLayout(String windowName, Context context, Configuration configuration, SplitLayoutHandler splitLayoutHandler, SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks, DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer)96     public SplitLayout(String windowName, Context context, Configuration configuration,
97             SplitLayoutHandler splitLayoutHandler,
98             SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks,
99             DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer) {
100         mContext = context.createConfigurationContext(configuration);
101         mSplitLayoutHandler = splitLayoutHandler;
102         mDisplayImeController = displayImeController;
103         mSplitWindowManager = new SplitWindowManager(
104                 windowName, mContext, configuration, parentContainerCallbacks);
105         mTaskOrganizer = taskOrganizer;
106         mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId());
107 
108         final Resources resources = context.getResources();
109         mDividerWindowWidth = resources.getDimensionPixelSize(
110                 com.android.internal.R.dimen.docked_stack_divider_thickness);
111         mDividerInsets = resources.getDimensionPixelSize(
112                 com.android.internal.R.dimen.docked_stack_divider_insets);
113         mDividerSize = mDividerWindowWidth - mDividerInsets * 2;
114 
115         mRootBounds.set(configuration.windowConfiguration.getBounds());
116         mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
117         resetDividerPosition();
118     }
119 
120     /** Gets bounds of the primary split. */
getBounds1()121     public Rect getBounds1() {
122         return new Rect(mBounds1);
123     }
124 
125     /** Gets bounds of the secondary split. */
getBounds2()126     public Rect getBounds2() {
127         return new Rect(mBounds2);
128     }
129 
130     /** Gets bounds of divider window. */
getDividerBounds()131     public Rect getDividerBounds() {
132         return new Rect(mDividerBounds);
133     }
134 
135     /** Returns leash of the current divider bar. */
136     @Nullable
getDividerLeash()137     public SurfaceControl getDividerLeash() {
138         return mSplitWindowManager == null ? null : mSplitWindowManager.getSurfaceControl();
139     }
140 
getDividePosition()141     int getDividePosition() {
142         return mDividePosition;
143     }
144 
145     /** Applies new configuration, returns {@code false} if there's no effect to the layout. */
updateConfiguration(Configuration configuration)146     public boolean updateConfiguration(Configuration configuration) {
147         final Rect rootBounds = configuration.windowConfiguration.getBounds();
148         if (mRootBounds.equals(rootBounds)) {
149             return false;
150         }
151 
152         mContext = mContext.createConfigurationContext(configuration);
153         mSplitWindowManager.setConfiguration(configuration);
154         mRootBounds.set(rootBounds);
155         mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
156         resetDividerPosition();
157 
158         // Don't inflate divider bar if it is not initialized.
159         if (!mInitialized) {
160             return false;
161         }
162 
163         release();
164         init();
165         return true;
166     }
167 
168     /** Updates recording bounds of divider window and both of the splits. */
updateBounds(int position)169     private void updateBounds(int position) {
170         mDividerBounds.set(mRootBounds);
171         mBounds1.set(mRootBounds);
172         mBounds2.set(mRootBounds);
173         if (isLandscape(mRootBounds)) {
174             position += mRootBounds.left;
175             mDividerBounds.left = position - mDividerInsets;
176             mDividerBounds.right = mDividerBounds.left + mDividerWindowWidth;
177             mBounds1.right = position;
178             mBounds2.left = mBounds1.right + mDividerSize;
179         } else {
180             position += mRootBounds.top;
181             mDividerBounds.top = position - mDividerInsets;
182             mDividerBounds.bottom = mDividerBounds.top + mDividerWindowWidth;
183             mBounds1.bottom = position;
184             mBounds2.top = mBounds1.bottom + mDividerSize;
185         }
186     }
187 
188     /** Inflates {@link DividerView} on the root surface. */
init()189     public void init() {
190         if (mInitialized) return;
191         mInitialized = true;
192         mSplitWindowManager.init(this);
193         mDisplayImeController.addPositionProcessor(mImePositionProcessor);
194     }
195 
196     /** Releases the surface holding the current {@link DividerView}. */
release()197     public void release() {
198         if (!mInitialized) return;
199         mInitialized = false;
200         mSplitWindowManager.release();
201         mDisplayImeController.removePositionProcessor(mImePositionProcessor);
202         mImePositionProcessor.reset();
203     }
204 
205     /**
206      * Updates bounds with the passing position. Usually used to update recording bounds while
207      * performing animation or dragging divider bar to resize the splits.
208      */
updateDivideBounds(int position)209     void updateDivideBounds(int position) {
210         updateBounds(position);
211         mSplitWindowManager.setResizingSplits(true);
212         mSplitLayoutHandler.onBoundsChanging(this);
213     }
214 
setDividePosition(int position)215     void setDividePosition(int position) {
216         mDividePosition = position;
217         updateBounds(mDividePosition);
218         mSplitLayoutHandler.onBoundsChanged(this);
219         mSplitWindowManager.setResizingSplits(false);
220     }
221 
222     /** Resets divider position. */
resetDividerPosition()223     public void resetDividerPosition() {
224         mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position;
225         updateBounds(mDividePosition);
226     }
227 
228     /**
229      * Sets new divide position and updates bounds correspondingly. Notifies listener if the new
230      * target indicates dismissing split.
231      */
snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget)232     public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) {
233         switch (snapTarget.flag) {
234             case FLAG_DISMISS_START:
235                 mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */);
236                 mSplitWindowManager.setResizingSplits(false);
237                 break;
238             case FLAG_DISMISS_END:
239                 mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */);
240                 mSplitWindowManager.setResizingSplits(false);
241                 break;
242             default:
243                 flingDividePosition(currentPosition, snapTarget.position);
244                 break;
245         }
246     }
247 
onDoubleTappedDivider()248     void onDoubleTappedDivider() {
249         mSplitLayoutHandler.onDoubleTappedDivider();
250     }
251 
252     /**
253      * Returns {@link DividerSnapAlgorithm.SnapTarget} which matches passing position and velocity.
254      * If hardDismiss is set to {@code true}, it will be harder to reach dismiss target.
255      */
findSnapTarget(int position, float velocity, boolean hardDismiss)256     public DividerSnapAlgorithm.SnapTarget findSnapTarget(int position, float velocity,
257             boolean hardDismiss) {
258         return mDividerSnapAlgorithm.calculateSnapTarget(position, velocity, hardDismiss);
259     }
260 
getSnapAlgorithm(Context context, Rect rootBounds)261     private DividerSnapAlgorithm getSnapAlgorithm(Context context, Rect rootBounds) {
262         final boolean isLandscape = isLandscape(rootBounds);
263         return new DividerSnapAlgorithm(
264                 context.getResources(),
265                 rootBounds.width(),
266                 rootBounds.height(),
267                 mDividerSize,
268                 !isLandscape,
269                 getDisplayInsets(context),
270                 isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */);
271     }
272 
flingDividePosition(int from, int to)273     private void flingDividePosition(int from, int to) {
274         if (from == to) return;
275         ValueAnimator animator = ValueAnimator
276                 .ofInt(from, to)
277                 .setDuration(250);
278         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
279         animator.addUpdateListener(
280                 animation -> updateDivideBounds((int) animation.getAnimatedValue()));
281         animator.addListener(new AnimatorListenerAdapter() {
282             @Override
283             public void onAnimationEnd(Animator animation) {
284                 setDividePosition(to);
285             }
286 
287             @Override
288             public void onAnimationCancel(Animator animation) {
289                 setDividePosition(to);
290             }
291         });
292         animator.start();
293     }
294 
getDisplayInsets(Context context)295     private static Rect getDisplayInsets(Context context) {
296         return context.getSystemService(WindowManager.class)
297                 .getMaximumWindowMetrics()
298                 .getWindowInsets()
299                 .getInsets(WindowInsets.Type.navigationBars()
300                         | WindowInsets.Type.statusBars()
301                         | WindowInsets.Type.displayCutout()).toRect();
302     }
303 
isLandscape(Rect bounds)304     private static boolean isLandscape(Rect bounds) {
305         return bounds.width() > bounds.height();
306     }
307 
308     /** Apply recorded surface layout to the {@link SurfaceControl.Transaction}. */
applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2)309     public void applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1,
310             SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
311         final Rect dividerBounds = mImePositionProcessor.adjustForIme(mDividerBounds);
312         final Rect bounds1 = mImePositionProcessor.adjustForIme(mBounds1);
313         final Rect bounds2 = mImePositionProcessor.adjustForIme(mBounds2);
314         final SurfaceControl dividerLeash = getDividerLeash();
315         if (dividerLeash != null) {
316             t.setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)
317                     // Resets layer of divider bar to make sure it is always on top.
318                     .setLayer(dividerLeash, Integer.MAX_VALUE);
319         }
320 
321         t.setPosition(leash1, bounds1.left, bounds1.top)
322                 .setWindowCrop(leash1, bounds1.width(), bounds1.height());
323 
324         t.setPosition(leash2, bounds2.left, bounds2.top)
325                 .setWindowCrop(leash2, bounds2.width(), bounds2.height());
326 
327         mImePositionProcessor.applySurfaceDimValues(t, dimLayer1, dimLayer2);
328     }
329 
330     /** Apply recorded task layout to the {@link WindowContainerTransaction}. */
applyTaskChanges(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2)331     public void applyTaskChanges(WindowContainerTransaction wct,
332             ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) {
333         wct.setBounds(task1.token, mImePositionProcessor.adjustForIme(mBounds1))
334                 .setBounds(task2.token, mImePositionProcessor.adjustForIme(mBounds2));
335     }
336 
337     /** Handles layout change event. */
338     public interface SplitLayoutHandler {
339 
340         /** Calls when dismissing split. */
onSnappedToDismiss(boolean snappedToEnd)341         void onSnappedToDismiss(boolean snappedToEnd);
342 
343         /** Calls when the bounds is changing due to animation or dragging divider bar. */
onBoundsChanging(SplitLayout layout)344         void onBoundsChanging(SplitLayout layout);
345 
346         /** Calls when the target bounds changed. */
onBoundsChanged(SplitLayout layout)347         void onBoundsChanged(SplitLayout layout);
348 
349         /** Calls when user double tapped on the divider bar. */
onDoubleTappedDivider()350         default void onDoubleTappedDivider() {
351         }
352 
353         /** Returns split position of the token. */
354         @SplitPosition
getSplitItemPosition(WindowContainerToken token)355         int getSplitItemPosition(WindowContainerToken token);
356     }
357 
358     /** Records IME top offset changes and updates SplitLayout correspondingly. */
359     private class ImePositionProcessor implements DisplayImeController.ImePositionProcessor {
360         /**
361          * Maximum size of an adjusted split bounds relative to original stack bounds. Used to
362          * restrict IME adjustment so that a min portion of top split remains visible.
363          */
364         private static final float ADJUSTED_SPLIT_FRACTION_MAX = 0.7f;
365         private static final float ADJUSTED_NONFOCUS_DIM = 0.3f;
366 
367         private final int mDisplayId;
368 
369         private boolean mImeShown;
370         private int mYOffsetForIme;
371         private float mDimValue1;
372         private float mDimValue2;
373 
374         private int mStartImeTop;
375         private int mEndImeTop;
376 
377         private int mTargetYOffset;
378         private int mLastYOffset;
379         private float mTargetDim1;
380         private float mTargetDim2;
381         private float mLastDim1;
382         private float mLastDim2;
383 
ImePositionProcessor(int displayId)384         private ImePositionProcessor(int displayId) {
385             mDisplayId = displayId;
386         }
387 
388         @Override
onImeStartPositioning(int displayId, int hiddenTop, int shownTop, boolean showing, boolean isFloating, SurfaceControl.Transaction t)389         public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop,
390                 boolean showing, boolean isFloating, SurfaceControl.Transaction t) {
391             if (displayId != mDisplayId) return 0;
392             final int imeTargetPosition = getImeTargetPosition();
393             if (!mInitialized || imeTargetPosition == SPLIT_POSITION_UNDEFINED) return 0;
394             mStartImeTop = showing ? hiddenTop : shownTop;
395             mEndImeTop = showing ? shownTop : hiddenTop;
396             mImeShown = showing;
397 
398             // Update target dim values
399             mLastDim1 = mDimValue1;
400             mTargetDim1 = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT && showing
401                     ? ADJUSTED_NONFOCUS_DIM : 0.0f;
402             mLastDim2 = mDimValue2;
403             mTargetDim2 = imeTargetPosition == SPLIT_POSITION_TOP_OR_LEFT && showing
404                     ? ADJUSTED_NONFOCUS_DIM : 0.0f;
405 
406             // Calculate target bounds offset for IME
407             mLastYOffset = mYOffsetForIme;
408             final boolean needOffset = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
409                     && !isFloating && !isLandscape(mRootBounds) && showing;
410             mTargetYOffset = needOffset ? getTargetYOffset() : 0;
411 
412             // Make {@link DividerView} non-interactive while IME showing in split mode. Listen to
413             // ImePositionProcessor#onImeVisibilityChanged directly in DividerView is not enough
414             // because DividerView won't receive onImeVisibilityChanged callback after it being
415             // re-inflated.
416             mSplitWindowManager.setInteractive(
417                     !showing || imeTargetPosition == SPLIT_POSITION_UNDEFINED);
418 
419             return needOffset ? IME_ANIMATION_NO_ALPHA : 0;
420         }
421 
422         @Override
onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t)423         public void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) {
424             if (displayId != mDisplayId) return;
425             onProgress(getProgress(imeTop));
426             mSplitLayoutHandler.onBoundsChanging(SplitLayout.this);
427         }
428 
429         @Override
onImeEndPositioning(int displayId, boolean cancel, SurfaceControl.Transaction t)430         public void onImeEndPositioning(int displayId, boolean cancel,
431                 SurfaceControl.Transaction t) {
432             if (displayId != mDisplayId || cancel) return;
433             onProgress(1.0f);
434             mSplitLayoutHandler.onBoundsChanging(SplitLayout.this);
435         }
436 
437         @Override
onImeControlTargetChanged(int displayId, boolean controlling)438         public void onImeControlTargetChanged(int displayId, boolean controlling) {
439             if (displayId != mDisplayId) return;
440             // Restore the split layout when wm-shell is not controlling IME insets anymore.
441             if (!controlling && mImeShown) {
442                 reset();
443                 mSplitWindowManager.setInteractive(true);
444                 mSplitLayoutHandler.onBoundsChanging(SplitLayout.this);
445             }
446         }
447 
getTargetYOffset()448         private int getTargetYOffset() {
449             final int desireOffset = Math.abs(mEndImeTop - mStartImeTop);
450             // Make sure to keep at least 30% visible for the top split.
451             final int maxOffset = (int) (mBounds1.height() * ADJUSTED_SPLIT_FRACTION_MAX);
452             return -Math.min(desireOffset, maxOffset);
453         }
454 
455         @SplitPosition
getImeTargetPosition()456         private int getImeTargetPosition() {
457             final WindowContainerToken token = mTaskOrganizer.getImeTarget(mDisplayId);
458             return mSplitLayoutHandler.getSplitItemPosition(token);
459         }
460 
getProgress(int currImeTop)461         private float getProgress(int currImeTop) {
462             return ((float) currImeTop - mStartImeTop) / (mEndImeTop - mStartImeTop);
463         }
464 
onProgress(float progress)465         private void onProgress(float progress) {
466             mDimValue1 = getProgressValue(mLastDim1, mTargetDim1, progress);
467             mDimValue2 = getProgressValue(mLastDim2, mTargetDim2, progress);
468             mYOffsetForIme =
469                     (int) getProgressValue((float) mLastYOffset, (float) mTargetYOffset, progress);
470         }
471 
getProgressValue(float start, float end, float progress)472         private float getProgressValue(float start, float end, float progress) {
473             return start + (end - start) * progress;
474         }
475 
reset()476         private void reset() {
477             mImeShown = false;
478             mYOffsetForIme = mLastYOffset = mTargetYOffset = 0;
479             mDimValue1 = mLastDim1 = mTargetDim1 = 0.0f;
480             mDimValue2 = mLastDim2 = mTargetDim2 = 0.0f;
481         }
482 
483         /* Adjust bounds with IME offset. */
adjustForIme(Rect bounds)484         private Rect adjustForIme(Rect bounds) {
485             final Rect temp = new Rect(bounds);
486             if (mYOffsetForIme != 0) temp.offset(0, mYOffsetForIme);
487             return temp;
488         }
489 
applySurfaceDimValues(SurfaceControl.Transaction t, SurfaceControl dimLayer1, SurfaceControl dimLayer2)490         private void applySurfaceDimValues(SurfaceControl.Transaction t, SurfaceControl dimLayer1,
491                 SurfaceControl dimLayer2) {
492             t.setAlpha(dimLayer1, mDimValue1).setVisibility(dimLayer1, mDimValue1 > 0.001f);
493             t.setAlpha(dimLayer2, mDimValue2).setVisibility(dimLayer2, mDimValue2 > 0.001f);
494         }
495     }
496 }
497