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