diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml index 2fde9479d42a3..a33625212d340 100644 --- a/packages/SystemUI/res/layout/super_notification_shade.xml +++ b/packages/SystemUI/res/layout/super_notification_shade.xml @@ -76,6 +76,13 @@ android:layout_height="match_parent" android:visibility="invisible" /> + + + + diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index f59ad90d86ffa..23f6fa6bbf239 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -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(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() diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index fba01201190e1..5c1dd5670d8af 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -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(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) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java index e5b84bd86514f..3b3df50f15206 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java @@ -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); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt index 8ec8d115de78d..3c4ad72225763 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt new file mode 100644 index 0000000000000..874450b36bf03 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt @@ -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 = + 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, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt new file mode 100644 index 0000000000000..688843de06f2f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt @@ -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) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt new file mode 100644 index 0000000000000..fb1d55d6bb7b1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -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, + ) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt new file mode 100644 index 0000000000000..b2e5ac116e468 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -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 = + 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, + ) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt similarity index 50% rename from packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt rename to packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt index 168cbb7b8da3f..2bc112d68ae2a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt @@ -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 - @Captor - lateinit var taskbarVisibilityCaptor: ArgumentCaptor - @Captor - lateinit var windowInsetsCallbackCaptor: ArgumentCaptor> - @Captor - lateinit var constraintSetCaptor: ArgumentCaptor - @Captor - lateinit var attachStateListenerCaptor: ArgumentCaptor + @Captor lateinit var navigationModeCaptor: ArgumentCaptor + @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor + @Captor lateinit var windowInsetsCallbackCaptor: ArgumentCaptor> + @Captor lateinit var constraintSetCaptor: ArgumentCaptor + @Captor lateinit var attachStateListenerCaptor: ArgumentCaptor - 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 - 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 + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt index d4751c86a87f7..a5048187b1b42 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt @@ -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 + @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor + @Captor lateinit var windowInsetsCallbackCaptor: ArgumentCaptor> + @Captor lateinit var constraintSetCaptor: ArgumentCaptor + @Captor lateinit var attachStateListenerCaptor: ArgumentCaptor 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 + 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 + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorTest.kt new file mode 100644 index 0000000000000..7bbb09483b5ff --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorTest.kt @@ -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) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt new file mode 100644 index 0000000000000..afd9954601513 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -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) + } +}