feat: WM-Shell QPR1 (not src or shared)
This commit is contained in:
-670
@@ -1,670 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.wm.shell.bubbles
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Insets
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import android.os.UserHandle
|
||||
import android.view.WindowManager
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.internal.protolog.common.ProtoLog
|
||||
import com.android.wm.shell.R
|
||||
import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT
|
||||
import com.android.wm.shell.common.bubbles.BubbleBarLocation
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.google.common.util.concurrent.MoreExecutors.directExecutor
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/** Tests operations and the resulting state managed by [BubblePositioner]. */
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class BubblePositionerTest {
|
||||
|
||||
private lateinit var positioner: BubblePositioner
|
||||
private val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
private val resources: Resources
|
||||
get() = context.resources
|
||||
|
||||
private val defaultDeviceConfig =
|
||||
DeviceConfig(
|
||||
windowBounds = Rect(0, 0, 1000, 2000),
|
||||
isLargeScreen = false,
|
||||
isSmallTablet = false,
|
||||
isLandscape = false,
|
||||
isRtl = false,
|
||||
insets = Insets.of(0, 0, 0, 0)
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
ProtoLog.REQUIRE_PROTOLOGTOOL = false
|
||||
val windowManager = context.getSystemService(WindowManager::class.java)
|
||||
positioner = BubblePositioner(context, windowManager)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUpdate() {
|
||||
val insets = Insets.of(10, 20, 5, 15)
|
||||
val screenBounds = Rect(0, 0, 1000, 1200)
|
||||
val availableRect = Rect(screenBounds)
|
||||
availableRect.inset(insets)
|
||||
positioner.update(defaultDeviceConfig.copy(insets = insets, windowBounds = screenBounds))
|
||||
assertThat(positioner.availableRect).isEqualTo(availableRect)
|
||||
assertThat(positioner.isLandscape).isFalse()
|
||||
assertThat(positioner.isLargeScreen).isFalse()
|
||||
assertThat(positioner.insets).isEqualTo(insets)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testShowBubblesVertically_phonePortrait() {
|
||||
positioner.update(defaultDeviceConfig)
|
||||
assertThat(positioner.showBubblesVertically()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testShowBubblesVertically_phoneLandscape() {
|
||||
positioner.update(defaultDeviceConfig.copy(isLandscape = true))
|
||||
assertThat(positioner.isLandscape).isTrue()
|
||||
assertThat(positioner.showBubblesVertically()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testShowBubblesVertically_tablet() {
|
||||
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true))
|
||||
assertThat(positioner.showBubblesVertically()).isTrue()
|
||||
}
|
||||
|
||||
/** If a resting position hasn't been set, calling it will return the default position. */
|
||||
@Test
|
||||
fun testGetRestingPosition_returnsDefaultPosition() {
|
||||
positioner.update(defaultDeviceConfig)
|
||||
val restingPosition = positioner.getRestingPosition()
|
||||
val defaultPosition = positioner.defaultStartPosition
|
||||
assertThat(restingPosition).isEqualTo(defaultPosition)
|
||||
}
|
||||
|
||||
/** If a resting position has been set, it'll return that instead of the default position. */
|
||||
@Test
|
||||
fun testGetRestingPosition_returnsRestingPosition() {
|
||||
positioner.update(defaultDeviceConfig)
|
||||
val restingPosition = PointF(100f, 100f)
|
||||
positioner.restingPosition = restingPosition
|
||||
assertThat(positioner.getRestingPosition()).isEqualTo(restingPosition)
|
||||
}
|
||||
|
||||
/** Test that the default resting position on phone is in upper left. */
|
||||
@Test
|
||||
fun testGetRestingPosition_bubble_onPhone() {
|
||||
positioner.update(defaultDeviceConfig)
|
||||
val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
|
||||
val restingPosition = positioner.getRestingPosition()
|
||||
assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left)
|
||||
assertThat(restingPosition.y).isEqualTo(defaultYPosition)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetRestingPosition_bubble_onPhone_RTL() {
|
||||
positioner.update(defaultDeviceConfig.copy(isRtl = true))
|
||||
val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
|
||||
val restingPosition = positioner.getRestingPosition()
|
||||
assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right)
|
||||
assertThat(restingPosition.y).isEqualTo(defaultYPosition)
|
||||
}
|
||||
|
||||
/** Test that the default resting position on tablet is middle left. */
|
||||
@Test
|
||||
fun testGetRestingPosition_chatBubble_onTablet() {
|
||||
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true))
|
||||
val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
|
||||
val restingPosition = positioner.getRestingPosition()
|
||||
assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left)
|
||||
assertThat(restingPosition.y).isEqualTo(defaultYPosition)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetRestingPosition_chatBubble_onTablet_RTL() {
|
||||
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
|
||||
val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
|
||||
val restingPosition = positioner.getRestingPosition()
|
||||
assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right)
|
||||
assertThat(restingPosition.y).isEqualTo(defaultYPosition)
|
||||
}
|
||||
|
||||
/** Test that the default resting position on tablet is middle right. */
|
||||
@Test
|
||||
fun testGetDefaultPosition_appBubble_onTablet() {
|
||||
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true))
|
||||
val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
|
||||
val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */)
|
||||
assertThat(startPosition.x).isEqualTo(allowableStackRegion.right)
|
||||
assertThat(startPosition.y).isEqualTo(defaultYPosition)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetRestingPosition_appBubble_onTablet_RTL() {
|
||||
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
|
||||
val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
|
||||
val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */)
|
||||
assertThat(startPosition.x).isEqualTo(allowableStackRegion.left)
|
||||
assertThat(startPosition.y).isEqualTo(defaultYPosition)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetRestingPosition_afterBoundsChange() {
|
||||
positioner.update(
|
||||
defaultDeviceConfig.copy(isLargeScreen = true, windowBounds = Rect(0, 0, 2000, 1600))
|
||||
)
|
||||
|
||||
// Set the resting position to the right side
|
||||
var allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
|
||||
val restingPosition = PointF(allowableStackRegion.right, allowableStackRegion.centerY())
|
||||
positioner.restingPosition = restingPosition
|
||||
|
||||
// Now make the device smaller
|
||||
positioner.update(
|
||||
defaultDeviceConfig.copy(isLargeScreen = false, windowBounds = Rect(0, 0, 1000, 1600))
|
||||
)
|
||||
|
||||
// Check the resting position is on the correct side
|
||||
allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
|
||||
assertThat(positioner.restingPosition.x).isEqualTo(allowableStackRegion.right)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHasUserModifiedDefaultPosition_false() {
|
||||
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
|
||||
assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse()
|
||||
positioner.restingPosition = positioner.defaultStartPosition
|
||||
assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHasUserModifiedDefaultPosition_true() {
|
||||
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
|
||||
assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse()
|
||||
positioner.restingPosition = PointF(0f, 100f)
|
||||
assertThat(positioner.hasUserModifiedDefaultPosition()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBubbleBarExpandedViewHeightAndWidth() {
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
// portrait orientation
|
||||
isLandscape = false,
|
||||
isLargeScreen = true,
|
||||
insets = Insets.of(10, 20, 5, 15),
|
||||
windowBounds = Rect(0, 0, 1800, 2600)
|
||||
)
|
||||
|
||||
positioner.setShowingInBubbleBar(true)
|
||||
positioner.update(deviceConfig)
|
||||
positioner.bubbleBarTopOnScreen = 2500
|
||||
|
||||
val spaceBetweenTopInsetAndBubbleBarInLandscape = 1680
|
||||
val expandedViewVerticalSpacing =
|
||||
resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
|
||||
val expectedHeight =
|
||||
spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewVerticalSpacing
|
||||
val expectedWidth = resources.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width)
|
||||
|
||||
assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth)
|
||||
assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBubbleBarExpandedViewHeightAndWidth_screenWidthTooSmall() {
|
||||
val screenWidth = 300
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
// portrait orientation
|
||||
isLandscape = false,
|
||||
isLargeScreen = true,
|
||||
insets = Insets.of(10, 20, 5, 15),
|
||||
windowBounds = Rect(0, 0, screenWidth, 2600)
|
||||
)
|
||||
positioner.setShowingInBubbleBar(true)
|
||||
positioner.update(deviceConfig)
|
||||
positioner.bubbleBarTopOnScreen = 2500
|
||||
|
||||
val spaceBetweenTopInsetAndBubbleBarInLandscape = 180
|
||||
val expandedViewSpacing =
|
||||
resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
|
||||
val expectedHeight = spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewSpacing
|
||||
val expectedWidth = screenWidth - 15 /* horizontal insets */ - 2 * expandedViewSpacing
|
||||
assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth)
|
||||
assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetExpandedViewHeight_max() {
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
isLargeScreen = true,
|
||||
insets = Insets.of(10, 20, 5, 15),
|
||||
windowBounds = Rect(0, 0, 1800, 2600)
|
||||
)
|
||||
positioner.update(deviceConfig)
|
||||
val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
|
||||
val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
|
||||
|
||||
assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(MAX_HEIGHT)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetExpandedViewHeight_customHeight_valid() {
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
isLargeScreen = true,
|
||||
insets = Insets.of(10, 20, 5, 15),
|
||||
windowBounds = Rect(0, 0, 1800, 2600)
|
||||
)
|
||||
positioner.update(deviceConfig)
|
||||
val minHeight =
|
||||
context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height)
|
||||
val bubble =
|
||||
Bubble(
|
||||
"key",
|
||||
ShortcutInfo.Builder(context, "id").build(),
|
||||
minHeight + 100 /* desiredHeight */,
|
||||
0 /* desiredHeightResId */,
|
||||
"title",
|
||||
0 /* taskId */,
|
||||
null /* locus */,
|
||||
true /* isDismissable */,
|
||||
directExecutor()
|
||||
) {}
|
||||
|
||||
// Ensure the height is the same as the desired value
|
||||
assertThat(positioner.getExpandedViewHeight(bubble))
|
||||
.isEqualTo(bubble.getDesiredHeight(context))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetExpandedViewHeight_customHeight_tooSmall() {
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
isLargeScreen = true,
|
||||
insets = Insets.of(10, 20, 5, 15),
|
||||
windowBounds = Rect(0, 0, 1800, 2600)
|
||||
)
|
||||
positioner.update(deviceConfig)
|
||||
|
||||
val bubble =
|
||||
Bubble(
|
||||
"key",
|
||||
ShortcutInfo.Builder(context, "id").build(),
|
||||
10 /* desiredHeight */,
|
||||
0 /* desiredHeightResId */,
|
||||
"title",
|
||||
0 /* taskId */,
|
||||
null /* locus */,
|
||||
true /* isDismissable */,
|
||||
directExecutor()
|
||||
) {}
|
||||
|
||||
// Ensure the height is the same as the desired value
|
||||
val minHeight =
|
||||
context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height)
|
||||
assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(minHeight)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetMaxExpandedViewHeight_onLargeTablet() {
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
isLargeScreen = true,
|
||||
insets = Insets.of(10, 20, 5, 15),
|
||||
windowBounds = Rect(0, 0, 1800, 2600)
|
||||
)
|
||||
positioner.update(deviceConfig)
|
||||
|
||||
val manageButtonHeight =
|
||||
context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height)
|
||||
val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width)
|
||||
val expandedViewPadding =
|
||||
context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
|
||||
val expectedHeight =
|
||||
1800 - 2 * 20 - manageButtonHeight - pointerWidth - expandedViewPadding * 2
|
||||
assertThat(positioner.getMaxExpandedViewHeight(false /* isOverflow */))
|
||||
.isEqualTo(expectedHeight)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAreBubblesBottomAligned_largeScreen_true() {
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
isLargeScreen = true,
|
||||
insets = Insets.of(10, 20, 5, 15),
|
||||
windowBounds = Rect(0, 0, 1800, 2600)
|
||||
)
|
||||
positioner.update(deviceConfig)
|
||||
|
||||
assertThat(positioner.areBubblesBottomAligned()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAreBubblesBottomAligned_largeScreen_landscape_false() {
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
isLargeScreen = true,
|
||||
isLandscape = true,
|
||||
insets = Insets.of(10, 20, 5, 15),
|
||||
windowBounds = Rect(0, 0, 1800, 2600)
|
||||
)
|
||||
positioner.update(deviceConfig)
|
||||
|
||||
assertThat(positioner.areBubblesBottomAligned()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAreBubblesBottomAligned_smallTablet_false() {
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
isLargeScreen = true,
|
||||
isSmallTablet = true,
|
||||
insets = Insets.of(10, 20, 5, 15),
|
||||
windowBounds = Rect(0, 0, 1800, 2600)
|
||||
)
|
||||
positioner.update(deviceConfig)
|
||||
|
||||
assertThat(positioner.areBubblesBottomAligned()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAreBubblesBottomAligned_phone_false() {
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
insets = Insets.of(10, 20, 5, 15),
|
||||
windowBounds = Rect(0, 0, 1800, 2600)
|
||||
)
|
||||
positioner.update(deviceConfig)
|
||||
|
||||
assertThat(positioner.areBubblesBottomAligned()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExpandedViewY_phoneLandscape() {
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
isLandscape = true,
|
||||
insets = Insets.of(10, 20, 5, 15),
|
||||
windowBounds = Rect(0, 0, 1800, 2600)
|
||||
)
|
||||
positioner.update(deviceConfig)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
|
||||
val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
|
||||
|
||||
// This bubble will have max height so it'll always be top aligned
|
||||
assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
|
||||
.isEqualTo(positioner.getExpandedViewYTopAligned())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExpandedViewY_phonePortrait() {
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
insets = Insets.of(10, 20, 5, 15),
|
||||
windowBounds = Rect(0, 0, 1800, 2600)
|
||||
)
|
||||
positioner.update(deviceConfig)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
|
||||
val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
|
||||
|
||||
// Always top aligned in phone portrait
|
||||
assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
|
||||
.isEqualTo(positioner.getExpandedViewYTopAligned())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExpandedViewY_smallTabletLandscape() {
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
isSmallTablet = true,
|
||||
isLandscape = true,
|
||||
insets = Insets.of(10, 20, 5, 15),
|
||||
windowBounds = Rect(0, 0, 1800, 2600)
|
||||
)
|
||||
positioner.update(deviceConfig)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
|
||||
val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
|
||||
|
||||
// This bubble will have max height which is always top aligned on small tablets
|
||||
assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
|
||||
.isEqualTo(positioner.getExpandedViewYTopAligned())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExpandedViewY_smallTabletPortrait() {
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
isSmallTablet = true,
|
||||
insets = Insets.of(10, 20, 5, 15),
|
||||
windowBounds = Rect(0, 0, 1800, 2600)
|
||||
)
|
||||
positioner.update(deviceConfig)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
|
||||
val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
|
||||
|
||||
// This bubble will have max height which is always top aligned on small tablets
|
||||
assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
|
||||
.isEqualTo(positioner.getExpandedViewYTopAligned())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExpandedViewY_largeScreenLandscape() {
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
isLargeScreen = true,
|
||||
isLandscape = true,
|
||||
insets = Insets.of(10, 20, 5, 15),
|
||||
windowBounds = Rect(0, 0, 1800, 2600)
|
||||
)
|
||||
positioner.update(deviceConfig)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
|
||||
val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
|
||||
|
||||
// This bubble will have max height which is always top aligned on landscape, large tablet
|
||||
assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
|
||||
.isEqualTo(positioner.getExpandedViewYTopAligned())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExpandedViewY_largeScreenPortrait() {
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
isLargeScreen = true,
|
||||
insets = Insets.of(10, 20, 5, 15),
|
||||
windowBounds = Rect(0, 0, 1800, 2600)
|
||||
)
|
||||
positioner.update(deviceConfig)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
|
||||
val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
|
||||
|
||||
val manageButtonHeight =
|
||||
context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height)
|
||||
val manageButtonPlusMargin =
|
||||
manageButtonHeight +
|
||||
2 * context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_margin)
|
||||
val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width)
|
||||
|
||||
val expectedExpandedViewY =
|
||||
positioner.availableRect.bottom -
|
||||
manageButtonPlusMargin -
|
||||
positioner.getExpandedViewHeightForLargeScreen() -
|
||||
pointerWidth
|
||||
|
||||
// Bubbles are bottom aligned on portrait, large tablet
|
||||
assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
|
||||
.isEqualTo(expectedExpandedViewY)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetTaskViewContentWidth_onLeft() {
|
||||
positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
|
||||
val taskViewWidth = positioner.getTaskViewContentWidth(true /* onLeft */)
|
||||
val paddings =
|
||||
positioner.getExpandedViewContainerPadding(true /* onLeft */, false /* isOverflow */)
|
||||
assertThat(taskViewWidth)
|
||||
.isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetTaskViewContentWidth_onRight() {
|
||||
positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
|
||||
val taskViewWidth = positioner.getTaskViewContentWidth(false /* onLeft */)
|
||||
val paddings =
|
||||
positioner.getExpandedViewContainerPadding(false /* onLeft */, false /* isOverflow */)
|
||||
assertThat(taskViewWidth)
|
||||
.isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsBubbleBarOnLeft_defaultsToRight() {
|
||||
positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT
|
||||
assertThat(positioner.isBubbleBarOnLeft).isFalse()
|
||||
|
||||
// Check that left and right return expected position
|
||||
positioner.bubbleBarLocation = BubbleBarLocation.LEFT
|
||||
assertThat(positioner.isBubbleBarOnLeft).isTrue()
|
||||
positioner.bubbleBarLocation = BubbleBarLocation.RIGHT
|
||||
assertThat(positioner.isBubbleBarOnLeft).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsBubbleBarOnLeft_rtlEnabled_defaultsToLeft() {
|
||||
positioner.update(defaultDeviceConfig.copy(isRtl = true))
|
||||
|
||||
positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT
|
||||
assertThat(positioner.isBubbleBarOnLeft).isTrue()
|
||||
|
||||
// Check that left and right return expected position
|
||||
positioner.bubbleBarLocation = BubbleBarLocation.LEFT
|
||||
assertThat(positioner.isBubbleBarOnLeft).isTrue()
|
||||
positioner.bubbleBarLocation = BubbleBarLocation.RIGHT
|
||||
assertThat(positioner.isBubbleBarOnLeft).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetBubbleBarExpandedViewBounds_onLeft() {
|
||||
testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetBubbleBarExpandedViewBounds_onRight() {
|
||||
testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetBubbleBarExpandedViewBounds_isOverflow_onLeft() {
|
||||
testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetBubbleBarExpandedViewBounds_isOverflow_onRight() {
|
||||
testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = true)
|
||||
}
|
||||
|
||||
private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) {
|
||||
positioner.setShowingInBubbleBar(true)
|
||||
val windowBounds = Rect(0, 0, 2000, 2600)
|
||||
val insets = Insets.of(10, 20, 5, 15)
|
||||
val deviceConfig =
|
||||
defaultDeviceConfig.copy(
|
||||
isLargeScreen = true,
|
||||
isLandscape = true,
|
||||
insets = insets,
|
||||
windowBounds = windowBounds
|
||||
)
|
||||
positioner.update(deviceConfig)
|
||||
|
||||
val bubbleBarHeight = 100
|
||||
positioner.bubbleBarTopOnScreen = windowBounds.bottom - insets.bottom - bubbleBarHeight
|
||||
|
||||
val expandedViewPadding =
|
||||
context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
|
||||
|
||||
val left: Int
|
||||
val right: Int
|
||||
if (onLeft) {
|
||||
// Pin to the left, calculate right
|
||||
left = deviceConfig.insets.left + expandedViewPadding
|
||||
right = left + positioner.getExpandedViewWidthForBubbleBar(isOverflow)
|
||||
} else {
|
||||
// Pin to the right, calculate left
|
||||
right =
|
||||
deviceConfig.windowBounds.right - deviceConfig.insets.right - expandedViewPadding
|
||||
left = right - positioner.getExpandedViewWidthForBubbleBar(isOverflow)
|
||||
}
|
||||
// Above the bubble bar
|
||||
val bottom = positioner.bubbleBarTopOnScreen - expandedViewPadding
|
||||
// Calculate right and top based on size
|
||||
val top = bottom - positioner.getExpandedViewHeightForBubbleBar(isOverflow)
|
||||
val expectedBounds = Rect(left, top, right, bottom)
|
||||
|
||||
val bounds = Rect()
|
||||
positioner.getBubbleBarExpandedViewBounds(onLeft, isOverflow, bounds)
|
||||
|
||||
assertThat(bounds).isEqualTo(expectedBounds)
|
||||
}
|
||||
|
||||
private val defaultYPosition: Float
|
||||
/**
|
||||
* Calculates the Y position bubbles should be placed based on the config. Based on the
|
||||
* calculations in [BubblePositioner.getDefaultStartPosition] and
|
||||
* [BubbleStackView.RelativeStackPosition].
|
||||
*/
|
||||
get() {
|
||||
val isTablet = positioner.isLargeScreen
|
||||
|
||||
// On tablet the position is centered, on phone it is an offset from the top.
|
||||
val desiredY =
|
||||
if (isTablet) {
|
||||
positioner.screenRect.height() / 2f - positioner.bubbleSize / 2f
|
||||
} else {
|
||||
context.resources
|
||||
.getDimensionPixelOffset(R.dimen.bubble_stack_starting_offset_y)
|
||||
.toFloat()
|
||||
}
|
||||
// Since we're visually centering the bubbles on tablet, use total screen height rather
|
||||
// than the available height.
|
||||
val height =
|
||||
if (isTablet) {
|
||||
positioner.screenRect.height()
|
||||
} else {
|
||||
positioner.availableRect.height()
|
||||
}
|
||||
val offsetPercent = (desiredY / height).coerceIn(0f, 1f)
|
||||
val allowableStackRegion =
|
||||
positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
|
||||
return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent
|
||||
}
|
||||
}
|
||||
-462
@@ -1,462 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.wm.shell.bubbles
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.UserHandle
|
||||
import android.platform.test.flag.junit.SetFlagsRule
|
||||
import android.view.IWindowManager
|
||||
import android.view.WindowManager
|
||||
import android.view.WindowManagerGlobal
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.android.internal.logging.testing.UiEventLoggerFake
|
||||
import com.android.internal.protolog.common.ProtoLog
|
||||
import com.android.launcher3.icons.BubbleIconFactory
|
||||
import com.android.wm.shell.Flags
|
||||
import com.android.wm.shell.R
|
||||
import com.android.wm.shell.bubbles.Bubbles.SysuiProxy
|
||||
import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix
|
||||
import com.android.wm.shell.common.FloatingContentCoordinator
|
||||
import com.android.wm.shell.common.ShellExecutor
|
||||
import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
|
||||
import com.android.wm.shell.taskview.TaskView
|
||||
import com.android.wm.shell.taskview.TaskViewTaskController
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.google.common.util.concurrent.MoreExecutors.directExecutor
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.mock
|
||||
import android.platform.test.annotations.DisableFlags
|
||||
import android.platform.test.annotations.EnableFlags
|
||||
import java.util.concurrent.Semaphore
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.function.Consumer
|
||||
|
||||
/** Unit tests for [BubbleStackView]. */
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class BubbleStackViewTest {
|
||||
|
||||
@get:Rule val setFlagsRule = SetFlagsRule()
|
||||
|
||||
private val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
private lateinit var positioner: BubblePositioner
|
||||
private lateinit var iconFactory: BubbleIconFactory
|
||||
private lateinit var expandedViewManager: FakeBubbleExpandedViewManager
|
||||
private lateinit var bubbleStackView: BubbleStackView
|
||||
private lateinit var shellExecutor: ShellExecutor
|
||||
private lateinit var windowManager: IWindowManager
|
||||
private lateinit var bubbleTaskViewFactory: BubbleTaskViewFactory
|
||||
private lateinit var bubbleData: BubbleData
|
||||
private lateinit var bubbleStackViewManager: FakeBubbleStackViewManager
|
||||
private var sysuiProxy = mock<SysuiProxy>()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
PhysicsAnimatorTestUtils.prepareForTest()
|
||||
// Disable protolog tool when running the tests from studio
|
||||
ProtoLog.REQUIRE_PROTOLOGTOOL = false
|
||||
windowManager = WindowManagerGlobal.getWindowManagerService()!!
|
||||
shellExecutor = TestShellExecutor()
|
||||
val windowManager = context.getSystemService(WindowManager::class.java)
|
||||
iconFactory =
|
||||
BubbleIconFactory(
|
||||
context,
|
||||
context.resources.getDimensionPixelSize(R.dimen.bubble_size),
|
||||
context.resources.getDimensionPixelSize(R.dimen.bubble_badge_size),
|
||||
Color.BLACK,
|
||||
context.resources.getDimensionPixelSize(
|
||||
com.android.internal.R.dimen.importance_ring_stroke_width
|
||||
)
|
||||
)
|
||||
positioner = BubblePositioner(context, windowManager)
|
||||
bubbleData =
|
||||
BubbleData(
|
||||
context,
|
||||
BubbleLogger(UiEventLoggerFake()),
|
||||
positioner,
|
||||
BubbleEducationController(context),
|
||||
shellExecutor
|
||||
)
|
||||
bubbleStackViewManager = FakeBubbleStackViewManager()
|
||||
expandedViewManager = FakeBubbleExpandedViewManager()
|
||||
bubbleTaskViewFactory = FakeBubbleTaskViewFactory()
|
||||
bubbleStackView =
|
||||
BubbleStackView(
|
||||
context,
|
||||
bubbleStackViewManager,
|
||||
positioner,
|
||||
bubbleData,
|
||||
null,
|
||||
FloatingContentCoordinator(),
|
||||
{ sysuiProxy },
|
||||
shellExecutor
|
||||
)
|
||||
|
||||
context
|
||||
.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putBoolean(StackEducationView.PREF_STACK_EDUCATION, true)
|
||||
.apply()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
PhysicsAnimatorTestUtils.tearDown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addBubble() {
|
||||
val bubble = createAndInflateBubble()
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
bubbleStackView.addBubble(bubble)
|
||||
}
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
|
||||
assertThat(bubbleStackView.bubbleCount).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tapBubbleToExpand() {
|
||||
val bubble = createAndInflateBubble()
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
bubbleStackView.addBubble(bubble)
|
||||
}
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
|
||||
assertThat(bubbleStackView.bubbleCount).isEqualTo(1)
|
||||
var lastUpdate: BubbleData.Update? = null
|
||||
val semaphore = Semaphore(0)
|
||||
val listener =
|
||||
BubbleData.Listener { update ->
|
||||
lastUpdate = update
|
||||
semaphore.release()
|
||||
}
|
||||
bubbleData.setListener(listener)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
bubble.iconView!!.performClick()
|
||||
// we're checking the expanded state in BubbleData because that's the source of truth.
|
||||
// This will eventually propagate an update back to the stack view, but setting the
|
||||
// entire pipeline is outside the scope of a unit test.
|
||||
assertThat(bubbleData.isExpanded).isTrue()
|
||||
}
|
||||
|
||||
assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
|
||||
assertThat(lastUpdate).isNotNull()
|
||||
assertThat(lastUpdate!!.expandedChanged).isTrue()
|
||||
assertThat(lastUpdate!!.expanded).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tapDifferentBubble_shouldReorder() {
|
||||
val bubble1 = createAndInflateChatBubble(key = "bubble1")
|
||||
val bubble2 = createAndInflateChatBubble(key = "bubble2")
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
bubbleStackView.addBubble(bubble1)
|
||||
bubbleStackView.addBubble(bubble2)
|
||||
}
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
|
||||
|
||||
assertThat(bubbleStackView.bubbleCount).isEqualTo(2)
|
||||
assertThat(bubbleData.bubbles).hasSize(2)
|
||||
assertThat(bubbleData.selectedBubble).isEqualTo(bubble2)
|
||||
assertThat(bubble2.iconView).isNotNull()
|
||||
|
||||
var lastUpdate: BubbleData.Update? = null
|
||||
val semaphore = Semaphore(0)
|
||||
val listener =
|
||||
BubbleData.Listener { update ->
|
||||
lastUpdate = update
|
||||
semaphore.release()
|
||||
}
|
||||
bubbleData.setListener(listener)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
bubble2.iconView!!.performClick()
|
||||
assertThat(bubbleData.isExpanded).isTrue()
|
||||
|
||||
bubbleStackView.setSelectedBubble(bubble2)
|
||||
bubbleStackView.isExpanded = true
|
||||
}
|
||||
|
||||
assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
|
||||
assertThat(lastUpdate!!.expanded).isTrue()
|
||||
assertThat(lastUpdate!!.bubbles.map { it.key })
|
||||
.containsExactly("bubble2", "bubble1")
|
||||
.inOrder()
|
||||
|
||||
// wait for idle to allow the animation to start
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
|
||||
// wait for the expansion animation to complete before interacting with the bubbles
|
||||
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(
|
||||
AnimatableScaleMatrix.SCALE_X, AnimatableScaleMatrix.SCALE_Y)
|
||||
|
||||
// tap on bubble1 to select it
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
bubble1.iconView!!.performClick()
|
||||
}
|
||||
assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
|
||||
assertThat(bubbleData.selectedBubble).isEqualTo(bubble1)
|
||||
|
||||
// tap on bubble1 again to collapse the stack
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
// we have to set the selected bubble in the stack view manually because we don't have a
|
||||
// listener wired up.
|
||||
bubbleStackView.setSelectedBubble(bubble1)
|
||||
bubble1.iconView!!.performClick()
|
||||
}
|
||||
|
||||
assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
|
||||
assertThat(bubbleData.selectedBubble).isEqualTo(bubble1)
|
||||
assertThat(bubbleData.isExpanded).isFalse()
|
||||
assertThat(lastUpdate!!.orderChanged).isTrue()
|
||||
assertThat(lastUpdate!!.bubbles.map { it.key })
|
||||
.containsExactly("bubble1", "bubble2")
|
||||
.inOrder()
|
||||
}
|
||||
|
||||
@EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
|
||||
@Test
|
||||
fun testCreateStackView_noOverflowContents_noOverflow() {
|
||||
bubbleStackView =
|
||||
BubbleStackView(
|
||||
context,
|
||||
bubbleStackViewManager,
|
||||
positioner,
|
||||
bubbleData,
|
||||
null,
|
||||
FloatingContentCoordinator(),
|
||||
{ sysuiProxy },
|
||||
shellExecutor
|
||||
)
|
||||
|
||||
assertThat(bubbleData.overflowBubbles).isEmpty()
|
||||
val bubbleOverflow = bubbleData.overflow
|
||||
// Overflow shouldn't be attached
|
||||
assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isEqualTo(-1)
|
||||
}
|
||||
|
||||
@EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
|
||||
@Test
|
||||
fun testCreateStackView_hasOverflowContents_hasOverflow() {
|
||||
// Add a bubble to the overflow
|
||||
val bubble1 = createAndInflateChatBubble(key = "bubble1")
|
||||
bubbleData.notificationEntryUpdated(bubble1, false, false)
|
||||
bubbleData.dismissBubbleWithKey(bubble1.key, Bubbles.DISMISS_USER_GESTURE)
|
||||
assertThat(bubbleData.overflowBubbles).isNotEmpty()
|
||||
|
||||
bubbleStackView =
|
||||
BubbleStackView(
|
||||
context,
|
||||
bubbleStackViewManager,
|
||||
positioner,
|
||||
bubbleData,
|
||||
null,
|
||||
FloatingContentCoordinator(),
|
||||
{ sysuiProxy },
|
||||
shellExecutor
|
||||
)
|
||||
val bubbleOverflow = bubbleData.overflow
|
||||
assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
|
||||
}
|
||||
|
||||
@DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
|
||||
@Test
|
||||
fun testCreateStackView_noOverflowContents_hasOverflow() {
|
||||
bubbleStackView =
|
||||
BubbleStackView(
|
||||
context,
|
||||
bubbleStackViewManager,
|
||||
positioner,
|
||||
bubbleData,
|
||||
null,
|
||||
FloatingContentCoordinator(),
|
||||
{ sysuiProxy },
|
||||
shellExecutor
|
||||
)
|
||||
|
||||
assertThat(bubbleData.overflowBubbles).isEmpty()
|
||||
val bubbleOverflow = bubbleData.overflow
|
||||
assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
|
||||
}
|
||||
|
||||
@EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
|
||||
@Test
|
||||
fun showOverflow_true() {
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
bubbleStackView.showOverflow(true)
|
||||
}
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
|
||||
|
||||
val bubbleOverflow = bubbleData.overflow
|
||||
assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
|
||||
}
|
||||
|
||||
@EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
|
||||
@Test
|
||||
fun showOverflow_false() {
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
bubbleStackView.showOverflow(true)
|
||||
}
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
|
||||
val bubbleOverflow = bubbleData.overflow
|
||||
assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
bubbleStackView.showOverflow(false)
|
||||
}
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
|
||||
|
||||
// The overflow should've been removed
|
||||
assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isEqualTo(-1)
|
||||
}
|
||||
|
||||
@DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
|
||||
@Test
|
||||
fun showOverflow_ignored() {
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
bubbleStackView.showOverflow(false)
|
||||
}
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
|
||||
|
||||
// showOverflow should've been ignored, so the overflow would be attached
|
||||
val bubbleOverflow = bubbleData.overflow
|
||||
assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
|
||||
}
|
||||
|
||||
private fun createAndInflateChatBubble(key: String): Bubble {
|
||||
val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button)
|
||||
val shortcutInfo = ShortcutInfo.Builder(context, "fakeId").setIcon(icon).build()
|
||||
val bubble =
|
||||
Bubble(
|
||||
key,
|
||||
shortcutInfo,
|
||||
/* desiredHeight= */ 6,
|
||||
Resources.ID_NULL,
|
||||
"title",
|
||||
/* taskId= */ 0,
|
||||
"locus",
|
||||
/* isDismissable= */ true,
|
||||
directExecutor()
|
||||
) {}
|
||||
inflateBubble(bubble)
|
||||
return bubble
|
||||
}
|
||||
|
||||
private fun createAndInflateBubble(): Bubble {
|
||||
val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
|
||||
val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button)
|
||||
val bubble = Bubble.createAppBubble(intent, UserHandle(1), icon, directExecutor())
|
||||
inflateBubble(bubble)
|
||||
return bubble
|
||||
}
|
||||
|
||||
private fun inflateBubble(bubble: Bubble) {
|
||||
bubble.setInflateSynchronously(true)
|
||||
bubbleData.notificationEntryUpdated(bubble, true, false)
|
||||
|
||||
val semaphore = Semaphore(0)
|
||||
val callback: BubbleViewInfoTask.Callback =
|
||||
BubbleViewInfoTask.Callback { semaphore.release() }
|
||||
bubble.inflate(
|
||||
callback,
|
||||
context,
|
||||
expandedViewManager,
|
||||
bubbleTaskViewFactory,
|
||||
positioner,
|
||||
bubbleStackView,
|
||||
null,
|
||||
iconFactory,
|
||||
false
|
||||
)
|
||||
|
||||
assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
|
||||
assertThat(bubble.isInflated).isTrue()
|
||||
}
|
||||
|
||||
private class FakeBubbleStackViewManager : BubbleStackViewManager {
|
||||
|
||||
override fun onAllBubblesAnimatedOut() {}
|
||||
|
||||
override fun updateWindowFlagsForBackpress(interceptBack: Boolean) {}
|
||||
|
||||
override fun checkNotificationPanelExpandedState(callback: Consumer<Boolean>) {}
|
||||
|
||||
override fun hideCurrentInputMethod() {}
|
||||
}
|
||||
|
||||
private class TestShellExecutor : ShellExecutor {
|
||||
|
||||
override fun execute(runnable: Runnable) {
|
||||
runnable.run()
|
||||
}
|
||||
|
||||
override fun executeDelayed(r: Runnable, delayMillis: Long) {
|
||||
r.run()
|
||||
}
|
||||
|
||||
override fun removeCallbacks(r: Runnable?) {}
|
||||
|
||||
override fun hasCallback(r: Runnable): Boolean = false
|
||||
}
|
||||
|
||||
private inner class FakeBubbleTaskViewFactory : BubbleTaskViewFactory {
|
||||
override fun create(): BubbleTaskView {
|
||||
val taskViewTaskController = mock<TaskViewTaskController>()
|
||||
val taskView = TaskView(context, taskViewTaskController)
|
||||
return BubbleTaskView(taskView, shellExecutor)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class FakeBubbleExpandedViewManager : BubbleExpandedViewManager {
|
||||
|
||||
override val overflowBubbles: List<Bubble>
|
||||
get() = emptyList()
|
||||
|
||||
override fun setOverflowListener(listener: BubbleData.Listener) {}
|
||||
|
||||
override fun collapseStack() {}
|
||||
|
||||
override fun updateWindowFlagsForBackpress(intercept: Boolean) {}
|
||||
|
||||
override fun promoteBubbleFromOverflow(bubble: Bubble) {}
|
||||
|
||||
override fun removeBubble(key: String, reason: Int) {}
|
||||
|
||||
override fun dismissBubble(bubble: Bubble, reason: Int) {}
|
||||
|
||||
override fun setAppBubbleTaskId(key: String, taskId: Int) {}
|
||||
|
||||
override fun isStackExpanded(): Boolean = false
|
||||
|
||||
override fun isShowingAsBubbleBar(): Boolean = false
|
||||
|
||||
override fun hideCurrentInputMethod() {}
|
||||
}
|
||||
}
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.wm.shell.bubbles
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.wm.shell.taskview.TaskView
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.google.common.util.concurrent.MoreExecutors.directExecutor
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.never
|
||||
import org.mockito.kotlin.verify
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class BubbleTaskViewTest {
|
||||
|
||||
private lateinit var bubbleTaskView: BubbleTaskView
|
||||
private val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
private lateinit var taskView: TaskView
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
taskView = mock()
|
||||
bubbleTaskView = BubbleTaskView(taskView, directExecutor())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTaskCreated_updatesState() {
|
||||
val componentName = ComponentName(context, "TestClass")
|
||||
bubbleTaskView.listener.onTaskCreated(123, componentName)
|
||||
|
||||
assertThat(bubbleTaskView.taskId).isEqualTo(123)
|
||||
assertThat(bubbleTaskView.componentName).isEqualTo(componentName)
|
||||
assertThat(bubbleTaskView.isCreated).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTaskCreated_callsDelegateListener() {
|
||||
var actualTaskId = -1
|
||||
var actualComponentName: ComponentName? = null
|
||||
val delegateListener = object : TaskView.Listener {
|
||||
override fun onTaskCreated(taskId: Int, name: ComponentName) {
|
||||
actualTaskId = taskId
|
||||
actualComponentName = name
|
||||
}
|
||||
}
|
||||
bubbleTaskView.delegateListener = delegateListener
|
||||
|
||||
val componentName = ComponentName(context, "TestClass")
|
||||
bubbleTaskView.listener.onTaskCreated(123, componentName)
|
||||
|
||||
assertThat(actualTaskId).isEqualTo(123)
|
||||
assertThat(actualComponentName).isEqualTo(componentName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cleanup_invalidTaskId_doesNotRemoveTask() {
|
||||
bubbleTaskView.cleanup()
|
||||
verify(taskView, never()).removeTask()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cleanup_validTaskId_removesTask() {
|
||||
val componentName = ComponentName(context, "TestClass")
|
||||
bubbleTaskView.listener.onTaskCreated(123, componentName)
|
||||
|
||||
bubbleTaskView.cleanup()
|
||||
verify(taskView).removeTask()
|
||||
}
|
||||
}
|
||||
-459
@@ -1,459 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.wm.shell.bubbles.bar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Insets
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.animation.AnimatorTestRule
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import com.android.internal.protolog.common.ProtoLog
|
||||
import com.android.wm.shell.R
|
||||
import com.android.wm.shell.bubbles.BubblePositioner
|
||||
import com.android.wm.shell.bubbles.DeviceConfig
|
||||
import com.android.wm.shell.common.bubbles.BaseBubblePinController
|
||||
import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_IN_DURATION
|
||||
import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_DURATION
|
||||
import com.android.wm.shell.common.bubbles.BubbleBarLocation
|
||||
import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT
|
||||
import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.ClassRule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/** Tests for [BubbleExpandedViewPinController] */
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class BubbleExpandedViewPinControllerTest {
|
||||
|
||||
companion object {
|
||||
@JvmField @ClassRule val animatorTestRule: AnimatorTestRule = AnimatorTestRule()
|
||||
|
||||
const val SCREEN_WIDTH = 2000
|
||||
const val SCREEN_HEIGHT = 1000
|
||||
|
||||
const val BUBBLE_BAR_HEIGHT = 50
|
||||
}
|
||||
|
||||
private val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
private lateinit var positioner: BubblePositioner
|
||||
private lateinit var container: FrameLayout
|
||||
|
||||
private lateinit var controller: BubbleExpandedViewPinController
|
||||
private lateinit var testListener: TestLocationChangeListener
|
||||
|
||||
private val dropTargetView: View?
|
||||
get() = container.findViewById(R.id.bubble_bar_drop_target)
|
||||
|
||||
private val pointOnLeft = PointF(100f, 100f)
|
||||
private val pointOnRight = PointF(1900f, 500f)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
ProtoLog.REQUIRE_PROTOLOGTOOL = false
|
||||
container = FrameLayout(context)
|
||||
val windowManager = context.getSystemService(WindowManager::class.java)
|
||||
positioner = BubblePositioner(context, windowManager)
|
||||
positioner.setShowingInBubbleBar(true)
|
||||
val deviceConfig =
|
||||
DeviceConfig(
|
||||
windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT),
|
||||
isLargeScreen = true,
|
||||
isSmallTablet = false,
|
||||
isLandscape = true,
|
||||
isRtl = false,
|
||||
insets = Insets.of(10, 20, 30, 40)
|
||||
)
|
||||
positioner.update(deviceConfig)
|
||||
positioner.bubbleBarTopOnScreen =
|
||||
SCREEN_HEIGHT - deviceConfig.insets.bottom - BUBBLE_BAR_HEIGHT
|
||||
controller = BubbleExpandedViewPinController(context, container, positioner)
|
||||
testListener = TestLocationChangeListener()
|
||||
controller.setListener(testListener)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
getInstrumentation().runOnMainSync { controller.onDragEnd() }
|
||||
waitForAnimateOut()
|
||||
}
|
||||
|
||||
/** Dragging on same side should not show drop target or trigger location changes */
|
||||
@Test
|
||||
fun drag_stayOnRightSide() {
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragStart(initialLocationOnLeft = false)
|
||||
controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
|
||||
controller.onDragEnd()
|
||||
}
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNull()
|
||||
assertThat(testListener.locationChanges).isEmpty()
|
||||
assertThat(testListener.locationReleases).containsExactly(RIGHT)
|
||||
}
|
||||
|
||||
/** Dragging on same side should not show drop target or trigger location changes */
|
||||
@Test
|
||||
fun drag_stayOnLeftSide() {
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragStart(initialLocationOnLeft = true)
|
||||
controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
|
||||
controller.onDragEnd()
|
||||
}
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNull()
|
||||
assertThat(testListener.locationChanges).isEmpty()
|
||||
assertThat(testListener.locationReleases).containsExactly(LEFT)
|
||||
}
|
||||
|
||||
/** Drag crosses to the other side. Show drop target and trigger a location change. */
|
||||
@Test
|
||||
fun drag_rightToLeft() {
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragStart(initialLocationOnLeft = false)
|
||||
controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
|
||||
controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
|
||||
}
|
||||
waitForAnimateIn()
|
||||
|
||||
assertThat(dropTargetView).isNotNull()
|
||||
assertThat(dropTargetView!!.alpha).isEqualTo(1f)
|
||||
assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft())
|
||||
assertThat(testListener.locationChanges).containsExactly(LEFT)
|
||||
assertThat(testListener.locationReleases).isEmpty()
|
||||
}
|
||||
|
||||
/** Drag crosses to the other side. Show drop target and trigger a location change. */
|
||||
@Test
|
||||
fun drag_leftToRight() {
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragStart(initialLocationOnLeft = true)
|
||||
controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
|
||||
controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
|
||||
}
|
||||
waitForAnimateIn()
|
||||
|
||||
assertThat(dropTargetView).isNotNull()
|
||||
assertThat(dropTargetView!!.alpha).isEqualTo(1f)
|
||||
assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight())
|
||||
assertThat(testListener.locationChanges).containsExactly(RIGHT)
|
||||
assertThat(testListener.locationReleases).isEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop target does not initially show on the side that the drag starts. Check that it shows up
|
||||
* after the dragging the view to other side and back to the initial side.
|
||||
*/
|
||||
@Test
|
||||
fun drag_rightToLeftToRight() {
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragStart(initialLocationOnLeft = false)
|
||||
controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
|
||||
}
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNull()
|
||||
|
||||
getInstrumentation().runOnMainSync { controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) }
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNotNull()
|
||||
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
|
||||
}
|
||||
waitForAnimateOut()
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNotNull()
|
||||
assertThat(dropTargetView!!.alpha).isEqualTo(1f)
|
||||
assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight())
|
||||
assertThat(testListener.locationChanges).containsExactly(LEFT, RIGHT).inOrder()
|
||||
assertThat(testListener.locationReleases).isEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop target does not initially show on the side that the drag starts. Check that it shows up
|
||||
* after the dragging the view to other side and back to the initial side.
|
||||
*/
|
||||
@Test
|
||||
fun drag_leftToRightToLeft() {
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragStart(initialLocationOnLeft = true)
|
||||
controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
|
||||
}
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNull()
|
||||
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
|
||||
}
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNotNull()
|
||||
|
||||
getInstrumentation().runOnMainSync { controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) }
|
||||
waitForAnimateOut()
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNotNull()
|
||||
assertThat(dropTargetView!!.alpha).isEqualTo(1f)
|
||||
assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft())
|
||||
assertThat(testListener.locationChanges).containsExactly(RIGHT, LEFT).inOrder()
|
||||
assertThat(testListener.locationReleases).isEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag from right to left, but stay in exclusion rect around the dismiss view. Drop target
|
||||
* should not show and location change should not trigger.
|
||||
*/
|
||||
@Test
|
||||
fun drag_rightToLeft_inExclusionRect() {
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragStart(initialLocationOnLeft = false)
|
||||
controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
|
||||
// Exclusion rect is around the bottom center area of the screen
|
||||
controller.onDragUpdate(SCREEN_WIDTH / 2f - 50, SCREEN_HEIGHT - 100f)
|
||||
}
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNull()
|
||||
assertThat(testListener.locationChanges).isEmpty()
|
||||
assertThat(testListener.locationReleases).isEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag from left to right, but stay in exclusion rect around the dismiss view. Drop target
|
||||
* should not show and location change should not trigger.
|
||||
*/
|
||||
@Test
|
||||
fun drag_leftToRight_inExclusionRect() {
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragStart(initialLocationOnLeft = true)
|
||||
controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
|
||||
// Exclusion rect is around the bottom center area of the screen
|
||||
controller.onDragUpdate(SCREEN_WIDTH / 2f + 50, SCREEN_HEIGHT - 100f)
|
||||
}
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNull()
|
||||
assertThat(testListener.locationChanges).isEmpty()
|
||||
assertThat(testListener.locationReleases).isEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag to dismiss target and back to the same side should not cause the drop target to show.
|
||||
*/
|
||||
@Test
|
||||
fun drag_rightToDismissToRight() {
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragStart(initialLocationOnLeft = false)
|
||||
controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
|
||||
controller.onStuckToDismissTarget()
|
||||
controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
|
||||
}
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNull()
|
||||
assertThat(testListener.locationChanges).isEmpty()
|
||||
assertThat(testListener.locationReleases).isEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag to dismiss target and back to the same side should not cause the drop target to show.
|
||||
*/
|
||||
@Test
|
||||
fun drag_leftToDismissToLeft() {
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragStart(initialLocationOnLeft = true)
|
||||
controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
|
||||
controller.onStuckToDismissTarget()
|
||||
controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
|
||||
}
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNull()
|
||||
assertThat(testListener.locationChanges).isEmpty()
|
||||
assertThat(testListener.locationReleases).isEmpty()
|
||||
}
|
||||
|
||||
/** Drag to dismiss target and other side should show drop target on the other side. */
|
||||
@Test
|
||||
fun drag_rightToDismissToLeft() {
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragStart(initialLocationOnLeft = false)
|
||||
controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
|
||||
controller.onStuckToDismissTarget()
|
||||
controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
|
||||
}
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNotNull()
|
||||
assertThat(dropTargetView!!.alpha).isEqualTo(1f)
|
||||
assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft())
|
||||
|
||||
assertThat(testListener.locationChanges).containsExactly(LEFT)
|
||||
assertThat(testListener.locationReleases).isEmpty()
|
||||
}
|
||||
|
||||
/** Drag to dismiss target and other side should show drop target on the other side. */
|
||||
@Test
|
||||
fun drag_leftToDismissToRight() {
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragStart(initialLocationOnLeft = true)
|
||||
controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
|
||||
controller.onStuckToDismissTarget()
|
||||
controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
|
||||
}
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNotNull()
|
||||
assertThat(dropTargetView!!.alpha).isEqualTo(1f)
|
||||
assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight())
|
||||
|
||||
assertThat(testListener.locationChanges).containsExactly(RIGHT)
|
||||
assertThat(testListener.locationReleases).isEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag to dismiss should trigger a location change to the initial location, if the current
|
||||
* location is different. And hide the drop target.
|
||||
*/
|
||||
@Test
|
||||
fun drag_rightToLeftToDismiss() {
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragStart(initialLocationOnLeft = false)
|
||||
controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
|
||||
controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
|
||||
}
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNotNull()
|
||||
assertThat(dropTargetView!!.alpha).isEqualTo(1f)
|
||||
|
||||
getInstrumentation().runOnMainSync { controller.onStuckToDismissTarget() }
|
||||
waitForAnimateOut()
|
||||
assertThat(dropTargetView!!.alpha).isEqualTo(0f)
|
||||
|
||||
assertThat(testListener.locationChanges).containsExactly(LEFT, RIGHT).inOrder()
|
||||
assertThat(testListener.locationReleases).isEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag to dismiss should trigger a location change to the initial location, if the current
|
||||
* location is different. And hide the drop target.
|
||||
*/
|
||||
@Test
|
||||
fun drag_leftToRightToDismiss() {
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragStart(initialLocationOnLeft = true)
|
||||
controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
|
||||
controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
|
||||
}
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNotNull()
|
||||
assertThat(dropTargetView!!.alpha).isEqualTo(1f)
|
||||
getInstrumentation().runOnMainSync { controller.onStuckToDismissTarget() }
|
||||
waitForAnimateOut()
|
||||
assertThat(dropTargetView!!.alpha).isEqualTo(0f)
|
||||
assertThat(testListener.locationChanges).containsExactly(RIGHT, LEFT).inOrder()
|
||||
assertThat(testListener.locationReleases).isEmpty()
|
||||
}
|
||||
|
||||
/** Finishing drag should remove drop target and send location update. */
|
||||
@Test
|
||||
fun drag_rightToLeftRelease() {
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragStart(initialLocationOnLeft = false)
|
||||
controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
|
||||
controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
|
||||
}
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNotNull()
|
||||
|
||||
getInstrumentation().runOnMainSync { controller.onDragEnd() }
|
||||
waitForAnimateOut()
|
||||
assertThat(dropTargetView).isNull()
|
||||
assertThat(testListener.locationChanges).containsExactly(LEFT)
|
||||
assertThat(testListener.locationReleases).containsExactly(LEFT)
|
||||
}
|
||||
|
||||
/** Finishing drag should remove drop target and send location update. */
|
||||
@Test
|
||||
fun drag_leftToRightRelease() {
|
||||
getInstrumentation().runOnMainSync {
|
||||
controller.onDragStart(initialLocationOnLeft = true)
|
||||
controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
|
||||
controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
|
||||
}
|
||||
waitForAnimateIn()
|
||||
assertThat(dropTargetView).isNotNull()
|
||||
|
||||
getInstrumentation().runOnMainSync { controller.onDragEnd() }
|
||||
waitForAnimateOut()
|
||||
assertThat(dropTargetView).isNull()
|
||||
assertThat(testListener.locationChanges).containsExactly(RIGHT)
|
||||
assertThat(testListener.locationReleases).containsExactly(RIGHT)
|
||||
}
|
||||
|
||||
private fun getExpectedDropTargetBoundsOnLeft(): Rect =
|
||||
Rect().also {
|
||||
positioner.getBubbleBarExpandedViewBounds(
|
||||
true /* onLeft */,
|
||||
false /* isOverflowExpanded */,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
private fun getExpectedDropTargetBoundsOnRight(): Rect =
|
||||
Rect().also {
|
||||
positioner.getBubbleBarExpandedViewBounds(
|
||||
false /* onLeft */,
|
||||
false /* isOverflowExpanded */,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
private fun waitForAnimateIn() {
|
||||
// Advance animator for on-device test
|
||||
getInstrumentation().runOnMainSync {
|
||||
animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION)
|
||||
}
|
||||
}
|
||||
|
||||
private fun waitForAnimateOut() {
|
||||
// Advance animator for on-device test
|
||||
getInstrumentation().runOnMainSync {
|
||||
animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION)
|
||||
}
|
||||
}
|
||||
|
||||
private fun View.bounds(): Rect {
|
||||
return Rect(0, 0, layoutParams.width, layoutParams.height).also { rect ->
|
||||
rect.offsetTo(x.toInt(), y.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
internal class TestLocationChangeListener : BaseBubblePinController.LocationChangeListener {
|
||||
val locationChanges = mutableListOf<BubbleBarLocation>()
|
||||
val locationReleases = mutableListOf<BubbleBarLocation>()
|
||||
override fun onChange(location: BubbleBarLocation) {
|
||||
locationChanges.add(location)
|
||||
}
|
||||
|
||||
override fun onRelease(location: BubbleBarLocation) {
|
||||
locationReleases.add(location)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user