• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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