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 package com.android.wm.shell.bubbles 17 18 import android.content.Context 19 import android.content.Intent 20 import android.content.pm.ShortcutInfo 21 import android.content.res.Resources 22 import android.graphics.Insets 23 import android.graphics.PointF 24 import android.graphics.Rect 25 import android.os.UserHandle 26 import android.view.WindowManager 27 import androidx.test.core.app.ApplicationProvider 28 import androidx.test.ext.junit.runners.AndroidJUnit4 29 import androidx.test.filters.SmallTest 30 import com.android.internal.protolog.ProtoLog 31 import com.android.wm.shell.R 32 import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT 33 import com.android.wm.shell.shared.bubbles.BubbleBarLocation 34 import com.android.wm.shell.shared.bubbles.DeviceConfig 35 import com.google.common.truth.Truth.assertThat 36 import com.google.common.util.concurrent.MoreExecutors.directExecutor 37 import org.junit.Before 38 import org.junit.Test 39 import org.junit.runner.RunWith 40 41 /** Tests operations and the resulting state managed by [BubblePositioner]. */ 42 @SmallTest 43 @RunWith(AndroidJUnit4::class) 44 class BubblePositionerTest { 45 46 private lateinit var positioner: BubblePositioner 47 private val context = ApplicationProvider.getApplicationContext<Context>() 48 private val resources: Resources 49 get() = context.resources 50 51 private val defaultDeviceConfig = 52 DeviceConfig( 53 windowBounds = Rect(0, 0, 1000, 2000), 54 isLargeScreen = false, 55 isSmallTablet = false, 56 isLandscape = false, 57 isRtl = false, 58 insets = Insets.of(0, 0, 0, 0) 59 ) 60 61 @Before setUpnull62 fun setUp() { 63 ProtoLog.REQUIRE_PROTOLOGTOOL = false 64 val windowManager = context.getSystemService(WindowManager::class.java) 65 positioner = BubblePositioner(context, windowManager) 66 } 67 68 @Test testUpdatenull69 fun testUpdate() { 70 val insets = Insets.of(10, 20, 5, 15) 71 val screenBounds = Rect(0, 0, 1000, 1200) 72 val availableRect = Rect(screenBounds) 73 availableRect.inset(insets) 74 positioner.update(defaultDeviceConfig.copy(insets = insets, windowBounds = screenBounds)) 75 assertThat(positioner.availableRect).isEqualTo(availableRect) 76 assertThat(positioner.isLandscape).isFalse() 77 assertThat(positioner.isLargeScreen).isFalse() 78 assertThat(positioner.insets).isEqualTo(insets) 79 } 80 81 @Test testShowBubblesVertically_phonePortraitnull82 fun testShowBubblesVertically_phonePortrait() { 83 positioner.update(defaultDeviceConfig) 84 assertThat(positioner.showBubblesVertically()).isFalse() 85 } 86 87 @Test testShowBubblesVertically_phoneLandscapenull88 fun testShowBubblesVertically_phoneLandscape() { 89 positioner.update(defaultDeviceConfig.copy(isLandscape = true)) 90 assertThat(positioner.isLandscape).isTrue() 91 assertThat(positioner.showBubblesVertically()).isTrue() 92 } 93 94 @Test testShowBubblesVertically_tabletnull95 fun testShowBubblesVertically_tablet() { 96 positioner.update(defaultDeviceConfig.copy(isLargeScreen = true)) 97 assertThat(positioner.showBubblesVertically()).isTrue() 98 } 99 100 /** If a resting position hasn't been set, calling it will return the default position. */ 101 @Test testGetRestingPosition_returnsDefaultPositionnull102 fun testGetRestingPosition_returnsDefaultPosition() { 103 positioner.update(defaultDeviceConfig) 104 val restingPosition = positioner.getRestingPosition() 105 val defaultPosition = positioner.defaultStartPosition 106 assertThat(restingPosition).isEqualTo(defaultPosition) 107 } 108 109 /** If a resting position has been set, it'll return that instead of the default position. */ 110 @Test testGetRestingPosition_returnsRestingPositionnull111 fun testGetRestingPosition_returnsRestingPosition() { 112 positioner.update(defaultDeviceConfig) 113 val restingPosition = PointF(100f, 100f) 114 positioner.restingPosition = restingPosition 115 assertThat(positioner.getRestingPosition()).isEqualTo(restingPosition) 116 } 117 118 /** Test that the default resting position on phone is in upper left. */ 119 @Test testGetRestingPosition_bubble_onPhonenull120 fun testGetRestingPosition_bubble_onPhone() { 121 positioner.update(defaultDeviceConfig) 122 val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) 123 val restingPosition = positioner.getRestingPosition() 124 assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left) 125 assertThat(restingPosition.y).isEqualTo(defaultYPosition) 126 } 127 128 @Test testGetRestingPosition_bubble_onPhone_RTLnull129 fun testGetRestingPosition_bubble_onPhone_RTL() { 130 positioner.update(defaultDeviceConfig.copy(isRtl = true)) 131 val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) 132 val restingPosition = positioner.getRestingPosition() 133 assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right) 134 assertThat(restingPosition.y).isEqualTo(defaultYPosition) 135 } 136 137 /** Test that the default resting position on tablet is middle left. */ 138 @Test testGetRestingPosition_chatBubble_onTabletnull139 fun testGetRestingPosition_chatBubble_onTablet() { 140 positioner.update(defaultDeviceConfig.copy(isLargeScreen = true)) 141 val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) 142 val restingPosition = positioner.getRestingPosition() 143 assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left) 144 assertThat(restingPosition.y).isEqualTo(defaultYPosition) 145 } 146 147 @Test testGetRestingPosition_chatBubble_onTablet_RTLnull148 fun testGetRestingPosition_chatBubble_onTablet_RTL() { 149 positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) 150 val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) 151 val restingPosition = positioner.getRestingPosition() 152 assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right) 153 assertThat(restingPosition.y).isEqualTo(defaultYPosition) 154 } 155 156 /** Test that the default resting position on tablet is middle right. */ 157 @Test testGetDefaultPosition_noteBubble_onTabletnull158 fun testGetDefaultPosition_noteBubble_onTablet() { 159 positioner.update(defaultDeviceConfig.copy(isLargeScreen = true)) 160 val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) 161 val startPosition = positioner.getDefaultStartPosition(true /* isNoteBubble */) 162 assertThat(startPosition.x).isEqualTo(allowableStackRegion.right) 163 assertThat(startPosition.y).isEqualTo(defaultYPosition) 164 } 165 166 @Test testGetRestingPosition_noteBubble_onTablet_RTLnull167 fun testGetRestingPosition_noteBubble_onTablet_RTL() { 168 positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) 169 val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) 170 val startPosition = positioner.getDefaultStartPosition(true /* isNoteBubble */) 171 assertThat(startPosition.x).isEqualTo(allowableStackRegion.left) 172 assertThat(startPosition.y).isEqualTo(defaultYPosition) 173 } 174 175 @Test testGetRestingPosition_afterBoundsChangenull176 fun testGetRestingPosition_afterBoundsChange() { 177 positioner.update( 178 defaultDeviceConfig.copy(isLargeScreen = true, windowBounds = Rect(0, 0, 2000, 1600)) 179 ) 180 181 // Set the resting position to the right side 182 var allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) 183 val restingPosition = PointF(allowableStackRegion.right, allowableStackRegion.centerY()) 184 positioner.restingPosition = restingPosition 185 186 // Now make the device smaller 187 positioner.update( 188 defaultDeviceConfig.copy(isLargeScreen = false, windowBounds = Rect(0, 0, 1000, 1600)) 189 ) 190 191 // Check the resting position is on the correct side 192 allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) 193 assertThat(positioner.restingPosition.x).isEqualTo(allowableStackRegion.right) 194 } 195 196 @Test testHasUserModifiedDefaultPosition_falsenull197 fun testHasUserModifiedDefaultPosition_false() { 198 positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) 199 assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse() 200 positioner.restingPosition = positioner.defaultStartPosition 201 assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse() 202 } 203 204 @Test testHasUserModifiedDefaultPosition_truenull205 fun testHasUserModifiedDefaultPosition_true() { 206 positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) 207 assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse() 208 positioner.restingPosition = PointF(0f, 100f) 209 assertThat(positioner.hasUserModifiedDefaultPosition()).isTrue() 210 } 211 212 @Test testBubbleBarExpandedViewHeightAndWidthnull213 fun testBubbleBarExpandedViewHeightAndWidth() { 214 val deviceConfig = 215 defaultDeviceConfig.copy( 216 // portrait orientation 217 isLandscape = false, 218 isLargeScreen = true, 219 insets = Insets.of(10, 20, 5, 15), 220 windowBounds = Rect(0, 0, 1800, 2600) 221 ) 222 223 positioner.setShowingInBubbleBar(true) 224 positioner.update(deviceConfig) 225 positioner.bubbleBarTopOnScreen = 2500 226 227 val spaceBetweenTopInsetAndBubbleBarInLandscape = 1680 228 val expandedViewVerticalSpacing = 229 resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) 230 val expectedHeight = 231 spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewVerticalSpacing 232 val expectedWidth = resources.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width) 233 234 assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth) 235 assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight) 236 } 237 238 @Test testBubbleBarExpandedViewHeightAndWidth_screenWidthTooSmallnull239 fun testBubbleBarExpandedViewHeightAndWidth_screenWidthTooSmall() { 240 val screenWidth = 300 241 val deviceConfig = 242 defaultDeviceConfig.copy( 243 // portrait orientation 244 isLandscape = false, 245 isLargeScreen = true, 246 insets = Insets.of(10, 20, 5, 15), 247 windowBounds = Rect(0, 0, screenWidth, 2600) 248 ) 249 positioner.setShowingInBubbleBar(true) 250 positioner.update(deviceConfig) 251 positioner.bubbleBarTopOnScreen = 2500 252 253 val spaceBetweenTopInsetAndBubbleBarInLandscape = 180 254 val expandedViewSpacing = 255 resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) 256 val expectedHeight = spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewSpacing 257 val expectedWidth = screenWidth - 15 /* horizontal insets */ - 2 * expandedViewSpacing 258 assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth) 259 assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight) 260 } 261 262 @Test testGetExpandedViewHeight_maxnull263 fun testGetExpandedViewHeight_max() { 264 val deviceConfig = 265 defaultDeviceConfig.copy( 266 isLargeScreen = true, 267 insets = Insets.of(10, 20, 5, 15), 268 windowBounds = Rect(0, 0, 1800, 2600) 269 ) 270 positioner.update(deviceConfig) 271 val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) 272 val bubble = 273 Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor(), directExecutor()) 274 275 assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(MAX_HEIGHT) 276 } 277 278 @Test testGetExpandedViewHeight_customHeight_validnull279 fun testGetExpandedViewHeight_customHeight_valid() { 280 val deviceConfig = 281 defaultDeviceConfig.copy( 282 isLargeScreen = true, 283 insets = Insets.of(10, 20, 5, 15), 284 windowBounds = Rect(0, 0, 1800, 2600) 285 ) 286 positioner.update(deviceConfig) 287 val minHeight = 288 context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height) 289 val bubble = 290 Bubble( 291 "key", 292 ShortcutInfo.Builder(context, "id").build(), 293 minHeight + 100 /* desiredHeight */, 294 0 /* desiredHeightResId */, 295 "title", 296 0 /* taskId */, 297 null /* locus */, 298 true /* isDismissable */, 299 directExecutor(), 300 directExecutor() 301 ) {} 302 303 // Ensure the height is the same as the desired value 304 assertThat(positioner.getExpandedViewHeight(bubble)) 305 .isEqualTo(bubble.getDesiredHeight(context)) 306 } 307 308 @Test testGetExpandedViewHeight_customHeight_tooSmallnull309 fun testGetExpandedViewHeight_customHeight_tooSmall() { 310 val deviceConfig = 311 defaultDeviceConfig.copy( 312 isLargeScreen = true, 313 insets = Insets.of(10, 20, 5, 15), 314 windowBounds = Rect(0, 0, 1800, 2600) 315 ) 316 positioner.update(deviceConfig) 317 318 val bubble = 319 Bubble( 320 "key", 321 ShortcutInfo.Builder(context, "id").build(), 322 10 /* desiredHeight */, 323 0 /* desiredHeightResId */, 324 "title", 325 0 /* taskId */, 326 null /* locus */, 327 true /* isDismissable */, 328 directExecutor(), 329 directExecutor() 330 ) {} 331 332 // Ensure the height is the same as the desired value 333 val minHeight = 334 context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height) 335 assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(minHeight) 336 } 337 338 @Test testGetMaxExpandedViewHeight_onLargeTabletnull339 fun testGetMaxExpandedViewHeight_onLargeTablet() { 340 val deviceConfig = 341 defaultDeviceConfig.copy( 342 isLargeScreen = true, 343 insets = Insets.of(10, 20, 5, 15), 344 windowBounds = Rect(0, 0, 1800, 2600) 345 ) 346 positioner.update(deviceConfig) 347 348 val manageButtonHeight = 349 context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height) 350 val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width) 351 val expandedViewPadding = 352 context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) 353 val expectedHeight = 354 1800 - 2 * 20 - manageButtonHeight - pointerWidth - expandedViewPadding * 2 355 assertThat(positioner.getMaxExpandedViewHeight(false /* isOverflow */)) 356 .isEqualTo(expectedHeight) 357 } 358 359 @Test testAreBubblesBottomAligned_largeScreen_truenull360 fun testAreBubblesBottomAligned_largeScreen_true() { 361 val deviceConfig = 362 defaultDeviceConfig.copy( 363 isLargeScreen = true, 364 insets = Insets.of(10, 20, 5, 15), 365 windowBounds = Rect(0, 0, 1800, 2600) 366 ) 367 positioner.update(deviceConfig) 368 369 assertThat(positioner.areBubblesBottomAligned()).isTrue() 370 } 371 372 @Test testAreBubblesBottomAligned_largeScreen_landscape_falsenull373 fun testAreBubblesBottomAligned_largeScreen_landscape_false() { 374 val deviceConfig = 375 defaultDeviceConfig.copy( 376 isLargeScreen = true, 377 isLandscape = true, 378 insets = Insets.of(10, 20, 5, 15), 379 windowBounds = Rect(0, 0, 1800, 2600) 380 ) 381 positioner.update(deviceConfig) 382 383 assertThat(positioner.areBubblesBottomAligned()).isFalse() 384 } 385 386 @Test testAreBubblesBottomAligned_smallTablet_falsenull387 fun testAreBubblesBottomAligned_smallTablet_false() { 388 val deviceConfig = 389 defaultDeviceConfig.copy( 390 isLargeScreen = true, 391 isSmallTablet = true, 392 insets = Insets.of(10, 20, 5, 15), 393 windowBounds = Rect(0, 0, 1800, 2600) 394 ) 395 positioner.update(deviceConfig) 396 397 assertThat(positioner.areBubblesBottomAligned()).isFalse() 398 } 399 400 @Test testAreBubblesBottomAligned_phone_falsenull401 fun testAreBubblesBottomAligned_phone_false() { 402 val deviceConfig = 403 defaultDeviceConfig.copy( 404 insets = Insets.of(10, 20, 5, 15), 405 windowBounds = Rect(0, 0, 1800, 2600) 406 ) 407 positioner.update(deviceConfig) 408 409 assertThat(positioner.areBubblesBottomAligned()).isFalse() 410 } 411 412 @Test testExpandedViewY_phoneLandscapenull413 fun testExpandedViewY_phoneLandscape() { 414 val deviceConfig = 415 defaultDeviceConfig.copy( 416 isLandscape = true, 417 insets = Insets.of(10, 20, 5, 15), 418 windowBounds = Rect(0, 0, 1800, 2600) 419 ) 420 positioner.update(deviceConfig) 421 422 val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) 423 val bubble = 424 Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor(), directExecutor()) 425 426 // This bubble will have max height so it'll always be top aligned 427 assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) 428 .isEqualTo(positioner.getExpandedViewYTopAligned()) 429 } 430 431 @Test testExpandedViewY_phonePortraitnull432 fun testExpandedViewY_phonePortrait() { 433 val deviceConfig = 434 defaultDeviceConfig.copy( 435 insets = Insets.of(10, 20, 5, 15), 436 windowBounds = Rect(0, 0, 1800, 2600) 437 ) 438 positioner.update(deviceConfig) 439 440 val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) 441 val bubble = 442 Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor(), directExecutor()) 443 444 // Always top aligned in phone portrait 445 assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) 446 .isEqualTo(positioner.getExpandedViewYTopAligned()) 447 } 448 449 @Test testExpandedViewY_smallTabletLandscapenull450 fun testExpandedViewY_smallTabletLandscape() { 451 val deviceConfig = 452 defaultDeviceConfig.copy( 453 isSmallTablet = true, 454 isLandscape = true, 455 insets = Insets.of(10, 20, 5, 15), 456 windowBounds = Rect(0, 0, 1800, 2600) 457 ) 458 positioner.update(deviceConfig) 459 460 val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) 461 val bubble = 462 Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor(), directExecutor()) 463 464 // This bubble will have max height which is always top aligned on small tablets 465 assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) 466 .isEqualTo(positioner.getExpandedViewYTopAligned()) 467 } 468 469 @Test testExpandedViewY_smallTabletPortraitnull470 fun testExpandedViewY_smallTabletPortrait() { 471 val deviceConfig = 472 defaultDeviceConfig.copy( 473 isSmallTablet = true, 474 insets = Insets.of(10, 20, 5, 15), 475 windowBounds = Rect(0, 0, 1800, 2600) 476 ) 477 positioner.update(deviceConfig) 478 479 val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) 480 val bubble = 481 Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor(), directExecutor()) 482 483 // This bubble will have max height which is always top aligned on small tablets 484 assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) 485 .isEqualTo(positioner.getExpandedViewYTopAligned()) 486 } 487 488 @Test testExpandedViewY_largeScreenLandscapenull489 fun testExpandedViewY_largeScreenLandscape() { 490 val deviceConfig = 491 defaultDeviceConfig.copy( 492 isLargeScreen = true, 493 isLandscape = true, 494 insets = Insets.of(10, 20, 5, 15), 495 windowBounds = Rect(0, 0, 1800, 2600) 496 ) 497 positioner.update(deviceConfig) 498 499 val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) 500 val bubble = 501 Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor(), directExecutor()) 502 503 // This bubble will have max height which is always top aligned on landscape, large tablet 504 assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) 505 .isEqualTo(positioner.getExpandedViewYTopAligned()) 506 } 507 508 @Test testExpandedViewY_largeScreenPortraitnull509 fun testExpandedViewY_largeScreenPortrait() { 510 val deviceConfig = 511 defaultDeviceConfig.copy( 512 isLargeScreen = true, 513 insets = Insets.of(10, 20, 5, 15), 514 windowBounds = Rect(0, 0, 1800, 2600) 515 ) 516 positioner.update(deviceConfig) 517 518 val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) 519 val bubble = 520 Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor(), directExecutor()) 521 522 val manageButtonHeight = 523 context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height) 524 val manageButtonPlusMargin = 525 manageButtonHeight + 526 2 * context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_margin) 527 val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width) 528 529 val expectedExpandedViewY = 530 positioner.availableRect.bottom - 531 manageButtonPlusMargin - 532 positioner.getExpandedViewHeightForLargeScreen() - 533 pointerWidth 534 535 // Bubbles are bottom aligned on portrait, large tablet 536 assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) 537 .isEqualTo(expectedExpandedViewY) 538 } 539 540 @Test testGetTaskViewContentWidth_onLeftnull541 fun testGetTaskViewContentWidth_onLeft() { 542 positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0))) 543 val taskViewWidth = positioner.getTaskViewContentWidth(true /* onLeft */) 544 val paddings = 545 positioner.getExpandedViewContainerPadding(true /* onLeft */, false /* isOverflow */) 546 assertThat(taskViewWidth) 547 .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2]) 548 } 549 550 @Test testGetTaskViewContentWidth_onRightnull551 fun testGetTaskViewContentWidth_onRight() { 552 positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0))) 553 val taskViewWidth = positioner.getTaskViewContentWidth(false /* onLeft */) 554 val paddings = 555 positioner.getExpandedViewContainerPadding(false /* onLeft */, false /* isOverflow */) 556 assertThat(taskViewWidth) 557 .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2]) 558 } 559 560 @Test testIsBubbleBarOnLeft_defaultsToRightnull561 fun testIsBubbleBarOnLeft_defaultsToRight() { 562 positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT 563 assertThat(positioner.isBubbleBarOnLeft).isFalse() 564 565 // Check that left and right return expected position 566 positioner.bubbleBarLocation = BubbleBarLocation.LEFT 567 assertThat(positioner.isBubbleBarOnLeft).isTrue() 568 positioner.bubbleBarLocation = BubbleBarLocation.RIGHT 569 assertThat(positioner.isBubbleBarOnLeft).isFalse() 570 } 571 572 @Test testIsBubbleBarOnLeft_rtlEnabled_defaultsToLeftnull573 fun testIsBubbleBarOnLeft_rtlEnabled_defaultsToLeft() { 574 positioner.update(defaultDeviceConfig.copy(isRtl = true)) 575 576 positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT 577 assertThat(positioner.isBubbleBarOnLeft).isTrue() 578 579 // Check that left and right return expected position 580 positioner.bubbleBarLocation = BubbleBarLocation.LEFT 581 assertThat(positioner.isBubbleBarOnLeft).isTrue() 582 positioner.bubbleBarLocation = BubbleBarLocation.RIGHT 583 assertThat(positioner.isBubbleBarOnLeft).isFalse() 584 } 585 586 @Test testGetBubbleBarExpandedViewBounds_onLeftnull587 fun testGetBubbleBarExpandedViewBounds_onLeft() { 588 testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = false) 589 } 590 591 @Test testGetBubbleBarExpandedViewBounds_onRightnull592 fun testGetBubbleBarExpandedViewBounds_onRight() { 593 testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = false) 594 } 595 596 @Test testGetBubbleBarExpandedViewBounds_isOverflow_onLeftnull597 fun testGetBubbleBarExpandedViewBounds_isOverflow_onLeft() { 598 testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = true) 599 } 600 601 @Test testGetBubbleBarExpandedViewBounds_isOverflow_onRightnull602 fun testGetBubbleBarExpandedViewBounds_isOverflow_onRight() { 603 testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = true) 604 } 605 606 @Test getExpandedViewContainerPadding_largeScreen_fitsMaxViewWidthnull607 fun getExpandedViewContainerPadding_largeScreen_fitsMaxViewWidth() { 608 val expandedViewWidth = context.resources.getDimensionPixelSize( 609 R.dimen.bubble_expanded_view_largescreen_width 610 ) 611 // set the screen size so that it is wide enough to fit the maximum width size 612 val screenWidth = expandedViewWidth * 2 613 positioner.update( 614 defaultDeviceConfig.copy( 615 windowBounds = Rect(0, 0, screenWidth, 2000), 616 isLargeScreen = true, 617 isLandscape = false 618 ) 619 ) 620 val paddings = 621 positioner.getExpandedViewContainerPadding(/* onLeft= */ true, /* isOverflow= */ false) 622 623 val padding = context.resources.getDimensionPixelSize( 624 R.dimen.bubble_expanded_view_largescreen_landscape_padding 625 ) 626 val right = screenWidth - expandedViewWidth - padding 627 assertThat(paddings).isEqualTo(intArrayOf(padding - positioner.pointerSize, 0, right, 0)) 628 } 629 630 @Test getExpandedViewContainerPadding_largeScreen_doesNotFitMaxViewWidthnull631 fun getExpandedViewContainerPadding_largeScreen_doesNotFitMaxViewWidth() { 632 positioner.update( 633 defaultDeviceConfig.copy( 634 windowBounds = Rect(0, 0, 600, 2000), 635 isLargeScreen = true, 636 isLandscape = false 637 ) 638 ) 639 val paddings = 640 positioner.getExpandedViewContainerPadding(/* onLeft= */ true, /* isOverflow= */ false) 641 642 val padding = context.resources.getDimensionPixelSize( 643 R.dimen.bubble_expanded_view_largescreen_landscape_padding 644 ) 645 // the screen is not wide enough to fit the maximum width size, so the view fills the screen 646 // minus left and right padding 647 assertThat(paddings).isEqualTo(intArrayOf(padding - positioner.pointerSize, 0, padding, 0)) 648 } 649 650 @Test getExpandedViewContainerPadding_smallTabletnull651 fun getExpandedViewContainerPadding_smallTablet() { 652 val screenWidth = 500 653 positioner.update( 654 defaultDeviceConfig.copy( 655 windowBounds = Rect(0, 0, screenWidth, 2000), 656 isLargeScreen = true, 657 isSmallTablet = true, 658 isLandscape = false 659 ) 660 ) 661 val paddings = 662 positioner.getExpandedViewContainerPadding(/* onLeft= */ true, /* isOverflow= */ false) 663 664 // for small tablets, the view width is set to be 0.72 * screen width 665 val viewWidth = (screenWidth * 0.72).toInt() 666 val padding = (screenWidth - viewWidth) / 2 667 assertThat(paddings).isEqualTo(intArrayOf(padding - positioner.pointerSize, 0, padding, 0)) 668 } 669 testGetBubbleBarExpandedViewBoundsnull670 private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) { 671 positioner.isShowingInBubbleBar = true 672 val windowBounds = Rect(0, 0, 2000, 2600) 673 val insets = Insets.of(10, 20, 5, 15) 674 val deviceConfig = 675 defaultDeviceConfig.copy( 676 isLargeScreen = true, 677 isLandscape = true, 678 insets = insets, 679 windowBounds = windowBounds 680 ) 681 positioner.update(deviceConfig) 682 683 val bubbleBarHeight = 100 684 positioner.bubbleBarTopOnScreen = windowBounds.bottom - insets.bottom - bubbleBarHeight 685 686 val expandedViewPadding = 687 context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) 688 689 val left: Int 690 val right: Int 691 if (onLeft) { 692 // Pin to the left, calculate right 693 left = deviceConfig.insets.left + expandedViewPadding 694 right = left + positioner.getExpandedViewWidthForBubbleBar(isOverflow) 695 } else { 696 // Pin to the right, calculate left 697 right = 698 deviceConfig.windowBounds.right - deviceConfig.insets.right - expandedViewPadding 699 left = right - positioner.getExpandedViewWidthForBubbleBar(isOverflow) 700 } 701 // Above the bubble bar 702 val bottom = positioner.bubbleBarTopOnScreen - expandedViewPadding 703 // Calculate right and top based on size 704 val top = bottom - positioner.getExpandedViewHeightForBubbleBar(isOverflow) 705 val expectedBounds = Rect(left, top, right, bottom) 706 707 val bounds = Rect() 708 positioner.getBubbleBarExpandedViewBounds(onLeft, isOverflow, bounds) 709 710 assertThat(bounds).isEqualTo(expectedBounds) 711 } 712 713 private val defaultYPosition: Float 714 /** 715 * Calculates the Y position bubbles should be placed based on the config. Based on the 716 * calculations in [BubblePositioner.getDefaultStartPosition] and 717 * [BubbleStackView.RelativeStackPosition]. 718 */ 719 get() { 720 val isTablet = positioner.isLargeScreen 721 722 // On tablet the position is centered, on phone it is an offset from the top. 723 val desiredY = 724 if (isTablet) { 725 positioner.screenRect.height() / 2f - positioner.bubbleSize / 2f 726 } else { 727 context.resources 728 .getDimensionPixelOffset(R.dimen.bubble_stack_starting_offset_y) 729 .toFloat() 730 } 731 // Since we're visually centering the bubbles on tablet, use total screen height rather 732 // than the available height. 733 val height = 734 if (isTablet) { 735 positioner.screenRect.height() 736 } else { 737 positioner.availableRect.height() 738 } 739 val offsetPercent = (desiredY / height).coerceIn(0f, 1f) 740 val allowableStackRegion = 741 positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) 742 return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent 743 } 744 } 745