diff options
19 files changed, 553 insertions, 349 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt index 8d5b84fea9ab..7bca86e2e8fb 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt @@ -15,18 +15,26 @@ package com.android.systemui.common.ui import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.DimenRes +import androidx.annotation.LayoutRes import com.android.settingslib.Utils import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.onDensityOrFontScaleChanged import com.android.systemui.statusbar.policy.onThemeChanged import com.android.systemui.util.kotlin.emitOnStart +import com.android.systemui.util.view.bindLatest import javax.inject.Inject +import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge /** Configuration-aware-state-tracking utilities. */ class ConfigurationState @@ -34,6 +42,7 @@ class ConfigurationState constructor( private val configurationController: ConfigurationController, @Application private val context: Context, + private val layoutInflater: LayoutInflater, ) { /** * Returns a [Flow] that emits a dimension pixel size that is kept in sync with the device @@ -57,4 +66,65 @@ constructor( Utils.getColorAttrDefaultColor(context, id, defaultValue) } } + + /** + * Returns a [Flow] that emits a [View] that is re-inflated as necessary to remain in sync with + * the device configuration. + * + * @see LayoutInflater.inflate + */ + @Suppress("UNCHECKED_CAST") + fun <T : View> inflateLayout( + @LayoutRes id: Int, + root: ViewGroup?, + attachToRoot: Boolean, + ): Flow<T> { + // TODO(b/305930747): This may lead to duplicate invocations if both flows emit, find a + // solution to only emit one event. + return merge( + configurationController.onThemeChanged, + configurationController.onDensityOrFontScaleChanged, + ) + .emitOnStart() + .map { layoutInflater.inflate(id, root, attachToRoot) as T } + } +} + +/** + * Perform an inflation right away, then re-inflate whenever the device configuration changes, and + * call [onInflate] on the resulting view each time. Disposes of the [DisposableHandle] returned by + * [onInflate] when done. + * + * This never completes unless cancelled, it just suspends and waits for updates. + * + * For parameters [resource], [root] and [attachToRoot], see [LayoutInflater.inflate]. + * + * An example use-case of this is when a view needs to be re-inflated whenever a configuration + * change occurs, which would require the ViewBinder to then re-bind the new view. For example, the + * code in the parent view's binder would look like: + * ``` + * parentView.repeatWhenAttached { + * configurationState + * .reinflateOnChange( + * R.layout.my_layout, + * parentView, + * attachToRoot = false, + * coroutineScope = lifecycleScope, + * configurationController.onThemeChanged, + * ) { view: ChildView -> + * ChildViewBinder.bind(view, childViewModel) + * } + * } + * ``` + * + * In turn, the bind method (passed through [onInflate]) uses [repeatWhenAttached], which returns a + * [DisposableHandle]. + */ +suspend fun <T : View> ConfigurationState.reinflateAndBindLatest( + @LayoutRes resource: Int, + root: ViewGroup?, + attachToRoot: Boolean, + onInflate: (T) -> DisposableHandle?, +) { + inflateLayout<T>(resource, root, attachToRoot).bindLatest(onInflate) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt index 246933ad69e3..07e19e619509 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt @@ -21,13 +21,10 @@ import android.view.View import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlagsClassic -import com.android.systemui.flags.Flags -import com.android.systemui.flags.RefactorFlag import com.android.systemui.statusbar.NotificationShelfController import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel -import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.NotificationIconAreaController @@ -53,15 +50,10 @@ constructor( private val dozeParameters: DozeParameters, private val featureFlags: FeatureFlagsClassic, private val screenOffAnimationController: ScreenOffAnimationController, - private val shelfIconViewStore: ShelfNotificationIconViewStore, - private val shelfIconsViewModel: NotificationIconContainerShelfViewModel, private val aodIconViewStore: AlwaysOnDisplayNotificationIconViewStore, private val aodIconsViewModel: NotificationIconContainerAlwaysOnDisplayViewModel, ) : NotificationIconAreaController { - private val shelfRefactor = RefactorFlag(featureFlags, Flags.NOTIFICATION_SHELF_REFACTOR) - - private var shelfIcons: NotificationIconContainer? = null private var aodIcons: NotificationIconContainer? = null private var aodBindJob: DisposableHandle? = null @@ -91,21 +83,7 @@ constructor( override fun setupShelf(notificationShelfController: NotificationShelfController) = NotificationShelfViewBinderWrapperControllerImpl.unsupported - override fun setShelfIcons(icons: NotificationIconContainer) { - if (shelfRefactor.isUnexpectedlyInLegacyMode()) { - NotificationIconContainerViewBinder.bind( - icons, - shelfIconsViewModel, - configuration, - configurationController, - dozeParameters, - featureFlags, - screenOffAnimationController, - shelfIconViewStore, - ) - shelfIcons = icons - } - } + override fun setShelfIcons(icons: NotificationIconContainer) = unsupported override fun onDensityOrFontScaleChanged(context: Context) = unsupported diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt index b92c51fac5f6..2a7d0876912f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt @@ -19,19 +19,26 @@ package com.android.systemui.statusbar.notification.shelf.ui.viewbinder import android.view.View import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.FeatureFlagsClassic +import com.android.systemui.flags.Flags import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.LegacyNotificationShelfControllerImpl import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.NotificationShelfController +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore import com.android.systemui.statusbar.notification.row.ui.viewbinder.ActivatableNotificationViewBinder import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel import com.android.systemui.statusbar.notification.stack.AmbientState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.NotificationIconAreaController import com.android.systemui.statusbar.phone.NotificationIconContainer +import com.android.systemui.statusbar.phone.ScreenOffAnimationController +import com.android.systemui.statusbar.policy.ConfigurationController import javax.inject.Inject import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch @@ -75,14 +82,31 @@ object NotificationShelfViewBinder { fun bind( shelf: NotificationShelf, viewModel: NotificationShelfViewModel, + configuration: ConfigurationState, + configurationController: ConfigurationController, + dozeParameters: DozeParameters, falsingManager: FalsingManager, - featureFlags: FeatureFlags, + featureFlags: FeatureFlagsClassic, notificationIconAreaController: NotificationIconAreaController, + screenOffAnimationController: ScreenOffAnimationController, + shelfIconViewStore: ShelfNotificationIconViewStore, ) { ActivatableNotificationViewBinder.bind(viewModel, shelf, falsingManager) shelf.apply { - // TODO(278765923): Replace with eventual NotificationIconContainerViewBinder#bind() - notificationIconAreaController.setShelfIcons(shelfIcons) + if (featureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) { + NotificationIconContainerViewBinder.bind( + shelfIcons, + viewModel.icons, + configuration, + configurationController, + dozeParameters, + featureFlags, + screenOffAnimationController, + shelfIconViewStore, + ) + } else { + notificationIconAreaController.setShelfIcons(shelfIcons) + } repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { launch { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt index 5ca8b53d0704..64b5b62c4331 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.shelf.ui.viewmodel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.NotificationShelf +import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor import javax.inject.Inject @@ -31,6 +32,7 @@ class NotificationShelfViewModel constructor( private val interactor: NotificationShelfInteractor, activatableViewModel: ActivatableNotificationViewModel, + val icons: NotificationIconContainerShelfViewModel, ) : ActivatableNotificationViewModel by activatableViewModel { /** Is the shelf allowed to be clickable when it has content? */ val isClickable: Flow<Boolean> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 79448b46fa06..b770b83cae9f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -59,6 +59,7 @@ import com.android.systemui.Gefingerpoken; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.classifier.Classifier; import com.android.systemui.classifier.FalsingCollector; +import com.android.systemui.common.ui.ConfigurationState; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; @@ -107,6 +108,7 @@ import com.android.systemui.statusbar.notification.collection.render.Notificatio import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; import com.android.systemui.statusbar.notification.dagger.SilentHeader; import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor; +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore; import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; @@ -117,10 +119,12 @@ import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.row.NotificationSnooze; import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder; import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel; +import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.phone.HeadsUpTouchHelper; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.NotificationIconAreaController; +import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; @@ -212,6 +216,10 @@ public class NotificationStackScrollLayoutController { private final SecureSettings mSecureSettings; private final NotificationDismissibilityProvider mDismissibilityProvider; private final ActivityStarter mActivityStarter; + private final ConfigurationState mConfigurationState; + private final DozeParameters mDozeParameters; + private final ScreenOffAnimationController mScreenOffAnimationController; + private final ShelfNotificationIconViewStore mShelfIconViewStore; private View mLongPressedView; @@ -674,7 +682,10 @@ public class NotificationStackScrollLayoutController { SecureSettings secureSettings, NotificationDismissibilityProvider dismissibilityProvider, ActivityStarter activityStarter, - SplitShadeStateController splitShadeStateController) { + SplitShadeStateController splitShadeStateController, + ConfigurationState configurationState, DozeParameters dozeParameters, + ScreenOffAnimationController screenOffAnimationController, + ShelfNotificationIconViewStore shelfIconViewStore) { mView = view; mKeyguardTransitionRepo = keyguardTransitionRepo; mStackStateLogger = stackLogger; @@ -724,6 +735,10 @@ public class NotificationStackScrollLayoutController { mSecureSettings = secureSettings; mDismissibilityProvider = dismissibilityProvider; mActivityStarter = activityStarter; + mConfigurationState = configurationState; + mDozeParameters = dozeParameters; + mScreenOffAnimationController = screenOffAnimationController; + mShelfIconViewStore = shelfIconViewStore; mView.passSplitShadeStateController(splitShadeStateController); updateResources(); setUpView(); @@ -832,8 +847,10 @@ public class NotificationStackScrollLayoutController { mViewModel.ifPresent( vm -> NotificationListViewBinder - .bind(mView, vm, mFalsingManager, mFeatureFlags, mNotifIconAreaController, - mConfigurationController)); + .bind(mView, vm, mConfigurationState, mConfigurationController, + mDozeParameters, mFalsingManager, mFeatureFlags, + mNotifIconAreaController, mScreenOffAnimationController, + mShelfIconViewStore)); collectFlow(mView, mKeyguardTransitionRepo.getTransitions(), this::onKeyguardTransitionChanged); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index a3792cf6a0f0..69b96fa95dbe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.notification.stack.ui.viewbinder import android.view.LayoutInflater +import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.common.ui.reinflateAndBindLatest import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager @@ -24,16 +26,15 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.notification.footer.ui.view.FooterView import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel +import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.NotificationIconAreaController +import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.statusbar.policy.onDensityOrFontScaleChanged -import com.android.systemui.statusbar.policy.onThemeChanged import com.android.systemui.util.traceSection -import com.android.systemui.util.view.reinflateAndBindLatest -import kotlinx.coroutines.flow.merge /** Binds a [NotificationStackScrollLayout] to its [view model][NotificationListViewModel]. */ object NotificationListViewBinder { @@ -41,10 +42,14 @@ object NotificationListViewBinder { fun bind( view: NotificationStackScrollLayout, viewModel: NotificationListViewModel, + configuration: ConfigurationState, + configurationController: ConfigurationController, + dozeParameters: DozeParameters, falsingManager: FalsingManager, featureFlags: FeatureFlagsClassic, iconAreaController: NotificationIconAreaController, - configurationController: ConfigurationController, + screenOffAnimationController: ScreenOffAnimationController, + shelfIconViewStore: ShelfNotificationIconViewStore, ) { val shelf = LayoutInflater.from(view.context) @@ -52,28 +57,27 @@ object NotificationListViewBinder { NotificationShelfViewBinder.bind( shelf, viewModel.shelf, + configuration, + configurationController, + dozeParameters, falsingManager, featureFlags, - iconAreaController + iconAreaController, + screenOffAnimationController, + shelfIconViewStore, ) view.setShelf(shelf) viewModel.footer.ifPresent { footerViewModel -> // The footer needs to be re-inflated every time the theme or the font size changes. view.repeatWhenAttached { - LayoutInflater.from(view.context).reinflateAndBindLatest( + configuration.reinflateAndBindLatest( R.layout.status_bar_notification_footer, view, attachToRoot = false, - // TODO(b/305930747): This may lead to duplicate invocations if both flows emit, - // find a solution to only emit one event. - merge( - configurationController.onThemeChanged, - configurationController.onDensityOrFontScaleChanged, - ), - ) { view -> + ) { footerView: FooterView -> traceSection("bind FooterView") { - FooterViewBinder.bind(view as FooterView, footerViewModel) + FooterViewBinder.bind(footerView, footerViewModel) } } } diff --git a/packages/SystemUI/src/com/android/systemui/util/view/DisposableHandleExt.kt b/packages/SystemUI/src/com/android/systemui/util/view/DisposableHandleExt.kt new file mode 100644 index 000000000000..d3653b4b7266 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/view/DisposableHandleExt.kt @@ -0,0 +1,35 @@ +/* + * 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.util.view + +import android.view.View +import com.android.systemui.util.kotlin.awaitCancellationThenDispose +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest + +/** + * Use the [bind] method to bind the view every time this flow emits, and suspend to await for more + * updates. New emissions lead to the previous binding call being cancelled if not completed. + * Dispose of the [DisposableHandle] returned by [bind] when done. + */ +suspend fun <T : View> Flow<T>.bindLatest(bind: (T) -> DisposableHandle?) { + this.collectLatest { view -> + val disposableHandle = bind(view) + disposableHandle?.awaitCancellationThenDispose() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/view/LayoutInflaterExt.kt b/packages/SystemUI/src/com/android/systemui/util/view/LayoutInflaterExt.kt deleted file mode 100644 index 6d45d23879e9..000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/view/LayoutInflaterExt.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.util.view - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.util.kotlin.awaitCancellationThenDispose -import com.android.systemui.util.kotlin.stateFlow -import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collectLatest - -/** - * Perform an inflation right away, then re-inflate whenever the [flow] emits, and call [onInflate] - * on the resulting view each time. Dispose of the [DisposableHandle] returned by [onInflate] when - * done. - * - * This never completes unless cancelled, it just suspends and waits for updates. - * - * For parameters [resource], [root] and [attachToRoot], see [LayoutInflater.inflate]. - * - * An example use-case of this is when a view needs to be re-inflated whenever a configuration - * change occurs, which would require the ViewBinder to then re-bind the new view. For example, the - * code in the parent view's binder would look like: - * ``` - * parentView.repeatWhenAttached { - * LayoutInflater.from(parentView.context) - * .reinflateOnChange( - * R.layout.my_layout, - * parentView, - * attachToRoot = false, - * coroutineScope = lifecycleScope, - * configurationController.onThemeChanged, - * ), - * ) { view -> - * ChildViewBinder.bind(view as ChildView, childViewModel) - * } - * } - * ``` - * - * In turn, the bind method (passed through [onInflate]) uses [repeatWhenAttached], which returns a - * [DisposableHandle]. - */ -suspend fun LayoutInflater.reinflateAndBindLatest( - resource: Int, - root: ViewGroup?, - attachToRoot: Boolean, - flow: Flow<Unit>, - onInflate: (View) -> DisposableHandle?, -) = coroutineScope { - val viewFlow: Flow<View> = stateFlow(flow) { inflate(resource, root, attachToRoot) } - viewFlow.bindLatest(onInflate) -} - -/** - * Use the [bind] method to bind the view every time this flow emits, and suspend to await for more - * updates. New emissions lead to the previous binding call being cancelled if not completed. - * Dispose of the [DisposableHandle] returned by [bind] when done. - */ -suspend fun Flow<View>.bindLatest(bind: (View) -> DisposableHandle?) { - this.collectLatest { view -> - val disposableHandle = bind(view) - disposableHandle?.awaitCancellationThenDispose() - } -} diff --git a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt b/packages/SystemUI/tests/src/com/android/TestMocksModule.kt index f49ba646e0a1..0cb913b10764 100644 --- a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt +++ b/packages/SystemUI/tests/src/com/android/TestMocksModule.kt @@ -19,6 +19,7 @@ import android.app.ActivityManager import android.app.admin.DevicePolicyManager import android.os.UserManager import android.util.DisplayMetrics +import android.view.LayoutInflater import com.android.internal.logging.MetricsLogger import com.android.keyguard.KeyguardSecurityModel import com.android.keyguard.KeyguardUpdateMonitor @@ -37,6 +38,7 @@ import com.android.systemui.model.SysUiState import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.NotificationListener import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.NotificationMediaManager @@ -75,6 +77,9 @@ data class TestMocksModule( @get:Provides val keyguardBypassController: KeyguardBypassController = mock(), @get:Provides val keyguardSecurityModel: KeyguardSecurityModel = mock(), @get:Provides val keyguardUpdateMonitor: KeyguardUpdateMonitor = mock(), + @get:Provides val layoutInflater: LayoutInflater = mock(), + @get:Provides + val lockscreenShadeTransitionController: LockscreenShadeTransitionController = mock(), @get:Provides val mediaHierarchyManager: MediaHierarchyManager = mock(), @get:Provides val notifCollection: NotifCollection = mock(), @get:Provides val notificationListener: NotificationListener = mock(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/ConfigurationStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/ui/ConfigurationStateTest.kt new file mode 100644 index 000000000000..034b8022305a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/common/ui/ConfigurationStateTest.kt @@ -0,0 +1,158 @@ +/* + * 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.common.ui + +import android.content.Context +import android.testing.AndroidTestingRunner +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.mockito.captureMany +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.atLeastOnce +import org.mockito.Mockito.verify + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidTestingRunner::class) +class ConfigurationStateTest : SysuiTestCase() { + + private val configurationController: ConfigurationController = mock() + private val layoutInflater = TestLayoutInflater() + + val underTest = ConfigurationState(configurationController, context, layoutInflater) + + @Test + fun reinflateAndBindLatest_inflatesWithoutEmission() = runTest { + var callbackCount = 0 + backgroundScope.launch { + underTest.reinflateAndBindLatest<View>( + resource = 0, + root = null, + attachToRoot = false, + ) { + callbackCount++ + null + } + } + + // Inflates without an emission + runCurrent() + assertThat(layoutInflater.inflationCount).isEqualTo(1) + assertThat(callbackCount).isEqualTo(1) + } + + @Test + fun reinflateAndBindLatest_reinflatesOnThemeChanged() = runTest { + var callbackCount = 0 + backgroundScope.launch { + underTest.reinflateAndBindLatest<View>( + resource = 0, + root = null, + attachToRoot = false, + ) { + callbackCount++ + null + } + } + runCurrent() + + val configListeners: List<ConfigurationController.ConfigurationListener> = captureMany { + verify(configurationController, atLeastOnce()).addCallback(capture()) + } + + listOf(1, 2, 3).forEach { count -> + assertThat(layoutInflater.inflationCount).isEqualTo(count) + assertThat(callbackCount).isEqualTo(count) + configListeners.forEach { it.onThemeChanged() } + runCurrent() + } + } + + @Test + fun reinflateAndBindLatest_reinflatesOnDensityOrFontScaleChanged() = runTest { + var callbackCount = 0 + backgroundScope.launch { + underTest.reinflateAndBindLatest<View>( + resource = 0, + root = null, + attachToRoot = false, + ) { + callbackCount++ + null + } + } + runCurrent() + + val configListeners: List<ConfigurationController.ConfigurationListener> = captureMany { + verify(configurationController, atLeastOnce()).addCallback(capture()) + } + + listOf(1, 2, 3).forEach { count -> + assertThat(layoutInflater.inflationCount).isEqualTo(count) + assertThat(callbackCount).isEqualTo(count) + configListeners.forEach { it.onDensityOrFontScaleChanged() } + runCurrent() + } + } + + @Test + fun testReinflateAndBindLatest_disposesOnCancel() = runTest { + var callbackCount = 0 + var disposed = false + val job = launch { + underTest.reinflateAndBindLatest<View>( + resource = 0, + root = null, + attachToRoot = false, + ) { + callbackCount++ + DisposableHandle { disposed = true } + } + } + + runCurrent() + job.cancelAndJoin() + assertThat(disposed).isTrue() + } + + inner class TestLayoutInflater : LayoutInflater(context) { + + var inflationCount = 0 + + override fun inflate(resource: Int, root: ViewGroup?, attachToRoot: Boolean): View { + inflationCount++ + return View(context) + } + + override fun cloneInContext(p0: Context?): LayoutInflater { + // not needed for this test + return this + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt index e8923a5baca8..970a0f7a3605 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt @@ -9,13 +9,18 @@ import com.android.SysUITestModule import com.android.TestMocksModule import com.android.systemui.ExpandHelper import com.android.systemui.SysuiTestCase +import com.android.systemui.classifier.FalsingCollectorFake +import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FakeFeatureFlagsClassicModule import com.android.systemui.flags.Flags import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.plugins.qs.QS +import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.res.R import com.android.systemui.shade.ShadeViewController +import com.android.systemui.shade.data.repository.FakeShadeRepository +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow @@ -26,7 +31,9 @@ import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.phone.ScrimController import com.android.systemui.statusbar.policy.FakeConfigurationController +import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController import com.android.systemui.user.domain.UserDomainLayerModule +import com.android.systemui.util.mockito.mock import dagger.BindsInstance import dagger.Component import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -51,6 +58,7 @@ import org.mockito.Mockito import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit @@ -64,10 +72,8 @@ private fun <T> anyObject(): T { @OptIn(ExperimentalCoroutinesApi::class) class LockscreenShadeTransitionControllerTest : SysuiTestCase() { + private lateinit var transitionController: LockscreenShadeTransitionController private lateinit var testComponent: TestComponent - - private val transitionController - get() = testComponent.transitionController private val configurationController get() = testComponent.configurationController private val disableFlagsRepository @@ -85,8 +91,11 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { @Mock lateinit var mediaHierarchyManager: MediaHierarchyManager @Mock lateinit var nsslController: NotificationStackScrollLayoutController @Mock lateinit var qS: QS + @Mock lateinit var qsTransitionController: LockscreenShadeQsTransitionController @Mock lateinit var scrimController: ScrimController @Mock lateinit var shadeViewController: ShadeViewController + @Mock lateinit var singleShadeOverScroller: SingleShadeLockScreenOverScroller + @Mock lateinit var splitShadeOverScroller: SplitShadeLockScreenOverScroller @Mock lateinit var stackscroller: NotificationStackScrollLayout @Mock lateinit var statusbarStateController: SysuiStatusBarStateController @Mock lateinit var transitionControllerCallback: LockscreenShadeTransitionController.Callback @@ -135,6 +144,49 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { ) ) + transitionController = + LockscreenShadeTransitionController( + statusBarStateController = statusbarStateController, + logger = mock(), + keyguardBypassController = keyguardBypassController, + lockScreenUserManager = lockScreenUserManager, + falsingCollector = FalsingCollectorFake(), + ambientState = mock(), + mediaHierarchyManager = mediaHierarchyManager, + scrimTransitionController = + LockscreenShadeScrimTransitionController( + scrimController = scrimController, + context = context, + configurationController = configurationController, + dumpManager = mock(), + splitShadeStateController = ResourcesSplitShadeStateController() + ), + keyguardTransitionControllerFactory = { notificationPanelController -> + LockscreenShadeKeyguardTransitionController( + mediaHierarchyManager = mediaHierarchyManager, + notificationPanelController = notificationPanelController, + context = context, + configurationController = configurationController, + dumpManager = mock(), + splitShadeStateController = ResourcesSplitShadeStateController() + ) + }, + depthController = depthController, + context = context, + splitShadeOverScrollerFactory = { _, _ -> splitShadeOverScroller }, + singleShadeOverScrollerFactory = { singleShadeOverScroller }, + activityStarter = mock(), + wakefulnessLifecycle = mock(), + configurationController = configurationController, + falsingManager = FalsingManagerFake(), + dumpManager = mock(), + qsTransitionControllerFactory = { qsTransitionController }, + shadeRepository = testComponent.shadeRepository, + shadeInteractor = testComponent.shadeInteractor, + powerInteractor = testComponent.powerInteractor, + splitShadeStateController = ResourcesSplitShadeStateController(), + ) + transitionController.addCallback(transitionControllerCallback) transitionController.shadeViewController = shadeViewController transitionController.centralSurfaces = centralSurfaces @@ -259,7 +311,7 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { verify(scrimController, never()).setTransitionToFullShadeProgress(anyFloat(), anyFloat()) verify(transitionControllerCallback, never()) .setTransitionToFullShadeAmount(anyFloat(), anyBoolean(), anyLong()) - verify(qS, never()).setTransitionToFullShadeProgress(anyBoolean(), anyFloat(), anyFloat()) + verify(qsTransitionController, never()).dragDownAmount = anyFloat() } @Test @@ -270,7 +322,7 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { verify(scrimController).setTransitionToFullShadeProgress(anyFloat(), anyFloat()) verify(transitionControllerCallback) .setTransitionToFullShadeAmount(anyFloat(), anyBoolean(), anyLong()) - verify(qS).setTransitionToFullShadeProgress(eq(true), anyFloat(), anyFloat()) + verify(qsTransitionController).dragDownAmount = 10f verify(depthController).transitionToFullShadeProgress = anyFloat() } @@ -473,8 +525,8 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { transitionController.dragDownAmount = 10f - verify(nsslController).setOverScrollAmount(0) - verify(scrimController, never()).setNotificationsOverScrollAmount(anyInt()) + verify(singleShadeOverScroller).expansionDragDownAmount = 10f + verifyZeroInteractions(splitShadeOverScroller) } @Test @@ -483,8 +535,8 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { transitionController.dragDownAmount = 10f - verify(nsslController).setOverScrollAmount(0) - verify(scrimController).setNotificationsOverScrollAmount(0) + verify(splitShadeOverScroller).expansionDragDownAmount = 10f + verifyZeroInteractions(singleShadeOverScroller) } @Test @@ -545,10 +597,11 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { ) interface TestComponent { - val transitionController: LockscreenShadeTransitionController - val configurationController: FakeConfigurationController val disableFlagsRepository: FakeDisableFlagsRepository + val powerInteractor: PowerInteractor + val shadeInteractor: ShadeInteractor + val shadeRepository: FakeShadeRepository val testScope: TestScope @Component.Factory diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt index 390c1dd47858..02a67d04ce8f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt @@ -21,23 +21,25 @@ package com.android.systemui.statusbar.notification.shelf.ui.viewmodel import android.os.PowerManager import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest +import com.android.SysUITestModule +import com.android.TestMocksModule import com.android.systemui.SysuiTestCase -import com.android.systemui.accessibility.data.repository.FakeAccessibilityRepository -import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.power.data.repository.FakePowerRepository -import com.android.systemui.power.domain.interactor.PowerInteractorFactory import com.android.systemui.statusbar.LockscreenShadeTransitionController -import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel -import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor +import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -55,98 +57,118 @@ class NotificationShelfViewModelTest : SysuiTestCase() { @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule() - // mocks @Mock private lateinit var keyguardTransitionController: LockscreenShadeTransitionController @Mock private lateinit var screenOffAnimationController: ScreenOffAnimationController - @Mock private lateinit var statusBarStateController: StatusBarStateController - - // fakes - private val keyguardRepository = FakeKeyguardRepository() - private val deviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository() - private val a11yRepo = FakeAccessibilityRepository() - private val powerRepository = FakePowerRepository() - private val powerInteractor by lazy { - PowerInteractorFactory.create( - repository = powerRepository, - screenOffAnimationController = screenOffAnimationController, - statusBarStateController = statusBarStateController, - ) - .powerInteractor - } + @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController - // real impls - private val a11yInteractor = AccessibilityInteractor(a11yRepo) - private val activatableViewModel = ActivatableNotificationViewModel(a11yInteractor) - private val interactor by lazy { - NotificationShelfInteractor( - keyguardRepository, - deviceEntryFaceAuthRepository, - powerInteractor, - keyguardTransitionController, - ) - } - private val underTest by lazy { NotificationShelfViewModel(interactor, activatableViewModel) } + private lateinit var testComponent: TestComponent @Before fun setUp() { whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true) + testComponent = + DaggerNotificationShelfViewModelTest_TestComponent.factory() + .create( + test = this, + mocks = + TestMocksModule( + lockscreenShadeTransitionController = keyguardTransitionController, + screenOffAnimationController = screenOffAnimationController, + statusBarStateController = statusBarStateController, + ) + ) } @Test - fun canModifyColorOfNotifications_whenKeyguardNotShowing() = runTest { - val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications) + fun canModifyColorOfNotifications_whenKeyguardNotShowing() = + with(testComponent) { + testScope.runTest { + val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications) - keyguardRepository.setKeyguardShowing(false) + keyguardRepository.setKeyguardShowing(false) - assertThat(canModifyNotifColor).isTrue() - } + assertThat(canModifyNotifColor).isTrue() + } + } @Test - fun canModifyColorOfNotifications_whenKeyguardShowingAndNotBypass() = runTest { - val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications) + fun canModifyColorOfNotifications_whenKeyguardShowingAndNotBypass() = + with(testComponent) { + testScope.runTest { + val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications) - keyguardRepository.setKeyguardShowing(true) - deviceEntryFaceAuthRepository.isBypassEnabled.value = false + keyguardRepository.setKeyguardShowing(true) + deviceEntryFaceAuthRepository.isBypassEnabled.value = false - assertThat(canModifyNotifColor).isTrue() - } + assertThat(canModifyNotifColor).isTrue() + } + } @Test - fun cannotModifyColorOfNotifications_whenBypass() = runTest { - val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications) + fun cannotModifyColorOfNotifications_whenBypass() = + with(testComponent) { + testScope.runTest { + val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications) - keyguardRepository.setKeyguardShowing(true) - deviceEntryFaceAuthRepository.isBypassEnabled.value = true + keyguardRepository.setKeyguardShowing(true) + deviceEntryFaceAuthRepository.isBypassEnabled.value = true - assertThat(canModifyNotifColor).isFalse() - } + assertThat(canModifyNotifColor).isFalse() + } + } @Test - fun isClickable_whenKeyguardShowing() = runTest { - val isClickable by collectLastValue(underTest.isClickable) + fun isClickable_whenKeyguardShowing() = + with(testComponent) { + testScope.runTest { + val isClickable by collectLastValue(underTest.isClickable) - keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setKeyguardShowing(true) - assertThat(isClickable).isTrue() - } + assertThat(isClickable).isTrue() + } + } @Test - fun isNotClickable_whenKeyguardNotShowing() = runTest { - val isClickable by collectLastValue(underTest.isClickable) + fun isNotClickable_whenKeyguardNotShowing() = + with(testComponent) { + testScope.runTest { + val isClickable by collectLastValue(underTest.isClickable) - keyguardRepository.setKeyguardShowing(false) + keyguardRepository.setKeyguardShowing(false) - assertThat(isClickable).isFalse() - } + assertThat(isClickable).isFalse() + } + } @Test - fun onClicked_goesToLockedShade() { - whenever(statusBarStateController.isDozing).thenReturn(true) - - underTest.onShelfClicked() - - assertThat(powerRepository.lastWakeReason).isNotNull() - assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE) - verify(keyguardTransitionController).goToLockedShade(Mockito.isNull(), eq(true)) + fun onClicked_goesToLockedShade() = + with(testComponent) { + whenever(statusBarStateController.isDozing).thenReturn(true) + + underTest.onShelfClicked() + + assertThat(powerRepository.lastWakeReason).isNotNull() + assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE) + verify(keyguardTransitionController).goToLockedShade(Mockito.isNull(), eq(true)) + } + + @Component(modules = [SysUITestModule::class, ActivatableNotificationViewModelModule::class]) + @SysUISingleton + interface TestComponent { + + val underTest: NotificationShelfViewModel + val deviceEntryFaceAuthRepository: FakeDeviceEntryFaceAuthRepository + val keyguardRepository: FakeKeyguardRepository + val powerRepository: FakePowerRepository + val testScope: TestScope + + @Component.Factory + interface Factory { + fun create( + @BindsInstance test: SysuiTestCase, + mocks: TestMocksModule, + ): TestComponent + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index 3dafb23c8a37..2b944c345643 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -53,6 +53,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.FalsingManagerFake; +import com.android.systemui.common.ui.ConfigurationState; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.flags.Flags; @@ -84,14 +85,17 @@ import com.android.systemui.statusbar.notification.collection.render.Notificatio import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository; import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor; +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore; import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent; import com.android.systemui.statusbar.notification.stack.NotificationSwipeHelper.NotificationCallback; import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel; +import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.NotificationIconAreaController; +import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; @@ -716,8 +720,11 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mSecureSettings, mock(NotificationDismissibilityProvider.class), mActivityStarter, - new ResourcesSplitShadeStateController() - ); + new ResourcesSplitShadeStateController(), + mock(ConfigurationState.class), + mock(DozeParameters.class), + mock(ScreenOffAnimationController.class), + mock(ShelfNotificationIconViewStore.class)); } static class LogMatcher implements ArgumentMatcher<LogMaker> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/LayoutInflaterUtilTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/LayoutInflaterUtilTest.kt deleted file mode 100644 index 1c8465a482de..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/LayoutInflaterUtilTest.kt +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.util.kotlin - -import android.content.Context -import android.testing.AndroidTestingRunner -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.util.view.reinflateAndBindLatest -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(AndroidTestingRunner::class) -class LayoutInflaterUtilTest : SysuiTestCase() { - @JvmField @Rule val mockito = MockitoJUnit.rule() - - private var inflationCount = 0 - private var callbackCount = 0 - @Mock private lateinit var disposableHandle: DisposableHandle - - inner class TestLayoutInflater : LayoutInflater(context) { - override fun inflate(resource: Int, root: ViewGroup?, attachToRoot: Boolean): View { - inflationCount++ - return View(context) - } - - override fun cloneInContext(p0: Context?): LayoutInflater { - // not needed for this test - return this - } - } - - val underTest = TestLayoutInflater() - - @After - fun cleanUp() { - inflationCount = 0 - callbackCount = 0 - } - - @Test - fun testReinflateAndBindLatest_inflatesWithoutEmission() = runTest { - backgroundScope.launch { - underTest.reinflateAndBindLatest( - resource = 0, - root = null, - attachToRoot = false, - emptyFlow<Unit>() - ) { - callbackCount++ - null - } - } - - // Inflates without an emission - runCurrent() - assertThat(inflationCount).isEqualTo(1) - assertThat(callbackCount).isEqualTo(1) - } - - @Test - fun testReinflateAndBindLatest_reinflatesOnEmission() = runTest { - val observable = MutableSharedFlow<Unit>() - val flow = observable.asSharedFlow() - backgroundScope.launch { - underTest.reinflateAndBindLatest( - resource = 0, - root = null, - attachToRoot = false, - flow - ) { - callbackCount++ - null - } - } - - listOf(1, 2, 3).forEach { count -> - runCurrent() - assertThat(inflationCount).isEqualTo(count) - assertThat(callbackCount).isEqualTo(count) - observable.emit(Unit) - } - } - - @Test - fun testReinflateAndBindLatest_disposesOnCancel() = runTest { - val job = launch { - underTest.reinflateAndBindLatest( - resource = 0, - root = null, - attachToRoot = false, - emptyFlow() - ) { - callbackCount++ - disposableHandle - } - } - - runCurrent() - job.cancelAndJoin() - verify(disposableHandle).dispose() - } -} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/FakeAccessibilityDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/FakeAccessibilityDataLayerModule.kt new file mode 100644 index 000000000000..baf100600827 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/FakeAccessibilityDataLayerModule.kt @@ -0,0 +1,22 @@ +/* + * 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.accessibility.data + +import com.android.systemui.accessibility.data.repository.FakeAccessibilityRepositoryModule +import dagger.Module + +@Module(includes = [FakeAccessibilityRepositoryModule::class]) +object FakeAccessibilityDataLayerModule diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt index 8444c7b52d1e..4085b1b5b5c5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt @@ -16,8 +16,20 @@ package com.android.systemui.accessibility.data.repository +import com.android.systemui.dagger.SysUISingleton +import dagger.Binds +import dagger.Module +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow +@SysUISingleton class FakeAccessibilityRepository( - override val isTouchExplorationEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false) -) : AccessibilityRepository + override val isTouchExplorationEnabled: MutableStateFlow<Boolean>, +) : AccessibilityRepository { + @Inject constructor() : this(MutableStateFlow(false)) +} + +@Module +interface FakeAccessibilityRepositoryModule { + @Binds fun bindFake(fake: FakeAccessibilityRepository): AccessibilityRepository +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt index cffbf0271c29..36f088214153 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.data +import com.android.systemui.accessibility.data.FakeAccessibilityDataLayerModule import com.android.systemui.authentication.data.FakeAuthenticationDataLayerModule import com.android.systemui.bouncer.data.repository.FakeBouncerDataLayerModule import com.android.systemui.common.ui.data.FakeCommonDataLayerModule @@ -30,6 +31,7 @@ import dagger.Module @Module( includes = [ + FakeAccessibilityDataLayerModule::class, FakeAuthenticationDataLayerModule::class, FakeBouncerDataLayerModule::class, FakeCommonDataLayerModule::class, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt index abf72af0e1d5..67100729bf2e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.data import com.android.systemui.keyguard.data.repository.FakeCommandQueueModule +import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepositoryModule import com.android.systemui.keyguard.data.repository.FakeKeyguardRepositoryModule import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepositoryModule import dagger.Module @@ -24,6 +25,7 @@ import dagger.Module includes = [ FakeCommandQueueModule::class, + FakeDeviceEntryFaceAuthRepositoryModule::class, FakeKeyguardRepositoryModule::class, FakeKeyguardTransitionRepositoryModule::class, ] diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt index 322fb284dad7..e289083a2d9e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt @@ -17,15 +17,20 @@ package com.android.systemui.keyguard.data.repository import com.android.keyguard.FaceAuthUiEvent +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus import com.android.systemui.keyguard.shared.model.FaceDetectionStatus +import dagger.Binds +import dagger.Module +import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterNotNull -class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository { +@SysUISingleton +class FakeDeviceEntryFaceAuthRepository @Inject constructor() : DeviceEntryFaceAuthRepository { override val isAuthenticated = MutableStateFlow(false) override val canRunFaceAuth = MutableStateFlow(false) @@ -66,3 +71,8 @@ class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository { _runningAuthRequest.value = null } } + +@Module +interface FakeDeviceEntryFaceAuthRepositoryModule { + @Binds fun bindFake(fake: FakeDeviceEntryFaceAuthRepository): DeviceEntryFaceAuthRepository +} |