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