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.view.WindowManager.DOCKED_LEFT; 20 import static android.view.WindowManager.DOCKED_TOP; 21 22 import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END; 23 import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START; 24 25 import android.animation.Animator; 26 import android.animation.AnimatorListenerAdapter; 27 import android.animation.ValueAnimator; 28 import android.annotation.IntDef; 29 import android.app.ActivityManager; 30 import android.content.Context; 31 import android.content.res.Configuration; 32 import android.content.res.Resources; 33 import android.graphics.Rect; 34 import android.view.SurfaceControl; 35 import android.view.WindowInsets; 36 import android.view.WindowManager; 37 import android.window.WindowContainerToken; 38 import android.window.WindowContainerTransaction; 39 40 import androidx.annotation.Nullable; 41 42 import com.android.internal.policy.DividerSnapAlgorithm; 43 import com.android.wm.shell.ShellTaskOrganizer; 44 import com.android.wm.shell.animation.Interpolators; 45 import com.android.wm.shell.common.DisplayImeController; 46 47 /** 48 * Records and handles layout of splits. Helps to calculate proper bounds when configuration or 49 * divide position changes. 50 */ 51 public final class SplitLayout { 52 /** 53 * Split position isn't specified normally meaning to use what ever it is currently set to. 54 */ 55 public static final int SPLIT_POSITION_UNDEFINED = -1; 56 57 /** 58 * Specifies that a split is positioned at the top half of the screen if 59 * in portrait mode or at the left half of the screen if in landscape mode. 60 */ 61 public static final int SPLIT_POSITION_TOP_OR_LEFT = 0; 62 63 /** 64 * Specifies that a split is positioned at the bottom half of the screen if 65 * in portrait mode or at the right half of the screen if in landscape mode. 66 */ 67 public static final int SPLIT_POSITION_BOTTOM_OR_RIGHT = 1; 68 69 @IntDef(prefix = {"SPLIT_POSITION_"}, value = { 70 SPLIT_POSITION_UNDEFINED, 71 SPLIT_POSITION_TOP_OR_LEFT, 72 SPLIT_POSITION_BOTTOM_OR_RIGHT 73 }) 74 public @interface SplitPosition { 75 } 76 77 private final int mDividerWindowWidth; 78 private final int mDividerInsets; 79 private final int mDividerSize; 80 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 SplitLayoutHandler mSplitLayoutHandler; 86 private final SplitWindowManager mSplitWindowManager; 87 private final DisplayImeController mDisplayImeController; 88 private final ImePositionProcessor mImePositionProcessor; 89 private final ShellTaskOrganizer mTaskOrganizer; 90 91 private Context mContext; 92 private DividerSnapAlgorithm mDividerSnapAlgorithm; 93 private int mDividePosition; 94 private boolean mInitialized = false; 95 SplitLayout(String windowName, Context context, Configuration configuration, SplitLayoutHandler splitLayoutHandler, SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks, DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer)96 public SplitLayout(String windowName, Context context, Configuration configuration, 97 SplitLayoutHandler splitLayoutHandler, 98 SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks, 99 DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer) { 100 mContext = context.createConfigurationContext(configuration); 101 mSplitLayoutHandler = splitLayoutHandler; 102 mDisplayImeController = displayImeController; 103 mSplitWindowManager = new SplitWindowManager( 104 windowName, mContext, configuration, parentContainerCallbacks); 105 mTaskOrganizer = taskOrganizer; 106 mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId()); 107 108 final Resources resources = context.getResources(); 109 mDividerWindowWidth = resources.getDimensionPixelSize( 110 com.android.internal.R.dimen.docked_stack_divider_thickness); 111 mDividerInsets = resources.getDimensionPixelSize( 112 com.android.internal.R.dimen.docked_stack_divider_insets); 113 mDividerSize = mDividerWindowWidth - mDividerInsets * 2; 114 115 mRootBounds.set(configuration.windowConfiguration.getBounds()); 116 mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); 117 resetDividerPosition(); 118 } 119 120 /** Gets bounds of the primary split. */ getBounds1()121 public Rect getBounds1() { 122 return new Rect(mBounds1); 123 } 124 125 /** Gets bounds of the secondary split. */ getBounds2()126 public Rect getBounds2() { 127 return new Rect(mBounds2); 128 } 129 130 /** Gets bounds of divider window. */ getDividerBounds()131 public Rect getDividerBounds() { 132 return new Rect(mDividerBounds); 133 } 134 135 /** Returns leash of the current divider bar. */ 136 @Nullable getDividerLeash()137 public SurfaceControl getDividerLeash() { 138 return mSplitWindowManager == null ? null : mSplitWindowManager.getSurfaceControl(); 139 } 140 getDividePosition()141 int getDividePosition() { 142 return mDividePosition; 143 } 144 145 /** Applies new configuration, returns {@code false} if there's no effect to the layout. */ updateConfiguration(Configuration configuration)146 public boolean updateConfiguration(Configuration configuration) { 147 final Rect rootBounds = configuration.windowConfiguration.getBounds(); 148 if (mRootBounds.equals(rootBounds)) { 149 return false; 150 } 151 152 mContext = mContext.createConfigurationContext(configuration); 153 mSplitWindowManager.setConfiguration(configuration); 154 mRootBounds.set(rootBounds); 155 mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); 156 resetDividerPosition(); 157 158 // Don't inflate divider bar if it is not initialized. 159 if (!mInitialized) { 160 return false; 161 } 162 163 release(); 164 init(); 165 return true; 166 } 167 168 /** Updates recording bounds of divider window and both of the splits. */ updateBounds(int position)169 private void updateBounds(int position) { 170 mDividerBounds.set(mRootBounds); 171 mBounds1.set(mRootBounds); 172 mBounds2.set(mRootBounds); 173 if (isLandscape(mRootBounds)) { 174 position += mRootBounds.left; 175 mDividerBounds.left = position - mDividerInsets; 176 mDividerBounds.right = mDividerBounds.left + mDividerWindowWidth; 177 mBounds1.right = position; 178 mBounds2.left = mBounds1.right + mDividerSize; 179 } else { 180 position += mRootBounds.top; 181 mDividerBounds.top = position - mDividerInsets; 182 mDividerBounds.bottom = mDividerBounds.top + mDividerWindowWidth; 183 mBounds1.bottom = position; 184 mBounds2.top = mBounds1.bottom + mDividerSize; 185 } 186 } 187 188 /** Inflates {@link DividerView} on the root surface. */ init()189 public void init() { 190 if (mInitialized) return; 191 mInitialized = true; 192 mSplitWindowManager.init(this); 193 mDisplayImeController.addPositionProcessor(mImePositionProcessor); 194 } 195 196 /** Releases the surface holding the current {@link DividerView}. */ release()197 public void release() { 198 if (!mInitialized) return; 199 mInitialized = false; 200 mSplitWindowManager.release(); 201 mDisplayImeController.removePositionProcessor(mImePositionProcessor); 202 mImePositionProcessor.reset(); 203 } 204 205 /** 206 * Updates bounds with the passing position. Usually used to update recording bounds while 207 * performing animation or dragging divider bar to resize the splits. 208 */ updateDivideBounds(int position)209 void updateDivideBounds(int position) { 210 updateBounds(position); 211 mSplitWindowManager.setResizingSplits(true); 212 mSplitLayoutHandler.onBoundsChanging(this); 213 } 214 setDividePosition(int position)215 void setDividePosition(int position) { 216 mDividePosition = position; 217 updateBounds(mDividePosition); 218 mSplitLayoutHandler.onBoundsChanged(this); 219 mSplitWindowManager.setResizingSplits(false); 220 } 221 222 /** Resets divider position. */ resetDividerPosition()223 public void resetDividerPosition() { 224 mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position; 225 updateBounds(mDividePosition); 226 } 227 228 /** 229 * Sets new divide position and updates bounds correspondingly. Notifies listener if the new 230 * target indicates dismissing split. 231 */ snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget)232 public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) { 233 switch (snapTarget.flag) { 234 case FLAG_DISMISS_START: 235 mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */); 236 mSplitWindowManager.setResizingSplits(false); 237 break; 238 case FLAG_DISMISS_END: 239 mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */); 240 mSplitWindowManager.setResizingSplits(false); 241 break; 242 default: 243 flingDividePosition(currentPosition, snapTarget.position); 244 break; 245 } 246 } 247 onDoubleTappedDivider()248 void onDoubleTappedDivider() { 249 mSplitLayoutHandler.onDoubleTappedDivider(); 250 } 251 252 /** 253 * Returns {@link DividerSnapAlgorithm.SnapTarget} which matches passing position and velocity. 254 * If hardDismiss is set to {@code true}, it will be harder to reach dismiss target. 255 */ findSnapTarget(int position, float velocity, boolean hardDismiss)256 public DividerSnapAlgorithm.SnapTarget findSnapTarget(int position, float velocity, 257 boolean hardDismiss) { 258 return mDividerSnapAlgorithm.calculateSnapTarget(position, velocity, hardDismiss); 259 } 260 getSnapAlgorithm(Context context, Rect rootBounds)261 private DividerSnapAlgorithm getSnapAlgorithm(Context context, Rect rootBounds) { 262 final boolean isLandscape = isLandscape(rootBounds); 263 return new DividerSnapAlgorithm( 264 context.getResources(), 265 rootBounds.width(), 266 rootBounds.height(), 267 mDividerSize, 268 !isLandscape, 269 getDisplayInsets(context), 270 isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */); 271 } 272 flingDividePosition(int from, int to)273 private void flingDividePosition(int from, int to) { 274 if (from == to) return; 275 ValueAnimator animator = ValueAnimator 276 .ofInt(from, to) 277 .setDuration(250); 278 animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 279 animator.addUpdateListener( 280 animation -> updateDivideBounds((int) animation.getAnimatedValue())); 281 animator.addListener(new AnimatorListenerAdapter() { 282 @Override 283 public void onAnimationEnd(Animator animation) { 284 setDividePosition(to); 285 } 286 287 @Override 288 public void onAnimationCancel(Animator animation) { 289 setDividePosition(to); 290 } 291 }); 292 animator.start(); 293 } 294 getDisplayInsets(Context context)295 private static Rect getDisplayInsets(Context context) { 296 return context.getSystemService(WindowManager.class) 297 .getMaximumWindowMetrics() 298 .getWindowInsets() 299 .getInsets(WindowInsets.Type.navigationBars() 300 | WindowInsets.Type.statusBars() 301 | WindowInsets.Type.displayCutout()).toRect(); 302 } 303 isLandscape(Rect bounds)304 private static boolean isLandscape(Rect bounds) { 305 return bounds.width() > bounds.height(); 306 } 307 308 /** Apply recorded surface layout to the {@link SurfaceControl.Transaction}. */ applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2)309 public void applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1, 310 SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2) { 311 final Rect dividerBounds = mImePositionProcessor.adjustForIme(mDividerBounds); 312 final Rect bounds1 = mImePositionProcessor.adjustForIme(mBounds1); 313 final Rect bounds2 = mImePositionProcessor.adjustForIme(mBounds2); 314 final SurfaceControl dividerLeash = getDividerLeash(); 315 if (dividerLeash != null) { 316 t.setPosition(dividerLeash, dividerBounds.left, dividerBounds.top) 317 // Resets layer of divider bar to make sure it is always on top. 318 .setLayer(dividerLeash, Integer.MAX_VALUE); 319 } 320 321 t.setPosition(leash1, bounds1.left, bounds1.top) 322 .setWindowCrop(leash1, bounds1.width(), bounds1.height()); 323 324 t.setPosition(leash2, bounds2.left, bounds2.top) 325 .setWindowCrop(leash2, bounds2.width(), bounds2.height()); 326 327 mImePositionProcessor.applySurfaceDimValues(t, dimLayer1, dimLayer2); 328 } 329 330 /** Apply recorded task layout to the {@link WindowContainerTransaction}. */ applyTaskChanges(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2)331 public void applyTaskChanges(WindowContainerTransaction wct, 332 ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) { 333 wct.setBounds(task1.token, mImePositionProcessor.adjustForIme(mBounds1)) 334 .setBounds(task2.token, mImePositionProcessor.adjustForIme(mBounds2)); 335 } 336 337 /** Handles layout change event. */ 338 public interface SplitLayoutHandler { 339 340 /** Calls when dismissing split. */ onSnappedToDismiss(boolean snappedToEnd)341 void onSnappedToDismiss(boolean snappedToEnd); 342 343 /** Calls when the bounds is changing due to animation or dragging divider bar. */ onBoundsChanging(SplitLayout layout)344 void onBoundsChanging(SplitLayout layout); 345 346 /** Calls when the target bounds changed. */ onBoundsChanged(SplitLayout layout)347 void onBoundsChanged(SplitLayout layout); 348 349 /** Calls when user double tapped on the divider bar. */ onDoubleTappedDivider()350 default void onDoubleTappedDivider() { 351 } 352 353 /** Returns split position of the token. */ 354 @SplitPosition getSplitItemPosition(WindowContainerToken token)355 int getSplitItemPosition(WindowContainerToken token); 356 } 357 358 /** Records IME top offset changes and updates SplitLayout correspondingly. */ 359 private class ImePositionProcessor implements DisplayImeController.ImePositionProcessor { 360 /** 361 * Maximum size of an adjusted split bounds relative to original stack bounds. Used to 362 * restrict IME adjustment so that a min portion of top split remains visible. 363 */ 364 private static final float ADJUSTED_SPLIT_FRACTION_MAX = 0.7f; 365 private static final float ADJUSTED_NONFOCUS_DIM = 0.3f; 366 367 private final int mDisplayId; 368 369 private boolean mImeShown; 370 private int mYOffsetForIme; 371 private float mDimValue1; 372 private float mDimValue2; 373 374 private int mStartImeTop; 375 private int mEndImeTop; 376 377 private int mTargetYOffset; 378 private int mLastYOffset; 379 private float mTargetDim1; 380 private float mTargetDim2; 381 private float mLastDim1; 382 private float mLastDim2; 383 ImePositionProcessor(int displayId)384 private ImePositionProcessor(int displayId) { 385 mDisplayId = displayId; 386 } 387 388 @Override onImeStartPositioning(int displayId, int hiddenTop, int shownTop, boolean showing, boolean isFloating, SurfaceControl.Transaction t)389 public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop, 390 boolean showing, boolean isFloating, SurfaceControl.Transaction t) { 391 if (displayId != mDisplayId) return 0; 392 final int imeTargetPosition = getImeTargetPosition(); 393 if (!mInitialized || imeTargetPosition == SPLIT_POSITION_UNDEFINED) return 0; 394 mStartImeTop = showing ? hiddenTop : shownTop; 395 mEndImeTop = showing ? shownTop : hiddenTop; 396 mImeShown = showing; 397 398 // Update target dim values 399 mLastDim1 = mDimValue1; 400 mTargetDim1 = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT && showing 401 ? ADJUSTED_NONFOCUS_DIM : 0.0f; 402 mLastDim2 = mDimValue2; 403 mTargetDim2 = imeTargetPosition == SPLIT_POSITION_TOP_OR_LEFT && showing 404 ? ADJUSTED_NONFOCUS_DIM : 0.0f; 405 406 // Calculate target bounds offset for IME 407 mLastYOffset = mYOffsetForIme; 408 final boolean needOffset = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT 409 && !isFloating && !isLandscape(mRootBounds) && showing; 410 mTargetYOffset = needOffset ? getTargetYOffset() : 0; 411 412 // Make {@link DividerView} non-interactive while IME showing in split mode. Listen to 413 // ImePositionProcessor#onImeVisibilityChanged directly in DividerView is not enough 414 // because DividerView won't receive onImeVisibilityChanged callback after it being 415 // re-inflated. 416 mSplitWindowManager.setInteractive( 417 !showing || imeTargetPosition == SPLIT_POSITION_UNDEFINED); 418 419 return needOffset ? IME_ANIMATION_NO_ALPHA : 0; 420 } 421 422 @Override onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t)423 public void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) { 424 if (displayId != mDisplayId) return; 425 onProgress(getProgress(imeTop)); 426 mSplitLayoutHandler.onBoundsChanging(SplitLayout.this); 427 } 428 429 @Override onImeEndPositioning(int displayId, boolean cancel, SurfaceControl.Transaction t)430 public void onImeEndPositioning(int displayId, boolean cancel, 431 SurfaceControl.Transaction t) { 432 if (displayId != mDisplayId || cancel) return; 433 onProgress(1.0f); 434 mSplitLayoutHandler.onBoundsChanging(SplitLayout.this); 435 } 436 437 @Override onImeControlTargetChanged(int displayId, boolean controlling)438 public void onImeControlTargetChanged(int displayId, boolean controlling) { 439 if (displayId != mDisplayId) return; 440 // Restore the split layout when wm-shell is not controlling IME insets anymore. 441 if (!controlling && mImeShown) { 442 reset(); 443 mSplitWindowManager.setInteractive(true); 444 mSplitLayoutHandler.onBoundsChanging(SplitLayout.this); 445 } 446 } 447 getTargetYOffset()448 private int getTargetYOffset() { 449 final int desireOffset = Math.abs(mEndImeTop - mStartImeTop); 450 // Make sure to keep at least 30% visible for the top split. 451 final int maxOffset = (int) (mBounds1.height() * ADJUSTED_SPLIT_FRACTION_MAX); 452 return -Math.min(desireOffset, maxOffset); 453 } 454 455 @SplitPosition getImeTargetPosition()456 private int getImeTargetPosition() { 457 final WindowContainerToken token = mTaskOrganizer.getImeTarget(mDisplayId); 458 return mSplitLayoutHandler.getSplitItemPosition(token); 459 } 460 getProgress(int currImeTop)461 private float getProgress(int currImeTop) { 462 return ((float) currImeTop - mStartImeTop) / (mEndImeTop - mStartImeTop); 463 } 464 onProgress(float progress)465 private void onProgress(float progress) { 466 mDimValue1 = getProgressValue(mLastDim1, mTargetDim1, progress); 467 mDimValue2 = getProgressValue(mLastDim2, mTargetDim2, progress); 468 mYOffsetForIme = 469 (int) getProgressValue((float) mLastYOffset, (float) mTargetYOffset, progress); 470 } 471 getProgressValue(float start, float end, float progress)472 private float getProgressValue(float start, float end, float progress) { 473 return start + (end - start) * progress; 474 } 475 reset()476 private void reset() { 477 mImeShown = false; 478 mYOffsetForIme = mLastYOffset = mTargetYOffset = 0; 479 mDimValue1 = mLastDim1 = mTargetDim1 = 0.0f; 480 mDimValue2 = mLastDim2 = mTargetDim2 = 0.0f; 481 } 482 483 /* Adjust bounds with IME offset. */ adjustForIme(Rect bounds)484 private Rect adjustForIme(Rect bounds) { 485 final Rect temp = new Rect(bounds); 486 if (mYOffsetForIme != 0) temp.offset(0, mYOffsetForIme); 487 return temp; 488 } 489 applySurfaceDimValues(SurfaceControl.Transaction t, SurfaceControl dimLayer1, SurfaceControl dimLayer2)490 private void applySurfaceDimValues(SurfaceControl.Transaction t, SurfaceControl dimLayer1, 491 SurfaceControl dimLayer2) { 492 t.setAlpha(dimLayer1, mDimValue1).setVisibility(dimLayer1, mDimValue1 > 0.001f); 493 t.setAlpha(dimLayer2, mDimValue2).setVisibility(dimLayer2, mDimValue2 > 0.001f); 494 } 495 } 496 } 497