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