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 import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER;
27 
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 
36 import android.animation.Animator;
37 import android.animation.AnimatorListenerAdapter;
38 import android.animation.ValueAnimator;
39 import android.annotation.NonNull;
40 import android.app.ActivityManager;
41 import android.content.Context;
42 import android.content.res.Configuration;
43 import android.content.res.Resources;
44 import android.graphics.Point;
45 import android.graphics.Rect;
46 import android.view.Display;
47 import android.view.InsetsSourceControl;
48 import android.view.InsetsState;
49 import android.view.RoundedCorner;
50 import android.view.SurfaceControl;
51 import android.view.WindowInsets;
52 import android.view.WindowManager;
53 import android.window.WindowContainerToken;
54 import android.window.WindowContainerTransaction;
55 
56 import androidx.annotation.Nullable;
57 
58 import com.android.internal.annotations.VisibleForTesting;
59 import com.android.internal.policy.DividerSnapAlgorithm;
60 import com.android.internal.policy.DockedDividerUtils;
61 import com.android.wm.shell.R;
62 import com.android.wm.shell.ShellTaskOrganizer;
63 import com.android.wm.shell.animation.Interpolators;
64 import com.android.wm.shell.common.DisplayImeController;
65 import com.android.wm.shell.common.DisplayInsetsController;
66 import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
67 
68 import java.io.PrintWriter;
69 
70 /**
71  * Records and handles layout of splits. Helps to calculate proper bounds when configuration or
72  * divide position changes.
73  */
74 public final class SplitLayout implements DisplayInsetsController.OnInsetsChangedListener {
75 
76     private final int mDividerWindowWidth;
77     private final int mDividerInsets;
78     private final int mDividerSize;
79 
80     private final Rect mTempRect = new Rect();
81     private final Rect mRootBounds = new Rect();
82     private final Rect mDividerBounds = new Rect();
83     private final Rect mBounds1 = new Rect();
84     private final Rect mBounds2 = new Rect();
85     private final Rect mWinBounds1 = new Rect();
86     private final Rect mWinBounds2 = new Rect();
87     private final SplitLayoutHandler mSplitLayoutHandler;
88     private final SplitWindowManager mSplitWindowManager;
89     private final DisplayImeController mDisplayImeController;
90     private final ImePositionProcessor mImePositionProcessor;
91     private final DismissingEffectPolicy mDismissingEffectPolicy;
92     private final ShellTaskOrganizer mTaskOrganizer;
93     private final InsetsState mInsetsState = new InsetsState();
94 
95     private Context mContext;
96     private DividerSnapAlgorithm mDividerSnapAlgorithm;
97     private WindowContainerToken mWinToken1;
98     private WindowContainerToken mWinToken2;
99     private int mDividePosition;
100     private boolean mInitialized = false;
101     private int mOrientation;
102     private int mRotation;
103 
SplitLayout(String windowName, Context context, Configuration configuration, SplitLayoutHandler splitLayoutHandler, SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks, DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer, boolean applyDismissingParallax)104     public SplitLayout(String windowName, Context context, Configuration configuration,
105             SplitLayoutHandler splitLayoutHandler,
106             SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks,
107             DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer,
108             boolean applyDismissingParallax) {
109         mContext = context.createConfigurationContext(configuration);
110         mOrientation = configuration.orientation;
111         mRotation = configuration.windowConfiguration.getRotation();
112         mSplitLayoutHandler = splitLayoutHandler;
113         mDisplayImeController = displayImeController;
114         mSplitWindowManager = new SplitWindowManager(windowName, mContext, configuration,
115                 parentContainerCallbacks);
116         mTaskOrganizer = taskOrganizer;
117         mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId());
118         mDismissingEffectPolicy = new DismissingEffectPolicy(applyDismissingParallax);
119 
120         final Resources resources = context.getResources();
121         mDividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width);
122         mDividerInsets = getDividerInsets(resources, context.getDisplay());
123         mDividerWindowWidth = mDividerSize + 2 * mDividerInsets;
124 
125         mRootBounds.set(configuration.windowConfiguration.getBounds());
126         mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
127         resetDividerPosition();
128     }
129 
getDividerInsets(Resources resources, Display display)130     private int getDividerInsets(Resources resources, Display display) {
131         final int dividerInset = resources.getDimensionPixelSize(
132                 com.android.internal.R.dimen.docked_stack_divider_insets);
133 
134         int radius = 0;
135         RoundedCorner corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT);
136         radius = corner != null ? Math.max(radius, corner.getRadius()) : radius;
137         corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT);
138         radius = corner != null ? Math.max(radius, corner.getRadius()) : radius;
139         corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT);
140         radius = corner != null ? Math.max(radius, corner.getRadius()) : radius;
141         corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT);
142         radius = corner != null ? Math.max(radius, corner.getRadius()) : radius;
143 
144         return Math.max(dividerInset, radius);
145     }
146 
147     /** Gets bounds of the primary split. */
getBounds1()148     public Rect getBounds1() {
149         return new Rect(mBounds1);
150     }
151 
152     /** Gets bounds of the secondary split. */
getBounds2()153     public Rect getBounds2() {
154         return new Rect(mBounds2);
155     }
156 
157     /** Gets bounds of divider window. */
getDividerBounds()158     public Rect getDividerBounds() {
159         return new Rect(mDividerBounds);
160     }
161 
162     /** Returns leash of the current divider bar. */
163     @Nullable
getDividerLeash()164     public SurfaceControl getDividerLeash() {
165         return mSplitWindowManager == null ? null : mSplitWindowManager.getSurfaceControl();
166     }
167 
getDividePosition()168     int getDividePosition() {
169         return mDividePosition;
170     }
171 
172     /**
173      * Returns the divider position as a fraction from 0 to 1.
174      */
getDividerPositionAsFraction()175     public float getDividerPositionAsFraction() {
176         return Math.min(1f, Math.max(0f, isLandscape()
177                 ? (float) ((mBounds1.right + mBounds2.left) / 2f) / mBounds2.right
178                 : (float) ((mBounds1.bottom + mBounds2.top) / 2f) / mBounds2.bottom));
179     }
180 
181     /** Applies new configuration, returns {@code false} if there's no effect to the layout. */
updateConfiguration(Configuration configuration)182     public boolean updateConfiguration(Configuration configuration) {
183         boolean affectsLayout = false;
184 
185         // Update the split bounds when necessary. Besides root bounds changed, split bounds need to
186         // be updated when the rotation changed to cover the case that users rotated the screen 180
187         // degrees.
188         // Make sure to render the divider bar with proper resources that matching the screen
189         // orientation.
190         final int rotation = configuration.windowConfiguration.getRotation();
191         final Rect rootBounds = configuration.windowConfiguration.getBounds();
192         final int orientation = configuration.orientation;
193 
194         if (mOrientation == orientation
195                 && rotation == mRotation
196                 && mRootBounds.equals(rootBounds)) {
197             return false;
198         }
199 
200         mContext = mContext.createConfigurationContext(configuration);
201         mSplitWindowManager.setConfiguration(configuration);
202         mOrientation = orientation;
203         mTempRect.set(mRootBounds);
204         mRootBounds.set(rootBounds);
205         mRotation = rotation;
206         mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
207         initDividerPosition(mTempRect);
208 
209         if (mInitialized) {
210             release();
211             init();
212         }
213 
214         return true;
215     }
216 
initDividerPosition(Rect oldBounds)217     private void initDividerPosition(Rect oldBounds) {
218         final float snapRatio = (float) mDividePosition
219                 / (float) (isLandscape(oldBounds) ? oldBounds.width() : oldBounds.height());
220         // Estimate position by previous ratio.
221         final float length =
222                 (float) (isLandscape() ? mRootBounds.width() : mRootBounds.height());
223         final int estimatePosition = (int) (length * snapRatio);
224         // Init divider position by estimated position using current bounds snap algorithm.
225         mDividePosition = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget(
226                 estimatePosition).position;
227         updateBounds(mDividePosition);
228     }
229 
230     /** Updates recording bounds of divider window and both of the splits. */
updateBounds(int position)231     private void updateBounds(int position) {
232         mDividerBounds.set(mRootBounds);
233         mBounds1.set(mRootBounds);
234         mBounds2.set(mRootBounds);
235         final boolean isLandscape = isLandscape(mRootBounds);
236         if (isLandscape) {
237             position += mRootBounds.left;
238             mDividerBounds.left = position - mDividerInsets;
239             mDividerBounds.right = mDividerBounds.left + mDividerWindowWidth;
240             mBounds1.right = position;
241             mBounds2.left = mBounds1.right + mDividerSize;
242         } else {
243             position += mRootBounds.top;
244             mDividerBounds.top = position - mDividerInsets;
245             mDividerBounds.bottom = mDividerBounds.top + mDividerWindowWidth;
246             mBounds1.bottom = position;
247             mBounds2.top = mBounds1.bottom + mDividerSize;
248         }
249         DockedDividerUtils.sanitizeStackBounds(mBounds1, true /** topLeft */);
250         DockedDividerUtils.sanitizeStackBounds(mBounds2, false /** topLeft */);
251         mDismissingEffectPolicy.applyDividerPosition(position, isLandscape);
252     }
253 
254     /** Inflates {@link DividerView} on the root surface. */
init()255     public void init() {
256         if (mInitialized) return;
257         mInitialized = true;
258         mSplitWindowManager.init(this, mInsetsState);
259         mDisplayImeController.addPositionProcessor(mImePositionProcessor);
260     }
261 
262     /** Releases the surface holding the current {@link DividerView}. */
release()263     public void release() {
264         if (!mInitialized) return;
265         mInitialized = false;
266         mSplitWindowManager.release();
267         mDisplayImeController.removePositionProcessor(mImePositionProcessor);
268         mImePositionProcessor.reset();
269     }
270 
271     @Override
insetsChanged(InsetsState insetsState)272     public void insetsChanged(InsetsState insetsState) {
273         mInsetsState.set(insetsState);
274         if (!mInitialized) {
275             return;
276         }
277         mSplitWindowManager.onInsetsChanged(insetsState);
278     }
279 
280     @Override
insetsControlChanged(InsetsState insetsState, InsetsSourceControl[] activeControls)281     public void insetsControlChanged(InsetsState insetsState,
282             InsetsSourceControl[] activeControls) {
283         if (!mInsetsState.equals(insetsState)) {
284             insetsChanged(insetsState);
285         }
286     }
287 
288     /**
289      * Updates bounds with the passing position. Usually used to update recording bounds while
290      * performing animation or dragging divider bar to resize the splits.
291      */
updateDivideBounds(int position)292     void updateDivideBounds(int position) {
293         updateBounds(position);
294         mSplitLayoutHandler.onLayoutSizeChanging(this);
295     }
296 
setDividePosition(int position)297     void setDividePosition(int position) {
298         mDividePosition = position;
299         updateBounds(mDividePosition);
300         mSplitLayoutHandler.onLayoutSizeChanged(this);
301     }
302 
303     /** Sets divide position base on the ratio within root bounds. */
setDivideRatio(float ratio)304     public void setDivideRatio(float ratio) {
305         final int position = isLandscape()
306                 ? mRootBounds.left + (int) (mRootBounds.width() * ratio)
307                 : mRootBounds.top + (int) (mRootBounds.height() * ratio);
308         DividerSnapAlgorithm.SnapTarget snapTarget =
309                 mDividerSnapAlgorithm.calculateNonDismissingSnapTarget(position);
310         setDividePosition(snapTarget.position);
311     }
312 
313     /** Resets divider position. */
resetDividerPosition()314     public void resetDividerPosition() {
315         mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position;
316         updateBounds(mDividePosition);
317         mWinToken1 = null;
318         mWinToken2 = null;
319         mWinBounds1.setEmpty();
320         mWinBounds2.setEmpty();
321     }
322 
323     /**
324      * Sets new divide position and updates bounds correspondingly. Notifies listener if the new
325      * target indicates dismissing split.
326      */
snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget)327     public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) {
328         switch (snapTarget.flag) {
329             case FLAG_DISMISS_START:
330                 flingDividePosition(currentPosition, snapTarget.position,
331                         () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */));
332                 break;
333             case FLAG_DISMISS_END:
334                 flingDividePosition(currentPosition, snapTarget.position,
335                         () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */));
336                 break;
337             default:
338                 flingDividePosition(currentPosition, snapTarget.position, null);
339                 break;
340         }
341     }
342 
onDoubleTappedDivider()343     void onDoubleTappedDivider() {
344         mSplitLayoutHandler.onDoubleTappedDivider();
345     }
346 
347     /**
348      * Returns {@link DividerSnapAlgorithm.SnapTarget} which matches passing position and velocity.
349      * If hardDismiss is set to {@code true}, it will be harder to reach dismiss target.
350      */
findSnapTarget(int position, float velocity, boolean hardDismiss)351     public DividerSnapAlgorithm.SnapTarget findSnapTarget(int position, float velocity,
352             boolean hardDismiss) {
353         return mDividerSnapAlgorithm.calculateSnapTarget(position, velocity, hardDismiss);
354     }
355 
getSnapAlgorithm(Context context, Rect rootBounds)356     private DividerSnapAlgorithm getSnapAlgorithm(Context context, Rect rootBounds) {
357         final boolean isLandscape = isLandscape(rootBounds);
358         return new DividerSnapAlgorithm(
359                 context.getResources(),
360                 rootBounds.width(),
361                 rootBounds.height(),
362                 mDividerSize,
363                 !isLandscape,
364                 getDisplayInsets(context),
365                 isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */);
366     }
367 
368     @VisibleForTesting
flingDividePosition(int from, int to, @Nullable Runnable flingFinishedCallback)369     void flingDividePosition(int from, int to, @Nullable Runnable flingFinishedCallback) {
370         if (from == to) {
371             // No animation run, still callback to stop resizing.
372             mSplitLayoutHandler.onLayoutSizeChanged(this);
373             return;
374         }
375         ValueAnimator animator = ValueAnimator
376                 .ofInt(from, to)
377                 .setDuration(250);
378         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
379         animator.addUpdateListener(
380                 animation -> updateDivideBounds((int) animation.getAnimatedValue()));
381         animator.addListener(new AnimatorListenerAdapter() {
382             @Override
383             public void onAnimationEnd(Animator animation) {
384                 setDividePosition(to);
385                 if (flingFinishedCallback != null) {
386                     flingFinishedCallback.run();
387                 }
388             }
389 
390             @Override
391             public void onAnimationCancel(Animator animation) {
392                 setDividePosition(to);
393             }
394         });
395         animator.start();
396     }
397 
getDisplayInsets(Context context)398     private static Rect getDisplayInsets(Context context) {
399         return context.getSystemService(WindowManager.class)
400                 .getMaximumWindowMetrics()
401                 .getWindowInsets()
402                 .getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout())
403                 .toRect();
404     }
405 
isLandscape(Rect bounds)406     private static boolean isLandscape(Rect bounds) {
407         return bounds.width() > bounds.height();
408     }
409 
410     /** Reverse the split position. */
411     @SplitPosition
reversePosition(@plitPosition int position)412     public static int reversePosition(@SplitPosition int position) {
413         switch (position) {
414             case SPLIT_POSITION_TOP_OR_LEFT:
415                 return SPLIT_POSITION_BOTTOM_OR_RIGHT;
416             case SPLIT_POSITION_BOTTOM_OR_RIGHT:
417                 return SPLIT_POSITION_TOP_OR_LEFT;
418             default:
419                 return SPLIT_POSITION_UNDEFINED;
420         }
421     }
422 
423     /**
424      * Return if this layout is landscape.
425      */
isLandscape()426     public boolean isLandscape() {
427         return isLandscape(mRootBounds);
428     }
429 
430     /** Apply recorded surface layout to the {@link SurfaceControl.Transaction}. */
applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2)431     public void applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1,
432             SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
433         final SurfaceControl dividerLeash = getDividerLeash();
434         if (dividerLeash != null) {
435             t.setPosition(dividerLeash, mDividerBounds.left, mDividerBounds.top);
436             // Resets layer of divider bar to make sure it is always on top.
437             t.setLayer(dividerLeash, SPLIT_DIVIDER_LAYER);
438         }
439         t.setPosition(leash1, mBounds1.left, mBounds1.top)
440                 .setWindowCrop(leash1, mBounds1.width(), mBounds1.height());
441         t.setPosition(leash2, mBounds2.left, mBounds2.top)
442                 .setWindowCrop(leash2, mBounds2.width(), mBounds2.height());
443 
444         if (mImePositionProcessor.adjustSurfaceLayoutForIme(
445                 t, dividerLeash, leash1, leash2, dimLayer1, dimLayer2)) {
446             return;
447         }
448 
449         mDismissingEffectPolicy.adjustDismissingSurface(t, leash1, leash2, dimLayer1, dimLayer2);
450     }
451 
452     /** Apply recorded task layout to the {@link WindowContainerTransaction}. */
applyTaskChanges(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2)453     public void applyTaskChanges(WindowContainerTransaction wct,
454             ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) {
455         if (mImePositionProcessor.applyTaskLayoutForIme(wct, task1.token, task2.token)) {
456             return;
457         }
458 
459         if (!mBounds1.equals(mWinBounds1) || !task1.token.equals(mWinToken1)) {
460             wct.setBounds(task1.token, mBounds1);
461             mWinBounds1.set(mBounds1);
462             mWinToken1 = task1.token;
463         }
464         if (!mBounds2.equals(mWinBounds2) || !task2.token.equals(mWinToken2)) {
465             wct.setBounds(task2.token, mBounds2);
466             mWinBounds2.set(mBounds2);
467             mWinToken2 = task2.token;
468         }
469     }
470 
471     /**
472      * Shift configuration bounds to prevent client apps get configuration changed or relaunch. And
473      * restore shifted configuration bounds if it's no longer shifted.
474      */
applyLayoutOffsetTarget(WindowContainerTransaction wct, int offsetX, int offsetY, ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2)475     public void applyLayoutOffsetTarget(WindowContainerTransaction wct, int offsetX, int offsetY,
476             ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2) {
477         if (offsetX == 0 && offsetY == 0) {
478             wct.setBounds(taskInfo1.token, mBounds1);
479             wct.setAppBounds(taskInfo1.token, null);
480             wct.setScreenSizeDp(taskInfo1.token,
481                     SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
482 
483             wct.setBounds(taskInfo2.token, mBounds2);
484             wct.setAppBounds(taskInfo2.token, null);
485             wct.setScreenSizeDp(taskInfo2.token,
486                     SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
487         } else {
488             mTempRect.set(taskInfo1.configuration.windowConfiguration.getBounds());
489             mTempRect.offset(offsetX, offsetY);
490             wct.setBounds(taskInfo1.token, mTempRect);
491             mTempRect.set(taskInfo1.configuration.windowConfiguration.getAppBounds());
492             mTempRect.offset(offsetX, offsetY);
493             wct.setAppBounds(taskInfo1.token, mTempRect);
494             wct.setScreenSizeDp(taskInfo1.token,
495                     taskInfo1.configuration.screenWidthDp,
496                     taskInfo1.configuration.screenHeightDp);
497 
498             mTempRect.set(taskInfo2.configuration.windowConfiguration.getBounds());
499             mTempRect.offset(offsetX, offsetY);
500             wct.setBounds(taskInfo2.token, mTempRect);
501             mTempRect.set(taskInfo2.configuration.windowConfiguration.getAppBounds());
502             mTempRect.offset(offsetX, offsetY);
503             wct.setAppBounds(taskInfo2.token, mTempRect);
504             wct.setScreenSizeDp(taskInfo2.token,
505                     taskInfo2.configuration.screenWidthDp,
506                     taskInfo2.configuration.screenHeightDp);
507         }
508     }
509 
510     /** Dumps the current split bounds recorded in this layout. */
dump(@onNull PrintWriter pw, String prefix)511     public void dump(@NonNull PrintWriter pw, String prefix) {
512         pw.println(prefix + "bounds1=" + mBounds1.toShortString());
513         pw.println(prefix + "dividerBounds=" + mDividerBounds.toShortString());
514         pw.println(prefix + "bounds2=" + mBounds2.toShortString());
515     }
516 
517     /** Handles layout change event. */
518     public interface SplitLayoutHandler {
519 
520         /** Calls when dismissing split. */
onSnappedToDismiss(boolean snappedToEnd)521         void onSnappedToDismiss(boolean snappedToEnd);
522 
523         /**
524          * Calls when resizing the split bounds.
525          *
526          * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl,
527          * SurfaceControl, SurfaceControl)
528          */
onLayoutSizeChanging(SplitLayout layout)529         void onLayoutSizeChanging(SplitLayout layout);
530 
531         /**
532          * Calls when finish resizing the split bounds.
533          *
534          * @see #applyTaskChanges(WindowContainerTransaction, ActivityManager.RunningTaskInfo,
535          * ActivityManager.RunningTaskInfo)
536          * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl,
537          * SurfaceControl, SurfaceControl)
538          */
onLayoutSizeChanged(SplitLayout layout)539         void onLayoutSizeChanged(SplitLayout layout);
540 
541         /**
542          * Calls when re-positioning the split bounds. Like moving split bounds while showing IME
543          * panel.
544          *
545          * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl,
546          * SurfaceControl, SurfaceControl)
547          */
onLayoutPositionChanging(SplitLayout layout)548         void onLayoutPositionChanging(SplitLayout layout);
549 
550         /**
551          * Notifies the target offset for shifting layout. So layout handler can shift configuration
552          * bounds correspondingly to make sure client apps won't get configuration changed or
553          * relaunched. If the layout is no longer shifted, layout handler should restore shifted
554          * configuration bounds.
555          *
556          * @see #applyLayoutOffsetTarget(WindowContainerTransaction, int, int,
557          * ActivityManager.RunningTaskInfo, ActivityManager.RunningTaskInfo)
558          */
setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout)559         void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout);
560 
561         /** Calls when user double tapped on the divider bar. */
onDoubleTappedDivider()562         default void onDoubleTappedDivider() {
563         }
564 
565         /** Returns split position of the token. */
566         @SplitPosition
getSplitItemPosition(WindowContainerToken token)567         int getSplitItemPosition(WindowContainerToken token);
568     }
569 
570     /**
571      * Calculates and applies proper dismissing parallax offset and dimming value to hint users
572      * dismissing gesture.
573      */
574     private class DismissingEffectPolicy {
575         /** Indicates whether to offset splitting bounds to hint dismissing progress or not. */
576         private final boolean mApplyParallax;
577 
578         // The current dismissing side.
579         int mDismissingSide = DOCKED_INVALID;
580 
581         // The parallax offset to hint the dismissing side and progress.
582         final Point mDismissingParallaxOffset = new Point();
583 
584         // The dimming value to hint the dismissing side and progress.
585         float mDismissingDimValue = 0.0f;
586 
DismissingEffectPolicy(boolean applyDismissingParallax)587         DismissingEffectPolicy(boolean applyDismissingParallax) {
588             mApplyParallax = applyDismissingParallax;
589         }
590 
591         /**
592          * Applies a parallax to the task to hint dismissing progress.
593          *
594          * @param position    the split position to apply dismissing parallax effect
595          * @param isLandscape indicates whether it's splitting horizontally or vertically
596          */
applyDividerPosition(int position, boolean isLandscape)597         void applyDividerPosition(int position, boolean isLandscape) {
598             mDismissingSide = DOCKED_INVALID;
599             mDismissingParallaxOffset.set(0, 0);
600             mDismissingDimValue = 0;
601 
602             int totalDismissingDistance = 0;
603             if (position < mDividerSnapAlgorithm.getFirstSplitTarget().position) {
604                 mDismissingSide = isLandscape ? DOCKED_LEFT : DOCKED_TOP;
605                 totalDismissingDistance = mDividerSnapAlgorithm.getDismissStartTarget().position
606                         - mDividerSnapAlgorithm.getFirstSplitTarget().position;
607             } else if (position > mDividerSnapAlgorithm.getLastSplitTarget().position) {
608                 mDismissingSide = isLandscape ? DOCKED_RIGHT : DOCKED_BOTTOM;
609                 totalDismissingDistance = mDividerSnapAlgorithm.getLastSplitTarget().position
610                         - mDividerSnapAlgorithm.getDismissEndTarget().position;
611             }
612 
613             if (mDismissingSide != DOCKED_INVALID) {
614                 float fraction = Math.max(0,
615                         Math.min(mDividerSnapAlgorithm.calculateDismissingFraction(position), 1f));
616                 mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction);
617                 fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide);
618                 if (isLandscape) {
619                     mDismissingParallaxOffset.x = (int) (fraction * totalDismissingDistance);
620                 } else {
621                     mDismissingParallaxOffset.y = (int) (fraction * totalDismissingDistance);
622                 }
623             }
624         }
625 
626         /**
627          * @return for a specified {@code fraction}, this returns an adjusted value that simulates a
628          * slowing down parallax effect
629          */
calculateParallaxDismissingFraction(float fraction, int dockSide)630         private float calculateParallaxDismissingFraction(float fraction, int dockSide) {
631             float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f;
632 
633             // Less parallax at the top, just because.
634             if (dockSide == WindowManager.DOCKED_TOP) {
635                 result /= 2f;
636             }
637             return result;
638         }
639 
640         /** Applies parallax offset and dimming value to the root surface at the dismissing side. */
adjustDismissingSurface(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2)641         boolean adjustDismissingSurface(SurfaceControl.Transaction t,
642                 SurfaceControl leash1, SurfaceControl leash2,
643                 SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
644             SurfaceControl targetLeash, targetDimLayer;
645             switch (mDismissingSide) {
646                 case DOCKED_TOP:
647                 case DOCKED_LEFT:
648                     targetLeash = leash1;
649                     targetDimLayer = dimLayer1;
650                     mTempRect.set(mBounds1);
651                     break;
652                 case DOCKED_BOTTOM:
653                 case DOCKED_RIGHT:
654                     targetLeash = leash2;
655                     targetDimLayer = dimLayer2;
656                     mTempRect.set(mBounds2);
657                     break;
658                 case DOCKED_INVALID:
659                 default:
660                     t.setAlpha(dimLayer1, 0).hide(dimLayer1);
661                     t.setAlpha(dimLayer2, 0).hide(dimLayer2);
662                     return false;
663             }
664 
665             if (mApplyParallax) {
666                 t.setPosition(targetLeash,
667                         mTempRect.left + mDismissingParallaxOffset.x,
668                         mTempRect.top + mDismissingParallaxOffset.y);
669                 // Transform the screen-based split bounds to surface-based crop bounds.
670                 mTempRect.offsetTo(-mDismissingParallaxOffset.x, -mDismissingParallaxOffset.y);
671                 t.setWindowCrop(targetLeash, mTempRect);
672             }
673             t.setAlpha(targetDimLayer, mDismissingDimValue)
674                     .setVisibility(targetDimLayer, mDismissingDimValue > 0.001f);
675             return true;
676         }
677     }
678 
679     /** Records IME top offset changes and updates SplitLayout correspondingly. */
680     private class ImePositionProcessor implements DisplayImeController.ImePositionProcessor {
681         /**
682          * Maximum size of an adjusted split bounds relative to original stack bounds. Used to
683          * restrict IME adjustment so that a min portion of top split remains visible.
684          */
685         private static final float ADJUSTED_SPLIT_FRACTION_MAX = 0.7f;
686         private static final float ADJUSTED_NONFOCUS_DIM = 0.3f;
687 
688         private final int mDisplayId;
689 
690         private boolean mImeShown;
691         private int mYOffsetForIme;
692         private float mDimValue1;
693         private float mDimValue2;
694 
695         private int mStartImeTop;
696         private int mEndImeTop;
697 
698         private int mTargetYOffset;
699         private int mLastYOffset;
700         private float mTargetDim1;
701         private float mTargetDim2;
702         private float mLastDim1;
703         private float mLastDim2;
704 
ImePositionProcessor(int displayId)705         private ImePositionProcessor(int displayId) {
706             mDisplayId = displayId;
707         }
708 
709         @Override
onImeStartPositioning(int displayId, int hiddenTop, int shownTop, boolean showing, boolean isFloating, SurfaceControl.Transaction t)710         public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop,
711                 boolean showing, boolean isFloating, SurfaceControl.Transaction t) {
712             if (displayId != mDisplayId) return 0;
713             final int imeTargetPosition = getImeTargetPosition();
714             if (!mInitialized || imeTargetPosition == SPLIT_POSITION_UNDEFINED) return 0;
715             mStartImeTop = showing ? hiddenTop : shownTop;
716             mEndImeTop = showing ? shownTop : hiddenTop;
717             mImeShown = showing;
718 
719             // Update target dim values
720             mLastDim1 = mDimValue1;
721             mTargetDim1 = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT && showing
722                     ? ADJUSTED_NONFOCUS_DIM : 0.0f;
723             mLastDim2 = mDimValue2;
724             mTargetDim2 = imeTargetPosition == SPLIT_POSITION_TOP_OR_LEFT && showing
725                     ? ADJUSTED_NONFOCUS_DIM : 0.0f;
726 
727             // Calculate target bounds offset for IME
728             mLastYOffset = mYOffsetForIme;
729             final boolean needOffset = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
730                     && !isFloating && !isLandscape(mRootBounds) && showing;
731             mTargetYOffset = needOffset ? getTargetYOffset() : 0;
732 
733             if (mTargetYOffset != mLastYOffset) {
734                 // Freeze the configuration size with offset to prevent app get a configuration
735                 // changed or relaunch. This is required to make sure client apps will calculate
736                 // insets properly after layout shifted.
737                 if (mTargetYOffset == 0) {
738                     mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this);
739                 } else {
740                     mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset - mLastYOffset,
741                             SplitLayout.this);
742                 }
743             }
744 
745             // Make {@link DividerView} non-interactive while IME showing in split mode. Listen to
746             // ImePositionProcessor#onImeVisibilityChanged directly in DividerView is not enough
747             // because DividerView won't receive onImeVisibilityChanged callback after it being
748             // re-inflated.
749             mSplitWindowManager.setInteractive(
750                     !showing || imeTargetPosition == SPLIT_POSITION_UNDEFINED);
751 
752             return needOffset ? IME_ANIMATION_NO_ALPHA : 0;
753         }
754 
755         @Override
onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t)756         public void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) {
757             if (displayId != mDisplayId) return;
758             onProgress(getProgress(imeTop));
759             mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
760         }
761 
762         @Override
onImeEndPositioning(int displayId, boolean cancel, SurfaceControl.Transaction t)763         public void onImeEndPositioning(int displayId, boolean cancel,
764                 SurfaceControl.Transaction t) {
765             if (displayId != mDisplayId || cancel) return;
766             onProgress(1.0f);
767             mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
768         }
769 
770         @Override
onImeControlTargetChanged(int displayId, boolean controlling)771         public void onImeControlTargetChanged(int displayId, boolean controlling) {
772             if (displayId != mDisplayId) return;
773             // Restore the split layout when wm-shell is not controlling IME insets anymore.
774             if (!controlling && mImeShown) {
775                 reset();
776                 mSplitWindowManager.setInteractive(true);
777                 mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
778             }
779         }
780 
getTargetYOffset()781         private int getTargetYOffset() {
782             final int desireOffset = Math.abs(mEndImeTop - mStartImeTop);
783             // Make sure to keep at least 30% visible for the top split.
784             final int maxOffset = (int) (mBounds1.height() * ADJUSTED_SPLIT_FRACTION_MAX);
785             return -Math.min(desireOffset, maxOffset);
786         }
787 
788         @SplitPosition
getImeTargetPosition()789         private int getImeTargetPosition() {
790             final WindowContainerToken token = mTaskOrganizer.getImeTarget(mDisplayId);
791             return mSplitLayoutHandler.getSplitItemPosition(token);
792         }
793 
getProgress(int currImeTop)794         private float getProgress(int currImeTop) {
795             return ((float) currImeTop - mStartImeTop) / (mEndImeTop - mStartImeTop);
796         }
797 
onProgress(float progress)798         private void onProgress(float progress) {
799             mDimValue1 = getProgressValue(mLastDim1, mTargetDim1, progress);
800             mDimValue2 = getProgressValue(mLastDim2, mTargetDim2, progress);
801             mYOffsetForIme =
802                     (int) getProgressValue((float) mLastYOffset, (float) mTargetYOffset, progress);
803         }
804 
getProgressValue(float start, float end, float progress)805         private float getProgressValue(float start, float end, float progress) {
806             return start + (end - start) * progress;
807         }
808 
reset()809         void reset() {
810             mImeShown = false;
811             mYOffsetForIme = mLastYOffset = mTargetYOffset = 0;
812             mDimValue1 = mLastDim1 = mTargetDim1 = 0.0f;
813             mDimValue2 = mLastDim2 = mTargetDim2 = 0.0f;
814         }
815 
816         /**
817          * Applies adjusted task layout for showing IME.
818          *
819          * @return {@code false} if there's no need to adjust, otherwise {@code true}
820          */
applyTaskLayoutForIme(WindowContainerTransaction wct, WindowContainerToken token1, WindowContainerToken token2)821         boolean applyTaskLayoutForIme(WindowContainerTransaction wct,
822                 WindowContainerToken token1, WindowContainerToken token2) {
823             if (mYOffsetForIme == 0) return false;
824 
825             mTempRect.set(mBounds1);
826             mTempRect.offset(0, mYOffsetForIme);
827             wct.setBounds(token1, mTempRect);
828 
829             mTempRect.set(mBounds2);
830             mTempRect.offset(0, mYOffsetForIme);
831             wct.setBounds(token2, mTempRect);
832 
833             return true;
834         }
835 
836         /**
837          * Adjusts surface layout while showing IME.
838          *
839          * @return {@code false} if there's no need to adjust, otherwise {@code true}
840          */
adjustSurfaceLayoutForIme(SurfaceControl.Transaction t, SurfaceControl dividerLeash, SurfaceControl leash1, SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2)841         boolean adjustSurfaceLayoutForIme(SurfaceControl.Transaction t,
842                 SurfaceControl dividerLeash, SurfaceControl leash1, SurfaceControl leash2,
843                 SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
844             final boolean showDim = mDimValue1 > 0.001f || mDimValue2 > 0.001f;
845             boolean adjusted = false;
846             if (mYOffsetForIme != 0) {
847                 if (dividerLeash != null) {
848                     mTempRect.set(mDividerBounds);
849                     mTempRect.offset(0, mYOffsetForIme);
850                     t.setPosition(dividerLeash, mTempRect.left, mTempRect.top);
851                 }
852 
853                 mTempRect.set(mBounds1);
854                 mTempRect.offset(0, mYOffsetForIme);
855                 t.setPosition(leash1, mTempRect.left, mTempRect.top);
856 
857                 mTempRect.set(mBounds2);
858                 mTempRect.offset(0, mYOffsetForIme);
859                 t.setPosition(leash2, mTempRect.left, mTempRect.top);
860                 adjusted = true;
861             }
862 
863             if (showDim) {
864                 t.setAlpha(dimLayer1, mDimValue1).setVisibility(dimLayer1, mDimValue1 > 0.001f);
865                 t.setAlpha(dimLayer2, mDimValue2).setVisibility(dimLayer2, mDimValue2 > 0.001f);
866                 adjusted = true;
867             }
868             return adjusted;
869         }
870     }
871 }
872