1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.legacysplitscreen;
18 
19 import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
20 import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED;
21 
22 import android.animation.Animator;
23 import android.animation.AnimatorListenerAdapter;
24 import android.animation.ValueAnimator;
25 import android.annotation.Nullable;
26 import android.graphics.Rect;
27 import android.util.Slog;
28 import android.view.Choreographer;
29 import android.view.SurfaceControl;
30 import android.window.TaskOrganizer;
31 import android.window.WindowContainerToken;
32 import android.window.WindowContainerTransaction;
33 
34 import com.android.wm.shell.common.DisplayImeController;
35 import com.android.wm.shell.common.ShellExecutor;
36 import com.android.wm.shell.common.TransactionPool;
37 
38 class DividerImeController implements DisplayImeController.ImePositionProcessor {
39     private static final String TAG = "DividerImeController";
40     private static final boolean DEBUG = LegacySplitScreenController.DEBUG;
41 
42     private static final float ADJUSTED_NONFOCUS_DIM = 0.3f;
43 
44     private final LegacySplitScreenTaskListener mSplits;
45     private final TransactionPool mTransactionPool;
46     private final ShellExecutor mMainExecutor;
47     private final TaskOrganizer mTaskOrganizer;
48 
49     /**
50      * These are the y positions of the top of the IME surface when it is hidden and when it is
51      * shown respectively. These are NOT necessarily the top of the visible IME itself.
52      */
53     private int mHiddenTop = 0;
54     private int mShownTop = 0;
55 
56     // The following are target states (what we are curretly animating towards).
57     /**
58      * {@code true} if, at the end of the animation, the split task positions should be
59      * adjusted by height of the IME. This happens when the secondary split is the IME target.
60      */
61     private boolean mTargetAdjusted = false;
62     /**
63      * {@code true} if, at the end of the animation, the IME should be shown/visible
64      * regardless of what has focus.
65      */
66     private boolean mTargetShown = false;
67     private float mTargetPrimaryDim = 0.f;
68     private float mTargetSecondaryDim = 0.f;
69 
70     // The following are the current (most recent) states set during animation
71     /** {@code true} if the secondary split has IME focus. */
72     private boolean mSecondaryHasFocus = false;
73     /** The dimming currently applied to the primary/secondary splits. */
74     private float mLastPrimaryDim = 0.f;
75     private float mLastSecondaryDim = 0.f;
76     /** The most recent y position of the top of the IME surface */
77     private int mLastAdjustTop = -1;
78 
79     // The following are states reached last time an animation fully completed.
80     /** {@code true} if the IME was shown/visible by the last-completed animation. */
81     private boolean mImeWasShown = false;
82     /** {@code true} if the split positions were adjusted by the last-completed animation. */
83     private boolean mAdjusted = false;
84 
85     /**
86      * When some aspect of split-screen needs to animate independent from the IME,
87      * this will be non-null and control split animation.
88      */
89     @Nullable
90     private ValueAnimator mAnimation = null;
91 
92     private boolean mPaused = true;
93     private boolean mPausedTargetAdjusted = false;
94 
DividerImeController(LegacySplitScreenTaskListener splits, TransactionPool pool, ShellExecutor mainExecutor, TaskOrganizer taskOrganizer)95     DividerImeController(LegacySplitScreenTaskListener splits, TransactionPool pool,
96             ShellExecutor mainExecutor, TaskOrganizer taskOrganizer) {
97         mSplits = splits;
98         mTransactionPool = pool;
99         mMainExecutor = mainExecutor;
100         mTaskOrganizer = taskOrganizer;
101     }
102 
getView()103     private DividerView getView() {
104         return mSplits.mSplitScreenController.getDividerView();
105     }
106 
getLayout()107     private LegacySplitDisplayLayout getLayout() {
108         return mSplits.mSplitScreenController.getSplitLayout();
109     }
110 
isDividerHidden()111     private boolean isDividerHidden() {
112         final DividerView view = mSplits.mSplitScreenController.getDividerView();
113         return view == null || view.isHidden();
114     }
115 
getSecondaryHasFocus(int displayId)116     private boolean getSecondaryHasFocus(int displayId) {
117         WindowContainerToken imeSplit = mTaskOrganizer.getImeTarget(displayId);
118         return imeSplit != null
119                 && (imeSplit.asBinder() == mSplits.mSecondary.token.asBinder());
120     }
121 
reset()122     void reset() {
123         mPaused = true;
124         mPausedTargetAdjusted = false;
125         mAnimation = null;
126         mAdjusted = mTargetAdjusted = false;
127         mImeWasShown = mTargetShown = false;
128         mTargetPrimaryDim = mTargetSecondaryDim = mLastPrimaryDim = mLastSecondaryDim = 0.f;
129         mSecondaryHasFocus = false;
130         mLastAdjustTop = -1;
131     }
132 
updateDimTargets()133     private void updateDimTargets() {
134         final boolean splitIsVisible = !getView().isHidden();
135         mTargetPrimaryDim = (mSecondaryHasFocus && mTargetShown && splitIsVisible)
136                 ? ADJUSTED_NONFOCUS_DIM : 0.f;
137         mTargetSecondaryDim = (!mSecondaryHasFocus && mTargetShown && splitIsVisible)
138                 ? ADJUSTED_NONFOCUS_DIM : 0.f;
139     }
140 
141 
142     @Override
onImeControlTargetChanged(int displayId, boolean controlling)143     public void onImeControlTargetChanged(int displayId, boolean controlling) {
144         // Restore the split layout when wm-shell is not controlling IME insets anymore.
145         if (!controlling && mTargetShown) {
146             mPaused = false;
147             mTargetAdjusted = mTargetShown = false;
148             mTargetPrimaryDim = mTargetSecondaryDim = 0.f;
149             updateImeAdjustState(true /* force */);
150             startAsyncAnimation();
151         }
152     }
153 
154     @Override
155     @ImeAnimationFlags
onImeStartPositioning(int displayId, int hiddenTop, int shownTop, boolean imeShouldShow, boolean imeIsFloating, SurfaceControl.Transaction t)156     public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop,
157             boolean imeShouldShow, boolean imeIsFloating, SurfaceControl.Transaction t) {
158         if (isDividerHidden()) {
159             return 0;
160         }
161         mHiddenTop = hiddenTop;
162         mShownTop = shownTop;
163         mTargetShown = imeShouldShow;
164         mSecondaryHasFocus = getSecondaryHasFocus(displayId);
165         final boolean targetAdjusted = imeShouldShow && mSecondaryHasFocus
166                 && !imeIsFloating && !getLayout().mDisplayLayout.isLandscape()
167                 && !mSplits.mSplitScreenController.isMinimized();
168         if (mLastAdjustTop < 0) {
169             mLastAdjustTop = imeShouldShow ? hiddenTop : shownTop;
170         } else if (mLastAdjustTop != (imeShouldShow ? mShownTop : mHiddenTop)) {
171             if (mTargetAdjusted != targetAdjusted && targetAdjusted == mAdjusted) {
172                 // Check for an "interruption" of an existing animation. In this case, we
173                 // need to fake-flip the last-known state direction so that the animation
174                 // completes in the other direction.
175                 mAdjusted = mTargetAdjusted;
176             } else if (targetAdjusted && mTargetAdjusted && mAdjusted) {
177                 // Already fully adjusted for IME, but IME height has changed; so, force-start
178                 // an async animation to the new IME height.
179                 mAdjusted = false;
180             }
181         }
182         if (mPaused) {
183             mPausedTargetAdjusted = targetAdjusted;
184             if (DEBUG) Slog.d(TAG, " ime starting but paused " + dumpState());
185             return (targetAdjusted || mAdjusted) ? IME_ANIMATION_NO_ALPHA : 0;
186         }
187         mTargetAdjusted = targetAdjusted;
188         updateDimTargets();
189         if (DEBUG) Slog.d(TAG, " ime starting.  " + dumpState());
190         if (mAnimation != null || (mImeWasShown && imeShouldShow
191                 && mTargetAdjusted != mAdjusted)) {
192             // We need to animate adjustment independently of the IME position, so
193             // start our own animation to drive adjustment. This happens when a
194             // different split's editor has gained focus while the IME is still visible.
195             startAsyncAnimation();
196         }
197         updateImeAdjustState();
198 
199         return (mTargetAdjusted || mAdjusted) ? IME_ANIMATION_NO_ALPHA : 0;
200     }
201 
updateImeAdjustState()202     private void updateImeAdjustState() {
203         updateImeAdjustState(false /* force */);
204     }
205 
updateImeAdjustState(boolean force)206     private void updateImeAdjustState(boolean force) {
207         if (mAdjusted != mTargetAdjusted || force) {
208             // Reposition the server's secondary split position so that it evaluates
209             // insets properly.
210             WindowContainerTransaction wct = new WindowContainerTransaction();
211             final LegacySplitDisplayLayout splitLayout = getLayout();
212             if (mTargetAdjusted) {
213                 splitLayout.updateAdjustedBounds(mShownTop, mHiddenTop, mShownTop);
214                 wct.setBounds(mSplits.mSecondary.token, splitLayout.mAdjustedSecondary);
215                 // "Freeze" the configuration size so that the app doesn't get a config
216                 // or relaunch. This is required because normally nav-bar contributes
217                 // to configuration bounds (via nondecorframe).
218                 Rect adjustAppBounds = new Rect(mSplits.mSecondary.configuration
219                         .windowConfiguration.getAppBounds());
220                 adjustAppBounds.offset(0, splitLayout.mAdjustedSecondary.top
221                         - splitLayout.mSecondary.top);
222                 wct.setAppBounds(mSplits.mSecondary.token, adjustAppBounds);
223                 wct.setScreenSizeDp(mSplits.mSecondary.token,
224                         mSplits.mSecondary.configuration.screenWidthDp,
225                         mSplits.mSecondary.configuration.screenHeightDp);
226 
227                 wct.setBounds(mSplits.mPrimary.token, splitLayout.mAdjustedPrimary);
228                 adjustAppBounds = new Rect(mSplits.mPrimary.configuration
229                         .windowConfiguration.getAppBounds());
230                 adjustAppBounds.offset(0, splitLayout.mAdjustedPrimary.top
231                         - splitLayout.mPrimary.top);
232                 wct.setAppBounds(mSplits.mPrimary.token, adjustAppBounds);
233                 wct.setScreenSizeDp(mSplits.mPrimary.token,
234                         mSplits.mPrimary.configuration.screenWidthDp,
235                         mSplits.mPrimary.configuration.screenHeightDp);
236             } else {
237                 wct.setBounds(mSplits.mSecondary.token, splitLayout.mSecondary);
238                 wct.setAppBounds(mSplits.mSecondary.token, null);
239                 wct.setScreenSizeDp(mSplits.mSecondary.token,
240                         SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
241                 wct.setBounds(mSplits.mPrimary.token, splitLayout.mPrimary);
242                 wct.setAppBounds(mSplits.mPrimary.token, null);
243                 wct.setScreenSizeDp(mSplits.mPrimary.token,
244                         SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
245             }
246 
247             if (!mSplits.mSplitScreenController.getWmProxy().queueSyncTransactionIfWaiting(wct)) {
248                 mTaskOrganizer.applyTransaction(wct);
249             }
250         }
251 
252         // Update all the adjusted-for-ime states
253         if (!mPaused) {
254             final DividerView view = getView();
255             if (view != null) {
256                 view.setAdjustedForIme(mTargetShown, mTargetShown
257                         ? DisplayImeController.ANIMATION_DURATION_SHOW_MS
258                         : DisplayImeController.ANIMATION_DURATION_HIDE_MS);
259             }
260         }
261         mSplits.mSplitScreenController.setAdjustedForIme(mTargetShown && !mPaused);
262     }
263 
264     @Override
onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t)265     public void onImePositionChanged(int displayId, int imeTop,
266             SurfaceControl.Transaction t) {
267         if (mAnimation != null || isDividerHidden() || mPaused) {
268             // Not synchronized with IME anymore, so return.
269             return;
270         }
271         final float fraction = ((float) imeTop - mHiddenTop) / (mShownTop - mHiddenTop);
272         final float progress = mTargetShown ? fraction : 1.f - fraction;
273         onProgress(progress, t);
274     }
275 
276     @Override
onImeEndPositioning(int displayId, boolean cancelled, SurfaceControl.Transaction t)277     public void onImeEndPositioning(int displayId, boolean cancelled,
278             SurfaceControl.Transaction t) {
279         if (mAnimation != null || isDividerHidden() || mPaused) {
280             // Not synchronized with IME anymore, so return.
281             return;
282         }
283         onEnd(cancelled, t);
284     }
285 
onProgress(float progress, SurfaceControl.Transaction t)286     private void onProgress(float progress, SurfaceControl.Transaction t) {
287         final DividerView view = getView();
288         if (mTargetAdjusted != mAdjusted && !mPaused) {
289             final LegacySplitDisplayLayout splitLayout = getLayout();
290             final float fraction = mTargetAdjusted ? progress : 1.f - progress;
291             mLastAdjustTop = (int) (fraction * mShownTop + (1.f - fraction) * mHiddenTop);
292             splitLayout.updateAdjustedBounds(mLastAdjustTop, mHiddenTop, mShownTop);
293             view.resizeSplitSurfaces(t, splitLayout.mAdjustedPrimary,
294                     splitLayout.mAdjustedSecondary);
295         }
296         final float invProg = 1.f - progress;
297         view.setResizeDimLayer(t, true /* primary */,
298                 mLastPrimaryDim * invProg + progress * mTargetPrimaryDim);
299         view.setResizeDimLayer(t, false /* primary */,
300                 mLastSecondaryDim * invProg + progress * mTargetSecondaryDim);
301     }
302 
setDimsHidden(SurfaceControl.Transaction t, boolean hidden)303     void setDimsHidden(SurfaceControl.Transaction t, boolean hidden) {
304         final DividerView view = getView();
305         if (hidden) {
306             view.setResizeDimLayer(t, true /* primary */, 0.f /* alpha */);
307             view.setResizeDimLayer(t, false /* primary */, 0.f /* alpha */);
308         } else {
309             updateDimTargets();
310             view.setResizeDimLayer(t, true /* primary */, mTargetPrimaryDim);
311             view.setResizeDimLayer(t, false /* primary */, mTargetSecondaryDim);
312         }
313     }
314 
onEnd(boolean cancelled, SurfaceControl.Transaction t)315     private void onEnd(boolean cancelled, SurfaceControl.Transaction t) {
316         if (!cancelled) {
317             onProgress(1.f, t);
318             mAdjusted = mTargetAdjusted;
319             mImeWasShown = mTargetShown;
320             mLastAdjustTop = mAdjusted ? mShownTop : mHiddenTop;
321             mLastPrimaryDim = mTargetPrimaryDim;
322             mLastSecondaryDim = mTargetSecondaryDim;
323         }
324     }
325 
startAsyncAnimation()326     private void startAsyncAnimation() {
327         if (mAnimation != null) {
328             mAnimation.cancel();
329         }
330         mAnimation = ValueAnimator.ofFloat(0.f, 1.f);
331         mAnimation.setDuration(DisplayImeController.ANIMATION_DURATION_SHOW_MS);
332         if (mTargetAdjusted != mAdjusted) {
333             final float fraction =
334                     ((float) mLastAdjustTop - mHiddenTop) / (mShownTop - mHiddenTop);
335             final float progress = mTargetAdjusted ? fraction : 1.f - fraction;
336             mAnimation.setCurrentFraction(progress);
337         }
338 
339         mAnimation.addUpdateListener(animation -> {
340             SurfaceControl.Transaction t = mTransactionPool.acquire();
341             float value = (float) animation.getAnimatedValue();
342             onProgress(value, t);
343             t.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId());
344             t.apply();
345             mTransactionPool.release(t);
346         });
347         mAnimation.setInterpolator(DisplayImeController.INTERPOLATOR);
348         mAnimation.addListener(new AnimatorListenerAdapter() {
349             private boolean mCancel = false;
350 
351             @Override
352             public void onAnimationCancel(Animator animation) {
353                 mCancel = true;
354             }
355 
356             @Override
357             public void onAnimationEnd(Animator animation) {
358                 SurfaceControl.Transaction t = mTransactionPool.acquire();
359                 onEnd(mCancel, t);
360                 t.apply();
361                 mTransactionPool.release(t);
362                 mAnimation = null;
363             }
364         });
365         mAnimation.start();
366     }
367 
dumpState()368     private String dumpState() {
369         return "top:" + mHiddenTop + "->" + mShownTop
370                 + " adj:" + mAdjusted + "->" + mTargetAdjusted + "(" + mLastAdjustTop + ")"
371                 + " shw:" + mImeWasShown + "->" + mTargetShown
372                 + " dims:" + mLastPrimaryDim + "," + mLastSecondaryDim
373                 + "->" + mTargetPrimaryDim + "," + mTargetSecondaryDim
374                 + " shf:" + mSecondaryHasFocus + " desync:" + (mAnimation != null)
375                 + " paus:" + mPaused + "[" + mPausedTargetAdjusted + "]";
376     }
377 
378     /** Completely aborts/resets adjustment state */
pause(int displayId)379     public void pause(int displayId) {
380         if (DEBUG) Slog.d(TAG, "ime pause posting " + dumpState());
381         mMainExecutor.execute(() -> {
382             if (DEBUG) Slog.d(TAG, "ime pause run posted " + dumpState());
383             if (mPaused) {
384                 return;
385             }
386             mPaused = true;
387             mPausedTargetAdjusted = mTargetAdjusted;
388             mTargetAdjusted = false;
389             mTargetPrimaryDim = mTargetSecondaryDim = 0.f;
390             updateImeAdjustState();
391             startAsyncAnimation();
392             if (mAnimation != null) {
393                 mAnimation.end();
394             }
395         });
396     }
397 
resume(int displayId)398     public void resume(int displayId) {
399         if (DEBUG) Slog.d(TAG, "ime resume posting " + dumpState());
400         mMainExecutor.execute(() -> {
401             if (DEBUG) Slog.d(TAG, "ime resume run posted " + dumpState());
402             if (!mPaused) {
403                 return;
404             }
405             mPaused = false;
406             mTargetAdjusted = mPausedTargetAdjusted;
407             updateDimTargets();
408             final DividerView view = getView();
409             if ((mTargetAdjusted != mAdjusted) && !mSplits.mSplitScreenController.isMinimized()
410                     && view != null) {
411                 // End unminimize animations since they conflict with adjustment animations.
412                 view.finishAnimations();
413             }
414             updateImeAdjustState();
415             startAsyncAnimation();
416         });
417     }
418 }
419