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.systemui.stackdivider;
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.graphics.Rect;
26 import android.os.Handler;
27 import android.util.Slog;
28 import android.view.SurfaceControl;
29 import android.window.TaskOrganizer;
30 import android.window.WindowContainerToken;
31 import android.window.WindowContainerTransaction;
32 import android.window.WindowOrganizer;
33 
34 import androidx.annotation.Nullable;
35 
36 import com.android.systemui.TransactionPool;
37 import com.android.systemui.wm.DisplayImeController;
38 
39 class DividerImeController implements DisplayImeController.ImePositionProcessor {
40     private static final String TAG = "DividerImeController";
41     private static final boolean DEBUG = Divider.DEBUG;
42 
43     private static final float ADJUSTED_NONFOCUS_DIM = 0.3f;
44 
45     private final SplitScreenTaskOrganizer mSplits;
46     private final TransactionPool mTransactionPool;
47     private final Handler mHandler;
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     private boolean mAdjustedWhileHidden = false;
95 
DividerImeController(SplitScreenTaskOrganizer splits, TransactionPool pool, Handler handler)96     DividerImeController(SplitScreenTaskOrganizer splits, TransactionPool pool, Handler handler) {
97         mSplits = splits;
98         mTransactionPool = pool;
99         mHandler = handler;
100     }
101 
getView()102     private DividerView getView() {
103         return mSplits.mDivider.getView();
104     }
105 
getLayout()106     private SplitDisplayLayout getLayout() {
107         return mSplits.mDivider.getSplitLayout();
108     }
109 
isDividerVisible()110     private boolean isDividerVisible() {
111         return mSplits.mDivider.isDividerVisible();
112     }
113 
getSecondaryHasFocus(int displayId)114     private boolean getSecondaryHasFocus(int displayId) {
115         WindowContainerToken imeSplit = TaskOrganizer.getImeTarget(displayId);
116         return imeSplit != null
117                 && (imeSplit.asBinder() == mSplits.mSecondary.token.asBinder());
118     }
119 
reset()120     void reset() {
121         mPaused = true;
122         mPausedTargetAdjusted = false;
123         mAdjustedWhileHidden = false;
124         mAnimation = null;
125         mAdjusted = mTargetAdjusted = false;
126         mImeWasShown = mTargetShown = false;
127         mTargetPrimaryDim = mTargetSecondaryDim = mLastPrimaryDim = mLastSecondaryDim = 0.f;
128         mSecondaryHasFocus = false;
129         mLastAdjustTop = -1;
130     }
131 
updateDimTargets()132     private void updateDimTargets() {
133         final boolean splitIsVisible = !getView().isHidden();
134         mTargetPrimaryDim = (mSecondaryHasFocus && mTargetShown && splitIsVisible)
135                 ? ADJUSTED_NONFOCUS_DIM : 0.f;
136         mTargetSecondaryDim = (!mSecondaryHasFocus && mTargetShown && splitIsVisible)
137                 ? ADJUSTED_NONFOCUS_DIM : 0.f;
138     }
139 
140     @Override
141     @ImeAnimationFlags
onImeStartPositioning(int displayId, int hiddenTop, int shownTop, boolean imeShouldShow, boolean imeIsFloating, SurfaceControl.Transaction t)142     public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop,
143             boolean imeShouldShow, boolean imeIsFloating, SurfaceControl.Transaction t) {
144         mHiddenTop = hiddenTop;
145         mShownTop = shownTop;
146         mTargetShown = imeShouldShow;
147         if (!isDividerVisible()) {
148             return 0;
149         }
150         final boolean splitIsVisible = !getView().isHidden();
151         mSecondaryHasFocus = getSecondaryHasFocus(displayId);
152         final boolean targetAdjusted = splitIsVisible && imeShouldShow && mSecondaryHasFocus
153                 && !imeIsFloating && !getLayout().mDisplayLayout.isLandscape()
154                 && !mSplits.mDivider.isMinimized();
155         if (mLastAdjustTop < 0) {
156             mLastAdjustTop = imeShouldShow ? hiddenTop : shownTop;
157         } else if (mLastAdjustTop != (imeShouldShow ? mShownTop : mHiddenTop)) {
158             if (mTargetAdjusted != targetAdjusted && targetAdjusted == mAdjusted) {
159                 // Check for an "interruption" of an existing animation. In this case, we
160                 // need to fake-flip the last-known state direction so that the animation
161                 // completes in the other direction.
162                 mAdjusted = mTargetAdjusted;
163             } else if (targetAdjusted && mTargetAdjusted && mAdjusted) {
164                 // Already fully adjusted for IME, but IME height has changed; so, force-start
165                 // an async animation to the new IME height.
166                 mAdjusted = false;
167             }
168         }
169         if (mPaused) {
170             mPausedTargetAdjusted = targetAdjusted;
171             if (DEBUG) Slog.d(TAG, " ime starting but paused " + dumpState());
172             return (targetAdjusted || mAdjusted) ? IME_ANIMATION_NO_ALPHA : 0;
173         }
174         mTargetAdjusted = targetAdjusted;
175         updateDimTargets();
176         if (DEBUG) Slog.d(TAG, " ime starting. vis:" + splitIsVisible + "  " + dumpState());
177         if (mAnimation != null || (mImeWasShown && imeShouldShow
178                 && mTargetAdjusted != mAdjusted)) {
179             // We need to animate adjustment independently of the IME position, so
180             // start our own animation to drive adjustment. This happens when a
181             // different split's editor has gained focus while the IME is still visible.
182             startAsyncAnimation();
183         }
184         if (splitIsVisible) {
185             // If split is hidden, we don't want to trigger any relayouts that would cause the
186             // divider to show again.
187             updateImeAdjustState();
188         } else {
189             mAdjustedWhileHidden = true;
190         }
191         return (mTargetAdjusted || mAdjusted) ? IME_ANIMATION_NO_ALPHA : 0;
192     }
193 
updateImeAdjustState()194     private void updateImeAdjustState() {
195         updateImeAdjustState(false /* force */);
196     }
197 
updateImeAdjustState(boolean force)198     private void updateImeAdjustState(boolean force) {
199         if (mAdjusted != mTargetAdjusted || force) {
200             // Reposition the server's secondary split position so that it evaluates
201             // insets properly.
202             WindowContainerTransaction wct = new WindowContainerTransaction();
203             final SplitDisplayLayout splitLayout = getLayout();
204             if (mTargetAdjusted) {
205                 splitLayout.updateAdjustedBounds(mShownTop, mHiddenTop, mShownTop);
206                 wct.setBounds(mSplits.mSecondary.token, splitLayout.mAdjustedSecondary);
207                 // "Freeze" the configuration size so that the app doesn't get a config
208                 // or relaunch. This is required because normally nav-bar contributes
209                 // to configuration bounds (via nondecorframe).
210                 Rect adjustAppBounds = new Rect(mSplits.mSecondary.configuration
211                         .windowConfiguration.getAppBounds());
212                 adjustAppBounds.offset(0, splitLayout.mAdjustedSecondary.top
213                         - splitLayout.mSecondary.top);
214                 wct.setAppBounds(mSplits.mSecondary.token, adjustAppBounds);
215                 wct.setScreenSizeDp(mSplits.mSecondary.token,
216                         mSplits.mSecondary.configuration.screenWidthDp,
217                         mSplits.mSecondary.configuration.screenHeightDp);
218 
219                 wct.setBounds(mSplits.mPrimary.token, splitLayout.mAdjustedPrimary);
220                 adjustAppBounds = new Rect(mSplits.mPrimary.configuration
221                         .windowConfiguration.getAppBounds());
222                 adjustAppBounds.offset(0, splitLayout.mAdjustedPrimary.top
223                         - splitLayout.mPrimary.top);
224                 wct.setAppBounds(mSplits.mPrimary.token, adjustAppBounds);
225                 wct.setScreenSizeDp(mSplits.mPrimary.token,
226                         mSplits.mPrimary.configuration.screenWidthDp,
227                         mSplits.mPrimary.configuration.screenHeightDp);
228             } else {
229                 wct.setBounds(mSplits.mSecondary.token, splitLayout.mSecondary);
230                 wct.setAppBounds(mSplits.mSecondary.token, null);
231                 wct.setScreenSizeDp(mSplits.mSecondary.token,
232                         SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
233                 wct.setBounds(mSplits.mPrimary.token, splitLayout.mPrimary);
234                 wct.setAppBounds(mSplits.mPrimary.token, null);
235                 wct.setScreenSizeDp(mSplits.mPrimary.token,
236                         SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
237             }
238 
239             if (!mSplits.mDivider.getWmProxy().queueSyncTransactionIfWaiting(wct)) {
240                 WindowOrganizer.applyTransaction(wct);
241             }
242         }
243 
244         // Update all the adjusted-for-ime states
245         if (!mPaused) {
246             final DividerView view = getView();
247             if (view != null) {
248                 view.setAdjustedForIme(mTargetShown, mTargetShown
249                         ? DisplayImeController.ANIMATION_DURATION_SHOW_MS
250                         : DisplayImeController.ANIMATION_DURATION_HIDE_MS);
251             }
252         }
253         mSplits.mDivider.setAdjustedForIme(mTargetShown && !mPaused);
254     }
255 
updateAdjustForIme()256     public void updateAdjustForIme() {
257         updateImeAdjustState(mAdjustedWhileHidden);
258         mAdjustedWhileHidden = false;
259     }
260 
261     @Override
onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t)262     public void onImePositionChanged(int displayId, int imeTop,
263             SurfaceControl.Transaction t) {
264         if (mAnimation != null || !isDividerVisible() || mPaused) {
265             // Not synchronized with IME anymore, so return.
266             return;
267         }
268         final float fraction = ((float) imeTop - mHiddenTop) / (mShownTop - mHiddenTop);
269         final float progress = mTargetShown ? fraction : 1.f - fraction;
270         onProgress(progress, t);
271     }
272 
273     @Override
onImeEndPositioning(int displayId, boolean cancelled, SurfaceControl.Transaction t)274     public void onImeEndPositioning(int displayId, boolean cancelled,
275             SurfaceControl.Transaction t) {
276         if (mAnimation != null || !isDividerVisible() || mPaused) {
277             // Not synchronized with IME anymore, so return.
278             return;
279         }
280         onEnd(cancelled, t);
281     }
282 
onProgress(float progress, SurfaceControl.Transaction t)283     private void onProgress(float progress, SurfaceControl.Transaction t) {
284         final DividerView view = getView();
285         if (mTargetAdjusted != mAdjusted && !mPaused) {
286             final SplitDisplayLayout splitLayout = getLayout();
287             final float fraction = mTargetAdjusted ? progress : 1.f - progress;
288             mLastAdjustTop = (int) (fraction * mShownTop + (1.f - fraction) * mHiddenTop);
289             splitLayout.updateAdjustedBounds(mLastAdjustTop, mHiddenTop, mShownTop);
290             view.resizeSplitSurfaces(t, splitLayout.mAdjustedPrimary,
291                     splitLayout.mAdjustedSecondary);
292         }
293         final float invProg = 1.f - progress;
294         view.setResizeDimLayer(t, true /* primary */,
295                 mLastPrimaryDim * invProg + progress * mTargetPrimaryDim);
296         view.setResizeDimLayer(t, false /* primary */,
297                 mLastSecondaryDim * invProg + progress * mTargetSecondaryDim);
298     }
299 
setDimsHidden(SurfaceControl.Transaction t, boolean hidden)300     void setDimsHidden(SurfaceControl.Transaction t, boolean hidden) {
301         final DividerView view = getView();
302         if (hidden) {
303             view.setResizeDimLayer(t, true /* primary */, 0.f /* alpha */);
304             view.setResizeDimLayer(t, false /* primary */, 0.f /* alpha */);
305         } else {
306             updateDimTargets();
307             view.setResizeDimLayer(t, true /* primary */, mTargetPrimaryDim);
308             view.setResizeDimLayer(t, false /* primary */, mTargetSecondaryDim);
309         }
310     }
311 
onEnd(boolean cancelled, SurfaceControl.Transaction t)312     private void onEnd(boolean cancelled, SurfaceControl.Transaction t) {
313         if (!cancelled) {
314             onProgress(1.f, t);
315             mAdjusted = mTargetAdjusted;
316             mImeWasShown = mTargetShown;
317             mLastAdjustTop = mAdjusted ? mShownTop : mHiddenTop;
318             mLastPrimaryDim = mTargetPrimaryDim;
319             mLastSecondaryDim = mTargetSecondaryDim;
320         }
321     }
322 
startAsyncAnimation()323     private void startAsyncAnimation() {
324         if (mAnimation != null) {
325             mAnimation.cancel();
326         }
327         mAnimation = ValueAnimator.ofFloat(0.f, 1.f);
328         mAnimation.setDuration(DisplayImeController.ANIMATION_DURATION_SHOW_MS);
329         if (mTargetAdjusted != mAdjusted) {
330             final float fraction =
331                     ((float) mLastAdjustTop - mHiddenTop) / (mShownTop - mHiddenTop);
332             final float progress = mTargetAdjusted ? fraction : 1.f - fraction;
333             mAnimation.setCurrentFraction(progress);
334         }
335 
336         mAnimation.addUpdateListener(animation -> {
337             SurfaceControl.Transaction t = mTransactionPool.acquire();
338             float value = (float) animation.getAnimatedValue();
339             onProgress(value, t);
340             t.apply();
341             mTransactionPool.release(t);
342         });
343         mAnimation.setInterpolator(DisplayImeController.INTERPOLATOR);
344         mAnimation.addListener(new AnimatorListenerAdapter() {
345             private boolean mCancel = false;
346             @Override
347             public void onAnimationCancel(Animator animation) {
348                 mCancel = true;
349             }
350             @Override
351             public void onAnimationEnd(Animator animation) {
352                 SurfaceControl.Transaction t = mTransactionPool.acquire();
353                 onEnd(mCancel, t);
354                 t.apply();
355                 mTransactionPool.release(t);
356                 mAnimation = null;
357             }
358         });
359         mAnimation.start();
360     }
361 
dumpState()362     private String dumpState() {
363         return "top:" + mHiddenTop + "->" + mShownTop
364                 + " adj:" + mAdjusted + "->" + mTargetAdjusted + "(" + mLastAdjustTop + ")"
365                 + " shw:" + mImeWasShown + "->" + mTargetShown
366                 + " dims:" + mLastPrimaryDim + "," + mLastSecondaryDim
367                 + "->" + mTargetPrimaryDim + "," + mTargetSecondaryDim
368                 + " shf:" + mSecondaryHasFocus + " desync:" + (mAnimation != null)
369                 + " paus:" + mPaused + "[" + mPausedTargetAdjusted + "]";
370     }
371 
372     /** Completely aborts/resets adjustment state */
pause(int displayId)373     public void pause(int displayId) {
374         if (DEBUG) Slog.d(TAG, "ime pause posting " + dumpState());
375         mHandler.post(() -> {
376             if (DEBUG) Slog.d(TAG, "ime pause run posted " + dumpState());
377             if (mPaused) {
378                 return;
379             }
380             mPaused = true;
381             mPausedTargetAdjusted = mTargetAdjusted;
382             mTargetAdjusted = false;
383             mTargetPrimaryDim = mTargetSecondaryDim = 0.f;
384             updateImeAdjustState();
385             startAsyncAnimation();
386             if (mAnimation != null) {
387                 mAnimation.end();
388             }
389         });
390     }
391 
resume(int displayId)392     public void resume(int displayId) {
393         if (DEBUG) Slog.d(TAG, "ime resume posting " + dumpState());
394         mHandler.post(() -> {
395             if (DEBUG) Slog.d(TAG, "ime resume run posted " + dumpState());
396             if (!mPaused) {
397                 return;
398             }
399             mPaused = false;
400             mTargetAdjusted = mPausedTargetAdjusted;
401             updateDimTargets();
402             final DividerView view = getView();
403             if ((mTargetAdjusted != mAdjusted) && !mSplits.mDivider.isMinimized() && view != null) {
404                 // End unminimize animations since they conflict with adjustment animations.
405                 view.finishAnimations();
406             }
407             updateImeAdjustState();
408             startAsyncAnimation();
409         });
410     }
411 }
412