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