Merge "Begin Migrate NSSL" into udc-qpr-dev

This commit is contained in:
Matt Pietal
2023-07-14 15:40:57 +00:00
committed by Android (Google) Code Review
14 changed files with 1301 additions and 213 deletions

View File

@@ -76,6 +76,13 @@
android:layout_height="match_parent"
android:visibility="invisible" />
<!-- Shared container for the notification stack. Can be positioned by either
the keyguard_root_view or notification_panel -->
<com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
android:id="@+id/shared_notification_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<include layout="@layout/brightness_mirror_container" />
<com.android.systemui.scrim.ScrimView

View File

@@ -211,6 +211,7 @@
<item type="id" name="keyguard_indication_area" />
<item type="id" name="keyguard_indication_text" />
<item type="id" name="keyguard_indication_text_bottom" />
<item type="id" name="nssl_guideline" />
<item type="id" name="lock_icon" />
<item type="id" name="lock_icon_bg" />
</resources>

View File

@@ -31,6 +31,9 @@ import com.android.systemui.keyguard.ui.view.layout.KeyguardLayoutManagerCommand
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
import com.android.systemui.shade.NotificationShadeWindowView
import com.android.systemui.statusbar.KeyguardIndicationController
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
import javax.inject.Inject
import kotlinx.coroutines.DisposableHandle
@@ -40,7 +43,9 @@ class KeyguardViewConfigurator
@Inject
constructor(
private val keyguardRootView: KeyguardRootView,
private val sharedNotificationContainer: SharedNotificationContainer,
private val keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel,
private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
private val notificationShadeWindowView: NotificationShadeWindowView,
private val featureFlags: FeatureFlags,
private val indicationController: KeyguardIndicationController,
@@ -55,10 +60,28 @@ constructor(
notificationShadeWindowView.requireViewById(R.id.notification_panel) as ViewGroup
bindIndicationArea(notificationPanel)
bindLockIconView(notificationPanel)
setupNotificationStackScrollLayout(notificationPanel)
keyguardLayoutManager.layoutViews()
keyguardLayoutManagerCommandListener.start()
}
fun setupNotificationStackScrollLayout(legacyParent: ViewGroup) {
if (featureFlags.isEnabled(Flags.MIGRATE_NSSL)) {
// This moves the existing NSSL view to a different parent, as the controller is a
// singleton and recreating it has other bad side effects
val nssl =
legacyParent.requireViewById<View>(R.id.notification_stack_scroller).also {
(it.getParent() as ViewGroup).removeView(it)
}
sharedNotificationContainer.addNotificationStackScrollLayout(nssl)
SharedNotificationContainerBinder.bind(
sharedNotificationContainer,
sharedNotificationContainerViewModel
)
}
}
fun bindIndicationArea(legacyParent: ViewGroup) {
indicationAreaHandle?.dispose()

View File

@@ -29,6 +29,8 @@ import androidx.constraintlayout.widget.ConstraintSet.TOP
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.fragments.FragmentService
import com.android.systemui.navigationbar.NavigationModeController
import com.android.systemui.plugins.qs.QS
@@ -36,6 +38,7 @@ import com.android.systemui.plugins.qs.QSContainerController
import com.android.systemui.recents.OverviewProxyService
import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
import com.android.systemui.shared.system.QuickStepContract
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.util.LargeScreenUtils
import com.android.systemui.util.ViewController
import com.android.systemui.util.concurrency.DelayableExecutor
@@ -54,7 +57,10 @@ class NotificationsQSContainerController @Inject constructor(
private val shadeHeaderController: ShadeHeaderController,
private val shadeExpansionStateManager: ShadeExpansionStateManager,
private val fragmentService: FragmentService,
@Main private val delayableExecutor: DelayableExecutor
@Main private val delayableExecutor: DelayableExecutor,
private val featureFlags: FeatureFlags,
private val
notificationStackScrollLayoutController: NotificationStackScrollLayoutController,
) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController {
private var qsExpanded = false
@@ -118,6 +124,9 @@ class NotificationsQSContainerController @Inject constructor(
isGestureNavigation = QuickStepContract.isGesturalMode(mode)
}
isGestureNavigation = QuickStepContract.isGesturalMode(currentMode)
mView.setStackScroller(notificationStackScrollLayoutController.getView())
mView.setMigratingNSSL(featureFlags.isEnabled(Flags.MIGRATE_NSSL))
}
public override fun onViewAttached() {
@@ -254,14 +263,17 @@ class NotificationsQSContainerController @Inject constructor(
}
private fun setNotificationsConstraints(constraintSet: ConstraintSet) {
if (featureFlags.isEnabled(Flags.MIGRATE_NSSL)) {
return
}
val startConstraintId = if (splitShadeEnabled) R.id.qs_edge_guideline else PARENT_ID
val nsslId = R.id.notification_stack_scroller
constraintSet.apply {
connect(R.id.notification_stack_scroller, START, startConstraintId, START)
setMargin(R.id.notification_stack_scroller, START,
if (splitShadeEnabled) 0 else panelMarginHorizontal)
setMargin(R.id.notification_stack_scroller, END, panelMarginHorizontal)
setMargin(R.id.notification_stack_scroller, TOP, topMargin)
setMargin(R.id.notification_stack_scroller, BOTTOM, notificationsBottomMargin)
connect(nsslId, START, startConstraintId, START)
setMargin(nsslId, START, if (splitShadeEnabled) 0 else panelMarginHorizontal)
setMargin(nsslId, END, panelMarginHorizontal)
setMargin(nsslId, TOP, topMargin)
setMargin(nsslId, BOTTOM, notificationsBottomMargin)
}
}

View File

@@ -23,6 +23,7 @@ import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.WindowInsets;
import androidx.annotation.Nullable;
@@ -56,6 +57,7 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout
private QS mQs;
private View mQSContainer;
private int mLastQSPaddingBottom;
private boolean mIsMigratingNSSL;
/**
* These are used to compute the bounding box containing the shade and the notification scrim,
@@ -75,10 +77,13 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout
protected void onFinishInflate() {
super.onFinishInflate();
mQsFrame = findViewById(R.id.qs_frame);
mStackScroller = findViewById(R.id.notification_stack_scroller);
mKeyguardStatusBar = findViewById(R.id.keyguard_header);
}
void setStackScroller(View stackScroller) {
mStackScroller = stackScroller;
}
@Override
public void onFragmentViewCreated(String tag, Fragment fragment) {
mQs = (QS) fragment;
@@ -108,7 +113,7 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout
}
public void setNotificationsMarginBottom(int margin) {
LayoutParams params = (LayoutParams) mStackScroller.getLayoutParams();
MarginLayoutParams params = (MarginLayoutParams) mStackScroller.getLayoutParams();
params.bottomMargin = margin;
mStackScroller.setLayoutParams(params);
}
@@ -173,8 +178,15 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout
super.dispatchDraw(canvas);
}
void setMigratingNSSL(boolean isMigrating) {
mIsMigratingNSSL = isMigrating;
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
if (mIsMigratingNSSL) {
return super.drawChild(canvas, child, drawingTime);
}
int layoutIndex = mLayoutDrawingOrder.indexOf(child);
if (layoutIndex >= 0) {
return super.drawChild(canvas, mDrawingOrderedChildren.get(layoutIndex), drawingTime);

View File

@@ -49,6 +49,7 @@ import com.android.systemui.statusbar.NotificationShelfController
import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent
import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.phone.StatusIconContainer
@@ -203,6 +204,14 @@ abstract class ShadeModule {
return notificationShadeWindowView.findViewById(R.id.keyguard_root_view)
}
@Provides
@SysUISingleton
fun providesSharedNotificationContainer(
notificationShadeWindowView: NotificationShadeWindowView,
): SharedNotificationContainer {
return notificationShadeWindowView.findViewById(R.id.shared_notification_container)
}
// TODO(b/277762009): Only allow this view's controller to inject the view. See above.
@Provides
@SysUISingleton

View File

@@ -0,0 +1,65 @@
/*
* 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.systemui.statusbar.notification.stack.domain.interactor
import android.content.Context
import com.android.systemui.R
import com.android.systemui.common.ui.data.repository.ConfigurationRepository
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
/** Encapsulates business-logic specifically related to the shared notification stack container. */
class SharedNotificationContainerInteractor
@Inject
constructor(
configurationRepository: ConfigurationRepository,
private val context: Context,
) {
val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
configurationRepository.onAnyConfigurationChange
.onStart { emit(Unit) }
.map { _ ->
with(context.resources) {
ConfigurationBasedDimensions(
useSplitShade = getBoolean(R.bool.config_use_split_notification_shade),
useLargeScreenHeader =
getBoolean(R.bool.config_use_large_screen_shade_header),
marginHorizontal =
getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal),
marginBottom =
getDimensionPixelSize(R.dimen.notification_panel_margin_bottom),
marginTop = getDimensionPixelSize(R.dimen.notification_panel_margin_top),
marginTopLargeScreen =
getDimensionPixelSize(R.dimen.large_screen_shade_header_height),
)
}
}
.distinctUntilChanged()
data class ConfigurationBasedDimensions(
val useSplitShade: Boolean,
val useLargeScreenHeader: Boolean,
val marginHorizontal: Int,
val marginBottom: Int,
val marginTop: Int,
val marginTopLargeScreen: Int,
)
}

View File

@@ -0,0 +1,88 @@
/*
* 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.systemui.statusbar.notification.stack.ui.view
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
import androidx.constraintlayout.widget.ConstraintSet.END
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.START
import androidx.constraintlayout.widget.ConstraintSet.TOP
import androidx.constraintlayout.widget.ConstraintSet.VERTICAL
import com.android.systemui.R
/**
* Container for the stack scroller, so that the bounds can be externally specified, such as from
* the keyguard or shade scenes.
*/
class SharedNotificationContainer(
context: Context,
private val attrs: AttributeSet?,
) :
ConstraintLayout(
context,
attrs,
) {
private val baseConstraintSet = ConstraintSet()
init {
baseConstraintSet.apply {
create(R.id.nssl_guideline, VERTICAL)
setGuidelinePercent(R.id.nssl_guideline, 0.5f)
}
baseConstraintSet.applyTo(this)
}
fun addNotificationStackScrollLayout(nssl: View) {
addView(nssl)
}
fun updateConstraints(
useSplitShade: Boolean,
marginStart: Int,
marginTop: Int,
marginEnd: Int,
marginBottom: Int
) {
val constraintSet = ConstraintSet()
constraintSet.clone(baseConstraintSet)
val startConstraintId =
if (useSplitShade) {
R.id.nssl_guideline
} else {
PARENT_ID
}
val nsslId = R.id.notification_stack_scroller
constraintSet.apply {
connect(nsslId, START, startConstraintId, START)
connect(nsslId, END, PARENT_ID, END)
connect(nsslId, BOTTOM, PARENT_ID, BOTTOM)
connect(nsslId, TOP, PARENT_ID, TOP)
setMargin(nsslId, START, marginStart)
setMargin(nsslId, END, marginEnd)
setMargin(nsslId, TOP, marginTop)
setMargin(nsslId, BOTTOM, marginBottom)
}
constraintSet.applyTo(this)
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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.systemui.statusbar.notification.stack.ui.viewbinder
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
import kotlinx.coroutines.launch
/** Binds the shared notification container to its view-model. */
object SharedNotificationContainerBinder {
@JvmStatic
fun bind(
view: SharedNotificationContainer,
viewModel: SharedNotificationContainerViewModel,
) {
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.configurationBasedDimensions.collect {
view.updateConstraints(
useSplitShade = it.useSplitShade,
marginStart = it.marginStart,
marginTop = it.marginTop,
marginEnd = it.marginEnd,
marginBottom = it.marginBottom,
)
}
}
}
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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.systemui.statusbar.notification.stack.ui.viewmodel
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/** View-model for the shared notification container */
class SharedNotificationContainerViewModel
@Inject
constructor(
interactor: SharedNotificationContainerInteractor,
) {
val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
interactor.configurationBasedDimensions
.map {
ConfigurationBasedDimensions(
marginStart = if (it.useSplitShade) 0 else it.marginHorizontal,
marginEnd = it.marginHorizontal,
marginBottom = it.marginBottom,
marginTop =
if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop,
useSplitShade = it.useSplitShade,
)
}
.distinctUntilChanged()
data class ConfigurationBasedDimensions(
val marginStart: Int,
val marginTop: Int,
val marginEnd: Int,
val marginBottom: Int,
val useSplitShade: Boolean,
)
}

View File

@@ -1,7 +1,24 @@
/*
* 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.systemui.shade
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.TestableResources
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
@@ -12,6 +29,8 @@ 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.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.fragments.FragmentHostManager
import com.android.systemui.fragments.FragmentService
import com.android.systemui.navigationbar.NavigationModeController
@@ -19,7 +38,10 @@ import com.android.systemui.navigationbar.NavigationModeController.ModeChangedLi
import com.android.systemui.plugins.qs.QS
import com.android.systemui.recents.OverviewProxyService
import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.function.Consumer
@@ -40,77 +62,63 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.mockito.Mockito.`when` as whenever
@SmallTest
/** Uses Flags.MIGRATE_NSSL set to false. If all goes well, this set of tests will be deleted. */
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class NotificationQSContainerControllerTest : SysuiTestCase() {
@SmallTest
class NotificationsQSContainerControllerLegacyTest : 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 lateinit var view: NotificationsQuickSettingsContainer
@Mock lateinit var navigationModeController: NavigationModeController
@Mock lateinit var overviewProxyService: OverviewProxyService
@Mock lateinit var shadeHeaderController: ShadeHeaderController
@Mock lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
@Mock lateinit var fragmentService: FragmentService
@Mock lateinit var fragmentHostManager: FragmentHostManager
@Mock
lateinit var notificationStackScrollLayoutController: NotificationStackScrollLayoutController
@Mock
private lateinit var navigationModeController: NavigationModeController
@Mock
private lateinit var overviewProxyService: OverviewProxyService
@Mock
private lateinit var notificationsQSContainer: NotificationsQuickSettingsContainer
@Mock
private lateinit var mShadeHeaderController: ShadeHeaderController
@Mock
private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
@Mock
private lateinit var fragmentService: FragmentService
@Mock
private lateinit var fragmentHostManager: FragmentHostManager
@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>
@Captor
lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener>
@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>
@Captor lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener>
private lateinit var controller: NotificationsQSContainerController
lateinit var underTest: NotificationsQSContainerController
private lateinit var fakeResources: TestableResources
private lateinit var featureFlags: FakeFeatureFlags
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
private lateinit var delayableExecutor: FakeExecutor
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
mContext.ensureTestableResources()
whenever(notificationsQSContainer.context).thenReturn(mContext)
whenever(notificationsQSContainer.resources).thenReturn(mContext.resources)
whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager)
fakeSystemClock = FakeSystemClock()
delayableExecutor = FakeExecutor(fakeSystemClock)
featureFlags = FakeFeatureFlags().apply { set(Flags.MIGRATE_NSSL, false) }
mContext.ensureTestableResources()
whenever(view.context).thenReturn(mContext)
whenever(view.resources).thenReturn(mContext.resources)
controller = NotificationsQSContainerController(
notificationsQSContainer,
whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager)
underTest =
NotificationsQSContainerController(
view,
navigationModeController,
overviewProxyService,
mShadeHeaderController,
shadeHeaderController,
shadeExpansionStateManager,
fragmentService,
delayableExecutor
)
delayableExecutor,
featureFlags,
notificationStackScrollLayoutController,
)
overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN)
overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN)
@@ -118,38 +126,72 @@ class NotificationQSContainerControllerTest : SysuiTestCase() {
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)
.thenReturn(GESTURES_NAVIGATION)
doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture())
doNothing().`when`(notificationsQSContainer)
.setInsetsChangedListener(windowInsetsCallbackCaptor.capture())
doNothing().`when`(notificationsQSContainer).applyConstraints(constraintSetCaptor.capture())
doNothing().`when`(notificationsQSContainer)
.addOnAttachStateChangeListener(attachStateListenerCaptor.capture())
controller.init()
attachStateListenerCaptor.value.onViewAttachedToWindow(notificationsQSContainer)
doNothing().`when`(view).setInsetsChangedListener(windowInsetsCallbackCaptor.capture())
doNothing().`when`(view).applyConstraints(constraintSetCaptor.capture())
doNothing().`when`(view).addOnAttachStateChangeListener(attachStateListenerCaptor.capture())
underTest.init()
attachStateListenerCaptor.value.onViewAttachedToWindow(view)
navigationModeCallback = navigationModeCaptor.value
taskbarVisibilityCallback = taskbarVisibilityCaptor.value
windowInsetsCallback = windowInsetsCallbackCaptor.value
Mockito.clearInvocations(view)
}
@Test
fun testSmallScreen_updateResources_splitShadeHeightIsSet() {
overrideResource(R.bool.config_use_large_screen_shade_header, false)
overrideResource(R.dimen.qs_header_height, 1)
overrideResource(R.dimen.large_screen_shade_header_height, 2)
underTest.updateResources()
val captor = ArgumentCaptor.forClass(ConstraintSet::class.java)
verify(view).applyConstraints(capture(captor))
assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(1)
}
@Test
fun testLargeScreen_updateResources_splitShadeHeightIsSet() {
overrideResource(R.bool.config_use_large_screen_shade_header, true)
overrideResource(R.dimen.qs_header_height, 1)
overrideResource(R.dimen.large_screen_shade_header_height, 2)
underTest.updateResources()
val captor = ArgumentCaptor.forClass(ConstraintSet::class.java)
verify(view).applyConstraints(capture(captor))
assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(2)
}
@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 = 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)
given(
taskbarVisible = true,
navigationMode = BUTTONS_NAVIGATION,
insets = windowInsets().withStableBottom()
)
then(
expectedContainerPadding = STABLE_INSET_BOTTOM,
expectedNotificationsMargin = NOTIFICATIONS_MARGIN,
expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
)
}
@Test
@@ -157,161 +199,185 @@ class NotificationQSContainerControllerTest : SysuiTestCase() {
// 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 = 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)
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 = 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)
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 = 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)
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())
given(taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, insets = emptyInsets())
then(expectedContainerPadding = 0)
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withCutout().withStableBottom())
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)
given(
taskbarVisible = false,
navigationMode = BUTTONS_NAVIGATION,
insets = windowInsets().withStableBottom()
)
then(
expectedContainerPadding = 0,
expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN,
expectedQsPadding = STABLE_INSET_BOTTOM
)
}
@Test
fun testDetailShowingInSinglePaneShade() {
disableSplitShade()
controller.setDetailShowing(true)
underTest.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 = GESTURES_NAVIGATION,
insets = windowInsets().withStableBottom()
)
then(expectedContainerPadding = 0, expectedNotificationsMargin = 0)
given(taskbarVisible = false,
navigationMode = BUTTONS_NAVIGATION,
insets = emptyInsets())
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)
underTest.setDetailShowing(true)
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withStableBottom())
given(
taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withStableBottom()
)
then(expectedContainerPadding = 0)
// should not influence spacing
given(taskbarVisible = false,
navigationMode = BUTTONS_NAVIGATION,
insets = emptyInsets())
given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets())
then(expectedContainerPadding = 0)
}
@Test
fun testNotificationsMarginBottomIsUpdated() {
Mockito.clearInvocations(notificationsQSContainer)
Mockito.clearInvocations(view)
enableSplitShade()
verify(notificationsQSContainer).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN)
verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN)
overrideResource(R.dimen.notification_panel_margin_bottom, 100)
disableSplitShade()
verify(notificationsQSContainer).setNotificationsMarginBottom(100)
verify(view).setNotificationsMarginBottom(100)
}
@Test
fun testSplitShadeLayout_isAlignedToGuideline() {
enableSplitShade()
controller.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd)
.isEqualTo(R.id.qs_edge_guideline)
underTest.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)
.isEqualTo(R.id.qs_edge_guideline)
}
@Test
fun testSinglePaneLayout_childrenHaveEqualMargins() {
disableSplitShade()
controller.updateResources()
underTest.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()
assertThat(
qsStartMargin == qsEndMargin &&
notifStartMargin == notifEndMargin &&
qsStartMargin == notifStartMargin
)
.isTrue()
}
@Test
fun testSplitShadeLayout_childrenHaveInsideMarginsOfZero() {
enableSplitShade()
controller.updateResources()
underTest.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0)
assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startMargin)
.isEqualTo(0)
.isEqualTo(0)
}
@Test
fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() {
enableSplitShade()
controller.updateResources()
underTest.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0)
assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin).isEqualTo(0)
}
@@ -322,62 +388,64 @@ class NotificationQSContainerControllerTest : SysuiTestCase() {
val largeScreenHeaderHeight = 100
overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderHeight)
controller.updateResources()
underTest.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin)
.isEqualTo(largeScreenHeaderHeight)
.isEqualTo(largeScreenHeaderHeight)
assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin)
.isEqualTo(largeScreenHeaderHeight)
.isEqualTo(largeScreenHeaderHeight)
}
@Test
fun testSmallScreenLayout_qsAndNotifsTopMarginIsZero() {
setSmallScreen()
controller.updateResources()
underTest.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin).isEqualTo(0)
assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).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)
underTest.updateResources()
val notificationPanelMarginHorizontal =
mContext.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal)
assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin)
.isEqualTo(notificationPanelMarginHorizontal)
.isEqualTo(notificationPanelMarginHorizontal)
assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin)
.isEqualTo(notificationPanelMarginHorizontal)
.isEqualTo(notificationPanelMarginHorizontal)
}
@Test
fun testSinglePaneShadeLayout_isAlignedToParent() {
disableSplitShade()
controller.updateResources()
underTest.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd)
.isEqualTo(ConstraintSet.PARENT_ID)
.isEqualTo(ConstraintSet.PARENT_ID)
assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart)
.isEqualTo(ConstraintSet.PARENT_ID)
.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)
val container = NotificationsQuickSettingsContainer(mContext, null)
container.removeAllViews()
container.addView(newViewWithId(1))
container.addView(newViewWithId(View.NO_ID))
val controller = NotificationsQSContainerController(
val controller =
NotificationsQSContainerController(
container,
navigationModeController,
overviewProxyService,
mShadeHeaderController,
shadeHeaderController,
shadeExpansionStateManager,
fragmentService,
delayableExecutor
)
delayableExecutor,
featureFlags,
notificationStackScrollLayoutController,
)
controller.updateConstraints()
assertThat(container.getChildAt(0).id).isEqualTo(1)
@@ -388,44 +456,46 @@ class NotificationQSContainerControllerTest : SysuiTestCase() {
fun testWindowInsetDebounce() {
disableSplitShade()
given(taskbarVisible = false,
given(
taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = emptyInsets(),
applyImmediately = false)
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)
verify(view, never()).setQSContainerPaddingBottom(0)
verify(view).setQSContainerPaddingBottom(STABLE_INSET_BOTTOM)
}
@Test
fun testStartCustomizingWithDuration() {
controller.setCustomizerShowing(true, 100L)
verify(mShadeHeaderController).startCustomizingAnimation(true, 100L)
underTest.setCustomizerShowing(true, 100L)
verify(shadeHeaderController).startCustomizingAnimation(true, 100L)
}
@Test
fun testEndCustomizingWithDuration() {
controller.setCustomizerShowing(true, 0L) // Only tracks changes
reset(mShadeHeaderController)
underTest.setCustomizerShowing(true, 0L) // Only tracks changes
reset(shadeHeaderController)
controller.setCustomizerShowing(false, 100L)
verify(mShadeHeaderController).startCustomizingAnimation(false, 100L)
underTest.setCustomizerShowing(false, 100L)
verify(shadeHeaderController).startCustomizingAnimation(false, 100L)
}
@Test
fun testTagListenerAdded() {
verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(notificationsQSContainer))
verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(view))
}
@Test
fun testTagListenerRemoved() {
attachStateListenerCaptor.value.onViewDetachedFromWindow(notificationsQSContainer)
verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(notificationsQSContainer))
attachStateListenerCaptor.value.onViewDetachedFromWindow(view)
verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(view))
}
private fun disableSplitShade() {
@@ -438,7 +508,7 @@ class NotificationQSContainerControllerTest : SysuiTestCase() {
private fun setSplitShadeEnabled(enabled: Boolean) {
overrideResource(R.bool.config_use_split_notification_shade, enabled)
controller.updateResources()
underTest.updateResources()
}
private fun setSmallScreen() {
@@ -459,7 +529,7 @@ class NotificationQSContainerControllerTest : SysuiTestCase() {
insets: WindowInsets,
applyImmediately: Boolean = true
) {
Mockito.clearInvocations(notificationsQSContainer)
Mockito.clearInvocations(view)
taskbarVisibilityCallback.onTaskbarStatusUpdated(taskbarVisible, false)
navigationModeCallback.onNavigationModeChanged(navigationMode)
windowInsetsCallback.accept(insets)
@@ -474,12 +544,10 @@ class NotificationQSContainerControllerTest : SysuiTestCase() {
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)
verify(view).setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding))
verify(view).setNotificationsMarginBottom(expectedNotificationsMargin)
verify(view).setQSContainerPaddingBottom(expectedQsPadding)
Mockito.clearInvocations(view)
}
private fun windowInsets() = mock(WindowInsets::class.java, RETURNS_DEEP_STUBS)
@@ -503,10 +571,26 @@ class NotificationQSContainerControllerTest : SysuiTestCase() {
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)
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
}
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
}
}

View File

@@ -19,24 +19,47 @@ package com.android.systemui.shade
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.TestableResources
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.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.fragments.FragmentHostManager
import com.android.systemui.fragments.FragmentService
import com.android.systemui.navigationbar.NavigationModeController
import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener
import com.android.systemui.plugins.qs.QS
import com.android.systemui.recents.OverviewProxyService
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.function.Consumer
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.any
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.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -51,19 +74,37 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() {
@Mock lateinit var shadeHeaderController: ShadeHeaderController
@Mock lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
@Mock lateinit var fragmentService: FragmentService
@Mock lateinit var fragmentHostManager: FragmentHostManager
@Mock
lateinit var notificationStackScrollLayoutController: NotificationStackScrollLayoutController
@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>
@Captor lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener>
lateinit var underTest: NotificationsQSContainerController
private lateinit var fakeResources: TestableResources
private val delayableExecutor: DelayableExecutor = FakeExecutor(FakeSystemClock())
private lateinit var featureFlags: FakeFeatureFlags
private lateinit var navigationModeCallback: ModeChangedListener
private lateinit var taskbarVisibilityCallback: OverviewProxyListener
private lateinit var windowInsetsCallback: Consumer<WindowInsets>
private lateinit var fakeSystemClock: FakeSystemClock
private lateinit var delayableExecutor: FakeExecutor
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
fakeResources = TestableResources(context.resources)
fakeSystemClock = FakeSystemClock()
delayableExecutor = FakeExecutor(fakeSystemClock)
featureFlags = FakeFeatureFlags().apply { set(Flags.MIGRATE_NSSL, true) }
mContext.ensureTestableResources()
whenever(view.context).thenReturn(mContext)
whenever(view.resources).thenReturn(mContext.resources)
whenever(view.resources).thenReturn(fakeResources.resources)
whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager)
underTest =
NotificationsQSContainerController(
@@ -74,16 +115,36 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() {
shadeExpansionStateManager,
fragmentService,
delayableExecutor,
featureFlags,
notificationStackScrollLayoutController,
)
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`(view).setInsetsChangedListener(windowInsetsCallbackCaptor.capture())
doNothing().`when`(view).applyConstraints(constraintSetCaptor.capture())
doNothing().`when`(view).addOnAttachStateChangeListener(attachStateListenerCaptor.capture())
underTest.init()
attachStateListenerCaptor.value.onViewAttachedToWindow(view)
navigationModeCallback = navigationModeCaptor.value
taskbarVisibilityCallback = taskbarVisibilityCaptor.value
windowInsetsCallback = windowInsetsCallbackCaptor.value
Mockito.clearInvocations(view)
}
@Test
fun testSmallScreen_updateResources_splitShadeHeightIsSet() {
with(fakeResources) {
addOverride(R.bool.config_use_large_screen_shade_header, false)
addOverride(R.dimen.qs_header_height, 1)
addOverride(R.dimen.large_screen_shade_header_height, 2)
}
overrideResource(R.bool.config_use_large_screen_shade_header, false)
overrideResource(R.dimen.qs_header_height, 1)
overrideResource(R.dimen.large_screen_shade_header_height, 2)
underTest.updateResources()
@@ -94,11 +155,9 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() {
@Test
fun testLargeScreen_updateResources_splitShadeHeightIsSet() {
with(fakeResources) {
addOverride(R.bool.config_use_large_screen_shade_header, true)
addOverride(R.dimen.qs_header_height, 1)
addOverride(R.dimen.large_screen_shade_header_height, 2)
}
overrideResource(R.bool.config_use_large_screen_shade_header, true)
overrideResource(R.dimen.qs_header_height, 1)
overrideResource(R.dimen.large_screen_shade_header_height, 2)
underTest.updateResources()
@@ -106,4 +165,415 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() {
verify(view).applyConstraints(capture(captor))
assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(2)
}
@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 testDetailShowingInSinglePaneShade() {
disableSplitShade()
underTest.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()
underTest.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(view)
enableSplitShade()
verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN)
overrideResource(R.dimen.notification_panel_margin_bottom, 100)
disableSplitShade()
verify(view).setNotificationsMarginBottom(100)
}
@Test
fun testSplitShadeLayout_isAlignedToGuideline() {
enableSplitShade()
underTest.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd).isEqualTo(R.id.qs_edge_guideline)
}
@Test
fun testSinglePaneLayout_childrenHaveEqualMargins() {
disableSplitShade()
underTest.updateResources()
val qsStartMargin = getConstraintSetLayout(R.id.qs_frame).startMargin
val qsEndMargin = getConstraintSetLayout(R.id.qs_frame).endMargin
assertThat(qsStartMargin == qsEndMargin).isTrue()
}
@Test
fun testSplitShadeLayout_childrenHaveInsideMarginsOfZero() {
enableSplitShade()
underTest.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0)
}
@Test
fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() {
enableSplitShade()
underTest.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)
underTest.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin)
.isEqualTo(largeScreenHeaderHeight)
}
@Test
fun testSmallScreenLayout_qsAndNotifsTopMarginIsZero() {
setSmallScreen()
underTest.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin).isEqualTo(0)
}
@Test
fun testSinglePaneShadeLayout_qsFrameHasHorizontalMarginsSetToCorrectValue() {
disableSplitShade()
underTest.updateResources()
val notificationPanelMarginHorizontal =
mContext.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()
underTest.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd)
.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(mContext, null)
container.removeAllViews()
container.addView(newViewWithId(1))
container.addView(newViewWithId(View.NO_ID))
val controller =
NotificationsQSContainerController(
container,
navigationModeController,
overviewProxyService,
shadeHeaderController,
shadeExpansionStateManager,
fragmentService,
delayableExecutor,
featureFlags,
notificationStackScrollLayoutController,
)
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(view, never()).setQSContainerPaddingBottom(0)
verify(view).setQSContainerPaddingBottom(STABLE_INSET_BOTTOM)
}
@Test
fun testStartCustomizingWithDuration() {
underTest.setCustomizerShowing(true, 100L)
verify(shadeHeaderController).startCustomizingAnimation(true, 100L)
}
@Test
fun testEndCustomizingWithDuration() {
underTest.setCustomizerShowing(true, 0L) // Only tracks changes
reset(shadeHeaderController)
underTest.setCustomizerShowing(false, 100L)
verify(shadeHeaderController).startCustomizingAnimation(false, 100L)
}
@Test
fun testTagListenerAdded() {
verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(view))
}
@Test
fun testTagListenerRemoved() {
attachStateListenerCaptor.value.onViewDetachedFromWindow(view)
verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(view))
}
private fun disableSplitShade() {
setSplitShadeEnabled(false)
}
private fun enableSplitShade() {
setSplitShadeEnabled(true)
}
private fun setSplitShadeEnabled(enabled: Boolean) {
overrideResource(R.bool.config_use_split_notification_shade, enabled)
underTest.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(view)
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(view).setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding))
verify(view).setNotificationsMarginBottom(expectedNotificationsMargin)
verify(view).setQSContainerPaddingBottom(expectedQsPadding)
Mockito.clearInvocations(view)
}
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
}
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
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.systemui.statusbar.notification.stack.domain.interactor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class SharedNotificationContainerInteractorTest : SysuiTestCase() {
private lateinit var configurationRepository: FakeConfigurationRepository
private lateinit var underTest: SharedNotificationContainerInteractor
@Before
fun setUp() {
configurationRepository = FakeConfigurationRepository()
underTest =
SharedNotificationContainerInteractor(
configurationRepository,
mContext,
)
}
@Test
fun validateConfigValues() = runTest {
overrideResource(R.bool.config_use_split_notification_shade, true)
overrideResource(R.bool.config_use_large_screen_shade_header, false)
overrideResource(R.dimen.notification_panel_margin_horizontal, 0)
overrideResource(R.dimen.notification_panel_margin_bottom, 10)
overrideResource(R.dimen.notification_panel_margin_top, 10)
overrideResource(R.dimen.large_screen_shade_header_height, 0)
val dimens = collectLastValue(underTest.configurationBasedDimensions)
configurationRepository.onAnyConfigurationChange()
runCurrent()
val lastDimens = dimens()!!
assertThat(lastDimens.useSplitShade).isTrue()
assertThat(lastDimens.useLargeScreenHeader).isFalse()
assertThat(lastDimens.marginHorizontal).isEqualTo(0)
assertThat(lastDimens.marginBottom).isGreaterThan(0)
assertThat(lastDimens.marginTop).isGreaterThan(0)
assertThat(lastDimens.marginTopLargeScreen).isEqualTo(0)
}
}

View File

@@ -0,0 +1,142 @@
/*
* 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.systemui.statusbar.notification.stack.ui.viewmodel
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class SharedNotificationContainerViewModelTest : SysuiTestCase() {
private lateinit var configurationRepository: FakeConfigurationRepository
private lateinit var sharedNotificationContainerInteractor:
SharedNotificationContainerInteractor
private lateinit var underTest: SharedNotificationContainerViewModel
@Before
fun setUp() {
configurationRepository = FakeConfigurationRepository()
sharedNotificationContainerInteractor =
SharedNotificationContainerInteractor(
configurationRepository,
mContext,
)
underTest = SharedNotificationContainerViewModel(sharedNotificationContainerInteractor)
}
@Test
fun validateMarginStartInSplitShade() = runTest {
overrideResource(R.bool.config_use_split_notification_shade, true)
overrideResource(R.dimen.notification_panel_margin_horizontal, 20)
val dimens = collectLastValue(underTest.configurationBasedDimensions)
configurationRepository.onAnyConfigurationChange()
runCurrent()
val lastDimens = dimens()!!
assertThat(lastDimens.marginStart).isEqualTo(0)
}
@Test
fun validateMarginStart() = runTest {
overrideResource(R.bool.config_use_split_notification_shade, false)
overrideResource(R.dimen.notification_panel_margin_horizontal, 20)
val dimens = collectLastValue(underTest.configurationBasedDimensions)
configurationRepository.onAnyConfigurationChange()
runCurrent()
val lastDimens = dimens()!!
assertThat(lastDimens.marginStart).isEqualTo(20)
}
@Test
fun validateMarginEnd() = runTest {
overrideResource(R.dimen.notification_panel_margin_horizontal, 50)
val dimens = collectLastValue(underTest.configurationBasedDimensions)
configurationRepository.onAnyConfigurationChange()
runCurrent()
val lastDimens = dimens()!!
assertThat(lastDimens.marginEnd).isEqualTo(50)
}
@Test
fun validateMarginBottom() = runTest {
overrideResource(R.dimen.notification_panel_margin_bottom, 50)
val dimens = collectLastValue(underTest.configurationBasedDimensions)
configurationRepository.onAnyConfigurationChange()
runCurrent()
val lastDimens = dimens()!!
assertThat(lastDimens.marginBottom).isEqualTo(50)
}
@Test
fun validateMarginTopWithLargeScreenHeader() = runTest {
overrideResource(R.bool.config_use_large_screen_shade_header, true)
overrideResource(R.dimen.large_screen_shade_header_height, 50)
overrideResource(R.dimen.notification_panel_margin_top, 0)
val dimens = collectLastValue(underTest.configurationBasedDimensions)
configurationRepository.onAnyConfigurationChange()
runCurrent()
val lastDimens = dimens()!!
assertThat(lastDimens.marginTop).isEqualTo(50)
}
@Test
fun validateMarginTop() = runTest {
overrideResource(R.bool.config_use_large_screen_shade_header, false)
overrideResource(R.dimen.large_screen_shade_header_height, 50)
overrideResource(R.dimen.notification_panel_margin_top, 0)
val dimens = collectLastValue(underTest.configurationBasedDimensions)
configurationRepository.onAnyConfigurationChange()
runCurrent()
val lastDimens = dimens()!!
assertThat(lastDimens.marginTop).isEqualTo(0)
}
}