• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.desktopmode;
18 
19 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
21 
22 import static com.android.internal.policy.SystemBarUtils.getDesktopViewAppHeaderHeightPx;
23 import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR;
24 import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_LEFT_INDICATOR;
25 import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR;
26 import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR;
27 import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR;
28 import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR;
29 import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR;
30 import static com.android.wm.shell.shared.ShellSharedConstants.SMALL_TABLET_MAX_EDGE_DP;
31 
32 import android.annotation.NonNull;
33 import android.annotation.Nullable;
34 import android.app.ActivityManager;
35 import android.content.Context;
36 import android.graphics.PointF;
37 import android.graphics.Rect;
38 import android.graphics.Region;
39 import android.util.Pair;
40 import android.view.Display;
41 import android.view.SurfaceControl;
42 import android.window.DesktopModeFlags;
43 
44 import androidx.annotation.VisibleForTesting;
45 
46 import com.android.internal.policy.SystemBarUtils;
47 import com.android.wm.shell.R;
48 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
49 import com.android.wm.shell.common.DisplayController;
50 import com.android.wm.shell.common.DisplayLayout;
51 import com.android.wm.shell.common.ShellExecutor;
52 import com.android.wm.shell.common.SyncTransactionQueue;
53 import com.android.wm.shell.common.split.SplitScreenUtils;
54 import com.android.wm.shell.shared.annotations.ShellDesktopThread;
55 import com.android.wm.shell.shared.annotations.ShellMainThread;
56 import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
57 import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider;
58 import com.android.wm.shell.windowdecor.tiling.SnapEventHandler;
59 
60 import java.util.ArrayList;
61 import java.util.Collections;
62 import java.util.List;
63 
64 /**
65  * Animated visual indicator for Desktop Mode windowing transitions.
66  */
67 public class DesktopModeVisualIndicator {
68     public enum IndicatorType {
69         /** To be used when we don't want to indicate any transition */
70         NO_INDICATOR,
71         /** Indicates impending transition into desktop mode */
72         TO_DESKTOP_INDICATOR,
73         /** Indicates impending transition into fullscreen */
74         TO_FULLSCREEN_INDICATOR,
75         /** Indicates impending transition into split select on the left side */
76         TO_SPLIT_LEFT_INDICATOR,
77         /** Indicates impending transition into split select on the right side */
78         TO_SPLIT_RIGHT_INDICATOR,
79         /** Indicates impending transition into bubble on the left side */
80         TO_BUBBLE_LEFT_INDICATOR,
81         /** Indicates impending transition into bubble on the right side */
82         TO_BUBBLE_RIGHT_INDICATOR
83     }
84 
85     /**
86      * The conditions surrounding the drag event that led to the indicator's creation.
87      */
88     public enum DragStartState {
89         /** The indicator is resulting from a freeform task drag. */
90         FROM_FREEFORM,
91         /** The indicator is resulting from a split screen task drag */
92         FROM_SPLIT,
93         /** The indicator is resulting from a fullscreen task drag */
94         FROM_FULLSCREEN,
95         /** The indicator is resulting from an Intent generated during a drag-and-drop event */
96         DRAGGED_INTENT;
97 
98         /**
99          * Get the {@link DragStartState} of a drag event based on the windowing mode of the task.
100          * Note that DRAGGED_INTENT will be specified by the caller if needed and not returned
101          * here.
102          */
getDragStartState( ActivityManager.RunningTaskInfo taskInfo )103         public static DesktopModeVisualIndicator.DragStartState getDragStartState(
104                 ActivityManager.RunningTaskInfo taskInfo
105         ) {
106             if (taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
107                 return FROM_FULLSCREEN;
108             } else if (taskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) {
109                 return FROM_SPLIT;
110             } else if (taskInfo.isFreeform()) {
111                 return FROM_FREEFORM;
112             } else {
113                 return null;
114             }
115         }
116 
isDragToDesktopStartState(DragStartState startState)117         private static boolean isDragToDesktopStartState(DragStartState startState) {
118             return startState == FROM_FULLSCREEN || startState == FROM_SPLIT;
119         }
120     }
121 
122     private final VisualIndicatorViewContainer mVisualIndicatorViewContainer;
123 
124     private final Context mContext;
125     private final DisplayController mDisplayController;
126     private final ActivityManager.RunningTaskInfo mTaskInfo;
127 
128     private IndicatorType mCurrentType;
129     private final DragStartState mDragStartState;
130     private final SnapEventHandler mSnapEventHandler;
131 
132     private final boolean mUseSmallTabletRegions;
133     /**
134      * Ordered list of {@link Rect} zones that we will match an input coordinate against.
135      * List is traversed from first to last element. The first rect that contains the input event
136      * will be used and the matching {@link IndicatorType} is returned.
137      * Empty rect matches all.
138      */
139     private final List<Pair<Rect, IndicatorType>> mSortedRegions;
140 
DesktopModeVisualIndicator(@hellDesktopThread ShellExecutor desktopExecutor, @ShellMainThread ShellExecutor mainExecutor, SyncTransactionQueue syncQueue, ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController, Context context, SurfaceControl taskSurface, RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer, DragStartState dragStartState, @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider, SnapEventHandler snapEventHandler)141     public DesktopModeVisualIndicator(@ShellDesktopThread ShellExecutor desktopExecutor,
142             @ShellMainThread ShellExecutor mainExecutor,
143             SyncTransactionQueue syncQueue,
144             ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController,
145             Context context, SurfaceControl taskSurface,
146             RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer,
147             DragStartState dragStartState,
148             @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider,
149             SnapEventHandler snapEventHandler) {
150         this(desktopExecutor, mainExecutor, syncQueue, taskInfo, displayController, context,
151                 taskSurface, taskDisplayAreaOrganizer, dragStartState, bubbleBoundsProvider,
152                 snapEventHandler, useSmallTabletRegions(displayController, taskInfo),
153                 isLeftRightSplit(context, displayController, taskInfo));
154     }
155 
156     @VisibleForTesting
DesktopModeVisualIndicator(@hellDesktopThread ShellExecutor desktopExecutor, @ShellMainThread ShellExecutor mainExecutor, SyncTransactionQueue syncQueue, ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController, Context context, SurfaceControl taskSurface, RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer, DragStartState dragStartState, @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider, SnapEventHandler snapEventHandler, boolean useSmallTabletRegions, boolean isLeftRightSplit)157     DesktopModeVisualIndicator(@ShellDesktopThread ShellExecutor desktopExecutor,
158             @ShellMainThread ShellExecutor mainExecutor,
159             SyncTransactionQueue syncQueue,
160             ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController,
161             Context context, SurfaceControl taskSurface,
162             RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer,
163             DragStartState dragStartState,
164             @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider,
165             SnapEventHandler snapEventHandler,
166             boolean useSmallTabletRegions,
167             boolean isLeftRightSplit) {
168         SurfaceControl.Builder builder = new SurfaceControl.Builder();
169         if (!DragStartState.isDragToDesktopStartState(dragStartState)
170                 || !DesktopModeFlags.ENABLE_VISUAL_INDICATOR_IN_TRANSITION_BUGFIX.isTrue()) {
171             // In the DragToDesktop transition we attach the indicator to the transition root once
172             // that is available - for all other cases attach the indicator here.
173             taskDisplayAreaOrganizer.attachToDisplayArea(taskInfo.displayId, builder);
174         }
175         mVisualIndicatorViewContainer = new VisualIndicatorViewContainer(
176                 DesktopModeFlags.ENABLE_DESKTOP_INDICATOR_IN_SEPARATE_THREAD_BUGFIX.isTrue()
177                         ? desktopExecutor : mainExecutor,
178                 mainExecutor, builder, syncQueue, bubbleBoundsProvider, snapEventHandler);
179         mTaskInfo = taskInfo;
180         mDisplayController = displayController;
181         mContext = context;
182         mCurrentType = NO_INDICATOR;
183         mDragStartState = dragStartState;
184         mSnapEventHandler = snapEventHandler;
185         Display display = mDisplayController.getDisplay(mTaskInfo.displayId);
186         DisplayLayout displayLayout = mDisplayController.getDisplayLayout(mTaskInfo.displayId);
187         mVisualIndicatorViewContainer.createView(
188                 mContext,
189                 display,
190                 displayLayout,
191                 mTaskInfo,
192                 taskSurface
193         );
194 
195         mUseSmallTabletRegions = useSmallTabletRegions;
196 
197         if (useSmallTabletRegions) {
198             mSortedRegions = initSmallTabletRegions(displayLayout, isLeftRightSplit);
199         } else {
200             // TODO(b/401596837): add support for initializing regions for large tablets
201             mSortedRegions = Collections.emptyList();
202         }
203     }
204 
useSmallTabletRegions(DisplayController displayController, ActivityManager.RunningTaskInfo taskInfo)205     private static boolean useSmallTabletRegions(DisplayController displayController,
206             ActivityManager.RunningTaskInfo taskInfo) {
207         if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
208             // Small tablet regions get enabled with bubbles feature
209             return false;
210         }
211         Display display = displayController.getDisplay(taskInfo.displayId);
212         DisplayLayout displayLayout = displayController.getDisplayLayout(taskInfo.displayId);
213         if (displayLayout == null) return false;
214         return displayLayout.pxToDp(display.getMaximumSizeDimension()) < SMALL_TABLET_MAX_EDGE_DP;
215     }
216 
isLeftRightSplit(Context context, DisplayController displayController, ActivityManager.RunningTaskInfo taskInfo)217     private static boolean isLeftRightSplit(Context context, DisplayController displayController,
218             ActivityManager.RunningTaskInfo taskInfo) {
219         DisplayLayout layout = displayController.getDisplayLayout(taskInfo.displayId);
220         boolean landscape = layout != null && layout.isLandscape();
221         boolean leftRightSplitInPortrait = SplitScreenUtils.allowLeftRightSplitInPortrait(
222                 context.getResources());
223         return SplitScreenUtils.isLeftRightSplit(leftRightSplitInPortrait,
224                 /* isLargeScreen= */ true, landscape);
225     }
226 
227     /** Start the fade out animation, running the callback on the main thread once it is done. */
fadeOutIndicator( @onNull Runnable callback)228     public void fadeOutIndicator(
229             @NonNull Runnable callback) {
230         mVisualIndicatorViewContainer.fadeOutIndicator(
231                 mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType, callback,
232                 mTaskInfo.displayId, mSnapEventHandler
233         );
234     }
235 
236     /** Release the visual indicator view and its viewhost. */
releaseVisualIndicator()237     public void releaseVisualIndicator() {
238         mVisualIndicatorViewContainer.releaseVisualIndicator();
239     }
240 
241     /** Reparent the visual indicator to {@code newParent}. */
reparentLeash(SurfaceControl.Transaction t, SurfaceControl newParent)242     void reparentLeash(SurfaceControl.Transaction t, SurfaceControl newParent) {
243         mVisualIndicatorViewContainer.reparentLeash(t, newParent);
244     }
245 
246     /** Start the fade-in animation. */
fadeInIndicator()247     void fadeInIndicator() {
248         if (mCurrentType == NO_INDICATOR) return;
249         mVisualIndicatorViewContainer.fadeInIndicator(
250                 mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType,
251                 mTaskInfo.displayId);
252     }
253 
254     /**
255      * Based on the coordinates of the current drag event, determine which indicator type we should
256      * display, including no visible indicator.
257      */
258     @NonNull
updateIndicatorType(PointF inputCoordinates)259     IndicatorType updateIndicatorType(PointF inputCoordinates) {
260         final IndicatorType result;
261         if (mUseSmallTabletRegions) {
262             result = getIndicatorSmallTablet(inputCoordinates);
263         } else {
264             result = getIndicatorLargeTablet(inputCoordinates);
265         }
266         if (mDragStartState != DragStartState.DRAGGED_INTENT) {
267             mVisualIndicatorViewContainer.transitionIndicator(
268                     mTaskInfo, mDisplayController, mCurrentType, result
269             );
270             mCurrentType = result;
271         }
272         return result;
273     }
274 
275     @NonNull
getIndicatorLargeTablet(PointF inputCoordinates)276     private IndicatorType getIndicatorLargeTablet(PointF inputCoordinates) {
277         // TODO(b/401596837): cache the regions to avoid recalculating on each motion event
278         final DisplayLayout layout = mDisplayController.getDisplayLayout(mTaskInfo.displayId);
279         // Perform a quick check first: any input off the left edge of the display should be split
280         // left, and split right for the right edge. This is universal across all drag event types.
281         if (inputCoordinates.x < 0) return TO_SPLIT_LEFT_INDICATOR;
282         if (inputCoordinates.x > layout.width()) return TO_SPLIT_RIGHT_INDICATOR;
283         // If we are in freeform, we don't want a visible indicator in the "freeform" drag zone.
284         // In drags not originating on a freeform caption, we should default to a TO_DESKTOP
285         // indicator.
286         IndicatorType result = mDragStartState == DragStartState.FROM_FREEFORM
287                     ? NO_INDICATOR
288                     : TO_DESKTOP_INDICATOR;
289         final int transitionAreaWidth = mContext.getResources().getDimensionPixelSize(
290                 com.android.wm.shell.R.dimen.desktop_mode_transition_region_thickness);
291         // Because drags in freeform use task position for indicator calculation, we need to
292         // account for the possibility of the task going off the top of the screen by captionHeight
293         final int captionHeight = getDesktopViewAppHeaderHeightPx(mContext);
294         final Region fullscreenRegion = calculateFullscreenRegion(layout, captionHeight);
295         final Rect splitLeftRegion = calculateSplitLeftRegion(layout, transitionAreaWidth,
296                 captionHeight);
297         final Rect splitRightRegion = calculateSplitRightRegion(layout, transitionAreaWidth,
298                 captionHeight);
299         final int x = (int) inputCoordinates.x;
300         final int y = (int) inputCoordinates.y;
301         if (fullscreenRegion.contains(x, y)) {
302             result = TO_FULLSCREEN_INDICATOR;
303         }
304         if (splitLeftRegion.contains(x, y)) {
305             result = IndicatorType.TO_SPLIT_LEFT_INDICATOR;
306         }
307         if (splitRightRegion.contains(x, y)) {
308             result = IndicatorType.TO_SPLIT_RIGHT_INDICATOR;
309         }
310         if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()
311                 && mDragStartState == DragStartState.FROM_FULLSCREEN) {
312             if (calculateBubbleLeftRegion(layout).contains(x, y)) {
313                 result = IndicatorType.TO_BUBBLE_LEFT_INDICATOR;
314             } else if (calculateBubbleRightRegion(layout).contains(x, y)) {
315                 result = TO_BUBBLE_RIGHT_INDICATOR;
316             }
317         }
318         return result;
319     }
320 
321     @NonNull
getIndicatorSmallTablet(PointF inputCoordinates)322     private IndicatorType getIndicatorSmallTablet(PointF inputCoordinates) {
323         for (Pair<Rect, IndicatorType> region : mSortedRegions) {
324             if (region.first.isEmpty()) return region.second; // empty rect matches all
325             if (region.first.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) {
326                 return region.second;
327             }
328         }
329         return NO_INDICATOR;
330     }
331 
332     /**
333      * Returns the [DragStartState] of the visual indicator.
334      */
getDragStartState()335     DragStartState getDragStartState() {
336         return mDragStartState;
337     }
338 
339     @VisibleForTesting
calculateFullscreenRegion(DisplayLayout layout, int captionHeight)340     Region calculateFullscreenRegion(DisplayLayout layout, int captionHeight) {
341         final Region region = new Region();
342         int transitionHeight = mDragStartState == DragStartState.FROM_FREEFORM
343                 || mDragStartState == DragStartState.DRAGGED_INTENT
344                 ? SystemBarUtils.getStatusBarHeight(mContext)
345                 : 2 * layout.stableInsets().top;
346         // A Rect at the top of the screen that takes up the center 40%.
347         if (mDragStartState == DragStartState.FROM_FREEFORM) {
348             final float toFullscreenScale = mContext.getResources().getFloat(
349                     R.dimen.desktop_mode_fullscreen_region_scale);
350             final float toFullscreenWidth = (layout.width() * toFullscreenScale);
351             region.union(new Rect((int) ((layout.width() / 2f) - (toFullscreenWidth / 2f)),
352                     Short.MIN_VALUE,
353                     (int) ((layout.width() / 2f) + (toFullscreenWidth / 2f)),
354                     transitionHeight));
355         }
356         // A screen-wide Rect if the task is in fullscreen, split, or a dragged intent.
357         if (mDragStartState == DragStartState.FROM_FULLSCREEN
358                 || mDragStartState == DragStartState.FROM_SPLIT
359                 || mDragStartState == DragStartState.DRAGGED_INTENT
360         ) {
361             region.union(new Rect(0,
362                     Short.MIN_VALUE,
363                     layout.width(),
364                     transitionHeight));
365         }
366         return region;
367     }
368 
369     @VisibleForTesting
calculateSplitLeftRegion(DisplayLayout layout, int transitionEdgeWidth, int captionHeight)370     Rect calculateSplitLeftRegion(DisplayLayout layout,
371             int transitionEdgeWidth, int captionHeight) {
372         // In freeform, keep the top corners clear.
373         int transitionHeight = mDragStartState == DragStartState.FROM_FREEFORM
374                 ? mContext.getResources().getDimensionPixelSize(
375                 com.android.wm.shell.R.dimen.desktop_mode_split_from_desktop_height) :
376                 -captionHeight;
377         return new Rect(0, transitionHeight, transitionEdgeWidth, layout.height());
378     }
379 
380     @VisibleForTesting
calculateSplitRightRegion(DisplayLayout layout, int transitionEdgeWidth, int captionHeight)381     Rect calculateSplitRightRegion(DisplayLayout layout,
382             int transitionEdgeWidth, int captionHeight) {
383         // In freeform, keep the top corners clear.
384         int transitionHeight = mDragStartState == DragStartState.FROM_FREEFORM
385                 ? mContext.getResources().getDimensionPixelSize(
386                 com.android.wm.shell.R.dimen.desktop_mode_split_from_desktop_height) :
387                 -captionHeight;
388         return new Rect(layout.width() - transitionEdgeWidth, transitionHeight,
389                 layout.width(), layout.height());
390     }
391 
392     @VisibleForTesting
calculateBubbleLeftRegion(DisplayLayout layout)393     Rect calculateBubbleLeftRegion(DisplayLayout layout) {
394         int regionSize = getBubbleRegionSize();
395         return new Rect(0, layout.height() - regionSize, regionSize, layout.height());
396     }
397 
398     @VisibleForTesting
calculateBubbleRightRegion(DisplayLayout layout)399     Rect calculateBubbleRightRegion(DisplayLayout layout) {
400         int regionSize = getBubbleRegionSize();
401         return new Rect(layout.width() - regionSize, layout.height() - regionSize,
402                 layout.width(), layout.height());
403     }
404 
getBubbleRegionSize()405     private int getBubbleRegionSize() {
406         int resId = mUseSmallTabletRegions
407                 ? com.android.wm.shell.shared.R.dimen.drag_zone_bubble_fold
408                 : com.android.wm.shell.shared.R.dimen.drag_zone_bubble_tablet;
409         return mContext.getResources().getDimensionPixelSize(resId);
410     }
411 
412     @VisibleForTesting
getIndicatorBounds()413     Rect getIndicatorBounds() {
414         return mVisualIndicatorViewContainer.getIndicatorBounds();
415     }
416 
initSmallTabletRegions(DisplayLayout layout, boolean isLeftRightSplit)417     private List<Pair<Rect, IndicatorType>> initSmallTabletRegions(DisplayLayout layout,
418             boolean isLeftRightSplit) {
419         return switch (mDragStartState) {
420             case FROM_FULLSCREEN -> initSmallTabletRegionsFromFullscreen(layout, isLeftRightSplit);
421             case FROM_SPLIT -> initSmallTabletRegionsFromSplit(layout, isLeftRightSplit);
422             default -> Collections.emptyList();
423         };
424     }
425 
initSmallTabletRegionsFromFullscreen( DisplayLayout layout, boolean isLeftRightSplit)426     private List<Pair<Rect, IndicatorType>> initSmallTabletRegionsFromFullscreen(
427             DisplayLayout layout, boolean isLeftRightSplit) {
428 
429         List<Pair<Rect, IndicatorType>> result = new ArrayList<>();
430         if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
431             result.add(new Pair<>(calculateBubbleLeftRegion(layout), TO_BUBBLE_LEFT_INDICATOR));
432             result.add(new Pair<>(calculateBubbleRightRegion(layout), TO_BUBBLE_RIGHT_INDICATOR));
433         }
434 
435         if (isLeftRightSplit) {
436             int splitRegionWidth = mContext.getResources().getDimensionPixelSize(
437                     com.android.wm.shell.shared.R.dimen.drag_zone_h_split_from_app_width_fold);
438             result.add(new Pair<>(calculateSplitLeftRegion(layout, splitRegionWidth,
439                     /* captionHeight= */ 0), TO_SPLIT_LEFT_INDICATOR));
440             result.add(new Pair<>(calculateSplitRightRegion(layout, splitRegionWidth,
441                     /* captionHeight= */ 0), TO_SPLIT_RIGHT_INDICATOR));
442         }
443         // TODO(b/401352409): add support for top/bottom split zones
444         // default to fullscreen
445         result.add(new Pair<>(new Rect(), TO_FULLSCREEN_INDICATOR));
446         return result;
447     }
448 
initSmallTabletRegionsFromSplit(DisplayLayout layout, boolean isLeftRightSplit)449     private List<Pair<Rect, IndicatorType>> initSmallTabletRegionsFromSplit(DisplayLayout layout,
450             boolean isLeftRightSplit) {
451         if (!isLeftRightSplit) {
452             // Dragging a top/bottom split is not supported on small tablets
453             return Collections.emptyList();
454         }
455 
456         List<Pair<Rect, IndicatorType>> result = new ArrayList<>();
457         if (BubbleAnythingFlagHelper.enableBubbleAnything()) {
458             result.add(new Pair<>(calculateBubbleLeftRegion(layout), TO_BUBBLE_LEFT_INDICATOR));
459             result.add(new Pair<>(calculateBubbleRightRegion(layout), TO_BUBBLE_RIGHT_INDICATOR));
460         }
461 
462         int splitRegionWidth = mContext.getResources().getDimensionPixelSize(
463                 com.android.wm.shell.shared.R.dimen.drag_zone_h_split_from_app_width_fold);
464         result.add(new Pair<>(calculateSplitLeftRegion(layout, splitRegionWidth,
465                 /* captionHeight= */ 0), TO_SPLIT_LEFT_INDICATOR));
466         result.add(new Pair<>(calculateSplitRightRegion(layout, splitRegionWidth,
467                 /* captionHeight= */ 0), TO_SPLIT_RIGHT_INDICATOR));
468         // default to fullscreen
469         result.add(new Pair<>(new Rect(), TO_FULLSCREEN_INDICATOR));
470         return result;
471     }
472 }
473