Move NIC binding to NotificationShelfViewBinder
Flag: NOTIFICATION_ICON_CONTAINER_REFACTOR
Bug: 290787599
Bug: 278765923
Test: atest SystemUITests
Change-Id: Idb8e7f10a4179961108385ae27444d16d465ddc0
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 8d5b84f..7bca86e 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 @@
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 @@
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 246933a..07e19e6 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 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 @@
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 @@
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 b92c51f..2a7d087 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 @@
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 @@
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 5ca8b53..64b5b62c 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 @@
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 @@
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 79448b4..b770b83 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.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.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.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 @@
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 @@
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 @@
mSecureSettings = secureSettings;
mDismissibilityProvider = dismissibilityProvider;
mActivityStarter = activityStarter;
+ mConfigurationState = configurationState;
+ mDozeParameters = dozeParameters;
+ mScreenOffAnimationController = screenOffAnimationController;
+ mShelfIconViewStore = shelfIconViewStore;
mView.passSplitShadeStateController(splitShadeStateController);
updateResources();
setUpView();
@@ -832,8 +847,10 @@
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 a3792cf..69b96fa 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.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 @@
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 @@
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 0000000..d3653b4
--- /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 6d45d23..0000000
--- 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 f49ba64..0cb913b 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.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.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 @@
@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 0000000..034b802
--- /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 e8923a5..970a0f7 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.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.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.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 @@
@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 @@
@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 @@
)
)
+ 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 @@
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 @@
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 @@
transitionController.dragDownAmount = 10f
- verify(nsslController).setOverScrollAmount(0)
- verify(scrimController, never()).setNotificationsOverScrollAmount(anyInt())
+ verify(singleShadeOverScroller).expansionDragDownAmount = 10f
+ verifyZeroInteractions(splitShadeOverScroller)
}
@Test
@@ -483,8 +535,8 @@
transitionController.dragDownAmount = 10f
- verify(nsslController).setOverScrollAmount(0)
- verify(scrimController).setNotificationsOverScrollAmount(0)
+ verify(splitShadeOverScroller).expansionDragDownAmount = 10f
+ verifyZeroInteractions(singleShadeOverScroller)
}
@Test
@@ -545,10 +597,11 @@
)
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 390c1dd..02a67d0 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 @@
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 @@
@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
+ @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
- // 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
- }
-
- // 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)
+ fun onClicked_goesToLockedShade() =
+ with(testComponent) {
+ whenever(statusBarStateController.isDozing).thenReturn(true)
- underTest.onShelfClicked()
+ underTest.onShelfClicked()
- assertThat(powerRepository.lastWakeReason).isNotNull()
- assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE)
- verify(keyguardTransitionController).goToLockedShade(Mockito.isNull(), eq(true))
+ 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 3dafb23..2b944c3 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.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.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 @@
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 1c8465a..0000000
--- 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 0000000..baf1006
--- /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 8444c7b..4085b1b 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 cffbf02..36f0882 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 @@
@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 abf72af..6710072 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 @@
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 322fb28..e289083 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 @@
_runningAuthRequest.value = null
}
}
+
+@Module
+interface FakeDeviceEntryFaceAuthRepositoryModule {
+ @Binds fun bindFake(fake: FakeDeviceEntryFaceAuthRepository): DeviceEntryFaceAuthRepository
+}