1 /* 2 * Copyright (C) 2024 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 androidx.window.extensions.embedding; 18 19 import static android.content.pm.ActivityInfo.CONFIG_DENSITY; 20 import static android.content.pm.ActivityInfo.CONFIG_LAYOUT_DIRECTION; 21 import static android.content.pm.ActivityInfo.CONFIG_WINDOW_CONFIGURATION; 22 import static android.util.TypedValue.COMPLEX_UNIT_DIP; 23 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 24 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; 25 import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; 26 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; 27 import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; 28 import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE; 29 import static android.window.TaskFragmentOperation.OP_TYPE_SET_DECOR_SURFACE_BOOSTED; 30 31 import static androidx.window.extensions.embedding.DividerAttributes.RATIO_SYSTEM_DEFAULT; 32 import static androidx.window.extensions.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT; 33 import static androidx.window.extensions.embedding.SplitAttributesHelper.isReversedLayout; 34 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM; 35 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT; 36 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT; 37 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP; 38 39 import android.animation.Animator; 40 import android.animation.AnimatorListenerAdapter; 41 import android.animation.ValueAnimator; 42 import android.annotation.ColorInt; 43 import android.annotation.Nullable; 44 import android.app.Activity; 45 import android.app.ActivityThread; 46 import android.content.Context; 47 import android.content.res.Configuration; 48 import android.graphics.Color; 49 import android.graphics.PixelFormat; 50 import android.graphics.Rect; 51 import android.graphics.drawable.ColorDrawable; 52 import android.graphics.drawable.Drawable; 53 import android.graphics.drawable.RotateDrawable; 54 import android.hardware.display.DisplayManager; 55 import android.os.IBinder; 56 import android.util.TypedValue; 57 import android.view.Gravity; 58 import android.view.MotionEvent; 59 import android.view.SurfaceControl; 60 import android.view.SurfaceControlViewHost; 61 import android.view.VelocityTracker; 62 import android.view.View; 63 import android.view.WindowManager; 64 import android.view.WindowlessWindowManager; 65 import android.view.animation.PathInterpolator; 66 import android.widget.FrameLayout; 67 import android.widget.ImageButton; 68 import android.window.InputTransferToken; 69 import android.window.TaskFragmentOperation; 70 import android.window.TaskFragmentParentInfo; 71 import android.window.WindowContainerTransaction; 72 73 import androidx.annotation.GuardedBy; 74 import androidx.annotation.NonNull; 75 import androidx.window.extensions.core.util.function.Consumer; 76 import androidx.window.extensions.embedding.SplitAttributes.SplitType; 77 import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType; 78 import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType; 79 80 import com.android.internal.R; 81 import com.android.internal.annotations.VisibleForTesting; 82 import com.android.window.flags.Flags; 83 84 import java.util.Objects; 85 import java.util.concurrent.Executor; 86 87 /** 88 * Manages the rendering and interaction of the divider. 89 */ 90 class DividerPresenter implements View.OnTouchListener { 91 static final float RATIO_EXPANDED_PRIMARY = 1.0f; 92 static final float RATIO_EXPANDED_SECONDARY = 0.0f; 93 private static final String WINDOW_NAME = "AE Divider"; 94 private static final int VEIL_LAYER = 0; 95 private static final int DIVIDER_LAYER = 1; 96 97 // TODO(b/327067596) Update based on UX guidance. 98 private static final Color DEFAULT_PRIMARY_VEIL_COLOR = Color.valueOf(Color.BLACK); 99 private static final Color DEFAULT_SECONDARY_VEIL_COLOR = Color.valueOf(Color.GRAY); 100 @VisibleForTesting 101 static final float DEFAULT_MIN_RATIO = 0.35f; 102 @VisibleForTesting 103 static final float DEFAULT_MAX_RATIO = 0.65f; 104 @VisibleForTesting 105 static final int DEFAULT_DIVIDER_WIDTH_DP = 24; 106 107 @VisibleForTesting 108 static final PathInterpolator FLING_ANIMATION_INTERPOLATOR = 109 new PathInterpolator(0.4f, 0f, 0.2f, 1f); 110 @VisibleForTesting 111 static final int FLING_ANIMATION_DURATION = 250; 112 @VisibleForTesting 113 static final int MIN_DISMISS_VELOCITY_DP_PER_SECOND = 600; 114 @VisibleForTesting 115 static final int MIN_FLING_VELOCITY_DP_PER_SECOND = 400; 116 117 private final int mTaskId; 118 119 @NonNull 120 private final Object mLock = new Object(); 121 122 @NonNull 123 private final DragEventCallback mDragEventCallback; 124 125 @NonNull 126 private final Executor mCallbackExecutor; 127 128 /** 129 * The VelocityTracker of the divider, used to track the dragging velocity. This field is 130 * {@code null} until dragging starts. 131 */ 132 @GuardedBy("mLock") 133 @Nullable 134 VelocityTracker mVelocityTracker; 135 136 /** 137 * The {@link Properties} of the divider. This field is {@code null} when no divider should be 138 * drawn, e.g. when the split doesn't have {@link DividerAttributes} or when the decor surface 139 * is not available. 140 */ 141 @GuardedBy("mLock") 142 @Nullable 143 @VisibleForTesting 144 Properties mProperties; 145 146 /** 147 * The {@link Renderer} of the divider. This field is {@code null} when no divider should be 148 * drawn, i.e. when {@link #mProperties} is {@code null}. The {@link Renderer} is recreated or 149 * updated when {@link #mProperties} is changed. 150 */ 151 @GuardedBy("mLock") 152 @Nullable 153 @VisibleForTesting 154 Renderer mRenderer; 155 156 /** 157 * The owner TaskFragment token of the decor surface. The decor surface is placed right above 158 * the owner TaskFragment surface and is removed if the owner TaskFragment is destroyed. 159 */ 160 @GuardedBy("mLock") 161 @Nullable 162 @VisibleForTesting 163 IBinder mDecorSurfaceOwner; 164 165 /** 166 * The current divider position relative to the Task bounds. For vertical split (left-to-right 167 * or right-to-left), it is the x coordinate in the task window, and for horizontal split 168 * (top-to-bottom or bottom-to-top), it is the y coordinate in the task window. 169 */ 170 @GuardedBy("mLock") 171 private int mDividerPosition; 172 173 /** Indicates if there are containers to be finished since the divider has appeared. */ 174 @GuardedBy("mLock") 175 @VisibleForTesting 176 private boolean mHasContainersToFinish = false; 177 DividerPresenter(int taskId, @NonNull DragEventCallback dragEventCallback, @NonNull Executor callbackExecutor)178 DividerPresenter(int taskId, @NonNull DragEventCallback dragEventCallback, 179 @NonNull Executor callbackExecutor) { 180 mTaskId = taskId; 181 mDragEventCallback = dragEventCallback; 182 mCallbackExecutor = callbackExecutor; 183 } 184 185 /** Updates the divider when external conditions are changed. */ updateDivider( @onNull WindowContainerTransaction wct, @NonNull TaskFragmentParentInfo parentInfo, @Nullable SplitContainer topSplitContainer, boolean isTaskFragmentVanished)186 void updateDivider( 187 @NonNull WindowContainerTransaction wct, 188 @NonNull TaskFragmentParentInfo parentInfo, 189 @Nullable SplitContainer topSplitContainer, 190 boolean isTaskFragmentVanished) { 191 if (!Flags.activityEmbeddingInteractiveDividerFlag()) { 192 return; 193 } 194 195 synchronized (mLock) { 196 // Clean up the decor surface if top SplitContainer is null. 197 if (topSplitContainer == null) { 198 // Check if there are containers to finish but the TaskFragment hasn't vanished yet. 199 // Don't remove the decor surface and divider if so as the removal should happen in 200 // a following step when the TaskFragment has vanished. This ensures that the decor 201 // surface is removed only after the resulting Activity is ready to be shown, 202 // otherwise there may be flicker. 203 if (mHasContainersToFinish) { 204 if (isTaskFragmentVanished) { 205 setHasContainersToFinish(false); 206 } else { 207 return; 208 } 209 } 210 removeDecorSurfaceAndDivider(wct); 211 return; 212 } 213 214 final SplitAttributes splitAttributes = topSplitContainer.getCurrentSplitAttributes(); 215 final DividerAttributes dividerAttributes = splitAttributes.getDividerAttributes(); 216 217 // Clean up the decor surface if DividerAttributes is null. 218 if (dividerAttributes == null) { 219 removeDecorSurfaceAndDivider(wct); 220 return; 221 } 222 223 // At this point, a divider is required. 224 final TaskFragmentContainer primaryContainer = 225 topSplitContainer.getPrimaryContainer(); 226 final TaskFragmentContainer secondaryContainer = 227 topSplitContainer.getSecondaryContainer(); 228 229 // Create the decor surface if one is not available yet. 230 final SurfaceControl decorSurface = parentInfo.getDecorSurface(); 231 if (decorSurface == null) { 232 // Clean up when the decor surface is currently unavailable. 233 removeDivider(); 234 // Request to create the decor surface 235 createOrMoveDecorSurfaceLocked(wct, primaryContainer); 236 return; 237 } 238 239 // Update the decor surface owner if needed. 240 boolean isDraggableExpandType = 241 SplitAttributesHelper.isDraggableExpandType(splitAttributes); 242 final TaskFragmentContainer decorSurfaceOwnerContainer = 243 isDraggableExpandType ? secondaryContainer : primaryContainer; 244 245 if (!Objects.equals( 246 mDecorSurfaceOwner, decorSurfaceOwnerContainer.getTaskFragmentToken())) { 247 createOrMoveDecorSurfaceLocked(wct, decorSurfaceOwnerContainer); 248 } 249 250 final Configuration parentConfiguration = parentInfo.getConfiguration(); 251 final Rect taskBounds = parentConfiguration.windowConfiguration.getBounds(); 252 final boolean isVerticalSplit = isVerticalSplit(splitAttributes); 253 final boolean isReversedLayout = isReversedLayout(splitAttributes, parentConfiguration); 254 final int dividerWidthPx = getDividerWidthPx(dividerAttributes); 255 256 updateProperties( 257 new Properties( 258 parentConfiguration, 259 dividerAttributes, 260 decorSurface, 261 getInitialDividerPosition( 262 primaryContainer, secondaryContainer, taskBounds, 263 dividerWidthPx, isDraggableExpandType, isVerticalSplit, 264 isReversedLayout), 265 isVerticalSplit, 266 isReversedLayout, 267 parentInfo.getDisplayId(), 268 isDraggableExpandType, 269 primaryContainer, 270 secondaryContainer) 271 ); 272 } 273 } 274 275 @GuardedBy("mLock") updateProperties(@onNull Properties properties)276 private void updateProperties(@NonNull Properties properties) { 277 if (Properties.equalsForDivider(mProperties, properties)) { 278 return; 279 } 280 final Properties previousProperties = mProperties; 281 mProperties = properties; 282 283 if (mRenderer == null) { 284 // Create a new renderer when a renderer doesn't exist yet. 285 mRenderer = new Renderer(mProperties, this); 286 } else if (!Properties.areSameSurfaces( 287 previousProperties.mDecorSurface, mProperties.mDecorSurface) 288 || previousProperties.mDisplayId != mProperties.mDisplayId) { 289 // Release and recreate the renderer if the decor surface or the display has changed. 290 mRenderer.release(); 291 mRenderer = new Renderer(mProperties, this); 292 } else { 293 // Otherwise, update the renderer for the new properties. 294 mRenderer.update(mProperties); 295 } 296 } 297 298 /** 299 * Returns the window background color of the top activity in the container if set, or the 300 * default color if the background color of the top activity is unavailable. 301 */ 302 @VisibleForTesting 303 @NonNull getContainerBackgroundColor( @onNull TaskFragmentContainer container, @NonNull Color defaultColor)304 static Color getContainerBackgroundColor( 305 @NonNull TaskFragmentContainer container, @NonNull Color defaultColor) { 306 final Activity activity = container.getTopNonFinishingActivity(); 307 if (activity == null) { 308 // This can happen when the activities in the container are from a different process. 309 // TODO(b/340984203) Report whether the top activity is in the same process. Use default 310 // color if not. 311 return defaultColor; 312 } 313 314 final Drawable drawable = activity.getWindow().getDecorView().getBackground(); 315 if (drawable instanceof ColorDrawable colorDrawable) { 316 return Color.valueOf(colorDrawable.getColor()); 317 } 318 return defaultColor; 319 } 320 321 /** 322 * Creates a decor surface for the TaskFragment if no decor surface exists, or changes the owner 323 * of the existing decor surface to be the specified TaskFragment. 324 * 325 * See {@link TaskFragmentOperation#OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE}. 326 */ createOrMoveDecorSurface( @onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container)327 void createOrMoveDecorSurface( 328 @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { 329 synchronized (mLock) { 330 createOrMoveDecorSurfaceLocked(wct, container); 331 } 332 } 333 334 @GuardedBy("mLock") createOrMoveDecorSurfaceLocked( @onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container)335 private void createOrMoveDecorSurfaceLocked( 336 @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { 337 mDecorSurfaceOwner = container.getTaskFragmentToken(); 338 final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( 339 OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE) 340 .build(); 341 wct.addTaskFragmentOperation(mDecorSurfaceOwner, operation); 342 } 343 344 @GuardedBy("mLock") removeDecorSurfaceAndDivider(@onNull WindowContainerTransaction wct)345 private void removeDecorSurfaceAndDivider(@NonNull WindowContainerTransaction wct) { 346 if (mDecorSurfaceOwner != null) { 347 final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( 348 OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) 349 .build(); 350 wct.addTaskFragmentOperation(mDecorSurfaceOwner, operation); 351 mDecorSurfaceOwner = null; 352 } 353 removeDivider(); 354 } 355 356 @GuardedBy("mLock") removeDivider()357 private void removeDivider() { 358 if (mRenderer != null) { 359 mRenderer.release(); 360 } 361 mProperties = null; 362 mRenderer = null; 363 } 364 365 @VisibleForTesting getInitialDividerPosition( @onNull TaskFragmentContainer primaryContainer, @NonNull TaskFragmentContainer secondaryContainer, @NonNull Rect taskBounds, int dividerWidthPx, boolean isDraggableExpandType, boolean isVerticalSplit, boolean isReversedLayout)366 static int getInitialDividerPosition( 367 @NonNull TaskFragmentContainer primaryContainer, 368 @NonNull TaskFragmentContainer secondaryContainer, 369 @NonNull Rect taskBounds, 370 int dividerWidthPx, 371 boolean isDraggableExpandType, 372 boolean isVerticalSplit, 373 boolean isReversedLayout) { 374 if (isDraggableExpandType) { 375 // If the secondary container is fully expanded by dragging the divider, we display the 376 // divider on the edge. 377 final int fullyExpandedPosition = isVerticalSplit 378 ? taskBounds.width() - dividerWidthPx 379 : taskBounds.height() - dividerWidthPx; 380 return isReversedLayout ? fullyExpandedPosition : 0; 381 } else { 382 final Rect primaryBounds = primaryContainer.getLastRequestedBounds(); 383 final Rect secondaryBounds = secondaryContainer.getLastRequestedBounds(); 384 return isVerticalSplit 385 ? Math.min(primaryBounds.right, secondaryBounds.right) 386 : Math.min(primaryBounds.bottom, secondaryBounds.bottom); 387 } 388 } 389 isVerticalSplit(@onNull SplitAttributes splitAttributes)390 private static boolean isVerticalSplit(@NonNull SplitAttributes splitAttributes) { 391 final int layoutDirection = splitAttributes.getLayoutDirection(); 392 switch (layoutDirection) { 393 case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: 394 case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: 395 case SplitAttributes.LayoutDirection.LOCALE: 396 return true; 397 case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: 398 case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: 399 return false; 400 default: 401 throw new IllegalArgumentException("Invalid layout direction:" + layoutDirection); 402 } 403 } 404 getDividerWidthPx(@onNull DividerAttributes dividerAttributes)405 private static int getDividerWidthPx(@NonNull DividerAttributes dividerAttributes) { 406 int dividerWidthDp = dividerAttributes.getWidthDp(); 407 return convertDpToPixel(dividerWidthDp); 408 } 409 convertDpToPixel(int dp)410 private static int convertDpToPixel(int dp) { 411 // TODO(b/329193115) support divider on secondary display 412 final Context applicationContext = ActivityThread.currentActivityThread().getApplication(); 413 414 return (int) TypedValue.applyDimension( 415 COMPLEX_UNIT_DIP, 416 dp, 417 applicationContext.getResources().getDisplayMetrics()); 418 } 419 getDisplayDensity()420 private static float getDisplayDensity() { 421 // TODO(b/329193115) support divider on secondary display 422 final Context applicationContext = 423 ActivityThread.currentActivityThread().getApplication(); 424 return applicationContext.getResources().getDisplayMetrics().density; 425 } 426 427 /** 428 * Returns the container bound offset that is a result of the presence of a divider. 429 * 430 * The offset is the relative position change for the container edge that is next to the divider 431 * due to the presence of the divider. The value could be negative or positive depending on the 432 * container position. Positive values indicate that the edge is shifting towards the right 433 * (or bottom) and negative values indicate that the edge is shifting towards the left (or top). 434 * 435 * @param splitAttributes the {@link SplitAttributes} of the split container that we want to 436 * compute bounds offset. 437 * @param position the position of the container in the split that we want to compute 438 * bounds offset for. 439 * @return the bounds offset in pixels. 440 */ getBoundsOffsetForDivider( @onNull SplitAttributes splitAttributes, @SplitPresenter.ContainerPosition int position)441 static int getBoundsOffsetForDivider( 442 @NonNull SplitAttributes splitAttributes, 443 @SplitPresenter.ContainerPosition int position) { 444 if (!Flags.activityEmbeddingInteractiveDividerFlag()) { 445 return 0; 446 } 447 final DividerAttributes dividerAttributes = splitAttributes.getDividerAttributes(); 448 if (dividerAttributes == null) { 449 return 0; 450 } 451 final int dividerWidthPx = getDividerWidthPx(dividerAttributes); 452 return getBoundsOffsetForDivider( 453 dividerWidthPx, 454 splitAttributes.getSplitType(), 455 position); 456 } 457 458 @VisibleForTesting getBoundsOffsetForDivider( int dividerWidthPx, @NonNull SplitType splitType, @SplitPresenter.ContainerPosition int position)459 static int getBoundsOffsetForDivider( 460 int dividerWidthPx, 461 @NonNull SplitType splitType, 462 @SplitPresenter.ContainerPosition int position) { 463 if (splitType instanceof ExpandContainersSplitType) { 464 // No divider offset is needed for the ExpandContainersSplitType. 465 return 0; 466 } 467 int primaryOffset; 468 if (splitType instanceof final RatioSplitType splitRatio) { 469 // When a divider is present, both containers shrink by an amount proportional to their 470 // split ratio and sum to the width of the divider, so that the ending sizing of the 471 // containers still maintain the same ratio. 472 primaryOffset = (int) (dividerWidthPx * splitRatio.getRatio()); 473 } else { 474 // Hinge split type (and other future split types) will have the divider width equally 475 // distributed to both containers. 476 primaryOffset = dividerWidthPx / 2; 477 } 478 final int secondaryOffset = dividerWidthPx - primaryOffset; 479 switch (position) { 480 case CONTAINER_POSITION_LEFT: 481 case CONTAINER_POSITION_TOP: 482 return -primaryOffset; 483 case CONTAINER_POSITION_RIGHT: 484 case CONTAINER_POSITION_BOTTOM: 485 return secondaryOffset; 486 default: 487 throw new IllegalArgumentException("Unknown position:" + position); 488 } 489 } 490 491 /** 492 * Sanitizes and sets default values in the {@link DividerAttributes}. 493 * 494 * Unset values will be set with system default values. See 495 * {@link DividerAttributes#WIDTH_SYSTEM_DEFAULT} and 496 * {@link DividerAttributes#RATIO_SYSTEM_DEFAULT}. 497 * 498 * @param dividerAttributes input {@link DividerAttributes} 499 * @return a {@link DividerAttributes} that has all values properly set. 500 */ 501 @Nullable sanitizeDividerAttributes( @ullable DividerAttributes dividerAttributes)502 static DividerAttributes sanitizeDividerAttributes( 503 @Nullable DividerAttributes dividerAttributes) { 504 if (dividerAttributes == null) { 505 return null; 506 } 507 int widthDp = dividerAttributes.getWidthDp(); 508 float minRatio = dividerAttributes.getPrimaryMinRatio(); 509 float maxRatio = dividerAttributes.getPrimaryMaxRatio(); 510 511 if (widthDp == WIDTH_SYSTEM_DEFAULT) { 512 widthDp = DEFAULT_DIVIDER_WIDTH_DP; 513 } 514 515 if (dividerAttributes.getDividerType() == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { 516 // Update minRatio and maxRatio only when it is a draggable divider. 517 if (minRatio == RATIO_SYSTEM_DEFAULT) { 518 minRatio = DEFAULT_MIN_RATIO; 519 } 520 if (maxRatio == RATIO_SYSTEM_DEFAULT) { 521 maxRatio = DEFAULT_MAX_RATIO; 522 } 523 } 524 525 return new DividerAttributes.Builder(dividerAttributes) 526 .setWidthDp(widthDp) 527 .setPrimaryMinRatio(minRatio) 528 .setPrimaryMaxRatio(maxRatio) 529 .build(); 530 } 531 532 @Override onTouch(@onNull View view, @NonNull MotionEvent event)533 public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) { 534 synchronized (mLock) { 535 if (mProperties != null && mRenderer != null) { 536 final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); 537 mDividerPosition = calculateDividerPosition( 538 event, taskBounds, mProperties.mDividerWidthPx, 539 mProperties.mDividerAttributes, mProperties.mIsVerticalSplit, 540 calculateMinPosition(), calculateMaxPosition()); 541 mRenderer.setDividerPosition(mDividerPosition); 542 543 // Convert to use screen-based coordinates to prevent lost track of motion events 544 // while moving divider bar and calculating dragging velocity. 545 event.setLocation(event.getRawX(), event.getRawY()); 546 final int action = event.getAction() & MotionEvent.ACTION_MASK; 547 switch (action) { 548 case MotionEvent.ACTION_DOWN: 549 onStartDragging(event); 550 break; 551 case MotionEvent.ACTION_UP: 552 case MotionEvent.ACTION_CANCEL: 553 onFinishDragging(event); 554 break; 555 case MotionEvent.ACTION_MOVE: 556 onDrag(event); 557 break; 558 default: 559 break; 560 } 561 } 562 } 563 564 // Returns true to prevent the default button click callback. The button pressed state is 565 // set/unset when starting/finishing dragging. 566 return true; 567 } 568 569 // Only called by onTouch() and mRenderer is already null-checked. 570 @GuardedBy("mLock") onStartDragging(@onNull MotionEvent event)571 private void onStartDragging(@NonNull MotionEvent event) { 572 mVelocityTracker = VelocityTracker.obtain(); 573 mVelocityTracker.addMovement(event); 574 575 mRenderer.mIsDragging = true; 576 mRenderer.mDragHandle.setPressed(mRenderer.mIsDragging); 577 mRenderer.updateSurface(); 578 579 // Veil visibility change should be applied together with the surface boost transaction in 580 // the wct. 581 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 582 mRenderer.showVeils(t); 583 584 // Callbacks must be executed on the executor to release mLock and prevent deadlocks. 585 mCallbackExecutor.execute(() -> { 586 mDragEventCallback.onStartDragging( 587 wct -> { 588 synchronized (mLock) { 589 setDecorSurfaceBoosted(wct, mDecorSurfaceOwner, true /* boosted */, t); 590 } 591 }); 592 }); 593 } 594 595 // Only called by onTouch() and mRenderer is already null-checked. 596 @GuardedBy("mLock") onDrag(@onNull MotionEvent event)597 private void onDrag(@NonNull MotionEvent event) { 598 if (mVelocityTracker != null) { 599 mVelocityTracker.addMovement(event); 600 } 601 mRenderer.updateSurface(); 602 } 603 604 @GuardedBy("mLock") onFinishDragging(@onNull MotionEvent event)605 private void onFinishDragging(@NonNull MotionEvent event) { 606 float velocity = 0.0f; 607 if (mVelocityTracker != null) { 608 mVelocityTracker.addMovement(event); 609 mVelocityTracker.computeCurrentVelocity(1000 /* units */); 610 velocity = mProperties.mIsVerticalSplit 611 ? mVelocityTracker.getXVelocity() 612 : mVelocityTracker.getYVelocity(); 613 mVelocityTracker.recycle(); 614 } 615 616 final int prevDividerPosition = mDividerPosition; 617 mDividerPosition = dividerPositionForSnapPoints(mDividerPosition, velocity); 618 if (mDividerPosition != prevDividerPosition) { 619 ValueAnimator animator = getFlingAnimator(prevDividerPosition, mDividerPosition); 620 animator.start(); 621 } else { 622 onDraggingEnd(); 623 } 624 } 625 626 @GuardedBy("mLock") 627 @NonNull 628 @VisibleForTesting getFlingAnimator(int prevDividerPosition, int snappedDividerPosition)629 ValueAnimator getFlingAnimator(int prevDividerPosition, int snappedDividerPosition) { 630 final ValueAnimator animator = 631 getValueAnimator(prevDividerPosition, snappedDividerPosition); 632 animator.addUpdateListener(animation -> { 633 synchronized (mLock) { 634 updateDividerPosition((int) animation.getAnimatedValue()); 635 } 636 }); 637 animator.addListener(new AnimatorListenerAdapter() { 638 @Override 639 public void onAnimationEnd(Animator animation) { 640 synchronized (mLock) { 641 onDraggingEnd(); 642 } 643 } 644 645 @Override 646 public void onAnimationCancel(Animator animation) { 647 synchronized (mLock) { 648 onDraggingEnd(); 649 } 650 } 651 }); 652 return animator; 653 } 654 655 @VisibleForTesting getValueAnimator(int prevDividerPosition, int snappedDividerPosition)656 static ValueAnimator getValueAnimator(int prevDividerPosition, int snappedDividerPosition) { 657 ValueAnimator animator = ValueAnimator 658 .ofInt(prevDividerPosition, snappedDividerPosition) 659 .setDuration(FLING_ANIMATION_DURATION); 660 animator.setInterpolator(FLING_ANIMATION_INTERPOLATOR); 661 return animator; 662 } 663 664 @GuardedBy("mLock") updateDividerPosition(int position)665 private void updateDividerPosition(int position) { 666 if (mRenderer != null) { 667 mRenderer.setDividerPosition(position); 668 mRenderer.updateSurface(); 669 } 670 } 671 672 @GuardedBy("mLock") onDraggingEnd()673 private void onDraggingEnd() { 674 // Veil visibility change should be applied together with the surface boost transaction in 675 // the wct. 676 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 677 678 if (mRenderer != null) { 679 mRenderer.hideVeils(t); 680 } 681 682 // Callbacks must be executed on the executor to release mLock and prevent deadlocks. 683 // mDecorSurfaceOwner may change between here and when the callback is executed, 684 // e.g. when the decor surface owner becomes the secondary container when it is expanded to 685 // fullscreen. 686 mCallbackExecutor.execute(() -> { 687 mDragEventCallback.onFinishDragging( 688 mTaskId, 689 wct -> { 690 synchronized (mLock) { 691 setDecorSurfaceBoosted(wct, mDecorSurfaceOwner, false /* boosted */, t); 692 } 693 }); 694 }); 695 if (mRenderer != null) { 696 mRenderer.mIsDragging = false; 697 mRenderer.mDragHandle.setPressed(mRenderer.mIsDragging); 698 } 699 } 700 701 /** 702 * Returns the divider position adjusted for the min max ratio and fullscreen expansion. 703 * The adjusted divider position is in the range of [minPosition, maxPosition] for a split, 0 704 * for expanded right (bottom) container, or task width (height) minus the divider width for 705 * expanded left (top) container. 706 */ 707 @GuardedBy("mLock") dividerPositionForSnapPoints(int dividerPosition, float velocity)708 private int dividerPositionForSnapPoints(int dividerPosition, float velocity) { 709 final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); 710 final int minPosition = calculateMinPosition(); 711 final int maxPosition = calculateMaxPosition(); 712 final int fullyExpandedPosition = mProperties.mIsVerticalSplit 713 ? taskBounds.width() - mProperties.mDividerWidthPx 714 : taskBounds.height() - mProperties.mDividerWidthPx; 715 716 final float displayDensity = getDisplayDensity(); 717 final boolean isDraggingToFullscreenAllowed = 718 isDraggingToFullscreenAllowed(mProperties.mDividerAttributes); 719 return dividerPositionWithPositionOptions( 720 dividerPosition, 721 minPosition, 722 maxPosition, 723 fullyExpandedPosition, 724 velocity, 725 displayDensity, 726 isDraggingToFullscreenAllowed); 727 } 728 729 /** 730 * Returns the divider position given a set of position options. A snap algorithm can adjust 731 * the ending position to either fully expand one container or move the divider back to 732 * the specified min/max ratio depending on the dragging velocity and if dragging to fullscreen 733 * is allowed. 734 */ 735 @VisibleForTesting dividerPositionWithPositionOptions(int dividerPosition, int minPosition, int maxPosition, int fullyExpandedPosition, float velocity, float displayDensity, boolean isDraggingToFullscreenAllowed)736 static int dividerPositionWithPositionOptions(int dividerPosition, int minPosition, 737 int maxPosition, int fullyExpandedPosition, float velocity, float displayDensity, 738 boolean isDraggingToFullscreenAllowed) { 739 if (isDraggingToFullscreenAllowed) { 740 final float minDismissVelocityPxPerSecond = 741 MIN_DISMISS_VELOCITY_DP_PER_SECOND * displayDensity; 742 if (dividerPosition < minPosition && velocity < -minDismissVelocityPxPerSecond) { 743 return 0; 744 } 745 if (dividerPosition > maxPosition && velocity > minDismissVelocityPxPerSecond) { 746 return fullyExpandedPosition; 747 } 748 } 749 final float minFlingVelocityPxPerSecond = 750 MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity; 751 if (Math.abs(velocity) >= minFlingVelocityPxPerSecond) { 752 return dividerPositionForFling( 753 dividerPosition, minPosition, maxPosition, velocity); 754 } 755 if (dividerPosition >= minPosition && dividerPosition <= maxPosition) { 756 return dividerPosition; 757 } 758 return snap( 759 dividerPosition, 760 isDraggingToFullscreenAllowed 761 ? new int[] {0, minPosition, maxPosition, fullyExpandedPosition} 762 : new int[] {minPosition, maxPosition}); 763 } 764 765 /** 766 * Returns the closest position that is in the fling direction. 767 */ dividerPositionForFling(int dividerPosition, int minPosition, int maxPosition, float velocity)768 private static int dividerPositionForFling(int dividerPosition, int minPosition, 769 int maxPosition, float velocity) { 770 final boolean isBackwardDirection = velocity < 0; 771 if (isBackwardDirection) { 772 return dividerPosition < maxPosition ? minPosition : maxPosition; 773 } else { 774 return dividerPosition > minPosition ? maxPosition : minPosition; 775 } 776 } 777 778 /** 779 * Returns the snapped position from a list of possible positions. Currently, this method 780 * snaps to the closest position by distance from the divider position. 781 */ 782 private static int snap(int dividerPosition, int[] possiblePositions) { 783 int snappedPosition = dividerPosition; 784 float minDistance = Float.MAX_VALUE; 785 for (int position : possiblePositions) { 786 float distance = Math.abs(dividerPosition - position); 787 if (distance < minDistance) { 788 snappedPosition = position; 789 minDistance = distance; 790 } 791 } 792 return snappedPosition; 793 } 794 795 private static void setDecorSurfaceBoosted( 796 @NonNull WindowContainerTransaction wct, 797 @Nullable IBinder decorSurfaceOwner, 798 boolean boosted, 799 @NonNull SurfaceControl.Transaction clientTransaction) { 800 if (decorSurfaceOwner == null) { 801 return; 802 } 803 wct.addTaskFragmentOperation( 804 decorSurfaceOwner, 805 new TaskFragmentOperation.Builder(OP_TYPE_SET_DECOR_SURFACE_BOOSTED) 806 .setBooleanValue(boosted) 807 .setSurfaceTransaction(clientTransaction) 808 .build() 809 ); 810 } 811 812 /** Calculates the new divider position based on the touch event and divider attributes. */ 813 @VisibleForTesting 814 static int calculateDividerPosition(@NonNull MotionEvent event, @NonNull Rect taskBounds, 815 int dividerWidthPx, @NonNull DividerAttributes dividerAttributes, 816 boolean isVerticalSplit, int minPosition, int maxPosition) { 817 // The touch event is in display space. Converting it into the task window space. 818 final int touchPositionInTaskSpace = isVerticalSplit 819 ? (int) (event.getRawX()) - taskBounds.left 820 : (int) (event.getRawY()) - taskBounds.top; 821 822 // Assuming that the touch position is at the center of the divider bar, so the divider 823 // position is offset by half of the divider width. 824 int dividerPosition = touchPositionInTaskSpace - dividerWidthPx / 2; 825 826 // If dragging to fullscreen is not allowed, limit the divider position to the min and max 827 // ratios set in DividerAttributes. Otherwise, dragging beyond the min and max ratios is 828 // temporarily allowed and the final ratio will be adjusted in onFinishDragging. 829 if (!isDraggingToFullscreenAllowed(dividerAttributes)) { 830 dividerPosition = Math.clamp(dividerPosition, minPosition, maxPosition); 831 } 832 return dividerPosition; 833 } 834 835 @GuardedBy("mLock") 836 private int calculateMinPosition() { 837 return calculateMinPosition( 838 mProperties.mConfiguration.windowConfiguration.getBounds(), 839 mProperties.mDividerWidthPx, mProperties.mDividerAttributes, 840 mProperties.mIsVerticalSplit, mProperties.mIsReversedLayout); 841 } 842 843 @GuardedBy("mLock") 844 private int calculateMaxPosition() { 845 return calculateMaxPosition( 846 mProperties.mConfiguration.windowConfiguration.getBounds(), 847 mProperties.mDividerWidthPx, mProperties.mDividerAttributes, 848 mProperties.mIsVerticalSplit, mProperties.mIsReversedLayout); 849 } 850 851 /** Calculates the min position of the divider that the user is allowed to drag to. */ 852 @VisibleForTesting 853 static int calculateMinPosition(@NonNull Rect taskBounds, int dividerWidthPx, 854 @NonNull DividerAttributes dividerAttributes, boolean isVerticalSplit, 855 boolean isReversedLayout) { 856 // The usable size is the task window size minus the divider bar width. This is shared 857 // between the primary and secondary containers based on the split ratio. 858 final int usableSize = isVerticalSplit 859 ? taskBounds.width() - dividerWidthPx 860 : taskBounds.height() - dividerWidthPx; 861 return (int) (isReversedLayout 862 ? usableSize - usableSize * dividerAttributes.getPrimaryMaxRatio() 863 : usableSize * dividerAttributes.getPrimaryMinRatio()); 864 } 865 866 /** Calculates the max position of the divider that the user is allowed to drag to. */ 867 @VisibleForTesting 868 static int calculateMaxPosition(@NonNull Rect taskBounds, int dividerWidthPx, 869 @NonNull DividerAttributes dividerAttributes, boolean isVerticalSplit, 870 boolean isReversedLayout) { 871 // The usable size is the task window size minus the divider bar width. This is shared 872 // between the primary and secondary containers based on the split ratio. 873 final int usableSize = isVerticalSplit 874 ? taskBounds.width() - dividerWidthPx 875 : taskBounds.height() - dividerWidthPx; 876 return (int) (isReversedLayout 877 ? usableSize - usableSize * dividerAttributes.getPrimaryMinRatio() 878 : usableSize * dividerAttributes.getPrimaryMaxRatio()); 879 } 880 881 /** 882 * Returns the new split ratio of the {@link SplitContainer} based on the current divider 883 * position. 884 */ 885 float calculateNewSplitRatio() { 886 synchronized (mLock) { 887 return calculateNewSplitRatio( 888 mDividerPosition, 889 mProperties.mConfiguration.windowConfiguration.getBounds(), 890 mProperties.mDividerWidthPx, 891 mProperties.mIsVerticalSplit, 892 mProperties.mIsReversedLayout, 893 calculateMinPosition(), 894 calculateMaxPosition(), 895 isDraggingToFullscreenAllowed(mProperties.mDividerAttributes)); 896 } 897 } 898 899 void setHasContainersToFinish(boolean hasContainersToFinish) { 900 synchronized (mLock) { 901 mHasContainersToFinish = hasContainersToFinish; 902 } 903 } 904 905 private static boolean isDraggingToFullscreenAllowed( 906 @NonNull DividerAttributes dividerAttributes) { 907 return dividerAttributes.isDraggingToFullscreenAllowed(); 908 } 909 910 /** 911 * Returns the new split ratio of the {@link SplitContainer} based on the current divider 912 * position. 913 * 914 * @param dividerPosition the divider position. See {@link #mDividerPosition}. 915 * @param taskBounds the task bounds 916 * @param dividerWidthPx the width of the divider in pixels. 917 * @param isVerticalSplit if {@code true}, the split is a vertical split. If {@code false}, the 918 * split is a horizontal split. See 919 * {@link #isVerticalSplit(SplitAttributes)}. 920 * @param isReversedLayout if {@code true}, the split layout is reversed, i.e. right-to-left or 921 * bottom-to-top. If {@code false}, the split is not reversed, i.e. 922 * left-to-right or top-to-bottom. See 923 * {@link SplitAttributesHelper#isReversedLayout} 924 * @return the computed split ratio of the primary container. If the primary container is fully 925 * expanded, {@link #RATIO_EXPANDED_PRIMARY} is returned. If the secondary container is fully 926 * expanded, {@link #RATIO_EXPANDED_SECONDARY} is returned. 927 */ 928 @VisibleForTesting 929 static float calculateNewSplitRatio( 930 int dividerPosition, 931 @NonNull Rect taskBounds, 932 int dividerWidthPx, 933 boolean isVerticalSplit, 934 boolean isReversedLayout, 935 int minPosition, 936 int maxPosition, 937 boolean isDraggingToFullscreenAllowed) { 938 939 // Handle the fully expanded cases. 940 if (isDraggingToFullscreenAllowed) { 941 // The divider position is already adjusted by the snap algorithm in onFinishDragging. 942 // If the divider position is not in the range [minPosition, maxPosition], then one of 943 // the containers is fully expanded. 944 if (dividerPosition < minPosition) { 945 return isReversedLayout ? RATIO_EXPANDED_PRIMARY : RATIO_EXPANDED_SECONDARY; 946 } 947 if (dividerPosition > maxPosition) { 948 return isReversedLayout ? RATIO_EXPANDED_SECONDARY : RATIO_EXPANDED_PRIMARY; 949 } 950 } else { 951 dividerPosition = Math.clamp(dividerPosition, minPosition, maxPosition); 952 } 953 954 final int usableSize = isVerticalSplit 955 ? taskBounds.width() - dividerWidthPx 956 : taskBounds.height() - dividerWidthPx; 957 958 final float newRatio; 959 if (isVerticalSplit) { 960 final int newPrimaryWidth = isReversedLayout 961 ? taskBounds.width() - (dividerPosition + dividerWidthPx) 962 : dividerPosition; 963 newRatio = 1.0f * newPrimaryWidth / usableSize; 964 } else { 965 final int newPrimaryHeight = isReversedLayout 966 ? taskBounds.height() - (dividerPosition + dividerWidthPx) 967 : dividerPosition; 968 newRatio = 1.0f * newPrimaryHeight / usableSize; 969 } 970 return newRatio; 971 } 972 973 /** Callbacks for drag events */ 974 interface DragEventCallback { 975 /** 976 * Called when the user starts dragging the divider. Callbacks are executed on 977 * {@link #mCallbackExecutor}. 978 * 979 * @param action additional action that should be applied to the 980 * {@link WindowContainerTransaction} 981 */ 982 void onStartDragging(@NonNull Consumer<WindowContainerTransaction> action); 983 984 /** 985 * Called when the user finishes dragging the divider. Callbacks are executed on 986 * {@link #mCallbackExecutor}. 987 * 988 * @param taskId the Task id of the {@link TaskContainer} that this divider belongs to. 989 * @param action additional action that should be applied to the 990 * {@link WindowContainerTransaction} 991 */ 992 void onFinishDragging(int taskId, @NonNull Consumer<WindowContainerTransaction> action); 993 } 994 995 /** 996 * Properties for the {@link DividerPresenter}. The rendering of the divider solely depends on 997 * these properties. When any value is updated, the divider is re-rendered. The Properties 998 * instance is created only when all the pre-conditions of drawing a divider are met. 999 */ 1000 @VisibleForTesting 1001 static class Properties { 1002 private static final int CONFIGURATION_MASK_FOR_DIVIDER = 1003 CONFIG_DENSITY | CONFIG_WINDOW_CONFIGURATION | CONFIG_LAYOUT_DIRECTION; 1004 @NonNull 1005 private final Configuration mConfiguration; 1006 @NonNull 1007 private final DividerAttributes mDividerAttributes; 1008 @NonNull 1009 private final SurfaceControl mDecorSurface; 1010 1011 /** The initial position of the divider calculated based on container bounds. */ 1012 private final int mInitialDividerPosition; 1013 1014 /** Whether the split is vertical, such as left-to-right or right-to-left split. */ 1015 private final boolean mIsVerticalSplit; 1016 1017 private final int mDisplayId; 1018 private final boolean mIsReversedLayout; 1019 private final boolean mIsDraggableExpandType; 1020 @NonNull 1021 private final TaskFragmentContainer mPrimaryContainer; 1022 @NonNull 1023 private final TaskFragmentContainer mSecondaryContainer; 1024 private final int mDividerWidthPx; 1025 1026 @VisibleForTesting 1027 Properties( 1028 @NonNull Configuration configuration, 1029 @NonNull DividerAttributes dividerAttributes, 1030 @NonNull SurfaceControl decorSurface, 1031 int initialDividerPosition, 1032 boolean isVerticalSplit, 1033 boolean isReversedLayout, 1034 int displayId, 1035 boolean isDraggableExpandType, 1036 @NonNull TaskFragmentContainer primaryContainer, 1037 @NonNull TaskFragmentContainer secondaryContainer) { 1038 mConfiguration = configuration; 1039 mDividerAttributes = dividerAttributes; 1040 mDecorSurface = decorSurface; 1041 mInitialDividerPosition = initialDividerPosition; 1042 mIsVerticalSplit = isVerticalSplit; 1043 mIsReversedLayout = isReversedLayout; 1044 mDisplayId = displayId; 1045 mIsDraggableExpandType = isDraggableExpandType; 1046 mPrimaryContainer = primaryContainer; 1047 mSecondaryContainer = secondaryContainer; 1048 mDividerWidthPx = getDividerWidthPx(dividerAttributes); 1049 } 1050 1051 /** 1052 * Compares whether two Properties objects are equal for rendering the divider. The 1053 * Configuration is checked for rendering related fields, and other fields are checked for 1054 * regular equality. 1055 */ 1056 private static boolean equalsForDivider(@Nullable Properties a, @Nullable Properties b) { 1057 if (a == b) { 1058 return true; 1059 } 1060 if (a == null || b == null) { 1061 return false; 1062 } 1063 return areSameSurfaces(a.mDecorSurface, b.mDecorSurface) 1064 && Objects.equals(a.mDividerAttributes, b.mDividerAttributes) 1065 && areConfigurationsEqualForDivider(a.mConfiguration, b.mConfiguration) 1066 && a.mInitialDividerPosition == b.mInitialDividerPosition 1067 && a.mIsVerticalSplit == b.mIsVerticalSplit 1068 && a.mDisplayId == b.mDisplayId 1069 && a.mIsReversedLayout == b.mIsReversedLayout 1070 && a.mIsDraggableExpandType == b.mIsDraggableExpandType 1071 && a.mPrimaryContainer == b.mPrimaryContainer 1072 && a.mSecondaryContainer == b.mSecondaryContainer; 1073 } 1074 1075 private static boolean areSameSurfaces( 1076 @Nullable SurfaceControl sc1, @Nullable SurfaceControl sc2) { 1077 if (sc1 == sc2) { 1078 // If both are null or both refer to the same object. 1079 return true; 1080 } 1081 if (sc1 == null || sc2 == null) { 1082 return false; 1083 } 1084 return sc1.isSameSurface(sc2); 1085 } 1086 1087 private static boolean areConfigurationsEqualForDivider( 1088 @NonNull Configuration a, @NonNull Configuration b) { 1089 final int diff = a.diff(b); 1090 return (diff & CONFIGURATION_MASK_FOR_DIVIDER) == 0; 1091 } 1092 } 1093 1094 /** 1095 * Handles the rendering of the divider. When the decor surface is updated, the renderer is 1096 * recreated. When other fields in the Properties are changed, the renderer is updated. 1097 */ 1098 @VisibleForTesting 1099 static class Renderer { 1100 @NonNull 1101 private final SurfaceControl mDividerSurface; 1102 @NonNull 1103 private final SurfaceControl mDividerLineSurface; 1104 @NonNull 1105 private final WindowlessWindowManager mWindowlessWindowManager; 1106 @NonNull 1107 private final SurfaceControlViewHost mViewHost; 1108 @NonNull 1109 private final FrameLayout mDividerLayout; 1110 @Nullable 1111 private View mDragHandle; 1112 @NonNull 1113 private final View.OnTouchListener mListener; 1114 @NonNull 1115 private Properties mProperties; 1116 private int mHandleWidthPx; 1117 @Nullable 1118 private SurfaceControl mPrimaryVeil; 1119 @Nullable 1120 private SurfaceControl mSecondaryVeil; 1121 private boolean mIsDragging; 1122 private int mDividerPosition; 1123 private int mDividerSurfaceWidthPx; 1124 1125 private Renderer(@NonNull Properties properties, @NonNull View.OnTouchListener listener) { 1126 mProperties = properties; 1127 mListener = listener; 1128 1129 mDividerSurface = createChildSurface( 1130 mProperties.mDecorSurface, "DividerSurface", true /* visible */); 1131 mDividerLineSurface = createChildSurface( 1132 mDividerSurface, "DividerLineSurface", true /* visible */); 1133 mWindowlessWindowManager = new WindowlessWindowManager( 1134 mProperties.mConfiguration, 1135 mDividerSurface, 1136 new InputTransferToken()); 1137 1138 final Context context = ActivityThread.currentActivityThread().getApplication(); 1139 final DisplayManager displayManager = context.getSystemService(DisplayManager.class); 1140 mViewHost = new SurfaceControlViewHost( 1141 context, displayManager.getDisplay(mProperties.mDisplayId), 1142 mWindowlessWindowManager, "DividerContainer"); 1143 mDividerLayout = new FrameLayout(context); 1144 1145 update(); 1146 } 1147 1148 /** Updates the divider when properties are changed */ 1149 private void update(@NonNull Properties newProperties) { 1150 mProperties = newProperties; 1151 update(); 1152 } 1153 1154 /** Updates the divider when initializing or when properties are changed */ 1155 @VisibleForTesting 1156 void update() { 1157 mDividerPosition = mProperties.mInitialDividerPosition; 1158 mWindowlessWindowManager.setConfiguration(mProperties.mConfiguration); 1159 1160 if (mProperties.mDividerAttributes.getDividerType() 1161 == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { 1162 // TODO(b/329193115) support divider on secondary display 1163 final Context context = ActivityThread.currentActivityThread().getApplication(); 1164 mHandleWidthPx = context.getResources().getDimensionPixelSize( 1165 R.dimen.activity_embedding_divider_touch_target_width); 1166 } else { 1167 mHandleWidthPx = 0; 1168 } 1169 1170 // TODO handle synchronization between surface transactions and WCT. 1171 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 1172 updateSurface(t); 1173 updateLayout(); 1174 updateDivider(t); 1175 t.apply(); 1176 } 1177 1178 @VisibleForTesting 1179 void release() { 1180 mViewHost.release(); 1181 // TODO handle synchronization between surface transactions and WCT. 1182 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 1183 t.remove(mDividerSurface); 1184 removeVeils(t); 1185 t.apply(); 1186 } 1187 1188 private void setDividerPosition(int dividerPosition) { 1189 mDividerPosition = dividerPosition; 1190 } 1191 1192 /** 1193 * Updates the positions and crops of the divider surface and veil surfaces. This method 1194 * should be called when {@link #mProperties} is changed or while dragging to update the 1195 * position of the divider surface and the veil surfaces. 1196 * 1197 * This method applies the changes in a stand-alone surface transaction immediately. 1198 */ 1199 private void updateSurface() { 1200 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 1201 updateSurface(t); 1202 t.apply(); 1203 } 1204 1205 /** 1206 * Updates the positions and crops of the divider surface and veil surfaces. This method 1207 * should be called when {@link #mProperties} is changed or while dragging to update the 1208 * position of the divider surface and the veil surfaces. 1209 * 1210 * This method applies the changes in the provided surface transaction and can be synced 1211 * with other changes. 1212 */ 1213 private void updateSurface(@NonNull SurfaceControl.Transaction t) { 1214 final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); 1215 1216 int dividerSurfacePosition; 1217 if (mProperties.mDividerAttributes.getDividerType() 1218 == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { 1219 // When the divider drag handle width is larger than the divider width, the position 1220 // of the divider surface is adjusted so that it is large enough to host both the 1221 // divider line and the divider drag handle. 1222 mDividerSurfaceWidthPx = Math.max(mProperties.mDividerWidthPx, mHandleWidthPx); 1223 dividerSurfacePosition = mProperties.mIsReversedLayout 1224 ? mDividerPosition 1225 : mDividerPosition + mProperties.mDividerWidthPx - mDividerSurfaceWidthPx; 1226 dividerSurfacePosition = 1227 Math.clamp(dividerSurfacePosition, 0, 1228 mProperties.mIsVerticalSplit 1229 ? taskBounds.width() - mDividerSurfaceWidthPx 1230 : taskBounds.height() - mDividerSurfaceWidthPx); 1231 } else { 1232 mDividerSurfaceWidthPx = mProperties.mDividerWidthPx; 1233 dividerSurfacePosition = mDividerPosition; 1234 } 1235 1236 // Update the divider surface position relative to the decor surface 1237 if (mProperties.mIsVerticalSplit) { 1238 t.setPosition(mDividerSurface, dividerSurfacePosition, 0.0f); 1239 t.setWindowCrop(mDividerSurface, mDividerSurfaceWidthPx, taskBounds.height()); 1240 } else { 1241 t.setPosition(mDividerSurface, 0.0f, dividerSurfacePosition); 1242 t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerSurfaceWidthPx); 1243 } 1244 1245 // Update divider line surface position relative to the divider surface 1246 final int offset = mDividerPosition - dividerSurfacePosition; 1247 if (mProperties.mIsVerticalSplit) { 1248 t.setPosition(mDividerLineSurface, offset, 0); 1249 t.setWindowCrop(mDividerLineSurface, 1250 mProperties.mDividerWidthPx, taskBounds.height()); 1251 } else { 1252 t.setPosition(mDividerLineSurface, 0, offset); 1253 t.setWindowCrop(mDividerLineSurface, 1254 taskBounds.width(), mProperties.mDividerWidthPx); 1255 } 1256 1257 // Update divider line surface visibility and color. 1258 // If a container is fully expanded, the divider line is invisible unless dragging. 1259 final boolean isDividerLineVisible = mProperties.mDividerWidthPx > 0 1260 && (!mProperties.mIsDraggableExpandType || mIsDragging); 1261 t.setVisibility(mDividerLineSurface, isDividerLineVisible); 1262 t.setColor(mDividerLineSurface, colorToFloatArray( 1263 Color.valueOf(mProperties.mDividerAttributes.getDividerColor()))); 1264 1265 if (mIsDragging) { 1266 updateVeils(t); 1267 } 1268 } 1269 1270 /** 1271 * Updates the layout parameters of the layout used to host the divider. This method should 1272 * be called only when {@link #mProperties} is changed. This should not be called while 1273 * dragging, because the layout parameters are not changed during dragging. 1274 */ 1275 private void updateLayout() { 1276 final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); 1277 final WindowManager.LayoutParams lp = mProperties.mIsVerticalSplit 1278 ? new WindowManager.LayoutParams( 1279 mDividerSurfaceWidthPx, 1280 taskBounds.height(), 1281 TYPE_APPLICATION_PANEL, 1282 FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY, 1283 PixelFormat.TRANSLUCENT) 1284 : new WindowManager.LayoutParams( 1285 taskBounds.width(), 1286 mDividerSurfaceWidthPx, 1287 TYPE_APPLICATION_PANEL, 1288 FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY, 1289 PixelFormat.TRANSLUCENT); 1290 lp.setTitle(WINDOW_NAME); 1291 1292 // Ensure that the divider layout is always LTR regardless of the locale, because we 1293 // already considered the locale when determining the split layout direction and the 1294 // computed divider line position always starts from the left. This only affects the 1295 // horizontal layout and does not have any effect on the top-to-bottom layout. 1296 mDividerLayout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); 1297 mViewHost.setView(mDividerLayout, lp); 1298 mViewHost.relayout(lp); 1299 } 1300 1301 /** 1302 * Updates the UI component of the divider, including the drag handle and the veils. This 1303 * method should be called only when {@link #mProperties} is changed. This should not be 1304 * called while dragging, because the UI components are not changed during dragging and 1305 * only their surface positions are changed. 1306 */ 1307 private void updateDivider(@NonNull SurfaceControl.Transaction t) { 1308 mDividerLayout.removeAllViews(); 1309 if (mProperties.mDividerAttributes.getDividerType() 1310 == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { 1311 createVeils(); 1312 drawDragHandle(); 1313 } else { 1314 removeVeils(t); 1315 } 1316 mViewHost.getView().invalidate(); 1317 } 1318 1319 private void drawDragHandle() { 1320 final Context context = mDividerLayout.getContext(); 1321 final ImageButton button = new ImageButton(context); 1322 final FrameLayout.LayoutParams params = mProperties.mIsVerticalSplit 1323 ? new FrameLayout.LayoutParams( 1324 context.getResources().getDimensionPixelSize( 1325 R.dimen.activity_embedding_divider_touch_target_width), 1326 context.getResources().getDimensionPixelSize( 1327 R.dimen.activity_embedding_divider_touch_target_height)) 1328 : new FrameLayout.LayoutParams( 1329 context.getResources().getDimensionPixelSize( 1330 R.dimen.activity_embedding_divider_touch_target_height), 1331 context.getResources().getDimensionPixelSize( 1332 R.dimen.activity_embedding_divider_touch_target_width)); 1333 params.gravity = Gravity.CENTER; 1334 button.setLayoutParams(params); 1335 button.setBackgroundColor(Color.TRANSPARENT); 1336 1337 final Drawable handle = context.getResources().getDrawable( 1338 R.drawable.activity_embedding_divider_handle, context.getTheme()); 1339 if (mProperties.mIsVerticalSplit) { 1340 button.setImageDrawable(handle); 1341 } else { 1342 // Rotate the handle drawable 1343 RotateDrawable rotatedHandle = new RotateDrawable(); 1344 rotatedHandle.setFromDegrees(90f); 1345 rotatedHandle.setToDegrees(90f); 1346 rotatedHandle.setPivotXRelative(true); 1347 rotatedHandle.setPivotYRelative(true); 1348 rotatedHandle.setPivotX(0.5f); 1349 rotatedHandle.setPivotY(0.5f); 1350 rotatedHandle.setLevel(1); 1351 rotatedHandle.setDrawable(handle); 1352 1353 button.setImageDrawable(rotatedHandle); 1354 } 1355 1356 button.setOnTouchListener(mListener); 1357 mDragHandle = button; 1358 mDividerLayout.addView(button); 1359 } 1360 1361 @NonNull 1362 private SurfaceControl createChildSurface( 1363 @NonNull SurfaceControl parent, @NonNull String name, boolean visible) { 1364 final Rect bounds = mProperties.mConfiguration.windowConfiguration.getBounds(); 1365 return new SurfaceControl.Builder() 1366 .setParent(parent) 1367 .setName(name) 1368 .setHidden(!visible) 1369 .setCallsite("DividerManager.createChildSurface") 1370 .setBufferSize(bounds.width(), bounds.height()) 1371 .setEffectLayer() 1372 .build(); 1373 } 1374 1375 private void createVeils() { 1376 if (mPrimaryVeil == null) { 1377 mPrimaryVeil = createChildSurface( 1378 mProperties.mDecorSurface, "DividerPrimaryVeil", false /* visible */); 1379 } 1380 if (mSecondaryVeil == null) { 1381 mSecondaryVeil = createChildSurface( 1382 mProperties.mDecorSurface, "DividerSecondaryVeil", false /* visible */); 1383 } 1384 } 1385 1386 private void removeVeils(@NonNull SurfaceControl.Transaction t) { 1387 if (mPrimaryVeil != null) { 1388 t.remove(mPrimaryVeil); 1389 } 1390 if (mSecondaryVeil != null) { 1391 t.remove(mSecondaryVeil); 1392 } 1393 mPrimaryVeil = null; 1394 mSecondaryVeil = null; 1395 } 1396 1397 private void showVeils(@NonNull SurfaceControl.Transaction t) { 1398 final Color primaryVeilColor = getVeilColor( 1399 mProperties.mDividerAttributes.getPrimaryVeilColor(), 1400 mProperties.mPrimaryContainer, 1401 DEFAULT_PRIMARY_VEIL_COLOR); 1402 final Color secondaryVeilColor = getVeilColor( 1403 mProperties.mDividerAttributes.getSecondaryVeilColor(), 1404 mProperties.mSecondaryContainer, 1405 DEFAULT_SECONDARY_VEIL_COLOR); 1406 t.setColor(mPrimaryVeil, colorToFloatArray(primaryVeilColor)) 1407 .setColor(mSecondaryVeil, colorToFloatArray(secondaryVeilColor)) 1408 .setLayer(mDividerSurface, DIVIDER_LAYER) 1409 .setLayer(mPrimaryVeil, VEIL_LAYER) 1410 .setLayer(mSecondaryVeil, VEIL_LAYER) 1411 .setVisibility(mPrimaryVeil, true) 1412 .setVisibility(mSecondaryVeil, true); 1413 updateVeils(t); 1414 } 1415 1416 private void hideVeils(@NonNull SurfaceControl.Transaction t) { 1417 t.setVisibility(mPrimaryVeil, false).setVisibility(mSecondaryVeil, false); 1418 } 1419 1420 private void updateVeils(@NonNull SurfaceControl.Transaction t) { 1421 final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); 1422 1423 // Relative bounds of the primary and secondary containers in the Task. 1424 Rect primaryBounds; 1425 Rect secondaryBounds; 1426 if (mProperties.mIsVerticalSplit) { 1427 final Rect boundsLeft = new Rect(0, 0, mDividerPosition, taskBounds.height()); 1428 final Rect boundsRight = new Rect(mDividerPosition + mProperties.mDividerWidthPx, 0, 1429 taskBounds.width(), taskBounds.height()); 1430 primaryBounds = mProperties.mIsReversedLayout ? boundsRight : boundsLeft; 1431 secondaryBounds = mProperties.mIsReversedLayout ? boundsLeft : boundsRight; 1432 } else { 1433 final Rect boundsTop = new Rect(0, 0, taskBounds.width(), mDividerPosition); 1434 final Rect boundsBottom = new Rect( 1435 0, mDividerPosition + mProperties.mDividerWidthPx, 1436 taskBounds.width(), taskBounds.height()); 1437 primaryBounds = mProperties.mIsReversedLayout ? boundsBottom : boundsTop; 1438 secondaryBounds = mProperties.mIsReversedLayout ? boundsTop : boundsBottom; 1439 } 1440 if (mPrimaryVeil != null) { 1441 t.setWindowCrop(mPrimaryVeil, primaryBounds.width(), primaryBounds.height()); 1442 t.setPosition(mPrimaryVeil, primaryBounds.left, primaryBounds.top); 1443 t.setVisibility(mPrimaryVeil, !primaryBounds.isEmpty()); 1444 } 1445 if (mSecondaryVeil != null) { 1446 t.setWindowCrop(mSecondaryVeil, secondaryBounds.width(), secondaryBounds.height()); 1447 t.setPosition(mSecondaryVeil, secondaryBounds.left, secondaryBounds.top); 1448 t.setVisibility(mSecondaryVeil, !secondaryBounds.isEmpty()); 1449 } 1450 } 1451 1452 /** 1453 * Returns the veil color. 1454 * 1455 * If the configured color is not transparent, we use the configured color, otherwise we use 1456 * the window background color of the top activity. If the background color of the top 1457 * activity is unavailable, the default color is used. 1458 */ 1459 @NonNull 1460 private static Color getVeilColor(@ColorInt int configuredColor, 1461 @NonNull TaskFragmentContainer container, @NonNull Color defaultColor) { 1462 return configuredColor != Color.TRANSPARENT 1463 ? Color.valueOf(configuredColor) 1464 : getContainerBackgroundColor(container, defaultColor); 1465 } 1466 1467 private static float[] colorToFloatArray(@NonNull Color color) { 1468 return new float[]{color.red(), color.green(), color.blue()}; 1469 } 1470 } 1471 } 1472