• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 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.shared.bubbles
18 
19 import android.content.Context
20 import android.graphics.Rect
21 import android.util.TypedValue
22 import androidx.annotation.DimenRes
23 import com.android.wm.shell.shared.R
24 import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode
25 
26 /** A class for creating drag zones for dragging bubble objects or dragging into bubbles. */
27 class DragZoneFactory(
28     private val context: Context,
29     private val deviceConfig: DeviceConfig,
30     private val splitScreenModeChecker: SplitScreenModeChecker,
31     private val desktopWindowModeChecker: DesktopWindowModeChecker,
32 ) {
33 
34     private val windowBounds: Rect
35         get() = deviceConfig.windowBounds
36 
37     private var dismissDragZoneSize = 0
38     private var bubbleDragZoneTabletSize = 0
39     private var bubbleDragZoneFoldableSize = 0
40     private var fullScreenDragZoneWidth = 0
41     private var fullScreenDragZoneHeight = 0
42     private var desktopWindowDragZoneWidth = 0
43     private var desktopWindowDragZoneHeight = 0
44     private var desktopWindowFromExpandedViewDragZoneWidth = 0
45     private var desktopWindowFromExpandedViewDragZoneHeight = 0
46     private var splitFromBubbleDragZoneHeight = 0
47     private var splitFromBubbleDragZoneWidth = 0
48     private var hSplitFromExpandedViewDragZoneWidth = 0
49     private var vSplitFromExpandedViewDragZoneWidth = 0
50     private var vSplitFromExpandedViewDragZoneHeightTablet = 0
51     private var vSplitFromExpandedViewDragZoneHeightFoldTall = 0
52     private var vSplitFromExpandedViewDragZoneHeightFoldShort = 0
53 
54     private var fullScreenDropTargetPadding = 0
55     private var desktopWindowDropTargetPaddingSmall = 0
56     private var desktopWindowDropTargetPaddingLarge = 0
57     private var expandedViewDropTargetWidth = 0
58     private var expandedViewDropTargetHeight = 0
59     private var expandedViewDropTargetPaddingBottom = 0
60     private var expandedViewDropTargetPaddingHorizontal = 0
61 
62     private val fullScreenDropTarget: Rect
63         get() =
<lambda>null64             Rect(windowBounds).apply {
65                 inset(fullScreenDropTargetPadding, fullScreenDropTargetPadding)
66             }
67 
68     private val desktopWindowDropTarget: Rect
69         get() =
<lambda>null70             Rect(windowBounds).apply {
71                 if (deviceConfig.isLandscape) {
72                     inset(
73                         /* dx= */ desktopWindowDropTargetPaddingLarge,
74                         /* dy= */ desktopWindowDropTargetPaddingSmall
75                     )
76                 } else {
77                     inset(
78                         /* dx= */ desktopWindowDropTargetPaddingSmall,
79                         /* dy= */ desktopWindowDropTargetPaddingLarge
80                     )
81                 }
82             }
83 
84     private val expandedViewDropTargetLeft: Rect
85         get() =
86             Rect(
87                 expandedViewDropTargetPaddingHorizontal,
88                 windowBounds.bottom -
89                     expandedViewDropTargetPaddingBottom -
90                     expandedViewDropTargetHeight,
91                 expandedViewDropTargetWidth + expandedViewDropTargetPaddingHorizontal,
92                 windowBounds.bottom - expandedViewDropTargetPaddingBottom
93             )
94 
95     private val expandedViewDropTargetRight: Rect
96         get() =
97             Rect(
98                 windowBounds.right -
99                     expandedViewDropTargetPaddingHorizontal -
100                     expandedViewDropTargetWidth,
101                 windowBounds.bottom -
102                     expandedViewDropTargetPaddingBottom -
103                     expandedViewDropTargetHeight,
104                 windowBounds.right - expandedViewDropTargetPaddingHorizontal,
105                 windowBounds.bottom - expandedViewDropTargetPaddingBottom
106             )
107 
108     init {
109         onConfigurationUpdated()
110     }
111 
112     /** Updates all dimensions after a configuration change. */
onConfigurationUpdatednull113     fun onConfigurationUpdated() {
114         // TODO b/396539130: Use the shared xml resources once we can easily access them from
115         //  launcher
116         dismissDragZoneSize =
117             if (deviceConfig.isSmallTablet) 140.dpToPx() else 200.dpToPx()
118         bubbleDragZoneTabletSize = 200.dpToPx()
119         bubbleDragZoneFoldableSize = 140.dpToPx()
120         fullScreenDragZoneWidth = 512.dpToPx()
121         fullScreenDragZoneHeight = 44.dpToPx()
122         desktopWindowDragZoneWidth = 880.dpToPx()
123         desktopWindowDragZoneHeight = 300.dpToPx()
124         desktopWindowFromExpandedViewDragZoneWidth = 200.dpToPx()
125         desktopWindowFromExpandedViewDragZoneHeight = 350.dpToPx()
126         splitFromBubbleDragZoneHeight = 100.dpToPx()
127         splitFromBubbleDragZoneWidth = 60.dpToPx()
128         hSplitFromExpandedViewDragZoneWidth = 60.dpToPx()
129         vSplitFromExpandedViewDragZoneWidth = 200.dpToPx()
130         vSplitFromExpandedViewDragZoneHeightTablet = 285.dpToPx()
131         vSplitFromExpandedViewDragZoneHeightFoldTall = 150.dpToPx()
132         vSplitFromExpandedViewDragZoneHeightFoldShort = 100.dpToPx()
133         fullScreenDropTargetPadding = 20.dpToPx()
134         desktopWindowDropTargetPaddingSmall = 100.dpToPx()
135         desktopWindowDropTargetPaddingLarge = 130.dpToPx()
136         expandedViewDropTargetWidth = 330.dpToPx()
137         expandedViewDropTargetHeight = 578.dpToPx()
138         expandedViewDropTargetPaddingBottom = 108.dpToPx()
139         expandedViewDropTargetPaddingHorizontal = 24.dpToPx()
140     }
141 
Contextnull142     private fun Context.resolveDimension(@DimenRes dimension: Int) =
143         resources.getDimensionPixelSize(dimension)
144 
145     private fun Int.dpToPx() =
146         TypedValue.applyDimension(
147                 TypedValue.COMPLEX_UNIT_DIP,
148                 this.toFloat(),
149                 context.resources.displayMetrics
150             )
151             .toInt()
152 
153     /**
154      * Creates the list of drag zones for the dragged object.
155      *
156      * Drag zones may have overlap, but the list is sorted by priority where the first drag zone has
157      * the highest priority so it should be checked first.
158      */
159     fun createSortedDragZones(draggedObject: DraggedObject): List<DragZone> {
160         val dragZones = mutableListOf<DragZone>()
161         when (draggedObject) {
162             is DraggedObject.BubbleBar -> {
163                 dragZones.add(createDismissDragZone())
164                 dragZones.addAll(createBubbleHalfScreenDragZones())
165             }
166             is DraggedObject.Bubble -> {
167                 dragZones.add(createDismissDragZone())
168                 dragZones.addAll(createBubbleCornerDragZones())
169                 dragZones.add(createFullScreenDragZone())
170                 if (shouldShowDesktopWindowDragZones()) {
171                     dragZones.add(createDesktopWindowDragZoneForBubble())
172                 }
173                 dragZones.addAll(createSplitScreenDragZonesForBubble())
174             }
175             is DraggedObject.ExpandedView -> {
176                 dragZones.add(createDismissDragZone())
177                 dragZones.add(createFullScreenDragZone())
178                 if (shouldShowDesktopWindowDragZones()) {
179                     dragZones.add(createDesktopWindowDragZoneForExpandedView())
180                 }
181                 if (deviceConfig.isSmallTablet) {
182                     dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnFoldable())
183                 } else {
184                     dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnTablet())
185                 }
186                 dragZones.addAll(createBubbleHalfScreenDragZones())
187             }
188         }
189         return dragZones
190     }
191 
createDismissDragZonenull192     private fun createDismissDragZone(): DragZone {
193         return DragZone.Dismiss(
194             bounds =
195                 Rect(
196                     windowBounds.right / 2 - dismissDragZoneSize / 2,
197                     windowBounds.bottom - dismissDragZoneSize,
198                     windowBounds.right / 2 + dismissDragZoneSize / 2,
199                     windowBounds.bottom
200                 )
201         )
202     }
203 
createBubbleCornerDragZonesnull204     private fun createBubbleCornerDragZones(): List<DragZone> {
205         val dragZoneSize =
206             if (deviceConfig.isSmallTablet) {
207                 bubbleDragZoneFoldableSize
208             } else {
209                 bubbleDragZoneTabletSize
210             }
211         return listOf(
212             DragZone.Bubble.Left(
213                 bounds =
214                     Rect(0, windowBounds.bottom - dragZoneSize, dragZoneSize, windowBounds.bottom),
215                 dropTarget = expandedViewDropTargetLeft,
216             ),
217             DragZone.Bubble.Right(
218                 bounds =
219                     Rect(
220                         windowBounds.right - dragZoneSize,
221                         windowBounds.bottom - dragZoneSize,
222                         windowBounds.right,
223                         windowBounds.bottom,
224                     ),
225                 dropTarget = expandedViewDropTargetRight,
226             )
227         )
228     }
229 
createBubbleHalfScreenDragZonesnull230     private fun createBubbleHalfScreenDragZones(): List<DragZone> {
231         return listOf(
232             DragZone.Bubble.Left(
233                 bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom),
234                 dropTarget = expandedViewDropTargetLeft,
235             ),
236             DragZone.Bubble.Right(
237                 bounds =
238                     Rect(
239                         windowBounds.right / 2,
240                         0,
241                         windowBounds.right,
242                         windowBounds.bottom,
243                     ),
244                 dropTarget = expandedViewDropTargetRight,
245             )
246         )
247     }
248 
createFullScreenDragZonenull249     private fun createFullScreenDragZone(): DragZone {
250         return DragZone.FullScreen(
251             bounds =
252                 Rect(
253                     windowBounds.right / 2 - fullScreenDragZoneWidth / 2,
254                     0,
255                     windowBounds.right / 2 + fullScreenDragZoneWidth / 2,
256                     fullScreenDragZoneHeight
257                 ),
258             dropTarget = fullScreenDropTarget
259         )
260     }
261 
shouldShowDesktopWindowDragZonesnull262     private fun shouldShowDesktopWindowDragZones() =
263         !deviceConfig.isSmallTablet && desktopWindowModeChecker.isSupported()
264 
265     private fun createDesktopWindowDragZoneForBubble(): DragZone {
266         return DragZone.DesktopWindow(
267             bounds =
268                 if (deviceConfig.isLandscape) {
269                     Rect(
270                         windowBounds.right / 2 - desktopWindowDragZoneWidth / 2,
271                         windowBounds.bottom / 2 - desktopWindowDragZoneHeight / 2,
272                         windowBounds.right / 2 + desktopWindowDragZoneWidth / 2,
273                         windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2
274                     )
275                 } else {
276                     Rect(
277                         0,
278                         windowBounds.bottom / 2 - desktopWindowDragZoneHeight / 2,
279                         windowBounds.right,
280                         windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2
281                     )
282                 },
283             dropTarget = desktopWindowDropTarget
284         )
285     }
286 
createDesktopWindowDragZoneForExpandedViewnull287     private fun createDesktopWindowDragZoneForExpandedView(): DragZone {
288         return DragZone.DesktopWindow(
289             bounds =
290                 Rect(
291                     windowBounds.right / 2 - desktopWindowFromExpandedViewDragZoneWidth / 2,
292                     windowBounds.bottom / 2 - desktopWindowFromExpandedViewDragZoneHeight / 2,
293                     windowBounds.right / 2 + desktopWindowFromExpandedViewDragZoneWidth / 2,
294                     windowBounds.bottom / 2 + desktopWindowFromExpandedViewDragZoneHeight / 2
295                 ),
296             dropTarget = desktopWindowDropTarget
297         )
298     }
299 
createSplitScreenDragZonesForBubblenull300     private fun createSplitScreenDragZonesForBubble(): List<DragZone> {
301         // for foldables in landscape mode or tables in portrait modes we have vertical split drag
302         // zones. otherwise we have horizontal split drag zones.
303         val isVerticalSplit = deviceConfig.isSmallTablet == deviceConfig.isLandscape
304         return if (isVerticalSplit) {
305             when (splitScreenModeChecker.getSplitScreenMode()) {
306                 SplitScreenMode.UNSUPPORTED -> emptyList()
307                 SplitScreenMode.SPLIT_50_50,
308                 SplitScreenMode.NONE ->
309                     listOf(
310                         DragZone.Split.Top(
311                             bounds = Rect(0, 0, windowBounds.right, windowBounds.bottom / 2),
312                         ),
313                         DragZone.Split.Bottom(
314                             bounds =
315                                 Rect(
316                                     0,
317                                     windowBounds.bottom / 2,
318                                     windowBounds.right,
319                                     windowBounds.bottom
320                                 ),
321                         )
322                     )
323                 SplitScreenMode.SPLIT_90_10 -> {
324                     listOf(
325                         DragZone.Split.Top(
326                             bounds =
327                                 Rect(
328                                     0,
329                                     0,
330                                     windowBounds.right,
331                                     windowBounds.bottom - splitFromBubbleDragZoneHeight
332                                 ),
333                         ),
334                         DragZone.Split.Bottom(
335                             bounds =
336                                 Rect(
337                                     0,
338                                     windowBounds.bottom - splitFromBubbleDragZoneHeight,
339                                     windowBounds.right,
340                                     windowBounds.bottom
341                                 ),
342                         )
343                     )
344                 }
345                 SplitScreenMode.SPLIT_10_90 -> {
346                     listOf(
347                         DragZone.Split.Top(
348                             bounds = Rect(0, 0, windowBounds.right, splitFromBubbleDragZoneHeight),
349                         ),
350                         DragZone.Split.Bottom(
351                             bounds =
352                                 Rect(
353                                     0,
354                                     splitFromBubbleDragZoneHeight,
355                                     windowBounds.right,
356                                     windowBounds.bottom
357                                 ),
358                         )
359                     )
360                 }
361             }
362         } else {
363             when (splitScreenModeChecker.getSplitScreenMode()) {
364                 SplitScreenMode.UNSUPPORTED -> emptyList()
365                 SplitScreenMode.SPLIT_50_50,
366                 SplitScreenMode.NONE ->
367                     listOf(
368                         DragZone.Split.Left(
369                             bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom),
370                         ),
371                         DragZone.Split.Right(
372                             bounds =
373                                 Rect(
374                                     windowBounds.right / 2,
375                                     0,
376                                     windowBounds.right,
377                                     windowBounds.bottom
378                                 ),
379                         )
380                     )
381                 SplitScreenMode.SPLIT_90_10 ->
382                     listOf(
383                         DragZone.Split.Left(
384                             bounds =
385                                 Rect(
386                                     0,
387                                     0,
388                                     windowBounds.right - splitFromBubbleDragZoneWidth,
389                                     windowBounds.bottom
390                                 ),
391                         ),
392                         DragZone.Split.Right(
393                             bounds =
394                                 Rect(
395                                     windowBounds.right - splitFromBubbleDragZoneWidth,
396                                     0,
397                                     windowBounds.right,
398                                     windowBounds.bottom
399                                 ),
400                         )
401                     )
402                 SplitScreenMode.SPLIT_10_90 ->
403                     listOf(
404                         DragZone.Split.Left(
405                             bounds = Rect(0, 0, splitFromBubbleDragZoneWidth, windowBounds.bottom),
406                         ),
407                         DragZone.Split.Right(
408                             bounds =
409                                 Rect(
410                                     splitFromBubbleDragZoneWidth,
411                                     0,
412                                     windowBounds.right,
413                                     windowBounds.bottom
414                                 ),
415                         )
416                     )
417             }
418         }
419     }
420 
createSplitScreenDragZonesForExpandedViewOnTabletnull421     private fun createSplitScreenDragZonesForExpandedViewOnTablet(): List<DragZone> {
422         return if (deviceConfig.isLandscape) {
423             createHorizontalSplitDragZonesForExpandedView()
424         } else {
425             // for tablets in portrait mode, split drag zones appear below the full screen drag zone
426             // for the top split zone, and above the dismiss zone. Both are horizontally centered.
427             val splitZoneLeft = windowBounds.right / 2 - vSplitFromExpandedViewDragZoneWidth / 2
428             val splitZoneRight = splitZoneLeft + vSplitFromExpandedViewDragZoneWidth
429             val bottomSplitZoneBottom = windowBounds.bottom - dismissDragZoneSize
430             listOf(
431                 DragZone.Split.Top(
432                     bounds =
433                         Rect(
434                             splitZoneLeft,
435                             fullScreenDragZoneHeight,
436                             splitZoneRight,
437                             fullScreenDragZoneHeight + vSplitFromExpandedViewDragZoneHeightTablet
438                         ),
439                 ),
440                 DragZone.Split.Bottom(
441                     bounds =
442                         Rect(
443                             splitZoneLeft,
444                             bottomSplitZoneBottom - vSplitFromExpandedViewDragZoneHeightTablet,
445                             splitZoneRight,
446                             bottomSplitZoneBottom
447                         ),
448                 )
449             )
450         }
451     }
452 
createSplitScreenDragZonesForExpandedViewOnFoldablenull453     private fun createSplitScreenDragZonesForExpandedViewOnFoldable(): List<DragZone> {
454         return if (deviceConfig.isLandscape) {
455             // vertical split drag zones are aligned with the full screen drag zone width
456             val splitZoneLeft = windowBounds.right / 2 - fullScreenDragZoneWidth / 2
457             when (splitScreenModeChecker.getSplitScreenMode()) {
458                 SplitScreenMode.UNSUPPORTED -> emptyList()
459                 SplitScreenMode.SPLIT_50_50,
460                 SplitScreenMode.NONE ->
461                     listOf(
462                         DragZone.Split.Top(
463                             bounds =
464                                 Rect(
465                                     splitZoneLeft,
466                                     fullScreenDragZoneHeight,
467                                     splitZoneLeft + fullScreenDragZoneWidth,
468                                     fullScreenDragZoneHeight +
469                                         vSplitFromExpandedViewDragZoneHeightFoldTall
470                                 ),
471                         ),
472                         DragZone.Split.Bottom(
473                             bounds =
474                                 Rect(
475                                     splitZoneLeft,
476                                     windowBounds.bottom / 2,
477                                     splitZoneLeft + fullScreenDragZoneWidth,
478                                     windowBounds.bottom / 2 +
479                                         vSplitFromExpandedViewDragZoneHeightFoldTall
480                                 ),
481                         )
482                     )
483                 SplitScreenMode.SPLIT_10_90 ->
484                     listOf(
485                         DragZone.Split.Top(
486                             bounds =
487                                 Rect(
488                                     0,
489                                     0,
490                                     windowBounds.right,
491                                     vSplitFromExpandedViewDragZoneHeightFoldShort
492                                 ),
493                         ),
494                         DragZone.Split.Bottom(
495                             bounds =
496                                 Rect(
497                                     splitZoneLeft,
498                                     vSplitFromExpandedViewDragZoneHeightFoldShort,
499                                     splitZoneLeft + fullScreenDragZoneWidth,
500                                     vSplitFromExpandedViewDragZoneHeightFoldShort +
501                                         vSplitFromExpandedViewDragZoneHeightFoldTall
502                                 ),
503                         )
504                     )
505                 SplitScreenMode.SPLIT_90_10 ->
506                     listOf(
507                         DragZone.Split.Top(
508                             bounds =
509                                 Rect(
510                                     splitZoneLeft,
511                                     fullScreenDragZoneHeight,
512                                     splitZoneLeft + fullScreenDragZoneWidth,
513                                     fullScreenDragZoneHeight +
514                                         vSplitFromExpandedViewDragZoneHeightFoldTall
515                                 ),
516                         ),
517                         DragZone.Split.Bottom(
518                             bounds =
519                                 Rect(
520                                     0,
521                                     windowBounds.bottom -
522                                         vSplitFromExpandedViewDragZoneHeightFoldShort,
523                                     windowBounds.right,
524                                     windowBounds.bottom
525                                 ),
526                         )
527                     )
528             }
529         } else {
530             // horizontal split drag zones
531             createHorizontalSplitDragZonesForExpandedView()
532         }
533     }
534 
createHorizontalSplitDragZonesForExpandedViewnull535     private fun createHorizontalSplitDragZonesForExpandedView(): List<DragZone> {
536         // horizontal split drag zones for expanded view appear on the edges of the screen from the
537         // top down until the dismiss drag zone height
538         return listOf(
539             DragZone.Split.Left(
540                 bounds =
541                     Rect(
542                         0,
543                         0,
544                         hSplitFromExpandedViewDragZoneWidth,
545                         windowBounds.bottom - dismissDragZoneSize
546                     ),
547             ),
548             DragZone.Split.Right(
549                 bounds =
550                     Rect(
551                         windowBounds.right - hSplitFromExpandedViewDragZoneWidth,
552                         0,
553                         windowBounds.right,
554                         windowBounds.bottom - dismissDragZoneSize
555                     ),
556             )
557         )
558     }
559 
560     /** Checks the current split screen mode. */
interfacenull561     fun interface SplitScreenModeChecker {
562         enum class SplitScreenMode {
563             NONE,
564             SPLIT_50_50,
565             SPLIT_10_90,
566             SPLIT_90_10,
567             UNSUPPORTED
568         }
569 
570         fun getSplitScreenMode(): SplitScreenMode
571     }
572 
573     /** Checks if desktop window mode is supported. */
interfacenull574     fun interface DesktopWindowModeChecker {
575         fun isSupported(): Boolean
576     }
577 }
578