blob: de40b7fd0a136be8bc7b22dff367fc8956ac0f3d [file] [log] [blame]
package com.android.systemui.statusbar.phone
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import android.view.WindowManagerPolicyConstants
import androidx.annotation.IdRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.navigationbar.NavigationModeController
import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener
import com.android.systemui.recents.OverviewProxyService
import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.RETURNS_DEEP_STUBS
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.eq
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import java.util.function.Consumer
import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class NotificationQSContainerControllerTest : SysuiTestCase() {
companion object {
const val STABLE_INSET_BOTTOM = 100
const val CUTOUT_HEIGHT = 50
const val GESTURES_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL
const val BUTTONS_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON
const val NOTIFICATIONS_MARGIN = 50
const val SCRIM_MARGIN = 10
const val FOOTER_ACTIONS_INSET = 2
const val FOOTER_ACTIONS_PADDING = 2
const val FOOTER_ACTIONS_OFFSET = FOOTER_ACTIONS_INSET + FOOTER_ACTIONS_PADDING
const val QS_PADDING_OFFSET = SCRIM_MARGIN + FOOTER_ACTIONS_OFFSET
}
@Mock
private lateinit var navigationModeController: NavigationModeController
@Mock
private lateinit var overviewProxyService: OverviewProxyService
@Mock
private lateinit var notificationsQSContainer: NotificationsQuickSettingsContainer
@Mock
private lateinit var featureFlags: FeatureFlags
@Captor
lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener>
@Captor
lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener>
@Captor
lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>>
@Captor
lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet>
private lateinit var controller: NotificationsQSContainerController
private lateinit var navigationModeCallback: ModeChangedListener
private lateinit var taskbarVisibilityCallback: OverviewProxyListener
private lateinit var windowInsetsCallback: Consumer<WindowInsets>
private lateinit var delayableExecutor: FakeExecutor
private lateinit var fakeSystemClock: FakeSystemClock
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
mContext.ensureTestableResources()
whenever(notificationsQSContainer.context).thenReturn(mContext)
whenever(notificationsQSContainer.resources).thenReturn(mContext.resources)
fakeSystemClock = FakeSystemClock()
delayableExecutor = FakeExecutor(fakeSystemClock)
controller = NotificationsQSContainerController(
notificationsQSContainer,
navigationModeController,
overviewProxyService,
featureFlags,
delayableExecutor
)
overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN)
overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN)
overrideResource(R.bool.config_use_split_notification_shade, false)
overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING)
overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET)
whenever(navigationModeController.addListener(navigationModeCaptor.capture()))
.thenReturn(GESTURES_NAVIGATION)
doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture())
doNothing().`when`(notificationsQSContainer)
.setInsetsChangedListener(windowInsetsCallbackCaptor.capture())
doNothing().`when`(notificationsQSContainer).applyConstraints(constraintSetCaptor.capture())
controller.init()
controller.onViewAttached()
navigationModeCallback = navigationModeCaptor.value
taskbarVisibilityCallback = taskbarVisibilityCaptor.value
windowInsetsCallback = windowInsetsCallbackCaptor.value
}
@Test
fun testTaskbarVisibleInSplitShade() {
enableSplitShade()
given(taskbarVisible = true,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = 0, // taskbar should disappear when shade is expanded
expectedNotificationsMargin = NOTIFICATIONS_MARGIN,
expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET)
given(taskbarVisible = true,
navigationMode = BUTTONS_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = STABLE_INSET_BOTTOM,
expectedNotificationsMargin = NOTIFICATIONS_MARGIN,
expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET)
}
@Test
fun testTaskbarNotVisibleInSplitShade() {
// when taskbar is not visible, it means we're on the home screen
enableSplitShade()
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = 0,
expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET)
given(taskbarVisible = false,
navigationMode = BUTTONS_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons
expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN,
expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET)
}
@Test
fun testTaskbarNotVisibleInSplitShadeWithCutout() {
enableSplitShade()
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withCutout())
then(expectedContainerPadding = CUTOUT_HEIGHT,
expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET)
given(taskbarVisible = false,
navigationMode = BUTTONS_NAVIGATION,
insets = windowInsets().withCutout().withStableBottom())
then(expectedContainerPadding = 0,
expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN,
expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET)
}
@Test
fun testTaskbarVisibleInSinglePaneShade() {
disableSplitShade()
given(taskbarVisible = true,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = 0,
expectedQsPadding = STABLE_INSET_BOTTOM)
given(taskbarVisible = true,
navigationMode = BUTTONS_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = STABLE_INSET_BOTTOM,
expectedQsPadding = STABLE_INSET_BOTTOM)
}
@Test
fun testTaskbarNotVisibleInSinglePaneShade() {
disableSplitShade()
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = emptyInsets())
then(expectedContainerPadding = 0)
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withCutout().withStableBottom())
then(expectedContainerPadding = CUTOUT_HEIGHT, expectedQsPadding = STABLE_INSET_BOTTOM)
given(taskbarVisible = false,
navigationMode = BUTTONS_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = 0,
expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN,
expectedQsPadding = STABLE_INSET_BOTTOM)
}
@Test
fun testCustomizingInSinglePaneShade() {
disableSplitShade()
controller.setCustomizerShowing(true)
// always sets spacings to 0
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = 0,
expectedNotificationsMargin = 0)
given(taskbarVisible = false,
navigationMode = BUTTONS_NAVIGATION,
insets = emptyInsets())
then(expectedContainerPadding = 0,
expectedNotificationsMargin = 0)
}
@Test
fun testDetailShowingInSinglePaneShade() {
disableSplitShade()
controller.setDetailShowing(true)
// always sets spacings to 0
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = 0,
expectedNotificationsMargin = 0)
given(taskbarVisible = false,
navigationMode = BUTTONS_NAVIGATION,
insets = emptyInsets())
then(expectedContainerPadding = 0,
expectedNotificationsMargin = 0)
}
@Test
fun testDetailShowingInSplitShade() {
enableSplitShade()
controller.setDetailShowing(true)
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = 0)
// should not influence spacing
given(taskbarVisible = false,
navigationMode = BUTTONS_NAVIGATION,
insets = emptyInsets())
then(expectedContainerPadding = 0)
}
@Test
fun testNotificationsMarginBottomIsUpdated() {
Mockito.clearInvocations(notificationsQSContainer)
enableSplitShade()
verify(notificationsQSContainer).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN)
overrideResource(R.dimen.notification_panel_margin_bottom, 100)
disableSplitShade()
verify(notificationsQSContainer).setNotificationsMarginBottom(100)
}
@Test
fun testSplitShadeLayout_isAlignedToGuideline() {
enableSplitShade()
controller.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd)
.isEqualTo(R.id.qs_edge_guideline)
assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart)
.isEqualTo(R.id.qs_edge_guideline)
}
@Test
fun testSinglePaneLayout_childrenHaveEqualMargins() {
disableSplitShade()
controller.updateResources()
val qsStartMargin = getConstraintSetLayout(R.id.qs_frame).startMargin
val qsEndMargin = getConstraintSetLayout(R.id.qs_frame).endMargin
val notifStartMargin = getConstraintSetLayout(R.id.notification_stack_scroller).startMargin
val notifEndMargin = getConstraintSetLayout(R.id.notification_stack_scroller).endMargin
assertThat(qsStartMargin == qsEndMargin &&
notifStartMargin == notifEndMargin &&
qsStartMargin == notifStartMargin
).isTrue()
}
@Test
fun testSplitShadeLayout_childrenHaveInsideMarginsOfZero() {
enableSplitShade()
controller.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0)
assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startMargin)
.isEqualTo(0)
}
@Test
fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() {
enableSplitShade()
controller.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0)
assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin).isEqualTo(0)
}
@Test
fun testLargeScreenLayout_qsAndNotifsTopMarginIsOfHeaderHeight() {
setLargeScreen()
val largeScreenHeaderHeight = 100
overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderHeight)
controller.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin)
.isEqualTo(largeScreenHeaderHeight)
assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin)
.isEqualTo(largeScreenHeaderHeight)
}
@Test
fun testSmallScreenLayout_qsAndNotifsTopMarginIsZero() {
setSmallScreen()
controller.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin).isEqualTo(0)
assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin)
.isEqualTo(0)
}
@Test
fun testSinglePaneShadeLayout_qsFrameHasHorizontalMarginsSetToCorrectValue() {
disableSplitShade()
controller.updateResources()
val notificationPanelMarginHorizontal = context.resources
.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal)
assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin)
.isEqualTo(notificationPanelMarginHorizontal)
assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin)
.isEqualTo(notificationPanelMarginHorizontal)
}
@Test
fun testSinglePaneShadeLayout_isAlignedToParent() {
disableSplitShade()
controller.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd)
.isEqualTo(ConstraintSet.PARENT_ID)
assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart)
.isEqualTo(ConstraintSet.PARENT_ID)
}
@Test
fun testAllChildrenOfNotificationContainer_haveIds() {
// set dimen to 0 to avoid triggering updating bottom spacing
overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, 0)
val container = NotificationsQuickSettingsContainer(context, null)
container.removeAllViews()
container.addView(newViewWithId(1))
container.addView(newViewWithId(View.NO_ID))
val controller = NotificationsQSContainerController(container, navigationModeController,
overviewProxyService, featureFlags, delayableExecutor)
controller.updateConstraints()
assertThat(container.getChildAt(0).id).isEqualTo(1)
assertThat(container.getChildAt(1).id).isNotEqualTo(View.NO_ID)
}
@Test
fun testWindowInsetDebounce() {
disableSplitShade()
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = emptyInsets(),
applyImmediately = false)
fakeSystemClock.advanceTime(INSET_DEBOUNCE_MILLIS / 2)
windowInsetsCallback.accept(windowInsets().withStableBottom())
delayableExecutor.advanceClockToLast()
delayableExecutor.runAllReady()
verify(notificationsQSContainer, never()).setQSContainerPaddingBottom(0)
verify(notificationsQSContainer).setQSContainerPaddingBottom(STABLE_INSET_BOTTOM)
}
private fun disableSplitShade() {
setSplitShadeEnabled(false)
}
private fun enableSplitShade() {
setSplitShadeEnabled(true)
}
private fun setSplitShadeEnabled(enabled: Boolean) {
overrideResource(R.bool.config_use_split_notification_shade, enabled)
controller.updateResources()
}
private fun setSmallScreen() {
setLargeScreenEnabled(false)
}
private fun setLargeScreen() {
setLargeScreenEnabled(true)
}
private fun setLargeScreenEnabled(enabled: Boolean) {
overrideResource(R.bool.config_use_large_screen_shade_header, enabled)
}
private fun given(
taskbarVisible: Boolean,
navigationMode: Int,
insets: WindowInsets,
applyImmediately: Boolean = true
) {
Mockito.clearInvocations(notificationsQSContainer)
taskbarVisibilityCallback.onTaskbarStatusUpdated(taskbarVisible, false)
navigationModeCallback.onNavigationModeChanged(navigationMode)
windowInsetsCallback.accept(insets)
if (applyImmediately) {
delayableExecutor.advanceClockToLast()
delayableExecutor.runAllReady()
}
}
fun then(
expectedContainerPadding: Int,
expectedNotificationsMargin: Int = NOTIFICATIONS_MARGIN,
expectedQsPadding: Int = 0
) {
verify(notificationsQSContainer)
.setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding))
verify(notificationsQSContainer).setNotificationsMarginBottom(expectedNotificationsMargin)
verify(notificationsQSContainer)
.setQSContainerPaddingBottom(expectedQsPadding)
Mockito.clearInvocations(notificationsQSContainer)
}
private fun windowInsets() = mock(WindowInsets::class.java, RETURNS_DEEP_STUBS)
private fun emptyInsets() = mock(WindowInsets::class.java)
private fun WindowInsets.withCutout(): WindowInsets {
whenever(displayCutout.safeInsetBottom).thenReturn(CUTOUT_HEIGHT)
return this
}
private fun WindowInsets.withStableBottom(): WindowInsets {
whenever(stableInsetBottom).thenReturn(STABLE_INSET_BOTTOM)
return this
}
private fun getConstraintSetLayout(@IdRes id: Int): ConstraintSet.Layout {
return constraintSetCaptor.value.getConstraint(id).layout
}
private fun newViewWithId(id: Int): View {
val view = View(mContext)
view.id = id
val layoutParams = ConstraintLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
// required as cloning ConstraintSet fails if view doesn't have layout params
view.layoutParams = layoutParams
return view
}
}