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