• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 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