Merge "Introduce NICViewModel#iconsViewData" into main
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt b/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt
index 565bf24..baa07c1 100644
--- a/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt
@@ -95,7 +95,7 @@
tableLogBuffer.logChange(columnPrefix, columnName, initialValue, isInitial = true)
initialValue
}
- return this.pairwiseBy(initialValueFun) { prevVal, newVal: Boolean ->
+ return this.pairwiseBy(initialValueFun) { prevVal: Boolean, newVal: Boolean ->
if (prevVal != newVal) {
tableLogBuffer.logChange(columnPrefix, columnName, newVal)
}
@@ -114,7 +114,7 @@
tableLogBuffer.logChange(columnPrefix, columnName, initialValue, isInitial = true)
initialValue
}
- return this.pairwiseBy(initialValueFun) { prevVal, newVal: Int ->
+ return this.pairwiseBy(initialValueFun) { prevVal: Int, newVal: Int ->
if (prevVal != newVal) {
tableLogBuffer.logChange(columnPrefix, columnName, newVal)
}
@@ -133,7 +133,7 @@
tableLogBuffer.logChange(columnPrefix, columnName, initialValue, isInitial = true)
initialValue
}
- return this.pairwiseBy(initialValueFun) { prevVal, newVal: Int? ->
+ return this.pairwiseBy(initialValueFun) { prevVal: Int?, newVal: Int? ->
if (prevVal != newVal) {
tableLogBuffer.logChange(columnPrefix, columnName, newVal)
}
@@ -152,7 +152,7 @@
tableLogBuffer.logChange(columnPrefix, columnName, initialValue, isInitial = true)
initialValue
}
- return this.pairwiseBy(initialValueFun) { prevVal, newVal: String? ->
+ return this.pairwiseBy(initialValueFun) { prevVal: String?, newVal: String? ->
if (prevVal != newVal) {
tableLogBuffer.logChange(columnPrefix, columnName, newVal)
}
@@ -176,7 +176,7 @@
)
initialValue
}
- return this.pairwiseBy(initialValueFun) { prevVal, newVal: List<T> ->
+ return this.pairwiseBy(initialValueFun) { prevVal: List<T>, newVal: List<T> ->
if (prevVal != newVal) {
// TODO(b/267761156): Can we log list changes without using toString?
tableLogBuffer.logChange(columnPrefix, columnName, newVal.toString())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
index ff4570e..2e1e395 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
@@ -29,8 +29,11 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlagsClassic;
+import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.statusbar.dagger.CentralSurfacesModule;
+import com.android.systemui.statusbar.domain.interactor.SilentNotificationStatusIconsVisibilityInteractor;
import com.android.systemui.statusbar.notification.collection.NotifCollection;
import com.android.systemui.statusbar.notification.collection.PipelineDumpable;
import com.android.systemui.statusbar.notification.collection.PipelineDumper;
@@ -59,7 +62,9 @@
private static final long MAX_RANKING_DELAY_MILLIS = 500L;
private final Context mContext;
+ private final FeatureFlagsClassic mFeatureFlags;
private final NotificationManager mNotificationManager;
+ private final SilentNotificationStatusIconsVisibilityInteractor mStatusIconInteractor;
private final SystemClock mSystemClock;
private final Executor mMainExecutor;
private final List<NotificationHandler> mNotificationHandlers = new ArrayList<>();
@@ -75,13 +80,17 @@
@Inject
public NotificationListener(
Context context,
+ FeatureFlagsClassic featureFlags,
NotificationManager notificationManager,
+ SilentNotificationStatusIconsVisibilityInteractor statusIconInteractor,
SystemClock systemClock,
@Main Executor mainExecutor,
PluginManager pluginManager) {
super(pluginManager);
mContext = context;
+ mFeatureFlags = featureFlags;
mNotificationManager = notificationManager;
+ mStatusIconInteractor = statusIconInteractor;
mSystemClock = systemClock;
mMainExecutor = mainExecutor;
}
@@ -95,6 +104,7 @@
}
/** Registers a listener that's notified when any notification-related settings change. */
+ @Deprecated
public void addNotificationSettingsListener(NotificationSettingsListener listener) {
mSettingsListeners.add(listener);
}
@@ -230,8 +240,12 @@
@Override
public void onSilentStatusBarIconsVisibilityChanged(boolean hideSilentStatusIcons) {
- for (NotificationSettingsListener listener : mSettingsListeners) {
- listener.onStatusBarIconsBehaviorChanged(hideSilentStatusIcons);
+ if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+ mStatusIconInteractor.setHideSilentStatusIcons(hideSilentStatusIcons);
+ } else {
+ for (NotificationSettingsListener listener : mSettingsListeners) {
+ listener.onStatusBarIconsBehaviorChanged(hideSilentStatusIcons);
+ }
}
}
@@ -294,6 +308,7 @@
return ranking;
}
+ @Deprecated
public interface NotificationSettingsListener {
default void onStatusBarIconsBehaviorChanged(boolean hideSilentStatusIcons) { }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/NotificationListenerSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/NotificationListenerSettingsRepository.kt
new file mode 100644
index 0000000..2c706a5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/NotificationListenerSettingsRepository.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ *
+ */
+
+package com.android.systemui.statusbar.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.NotificationListener
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/** Exposes state pertaining to settings tracked over the [NotificationListener] boundary. */
+@SysUISingleton
+class NotificationListenerSettingsRepository @Inject constructor() {
+ /** Should icons for silent notifications be shown in the status bar? */
+ val showSilentStatusIcons = MutableStateFlow(true)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/SilentNotificationStatusIconsVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/SilentNotificationStatusIconsVisibilityInteractor.kt
new file mode 100644
index 0000000..1248b1c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/SilentNotificationStatusIconsVisibilityInteractor.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ *
+ */
+
+package com.android.systemui.statusbar.domain.interactor
+
+import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository
+import javax.inject.Inject
+
+class SilentNotificationStatusIconsVisibilityInteractor
+@Inject
+constructor(private val repository: NotificationListenerSettingsRepository) {
+ /** Set whether icons for silent notifications be hidden in the status bar. */
+ fun setHideSilentStatusIcons(hideIcons: Boolean) {
+ repository.showSilentStatusIcons.value = !hideIcons
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index e763797..7b3a93a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -225,7 +225,7 @@
/** @see NotifPipeline#getEntry(String) () */
@Nullable
- NotificationEntry getEntry(@NonNull String key) {
+ public NotificationEntry getEntry(@NonNull String key) {
return mNotificationSet.get(key);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
index c2a021d..07e84bb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
@@ -52,9 +52,10 @@
fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) =
traceSection("StackCoordinator.onAfterRenderList") {
controller.setNotifStats(calculateNotifStats(entries))
- notificationIconAreaController.updateNotificationIcons(entries)
if (featureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
renderListInteractor.setRenderedList(entries)
+ } else {
+ notificationIconAreaController.updateNotificationIcons(entries)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
index c5396dd..604ecbc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
@@ -15,12 +15,16 @@
*/
package com.android.systemui.statusbar.notification.domain.interactor
+import android.graphics.drawable.Icon
import com.android.systemui.statusbar.notification.collection.ListEntry
+import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import javax.inject.Inject
import kotlinx.coroutines.flow.update
+private typealias ModelStore = Map<String, ActiveNotificationModel>
+
/**
* Logic for passing information from the
* [com.android.systemui.statusbar.notification.collection.NotifPipeline] to the presentation
@@ -30,6 +34,7 @@
@Inject
constructor(
private val repository: ActiveNotificationListRepository,
+ private val sectionStyleProvider: SectionStyleProvider,
) {
/**
* Sets the current list of rendered notification entries as displayed in the notification
@@ -38,21 +43,100 @@
* @see com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository.activeNotifications
*/
fun setRenderedList(entries: List<ListEntry>) {
- repository.activeNotifications.update { modelsByKey ->
+ repository.activeNotifications.update { existingModels ->
entries.associateBy(
keySelector = { it.key },
- valueTransform = { it.toModel(modelsByKey[it.key]) }
+ valueTransform = { it.toModel(existingModels) },
)
}
}
- private fun ListEntry.toModel(existing: ActiveNotificationModel?): ActiveNotificationModel {
- val isCurrent =
- when {
- existing == null -> false
- key == existing.key -> true
- else -> false
- }
- return if (isCurrent) existing!! else ActiveNotificationModel(key = key)
+ private fun ListEntry.toModel(
+ existingModels: ModelStore,
+ ): ActiveNotificationModel =
+ existingModels.createOrReuse(
+ key = key,
+ groupKey = representativeEntry?.sbn?.groupKey,
+ isAmbient = sectionStyleProvider.isMinimized(this),
+ isRowDismissed = representativeEntry?.isRowDismissed == true,
+ isSilent = sectionStyleProvider.isSilent(this),
+ isLastMessageFromReply = representativeEntry?.isLastMessageFromReply == true,
+ isSuppressedFromStatusBar = representativeEntry?.shouldSuppressStatusBar() == true,
+ isPulsing = representativeEntry?.showingPulsing() == true,
+ aodIcon = representativeEntry?.icons?.aodIcon?.sourceIcon,
+ shelfIcon = representativeEntry?.icons?.shelfIcon?.sourceIcon,
+ statusBarIcon = representativeEntry?.icons?.statusBarIcon?.sourceIcon,
+ )
+
+ private fun ModelStore.createOrReuse(
+ key: String,
+ groupKey: String?,
+ isAmbient: Boolean,
+ isRowDismissed: Boolean,
+ isSilent: Boolean,
+ isLastMessageFromReply: Boolean,
+ isSuppressedFromStatusBar: Boolean,
+ isPulsing: Boolean,
+ aodIcon: Icon?,
+ shelfIcon: Icon?,
+ statusBarIcon: Icon?
+ ): ActiveNotificationModel {
+ return this[key]?.takeIf {
+ it.isCurrent(
+ key = key,
+ groupKey = groupKey,
+ isAmbient = isAmbient,
+ isRowDismissed = isRowDismissed,
+ isSilent = isSilent,
+ isLastMessageFromReply = isLastMessageFromReply,
+ isSuppressedFromStatusBar = isSuppressedFromStatusBar,
+ isPulsing = isPulsing,
+ aodIcon = aodIcon,
+ shelfIcon = shelfIcon,
+ statusBarIcon = statusBarIcon
+ )
+ }
+ ?: ActiveNotificationModel(
+ key = key,
+ groupKey = groupKey,
+ isAmbient = isAmbient,
+ isRowDismissed = isRowDismissed,
+ isSilent = isSilent,
+ isLastMessageFromReply = isLastMessageFromReply,
+ isSuppressedFromStatusBar = isSuppressedFromStatusBar,
+ isPulsing = isPulsing,
+ aodIcon = aodIcon,
+ shelfIcon = shelfIcon,
+ statusBarIcon = statusBarIcon,
+ )
+ }
+
+ private fun ActiveNotificationModel.isCurrent(
+ key: String,
+ groupKey: String?,
+ isAmbient: Boolean,
+ isRowDismissed: Boolean,
+ isSilent: Boolean,
+ isLastMessageFromReply: Boolean,
+ isSuppressedFromStatusBar: Boolean,
+ isPulsing: Boolean,
+ aodIcon: Icon?,
+ shelfIcon: Icon?,
+ statusBarIcon: Icon?
+ ): Boolean {
+ return when {
+ key != this.key -> false
+ groupKey != this.groupKey -> false
+ isAmbient != this.isAmbient -> false
+ isRowDismissed != this.isRowDismissed -> false
+ isSilent != this.isSilent -> false
+ isLastMessageFromReply != this.isLastMessageFromReply -> false
+ isSuppressedFromStatusBar != this.isSuppressedFromStatusBar -> false
+ isPulsing != this.isPulsing -> false
+ aodIcon != this.aodIcon -> false
+ shelfIcon != this.shelfIcon -> false
+ statusBarIcon != this.statusBarIcon -> false
+ else -> true
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
index f3e122c..1f7ab96 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
@@ -32,6 +32,7 @@
val hasFilteredOutSeenNotifications: StateFlow<Boolean> =
notificationListRepository.hasFilteredOutSeenNotifications
+ /** Set whether already-seen notifications are currently filtered out of the shade. */
fun setHasFilteredOutSeenNotifications(value: Boolean) {
notificationListRepository.hasFilteredOutSeenNotifications.value = value
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt
new file mode 100644
index 0000000..00d873e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ *
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.icon.domain.interactor
+
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository
+import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardViewStateRepository
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import com.android.wm.shell.bubbles.Bubbles
+import java.util.Optional
+import javax.inject.Inject
+import kotlin.jvm.optionals.getOrNull
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+
+/** Domain logic related to notification icons. */
+class NotificationIconsInteractor
+@Inject
+constructor(
+ private val activeNotificationsInteractor: ActiveNotificationsInteractor,
+ private val bubbles: Optional<Bubbles>,
+ private val keyguardViewStateRepository: NotificationsKeyguardViewStateRepository,
+) {
+ /** Returns a subset of all active notifications based on the supplied filtration parameters. */
+ fun filteredNotifSet(
+ showAmbient: Boolean = true,
+ showLowPriority: Boolean = true,
+ showDismissed: Boolean = true,
+ showRepliedMessages: Boolean = true,
+ showPulsing: Boolean = true,
+ ): Flow<Set<ActiveNotificationModel>> {
+ return combine(
+ activeNotificationsInteractor.notifications,
+ keyguardViewStateRepository.areNotificationsFullyHidden,
+ ) { notifications, notifsFullyHidden ->
+ notifications
+ .asSequence()
+ .filter { model: ActiveNotificationModel ->
+ shouldShowNotificationIcon(
+ model = model,
+ showAmbient = showAmbient,
+ showLowPriority = showLowPriority,
+ showDismissed = showDismissed,
+ showRepliedMessages = showRepliedMessages,
+ showPulsing = showPulsing,
+ notifsFullyHidden = notifsFullyHidden,
+ )
+ }
+ .toSet()
+ }
+ }
+
+ private fun shouldShowNotificationIcon(
+ model: ActiveNotificationModel,
+ showAmbient: Boolean,
+ showLowPriority: Boolean,
+ showDismissed: Boolean,
+ showRepliedMessages: Boolean,
+ showPulsing: Boolean,
+ notifsFullyHidden: Boolean,
+ ): Boolean {
+ return when {
+ !showAmbient && model.isAmbient -> false
+ !showLowPriority && model.isSilent -> false
+ !showDismissed && model.isRowDismissed -> false
+ !showRepliedMessages && model.isLastMessageFromReply -> false
+ !showAmbient && model.isSuppressedFromStatusBar -> false
+ !showPulsing && model.isPulsing && !notifsFullyHidden -> false
+ bubbles.getOrNull()?.isBubbleExpanded(model.key) == true -> false
+ else -> true
+ }
+ }
+}
+
+/** Domain logic related to notification icons shown on the always-on display. */
+class AlwaysOnDisplayNotificationIconsInteractor
+@Inject
+constructor(
+ deviceEntryInteractor: DeviceEntryInteractor,
+ iconsInteractor: NotificationIconsInteractor,
+) {
+ val aodNotifs: Flow<Set<ActiveNotificationModel>> =
+ deviceEntryInteractor.isBypassEnabled.flatMapLatest { isBypassEnabled ->
+ iconsInteractor.filteredNotifSet(
+ showAmbient = false,
+ showDismissed = false,
+ showRepliedMessages = false,
+ showPulsing = !isBypassEnabled,
+ )
+ }
+}
+
+/** Domain logic related to notification icons shown in the status bar. */
+class StatusBarNotificationIconsInteractor
+@Inject
+constructor(
+ iconsInteractor: NotificationIconsInteractor,
+ settingsRepository: NotificationListenerSettingsRepository,
+) {
+ val statusBarNotifs: Flow<Set<ActiveNotificationModel>> =
+ settingsRepository.showSilentStatusIcons.flatMapLatest { showSilentIcons ->
+ iconsInteractor.filteredNotifSet(
+ showAmbient = false,
+ showLowPriority = showSilentIcons,
+ showDismissed = false,
+ showRepliedMessages = false,
+ )
+ }
+}
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 de011db..d8a5f01 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
@@ -18,13 +18,8 @@
import android.content.Context
import android.graphics.Rect
import android.os.Bundle
-import android.os.Trace
import android.view.LayoutInflater
import android.view.View
-import android.widget.FrameLayout
-import androidx.annotation.VisibleForTesting
-import androidx.collection.ArrayMap
-import com.android.internal.statusbar.StatusBarIcon
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.demomode.DemoMode
@@ -33,27 +28,18 @@
import com.android.systemui.flags.Flags
import com.android.systemui.flags.RefactorFlag
import com.android.systemui.res.R
-import com.android.systemui.statusbar.NotificationListener
-import com.android.systemui.statusbar.NotificationMediaManager
import com.android.systemui.statusbar.NotificationShelfController
import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
import com.android.systemui.statusbar.notification.collection.ListEntry
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
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.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl
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.NotificationIconContainer
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
-import com.android.systemui.statusbar.window.StatusBarWindowController
-import com.android.wm.shell.bubbles.Bubbles
-import java.util.Optional
-import java.util.function.Function
+import com.android.systemui.statusbar.policy.ConfigurationController
import javax.inject.Inject
import kotlinx.coroutines.DisposableHandle
@@ -68,58 +54,34 @@
class NotificationIconAreaControllerViewBinderWrapperImpl
@Inject
constructor(
- private val context: Context,
+ context: Context,
private val configuration: ConfigurationState,
- private val wakeUpCoordinator: NotificationWakeUpCoordinator,
- private val bypassController: KeyguardBypassController,
- private val mediaManager: NotificationMediaManager,
- notificationListener: NotificationListener,
+ private val configurationController: ConfigurationController,
private val dozeParameters: DozeParameters,
- private val sectionStyleProvider: SectionStyleProvider,
- private val bubblesOptional: Optional<Bubbles>,
demoModeController: DemoModeController,
private val featureFlags: FeatureFlagsClassic,
- private val statusBarWindowController: StatusBarWindowController,
private val screenOffAnimationController: ScreenOffAnimationController,
+ private val shelfIconViewStore: ShelfNotificationIconViewStore,
private val shelfIconsViewModel: NotificationIconContainerShelfViewModel,
- private val statusBarIconsViewModel: NotificationIconContainerStatusBarViewModel,
+ private val aodIconViewStore: AlwaysOnDisplayNotificationIconViewStore,
private val aodIconsViewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
-) : NotificationIconAreaController, NotificationWakeUpCoordinator.WakeUpListener, DemoMode {
+ private val statusBarIconViewStore: StatusBarNotificationIconViewStore,
+ private val statusBarIconsViewModel: NotificationIconContainerStatusBarViewModel,
+) : NotificationIconAreaController, DemoMode {
- private val updateStatusBarIcons = Runnable { updateStatusBarIcons() }
private val shelfRefactor = RefactorFlag(featureFlags, Flags.NOTIFICATION_SHELF_REFACTOR)
- private var iconSize = 0
- private var iconHPadding = 0
- private var notificationEntries = listOf<ListEntry>()
private var notificationIconArea: View? = null
private var notificationIcons: NotificationIconContainer? = null
private var shelfIcons: NotificationIconContainer? = null
private var aodIcons: NotificationIconContainer? = null
private var aodBindJob: DisposableHandle? = null
- private var showLowPriority = true
-
- @VisibleForTesting
- val settingsListener: NotificationListener.NotificationSettingsListener =
- object : NotificationListener.NotificationSettingsListener {
- override fun onStatusBarIconsBehaviorChanged(hideSilentStatusIcons: Boolean) {
- showLowPriority = !hideSilentStatusIcons
- updateStatusBarIcons()
- }
- }
init {
- wakeUpCoordinator.addListener(this)
demoModeController.addCallback(this)
- notificationListener.addNotificationSettingsListener(settingsListener)
initializeNotificationAreaViews(context)
}
- @VisibleForTesting
- fun shouldShowLowPriorityIcons(): Boolean {
- return showLowPriority
- }
-
/** Called by the Keyguard*ViewController whose view contains the aod icons. */
override fun setupAodIcons(aodIcons: NotificationIconContainer) {
val changed = this.aodIcons != null && aodIcons !== this.aodIcons
@@ -135,14 +97,12 @@
aodIcons,
aodIconsViewModel,
configuration,
+ configurationController,
dozeParameters,
featureFlags,
screenOffAnimationController,
+ aodIconViewStore,
)
- if (changed) {
- updateAodNotificationIcons()
- }
- updateIconLayoutParams(context)
}
override fun setupShelf(notificationShelfController: NotificationShelfController) =
@@ -154,17 +114,17 @@
icons,
shelfIconsViewModel,
configuration,
+ configurationController,
dozeParameters,
featureFlags,
screenOffAnimationController,
+ shelfIconViewStore,
)
shelfIcons = icons
}
}
- override fun onDensityOrFontScaleChanged(context: Context) {
- updateIconLayoutParams(context)
- }
+ override fun onDensityOrFontScaleChanged(context: Context) = unsupported
/** Returns the view that represents the notification area. */
override fun getNotificationInnerAreaView(): View? {
@@ -172,39 +132,9 @@
}
/** Updates the notifications with the given list of notifications to display. */
- override fun updateNotificationIcons(entries: List<ListEntry>) {
- notificationEntries = entries
- updateNotificationIcons()
- }
+ override fun updateNotificationIcons(entries: List<ListEntry>) = unsupported
- private fun updateStatusBarIcons() {
- updateIconsForLayout(
- { entry: NotificationEntry -> entry.icons.statusBarIcon },
- notificationIcons,
- showAmbient = false /* showAmbient */,
- showLowPriority = showLowPriority,
- hideDismissed = true /* hideDismissed */,
- hideRepliedMessages = true /* hideRepliedMessages */,
- hideCurrentMedia = false /* hideCurrentMedia */,
- hidePulsing = false /* hidePulsing */
- )
- }
-
- override fun updateAodNotificationIcons() {
- if (aodIcons == null) {
- return
- }
- updateIconsForLayout(
- { entry: NotificationEntry -> entry.icons.aodIcon },
- aodIcons,
- showAmbient = false /* showAmbient */,
- showLowPriority = true /* showLowPriority */,
- hideDismissed = true /* hideDismissed */,
- hideRepliedMessages = true /* hideRepliedMessages */,
- hideCurrentMedia = true /* hideCurrentMedia */,
- hidePulsing = bypassController.bypassEnabled /* hidePulsing */
- )
- }
+ override fun updateAodNotificationIcons() = unsupported
override fun showIconIsolated(icon: StatusBarIconView?, animated: Boolean) {
notificationIcons!!.showIconIsolated(icon, animated)
@@ -222,10 +152,6 @@
return if (aodIcons == null) 0 else aodIcons!!.height
}
- override fun onFullyHiddenChanged(isFullyHidden: Boolean) {
- updateAodNotificationIcons()
- }
-
override fun demoCommands(): List<String> {
val commands = ArrayList<String>()
commands.add(DemoMode.COMMAND_NOTIFICATIONS)
@@ -252,7 +178,6 @@
/** Initializes the views that will represent the notification area. */
private fun initializeNotificationAreaViews(context: Context) {
- reloadDimens(context)
val layoutInflater = LayoutInflater.from(context)
notificationIconArea = inflateIconArea(layoutInflater)
notificationIcons = notificationIconArea?.findViewById(R.id.notificationIcons)
@@ -260,233 +185,14 @@
notificationIcons!!,
statusBarIconsViewModel,
configuration,
+ configurationController,
dozeParameters,
featureFlags,
screenOffAnimationController,
+ statusBarIconViewStore,
)
}
- private fun updateIconLayoutParams(context: Context) {
- reloadDimens(context)
- val params = generateIconLayoutParams()
- for (i in 0 until notificationIcons!!.childCount) {
- val child = notificationIcons!!.getChildAt(i)
- child.layoutParams = params
- }
- if (shelfIcons != null) {
- for (i in 0 until shelfIcons!!.childCount) {
- val child = shelfIcons!!.getChildAt(i)
- child.layoutParams = params
- }
- }
- if (aodIcons != null) {
- for (i in 0 until aodIcons!!.childCount) {
- val child = aodIcons!!.getChildAt(i)
- child.layoutParams = params
- }
- }
- }
-
- private fun generateIconLayoutParams(): FrameLayout.LayoutParams {
- return FrameLayout.LayoutParams(
- iconSize + 2 * iconHPadding,
- statusBarWindowController.statusBarHeight
- )
- }
-
- private fun reloadDimens(context: Context) {
- val res = context.resources
- iconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size_sp)
- iconHPadding = res.getDimensionPixelSize(R.dimen.status_bar_icon_horizontal_margin)
- }
-
- private fun shouldShowNotificationIcon(
- entry: NotificationEntry,
- showAmbient: Boolean,
- showLowPriority: Boolean,
- hideDismissed: Boolean,
- hideRepliedMessages: Boolean,
- hideCurrentMedia: Boolean,
- hidePulsing: Boolean
- ): Boolean {
- if (!showAmbient && sectionStyleProvider.isMinimized(entry)) {
- return false
- }
- if (hideCurrentMedia && entry.key == mediaManager.mediaNotificationKey) {
- return false
- }
- if (!showLowPriority && sectionStyleProvider.isSilent(entry)) {
- return false
- }
- if (entry.isRowDismissed && hideDismissed) {
- return false
- }
- if (hideRepliedMessages && entry.isLastMessageFromReply) {
- return false
- }
- // showAmbient == show in shade but not shelf
- if (!showAmbient && entry.shouldSuppressStatusBar()) {
- return false
- }
- if (
- hidePulsing &&
- entry.showingPulsing() &&
- (!wakeUpCoordinator.notificationsFullyHidden || !entry.isPulseSuppressed)
- ) {
- return false
- }
- return if (bubblesOptional.isPresent && bubblesOptional.get().isBubbleExpanded(entry.key)) {
- false
- } else true
- }
-
- private fun updateNotificationIcons() {
- Trace.beginSection("NotificationIconAreaController.updateNotificationIcons")
- updateStatusBarIcons()
- updateShelfIcons()
- updateAodNotificationIcons()
- Trace.endSection()
- }
-
- private fun updateShelfIcons() {
- if (shelfIcons == null) {
- return
- }
- updateIconsForLayout(
- { entry: NotificationEntry -> entry.icons.shelfIcon },
- shelfIcons,
- showAmbient = true,
- showLowPriority = true,
- hideDismissed = false,
- hideRepliedMessages = false,
- hideCurrentMedia = false,
- hidePulsing = false
- )
- }
-
- /**
- * Updates the notification icons for a host layout. This will ensure that the notification host
- * layout will have the same icons like the ones in here.
- *
- * @param function A function to look up an icon view based on an entry
- * @param hostLayout which layout should be updated
- * @param showAmbient should ambient notification icons be shown
- * @param showLowPriority should icons from silent notifications be shown
- * @param hideDismissed should dismissed icons be hidden
- * @param hideRepliedMessages should messages that have been replied to be hidden
- * @param hidePulsing should pulsing notifications be hidden
- */
- private fun updateIconsForLayout(
- function: Function<NotificationEntry, StatusBarIconView?>,
- hostLayout: NotificationIconContainer?,
- showAmbient: Boolean,
- showLowPriority: Boolean,
- hideDismissed: Boolean,
- hideRepliedMessages: Boolean,
- hideCurrentMedia: Boolean,
- hidePulsing: Boolean,
- ) {
- val toShow = ArrayList<StatusBarIconView>(notificationEntries.size)
- // Filter out ambient notifications and notification children.
- for (i in notificationEntries.indices) {
- val entry = notificationEntries[i].representativeEntry
- if (entry != null && entry.row != null) {
- if (
- shouldShowNotificationIcon(
- entry,
- showAmbient,
- showLowPriority,
- hideDismissed,
- hideRepliedMessages,
- hideCurrentMedia,
- hidePulsing
- )
- ) {
- val iconView = function.apply(entry)
- if (iconView != null) {
- toShow.add(iconView)
- }
- }
- }
- }
-
- // In case we are changing the suppression of a group, the replacement shouldn't flicker
- // and it should just be replaced instead. We therefore look for notifications that were
- // just replaced by the child or vice-versa to suppress this.
- val replacingIcons = ArrayMap<String, ArrayList<StatusBarIcon>>()
- val toRemove = ArrayList<View>()
- for (i in 0 until hostLayout!!.childCount) {
- val child = hostLayout.getChildAt(i) as? StatusBarIconView ?: continue
- if (!toShow.contains(child)) {
- var iconWasReplaced = false
- val removedGroupKey = child.notification.groupKey
- for (j in toShow.indices) {
- val candidate = toShow[j]
- if (
- candidate.sourceIcon.sameAs(child.sourceIcon) &&
- candidate.notification.groupKey == removedGroupKey
- ) {
- if (!iconWasReplaced) {
- iconWasReplaced = true
- } else {
- iconWasReplaced = false
- break
- }
- }
- }
- if (iconWasReplaced) {
- var statusBarIcons = replacingIcons[removedGroupKey]
- if (statusBarIcons == null) {
- statusBarIcons = ArrayList()
- replacingIcons[removedGroupKey] = statusBarIcons
- }
- statusBarIcons.add(child.statusBarIcon)
- }
- toRemove.add(child)
- }
- }
- // removing all duplicates
- val duplicates = ArrayList<String?>()
- for (key in replacingIcons.keys) {
- val statusBarIcons = replacingIcons[key]!!
- if (statusBarIcons.size != 1) {
- duplicates.add(key)
- }
- }
- replacingIcons.removeAll(duplicates)
- hostLayout.setReplacingIcons(replacingIcons)
- val toRemoveCount = toRemove.size
- for (i in 0 until toRemoveCount) {
- hostLayout.removeView(toRemove[i])
- }
- val params = generateIconLayoutParams()
- for (i in toShow.indices) {
- val v = toShow[i]
- // The view might still be transiently added if it was just removed and added again
- hostLayout.removeTransientView(v)
- if (v.parent == null) {
- if (hideDismissed) {
- v.setOnDismissListener(updateStatusBarIcons)
- }
- hostLayout.addView(v, i, params)
- }
- }
- hostLayout.setChangingViewPositions(true)
- // Re-sort notification icons
- val childCount = hostLayout.childCount
- for (i in 0 until childCount) {
- val actual = hostLayout.getChildAt(i)
- val expected = toShow[i]
- if (actual === expected) {
- continue
- }
- hostLayout.removeView(expected)
- hostLayout.addView(expected, i)
- }
- hostLayout.setChangingViewPositions(false)
- hostLayout.setReplacingIcons(null)
- }
-
companion object {
val unsupported: Nothing
get() =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
index 079004c..c6d7e21 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
@@ -17,9 +17,12 @@
import android.graphics.Rect
import android.view.View
+import android.widget.FrameLayout
+import androidx.collection.ArrayMap
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.animation.Interpolators
+import com.android.internal.policy.SystemBarUtils
import com.android.internal.util.ContrastColorUtil
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.flags.FeatureFlagsClassic
@@ -29,14 +32,26 @@
import com.android.systemui.statusbar.CrossFadeHelper
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.collection.NotifCollection
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconColors
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconsViewData
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.NotificationIconContainer
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.onConfigChanged
import com.android.systemui.util.children
+import com.android.systemui.util.kotlin.mapValuesNotNullTo
+import com.android.systemui.util.kotlin.sample
+import com.android.systemui.util.kotlin.stateFlow
+import javax.inject.Inject
import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -47,9 +62,11 @@
view: NotificationIconContainer,
viewModel: NotificationIconContainerViewModel,
configuration: ConfigurationState,
+ configurationController: ConfigurationController,
dozeParameters: DozeParameters,
featureFlags: FeatureFlagsClassic,
screenOffAnimationController: ScreenOffAnimationController,
+ viewStore: IconViewStore,
): DisposableHandle {
val contrastColorUtil = ContrastColorUtil.getInstance(view.context)
return view.repeatWhenAttached {
@@ -58,49 +75,137 @@
launch {
viewModel.isDozing.collect { (isDozing, animate) ->
val animateIfNotBlanking = animate && !dozeParameters.displayNeedsBlanking
- view.setDozing(isDozing, animateIfNotBlanking, /* delay= */ 0) {
- viewModel.completeDozeAnimation()
- }
+ view.setDozing(
+ /* dozing = */ isDozing,
+ /* fade = */ animateIfNotBlanking,
+ /* delay = */ 0,
+ /* endRunnable = */ viewModel::completeDozeAnimation,
+ )
}
}
// TODO(b/278765923): this should live where AOD is bound, not inside of the NIC
// view-binder
launch {
- val iconAppearTranslation =
- configuration
- .getDimensionPixelSize(R.dimen.shelf_appear_translation)
- .stateIn(this)
bindVisibility(
viewModel,
view,
+ configuration,
featureFlags,
screenOffAnimationController,
- iconAppearTranslation,
- ) {
- viewModel.completeVisibilityAnimation()
- }
+ onAnimationEnd = viewModel::completeVisibilityAnimation,
+ )
}
launch {
viewModel.iconColors
.mapNotNull { lookup -> lookup.iconColors(view.viewBounds) }
.collect { iconLookup -> applyTint(view, iconLookup, contrastColorUtil) }
}
+ launch {
+ bindIconViewData(
+ viewModel,
+ view,
+ configuration,
+ configurationController,
+ viewStore,
+ )
+ }
}
}
}
- // TODO(b/305739416): Once SBIV has its own Recommended Architecture stack, this can be moved
- // there and cleaned up.
+ private suspend fun bindIconViewData(
+ viewModel: NotificationIconContainerViewModel,
+ view: NotificationIconContainer,
+ configuration: ConfigurationState,
+ configurationController: ConfigurationController,
+ viewStore: IconViewStore,
+ ): Unit = coroutineScope {
+ val iconSizeFlow: Flow<Int> =
+ configuration.getDimensionPixelSize(
+ com.android.internal.R.dimen.status_bar_icon_size_sp,
+ )
+ val iconHorizontalPaddingFlow: Flow<Int> =
+ configuration.getDimensionPixelSize(R.dimen.status_bar_icon_horizontal_margin)
+ val statusBarHeightFlow: StateFlow<Int> =
+ stateFlow(changedSignals = configurationController.onConfigChanged) {
+ SystemBarUtils.getStatusBarHeight(view.context)
+ }
+ val layoutParams: Flow<FrameLayout.LayoutParams> =
+ combine(iconSizeFlow, iconHorizontalPaddingFlow, statusBarHeightFlow) {
+ iconSize,
+ iconHPadding,
+ statusBarHeight,
+ ->
+ FrameLayout.LayoutParams(iconSize + 2 * iconHPadding, statusBarHeight)
+ }
+
+ launch {
+ layoutParams.collect { params: FrameLayout.LayoutParams ->
+ for (child in view.children) {
+ child.layoutParams = params
+ }
+ }
+ }
+
+ var prevIcons = IconsViewData()
+ viewModel.iconsViewData.sample(layoutParams, ::Pair).collect {
+ (iconsData: IconsViewData, layoutParams: FrameLayout.LayoutParams),
+ ->
+ val iconsDiff = IconsViewData.computeDifference(iconsData, prevIcons)
+ prevIcons = iconsData
+
+ val replacingIcons =
+ iconsDiff.groupReplacements.mapValuesNotNullTo(ArrayMap()) { (_, v) ->
+ viewStore.iconView(v.notifKey)?.statusBarIcon
+ }
+ view.setReplacingIcons(replacingIcons)
+
+ val childrenByNotifKey: Map<String, StatusBarIconView> =
+ view.children.filterIsInstance<StatusBarIconView>().associateByTo(ArrayMap()) {
+ it.notification.key
+ }
+
+ iconsDiff.removed
+ .mapNotNull { key -> childrenByNotifKey[key] }
+ .forEach { child -> view.removeView(child) }
+
+ val toAdd = iconsDiff.added.mapNotNull { viewStore.iconView(it.notifKey) }
+ for ((i, sbiv) in toAdd.withIndex()) {
+ // The view might still be transiently added if it was just removed
+ // and added again
+ view.removeTransientView(sbiv)
+ view.addView(sbiv, i, layoutParams)
+ }
+
+ view.setChangingViewPositions(true)
+ // Re-sort notification icons
+ val childCount = view.childCount
+ for (i in 0 until childCount) {
+ val actual = view.getChildAt(i)
+ val expected = viewStore.iconView(iconsData.visibleKeys[i].notifKey)!!
+ if (actual === expected) {
+ continue
+ }
+ view.removeView(expected)
+ view.addView(expected, i)
+ }
+ view.setChangingViewPositions(false)
+
+ view.setReplacingIcons(null)
+ }
+ }
+
+ // TODO(b/305739416): Once StatusBarIconView has its own Recommended Architecture stack, this
+ // can be moved there and cleaned up.
private fun applyTint(
view: NotificationIconContainer,
iconColors: IconColors,
contrastColorUtil: ContrastColorUtil,
) {
- view.children.filterIsInstance<StatusBarIconView>().forEach { iv ->
- if (iv.width != 0) {
- updateTintForIcon(iv, iconColors, contrastColorUtil)
- }
- }
+ view.children
+ .filterIsInstance<StatusBarIconView>()
+ .filter { it.width != 0 }
+ .forEach { iv -> updateTintForIcon(iv, iconColors, contrastColorUtil) }
}
private fun updateTintForIcon(
@@ -117,11 +222,13 @@
private suspend fun bindVisibility(
viewModel: NotificationIconContainerViewModel,
view: NotificationIconContainer,
+ configuration: ConfigurationState,
featureFlags: FeatureFlagsClassic,
screenOffAnimationController: ScreenOffAnimationController,
- iconAppearTranslation: StateFlow<Int>,
onAnimationEnd: () -> Unit,
- ) {
+ ): Unit = coroutineScope {
+ val iconAppearTranslation =
+ configuration.getDimensionPixelSize(R.dimen.shelf_appear_translation).stateIn(this)
val statusViewMigrated = featureFlags.isEnabled(Flags.MIGRATE_KEYGUARD_STATUS_VIEW)
viewModel.isVisible.collect { (isVisible, animate) ->
view.animate().cancel()
@@ -218,4 +325,39 @@
/* bottom = */ top + height,
)
}
+
+ /** External storage for [StatusBarIconView] instances. */
+ fun interface IconViewStore {
+ fun iconView(key: String): StatusBarIconView?
+ }
+}
+
+/** [IconViewStore] for the [com.android.systemui.statusbar.NotificationShelf] */
+class ShelfNotificationIconViewStore
+@Inject
+constructor(
+ private val notifCollection: NotifCollection,
+) : IconViewStore {
+ override fun iconView(key: String): StatusBarIconView? =
+ notifCollection.getEntry(key)?.icons?.shelfIcon
+}
+
+/** [IconViewStore] for the always-on display. */
+class AlwaysOnDisplayNotificationIconViewStore
+@Inject
+constructor(
+ private val notifCollection: NotifCollection,
+) : IconViewStore {
+ override fun iconView(key: String): StatusBarIconView? =
+ notifCollection.getEntry(key)?.icons?.aodIcon
+}
+
+/** [IconViewStore] for the status bar. */
+class StatusBarNotificationIconViewStore
+@Inject
+constructor(
+ private val notifCollection: NotifCollection,
+) : IconViewStore {
+ override fun iconView(key: String): StatusBarIconView? =
+ notifCollection.getEntry(key)?.icons?.statusBarIcon
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
index e9de4bd..885f449e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
@@ -30,8 +30,10 @@
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
+import com.android.systemui.statusbar.notification.icon.domain.interactor.AlwaysOnDisplayNotificationIconsInteractor
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.ColorLookup
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconColors
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconsViewData
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
import com.android.systemui.util.kotlin.pairwise
@@ -55,6 +57,7 @@
private val deviceEntryInteractor: DeviceEntryInteractor,
private val dozeParameters: DozeParameters,
private val featureFlags: FeatureFlagsClassic,
+ iconsInteractor: AlwaysOnDisplayNotificationIconsInteractor,
keyguardInteractor: KeyguardInteractor,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
@@ -134,14 +137,19 @@
onVisAnimationComplete.tryEmit(Unit)
}
+ override val iconsViewData: Flow<IconsViewData> =
+ iconsInteractor.aodNotifs.map { entries ->
+ IconsViewData(
+ visibleKeys = entries.mapNotNull { it.toIconInfo(it.aodIcon) },
+ )
+ }
+
/** Is there an expanded pulse, are we animating in response? */
private fun isPulseExpandingAnimated(): Flow<AnimatedValue<Boolean>> {
return notificationsKeyguardInteractor.isPulseExpanding
.pairwise(initialValue = null)
// If pulsing changes, start animating, unless it's the first emission
- .map { (prev, expanding) ->
- AnimatableEvent(expanding!!, startAnimating = prev != null)
- }
+ .map { (prev, expanding) -> AnimatableEvent(expanding, startAnimating = prev != null) }
.toAnimatedValueFlow(completionEvents = onVisAnimationComplete)
}
@@ -164,9 +172,9 @@
// We only want the appear animations to happen when the notifications
// get fully hidden, since otherwise the un-hide animation overlaps.
featureFlags.isEnabled(Flags.NEW_AOD_TRANSITION) -> true
- else -> fullyHidden!!
+ else -> fullyHidden
}
- AnimatableEvent(fullyHidden!!, animate)
+ AnimatableEvent(fullyHidden, animate)
}
.toAnimatedValueFlow(completionEvents = onVisAnimationComplete)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
index f305155..38eae24 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
@@ -15,20 +15,33 @@
*/
package com.android.systemui.statusbar.notification.icon.ui.viewmodel
+import com.android.systemui.statusbar.notification.icon.domain.interactor.NotificationIconsInteractor
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.ColorLookup
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconsViewData
import com.android.systemui.util.ui.AnimatedValue
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
/** View-model for the overflow row of notification icons displayed in the notification shade. */
-class NotificationIconContainerShelfViewModel @Inject constructor() :
- NotificationIconContainerViewModel {
+class NotificationIconContainerShelfViewModel
+@Inject
+constructor(
+ interactor: NotificationIconsInteractor,
+) : NotificationIconContainerViewModel {
override val animationsEnabled: Flow<Boolean> = flowOf(true)
override val isDozing: Flow<AnimatedValue<Boolean>> = emptyFlow()
override val isVisible: Flow<AnimatedValue<Boolean>> = emptyFlow()
override fun completeDozeAnimation() {}
override fun completeVisibilityAnimation() {}
override val iconColors: Flow<ColorLookup> = emptyFlow()
+
+ override val iconsViewData: Flow<IconsViewData> =
+ interactor.filteredNotifSet().map { entries ->
+ IconsViewData(
+ visibleKeys = entries.mapNotNull { it.toIconInfo(it.shelfIcon) },
+ )
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
index ee01fcc..cdbabb6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
@@ -20,20 +20,24 @@
import com.android.systemui.plugins.DarkIconDispatcher
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.statusbar.notification.icon.domain.interactor.StatusBarNotificationIconsInteractor
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.ColorLookup
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconColors
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconsViewData
import com.android.systemui.statusbar.phone.domain.interactor.DarkIconInteractor
import com.android.systemui.util.ui.AnimatedValue
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.map
/** View-model for the row of notification icons displayed in the status bar, */
class NotificationIconContainerStatusBarViewModel
@Inject
constructor(
darkIconInteractor: DarkIconInteractor,
+ iconsInteractor: StatusBarNotificationIconsInteractor,
keyguardInteractor: KeyguardInteractor,
notificationsInteractor: ActiveNotificationsInteractor,
shadeInteractor: ShadeInteractor,
@@ -45,6 +49,7 @@
) { panelTouchesEnabled, isKeyguardShowing ->
panelTouchesEnabled && !isKeyguardShowing
}
+
override val iconColors: Flow<ColorLookup> =
combine(
darkIconInteractor.tintAreas,
@@ -60,11 +65,19 @@
}
}
}
+
override val isDozing: Flow<AnimatedValue<Boolean>> = emptyFlow()
override val isVisible: Flow<AnimatedValue<Boolean>> = emptyFlow()
override fun completeDozeAnimation() {}
override fun completeVisibilityAnimation() {}
+ override val iconsViewData: Flow<IconsViewData> =
+ iconsInteractor.statusBarNotifs.map { entries ->
+ IconsViewData(
+ visibleKeys = entries.mapNotNull { it.toIconInfo(it.statusBarIcon) },
+ )
+ }
+
private class IconColorsImpl(
override val tint: Int,
private val areas: Collection<Rect>,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt
index c98811b..0e8dfea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt
@@ -16,6 +16,11 @@
package com.android.systemui.statusbar.notification.icon.ui.viewmodel
import android.graphics.Rect
+import android.graphics.drawable.Icon
+import androidx.collection.ArrayMap
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconInfo
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import com.android.systemui.util.kotlin.mapValuesNotNullTo
import com.android.systemui.util.ui.AnimatedValue
import kotlinx.coroutines.flow.Flow
@@ -37,6 +42,9 @@
/** The colors with which to display the notification icons. */
val iconColors: Flow<ColorLookup>
+ /** [IconsViewData] indicating which icons to display in the view. */
+ val iconsViewData: Flow<IconsViewData>
+
/**
* Signal completion of the [isDozing] animation; if [isDozing]'s [AnimatedValue.isAnimating]
* property was `true`, calling this method will update it to `false`.
@@ -69,4 +77,126 @@
*/
fun staticDrawableColor(viewBounds: Rect, isColorized: Boolean): Int
}
+
+ /** Encapsulates the collection of notification icons present on the device. */
+ data class IconsViewData(
+ /** Icons that are visible in the container. */
+ val visibleKeys: List<IconInfo> = emptyList(),
+ /** Keys of icons that are "behind" the overflow dot. */
+ val collapsedKeys: Set<String> = emptySet(),
+ /** Whether the overflow dot should be shown regardless if [collapsedKeys] is empty. */
+ val forceShowDot: Boolean = false,
+ ) {
+ /** The difference between two [IconsViewData]s. */
+ data class Diff(
+ /** Icons added in the newer dataset. */
+ val added: List<IconInfo> = emptyList(),
+ /** Icons removed from the older dataset. */
+ val removed: List<String> = emptyList(),
+ /**
+ * Groups whose icon was replaced with a single new notification icon. The key of the
+ * [Map] is the notification group key, and the value is the new icon.
+ *
+ * Specifically, this models a difference where the older dataset had notification
+ * groups with a single icon in the set, and the newer dataset has a single, different
+ * icon for the same group. A view binder can use this information for special
+ * animations for this specific change.
+ */
+ val groupReplacements: Map<String, IconInfo> = emptyMap(),
+ )
+
+ companion object {
+ /**
+ * Returns an [IconsViewData.Diff] calculated from a [new] and [previous][prev]
+ * [IconsViewData] state.
+ */
+ fun computeDifference(new: IconsViewData, prev: IconsViewData): Diff {
+ val added: List<IconInfo> =
+ new.visibleKeys.filter {
+ it.notifKey !in prev.visibleKeys.asSequence().map { it.notifKey }
+ }
+ val removed: List<IconInfo> =
+ prev.visibleKeys.filter {
+ it.notifKey !in new.visibleKeys.asSequence().map { it.notifKey }
+ }
+ val groupsToShow: Set<IconGroupInfo> =
+ new.visibleKeys.asSequence().map { it.groupInfo }.toSet()
+ val replacements: ArrayMap<String, IconInfo> =
+ removed
+ .asSequence()
+ .filter { keyToRemove -> keyToRemove.groupInfo in groupsToShow }
+ .groupBy { it.groupInfo.groupKey }
+ .mapValuesNotNullTo(ArrayMap()) { (_, vs) ->
+ vs.takeIf { it.size == 1 }?.get(0)
+ }
+ return Diff(added, removed.map { it.notifKey }, replacements)
+ }
+ }
+ }
+
+ /** An Icon, and keys for unique identification. */
+ data class IconInfo(
+ val sourceIcon: Icon,
+ val notifKey: String,
+ val groupKey: String,
+ )
+}
+
+/**
+ * Construct an [IconInfo] out of an [ActiveNotificationModel], or return `null` if one cannot be
+ * created due to missing information.
+ */
+fun ActiveNotificationModel.toIconInfo(sourceIcon: Icon?): IconInfo? {
+ return sourceIcon?.let {
+ groupKey?.let { groupKey ->
+ IconInfo(
+ sourceIcon = sourceIcon,
+ notifKey = key,
+ groupKey = groupKey,
+ )
+ }
+ }
+}
+
+private val IconInfo.groupInfo: IconGroupInfo
+ get() = IconGroupInfo(sourceIcon, groupKey)
+
+private data class IconGroupInfo(
+ val sourceIcon: Icon,
+ val groupKey: String,
+) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as IconGroupInfo
+
+ if (groupKey != other.groupKey) return false
+ return sourceIcon.sameAs(other.sourceIcon)
+ }
+
+ override fun hashCode(): Int {
+ var result = groupKey.hashCode()
+ result = 31 * result + sourceIcon.type.hashCode()
+ when (sourceIcon.type) {
+ Icon.TYPE_BITMAP,
+ Icon.TYPE_ADAPTIVE_BITMAP -> {
+ result = 31 * result + sourceIcon.bitmap.hashCode()
+ }
+ Icon.TYPE_DATA -> {
+ result = 31 * result + sourceIcon.dataLength.hashCode()
+ result = 31 * result + sourceIcon.dataOffset.hashCode()
+ }
+ Icon.TYPE_RESOURCE -> {
+ result = 31 * result + sourceIcon.resId.hashCode()
+ result = 31 * result + sourceIcon.resPackage.hashCode()
+ }
+ Icon.TYPE_URI,
+ Icon.TYPE_URI_ADAPTIVE_BITMAP -> {
+ result = 31 * result + sourceIcon.uriString.hashCode()
+ }
+ }
+ return result
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
index ea29cab..78370ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
@@ -15,8 +15,36 @@
package com.android.systemui.statusbar.notification.shared
+import android.graphics.drawable.Icon
+
/** Model for entries in the notification stack. */
data class ActiveNotificationModel(
/** Notification key associated with this entry. */
val key: String,
+ /** Notification group key associated with this entry. */
+ val groupKey: String?,
+ /** Is this entry in the ambient / minimized section (lowest priority)? */
+ val isAmbient: Boolean,
+ /**
+ * Is this entry dismissed? This is `true` when the user has dismissed the notification in the
+ * UI, but `NotificationManager` has not yet signalled to us that it has received the dismissal.
+ */
+ val isRowDismissed: Boolean,
+ /** Is this entry in the silent section? */
+ val isSilent: Boolean,
+ /**
+ * Does this entry represent a conversation, the last message of which was from a remote input
+ * reply?
+ */
+ val isLastMessageFromReply: Boolean,
+ /** Is this entry suppressed from appearing in the status bar as an icon? */
+ val isSuppressedFromStatusBar: Boolean,
+ /** Is this entry actively pulsing on AOD or bypassed-keyguard? */
+ val isPulsing: Boolean,
+ /** Icon to display on AOD. */
+ val aodIcon: Icon?,
+ /** Icon to display in the notification shelf. */
+ val shelfIcon: Icon?,
+ /** Icon to display in the status bar. */
+ val statusBarIcon: Icon?,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index cb85966..f41b7df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -3106,7 +3106,9 @@
}
// TODO: Bring these out of CentralSurfaces.
mUserInfoControllerImpl.onDensityOrFontScaleChanged();
- mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext);
+ if (!mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+ mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext);
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index d3d11ea..66341ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -37,6 +37,8 @@
import com.android.systemui.doze.DozeHost;
import com.android.systemui.doze.DozeLog;
import com.android.systemui.doze.DozeReceiver;
+import com.android.systemui.flags.FeatureFlagsClassic;
+import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.keyguard.domain.interactor.DozeInteractor;
import com.android.systemui.shade.NotificationShadeWindowViewController;
@@ -82,6 +84,7 @@
private final SysuiStatusBarStateController mStatusBarStateController;
private final DeviceProvisionedController mDeviceProvisionedController;
private final HeadsUpManager mHeadsUpManager;
+ private final FeatureFlagsClassic mFeatureFlags;
private final BatteryController mBatteryController;
private final ScrimController mScrimController;
private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
@@ -107,6 +110,7 @@
WakefulnessLifecycle wakefulnessLifecycle,
SysuiStatusBarStateController statusBarStateController,
DeviceProvisionedController deviceProvisionedController,
+ FeatureFlagsClassic featureFlags,
HeadsUpManager headsUpManager, BatteryController batteryController,
ScrimController scrimController,
Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
@@ -130,6 +134,7 @@
mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
mAssistManagerLazy = assistManagerLazy;
mDozeScrimController = dozeScrimController;
+ mFeatureFlags = featureFlags;
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mPulseExpansionHandler = pulseExpansionHandler;
mNotificationShadeWindowController = notificationShadeWindowController;
@@ -173,8 +178,13 @@
void fireNotificationPulse(NotificationEntry entry) {
Runnable pulseSuppressedListener = () -> {
- entry.setPulseSuppressed(true);
- mNotificationIconAreaController.updateAodNotificationIcons();
+ if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+ mHeadsUpManager.removeNotification(
+ entry.getKey(), /* releaseImmediately= */ true, /* animate= */ false);
+ } else {
+ entry.setPulseSuppressed(true);
+ mNotificationIconAreaController.updateAodNotificationIcons();
+ }
};
Assert.isMainThread();
for (Callback callback : mCallbacks) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
index f9856b0..4284c96c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
@@ -448,7 +448,7 @@
}
}
replacingIcons.removeAll(duplicates);
- hostLayout.setReplacingIcons(replacingIcons);
+ hostLayout.setReplacingIconsLegacy(replacingIcons);
final int toRemoveCount = toRemove.size();
for (int i = 0; i < toRemoveCount; i++) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index b15c0fd..d70edbf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -40,6 +40,8 @@
import com.android.app.animation.Interpolators;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.settingslib.Utils;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.RefactorFlag;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.notification.stack.AnimationFilter;
@@ -48,7 +50,9 @@
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.Map;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
/**
* A container for notification icons. It handles overflowing icons properly and positions them
@@ -131,6 +135,9 @@
}
}.setDuration(CONTENT_FADE_DURATION);
+ private final RefactorFlag mIconContainerRefactorFlag =
+ RefactorFlag.forView(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR);
+
/* Maximum number of icons on AOD when also showing overflow dot. */
private int mMaxIconsOnAod;
@@ -156,7 +163,8 @@
private int mIconSize;
private boolean mDisallowNextAnimation;
private boolean mAnimationsEnabled = true;
- private ArrayMap<String, ArrayList<StatusBarIcon>> mReplacingIcons;
+ private ArrayMap<String, StatusBarIcon> mReplacingIcons;
+ private ArrayMap<String, ArrayList<StatusBarIcon>> mReplacingIconsLegacy;
// Keep track of the last visible icon so collapsed container can report on its location
private IconState mLastVisibleIconState;
private IconState mFirstVisibleIconState;
@@ -339,23 +347,29 @@
}
private boolean isReplacingIcon(View child) {
- if (mReplacingIcons == null) {
- return false;
- }
if (!(child instanceof StatusBarIconView)) {
return false;
}
StatusBarIconView iconView = (StatusBarIconView) child;
Icon sourceIcon = iconView.getSourceIcon();
String groupKey = iconView.getNotification().getGroupKey();
- ArrayList<StatusBarIcon> statusBarIcons = mReplacingIcons.get(groupKey);
- if (statusBarIcons != null) {
- StatusBarIcon replacedIcon = statusBarIcons.get(0);
- if (sourceIcon.sameAs(replacedIcon.icon)) {
- return true;
+ if (mIconContainerRefactorFlag.isEnabled()) {
+ if (mReplacingIcons == null) {
+ return false;
}
+ StatusBarIcon replacedIcon = mReplacingIcons.get(groupKey);
+ return replacedIcon != null && sourceIcon.sameAs(replacedIcon.icon);
+ } else {
+ if (mReplacingIconsLegacy == null) {
+ return false;
+ }
+ ArrayList<StatusBarIcon> statusBarIcons = mReplacingIconsLegacy.get(groupKey);
+ if (statusBarIcons != null) {
+ StatusBarIcon replacedIcon = statusBarIcons.get(0);
+ return sourceIcon.sameAs(replacedIcon.icon);
+ }
+ return false;
}
- return false;
}
@Override
@@ -681,7 +695,13 @@
mAnimationsEnabled = enabled;
}
- public void setReplacingIcons(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons) {
+ public void setReplacingIconsLegacy(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons) {
+ mIconContainerRefactorFlag.assertInLegacyMode();
+ mReplacingIconsLegacy = replacingIcons;
+ }
+
+ public void setReplacingIcons(ArrayMap<String, StatusBarIcon> replacingIcons) {
+ if (mIconContainerRefactorFlag.isUnexpectedlyInLegacyMode()) return;
mReplacingIcons = replacingIcons;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt
index 25d67af..0a2bbe5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt
@@ -13,6 +13,7 @@
*/
package com.android.systemui.statusbar.policy
+import android.content.res.Configuration
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
@@ -50,3 +51,20 @@
addCallback(listener)
awaitClose { removeCallback(listener) }
}
+
+/**
+ * A [Flow] that emits whenever the configuration has changed.
+ *
+ * @see ConfigurationController.ConfigurationListener.onConfigChanged
+ */
+val ConfigurationController.onConfigChanged: Flow<Configuration>
+ get() = conflatedCallbackFlow {
+ val listener =
+ object : ConfigurationController.ConfigurationListener {
+ override fun onConfigChanged(newConfig: Configuration) {
+ trySend(newConfig)
+ }
+ }
+ addCallback(listener)
+ awaitClose { removeCallback(listener) }
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
index 31b90ba..8fe57e11 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
@@ -61,10 +61,10 @@
*
* Useful for code that needs to compare the current value to the previous value.
*/
-fun <T, R> Flow<T>.pairwiseBy(
- initialValue: T,
- transform: suspend (previousValue: T, newValue: T) -> R,
-): Flow<R> = onStart { emit(initialValue) }.pairwiseBy(transform)
+fun <S, T : S, R> Flow<T>.pairwiseBy(
+ initialValue: S,
+ transform: suspend (previousValue: S, newValue: T) -> R,
+): Flow<R> = pairwiseBy(getInitialValue = { initialValue }, transform)
/**
* Returns a new [Flow] that combines the two most recent emissions from [this] using [transform].
@@ -75,10 +75,16 @@
*
* Useful for code that needs to compare the current value to the previous value.
*/
-fun <T, R> Flow<T>.pairwiseBy(
- getInitialValue: suspend () -> T,
- transform: suspend (previousValue: T, newValue: T) -> R,
-): Flow<R> = onStart { emit(getInitialValue()) }.pairwiseBy(transform)
+fun <S, T : S, R> Flow<T>.pairwiseBy(
+ getInitialValue: suspend () -> S,
+ transform: suspend (previousValue: S, newValue: T) -> R,
+): Flow<R> = flow {
+ var previousValue: S = getInitialValue()
+ collect { newVal ->
+ emit(transform(previousValue, newVal))
+ previousValue = newVal
+ }
+}
/**
* Returns a new [Flow] that produces the two most recent emissions from [this]. Note that the new
@@ -86,7 +92,7 @@
*
* Useful for code that needs to compare the current value to the previous value.
*/
-fun <T> Flow<T>.pairwise(): Flow<WithPrev<T>> = pairwiseBy(::WithPrev)
+fun <T> Flow<T>.pairwise(): Flow<WithPrev<T, T>> = pairwiseBy(::WithPrev)
/**
* Returns a new [Flow] that produces the two most recent emissions from [this]. [initialValue] will
@@ -94,10 +100,11 @@
*
* Useful for code that needs to compare the current value to the previous value.
*/
-fun <T> Flow<T>.pairwise(initialValue: T): Flow<WithPrev<T>> = pairwiseBy(initialValue, ::WithPrev)
+fun <S, T : S> Flow<T>.pairwise(initialValue: S): Flow<WithPrev<S, T>> =
+ pairwiseBy(initialValue, ::WithPrev)
/** Holds a [newValue] emitted from a [Flow], along with the [previousValue] emitted value. */
-data class WithPrev<T>(val previousValue: T, val newValue: T)
+data class WithPrev<out S, out T : S>(val previousValue: S, val newValue: T)
/**
* Returns a new [Flow] that combines the [Set] changes between each emission from [this] using
@@ -265,112 +272,120 @@
* immediately invoke [getValue] to establish its initial value.
*/
inline fun <T> CoroutineScope.stateFlow(
- changedSignals: Flow<Unit>,
+ changedSignals: Flow<*>,
crossinline getValue: () -> T,
): StateFlow<T> =
changedSignals.map { getValue() }.stateIn(this, SharingStarted.Eagerly, getValue())
inline fun <T1, T2, T3, T4, T5, T6, R> combine(
- flow: Flow<T1>,
- flow2: Flow<T2>,
- flow3: Flow<T3>,
- flow4: Flow<T4>,
- flow5: Flow<T5>,
- flow6: Flow<T6>,
- crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R
+ flow: Flow<T1>,
+ flow2: Flow<T2>,
+ flow3: Flow<T3>,
+ flow4: Flow<T4>,
+ flow5: Flow<T5>,
+ flow6: Flow<T6>,
+ crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R
): Flow<R> {
- return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) {
- args: Array<*> ->
+ return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*>
+ ->
@Suppress("UNCHECKED_CAST")
transform(
- args[0] as T1,
- args[1] as T2,
- args[2] as T3,
- args[3] as T4,
- args[4] as T5,
- args[5] as T6
+ args[0] as T1,
+ args[1] as T2,
+ args[2] as T3,
+ args[3] as T4,
+ args[4] as T5,
+ args[5] as T6
)
}
}
inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
- flow: Flow<T1>,
- flow2: Flow<T2>,
- flow3: Flow<T3>,
- flow4: Flow<T4>,
- flow5: Flow<T5>,
- flow6: Flow<T6>,
- flow7: Flow<T7>,
- crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R
+ flow: Flow<T1>,
+ flow2: Flow<T2>,
+ flow3: Flow<T3>,
+ flow4: Flow<T4>,
+ flow5: Flow<T5>,
+ flow6: Flow<T6>,
+ flow7: Flow<T7>,
+ crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R
): Flow<R> {
return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) {
args: Array<*> ->
@Suppress("UNCHECKED_CAST")
transform(
- args[0] as T1,
- args[1] as T2,
- args[2] as T3,
- args[3] as T4,
- args[4] as T5,
- args[5] as T6,
- args[6] as T7
+ args[0] as T1,
+ args[1] as T2,
+ args[2] as T3,
+ args[3] as T4,
+ args[4] as T5,
+ args[5] as T6,
+ args[6] as T7
)
}
}
inline fun <T1, T2, T3, T4, T5, T6, T7, T8, R> combine(
- flow: Flow<T1>,
- flow2: Flow<T2>,
- flow3: Flow<T3>,
- flow4: Flow<T4>,
- flow5: Flow<T5>,
- flow6: Flow<T6>,
- flow7: Flow<T7>,
- flow8: Flow<T8>,
- crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R
+ flow: Flow<T1>,
+ flow2: Flow<T2>,
+ flow3: Flow<T3>,
+ flow4: Flow<T4>,
+ flow5: Flow<T5>,
+ flow6: Flow<T6>,
+ flow7: Flow<T7>,
+ flow8: Flow<T8>,
+ crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R
): Flow<R> {
return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8) {
args: Array<*> ->
@Suppress("UNCHECKED_CAST")
transform(
- args[0] as T1,
- args[1] as T2,
- args[2] as T3,
- args[3] as T4,
- args[4] as T5,
- args[5] as T6,
- args[6] as T7,
- args[7] as T8
+ args[0] as T1,
+ args[1] as T2,
+ args[2] as T3,
+ args[3] as T4,
+ args[4] as T5,
+ args[5] as T6,
+ args[6] as T7,
+ args[7] as T8
)
}
}
inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, R> combine(
- flow: Flow<T1>,
- flow2: Flow<T2>,
- flow3: Flow<T3>,
- flow4: Flow<T4>,
- flow5: Flow<T5>,
- flow6: Flow<T6>,
- flow7: Flow<T7>,
- flow8: Flow<T8>,
- flow9: Flow<T9>,
- crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9) -> R
+ flow: Flow<T1>,
+ flow2: Flow<T2>,
+ flow3: Flow<T3>,
+ flow4: Flow<T4>,
+ flow5: Flow<T5>,
+ flow6: Flow<T6>,
+ flow7: Flow<T7>,
+ flow8: Flow<T8>,
+ flow9: Flow<T9>,
+ crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9) -> R
): Flow<R> {
return kotlinx.coroutines.flow.combine(
- flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8, flow9
+ flow,
+ flow2,
+ flow3,
+ flow4,
+ flow5,
+ flow6,
+ flow7,
+ flow8,
+ flow9
) { args: Array<*> ->
@Suppress("UNCHECKED_CAST")
transform(
- args[0] as T1,
- args[1] as T2,
- args[2] as T3,
- args[3] as T4,
- args[4] as T5,
- args[5] as T6,
- args[6] as T7,
- args[6] as T8,
- args[6] as T9,
+ args[0] as T1,
+ args[1] as T2,
+ args[2] as T3,
+ args[3] as T4,
+ args[4] as T5,
+ args[5] as T6,
+ args[6] as T7,
+ args[6] as T8,
+ args[6] as T9,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/MapUtils.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/MapUtils.kt
new file mode 100644
index 0000000..41cd95b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/MapUtils.kt
@@ -0,0 +1,32 @@
+/*
+ * 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
+
+/** Like [mapValues], but discards `null` values returned from [block]. */
+fun <K, V, R> Map<K, V>.mapValuesNotNull(block: (Map.Entry<K, V>) -> R?): Map<K, R> = buildMap {
+ this@mapValuesNotNull.mapValuesNotNullTo(this, block)
+}
+
+/** Like [mapValuesTo], but discards `null` values returned from [block]. */
+fun <K, V, R, M : MutableMap<in K, in R>> Map<out K, V>.mapValuesNotNullTo(
+ destination: M,
+ block: (Map.Entry<K, V>) -> R?,
+): M {
+ for (entry in this) {
+ block(entry)?.also { destination.put(entry.key, it) }
+ }
+ return destination
+}
diff --git a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt b/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
index ff1d5b2..f49ba64 100644
--- a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
+++ b/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
@@ -43,6 +43,7 @@
import com.android.systemui.statusbar.NotificationShadeDepthController
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
+import com.android.systemui.statusbar.notification.collection.NotifCollection
import com.android.systemui.statusbar.notification.stack.AmbientState
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
@@ -75,6 +76,7 @@
@get:Provides val keyguardSecurityModel: KeyguardSecurityModel = mock(),
@get:Provides val keyguardUpdateMonitor: KeyguardUpdateMonitor = mock(),
@get:Provides val mediaHierarchyManager: MediaHierarchyManager = mock(),
+ @get:Provides val notifCollection: NotifCollection = mock(),
@get:Provides val notificationListener: NotificationListener = mock(),
@get:Provides val notificationLockscreenUserManager: NotificationLockscreenUserManager = mock(),
@get:Provides val notificationMediaManager: NotificationMediaManager = mock(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
index 3412679..2b3fd34 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
@@ -37,8 +37,12 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FakeFeatureFlagsClassic;
+import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
+import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository;
+import com.android.systemui.statusbar.domain.interactor.SilentNotificationStatusIconsVisibilityInteractor;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
@@ -68,9 +72,14 @@
public void setUp() {
MockitoAnnotations.initMocks(this);
+ FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic();
+ featureFlags.setDefault(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR);
mListener = new NotificationListener(
mContext,
+ featureFlags,
mNotificationManager,
+ new SilentNotificationStatusIconsVisibilityInteractor(
+ new NotificationListenerSettingsRepository()),
mFakeSystemClock,
mFakeExecutor,
mPluginManager);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
index 8c5c439..ca8ea4e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
@@ -35,6 +35,7 @@
private val underTest =
RenderNotificationListInteractor(
notifsRepository,
+ sectionStyleProvider = mock(),
)
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
new file mode 100644
index 0000000..9988fc7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ *
+ */
+
+package com.android.systemui.statusbar.notification.icon.domain.interactor
+
+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.coroutines.collectLastValue
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
+import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository
+import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import com.android.systemui.statusbar.notification.shared.byIsAmbient
+import com.android.systemui.statusbar.notification.shared.byIsLastMessageFromReply
+import com.android.systemui.statusbar.notification.shared.byIsPulsing
+import com.android.systemui.statusbar.notification.shared.byIsRowDismissed
+import com.android.systemui.statusbar.notification.shared.byIsSilent
+import com.android.systemui.statusbar.notification.shared.byIsSuppressedFromStatusBar
+import com.android.systemui.statusbar.notification.shared.byKey
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.wm.shell.bubbles.Bubbles
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import java.util.Optional
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NotificationIconsInteractorTest : SysuiTestCase() {
+
+ private val bubbles: Bubbles = mock()
+
+ @Component(modules = [SysUITestModule::class])
+ @SysUISingleton
+ interface TestComponent {
+ val underTest: NotificationIconsInteractor
+
+ val activeNotificationListRepository: ActiveNotificationListRepository
+ val keyguardViewStateRepository: FakeNotificationsKeyguardViewStateRepository
+ val testScope: TestScope
+
+ @Component.Factory
+ interface Factory {
+ fun create(@BindsInstance test: SysuiTestCase, mocks: TestMocksModule): TestComponent
+ }
+ }
+
+ val testComponent: TestComponent =
+ DaggerNotificationIconsInteractorTest_TestComponent.factory()
+ .create(test = this, mocks = TestMocksModule(bubbles = Optional.of(bubbles)))
+
+ @Before
+ fun setup() =
+ with(testComponent) {
+ activeNotificationListRepository.activeNotifications.value =
+ testIcons.associateBy { it.key }
+ }
+
+ @Test
+ fun filteredEntrySet() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.filteredNotifSet())
+ assertThat(filteredSet).containsExactlyElementsIn(testIcons)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_noExpandedBubbles() =
+ with(testComponent) {
+ testScope.runTest {
+ whenever(bubbles.isBubbleExpanded(eq("notif1"))).thenReturn(true)
+ val filteredSet by collectLastValue(underTest.filteredNotifSet())
+ assertThat(filteredSet).comparingElementsUsing(byKey).doesNotContain("notif1")
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_noAmbient() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.filteredNotifSet(showAmbient = false))
+ assertThat(filteredSet).comparingElementsUsing(byIsAmbient).doesNotContain(true)
+ assertThat(filteredSet)
+ .comparingElementsUsing(byIsSuppressedFromStatusBar)
+ .doesNotContain(true)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_noLowPriority() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by
+ collectLastValue(underTest.filteredNotifSet(showLowPriority = false))
+ assertThat(filteredSet).comparingElementsUsing(byIsSilent).doesNotContain(true)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_noDismissed() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by
+ collectLastValue(underTest.filteredNotifSet(showDismissed = false))
+ assertThat(filteredSet)
+ .comparingElementsUsing(byIsRowDismissed)
+ .doesNotContain(true)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_noRepliedMessages() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by
+ collectLastValue(underTest.filteredNotifSet(showRepliedMessages = false))
+ assertThat(filteredSet)
+ .comparingElementsUsing(byIsLastMessageFromReply)
+ .doesNotContain(true)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_noPulsing_notifsNotFullyHidden() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.filteredNotifSet(showPulsing = false))
+ keyguardViewStateRepository.setNotificationsFullyHidden(false)
+ assertThat(filteredSet).comparingElementsUsing(byIsPulsing).doesNotContain(true)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_noPulsing_notifsFullyHidden() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.filteredNotifSet(showPulsing = false))
+ keyguardViewStateRepository.setNotificationsFullyHidden(true)
+ assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true)
+ }
+ }
+}
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() {
+
+ private val bubbles: Bubbles = mock()
+
+ @Component(modules = [SysUITestModule::class])
+ @SysUISingleton
+ interface TestComponent {
+ val underTest: AlwaysOnDisplayNotificationIconsInteractor
+
+ val activeNotificationListRepository: ActiveNotificationListRepository
+ val deviceEntryRepository: FakeDeviceEntryRepository
+ val keyguardViewStateRepository: FakeNotificationsKeyguardViewStateRepository
+ val testScope: TestScope
+
+ @Component.Factory
+ interface Factory {
+ fun create(@BindsInstance test: SysuiTestCase, mocks: TestMocksModule): TestComponent
+ }
+ }
+
+ val testComponent: TestComponent =
+ DaggerAlwaysOnDisplayNotificationIconsInteractorTest_TestComponent.factory()
+ .create(test = this, mocks = TestMocksModule(bubbles = Optional.of(bubbles)))
+
+ @Before
+ fun setup() =
+ with(testComponent) {
+ activeNotificationListRepository.activeNotifications.value =
+ testIcons.associateBy { it.key }
+ }
+
+ @Test
+ fun filteredEntrySet_noExpandedBubbles() =
+ with(testComponent) {
+ testScope.runTest {
+ whenever(bubbles.isBubbleExpanded(eq("notif1"))).thenReturn(true)
+ val filteredSet by collectLastValue(underTest.aodNotifs)
+ assertThat(filteredSet).comparingElementsUsing(byKey).doesNotContain("notif1")
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_noAmbient() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.aodNotifs)
+ assertThat(filteredSet).comparingElementsUsing(byIsAmbient).doesNotContain(true)
+ assertThat(filteredSet)
+ .comparingElementsUsing(byIsSuppressedFromStatusBar)
+ .doesNotContain(true)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_noDismissed() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.aodNotifs)
+ assertThat(filteredSet)
+ .comparingElementsUsing(byIsRowDismissed)
+ .doesNotContain(true)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_noRepliedMessages() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.aodNotifs)
+ assertThat(filteredSet)
+ .comparingElementsUsing(byIsLastMessageFromReply)
+ .doesNotContain(true)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_showPulsing_notifsNotFullyHidden_bypassDisabled() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.aodNotifs)
+ deviceEntryRepository.setBypassEnabled(false)
+ keyguardViewStateRepository.setNotificationsFullyHidden(false)
+ assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_showPulsing_notifsFullyHidden_bypassDisabled() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.aodNotifs)
+ deviceEntryRepository.setBypassEnabled(false)
+ keyguardViewStateRepository.setNotificationsFullyHidden(true)
+ assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_noPulsing_notifsNotFullyHidden_bypassEnabled() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.aodNotifs)
+ deviceEntryRepository.setBypassEnabled(true)
+ keyguardViewStateRepository.setNotificationsFullyHidden(false)
+ assertThat(filteredSet).comparingElementsUsing(byIsPulsing).doesNotContain(true)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_showPulsing_notifsFullyHidden_bypassEnabled() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.aodNotifs)
+ deviceEntryRepository.setBypassEnabled(true)
+ keyguardViewStateRepository.setNotificationsFullyHidden(true)
+ assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true)
+ }
+ }
+}
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class StatusBarNotificationIconsInteractorTest : SysuiTestCase() {
+
+ private val bubbles: Bubbles = mock()
+
+ @Component(modules = [SysUITestModule::class])
+ @SysUISingleton
+ interface TestComponent {
+ val underTest: StatusBarNotificationIconsInteractor
+
+ val activeNotificationListRepository: ActiveNotificationListRepository
+ val keyguardViewStateRepository: FakeNotificationsKeyguardViewStateRepository
+ val notificationListenerSettingsRepository: NotificationListenerSettingsRepository
+ val testScope: TestScope
+
+ @Component.Factory
+ interface Factory {
+ fun create(@BindsInstance test: SysuiTestCase, mocks: TestMocksModule): TestComponent
+ }
+ }
+
+ val testComponent: TestComponent =
+ DaggerStatusBarNotificationIconsInteractorTest_TestComponent.factory()
+ .create(test = this, mocks = TestMocksModule(bubbles = Optional.of(bubbles)))
+
+ @Before
+ fun setup() =
+ with(testComponent) {
+ activeNotificationListRepository.activeNotifications.value =
+ testIcons.associateBy { it.key }
+ }
+
+ @Test
+ fun filteredEntrySet_noExpandedBubbles() =
+ with(testComponent) {
+ testScope.runTest {
+ whenever(bubbles.isBubbleExpanded(eq("notif1"))).thenReturn(true)
+ val filteredSet by collectLastValue(underTest.statusBarNotifs)
+ assertThat(filteredSet).comparingElementsUsing(byKey).doesNotContain("notif1")
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_noAmbient() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.statusBarNotifs)
+ assertThat(filteredSet).comparingElementsUsing(byIsAmbient).doesNotContain(true)
+ assertThat(filteredSet)
+ .comparingElementsUsing(byIsSuppressedFromStatusBar)
+ .doesNotContain(true)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_noLowPriority_whenDontShowSilentIcons() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.statusBarNotifs)
+ notificationListenerSettingsRepository.showSilentStatusIcons.value = false
+ assertThat(filteredSet).comparingElementsUsing(byIsSilent).doesNotContain(true)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_showLowPriority_whenShowSilentIcons() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.statusBarNotifs)
+ notificationListenerSettingsRepository.showSilentStatusIcons.value = true
+ assertThat(filteredSet).comparingElementsUsing(byIsSilent).contains(true)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_noDismissed() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.statusBarNotifs)
+ assertThat(filteredSet)
+ .comparingElementsUsing(byIsRowDismissed)
+ .doesNotContain(true)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_noRepliedMessages() =
+ with(testComponent) {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.statusBarNotifs)
+ assertThat(filteredSet)
+ .comparingElementsUsing(byIsLastMessageFromReply)
+ .doesNotContain(true)
+ }
+ }
+}
+
+private val testIcons =
+ listOf(
+ ActiveNotificationModel(
+ key = "notif1",
+ ),
+ ActiveNotificationModel(
+ key = "notif2",
+ isAmbient = true,
+ ),
+ ActiveNotificationModel(
+ key = "notif3",
+ isRowDismissed = true,
+ ),
+ ActiveNotificationModel(
+ key = "notif4",
+ isSilent = true,
+ ),
+ ActiveNotificationModel(
+ key = "notif5",
+ isLastMessageFromReply = true,
+ ),
+ ActiveNotificationModel(
+ key = "notif6",
+ isSuppressedFromStatusBar = true,
+ ),
+ ActiveNotificationModel(
+ key = "notif7",
+ isPulsing = true,
+ ),
+ )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt
deleted file mode 100644
index e57986d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt
+++ /dev/null
@@ -1,106 +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.statusbar.notification.icon.ui.viewbinder
-
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper.RunWithLooper
-import androidx.test.filters.SmallTest
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FakeFeatureFlagsClassicModule
-import com.android.systemui.flags.Flags
-import com.android.systemui.statusbar.phone.DozeParameters
-import com.android.systemui.user.domain.UserDomainLayerModule
-import dagger.BindsInstance
-import dagger.Component
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@RunWithLooper(setAsMainLooper = true)
-class NotificationIconAreaControllerViewBinderWrapperImplTest : SysuiTestCase() {
-
- @Mock private lateinit var dozeParams: DozeParameters
-
- private lateinit var testComponent: TestComponent
- private val underTest
- get() = testComponent.underTest
-
- @Before
- fun setup() {
- MockitoAnnotations.initMocks(this)
- allowTestableLooperAsMainThread()
-
- testComponent =
- DaggerNotificationIconAreaControllerViewBinderWrapperImplTest_TestComponent.factory()
- .create(
- test = this,
- featureFlags =
- FakeFeatureFlagsClassicModule {
- set(Flags.FACE_AUTH_REFACTOR, value = false)
- set(Flags.MIGRATE_KEYGUARD_STATUS_VIEW, value = false)
- },
- mocks =
- TestMocksModule(
- dozeParameters = dozeParams,
- ),
- )
- }
-
- @Test
- fun testNotificationIcons_settingHideIcons() {
- underTest.settingsListener.onStatusBarIconsBehaviorChanged(true)
- assertFalse(underTest.shouldShowLowPriorityIcons())
- }
-
- @Test
- fun testNotificationIcons_settingShowIcons() {
- underTest.settingsListener.onStatusBarIconsBehaviorChanged(false)
- assertTrue(underTest.shouldShowLowPriorityIcons())
- }
-
- @SysUISingleton
- @Component(
- modules =
- [
- SysUITestModule::class,
- BiometricsDomainLayerModule::class,
- UserDomainLayerModule::class,
- ]
- )
- interface TestComponent {
-
- val underTest: NotificationIconAreaControllerViewBinderWrapperImpl
-
- @Component.Factory
- interface Factory {
- fun create(
- @BindsInstance test: SysuiTestCase,
- mocks: TestMocksModule,
- featureFlags: FakeFeatureFlagsClassicModule,
- ): TestComponent
- }
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
index ed94058..0eac1ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
@@ -19,3 +19,45 @@
val byKey: Correspondence<ActiveNotificationModel, String> =
Correspondence.transforming({ it?.key }, "has a key of")
+val byIsAmbient: Correspondence<ActiveNotificationModel, Boolean> =
+ Correspondence.transforming({ it?.isAmbient }, "has an isAmbient value of")
+val byIsSuppressedFromStatusBar: Correspondence<ActiveNotificationModel, Boolean> =
+ Correspondence.transforming(
+ { it?.isSuppressedFromStatusBar },
+ "has an isSuppressedFromStatusBar value of",
+ )
+val byIsSilent: Correspondence<ActiveNotificationModel, Boolean> =
+ Correspondence.transforming({ it?.isSilent }, "has an isSilent value of")
+val byIsRowDismissed: Correspondence<ActiveNotificationModel, Boolean> =
+ Correspondence.transforming({ it?.isRowDismissed }, "has an isRowDismissed value of")
+val byIsLastMessageFromReply: Correspondence<ActiveNotificationModel, Boolean> =
+ Correspondence.transforming(
+ { it?.isLastMessageFromReply },
+ "has an isLastMessageFromReply value of"
+ )
+val byIsPulsing: Correspondence<ActiveNotificationModel, Boolean> =
+ Correspondence.transforming({ it?.isPulsing }, "has an isPulsing value of")
+
+@Suppress("TestFunctionName")
+fun ActiveNotificationModel(
+ key: String,
+ isAmbient: Boolean = false,
+ isRowDismissed: Boolean = false,
+ isSilent: Boolean = false,
+ isLastMessageFromReply: Boolean = false,
+ isSuppressedFromStatusBar: Boolean = false,
+ isPulsing: Boolean = false,
+) =
+ ActiveNotificationModel(
+ key = key,
+ groupKey = null,
+ isAmbient = isAmbient,
+ isRowDismissed = isRowDismissed,
+ isSilent = isSilent,
+ isLastMessageFromReply = isLastMessageFromReply,
+ isSuppressedFromStatusBar = isSuppressedFromStatusBar,
+ isPulsing = isPulsing,
+ aodIcon = null,
+ shelfIcon = null,
+ statusBarIcon = null,
+ )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
index 593c587..472709c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
@@ -42,6 +42,8 @@
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.doze.DozeHost;
import com.android.systemui.doze.DozeLog;
+import com.android.systemui.flags.FakeFeatureFlagsClassic;
+import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.keyguard.domain.interactor.DozeInteractor;
import com.android.systemui.shade.NotificationShadeWindowViewController;
@@ -96,18 +98,21 @@
@Mock private BiometricUnlockController mBiometricUnlockController;
@Mock private AuthController mAuthController;
@Mock private DozeHost.Callback mCallback;
-
@Mock private DozeInteractor mDozeInteractor;
+
+ private final FakeFeatureFlagsClassic mFeatureFlags = new FakeFeatureFlagsClassic();
+
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
+ mFeatureFlags.setDefault(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR);
mDozeServiceHost = new DozeServiceHost(mDozeLog, mPowerManager, mWakefullnessLifecycle,
- mStatusBarStateController, mDeviceProvisionedController, mHeadsUpManager,
- mBatteryController, mScrimController, () -> mBiometricUnlockController,
- () -> mAssistManager, mDozeScrimController,
- mKeyguardUpdateMonitor, mPulseExpansionHandler,
- mNotificationShadeWindowController, mNotificationWakeUpCoordinator,
- mAuthController, mNotificationIconAreaController, mDozeInteractor);
+ mStatusBarStateController, mDeviceProvisionedController, mFeatureFlags,
+ mHeadsUpManager, mBatteryController, mScrimController,
+ () -> mBiometricUnlockController, () -> mAssistManager, mDozeScrimController,
+ mKeyguardUpdateMonitor, mPulseExpansionHandler, mNotificationShadeWindowController,
+ mNotificationWakeUpCoordinator, mAuthController, mNotificationIconAreaController,
+ mDozeInteractor);
mDozeServiceHost.initialize(
mCentralSurfaces,