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.common.ProtoLog
31 import com.android.wm.shell.R
32 import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT
33 import com.android.wm.shell.common.bubbles.BubbleBarLocation
34 import com.google.common.truth.Truth.assertThat
35 import com.google.common.util.concurrent.MoreExecutors.directExecutor
36 import org.junit.Before
37 import org.junit.Test
38 import org.junit.runner.RunWith
39 
40 /** Tests operations and the resulting state managed by [BubblePositioner]. */
41 @SmallTest
42 @RunWith(AndroidJUnit4::class)
43 class BubblePositionerTest {
44 
45     private lateinit var positioner: BubblePositioner
46     private val context = ApplicationProvider.getApplicationContext<Context>()
47     private val resources: Resources
48         get() = context.resources
49 
50     private val defaultDeviceConfig =
51         DeviceConfig(
52             windowBounds = Rect(0, 0, 1000, 2000),
53             isLargeScreen = false,
54             isSmallTablet = false,
55             isLandscape = false,
56             isRtl = false,
57             insets = Insets.of(0, 0, 0, 0)
58         )
59 
60     @Before
setUpnull61     fun setUp() {
62         ProtoLog.REQUIRE_PROTOLOGTOOL = false
63         val windowManager = context.getSystemService(WindowManager::class.java)
64         positioner = BubblePositioner(context, windowManager)
65     }
66 
67     @Test
testUpdatenull68     fun testUpdate() {
69         val insets = Insets.of(10, 20, 5, 15)
70         val screenBounds = Rect(0, 0, 1000, 1200)
71         val availableRect = Rect(screenBounds)
72         availableRect.inset(insets)
73         positioner.update(defaultDeviceConfig.copy(insets = insets, windowBounds = screenBounds))
74         assertThat(positioner.availableRect).isEqualTo(availableRect)
75         assertThat(positioner.isLandscape).isFalse()
76         assertThat(positioner.isLargeScreen).isFalse()
77         assertThat(positioner.insets).isEqualTo(insets)
78     }
79 
80     @Test
testShowBubblesVertically_phonePortraitnull81     fun testShowBubblesVertically_phonePortrait() {
82         positioner.update(defaultDeviceConfig)
83         assertThat(positioner.showBubblesVertically()).isFalse()
84     }
85 
86     @Test
testShowBubblesVertically_phoneLandscapenull87     fun testShowBubblesVertically_phoneLandscape() {
88         positioner.update(defaultDeviceConfig.copy(isLandscape = true))
89         assertThat(positioner.isLandscape).isTrue()
90         assertThat(positioner.showBubblesVertically()).isTrue()
91     }
92 
93     @Test
testShowBubblesVertically_tabletnull94     fun testShowBubblesVertically_tablet() {
95         positioner.update(defaultDeviceConfig.copy(isLargeScreen = true))
96         assertThat(positioner.showBubblesVertically()).isTrue()
97     }
98 
99     /** If a resting position hasn't been set, calling it will return the default position. */
100     @Test
testGetRestingPosition_returnsDefaultPositionnull101     fun testGetRestingPosition_returnsDefaultPosition() {
102         positioner.update(defaultDeviceConfig)
103         val restingPosition = positioner.getRestingPosition()
104         val defaultPosition = positioner.defaultStartPosition
105         assertThat(restingPosition).isEqualTo(defaultPosition)
106     }
107 
108     /** If a resting position has been set, it'll return that instead of the default position. */
109     @Test
testGetRestingPosition_returnsRestingPositionnull110     fun testGetRestingPosition_returnsRestingPosition() {
111         positioner.update(defaultDeviceConfig)
112         val restingPosition = PointF(100f, 100f)
113         positioner.restingPosition = restingPosition
114         assertThat(positioner.getRestingPosition()).isEqualTo(restingPosition)
115     }
116 
117     /** Test that the default resting position on phone is in upper left. */
118     @Test
testGetRestingPosition_bubble_onPhonenull119     fun testGetRestingPosition_bubble_onPhone() {
120         positioner.update(defaultDeviceConfig)
121         val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
122         val restingPosition = positioner.getRestingPosition()
123         assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left)
124         assertThat(restingPosition.y).isEqualTo(defaultYPosition)
125     }
126 
127     @Test
testGetRestingPosition_bubble_onPhone_RTLnull128     fun testGetRestingPosition_bubble_onPhone_RTL() {
129         positioner.update(defaultDeviceConfig.copy(isRtl = true))
130         val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
131         val restingPosition = positioner.getRestingPosition()
132         assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right)
133         assertThat(restingPosition.y).isEqualTo(defaultYPosition)
134     }
135 
136     /** Test that the default resting position on tablet is middle left. */
137     @Test
testGetRestingPosition_chatBubble_onTabletnull138     fun testGetRestingPosition_chatBubble_onTablet() {
139         positioner.update(defaultDeviceConfig.copy(isLargeScreen = true))
140         val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
141         val restingPosition = positioner.getRestingPosition()
142         assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left)
143         assertThat(restingPosition.y).isEqualTo(defaultYPosition)
144     }
145 
146     @Test
testGetRestingPosition_chatBubble_onTablet_RTLnull147     fun testGetRestingPosition_chatBubble_onTablet_RTL() {
148         positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
149         val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
150         val restingPosition = positioner.getRestingPosition()
151         assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right)
152         assertThat(restingPosition.y).isEqualTo(defaultYPosition)
153     }
154 
155     /** Test that the default resting position on tablet is middle right. */
156     @Test
testGetDefaultPosition_appBubble_onTabletnull157     fun testGetDefaultPosition_appBubble_onTablet() {
158         positioner.update(defaultDeviceConfig.copy(isLargeScreen = true))
159         val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
160         val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */)
161         assertThat(startPosition.x).isEqualTo(allowableStackRegion.right)
162         assertThat(startPosition.y).isEqualTo(defaultYPosition)
163     }
164 
165     @Test
testGetRestingPosition_appBubble_onTablet_RTLnull166     fun testGetRestingPosition_appBubble_onTablet_RTL() {
167         positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
168         val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
169         val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */)
170         assertThat(startPosition.x).isEqualTo(allowableStackRegion.left)
171         assertThat(startPosition.y).isEqualTo(defaultYPosition)
172     }
173 
174     @Test
testGetRestingPosition_afterBoundsChangenull175     fun testGetRestingPosition_afterBoundsChange() {
176         positioner.update(
177             defaultDeviceConfig.copy(isLargeScreen = true, windowBounds = Rect(0, 0, 2000, 1600))
178         )
179 
180         // Set the resting position to the right side
181         var allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
182         val restingPosition = PointF(allowableStackRegion.right, allowableStackRegion.centerY())
183         positioner.restingPosition = restingPosition
184 
185         // Now make the device smaller
186         positioner.update(
187             defaultDeviceConfig.copy(isLargeScreen = false, windowBounds = Rect(0, 0, 1000, 1600))
188         )
189 
190         // Check the resting position is on the correct side
191         allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
192         assertThat(positioner.restingPosition.x).isEqualTo(allowableStackRegion.right)
193     }
194 
195     @Test
testHasUserModifiedDefaultPosition_falsenull196     fun testHasUserModifiedDefaultPosition_false() {
197         positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
198         assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse()
199         positioner.restingPosition = positioner.defaultStartPosition
200         assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse()
201     }
202 
203     @Test
testHasUserModifiedDefaultPosition_truenull204     fun testHasUserModifiedDefaultPosition_true() {
205         positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
206         assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse()
207         positioner.restingPosition = PointF(0f, 100f)
208         assertThat(positioner.hasUserModifiedDefaultPosition()).isTrue()
209     }
210 
211     @Test
testBubbleBarExpandedViewHeightAndWidthnull212     fun testBubbleBarExpandedViewHeightAndWidth() {
213         val deviceConfig =
214             defaultDeviceConfig.copy(
215                 // portrait orientation
216                 isLandscape = false,
217                 isLargeScreen = true,
218                 insets = Insets.of(10, 20, 5, 15),
219                 windowBounds = Rect(0, 0, 1800, 2600)
220             )
221 
222         positioner.setShowingInBubbleBar(true)
223         positioner.update(deviceConfig)
224         positioner.bubbleBarTopOnScreen = 2500
225 
226         val spaceBetweenTopInsetAndBubbleBarInLandscape = 1680
227         val expandedViewVerticalSpacing =
228             resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
229         val expectedHeight =
230             spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewVerticalSpacing
231         val expectedWidth = resources.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width)
232 
233         assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth)
234         assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight)
235     }
236 
237     @Test
testBubbleBarExpandedViewHeightAndWidth_screenWidthTooSmallnull238     fun testBubbleBarExpandedViewHeightAndWidth_screenWidthTooSmall() {
239         val screenWidth = 300
240         val deviceConfig =
241             defaultDeviceConfig.copy(
242                 // portrait orientation
243                 isLandscape = false,
244                 isLargeScreen = true,
245                 insets = Insets.of(10, 20, 5, 15),
246                 windowBounds = Rect(0, 0, screenWidth, 2600)
247             )
248         positioner.setShowingInBubbleBar(true)
249         positioner.update(deviceConfig)
250         positioner.bubbleBarTopOnScreen = 2500
251 
252         val spaceBetweenTopInsetAndBubbleBarInLandscape = 180
253         val expandedViewSpacing =
254             resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
255         val expectedHeight = spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewSpacing
256         val expectedWidth = screenWidth - 15 /* horizontal insets */ - 2 * expandedViewSpacing
257         assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth)
258         assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight)
259     }
260 
261     @Test
testGetExpandedViewHeight_maxnull262     fun testGetExpandedViewHeight_max() {
263         val deviceConfig =
264             defaultDeviceConfig.copy(
265                 isLargeScreen = true,
266                 insets = Insets.of(10, 20, 5, 15),
267                 windowBounds = Rect(0, 0, 1800, 2600)
268             )
269         positioner.update(deviceConfig)
270         val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
271         val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
272 
273         assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(MAX_HEIGHT)
274     }
275 
276     @Test
testGetExpandedViewHeight_customHeight_validnull277     fun testGetExpandedViewHeight_customHeight_valid() {
278         val deviceConfig =
279             defaultDeviceConfig.copy(
280                 isLargeScreen = true,
281                 insets = Insets.of(10, 20, 5, 15),
282                 windowBounds = Rect(0, 0, 1800, 2600)
283             )
284         positioner.update(deviceConfig)
285         val minHeight =
286             context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height)
287         val bubble =
288             Bubble(
289                 "key",
290                 ShortcutInfo.Builder(context, "id").build(),
291                 minHeight + 100 /* desiredHeight */,
292                 0 /* desiredHeightResId */,
293                 "title",
294                 0 /* taskId */,
295                 null /* locus */,
296                 true /* isDismissable */,
297                 directExecutor()
298             ) {}
299 
300         // Ensure the height is the same as the desired value
301         assertThat(positioner.getExpandedViewHeight(bubble))
302             .isEqualTo(bubble.getDesiredHeight(context))
303     }
304 
305     @Test
testGetExpandedViewHeight_customHeight_tooSmallnull306     fun testGetExpandedViewHeight_customHeight_tooSmall() {
307         val deviceConfig =
308             defaultDeviceConfig.copy(
309                 isLargeScreen = true,
310                 insets = Insets.of(10, 20, 5, 15),
311                 windowBounds = Rect(0, 0, 1800, 2600)
312             )
313         positioner.update(deviceConfig)
314 
315         val bubble =
316             Bubble(
317                 "key",
318                 ShortcutInfo.Builder(context, "id").build(),
319                 10 /* desiredHeight */,
320                 0 /* desiredHeightResId */,
321                 "title",
322                 0 /* taskId */,
323                 null /* locus */,
324                 true /* isDismissable */,
325                 directExecutor()
326             ) {}
327 
328         // Ensure the height is the same as the desired value
329         val minHeight =
330             context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height)
331         assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(minHeight)
332     }
333 
334     @Test
testGetMaxExpandedViewHeight_onLargeTabletnull335     fun testGetMaxExpandedViewHeight_onLargeTablet() {
336         val deviceConfig =
337             defaultDeviceConfig.copy(
338                 isLargeScreen = true,
339                 insets = Insets.of(10, 20, 5, 15),
340                 windowBounds = Rect(0, 0, 1800, 2600)
341             )
342         positioner.update(deviceConfig)
343 
344         val manageButtonHeight =
345             context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height)
346         val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width)
347         val expandedViewPadding =
348             context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
349         val expectedHeight =
350             1800 - 2 * 20 - manageButtonHeight - pointerWidth - expandedViewPadding * 2
351         assertThat(positioner.getMaxExpandedViewHeight(false /* isOverflow */))
352             .isEqualTo(expectedHeight)
353     }
354 
355     @Test
testAreBubblesBottomAligned_largeScreen_truenull356     fun testAreBubblesBottomAligned_largeScreen_true() {
357         val deviceConfig =
358             defaultDeviceConfig.copy(
359                 isLargeScreen = true,
360                 insets = Insets.of(10, 20, 5, 15),
361                 windowBounds = Rect(0, 0, 1800, 2600)
362             )
363         positioner.update(deviceConfig)
364 
365         assertThat(positioner.areBubblesBottomAligned()).isTrue()
366     }
367 
368     @Test
testAreBubblesBottomAligned_largeScreen_landscape_falsenull369     fun testAreBubblesBottomAligned_largeScreen_landscape_false() {
370         val deviceConfig =
371             defaultDeviceConfig.copy(
372                 isLargeScreen = true,
373                 isLandscape = true,
374                 insets = Insets.of(10, 20, 5, 15),
375                 windowBounds = Rect(0, 0, 1800, 2600)
376             )
377         positioner.update(deviceConfig)
378 
379         assertThat(positioner.areBubblesBottomAligned()).isFalse()
380     }
381 
382     @Test
testAreBubblesBottomAligned_smallTablet_falsenull383     fun testAreBubblesBottomAligned_smallTablet_false() {
384         val deviceConfig =
385             defaultDeviceConfig.copy(
386                 isLargeScreen = true,
387                 isSmallTablet = true,
388                 insets = Insets.of(10, 20, 5, 15),
389                 windowBounds = Rect(0, 0, 1800, 2600)
390             )
391         positioner.update(deviceConfig)
392 
393         assertThat(positioner.areBubblesBottomAligned()).isFalse()
394     }
395 
396     @Test
testAreBubblesBottomAligned_phone_falsenull397     fun testAreBubblesBottomAligned_phone_false() {
398         val deviceConfig =
399             defaultDeviceConfig.copy(
400                 insets = Insets.of(10, 20, 5, 15),
401                 windowBounds = Rect(0, 0, 1800, 2600)
402             )
403         positioner.update(deviceConfig)
404 
405         assertThat(positioner.areBubblesBottomAligned()).isFalse()
406     }
407 
408     @Test
testExpandedViewY_phoneLandscapenull409     fun testExpandedViewY_phoneLandscape() {
410         val deviceConfig =
411             defaultDeviceConfig.copy(
412                 isLandscape = true,
413                 insets = Insets.of(10, 20, 5, 15),
414                 windowBounds = Rect(0, 0, 1800, 2600)
415             )
416         positioner.update(deviceConfig)
417 
418         val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
419         val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
420 
421         // This bubble will have max height so it'll always be top aligned
422         assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
423             .isEqualTo(positioner.getExpandedViewYTopAligned())
424     }
425 
426     @Test
testExpandedViewY_phonePortraitnull427     fun testExpandedViewY_phonePortrait() {
428         val deviceConfig =
429             defaultDeviceConfig.copy(
430                 insets = Insets.of(10, 20, 5, 15),
431                 windowBounds = Rect(0, 0, 1800, 2600)
432             )
433         positioner.update(deviceConfig)
434 
435         val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
436         val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
437 
438         // Always top aligned in phone portrait
439         assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
440             .isEqualTo(positioner.getExpandedViewYTopAligned())
441     }
442 
443     @Test
testExpandedViewY_smallTabletLandscapenull444     fun testExpandedViewY_smallTabletLandscape() {
445         val deviceConfig =
446             defaultDeviceConfig.copy(
447                 isSmallTablet = true,
448                 isLandscape = true,
449                 insets = Insets.of(10, 20, 5, 15),
450                 windowBounds = Rect(0, 0, 1800, 2600)
451             )
452         positioner.update(deviceConfig)
453 
454         val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
455         val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
456 
457         // This bubble will have max height which is always top aligned on small tablets
458         assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
459             .isEqualTo(positioner.getExpandedViewYTopAligned())
460     }
461 
462     @Test
testExpandedViewY_smallTabletPortraitnull463     fun testExpandedViewY_smallTabletPortrait() {
464         val deviceConfig =
465             defaultDeviceConfig.copy(
466                 isSmallTablet = true,
467                 insets = Insets.of(10, 20, 5, 15),
468                 windowBounds = Rect(0, 0, 1800, 2600)
469             )
470         positioner.update(deviceConfig)
471 
472         val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
473         val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
474 
475         // This bubble will have max height which is always top aligned on small tablets
476         assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
477             .isEqualTo(positioner.getExpandedViewYTopAligned())
478     }
479 
480     @Test
testExpandedViewY_largeScreenLandscapenull481     fun testExpandedViewY_largeScreenLandscape() {
482         val deviceConfig =
483             defaultDeviceConfig.copy(
484                 isLargeScreen = true,
485                 isLandscape = true,
486                 insets = Insets.of(10, 20, 5, 15),
487                 windowBounds = Rect(0, 0, 1800, 2600)
488             )
489         positioner.update(deviceConfig)
490 
491         val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
492         val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
493 
494         // This bubble will have max height which is always top aligned on landscape, large tablet
495         assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
496             .isEqualTo(positioner.getExpandedViewYTopAligned())
497     }
498 
499     @Test
testExpandedViewY_largeScreenPortraitnull500     fun testExpandedViewY_largeScreenPortrait() {
501         val deviceConfig =
502             defaultDeviceConfig.copy(
503                 isLargeScreen = true,
504                 insets = Insets.of(10, 20, 5, 15),
505                 windowBounds = Rect(0, 0, 1800, 2600)
506             )
507         positioner.update(deviceConfig)
508 
509         val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
510         val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
511 
512         val manageButtonHeight =
513             context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height)
514         val manageButtonPlusMargin =
515             manageButtonHeight +
516                 2 * context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_margin)
517         val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width)
518 
519         val expectedExpandedViewY =
520             positioner.availableRect.bottom -
521                 manageButtonPlusMargin -
522                 positioner.getExpandedViewHeightForLargeScreen() -
523                 pointerWidth
524 
525         // Bubbles are bottom aligned on portrait, large tablet
526         assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
527             .isEqualTo(expectedExpandedViewY)
528     }
529 
530     @Test
testGetTaskViewContentWidth_onLeftnull531     fun testGetTaskViewContentWidth_onLeft() {
532         positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
533         val taskViewWidth = positioner.getTaskViewContentWidth(true /* onLeft */)
534         val paddings =
535             positioner.getExpandedViewContainerPadding(true /* onLeft */, false /* isOverflow */)
536         assertThat(taskViewWidth)
537             .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2])
538     }
539 
540     @Test
testGetTaskViewContentWidth_onRightnull541     fun testGetTaskViewContentWidth_onRight() {
542         positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
543         val taskViewWidth = positioner.getTaskViewContentWidth(false /* onLeft */)
544         val paddings =
545             positioner.getExpandedViewContainerPadding(false /* onLeft */, false /* isOverflow */)
546         assertThat(taskViewWidth)
547             .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2])
548     }
549 
550     @Test
testIsBubbleBarOnLeft_defaultsToRightnull551     fun testIsBubbleBarOnLeft_defaultsToRight() {
552         positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT
553         assertThat(positioner.isBubbleBarOnLeft).isFalse()
554 
555         // Check that left and right return expected position
556         positioner.bubbleBarLocation = BubbleBarLocation.LEFT
557         assertThat(positioner.isBubbleBarOnLeft).isTrue()
558         positioner.bubbleBarLocation = BubbleBarLocation.RIGHT
559         assertThat(positioner.isBubbleBarOnLeft).isFalse()
560     }
561 
562     @Test
testIsBubbleBarOnLeft_rtlEnabled_defaultsToLeftnull563     fun testIsBubbleBarOnLeft_rtlEnabled_defaultsToLeft() {
564         positioner.update(defaultDeviceConfig.copy(isRtl = true))
565 
566         positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT
567         assertThat(positioner.isBubbleBarOnLeft).isTrue()
568 
569         // Check that left and right return expected position
570         positioner.bubbleBarLocation = BubbleBarLocation.LEFT
571         assertThat(positioner.isBubbleBarOnLeft).isTrue()
572         positioner.bubbleBarLocation = BubbleBarLocation.RIGHT
573         assertThat(positioner.isBubbleBarOnLeft).isFalse()
574     }
575 
576     @Test
testGetBubbleBarExpandedViewBounds_onLeftnull577     fun testGetBubbleBarExpandedViewBounds_onLeft() {
578         testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = false)
579     }
580 
581     @Test
testGetBubbleBarExpandedViewBounds_onRightnull582     fun testGetBubbleBarExpandedViewBounds_onRight() {
583         testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = false)
584     }
585 
586     @Test
testGetBubbleBarExpandedViewBounds_isOverflow_onLeftnull587     fun testGetBubbleBarExpandedViewBounds_isOverflow_onLeft() {
588         testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = true)
589     }
590 
591     @Test
testGetBubbleBarExpandedViewBounds_isOverflow_onRightnull592     fun testGetBubbleBarExpandedViewBounds_isOverflow_onRight() {
593         testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = true)
594     }
595 
testGetBubbleBarExpandedViewBoundsnull596     private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) {
597         positioner.setShowingInBubbleBar(true)
598         val windowBounds = Rect(0, 0, 2000, 2600)
599         val insets = Insets.of(10, 20, 5, 15)
600         val deviceConfig =
601             defaultDeviceConfig.copy(
602                 isLargeScreen = true,
603                 isLandscape = true,
604                 insets = insets,
605                 windowBounds = windowBounds
606             )
607         positioner.update(deviceConfig)
608 
609         val bubbleBarHeight = 100
610         positioner.bubbleBarTopOnScreen = windowBounds.bottom - insets.bottom - bubbleBarHeight
611 
612         val expandedViewPadding =
613             context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
614 
615         val left: Int
616         val right: Int
617         if (onLeft) {
618             // Pin to the left, calculate right
619             left = deviceConfig.insets.left + expandedViewPadding
620             right = left + positioner.getExpandedViewWidthForBubbleBar(isOverflow)
621         } else {
622             // Pin to the right, calculate left
623             right =
624                 deviceConfig.windowBounds.right - deviceConfig.insets.right - expandedViewPadding
625             left = right - positioner.getExpandedViewWidthForBubbleBar(isOverflow)
626         }
627         // Above the bubble bar
628         val bottom = positioner.bubbleBarTopOnScreen - expandedViewPadding
629         // Calculate right and top based on size
630         val top = bottom - positioner.getExpandedViewHeightForBubbleBar(isOverflow)
631         val expectedBounds = Rect(left, top, right, bottom)
632 
633         val bounds = Rect()
634         positioner.getBubbleBarExpandedViewBounds(onLeft, isOverflow, bounds)
635 
636         assertThat(bounds).isEqualTo(expectedBounds)
637     }
638 
639     private val defaultYPosition: Float
640         /**
641          * Calculates the Y position bubbles should be placed based on the config. Based on the
642          * calculations in [BubblePositioner.getDefaultStartPosition] and
643          * [BubbleStackView.RelativeStackPosition].
644          */
645         get() {
646             val isTablet = positioner.isLargeScreen
647 
648             // On tablet the position is centered, on phone it is an offset from the top.
649             val desiredY =
650                 if (isTablet) {
651                     positioner.screenRect.height() / 2f - positioner.bubbleSize / 2f
652                 } else {
653                     context.resources
654                         .getDimensionPixelOffset(R.dimen.bubble_stack_starting_offset_y)
655                         .toFloat()
656                 }
657             // Since we're visually centering the bubbles on tablet, use total screen height rather
658             // than the available height.
659             val height =
660                 if (isTablet) {
661                     positioner.screenRect.height()
662                 } else {
663                     positioner.availableRect.height()
664                 }
665             val offsetPercent = (desiredY / height).coerceIn(0f, 1f)
666             val allowableStackRegion =
667                 positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
668             return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent
669         }
670 }
671