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.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
20 import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED;
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 static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER;
28 import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE;
29 import static com.android.wm.shell.animation.Interpolators.DIM_INTERPOLATOR;
30 import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR;
31 import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS;
32 import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_START_AND_DISMISS;
33 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
34 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
35 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
36 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
37 
38 import android.animation.Animator;
39 import android.animation.AnimatorListenerAdapter;
40 import android.animation.AnimatorSet;
41 import android.animation.ValueAnimator;
42 import android.annotation.NonNull;
43 import android.app.ActivityManager;
44 import android.content.Context;
45 import android.content.res.Configuration;
46 import android.content.res.Resources;
47 import android.graphics.Point;
48 import android.graphics.Rect;
49 import android.view.Display;
50 import android.view.InsetsSourceControl;
51 import android.view.InsetsState;
52 import android.view.RoundedCorner;
53 import android.view.SurfaceControl;
54 import android.view.WindowInsets;
55 import android.view.WindowManager;
56 import android.window.WindowContainerToken;
57 import android.window.WindowContainerTransaction;
58 
59 import androidx.annotation.Nullable;
60 
61 import com.android.internal.annotations.VisibleForTesting;
62 import com.android.internal.protolog.common.ProtoLog;
63 import com.android.wm.shell.R;
64 import com.android.wm.shell.ShellTaskOrganizer;
65 import com.android.wm.shell.animation.Interpolators;
66 import com.android.wm.shell.common.DisplayController;
67 import com.android.wm.shell.common.DisplayImeController;
68 import com.android.wm.shell.common.DisplayInsetsController;
69 import com.android.wm.shell.common.DisplayLayout;
70 import com.android.wm.shell.common.InteractionJankMonitorUtils;
71 import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
72 import com.android.wm.shell.common.split.SplitScreenConstants.SnapPosition;
73 import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
74 import com.android.wm.shell.protolog.ShellProtoLogGroup;
75 
76 import java.io.PrintWriter;
77 import java.util.function.Consumer;
78 
79 /**
80  * Records and handles layout of splits. Helps to calculate proper bounds when configuration or
81  * divider position changes.
82  */
83 public final class SplitLayout implements DisplayInsetsController.OnInsetsChangedListener {
84     private static final String TAG = "SplitLayout";
85     public static final int PARALLAX_NONE = 0;
86     public static final int PARALLAX_DISMISSING = 1;
87     public static final int PARALLAX_ALIGN_CENTER = 2;
88 
89     public static final int FLING_RESIZE_DURATION = 250;
90     private static final int FLING_SWITCH_DURATION = 350;
91     private static final int FLING_ENTER_DURATION = 450;
92     private static final int FLING_EXIT_DURATION = 450;
93 
94     private int mDividerWindowWidth;
95     private int mDividerInsets;
96     private int mDividerSize;
97 
98     private final Rect mTempRect = new Rect();
99     private final Rect mRootBounds = new Rect();
100     private final Rect mDividerBounds = new Rect();
101     // Bounds1 final position should be always at top or left
102     private final Rect mBounds1 = new Rect();
103     // Bounds2 final position should be always at bottom or right
104     private final Rect mBounds2 = new Rect();
105     // The temp bounds outside of display bounds for side stage when split screen inactive to avoid
106     // flicker next time active split screen.
107     private final Rect mInvisibleBounds = new Rect();
108     private final Rect mWinBounds1 = new Rect();
109     private final Rect mWinBounds2 = new Rect();
110     private final SplitLayoutHandler mSplitLayoutHandler;
111     private final SplitWindowManager mSplitWindowManager;
112     private final DisplayController mDisplayController;
113     private final DisplayImeController mDisplayImeController;
114     private final ImePositionProcessor mImePositionProcessor;
115     private final ResizingEffectPolicy mSurfaceEffectPolicy;
116     private final ShellTaskOrganizer mTaskOrganizer;
117     private final InsetsState mInsetsState = new InsetsState();
118 
119     private Context mContext;
120     @VisibleForTesting DividerSnapAlgorithm mDividerSnapAlgorithm;
121     private WindowContainerToken mWinToken1;
122     private WindowContainerToken mWinToken2;
123     private int mDividerPosition;
124     private boolean mInitialized = false;
125     private boolean mFreezeDividerWindow = false;
126     private boolean mIsLargeScreen = false;
127     private int mOrientation;
128     private int mRotation;
129     private int mDensity;
130     private int mUiMode;
131 
132     private final boolean mDimNonImeSide;
133     private final boolean mAllowLeftRightSplitInPortrait;
134     private boolean mIsLeftRightSplit;
135     private ValueAnimator mDividerFlingAnimator;
136 
SplitLayout(String windowName, Context context, Configuration configuration, SplitLayoutHandler splitLayoutHandler, SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks, DisplayController displayController, DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer, int parallaxType)137     public SplitLayout(String windowName, Context context, Configuration configuration,
138             SplitLayoutHandler splitLayoutHandler,
139             SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks,
140             DisplayController displayController, DisplayImeController displayImeController,
141             ShellTaskOrganizer taskOrganizer, int parallaxType) {
142         mContext = context.createConfigurationContext(configuration);
143         mOrientation = configuration.orientation;
144         mRotation = configuration.windowConfiguration.getRotation();
145         mDensity = configuration.densityDpi;
146         mIsLargeScreen = configuration.smallestScreenWidthDp >= 600;
147         mSplitLayoutHandler = splitLayoutHandler;
148         mDisplayController = displayController;
149         mDisplayImeController = displayImeController;
150         mSplitWindowManager = new SplitWindowManager(windowName, mContext, configuration,
151                 parentContainerCallbacks);
152         mTaskOrganizer = taskOrganizer;
153         mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId());
154         mSurfaceEffectPolicy = new ResizingEffectPolicy(parallaxType);
155 
156         final Resources res = mContext.getResources();
157         mDimNonImeSide = res.getBoolean(R.bool.config_dimNonImeAttachedSide);
158         mAllowLeftRightSplitInPortrait = SplitScreenUtils.allowLeftRightSplitInPortrait(res);
159         mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait,
160                 configuration);
161 
162         updateDividerConfig(mContext);
163 
164         mRootBounds.set(configuration.windowConfiguration.getBounds());
165         mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
166         resetDividerPosition();
167         updateInvisibleRect();
168     }
169 
updateDividerConfig(Context context)170     private void updateDividerConfig(Context context) {
171         final Resources resources = context.getResources();
172         final Display display = context.getDisplay();
173         final int dividerInset = resources.getDimensionPixelSize(
174                 com.android.internal.R.dimen.docked_stack_divider_insets);
175         int radius = 0;
176         RoundedCorner corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT);
177         radius = corner != null ? Math.max(radius, corner.getRadius()) : radius;
178         corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT);
179         radius = corner != null ? Math.max(radius, corner.getRadius()) : radius;
180         corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT);
181         radius = corner != null ? Math.max(radius, corner.getRadius()) : radius;
182         corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT);
183         radius = corner != null ? Math.max(radius, corner.getRadius()) : radius;
184 
185         mDividerInsets = Math.max(dividerInset, radius);
186         mDividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width);
187         mDividerWindowWidth = mDividerSize + 2 * mDividerInsets;
188     }
189 
190     /** Gets bounds of the primary split with screen based coordinate. */
getBounds1()191     public Rect getBounds1() {
192         return new Rect(mBounds1);
193     }
194 
195     /** Gets bounds of the primary split with parent based coordinate. */
getRefBounds1()196     public Rect getRefBounds1() {
197         Rect outBounds = getBounds1();
198         outBounds.offset(-mRootBounds.left, -mRootBounds.top);
199         return outBounds;
200     }
201 
202     /** Gets bounds of the secondary split with screen based coordinate. */
getBounds2()203     public Rect getBounds2() {
204         return new Rect(mBounds2);
205     }
206 
207     /** Gets bounds of the secondary split with parent based coordinate. */
getRefBounds2()208     public Rect getRefBounds2() {
209         final Rect outBounds = getBounds2();
210         outBounds.offset(-mRootBounds.left, -mRootBounds.top);
211         return outBounds;
212     }
213 
214     /** Gets root bounds of the whole split layout */
getRootBounds()215     public Rect getRootBounds() {
216         return new Rect(mRootBounds);
217     }
218 
219     /** Gets bounds of divider window with screen based coordinate. */
getDividerBounds()220     public Rect getDividerBounds() {
221         return new Rect(mDividerBounds);
222     }
223 
224     /** Gets bounds of divider window with parent based coordinate. */
getRefDividerBounds()225     public Rect getRefDividerBounds() {
226         final Rect outBounds = getDividerBounds();
227         outBounds.offset(-mRootBounds.left, -mRootBounds.top);
228         return outBounds;
229     }
230 
231     /** Gets bounds of the primary split with screen based coordinate on the param Rect. */
getBounds1(Rect rect)232     public void getBounds1(Rect rect) {
233         rect.set(mBounds1);
234     }
235 
236     /** Gets bounds of the primary split with parent based coordinate on the param Rect. */
getRefBounds1(Rect rect)237     public void getRefBounds1(Rect rect) {
238         getBounds1(rect);
239         rect.offset(-mRootBounds.left, -mRootBounds.top);
240     }
241 
242     /** Gets bounds of the secondary split with screen based coordinate on the param Rect. */
getBounds2(Rect rect)243     public void getBounds2(Rect rect) {
244         rect.set(mBounds2);
245     }
246 
247     /** Gets bounds of the secondary split with parent based coordinate on the param Rect. */
getRefBounds2(Rect rect)248     public void getRefBounds2(Rect rect) {
249         getBounds2(rect);
250         rect.offset(-mRootBounds.left, -mRootBounds.top);
251     }
252 
253     /** Gets root bounds of the whole split layout on the param Rect. */
getRootBounds(Rect rect)254     public void getRootBounds(Rect rect) {
255         rect.set(mRootBounds);
256     }
257 
258     /** Gets bounds of divider window with screen based coordinate on the param Rect. */
getDividerBounds(Rect rect)259     public void getDividerBounds(Rect rect) {
260         rect.set(mDividerBounds);
261     }
262 
263     /** Gets bounds of divider window with parent based coordinate on the param Rect. */
getRefDividerBounds(Rect rect)264     public void getRefDividerBounds(Rect rect) {
265         getDividerBounds(rect);
266         rect.offset(-mRootBounds.left, -mRootBounds.top);
267     }
268 
269     /** Gets bounds size equal to root bounds but outside of screen, used for position side stage
270      * when split inactive to avoid flicker when next time active. */
getInvisibleBounds(Rect rect)271     public void getInvisibleBounds(Rect rect) {
272         rect.set(mInvisibleBounds);
273     }
274 
275     /** Returns leash of the current divider bar. */
276     @Nullable
getDividerLeash()277     public SurfaceControl getDividerLeash() {
278         return mSplitWindowManager == null ? null : mSplitWindowManager.getSurfaceControl();
279     }
280 
getDividerPosition()281     int getDividerPosition() {
282         return mDividerPosition;
283     }
284 
285     /**
286      * Finds the {@link SnapPosition} nearest to the current divider position.
287      */
calculateCurrentSnapPosition()288     public int calculateCurrentSnapPosition() {
289         return mDividerSnapAlgorithm.calculateNearestSnapPosition(mDividerPosition);
290     }
291 
292     /**
293      * Returns the divider position as a fraction from 0 to 1.
294      */
getDividerPositionAsFraction()295     public float getDividerPositionAsFraction() {
296         return Math.min(1f, Math.max(0f, mIsLeftRightSplit
297                 ? (float) ((mBounds1.right + mBounds2.left) / 2f) / mBounds2.right
298                 : (float) ((mBounds1.bottom + mBounds2.top) / 2f) / mBounds2.bottom));
299     }
300 
updateInvisibleRect()301     private void updateInvisibleRect() {
302         mInvisibleBounds.set(mRootBounds.left, mRootBounds.top,
303                 mIsLeftRightSplit ? mRootBounds.right / 2 : mRootBounds.right,
304                 mIsLeftRightSplit ? mRootBounds.bottom : mRootBounds.bottom / 2);
305         mInvisibleBounds.offset(mIsLeftRightSplit ? mRootBounds.right : 0,
306                 mIsLeftRightSplit ? 0 : mRootBounds.bottom);
307     }
308 
309     /** Applies new configuration, returns {@code false} if there's no effect to the layout. */
updateConfiguration(Configuration configuration)310     public boolean updateConfiguration(Configuration configuration) {
311         // Update the split bounds when necessary. Besides root bounds changed, split bounds need to
312         // be updated when the rotation changed to cover the case that users rotated the screen 180
313         // degrees.
314         // Make sure to render the divider bar with proper resources that matching the screen
315         // orientation.
316         final int rotation = configuration.windowConfiguration.getRotation();
317         final Rect rootBounds = configuration.windowConfiguration.getBounds();
318         final int orientation = configuration.orientation;
319         final int density = configuration.densityDpi;
320         final int uiMode = configuration.uiMode;
321         final boolean wasLeftRightSplit = mIsLeftRightSplit;
322 
323         if (mOrientation == orientation
324                 && mRotation == rotation
325                 && mDensity == density
326                 && mUiMode == uiMode
327                 && mRootBounds.equals(rootBounds)) {
328             return false;
329         }
330 
331         mContext = mContext.createConfigurationContext(configuration);
332         mSplitWindowManager.setConfiguration(configuration);
333         mOrientation = orientation;
334         mTempRect.set(mRootBounds);
335         mRootBounds.set(rootBounds);
336         mRotation = rotation;
337         mDensity = density;
338         mUiMode = uiMode;
339         mIsLargeScreen = configuration.smallestScreenWidthDp >= 600;
340         mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait,
341                 configuration);
342         mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
343         updateDividerConfig(mContext);
344         initDividerPosition(mTempRect, wasLeftRightSplit);
345         updateInvisibleRect();
346 
347         return true;
348     }
349 
350     /** Rotate the layout to specific rotation and calculate new bounds. The stable insets value
351      *  should be calculated by display layout. */
rotateTo(int newRotation)352     public void rotateTo(int newRotation) {
353         final int rotationDelta = (newRotation - mRotation + 4) % 4;
354         final boolean changeOrient = (rotationDelta % 2) != 0;
355 
356         mRotation = newRotation;
357         Rect tmpRect = new Rect(mRootBounds);
358         if (changeOrient) {
359             tmpRect.set(mRootBounds.top, mRootBounds.left, mRootBounds.bottom, mRootBounds.right);
360         }
361 
362         // We only need new bounds here, other configuration should be update later.
363         final boolean wasLeftRightSplit = SplitScreenUtils.isLeftRightSplit(
364                 mAllowLeftRightSplitInPortrait, mIsLargeScreen,
365                 mRootBounds.width() >= mRootBounds.height());
366         mTempRect.set(mRootBounds);
367         mRootBounds.set(tmpRect);
368         mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait,
369                 mIsLargeScreen, mRootBounds.width() >= mRootBounds.height());
370         mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
371         initDividerPosition(mTempRect, wasLeftRightSplit);
372     }
373 
374     /**
375      * Updates the divider position to the position in the current orientation and bounds using the
376      * snap fraction calculated based on the previous orientation and bounds.
377      */
initDividerPosition(Rect oldBounds, boolean wasLeftRightSplit)378     private void initDividerPosition(Rect oldBounds, boolean wasLeftRightSplit) {
379         final float snapRatio = (float) mDividerPosition
380                 / (float) (wasLeftRightSplit ? oldBounds.width() : oldBounds.height());
381         // Estimate position by previous ratio.
382         final float length =
383                 (float) (mIsLeftRightSplit ? mRootBounds.width() : mRootBounds.height());
384         final int estimatePosition = (int) (length * snapRatio);
385         // Init divider position by estimated position using current bounds snap algorithm.
386         mDividerPosition = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget(
387                 estimatePosition).position;
388         updateBounds(mDividerPosition);
389     }
390 
updateBounds(int position)391     private void updateBounds(int position) {
392         updateBounds(position, mBounds1, mBounds2, mDividerBounds, true /* setEffectBounds */);
393     }
394 
395     /** Updates recording bounds of divider window and both of the splits. */
updateBounds(int position, Rect bounds1, Rect bounds2, Rect dividerBounds, boolean setEffectBounds)396     private void updateBounds(int position, Rect bounds1, Rect bounds2, Rect dividerBounds,
397             boolean setEffectBounds) {
398         dividerBounds.set(mRootBounds);
399         bounds1.set(mRootBounds);
400         bounds2.set(mRootBounds);
401         if (mIsLeftRightSplit) {
402             position += mRootBounds.left;
403             dividerBounds.left = position - mDividerInsets;
404             dividerBounds.right = dividerBounds.left + mDividerWindowWidth;
405             bounds1.right = position;
406             bounds2.left = bounds1.right + mDividerSize;
407         } else {
408             position += mRootBounds.top;
409             dividerBounds.top = position - mDividerInsets;
410             dividerBounds.bottom = dividerBounds.top + mDividerWindowWidth;
411             bounds1.bottom = position;
412             bounds2.top = bounds1.bottom + mDividerSize;
413         }
414         DockedDividerUtils.sanitizeStackBounds(bounds1, true /** topLeft */);
415         DockedDividerUtils.sanitizeStackBounds(bounds2, false /** topLeft */);
416         if (setEffectBounds) {
417             mSurfaceEffectPolicy.applyDividerPosition(position, mIsLeftRightSplit);
418         }
419     }
420 
421     /** Inflates {@link DividerView} on the root surface. */
init()422     public void init() {
423         if (mInitialized) return;
424         mInitialized = true;
425         mSplitWindowManager.init(this, mInsetsState, false /* isRestoring */);
426         mDisplayImeController.addPositionProcessor(mImePositionProcessor);
427     }
428 
429     /** Releases the surface holding the current {@link DividerView}. */
release(SurfaceControl.Transaction t)430     public void release(SurfaceControl.Transaction t) {
431         if (!mInitialized) return;
432         mInitialized = false;
433         mSplitWindowManager.release(t);
434         mDisplayImeController.removePositionProcessor(mImePositionProcessor);
435         mImePositionProcessor.reset();
436         if (mDividerFlingAnimator != null) {
437             mDividerFlingAnimator.cancel();
438         }
439         resetDividerPosition();
440     }
441 
release()442     public void release() {
443         release(null /* t */);
444     }
445 
446     /** Releases and re-inflates {@link DividerView} on the root surface. */
update(SurfaceControl.Transaction t, boolean resetImePosition)447     public void update(SurfaceControl.Transaction t, boolean resetImePosition) {
448         if (!mInitialized) {
449             init();
450             return;
451         }
452         mSplitWindowManager.release(t);
453         if (resetImePosition) {
454             mImePositionProcessor.reset();
455         }
456         mSplitWindowManager.init(this, mInsetsState, true /* isRestoring */);
457         // Update the surface positions again after recreating the divider in case nothing else
458         // triggers it
459         mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
460     }
461 
462     @Override
insetsChanged(InsetsState insetsState)463     public void insetsChanged(InsetsState insetsState) {
464         mInsetsState.set(insetsState);
465         if (!mInitialized) {
466             return;
467         }
468         if (mFreezeDividerWindow) {
469             // DO NOT change its layout before transition actually run because it might cause
470             // flicker.
471             return;
472         }
473         mSplitWindowManager.onInsetsChanged(insetsState);
474     }
475 
476     @Override
insetsControlChanged(InsetsState insetsState, InsetsSourceControl[] activeControls)477     public void insetsControlChanged(InsetsState insetsState,
478             InsetsSourceControl[] activeControls) {
479         if (!mInsetsState.equals(insetsState)) {
480             insetsChanged(insetsState);
481         }
482     }
483 
setFreezeDividerWindow(boolean freezeDividerWindow)484     public void setFreezeDividerWindow(boolean freezeDividerWindow) {
485         mFreezeDividerWindow = freezeDividerWindow;
486     }
487 
488     /** Update current layout as divider put on start or end position. */
setDividerAtBorder(boolean start)489     public void setDividerAtBorder(boolean start) {
490         final int pos = start ? mDividerSnapAlgorithm.getDismissStartTarget().position
491                 : mDividerSnapAlgorithm.getDismissEndTarget().position;
492         setDividerPosition(pos, false /* applyLayoutChange */);
493     }
494 
495     /**
496      * Updates bounds with the passing position. Usually used to update recording bounds while
497      * performing animation or dragging divider bar to resize the splits.
498      */
updateDividerBounds(int position, boolean shouldUseParallaxEffect)499     void updateDividerBounds(int position, boolean shouldUseParallaxEffect) {
500         updateBounds(position);
501         mSplitLayoutHandler.onLayoutSizeChanging(this, mSurfaceEffectPolicy.mParallaxOffset.x,
502                 mSurfaceEffectPolicy.mParallaxOffset.y, shouldUseParallaxEffect);
503     }
504 
setDividerPosition(int position, boolean applyLayoutChange)505     void setDividerPosition(int position, boolean applyLayoutChange) {
506         mDividerPosition = position;
507         updateBounds(mDividerPosition);
508         if (applyLayoutChange) {
509             mSplitLayoutHandler.onLayoutSizeChanged(this);
510         }
511     }
512 
513     /**
514      * Updates divider position and split bounds base on the ratio within root bounds. Falls back
515      * to middle position if the provided SnapTarget is not supported.
516      */
setDivideRatio(@ersistentSnapPosition int snapPosition)517     public void setDivideRatio(@PersistentSnapPosition int snapPosition) {
518         final DividerSnapAlgorithm.SnapTarget snapTarget = mDividerSnapAlgorithm.findSnapTarget(
519                 snapPosition);
520 
521         setDividerPosition(snapTarget != null
522                 ? snapTarget.position
523                 : mDividerSnapAlgorithm.getMiddleTarget().position,
524                 false /* applyLayoutChange */);
525     }
526 
527     /** Resets divider position. */
resetDividerPosition()528     public void resetDividerPosition() {
529         mDividerPosition = mDividerSnapAlgorithm.getMiddleTarget().position;
530         updateBounds(mDividerPosition);
531         mWinToken1 = null;
532         mWinToken2 = null;
533         mWinBounds1.setEmpty();
534         mWinBounds2.setEmpty();
535     }
536 
537     /**
538      * Set divider should interactive to user or not.
539      *
540      * @param interactive divider interactive.
541      * @param hideHandle divider handle hidden or not, only work when interactive is false.
542      * @param from caller from where.
543      */
setDividerInteractive(boolean interactive, boolean hideHandle, String from)544     public void setDividerInteractive(boolean interactive, boolean hideHandle, String from) {
545         mSplitWindowManager.setInteractive(interactive, hideHandle, from);
546     }
547 
548     /**
549      * Sets new divider position and updates bounds correspondingly. Notifies listener if the new
550      * target indicates dismissing split.
551      */
snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget)552     public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) {
553         switch (snapTarget.snapPosition) {
554             case SNAP_TO_START_AND_DISMISS:
555                 flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
556                         () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */,
557                                 EXIT_REASON_DRAG_DIVIDER));
558                 break;
559             case SNAP_TO_END_AND_DISMISS:
560                 flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
561                         () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */,
562                                 EXIT_REASON_DRAG_DIVIDER));
563                 break;
564             default:
565                 flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
566                         () -> setDividerPosition(snapTarget.position, true /* applyLayoutChange */));
567                 break;
568         }
569     }
570 
onStartDragging()571     void onStartDragging() {
572         InteractionJankMonitorUtils.beginTracing(CUJ_SPLIT_SCREEN_RESIZE, mContext,
573                 getDividerLeash(), null /* tag */);
574     }
575 
onDraggingCancelled()576     void onDraggingCancelled() {
577         InteractionJankMonitorUtils.cancelTracing(CUJ_SPLIT_SCREEN_RESIZE);
578     }
579 
onDoubleTappedDivider()580     void onDoubleTappedDivider() {
581         mSplitLayoutHandler.onDoubleTappedDivider();
582     }
583 
584     /**
585      * Returns {@link DividerSnapAlgorithm.SnapTarget} which matches passing position and velocity.
586      * If hardDismiss is set to {@code true}, it will be harder to reach dismiss target.
587      */
findSnapTarget(int position, float velocity, boolean hardDismiss)588     public DividerSnapAlgorithm.SnapTarget findSnapTarget(int position, float velocity,
589             boolean hardDismiss) {
590         return mDividerSnapAlgorithm.calculateSnapTarget(position, velocity, hardDismiss);
591     }
592 
getSnapAlgorithm(Context context, Rect rootBounds)593     private DividerSnapAlgorithm getSnapAlgorithm(Context context, Rect rootBounds) {
594         final Rect insets = getDisplayStableInsets(context);
595 
596         // Make split axis insets value same as the larger one to avoid bounds1 and bounds2
597         // have difference for avoiding size-compat mode when switching unresizable apps in
598         // landscape while they are letterboxed.
599         if (!mIsLeftRightSplit) {
600             final int largerInsets = Math.max(insets.top, insets.bottom);
601             insets.set(insets.left, largerInsets, insets.right, largerInsets);
602         }
603 
604         return new DividerSnapAlgorithm(
605                 context.getResources(),
606                 rootBounds.width(),
607                 rootBounds.height(),
608                 mDividerSize,
609                 !mIsLeftRightSplit,
610                 insets,
611                 mIsLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP /* dockSide */);
612     }
613 
614     /** Fling divider from current position to end or start position then exit */
flingDividerToDismiss(boolean toEnd, int reason)615     public void flingDividerToDismiss(boolean toEnd, int reason) {
616         final int target = toEnd ? mDividerSnapAlgorithm.getDismissEndTarget().position
617                 : mDividerSnapAlgorithm.getDismissStartTarget().position;
618         flingDividerPosition(getDividerPosition(), target, FLING_EXIT_DURATION,
619                 () -> mSplitLayoutHandler.onSnappedToDismiss(toEnd, reason));
620     }
621 
622     /** Fling divider from current position to center position. */
flingDividerToCenter(@ullable Runnable finishCallback)623     public void flingDividerToCenter(@Nullable Runnable finishCallback) {
624         final int pos = mDividerSnapAlgorithm.getMiddleTarget().position;
625         flingDividerPosition(getDividerPosition(), pos, FLING_ENTER_DURATION,
626                 () -> {
627                     setDividerPosition(pos, true /* applyLayoutChange */);
628                     if (finishCallback != null) {
629                         finishCallback.run();
630                     }
631                 });
632     }
633 
634     @VisibleForTesting
flingDividerPosition(int from, int to, int duration, @Nullable Runnable flingFinishedCallback)635     void flingDividerPosition(int from, int to, int duration,
636             @Nullable Runnable flingFinishedCallback) {
637         if (from == to) {
638             if (flingFinishedCallback != null) {
639                 flingFinishedCallback.run();
640             }
641             InteractionJankMonitorUtils.endTracing(
642                     CUJ_SPLIT_SCREEN_RESIZE);
643             return;
644         }
645 
646         if (mDividerFlingAnimator != null) {
647             mDividerFlingAnimator.cancel();
648         }
649 
650         mDividerFlingAnimator = ValueAnimator
651                 .ofInt(from, to)
652                 .setDuration(duration);
653         mDividerFlingAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
654         mDividerFlingAnimator.addUpdateListener(
655                 animation -> updateDividerBounds(
656                         (int) animation.getAnimatedValue(), false /* shouldUseParallaxEffect */)
657         );
658         mDividerFlingAnimator.addListener(new AnimatorListenerAdapter() {
659             @Override
660             public void onAnimationEnd(Animator animation) {
661                 if (flingFinishedCallback != null) {
662                     flingFinishedCallback.run();
663                 }
664                 InteractionJankMonitorUtils.endTracing(
665                         CUJ_SPLIT_SCREEN_RESIZE);
666                 mDividerFlingAnimator = null;
667             }
668 
669             @Override
670             public void onAnimationCancel(Animator animation) {
671                 mDividerFlingAnimator = null;
672             }
673         });
674         mDividerFlingAnimator.start();
675     }
676 
677     /** Switch both surface position with animation. */
splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, Consumer<Rect> finishCallback)678     public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1,
679             SurfaceControl leash2, Consumer<Rect> finishCallback) {
680         final Rect insets = getDisplayStableInsets(mContext);
681         insets.set(mIsLeftRightSplit ? insets.left : 0, mIsLeftRightSplit ? 0 : insets.top,
682                 mIsLeftRightSplit ? insets.right : 0, mIsLeftRightSplit ? 0 : insets.bottom);
683 
684         final int dividerPos = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget(
685                 mIsLeftRightSplit ? mBounds2.width() : mBounds2.height()).position;
686         final Rect distBounds1 = new Rect();
687         final Rect distBounds2 = new Rect();
688         final Rect distDividerBounds = new Rect();
689         // Compute dist bounds.
690         updateBounds(dividerPos, distBounds2, distBounds1, distDividerBounds,
691                 false /* setEffectBounds */);
692         // Offset to real position under root container.
693         distBounds1.offset(-mRootBounds.left, -mRootBounds.top);
694         distBounds2.offset(-mRootBounds.left, -mRootBounds.top);
695         distDividerBounds.offset(-mRootBounds.left, -mRootBounds.top);
696 
697         ValueAnimator animator1 = moveSurface(t, leash1, getRefBounds1(), distBounds1,
698                 -insets.left, -insets.top);
699         ValueAnimator animator2 = moveSurface(t, leash2, getRefBounds2(), distBounds2,
700                 insets.left, insets.top);
701         ValueAnimator animator3 = moveSurface(t, getDividerLeash(), getRefDividerBounds(),
702                 distDividerBounds, 0 /* offsetX */, 0 /* offsetY */);
703 
704         AnimatorSet set = new AnimatorSet();
705         set.playTogether(animator1, animator2, animator3);
706         set.setDuration(FLING_SWITCH_DURATION);
707         set.addListener(new AnimatorListenerAdapter() {
708             @Override
709             public void onAnimationStart(Animator animation) {
710                 InteractionJankMonitorUtils.beginTracing(CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER,
711                         mContext, getDividerLeash(), null /*tag*/);
712             }
713 
714             @Override
715             public void onAnimationEnd(Animator animation) {
716                 mDividerPosition = dividerPos;
717                 updateBounds(mDividerPosition);
718                 finishCallback.accept(insets);
719                 InteractionJankMonitorUtils.endTracing(CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER);
720             }
721 
722             @Override
723             public void onAnimationCancel(Animator animation) {
724                 InteractionJankMonitorUtils.cancelTracing(CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER);
725             }
726         });
727         set.start();
728     }
729 
moveSurface(SurfaceControl.Transaction t, SurfaceControl leash, Rect start, Rect end, float offsetX, float offsetY)730     private ValueAnimator moveSurface(SurfaceControl.Transaction t, SurfaceControl leash,
731             Rect start, Rect end, float offsetX, float offsetY) {
732         Rect tempStart = new Rect(start);
733         Rect tempEnd = new Rect(end);
734         final float diffX = tempEnd.left - tempStart.left;
735         final float diffY = tempEnd.top - tempStart.top;
736         final float diffWidth = tempEnd.width() - tempStart.width();
737         final float diffHeight = tempEnd.height() - tempStart.height();
738         ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
739         animator.addUpdateListener(animation -> {
740             if (leash == null) return;
741 
742             final float scale = (float) animation.getAnimatedValue();
743             final float distX = tempStart.left + scale * diffX;
744             final float distY = tempStart.top + scale * diffY;
745             final int width = (int) (tempStart.width() + scale * diffWidth);
746             final int height = (int) (tempStart.height() + scale * diffHeight);
747             if (offsetX == 0 && offsetY == 0) {
748                 t.setPosition(leash, distX, distY);
749                 t.setWindowCrop(leash, width, height);
750             } else {
751                 final int diffOffsetX = (int) (scale * offsetX);
752                 final int diffOffsetY = (int) (scale * offsetY);
753                 t.setPosition(leash, distX + diffOffsetX, distY + diffOffsetY);
754                 mTempRect.set(0, 0, width, height);
755                 mTempRect.offsetTo(-diffOffsetX, -diffOffsetY);
756                 t.setCrop(leash, mTempRect);
757             }
758             t.apply();
759         });
760         return animator;
761     }
762 
getDisplayStableInsets(Context context)763     private Rect getDisplayStableInsets(Context context) {
764         final DisplayLayout displayLayout =
765                 mDisplayController.getDisplayLayout(context.getDisplayId());
766         return displayLayout != null
767                 ? displayLayout.stableInsets()
768                 : context.getSystemService(WindowManager.class)
769                         .getMaximumWindowMetrics()
770                         .getWindowInsets()
771                         .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()
772                                 | WindowInsets.Type.displayCutout())
773                         .toRect();
774     }
775 
776     /**
777      * @return {@code true} if we should create a left-right split, {@code false} if we should
778      * create a top-bottom split.
779      */
isLeftRightSplit()780     public boolean isLeftRightSplit() {
781         return mIsLeftRightSplit;
782     }
783 
784     /** Apply recorded surface layout to the {@link SurfaceControl.Transaction}. */
applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2, boolean applyResizingOffset)785     public void applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1,
786             SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2,
787             boolean applyResizingOffset) {
788         final SurfaceControl dividerLeash = getDividerLeash();
789         if (dividerLeash != null) {
790             getRefDividerBounds(mTempRect);
791             t.setPosition(dividerLeash, mTempRect.left, mTempRect.top);
792             // Resets layer of divider bar to make sure it is always on top.
793             t.setLayer(dividerLeash, Integer.MAX_VALUE);
794         }
795         getRefBounds1(mTempRect);
796         t.setPosition(leash1, mTempRect.left, mTempRect.top)
797                 .setWindowCrop(leash1, mTempRect.width(), mTempRect.height());
798         getRefBounds2(mTempRect);
799         t.setPosition(leash2, mTempRect.left, mTempRect.top)
800                 .setWindowCrop(leash2, mTempRect.width(), mTempRect.height());
801 
802         if (mImePositionProcessor.adjustSurfaceLayoutForIme(
803                 t, dividerLeash, leash1, leash2, dimLayer1, dimLayer2)) {
804             return;
805         }
806 
807         mSurfaceEffectPolicy.adjustDimSurface(t, dimLayer1, dimLayer2);
808         if (applyResizingOffset) {
809             mSurfaceEffectPolicy.adjustRootSurface(t, leash1, leash2);
810         }
811     }
812 
813     /** Apply recorded task layout to the {@link WindowContainerTransaction}.
814      *
815      * @return true if stage bounds actually update.
816      */
applyTaskChanges(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2)817     public boolean applyTaskChanges(WindowContainerTransaction wct,
818             ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) {
819         boolean boundsChanged = false;
820         if (!mBounds1.equals(mWinBounds1) || !task1.token.equals(mWinToken1)) {
821             setTaskBounds(wct, task1, mBounds1);
822             mWinBounds1.set(mBounds1);
823             mWinToken1 = task1.token;
824             boundsChanged = true;
825         }
826         if (!mBounds2.equals(mWinBounds2) || !task2.token.equals(mWinToken2)) {
827             setTaskBounds(wct, task2, mBounds2);
828             mWinBounds2.set(mBounds2);
829             mWinToken2 = task2.token;
830             boundsChanged = true;
831         }
832         return boundsChanged;
833     }
834 
835     /** Set bounds to the {@link WindowContainerTransaction} for single task. */
setTaskBounds(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo task, Rect bounds)836     public void setTaskBounds(WindowContainerTransaction wct,
837             ActivityManager.RunningTaskInfo task, Rect bounds) {
838         wct.setBounds(task.token, bounds);
839         wct.setSmallestScreenWidthDp(task.token, getSmallestWidthDp(bounds));
840     }
841 
getSmallestWidthDp(Rect bounds)842     private int getSmallestWidthDp(Rect bounds) {
843         mTempRect.set(bounds);
844         mTempRect.inset(getDisplayStableInsets(mContext));
845         final int minWidth = Math.min(mTempRect.width(), mTempRect.height());
846         final float density = mContext.getResources().getDisplayMetrics().density;
847         return (int) (minWidth / density);
848     }
849 
850     /**
851      * Shift configuration bounds to prevent client apps get configuration changed or relaunch. And
852      * restore shifted configuration bounds if it's no longer shifted.
853      */
applyLayoutOffsetTarget(WindowContainerTransaction wct, int offsetX, int offsetY, ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2)854     public void applyLayoutOffsetTarget(WindowContainerTransaction wct, int offsetX, int offsetY,
855             ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2) {
856         if (offsetX == 0 && offsetY == 0) {
857             wct.setBounds(taskInfo1.token, mBounds1);
858             wct.setScreenSizeDp(taskInfo1.token,
859                     SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
860 
861             wct.setBounds(taskInfo2.token, mBounds2);
862             wct.setScreenSizeDp(taskInfo2.token,
863                     SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
864         } else {
865             getBounds1(mTempRect);
866             mTempRect.offset(offsetX, offsetY);
867             wct.setBounds(taskInfo1.token, mTempRect);
868             wct.setScreenSizeDp(taskInfo1.token,
869                     taskInfo1.configuration.screenWidthDp,
870                     taskInfo1.configuration.screenHeightDp);
871 
872             getBounds2(mTempRect);
873             mTempRect.offset(offsetX, offsetY);
874             wct.setBounds(taskInfo2.token, mTempRect);
875             wct.setScreenSizeDp(taskInfo2.token,
876                     taskInfo2.configuration.screenWidthDp,
877                     taskInfo2.configuration.screenHeightDp);
878         }
879     }
880 
881     /** Dumps the current split bounds recorded in this layout. */
dump(@onNull PrintWriter pw, String prefix)882     public void dump(@NonNull PrintWriter pw, String prefix) {
883         final String innerPrefix = prefix + "\t";
884         pw.println(prefix + TAG + ":");
885         pw.println(innerPrefix + "mAllowLeftRightSplitInPortrait=" + mAllowLeftRightSplitInPortrait);
886         pw.println(innerPrefix + "mIsLeftRightSplit=" + mIsLeftRightSplit);
887         pw.println(innerPrefix + "mFreezeDividerWindow=" + mFreezeDividerWindow);
888         pw.println(innerPrefix + "mDimNonImeSide=" + mDimNonImeSide);
889         pw.println(innerPrefix + "mDividerPosition=" + mDividerPosition);
890         pw.println(innerPrefix + "bounds1=" + mBounds1.toShortString());
891         pw.println(innerPrefix + "dividerBounds=" + mDividerBounds.toShortString());
892         pw.println(innerPrefix + "bounds2=" + mBounds2.toShortString());
893     }
894 
895     /** Handles layout change event. */
896     public interface SplitLayoutHandler {
897 
898         /** Calls when dismissing split. */
onSnappedToDismiss(boolean snappedToEnd, int reason)899         void onSnappedToDismiss(boolean snappedToEnd, int reason);
900 
901         /**
902          * Calls when resizing the split bounds.
903          *
904          * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl,
905          * SurfaceControl, SurfaceControl, boolean)
906          */
onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY, boolean shouldUseParallaxEffect)907         void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY,
908                 boolean shouldUseParallaxEffect);
909 
910         /**
911          * Calls when finish resizing the split bounds.
912          *
913          * @see #applyTaskChanges(WindowContainerTransaction, ActivityManager.RunningTaskInfo,
914          * ActivityManager.RunningTaskInfo)
915          * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl,
916          * SurfaceControl, SurfaceControl, boolean)
917          */
onLayoutSizeChanged(SplitLayout layout)918         void onLayoutSizeChanged(SplitLayout layout);
919 
920         /**
921          * Calls when re-positioning the split bounds. Like moving split bounds while showing IME
922          * panel.
923          *
924          * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl,
925          * SurfaceControl, SurfaceControl, boolean)
926          */
onLayoutPositionChanging(SplitLayout layout)927         void onLayoutPositionChanging(SplitLayout layout);
928 
929         /**
930          * Notifies the target offset for shifting layout. So layout handler can shift configuration
931          * bounds correspondingly to make sure client apps won't get configuration changed or
932          * relaunched. If the layout is no longer shifted, layout handler should restore shifted
933          * configuration bounds.
934          *
935          * @see #applyLayoutOffsetTarget(WindowContainerTransaction, int, int,
936          * ActivityManager.RunningTaskInfo, ActivityManager.RunningTaskInfo)
937          */
setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout)938         void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout);
939 
940         /** Calls when user double tapped on the divider bar. */
onDoubleTappedDivider()941         default void onDoubleTappedDivider() {
942         }
943 
944         /** Returns split position of the token. */
945         @SplitPosition
getSplitItemPosition(WindowContainerToken token)946         int getSplitItemPosition(WindowContainerToken token);
947     }
948 
949     /**
950      * Calculates and applies proper dismissing parallax offset and dimming value to hint users
951      * dismissing gesture.
952      */
953     private class ResizingEffectPolicy {
954         /** Indicates whether to offset splitting bounds to hint dismissing progress or not. */
955         private final int mParallaxType;
956 
957         int mShrinkSide = DOCKED_INVALID;
958 
959         // The current dismissing side.
960         int mDismissingSide = DOCKED_INVALID;
961 
962         // The parallax offset to hint the dismissing side and progress.
963         final Point mParallaxOffset = new Point();
964 
965         // The dimming value to hint the dismissing side and progress.
966         float mDismissingDimValue = 0.0f;
967         final Rect mContentBounds = new Rect();
968         final Rect mSurfaceBounds = new Rect();
969 
ResizingEffectPolicy(int parallaxType)970         ResizingEffectPolicy(int parallaxType) {
971             mParallaxType = parallaxType;
972         }
973 
974         /**
975          * Applies a parallax to the task to hint dismissing progress.
976          *
977          * @param position    the split position to apply dismissing parallax effect
978          * @param isLeftRightSplit indicates whether it's splitting horizontally or vertically
979          */
applyDividerPosition(int position, boolean isLeftRightSplit)980         void applyDividerPosition(int position, boolean isLeftRightSplit) {
981             mDismissingSide = DOCKED_INVALID;
982             mParallaxOffset.set(0, 0);
983             mDismissingDimValue = 0;
984 
985             int totalDismissingDistance = 0;
986             if (position < mDividerSnapAlgorithm.getFirstSplitTarget().position) {
987                 mDismissingSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP;
988                 totalDismissingDistance = mDividerSnapAlgorithm.getDismissStartTarget().position
989                         - mDividerSnapAlgorithm.getFirstSplitTarget().position;
990             } else if (position > mDividerSnapAlgorithm.getLastSplitTarget().position) {
991                 mDismissingSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM;
992                 totalDismissingDistance = mDividerSnapAlgorithm.getLastSplitTarget().position
993                         - mDividerSnapAlgorithm.getDismissEndTarget().position;
994             }
995 
996             final boolean topLeftShrink = isLeftRightSplit
997                     ? position < mWinBounds1.right : position < mWinBounds1.bottom;
998             if (topLeftShrink) {
999                 mShrinkSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP;
1000                 mContentBounds.set(mWinBounds1);
1001                 mSurfaceBounds.set(mBounds1);
1002             } else {
1003                 mShrinkSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM;
1004                 mContentBounds.set(mWinBounds2);
1005                 mSurfaceBounds.set(mBounds2);
1006             }
1007 
1008             if (mDismissingSide != DOCKED_INVALID) {
1009                 float fraction = Math.max(0,
1010                         Math.min(mDividerSnapAlgorithm.calculateDismissingFraction(position), 1f));
1011                 mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction);
1012                 if (mParallaxType == PARALLAX_DISMISSING) {
1013                     fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide);
1014                     if (isLeftRightSplit) {
1015                         mParallaxOffset.x = (int) (fraction * totalDismissingDistance);
1016                     } else {
1017                         mParallaxOffset.y = (int) (fraction * totalDismissingDistance);
1018                     }
1019                 }
1020             }
1021 
1022             if (mParallaxType == PARALLAX_ALIGN_CENTER) {
1023                 if (isLeftRightSplit) {
1024                     mParallaxOffset.x =
1025                             (mSurfaceBounds.width() - mContentBounds.width()) / 2;
1026                 } else {
1027                     mParallaxOffset.y =
1028                             (mSurfaceBounds.height() - mContentBounds.height()) / 2;
1029                 }
1030             }
1031         }
1032 
1033         /**
1034          * @return for a specified {@code fraction}, this returns an adjusted value that simulates a
1035          * slowing down parallax effect
1036          */
1037         private float calculateParallaxDismissingFraction(float fraction, int dockSide) {
1038             float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f;
1039 
1040             // Less parallax at the top, just because.
1041             if (dockSide == WindowManager.DOCKED_TOP) {
1042                 result /= 2f;
1043             }
1044             return result;
1045         }
1046 
1047         /** Applies parallax offset and dimming value to the root surface at the dismissing side. */
1048         void adjustRootSurface(SurfaceControl.Transaction t,
1049                 SurfaceControl leash1, SurfaceControl leash2) {
1050             SurfaceControl targetLeash = null;
1051 
1052             if (mParallaxType == PARALLAX_DISMISSING) {
1053                 switch (mDismissingSide) {
1054                     case DOCKED_TOP:
1055                     case DOCKED_LEFT:
1056                         targetLeash = leash1;
1057                         mTempRect.set(mBounds1);
1058                         break;
1059                     case DOCKED_BOTTOM:
1060                     case DOCKED_RIGHT:
1061                         targetLeash = leash2;
1062                         mTempRect.set(mBounds2);
1063                         break;
1064                 }
1065             } else if (mParallaxType == PARALLAX_ALIGN_CENTER) {
1066                 switch (mShrinkSide) {
1067                     case DOCKED_TOP:
1068                     case DOCKED_LEFT:
1069                         targetLeash = leash1;
1070                         mTempRect.set(mBounds1);
1071                         break;
1072                     case DOCKED_BOTTOM:
1073                     case DOCKED_RIGHT:
1074                         targetLeash = leash2;
1075                         mTempRect.set(mBounds2);
1076                         break;
1077                 }
1078             }
1079             if (mParallaxType != PARALLAX_NONE && targetLeash != null) {
1080                 t.setPosition(targetLeash,
1081                         mTempRect.left + mParallaxOffset.x, mTempRect.top + mParallaxOffset.y);
1082                 // Transform the screen-based split bounds to surface-based crop bounds.
1083                 mTempRect.offsetTo(-mParallaxOffset.x, -mParallaxOffset.y);
1084                 t.setWindowCrop(targetLeash, mTempRect);
1085             }
1086         }
1087 
1088         void adjustDimSurface(SurfaceControl.Transaction t,
1089                 SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
1090             SurfaceControl targetDimLayer;
1091             switch (mDismissingSide) {
1092                 case DOCKED_TOP:
1093                 case DOCKED_LEFT:
1094                     targetDimLayer = dimLayer1;
1095                     break;
1096                 case DOCKED_BOTTOM:
1097                 case DOCKED_RIGHT:
1098                     targetDimLayer = dimLayer2;
1099                     break;
1100                 case DOCKED_INVALID:
1101                 default:
1102                     t.setAlpha(dimLayer1, 0).hide(dimLayer1);
1103                     t.setAlpha(dimLayer2, 0).hide(dimLayer2);
1104                     return;
1105             }
1106             t.setAlpha(targetDimLayer, mDismissingDimValue)
1107                     .setVisibility(targetDimLayer, mDismissingDimValue > 0.001f);
1108         }
1109     }
1110 
1111     /** Records IME top offset changes and updates SplitLayout correspondingly. */
1112     private class ImePositionProcessor implements DisplayImeController.ImePositionProcessor {
1113         /**
1114          * Maximum size of an adjusted split bounds relative to original stack bounds. Used to
1115          * restrict IME adjustment so that a min portion of top split remains visible.
1116          */
1117         private static final float ADJUSTED_SPLIT_FRACTION_MAX = 0.7f;
1118         private static final float ADJUSTED_NONFOCUS_DIM = 0.3f;
1119 
1120         private final int mDisplayId;
1121 
1122         private boolean mHasImeFocus;
1123         private boolean mImeShown;
1124         private int mYOffsetForIme;
1125         private float mDimValue1;
1126         private float mDimValue2;
1127 
1128         private int mStartImeTop;
1129         private int mEndImeTop;
1130 
1131         private int mTargetYOffset;
1132         private int mLastYOffset;
1133         private float mTargetDim1;
1134         private float mTargetDim2;
1135         private float mLastDim1;
1136         private float mLastDim2;
1137 
1138         private ImePositionProcessor(int displayId) {
1139             mDisplayId = displayId;
1140         }
1141 
1142         @Override
1143         public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop,
1144                 boolean showing, boolean isFloating, SurfaceControl.Transaction t) {
1145             if (displayId != mDisplayId || !mInitialized) {
1146                 return 0;
1147             }
1148 
1149             final int imeTargetPosition = getImeTargetPosition();
1150             mHasImeFocus = imeTargetPosition != SPLIT_POSITION_UNDEFINED;
1151             if (!mHasImeFocus) {
1152                 return 0;
1153             }
1154 
1155             mStartImeTop = showing ? hiddenTop : shownTop;
1156             mEndImeTop = showing ? shownTop : hiddenTop;
1157             mImeShown = showing;
1158 
1159             // Update target dim values
1160             mLastDim1 = mDimValue1;
1161             mTargetDim1 = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT && mImeShown
1162                     && mDimNonImeSide ? ADJUSTED_NONFOCUS_DIM : 0.0f;
1163             mLastDim2 = mDimValue2;
1164             mTargetDim2 = imeTargetPosition == SPLIT_POSITION_TOP_OR_LEFT && mImeShown
1165                     && mDimNonImeSide ? ADJUSTED_NONFOCUS_DIM : 0.0f;
1166 
1167             // Calculate target bounds offset for IME
1168             mLastYOffset = mYOffsetForIme;
1169             final boolean needOffset = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
1170                     && !isFloating && !mIsLeftRightSplit && mImeShown;
1171             mTargetYOffset = needOffset ? getTargetYOffset() : 0;
1172 
1173             if (mTargetYOffset != mLastYOffset) {
1174                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
1175                         "Split IME animation starting, fromY=%d toY=%d",
1176                         mLastYOffset, mTargetYOffset);
1177                 // Freeze the configuration size with offset to prevent app get a configuration
1178                 // changed or relaunch. This is required to make sure client apps will calculate
1179                 // insets properly after layout shifted.
1180                 if (mTargetYOffset == 0) {
1181                     mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this);
1182                 } else {
1183                     mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset, SplitLayout.this);
1184                 }
1185             }
1186 
1187             // Make {@link DividerView} non-interactive while IME showing in split mode. Listen to
1188             // ImePositionProcessor#onImeVisibilityChanged directly in DividerView is not enough
1189             // because DividerView won't receive onImeVisibilityChanged callback after it being
1190             // re-inflated.
1191             setDividerInteractive(!mImeShown || !mHasImeFocus || isFloating, true,
1192                     "onImeStartPositioning");
1193 
1194             return mTargetYOffset != mLastYOffset ? IME_ANIMATION_NO_ALPHA : 0;
1195         }
1196 
1197         @Override
1198         public void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) {
1199             if (displayId != mDisplayId || !mHasImeFocus) return;
1200             onProgress(getProgress(imeTop));
1201             mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
1202         }
1203 
1204         @Override
1205         public void onImeEndPositioning(int displayId, boolean cancel,
1206                 SurfaceControl.Transaction t) {
1207             if (displayId != mDisplayId || !mHasImeFocus || cancel) return;
1208             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
1209                     "Split IME animation ending, canceled=%b", cancel);
1210             onProgress(1.0f);
1211             mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
1212         }
1213 
1214         @Override
1215         public void onImeControlTargetChanged(int displayId, boolean controlling) {
1216             if (displayId != mDisplayId) return;
1217             // Restore the split layout when wm-shell is not controlling IME insets anymore.
1218             if (!controlling && mImeShown) {
1219                 reset();
1220                 setDividerInteractive(true, true, "onImeControlTargetChanged");
1221                 mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this);
1222                 mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
1223             }
1224         }
1225 
1226         private int getTargetYOffset() {
1227             final int desireOffset = Math.abs(mEndImeTop - mStartImeTop);
1228             // Make sure to keep at least 30% visible for the top split.
1229             final int maxOffset = (int) (mBounds1.height() * ADJUSTED_SPLIT_FRACTION_MAX);
1230             return -Math.min(desireOffset, maxOffset);
1231         }
1232 
1233         @SplitPosition
1234         private int getImeTargetPosition() {
1235             final WindowContainerToken token = mTaskOrganizer.getImeTarget(mDisplayId);
1236             return mSplitLayoutHandler.getSplitItemPosition(token);
1237         }
1238 
1239         private float getProgress(int currImeTop) {
1240             return ((float) currImeTop - mStartImeTop) / (mEndImeTop - mStartImeTop);
1241         }
1242 
1243         private void onProgress(float progress) {
1244             mDimValue1 = getProgressValue(mLastDim1, mTargetDim1, progress);
1245             mDimValue2 = getProgressValue(mLastDim2, mTargetDim2, progress);
1246             mYOffsetForIme =
1247                     (int) getProgressValue((float) mLastYOffset, (float) mTargetYOffset, progress);
1248         }
1249 
1250         private float getProgressValue(float start, float end, float progress) {
1251             return start + (end - start) * progress;
1252         }
1253 
1254         void reset() {
1255             mHasImeFocus = false;
1256             mImeShown = false;
1257             mYOffsetForIme = mLastYOffset = mTargetYOffset = 0;
1258             mDimValue1 = mLastDim1 = mTargetDim1 = 0.0f;
1259             mDimValue2 = mLastDim2 = mTargetDim2 = 0.0f;
1260         }
1261 
1262         /**
1263          * Adjusts surface layout while showing IME.
1264          *
1265          * @return {@code false} if there's no need to adjust, otherwise {@code true}
1266          */
1267         boolean adjustSurfaceLayoutForIme(SurfaceControl.Transaction t,
1268                 SurfaceControl dividerLeash, SurfaceControl leash1, SurfaceControl leash2,
1269                 SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
1270             final boolean showDim = mDimValue1 > 0.001f || mDimValue2 > 0.001f;
1271             boolean adjusted = false;
1272             if (mYOffsetForIme != 0) {
1273                 if (dividerLeash != null) {
1274                     getRefDividerBounds(mTempRect);
1275                     mTempRect.offset(0, mYOffsetForIme);
1276                     t.setPosition(dividerLeash, mTempRect.left, mTempRect.top);
1277                 }
1278 
1279                 getRefBounds1(mTempRect);
1280                 mTempRect.offset(0, mYOffsetForIme);
1281                 t.setPosition(leash1, mTempRect.left, mTempRect.top);
1282 
1283                 getRefBounds2(mTempRect);
1284                 mTempRect.offset(0, mYOffsetForIme);
1285                 t.setPosition(leash2, mTempRect.left, mTempRect.top);
1286                 adjusted = true;
1287             }
1288 
1289             if (showDim) {
1290                 t.setAlpha(dimLayer1, mDimValue1).setVisibility(dimLayer1, mDimValue1 > 0.001f);
1291                 t.setAlpha(dimLayer2, mDimValue2).setVisibility(dimLayer2, mDimValue2 > 0.001f);
1292                 adjusted = true;
1293             }
1294             return adjusted;
1295         }
1296     }
1297 }
1298