summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/NoOpCoreStartable.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStatsLoggerModule.kt148
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java50
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt59
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt46
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java41
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java23
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java26
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java73
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationRowStatsLogger.kt26
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLogger.kt44
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt326
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStatsLoggerBinder.kt82
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModel.kt44
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt12
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt107
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt429
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModelTest.kt147
-rw-r--r--packages/SystemUI/tests/utils/src/android/service/notification/NotificationListenerServiceKosmos.kt24
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt12
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifCollectionKosmos.kt (renamed from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt)6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt11
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt13
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.kt24
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt35
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListLoggerViewModelKosmos.kt30
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt6
43 files changed, 1856 insertions, 128 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/NoOpCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/NoOpCoreStartable.kt
new file mode 100644
index 000000000000..9f54c26b5151
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/NoOpCoreStartable.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui
+
+/** A [CoreStartable] that does nothing. */
+class NoOpCoreStartable : CoreStartable {
+ override fun start() {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStatsLoggerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStatsLoggerModule.kt
new file mode 100644
index 000000000000..cbd988720b94
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStatsLoggerModule.kt
@@ -0,0 +1,148 @@
+/*
+ * 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.dagger
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.NoOpCoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.UiBackground
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor
+import com.android.systemui.statusbar.NotificationListener
+import com.android.systemui.statusbar.notification.collection.NotifLiveDataStore
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider
+import com.android.systemui.statusbar.notification.logging.NotificationLogger
+import com.android.systemui.statusbar.notification.logging.NotificationLogger.ExpansionStateLogger
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationRowStatsLogger
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLoggerImpl
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLoggerViewModel
+import com.android.systemui.util.kotlin.JavaAdapter
+import com.android.systemui.util.kotlin.getOrNull
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import java.util.Optional
+import java.util.concurrent.Executor
+import javax.inject.Provider
+
+@Module
+interface NotificationStatsLoggerModule {
+
+ /** Binds an implementation to the [NotificationStatsLogger]. */
+ @Binds fun bindsStatsLoggerImpl(impl: NotificationStatsLoggerImpl): NotificationStatsLogger
+
+ companion object {
+
+ /** Provides a [NotificationStatsLogger] if the refactor flag is on. */
+ @Provides
+ fun provideStatsLogger(
+ provider: Provider<NotificationStatsLogger>
+ ): Optional<NotificationStatsLogger> {
+ return if (NotificationsLiveDataStoreRefactor.isEnabled) {
+ Optional.of(provider.get())
+ } else {
+ Optional.empty()
+ }
+ }
+
+ /** Provides a [NotificationLoggerViewModel] if the refactor flag is on. */
+ @Provides
+ fun provideViewModel(
+ provider: Provider<NotificationLoggerViewModel>
+ ): Optional<NotificationLoggerViewModel> {
+ return if (NotificationsLiveDataStoreRefactor.isEnabled) {
+ Optional.of(provider.get())
+ } else {
+ Optional.empty()
+ }
+ }
+
+ /** Provides the legacy [NotificationLogger] if the refactor flag is off. */
+ @Provides
+ @SysUISingleton
+ fun provideLegacyLoggerOptional(
+ notificationListener: NotificationListener?,
+ @UiBackground uiBgExecutor: Executor?,
+ notifLiveDataStore: NotifLiveDataStore?,
+ visibilityProvider: NotificationVisibilityProvider?,
+ notifPipeline: NotifPipeline?,
+ statusBarStateController: StatusBarStateController?,
+ windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor?,
+ javaAdapter: JavaAdapter?,
+ expansionStateLogger: ExpansionStateLogger?,
+ notificationPanelLogger: NotificationPanelLogger?
+ ): Optional<NotificationLogger> {
+ return if (NotificationsLiveDataStoreRefactor.isEnabled) {
+ Optional.empty()
+ } else {
+ Optional.of(
+ NotificationLogger(
+ notificationListener,
+ uiBgExecutor,
+ notifLiveDataStore,
+ visibilityProvider,
+ notifPipeline,
+ statusBarStateController,
+ windowRootViewVisibilityInteractor,
+ javaAdapter,
+ expansionStateLogger,
+ notificationPanelLogger
+ )
+ )
+ }
+ }
+
+ /**
+ * Provides a the legacy [NotificationLogger] or the new [NotificationStatsLogger] to the
+ * notification row.
+ *
+ * TODO(b/308623704) remove the [NotificationRowStatsLogger] interface, and provide a
+ * [NotificationStatsLogger] to the row directly.
+ */
+ @Provides
+ fun provideRowStatsLogger(
+ newProvider: Provider<NotificationStatsLogger>,
+ legacyLoggerOptional: Optional<NotificationLogger>,
+ ): NotificationRowStatsLogger {
+ return legacyLoggerOptional.getOrNull() ?: newProvider.get()
+ }
+
+ /**
+ * Binds the legacy [NotificationLogger] as a [CoreStartable] if the feature flag is off, or
+ * binds a no-op [CoreStartable] otherwise.
+ *
+ * The old [NotificationLogger] is a [CoreStartable], because it's managing its own data
+ * updates, but the new [NotificationStatsLogger] is not. Currently Dagger doesn't support
+ * optionally binding entries with @[IntoMap], therefore we provide a no-op [CoreStartable]
+ * here if the feature flag is on, but this can be removed once the flag is released.
+ */
+ @Provides
+ @IntoMap
+ @ClassKey(NotificationLogger::class)
+ fun provideCoreStartable(
+ legacyLoggerOptional: Optional<NotificationLogger>
+ ): CoreStartable {
+ return legacyLoggerOptional.getOrNull() ?: NoOpCoreStartable()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index 3a722050dab2..6bba72b2cd49 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -17,14 +17,12 @@
package com.android.systemui.statusbar.notification.dagger;
import android.content.Context;
+import android.service.notification.NotificationListenerService;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.UiBackground;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.res.R;
-import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.notification.NotificationActivityStarter;
import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider;
@@ -67,7 +65,6 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider;
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProviderImpl;
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor;
-import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger;
import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerImpl;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -80,7 +77,8 @@ import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.StatusBarNotificationActivityStarter;
import com.android.systemui.statusbar.policy.HeadsUpManager;
-import com.android.systemui.util.kotlin.JavaAdapter;
+
+import javax.inject.Provider;
import dagger.Binds;
import dagger.Module;
@@ -88,10 +86,6 @@ import dagger.Provides;
import dagger.multibindings.ClassKey;
import dagger.multibindings.IntoMap;
-import java.util.concurrent.Executor;
-
-import javax.inject.Provider;
-
/**
* Dagger Module for classes found within the com.android.systemui.statusbar.notification package.
*/
@@ -104,6 +98,7 @@ import javax.inject.Provider;
NotificationSectionHeadersModule.class,
ActivatableNotificationViewModelModule.class,
NotificationMemoryModule.class,
+ NotificationStatsLoggerModule.class,
})
public interface NotificationsModule {
@Binds
@@ -128,39 +123,6 @@ public interface NotificationsModule {
VisibilityLocationProvider bindVisibilityLocationProvider(
VisibilityLocationProviderDelegator visibilityLocationProviderDelegator);
- /** Provides an instance of {@link NotificationLogger} */
- @SysUISingleton
- @Provides
- static NotificationLogger provideNotificationLogger(
- NotificationListener notificationListener,
- @UiBackground Executor uiBgExecutor,
- NotifLiveDataStore notifLiveDataStore,
- NotificationVisibilityProvider visibilityProvider,
- NotifPipeline notifPipeline,
- StatusBarStateController statusBarStateController,
- WindowRootViewVisibilityInteractor windowRootViewVisibilityInteractor,
- JavaAdapter javaAdapter,
- NotificationLogger.ExpansionStateLogger expansionStateLogger,
- NotificationPanelLogger notificationPanelLogger) {
- return new NotificationLogger(
- notificationListener,
- uiBgExecutor,
- notifLiveDataStore,
- visibilityProvider,
- notifPipeline,
- statusBarStateController,
- windowRootViewVisibilityInteractor,
- javaAdapter,
- expansionStateLogger,
- notificationPanelLogger);
- }
-
- /** Binds {@link NotificationLogger} as a {@link CoreStartable}. */
- @Binds
- @IntoMap
- @ClassKey(NotificationLogger.class)
- CoreStartable bindsNotificationLogger(NotificationLogger notificationLogger);
-
/** Provides an instance of {@link NotificationPanelLogger} */
@SysUISingleton
@Provides
@@ -272,6 +234,10 @@ public interface NotificationsModule {
NotifLiveDataStore bindNotifLiveDataStore(NotifLiveDataStoreImpl notifLiveDataStoreImpl);
/** */
+ @Binds
+ NotificationListenerService bindNotificationListener(NotificationListener notificationListener);
+
+ /** */
@Provides
@SysUISingleton
static VisualInterruptionDecisionProvider provideVisualInterruptionDecisionProvider(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
index 5ed82cc1ed5c..5c844bcf749c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
@@ -55,6 +55,12 @@ data class ActiveNotificationsStore(
* invoking [get].
*/
val renderList: List<Key> = emptyList(),
+
+ /**
+ * Map of notification key to rank, where rank is the 0-based index of the notification on the
+ * system server, meaning that in the unfiltered flattened list of notification entries.
+ */
+ val rankingsMap: Map<String, Int> = emptyMap()
) {
operator fun get(key: Key): ActiveNotificationEntryModel? {
return when (key) {
@@ -74,8 +80,9 @@ data class ActiveNotificationsStore(
private val groups = mutableMapOf<String, ActiveNotificationGroupModel>()
private val individuals = mutableMapOf<String, ActiveNotificationModel>()
private val renderList = mutableListOf<Key>()
+ private var rankingsMap: Map<String, Int> = emptyMap()
- fun build() = ActiveNotificationsStore(groups, individuals, renderList)
+ fun build() = ActiveNotificationsStore(groups, individuals, renderList, rankingsMap)
fun addEntry(entry: ActiveNotificationEntryModel) {
when (entry) {
@@ -95,5 +102,9 @@ data class ActiveNotificationsStore(
individuals[group.summary.key] = group.summary
group.children.forEach { individuals[it.key] = it }
}
+
+ fun setRankingsMap(map: Map<String, Int>) {
+ rankingsMap = map.toMap()
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
index e90ddf98db00..ca6344d07df8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
@@ -33,7 +33,10 @@ constructor(
private val repository: ActiveNotificationListRepository,
@Background private val backgroundDispatcher: CoroutineDispatcher,
) {
- /** Notifications actively presented to the user in the notification stack, in order. */
+ /**
+ * Top level list of Notifications actively presented to the user in the notification stack, in
+ * order.
+ */
val topLevelRepresentativeNotifications: Flow<List<ActiveNotificationModel>> =
repository.activeNotifications
.map { store ->
@@ -51,6 +54,13 @@ constructor(
}
.flowOn(backgroundDispatcher)
+ /**
+ * Flattened list of Notifications actively presented to the user in the notification stack, in
+ * order.
+ */
+ val allRepresentativeNotifications: Flow<Map<String, ActiveNotificationModel>> =
+ repository.activeNotifications.map { store -> store.individuals }
+
/** Are any notifications being actively presented in the notification stack? */
val areAnyNotificationsPresent: Flow<Boolean> =
repository.activeNotifications
@@ -65,6 +75,16 @@ constructor(
val areAnyNotificationsPresentValue: Boolean
get() = repository.activeNotifications.value.renderList.isNotEmpty()
+ /**
+ * Map of notification key to rank, where rank is the 0-based index of the notification in the
+ * system server, meaning that in the unfiltered flattened list of notification entries. Used
+ * for logging purposes.
+ *
+ * @see [com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger].
+ */
+ val activeNotificationRanks: Flow<Map<String, Int>> =
+ repository.activeNotifications.map { store -> store.rankingsMap }
+
/** Are there are any notifications that can be cleared by the "Clear all" button? */
val hasClearableNotifications: Flow<Boolean> =
repository.notifStats
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 6f4ed9db20b1..695f21569f3c 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
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.domain.interactor
import android.graphics.drawable.Icon
+import android.util.ArrayMap
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -46,6 +47,7 @@ constructor(
repository.activeNotifications.update { existingModels ->
buildActiveNotificationsStore(existingModels, sectionStyleProvider) {
entries.forEach(::addListEntry)
+ setRankingsMap(entries)
}
}
}
@@ -94,6 +96,27 @@ private class ActiveNotificationsStoreBuilder(
}
}
+ fun setRankingsMap(entries: List<ListEntry>) {
+ builder.setRankingsMap(flatMapToRankingsMap(entries))
+ }
+
+ fun flatMapToRankingsMap(entries: List<ListEntry>): Map<String, Int> {
+ val result = ArrayMap<String, Int>()
+ for (entry in entries) {
+ if (entry is NotificationEntry) {
+ entry.representativeEntry?.let { representativeEntry ->
+ result[representativeEntry.key] = representativeEntry.ranking.rank
+ }
+ } else if (entry is GroupEntry) {
+ entry.summary?.let { summary -> result[summary.key] = summary.ranking.rank }
+ for (child in entry.children) {
+ result[child.key] = child.ranking.rank
+ }
+ }
+ }
+ return result
+ }
+
private fun NotificationEntry.toModel(): ActiveNotificationModel =
existingModels.createOrReuse(
key = key,
@@ -107,6 +130,11 @@ private class ActiveNotificationsStoreBuilder(
aodIcon = icons.aodIcon?.sourceIcon,
shelfIcon = icons.shelfIcon?.sourceIcon,
statusBarIcon = icons.statusBarIcon?.sourceIcon,
+ uid = sbn.uid,
+ packageName = sbn.packageName,
+ instanceId = sbn.instanceId?.id,
+ isGroupSummary = sbn.notification.isGroupSummary,
+ bucket = bucket,
)
}
@@ -121,7 +149,12 @@ private fun ActiveNotificationsStore.createOrReuse(
isPulsing: Boolean,
aodIcon: Icon?,
shelfIcon: Icon?,
- statusBarIcon: Icon?
+ statusBarIcon: Icon?,
+ uid: Int,
+ packageName: String,
+ instanceId: Int?,
+ isGroupSummary: Boolean,
+ bucket: Int,
): ActiveNotificationModel {
return individuals[key]?.takeIf {
it.isCurrent(
@@ -135,7 +168,12 @@ private fun ActiveNotificationsStore.createOrReuse(
isPulsing = isPulsing,
aodIcon = aodIcon,
shelfIcon = shelfIcon,
- statusBarIcon = statusBarIcon
+ statusBarIcon = statusBarIcon,
+ uid = uid,
+ instanceId = instanceId,
+ isGroupSummary = isGroupSummary,
+ packageName = packageName,
+ bucket = bucket,
)
}
?: ActiveNotificationModel(
@@ -150,6 +188,11 @@ private fun ActiveNotificationsStore.createOrReuse(
aodIcon = aodIcon,
shelfIcon = shelfIcon,
statusBarIcon = statusBarIcon,
+ uid = uid,
+ instanceId = instanceId,
+ isGroupSummary = isGroupSummary,
+ packageName = packageName,
+ bucket = bucket,
)
}
@@ -164,7 +207,12 @@ private fun ActiveNotificationModel.isCurrent(
isPulsing: Boolean,
aodIcon: Icon?,
shelfIcon: Icon?,
- statusBarIcon: Icon?
+ statusBarIcon: Icon?,
+ uid: Int,
+ packageName: String,
+ instanceId: Int?,
+ isGroupSummary: Boolean,
+ bucket: Int,
): Boolean {
return when {
key != this.key -> false
@@ -178,6 +226,11 @@ private fun ActiveNotificationModel.isCurrent(
aodIcon != this.aodIcon -> false
shelfIcon != this.shelfIcon -> false
statusBarIcon != this.statusBarIcon -> false
+ uid != this.uid -> false
+ instanceId != this.instanceId -> false
+ isGroupSummary != this.isGroupSummary -> false
+ packageName != this.packageName -> false
+ bucket != this.bucket -> false
else -> true
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
index 7d1cca81e614..ac499601b962 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
@@ -39,6 +39,7 @@ import com.android.systemui.statusbar.notification.collection.render.NotifStackC
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
import com.android.systemui.statusbar.notification.logging.NotificationLogger
import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
import com.android.wm.shell.bubbles.Bubbles
import dagger.Lazy
@@ -53,7 +54,9 @@ import javax.inject.Inject
* any initialization work that notifications require.
*/
@SysUISingleton
-class NotificationsControllerImpl @Inject constructor(
+class NotificationsControllerImpl
+@Inject
+constructor(
private val notificationListener: NotificationListener,
private val commonNotifCollection: Lazy<CommonNotifCollection>,
private val notifPipeline: Lazy<NotifPipeline>,
@@ -61,7 +64,7 @@ class NotificationsControllerImpl @Inject constructor(
private val targetSdkResolver: TargetSdkResolver,
private val notifPipelineInitializer: Lazy<NotifPipelineInitializer>,
private val notifBindPipelineInitializer: NotifBindPipelineInitializer,
- private val notificationLogger: NotificationLogger,
+ private val notificationLoggerOptional: Optional<NotificationLogger>,
private val notificationRowBinder: NotificationRowBinderImpl,
private val notificationsMediaManager: NotificationMediaManager,
private val headsUpViewBinder: HeadsUpViewBinder,
@@ -80,28 +83,35 @@ class NotificationsControllerImpl @Inject constructor(
) {
notificationListener.registerAsSystemService()
- notifPipeline.get().addCollectionListener(object : NotifCollectionListener {
- override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
- listContainer.cleanUpViewStateForEntry(entry)
- }
- })
+ notifPipeline
+ .get()
+ .addCollectionListener(
+ object : NotifCollectionListener {
+ override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+ listContainer.cleanUpViewStateForEntry(entry)
+ }
+ }
+ )
notificationRowBinder.setNotificationClicker(
- clickerBuilder.build(bubblesOptional, notificationActivityStarter))
+ clickerBuilder.build(bubblesOptional, notificationActivityStarter)
+ )
notificationRowBinder.setUpWithPresenter(presenter, listContainer)
headsUpViewBinder.setPresenter(presenter)
notifBindPipelineInitializer.initialize()
animatedImageNotificationManager.bind()
- notifPipelineInitializer.get().initialize(
- notificationListener,
- notificationRowBinder,
- listContainer,
- stackController)
+ notifPipelineInitializer
+ .get()
+ .initialize(notificationListener, notificationRowBinder, listContainer, stackController)
targetSdkResolver.initialize(notifPipeline.get())
notificationsMediaManager.setUpWithPresenter(presenter)
- notificationLogger.setUpWithContainer(listContainer)
+ if (!NotificationsLiveDataStoreRefactor.isEnabled) {
+ notificationLoggerOptional.ifPresent { logger ->
+ logger.setUpWithContainer(listContainer)
+ }
+ }
peopleSpaceWidgetManager.attach(notificationListener)
}
@@ -120,11 +130,11 @@ class NotificationsControllerImpl @Inject constructor(
notificationListener.snoozeNotification(sbn.key, snoozeOption.snoozeCriterion.id)
} else {
notificationListener.snoozeNotification(
- sbn.key,
- snoozeOption.minutesToSnoozeFor * 60 * 1000.toLong())
+ sbn.key,
+ snoozeOption.minutesToSnoozeFor * 60 * 1000.toLong()
+ )
}
}
- override fun getActiveNotificationsCount(): Int =
- notifLiveDataStore.activeNotifCount.value
+ override fun getActiveNotificationsCount(): Int = notifLiveDataStore.activeNotifCount.value
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
index 8d2a63e9b3fa..4349b3b5aeb3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
@@ -33,6 +33,7 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.NotificationVisibility;
+import com.android.internal.statusbar.NotificationVisibility.NotificationLocation;
import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.qualifiers.UiBackground;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -46,8 +47,10 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.dagger.NotificationsModule;
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationRowStatsLogger;
import com.android.systemui.util.Compile;
import com.android.systemui.util.kotlin.JavaAdapter;
@@ -64,7 +67,8 @@ import javax.inject.Inject;
* Handles notification logging, in particular, logging which notifications are visible and which
* are not.
*/
-public class NotificationLogger implements StateListener, CoreStartable {
+public class NotificationLogger implements StateListener, CoreStartable,
+ NotificationRowStatsLogger {
static final String TAG = "NotificationLogger";
private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
@@ -166,31 +170,31 @@ public class NotificationLogger implements StateListener, CoreStartable {
/**
* Returns the location of the notification referenced by the given {@link NotificationEntry}.
*/
- public static NotificationVisibility.NotificationLocation getNotificationLocation(
+ public static NotificationLocation getNotificationLocation(
NotificationEntry entry) {
if (entry == null || entry.getRow() == null || entry.getRow().getViewState() == null) {
- return NotificationVisibility.NotificationLocation.LOCATION_UNKNOWN;
+ return NotificationLocation.LOCATION_UNKNOWN;
}
return convertNotificationLocation(entry.getRow().getViewState().location);
}
- private static NotificationVisibility.NotificationLocation convertNotificationLocation(
+ private static NotificationLocation convertNotificationLocation(
int location) {
switch (location) {
case ExpandableViewState.LOCATION_FIRST_HUN:
- return NotificationVisibility.NotificationLocation.LOCATION_FIRST_HEADS_UP;
+ return NotificationLocation.LOCATION_FIRST_HEADS_UP;
case ExpandableViewState.LOCATION_HIDDEN_TOP:
- return NotificationVisibility.NotificationLocation.LOCATION_HIDDEN_TOP;
+ return NotificationLocation.LOCATION_HIDDEN_TOP;
case ExpandableViewState.LOCATION_MAIN_AREA:
- return NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA;
+ return NotificationLocation.LOCATION_MAIN_AREA;
case ExpandableViewState.LOCATION_BOTTOM_STACK_PEEKING:
- return NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_PEEKING;
+ return NotificationLocation.LOCATION_BOTTOM_STACK_PEEKING;
case ExpandableViewState.LOCATION_BOTTOM_STACK_HIDDEN:
- return NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_HIDDEN;
+ return NotificationLocation.LOCATION_BOTTOM_STACK_HIDDEN;
case ExpandableViewState.LOCATION_GONE:
- return NotificationVisibility.NotificationLocation.LOCATION_GONE;
+ return NotificationLocation.LOCATION_GONE;
default:
- return NotificationVisibility.NotificationLocation.LOCATION_UNKNOWN;
+ return NotificationLocation.LOCATION_UNKNOWN;
}
}
@@ -207,6 +211,9 @@ public class NotificationLogger implements StateListener, CoreStartable {
JavaAdapter javaAdapter,
ExpansionStateLogger expansionStateLogger,
NotificationPanelLogger notificationPanelLogger) {
+ // Not expected to be constructed if the feature flag is on
+ NotificationsLiveDataStoreRefactor.assertInLegacyMode();
+
mNotificationListener = notificationListener;
mUiBgExecutor = uiBgExecutor;
mNotifLiveDataStore = notifLiveDataStore;
@@ -382,9 +389,11 @@ public class NotificationLogger implements StateListener, CoreStartable {
/**
* Called when the notification is expanded / collapsed.
*/
- public void onExpansionChanged(String key, boolean isUserAction, boolean isExpanded) {
- NotificationVisibility.NotificationLocation location = mVisibilityProvider.getLocation(key);
- mExpansionStateLogger.onExpansionChanged(key, isUserAction, isExpanded, location);
+ @Override
+ public void onNotificationExpansionChanged(@NonNull String key, boolean isExpanded,
+ int location, boolean isUserAction) {
+ NotificationLocation notifLocation = mVisibilityProvider.getLocation(key);
+ mExpansionStateLogger.onExpansionChanged(key, isUserAction, isExpanded, notifLocation);
}
@VisibleForTesting
@@ -440,7 +449,7 @@ public class NotificationLogger implements StateListener, CoreStartable {
@VisibleForTesting
void onExpansionChanged(String key, boolean isUserAction, boolean isExpanded,
- NotificationVisibility.NotificationLocation location) {
+ NotificationLocation location) {
State state = getState(key);
state.mIsUserAction = isUserAction;
state.mIsExpanded = isExpanded;
@@ -528,7 +537,7 @@ public class NotificationLogger implements StateListener, CoreStartable {
@Nullable
Boolean mIsVisible;
@Nullable
- NotificationVisibility.NotificationLocation mLocation;
+ NotificationLocation mLocation;
private State() {}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
index 5ca13c95309f..6c63d1d6dcb5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
@@ -40,6 +40,11 @@ public interface NotificationPanelLogger {
/**
* Log a NOTIFICATION_PANEL_REPORTED statsd event.
+ */
+ void logPanelShown(boolean isLockscreen, Notifications.NotificationList proto);
+
+ /**
+ * Log a NOTIFICATION_PANEL_REPORTED statsd event.
* @param visibleNotifications as provided by NotificationEntryManager.getVisibleNotifications()
*/
void logPanelShown(boolean isLockscreen,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
index 9a632282ae16..d7f7b760dd04 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
@@ -21,6 +21,7 @@ import static com.android.systemui.statusbar.notification.logging.NotificationPa
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.logging.nano.Notifications;
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
import com.google.protobuf.nano.MessageNano;
@@ -31,15 +32,25 @@ import java.util.List;
* Normal implementation of NotificationPanelLogger.
*/
public class NotificationPanelLoggerImpl implements NotificationPanelLogger {
+
+ @Override
+ public void logPanelShown(boolean isLockscreen, Notifications.NotificationList proto) {
+ SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED,
+ /* event_id = */ NotificationPanelEvent.fromLockscreen(isLockscreen).getId(),
+ /* num_notifications = */ proto.notifications.length,
+ /* notifications = */ MessageNano.toByteArray(proto));
+ }
+
@Override
public void logPanelShown(boolean isLockscreen,
List<NotificationEntry> visibleNotifications) {
+ NotificationsLiveDataStoreRefactor.assertInLegacyMode();
final Notifications.NotificationList proto = NotificationPanelLogger.toNotificationProto(
visibleNotifications);
SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED,
- /* int event_id */ NotificationPanelEvent.fromLockscreen(isLockscreen).getId(),
- /* int num_notifications*/ proto.notifications.length,
- /* byte[] notifications*/ MessageNano.toByteArray(proto));
+ /* event_id = */ NotificationPanelEvent.fromLockscreen(isLockscreen).getId(),
+ /* num_notifications = */ proto.notifications.length,
+ /* notifications = */ MessageNano.toByteArray(proto));
}
@Override
@@ -47,8 +58,8 @@ public class NotificationPanelLoggerImpl implements NotificationPanelLogger {
final Notifications.NotificationList proto = NotificationPanelLogger.toNotificationProto(
Collections.singletonList(draggedNotification));
SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED,
- /* int event_id */ NOTIFICATION_DRAG.getId(),
- /* int num_notifications*/ proto.notifications.length,
- /* byte[] notifications*/ MessageNano.toByteArray(proto));
+ /* event_id = */ NOTIFICATION_DRAG.getId(),
+ /* num_notifications = */ proto.notifications.length,
+ /* notifications = */ MessageNano.toByteArray(proto));
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index e200e65a9f4a..5eeb0665da0e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -118,6 +118,7 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;
@@ -988,6 +989,25 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
/**
+ * Recursively collects the [{@link ExpandableViewState#location}]s populating the provided
+ * map.
+ * The visibility of each child is determined by the {@link View#getVisibility()}.
+ * Locations are added to the provided map including locations from child views, that are
+ * visible.
+ */
+ public void collectVisibleLocations(Map<String, Integer> locationsMap) {
+ if (getVisibility() == View.VISIBLE) {
+ locationsMap.put(getEntry().getKey(), getViewState().location);
+ if (mChildrenContainer != null) {
+ List<ExpandableNotificationRow> children = mChildrenContainer.getAttachedChildren();
+ for (int i = 0; i < children.size(); i++) {
+ children.get(i).collectVisibleLocations(locationsMap);
+ }
+ }
+ }
+ }
+
+ /**
* Updates states of all children.
*/
public void updateChildrenStates() {
@@ -1615,7 +1635,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
/**
* Called when the notification is expanded / collapsed.
*/
- void logNotificationExpansion(String key, boolean userAction, boolean expanded);
+ void logNotificationExpansion(String key, int location, boolean userAction,
+ boolean expanded);
/**
* Called when a notification which was previously kept in its parent for the
@@ -3312,7 +3333,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
if (nowExpanded != wasExpanded) {
updateShelfIconColor();
if (mLogger != null) {
- mLogger.logNotificationExpansion(mLoggingKey, userAction, nowExpanded);
+ mLogger.logNotificationExpansion(mLoggingKey, getViewState().location, userAction,
+ nowExpanded);
}
if (mIsSummaryWithChildren) {
mChildrenContainer.onExpansionChanged();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index af55f44b785a..0afdefabd4f0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -48,13 +48,13 @@ import com.android.systemui.statusbar.notification.collection.render.GroupExpans
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.notification.collection.render.NodeController;
import com.android.systemui.statusbar.notification.collection.render.NotifViewController;
-import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
import com.android.systemui.statusbar.notification.row.dagger.AppName;
import com.android.systemui.statusbar.notification.row.dagger.NotificationKey;
import com.android.systemui.statusbar.notification.row.dagger.NotificationRowScope;
import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationRowStatsLogger;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.SmartReplyConstants;
@@ -90,7 +90,7 @@ public class ExpandableNotificationRowController implements NotifViewController
private final GroupMembershipManager mGroupMembershipManager;
private final GroupExpansionManager mGroupExpansionManager;
private final RowContentBindStage mRowContentBindStage;
- private final NotificationLogger mNotificationLogger;
+ private final NotificationRowStatsLogger mStatsLogger;
private final NotificationRowLogger mLogBufferLogger;
private final HeadsUpManager mHeadsUpManager;
private final ExpandableNotificationRow.OnExpandClickListener mOnExpandClickListener;
@@ -130,9 +130,10 @@ public class ExpandableNotificationRowController implements NotifViewController
private final ExpandableNotificationRow.ExpandableNotificationRowLogger mLoggerCallback =
new ExpandableNotificationRow.ExpandableNotificationRowLogger() {
@Override
- public void logNotificationExpansion(String key, boolean userAction,
+ public void logNotificationExpansion(String key, int location, boolean userAction,
boolean expanded) {
- mNotificationLogger.onExpansionChanged(key, userAction, expanded);
+ mStatsLogger.onNotificationExpansionChanged(key, expanded, location,
+ userAction);
}
@Override
@@ -212,7 +213,7 @@ public class ExpandableNotificationRowController implements NotifViewController
GroupMembershipManager groupMembershipManager,
GroupExpansionManager groupExpansionManager,
RowContentBindStage rowContentBindStage,
- NotificationLogger notificationLogger,
+ NotificationRowStatsLogger statsLogger,
HeadsUpManager headsUpManager,
ExpandableNotificationRow.OnExpandClickListener onExpandClickListener,
StatusBarStateController statusBarStateController,
@@ -239,7 +240,7 @@ public class ExpandableNotificationRowController implements NotifViewController
mGroupMembershipManager = groupMembershipManager;
mGroupExpansionManager = groupExpansionManager;
mRowContentBindStage = rowContentBindStage;
- mNotificationLogger = notificationLogger;
+ mStatsLogger = statsLogger;
mHeadsUpManager = headsUpManager;
mOnExpandClickListener = onExpandClickListener;
mStatusBarStateController = statusBarStateController;
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 eb1c1bafae9e..5527efc4029d 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
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.shared
import android.graphics.drawable.Icon
+import com.android.systemui.statusbar.notification.stack.PriorityBucket
/**
* Model for a top-level "entry" in the notification list, either an
@@ -55,6 +56,16 @@ data class ActiveNotificationModel(
val shelfIcon: Icon?,
/** Icon to display in the status bar. */
val statusBarIcon: Icon?,
+ /** The notifying app's [packageName]'s uid. */
+ val uid: Int,
+ /** The notifying app's packageName. */
+ val packageName: String,
+ /** A small per-notification ID, used for statsd logging. */
+ val instanceId: Int?,
+ /** If this notification is the group summary for a group of notifications. */
+ val isGroupSummary: Boolean,
+ /** Indicates in which section the notification is displayed in. @see [PriorityBucket]. */
+ @PriorityBucket val bucket: Int,
) : ActiveNotificationEntryModel()
/** Model for a group of notifications. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java
index 6bb957339b6f..5c9a0b939dc7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java
@@ -23,7 +23,6 @@ import androidx.annotation.Nullable;
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
import com.android.systemui.statusbar.notification.LaunchAnimationParameters;
-import com.android.systemui.statusbar.notification.NotificationActivityStarter;
import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index ea414d2c78d0..e791a6490e15 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -113,6 +113,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableView;
import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
@@ -129,9 +130,12 @@ import java.lang.annotation.Retention;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
+import java.util.concurrent.Callable;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@@ -245,6 +249,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
*/
private float mOverScrolledBottomPixels;
private NotificationLogger.OnChildLocationsChangedListener mListener;
+ private OnNotificationLocationsChangedListener mLocationsChangedListener;
private OnOverscrollTopChangedListener mOverscrollTopChangedListener;
private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
private Runnable mOnHeightChangedRunnable;
@@ -390,6 +395,14 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
}
};
+ private final Callable<Map<String, Integer>> collectVisibleLocationsCallable =
+ new Callable<>() {
+ @Override
+ public Map<String, Integer> call() {
+ return collectVisibleNotificationLocations();
+ }
+ };
+
private boolean mPulsing;
private boolean mScrollable;
private View mForcedScroll;
@@ -1242,8 +1255,21 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
}
}
+ /**
+ * @param listener to be notified after the location of Notification children might have
+ * changed.
+ */
+ public void setNotificationLocationsChangedListener(
+ @Nullable OnNotificationLocationsChangedListener listener) {
+ if (NotificationsLiveDataStoreRefactor.isUnexpectedlyInLegacyMode()) {
+ return;
+ }
+ mLocationsChangedListener = listener;
+ }
+
public void setChildLocationsChangedListener(
NotificationLogger.OnChildLocationsChangedListener listener) {
+ NotificationsLiveDataStoreRefactor.assertInLegacyMode();
mListener = listener;
}
@@ -4398,15 +4424,40 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
child.applyViewState();
}
- if (mListener != null) {
- mListener.onChildLocationsChanged();
+ if (NotificationsLiveDataStoreRefactor.isEnabled()) {
+ if (mLocationsChangedListener != null) {
+ mLocationsChangedListener.onChildLocationsChanged(collectVisibleLocationsCallable);
+ }
+ } else {
+ if (mListener != null) {
+ mListener.onChildLocationsChanged();
+ }
}
+
runAnimationFinishedRunnables();
setAnimationRunning(false);
updateBackground();
updateViewShadows();
}
+ /**
+ * Retrieves a map of visible [{@link ExpandableViewState#location}]s of the actively displayed
+ * Notification children associated by their Notification keys.
+ * Locations are collected recursively including locations from the child views of Notification
+ * Groups, that are visible.
+ */
+ private Map<String, Integer> collectVisibleNotificationLocations() {
+ Map<String, Integer> visibilities = new HashMap<>();
+ int numChildren = getChildCount();
+ for (int i = 0; i < numChildren; i++) {
+ ExpandableView child = getChildAtIndex(i);
+ if (child instanceof ExpandableNotificationRow row) {
+ row.collectVisibleLocations(visibilities);
+ }
+ }
+ return visibilities;
+ }
+
private void updateViewShadows() {
// we need to work around an issue where the shadow would not cast between siblings when
// their z difference is between 0 and 0.1
@@ -5901,7 +5952,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
}
}
- protected void setLogger(StackStateLogger logger) {
+ /**
+ * Sets a {@link StackStateLogger} which is notified as the {@link StackStateAnimator} updates
+ * the views.
+ */
+ protected void setStackStateLogger(StackStateLogger logger) {
mStateAnimator.setLogger(logger);
}
@@ -5938,6 +5993,18 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
void flingTopOverscroll(float velocity, boolean open);
}
+ /**
+ * A listener that is notified when some ExpandableNotificationRow locations might have changed.
+ */
+ public interface OnNotificationLocationsChangedListener {
+ /**
+ * Called when the location of ExpandableNotificationRows might have changed.
+ *
+ * @param locations mapping of Notification keys to locations.
+ */
+ void onChildLocationsChanged(Callable<Map<String, Integer>> locations);
+ }
+
private void updateSpeedBumpIndex() {
mSpeedBumpIndexDirty = true;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index a30c29456b3b..c0e0b73c750d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -751,7 +751,7 @@ public class NotificationStackScrollLayoutController implements Dumpable {
}
private void setUpView() {
- mView.setLogger(mStackStateLogger);
+ mView.setStackStateLogger(mStackStateLogger);
mView.setController(this);
mView.setLogger(mLogger);
mView.setTouchHandler(new TouchHandler());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationRowStatsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationRowStatsLogger.kt
new file mode 100644
index 000000000000..2305c7e880bf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationRowStatsLogger.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.view
+
+interface NotificationRowStatsLogger {
+ fun onNotificationExpansionChanged(
+ key: String,
+ isExpanded: Boolean,
+ location: Int,
+ isUserAction: Boolean
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLogger.kt
new file mode 100644
index 000000000000..54186165c54a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLogger.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.view
+
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import java.util.concurrent.Callable
+
+/**
+ * Logs UI events of Notifications, in particular, logging which Notifications are visible and which
+ * are not.
+ */
+interface NotificationStatsLogger : NotificationRowStatsLogger {
+ fun onLockscreenOrShadeInteractive(
+ isOnLockScreen: Boolean,
+ activeNotifications: List<ActiveNotificationModel>,
+ )
+ fun onLockscreenOrShadeNotInteractive(activeNotifications: List<ActiveNotificationModel>)
+ fun onNotificationRemoved(key: String)
+ fun onNotificationUpdated(key: String)
+ fun onNotificationListUpdated(
+ locationsProvider: Callable<Map<String, Int>>,
+ notificationRanks: Map<String, Int>,
+ )
+ override fun onNotificationExpansionChanged(
+ key: String,
+ isExpanded: Boolean,
+ location: Int,
+ isUserAction: Boolean
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt
new file mode 100644
index 000000000000..0cb00bc8d01d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.view
+
+import android.service.notification.NotificationListenerService
+import androidx.annotation.VisibleForTesting
+import com.android.internal.statusbar.IStatusBarService
+import com.android.internal.statusbar.NotificationVisibility
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger
+import com.android.systemui.statusbar.notification.logging.nano.Notifications
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import com.android.systemui.statusbar.notification.stack.ExpandableViewState
+import java.util.concurrent.Callable
+import java.util.concurrent.ConcurrentHashMap
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+@VisibleForTesting const val UNKNOWN_RANK = -1
+
+@SysUISingleton
+class NotificationStatsLoggerImpl
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ private val notificationListenerService: NotificationListenerService,
+ private val notificationPanelLogger: NotificationPanelLogger,
+ private val statusBarService: IStatusBarService,
+) : NotificationStatsLogger {
+ private val lastLoggedVisibilities = mutableMapOf<String, VisibilityState>()
+ private var logVisibilitiesJob: Job? = null
+
+ private val expansionStates: MutableMap<String, ExpansionState> =
+ ConcurrentHashMap<String, ExpansionState>()
+ private val lastReportedExpansionValues: MutableMap<String, Boolean> =
+ ConcurrentHashMap<String, Boolean>()
+
+ override fun onNotificationListUpdated(
+ locationsProvider: Callable<Map<String, Int>>,
+ notificationRanks: Map<String, Int>,
+ ) {
+ if (logVisibilitiesJob?.isActive == true) {
+ return
+ }
+
+ logVisibilitiesJob =
+ startLogVisibilitiesJob(
+ newVisibilities =
+ combine(
+ visibilities = locationsProvider.call(),
+ rankingsMap = notificationRanks
+ ),
+ activeNotifCount = notificationRanks.size,
+ )
+ }
+
+ override fun onNotificationExpansionChanged(
+ key: String,
+ isExpanded: Boolean,
+ location: Int,
+ isUserAction: Boolean,
+ ) {
+ val expansionState =
+ ExpansionState(
+ key = key,
+ isExpanded = isExpanded,
+ isUserAction = isUserAction,
+ location = location,
+ )
+ expansionStates[key] = expansionState
+ maybeLogNotificationExpansionChange(expansionState)
+ }
+
+ private fun maybeLogNotificationExpansionChange(expansionState: ExpansionState) {
+ if (expansionState.visible.not()) {
+ // Only log visible expansion changes
+ return
+ }
+
+ val loggedExpansionValue: Boolean? = lastReportedExpansionValues[expansionState.key]
+ if (loggedExpansionValue == null && !expansionState.isExpanded) {
+ // Consider the Notification initially collapsed, so only expanded is logged in the
+ // first time.
+ return
+ }
+
+ if (loggedExpansionValue != null && loggedExpansionValue == expansionState.isExpanded) {
+ // We have already logged this state, don't log it again
+ return
+ }
+
+ logNotificationExpansionChange(expansionState)
+ lastReportedExpansionValues[expansionState.key] = expansionState.isExpanded
+ }
+
+ private fun logNotificationExpansionChange(expansionState: ExpansionState) =
+ applicationScope.launch {
+ withContext(bgDispatcher) {
+ statusBarService.onNotificationExpansionChanged(
+ /* key = */ expansionState.key,
+ /* userAction = */ expansionState.isUserAction,
+ /* expanded = */ expansionState.isExpanded,
+ /* notificationLocation = */ expansionState.location
+ .toNotificationLocation()
+ .ordinal
+ )
+ }
+ }
+
+ override fun onLockscreenOrShadeInteractive(
+ isOnLockScreen: Boolean,
+ activeNotifications: List<ActiveNotificationModel>,
+ ) {
+ applicationScope.launch {
+ withContext(bgDispatcher) {
+ notificationPanelLogger.logPanelShown(
+ isOnLockScreen,
+ activeNotifications.toNotificationProto()
+ )
+ }
+ }
+ }
+
+ override fun onLockscreenOrShadeNotInteractive(
+ activeNotifications: List<ActiveNotificationModel>
+ ) {
+ logVisibilitiesJob =
+ startLogVisibilitiesJob(
+ newVisibilities = emptyMap(),
+ activeNotifCount = activeNotifications.size
+ )
+ }
+
+ // TODO(b/308623704) wire this in with NotifPipeline updates
+ override fun onNotificationRemoved(key: String) {
+ // No need to track expansion states for Notifications that are removed.
+ expansionStates.remove(key)
+ lastReportedExpansionValues.remove(key)
+ }
+
+ // TODO(b/308623704) wire this in with NotifPipeline updates
+ override fun onNotificationUpdated(key: String) {
+ // When the Notification is updated, we should consider it as not yet logged.
+ lastReportedExpansionValues.remove(key)
+ }
+
+ private fun combine(
+ visibilities: Map<String, Int>,
+ rankingsMap: Map<String, Int>
+ ): Map<String, VisibilityState> =
+ visibilities.mapValues { entry ->
+ VisibilityState(entry.key, entry.value, rankingsMap[entry.key] ?: UNKNOWN_RANK)
+ }
+
+ private fun startLogVisibilitiesJob(
+ newVisibilities: Map<String, VisibilityState>,
+ activeNotifCount: Int,
+ ) =
+ applicationScope.launch {
+ val newlyVisible = newVisibilities - lastLoggedVisibilities.keys
+ val noLongerVisible = lastLoggedVisibilities - newVisibilities.keys
+
+ maybeLogVisibilityChanges(newlyVisible, noLongerVisible, activeNotifCount)
+ updateExpansionStates(newlyVisible, noLongerVisible)
+
+ lastLoggedVisibilities.clear()
+ lastLoggedVisibilities.putAll(newVisibilities)
+ }
+
+ private suspend fun maybeLogVisibilityChanges(
+ newlyVisible: Map<String, VisibilityState>,
+ noLongerVisible: Map<String, VisibilityState>,
+ activeNotifCount: Int,
+ ) {
+ if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) {
+ return
+ }
+
+ val newlyVisibleAr =
+ newlyVisible.mapToNotificationVisibilitiesAr(visible = true, count = activeNotifCount)
+
+ val noLongerVisibleAr =
+ noLongerVisible.mapToNotificationVisibilitiesAr(
+ visible = false,
+ count = activeNotifCount
+ )
+
+ withContext(bgDispatcher) {
+ statusBarService.onNotificationVisibilityChanged(newlyVisibleAr, noLongerVisibleAr)
+ if (newlyVisible.isNotEmpty()) {
+ notificationListenerService.setNotificationsShown(newlyVisible.keys.toTypedArray())
+ }
+ }
+ }
+
+ private fun updateExpansionStates(
+ newlyVisible: Map<String, VisibilityState>,
+ noLongerVisible: Map<String, VisibilityState>
+ ) {
+ expansionStates.forEach { (key, expansionState) ->
+ if (newlyVisible.contains(key)) {
+ val newState =
+ expansionState.copy(
+ visible = true,
+ location = newlyVisible.getValue(key).location,
+ )
+ expansionStates[key] = newState
+ maybeLogNotificationExpansionChange(newState)
+ }
+
+ if (noLongerVisible.contains(key)) {
+ expansionStates[key] =
+ expansionState.copy(
+ visible = false,
+ location = noLongerVisible.getValue(key).location,
+ )
+ }
+ }
+ }
+
+ private data class VisibilityState(
+ val key: String,
+ val location: Int,
+ val rank: Int,
+ )
+
+ private data class ExpansionState(
+ val key: String,
+ val isUserAction: Boolean,
+ val isExpanded: Boolean,
+ val visible: Boolean,
+ val location: Int,
+ ) {
+ constructor(
+ key: String,
+ isExpanded: Boolean,
+ location: Int,
+ isUserAction: Boolean,
+ ) : this(
+ key = key,
+ isExpanded = isExpanded,
+ isUserAction = isUserAction,
+ visible = isVisibleLocation(location),
+ location = location,
+ )
+ }
+
+ private fun Map<String, VisibilityState>.mapToNotificationVisibilitiesAr(
+ visible: Boolean,
+ count: Int,
+ ): Array<NotificationVisibility> =
+ this.map { (key, state) ->
+ NotificationVisibility.obtain(
+ /* key = */ key,
+ /* rank = */ state.rank,
+ /* count = */ count,
+ /* visible = */ visible,
+ /* location = */ state.location.toNotificationLocation()
+ )
+ }
+ .toTypedArray()
+}
+
+private fun Int.toNotificationLocation(): NotificationVisibility.NotificationLocation {
+ return when (this) {
+ ExpandableViewState.LOCATION_FIRST_HUN ->
+ NotificationVisibility.NotificationLocation.LOCATION_FIRST_HEADS_UP
+ ExpandableViewState.LOCATION_HIDDEN_TOP ->
+ NotificationVisibility.NotificationLocation.LOCATION_HIDDEN_TOP
+ ExpandableViewState.LOCATION_MAIN_AREA ->
+ NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA
+ ExpandableViewState.LOCATION_BOTTOM_STACK_PEEKING ->
+ NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_PEEKING
+ ExpandableViewState.LOCATION_BOTTOM_STACK_HIDDEN ->
+ NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_HIDDEN
+ ExpandableViewState.LOCATION_GONE ->
+ NotificationVisibility.NotificationLocation.LOCATION_GONE
+ else -> NotificationVisibility.NotificationLocation.LOCATION_UNKNOWN
+ }
+}
+
+private fun List<ActiveNotificationModel>.toNotificationProto(): Notifications.NotificationList {
+ val notificationList = Notifications.NotificationList()
+ val protoArray: Array<Notifications.Notification> =
+ map { notification ->
+ Notifications.Notification().apply {
+ uid = notification.uid
+ packageName = notification.packageName
+ notification.instanceId?.let { instanceId = it }
+ // TODO(b/308623704) check if we can set groupInstanceId as well
+ isGroupSummary = notification.isGroupSummary
+ section = NotificationPanelLogger.toNotificationSection(notification.bucket)
+ }
+ }
+ .toTypedArray()
+
+ if (protoArray.isNotEmpty()) {
+ notificationList.notifications = protoArray
+ }
+
+ return notificationList
+}
+
+private fun isVisibleLocation(location: Int): Boolean =
+ location and ExpandableViewState.VISIBLE_LOCATIONS != 0
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 1b3666078b27..da260cbb94e7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -33,13 +33,16 @@ import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefac
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
import com.android.systemui.statusbar.phone.NotificationIconAreaController
import com.android.systemui.util.kotlin.getOrNull
+import java.util.Optional
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.combine
@@ -49,13 +52,14 @@ import kotlinx.coroutines.launch
class NotificationListViewBinder
@Inject
constructor(
- private val viewModel: NotificationListViewModel,
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val configuration: ConfigurationState,
private val falsingManager: FalsingManager,
private val iconAreaController: NotificationIconAreaController,
private val metricsLogger: MetricsLogger,
private val nicBinder: NotificationIconContainerShelfViewBinder,
+ private val loggerOptional: Optional<NotificationStatsLogger>,
+ private val viewModel: NotificationListViewModel,
) {
fun bindWhileAttached(
@@ -75,10 +79,15 @@ constructor(
if (FooterViewRefactor.isEnabled) {
launch { bindFooter(view) }
launch { bindEmptyShade(view) }
- viewModel.isImportantForAccessibility.collect { isImportantForAccessibility ->
- view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
+ launch {
+ viewModel.isImportantForAccessibility.collect { isImportantForAccessibility
+ ->
+ view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
+ }
}
}
+
+ launch { bindLogger(view) }
}
}
}
@@ -136,4 +145,18 @@ constructor(
)
}
}
+
+ private suspend fun bindLogger(view: NotificationStackScrollLayout) {
+ if (NotificationsLiveDataStoreRefactor.isEnabled) {
+ viewModel.logger.getOrNull()?.let { viewModel ->
+ loggerOptional.getOrNull()?.let { logger ->
+ NotificationStatsLoggerBinder.bindLogger(
+ view,
+ logger,
+ viewModel,
+ )
+ }
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStatsLoggerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStatsLoggerBinder.kt
new file mode 100644
index 000000000000..a05ad6e45991
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStatsLoggerBinder.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLoggerViewModel
+import com.android.systemui.util.kotlin.Utils
+import com.android.systemui.util.kotlin.sample
+import com.android.systemui.util.kotlin.throttle
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+
+/**
+ * Binds a [NotificationStatsLogger] to its [NotificationLoggerViewModel], and wires in
+ * [NotificationStackScrollLayout.OnNotificationLocationsChangedListener] updates to it.
+ */
+object NotificationStatsLoggerBinder {
+
+ /** minimum delay in ms between Notification location updates */
+ private const val NOTIFICATION_UPDATE_PERIOD_MS = 500L
+
+ suspend fun bindLogger(
+ view: NotificationStackScrollLayout,
+ logger: NotificationStatsLogger,
+ viewModel: NotificationLoggerViewModel,
+ ) {
+ viewModel.isLockscreenOrShadeInteractive
+ .sample(
+ combine(viewModel.isOnLockScreen, viewModel.activeNotifications, ::Pair),
+ Utils.Companion::toTriple
+ )
+ .collectLatest { (isPanelInteractive, isOnLockScreen, notifications) ->
+ if (isPanelInteractive) {
+ logger.onLockscreenOrShadeInteractive(
+ isOnLockScreen = isOnLockScreen,
+ activeNotifications = notifications,
+ )
+ view.onNotificationsUpdated
+ // Delay the updates with [NOTIFICATION_UPDATES_PERIOD_MS]. If the original
+ // flow emits more than once during this period, only the latest value is
+ // emitted, meaning that we won't log the intermediate Notification states.
+ .throttle(NOTIFICATION_UPDATE_PERIOD_MS)
+ .sample(viewModel.activeNotificationRanks, ::Pair)
+ .collect { (locationsProvider, notificationRanks) ->
+ logger.onNotificationListUpdated(locationsProvider, notificationRanks)
+ }
+ } else {
+ logger.onLockscreenOrShadeNotInteractive(
+ activeNotifications = notifications,
+ )
+ }
+ }
+ }
+}
+
+private val NotificationStackScrollLayout.onNotificationsUpdated
+ get() =
+ ConflatedCallbackFlow.conflatedCallbackFlow {
+ val callback =
+ NotificationStackScrollLayout.OnNotificationLocationsChangedListener { callable ->
+ trySend(callable)
+ }
+ setNotificationLocationsChangedListener(callback)
+ awaitClose { setNotificationLocationsChangedListener(null) }
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index 569ae248ad23..86c0a67868e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -40,6 +40,7 @@ constructor(
val shelf: NotificationShelfViewModel,
val hideListViewModel: HideListViewModel,
val footer: Optional<FooterViewModel>,
+ val logger: Optional<NotificationLoggerViewModel>,
activeNotificationsInteractor: ActiveNotificationsInteractor,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
seenNotificationsInteractor: SeenNotificationsInteractor,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModel.kt
new file mode 100644
index 000000000000..0901a7f817eb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModel.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+class NotificationLoggerViewModel
+@Inject
+constructor(
+ activeNotificationsInteractor: ActiveNotificationsInteractor,
+ keyguardInteractor: KeyguardInteractor,
+ windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor,
+) {
+ val activeNotifications: Flow<List<ActiveNotificationModel>> =
+ activeNotificationsInteractor.allRepresentativeNotifications.map { it.values.toList() }
+
+ val activeNotificationRanks: Flow<Map<String, Int>> =
+ activeNotificationsInteractor.activeNotificationRanks
+
+ val isLockscreenOrShadeInteractive: Flow<Boolean> =
+ windowRootViewVisibilityInteractor.isLockscreenOrShadeVisibleAndInteractive
+
+ val isOnLockScreen: Flow<Boolean> = keyguardInteractor.isKeyguardShowing
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
index 4ab3cd49b297..b3b10eb8aec3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
@@ -74,6 +74,18 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() {
}
@Test
+ fun testActiveNotificationRanks_sizeMatches() {
+ testComponent.runTest {
+ val activeNotificationRanks by collectLastValue(underTest.activeNotificationRanks)
+
+ activeNotificationListRepository.setActiveNotifs(5)
+ runCurrent()
+
+ assertThat(activeNotificationRanks!!.size).isEqualTo(5)
+ }
+ }
+
+ @Test
fun testHasClearableNotifications_whenHasClearableAlertingNotifs() =
testComponent.runTest {
val hasClearable by collectLastValue(underTest.hasClearableNotifications)
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 6374d5e259fc..334776ca0bcc 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
@@ -15,10 +15,12 @@
package com.android.systemui.statusbar.notification.domain.interactor
+import android.service.notification.StatusBarNotification
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.statusbar.notification.collection.ListEntry
+import com.android.systemui.statusbar.RankingBuilder
+import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.shared.byKey
@@ -49,22 +51,101 @@ class RenderNotificationsListInteractorTest : SysuiTestCase() {
testScope.runTest {
val notifs by collectLastValue(notifsInteractor.topLevelRepresentativeNotifications)
val keys = (1..50).shuffled().map { "$it" }
- val entries =
- keys.map {
- mock<ListEntry> {
- val mockRep =
- mock<NotificationEntry> {
- whenever(key).thenReturn(it)
- whenever(sbn).thenReturn(mock())
- whenever(icons).thenReturn(mock())
- }
- whenever(representativeEntry).thenReturn(mockRep)
- }
- }
+ val entries = keys.map { mockNotificationEntry(key = it) }
underTest.setRenderedList(entries)
assertThat(notifs)
.comparingElementsUsing(byKey)
.containsExactlyElementsIn(keys)
.inOrder()
}
+
+ @Test
+ fun setRenderList_flatMapsRankings() =
+ testScope.runTest {
+ val ranks by collectLastValue(notifsInteractor.activeNotificationRanks)
+
+ val single = mockNotificationEntry("single", 0)
+ val group =
+ mockGroupEntry(
+ key = "group",
+ summary = mockNotificationEntry("summary", 1),
+ children =
+ listOf(
+ mockNotificationEntry("child0", 2),
+ mockNotificationEntry("child1", 3),
+ ),
+ )
+
+ underTest.setRenderedList(listOf(single, group))
+
+ assertThat(ranks)
+ .containsExactlyEntriesIn(
+ mapOf(
+ "single" to 0,
+ "summary" to 1,
+ "child0" to 2,
+ "child1" to 3,
+ )
+ )
+ }
+
+ @Test
+ fun setRenderList_singleItems_mapsRankings() =
+ testScope.runTest {
+ val actual by collectLastValue(notifsInteractor.activeNotificationRanks)
+ val expected =
+ (0..10).shuffled().mapIndexed { index, value -> "$value" to index }.toMap()
+
+ val entries = expected.map { (key, rank) -> mockNotificationEntry(key, rank) }
+
+ underTest.setRenderedList(entries)
+
+ assertThat(actual).containsAtLeastEntriesIn(expected)
+ }
+
+ @Test
+ fun setRenderList_groupWithNoSummary_flatMapsRankings() =
+ testScope.runTest {
+ val actual by collectLastValue(notifsInteractor.activeNotificationRanks)
+ val expected =
+ (0..10).shuffled().mapIndexed { index, value -> "$value" to index }.toMap()
+
+ val group =
+ mockGroupEntry(
+ key = "group",
+ summary = null,
+ children = expected.map { (key, rank) -> mockNotificationEntry(key, rank) },
+ )
+
+ underTest.setRenderedList(listOf(group))
+
+ assertThat(actual).containsAtLeastEntriesIn(expected)
+ }
+}
+
+private fun mockGroupEntry(
+ key: String,
+ summary: NotificationEntry?,
+ children: List<NotificationEntry>,
+): GroupEntry {
+ return mock<GroupEntry> {
+ whenever(this.key).thenReturn(key)
+ whenever(this.summary).thenReturn(summary)
+ whenever(this.children).thenReturn(children)
+ }
+}
+
+private fun mockNotificationEntry(key: String, rank: Int = 0): NotificationEntry {
+ val mockSbn =
+ mock<StatusBarNotification>() {
+ whenever(notification).thenReturn(mock())
+ whenever(packageName).thenReturn("com.android")
+ }
+ return mock<NotificationEntry> {
+ whenever(this.key).thenReturn(key)
+ whenever(this.icons).thenReturn(mock())
+ whenever(this.representativeEntry).thenReturn(this)
+ whenever(this.ranking).thenReturn(RankingBuilder().setRank(rank).build())
+ whenever(this.sbn).thenReturn(mockSbn)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
index dae0aa229dbf..d61fc05c699f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
@@ -34,6 +34,11 @@ public class NotificationPanelLoggerFake implements NotificationPanelLogger {
}
@Override
+ public void logPanelShown(boolean isLockscreen, Notifications.NotificationList proto) {
+ mCalls.add(new CallRecord(isLockscreen, proto));
+ }
+
+ @Override
public void logPanelShown(boolean isLockscreen,
List<NotificationEntry> visibleNotifications) {
mCalls.add(new CallRecord(isLockscreen,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
index 9547af19b36b..8ac2a334818c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
@@ -40,12 +40,12 @@ import com.android.systemui.statusbar.notification.collection.provider.Notificat
import com.android.systemui.statusbar.notification.collection.render.FakeNodeController
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
-import com.android.systemui.statusbar.notification.logging.NotificationLogger
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController.BUBBLES_SETTING_URI
import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer
import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationRowStatsLogger
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.SmartReplyConstants
@@ -92,7 +92,7 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() {
private val groupMembershipManager: GroupMembershipManager = mock()
private val groupExpansionManager: GroupExpansionManager = mock()
private val rowContentBindStage: RowContentBindStage = mock()
- private val notifLogger: NotificationLogger = mock()
+ private val notifLogger: NotificationRowStatsLogger = mock()
private val headsUpManager: HeadsUpManager = mock()
private val onExpandClickListener: ExpandableNotificationRow.OnExpandClickListener = mock()
private val statusBarStateController: StatusBarStateController = mock()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt
new file mode 100644
index 000000000000..d7d1ffcc3339
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.view
+
+import android.service.notification.notificationListenerService
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.statusbar.NotificationVisibility
+import com.android.internal.statusbar.statusBarService
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
+import com.android.systemui.statusbar.notification.logging.nano.Notifications
+import com.android.systemui.statusbar.notification.logging.notificationPanelLogger
+import com.android.systemui.statusbar.notification.stack.ExpandableViewState
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Callable
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NotificationStatsLoggerTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+
+ private val testScope = kosmos.testScope
+ private val mockNotificationListenerService = kosmos.notificationListenerService
+ private val mockPanelLogger = kosmos.notificationPanelLogger
+ private val mockStatusBarService = kosmos.statusBarService
+
+ private val underTest = kosmos.notificationStatsLogger
+
+ private val visibilityArrayCaptor = argumentCaptor<Array<NotificationVisibility>>()
+ private val stringArrayCaptor = argumentCaptor<Array<String>>()
+ private val notificationListProtoCaptor = argumentCaptor<Notifications.NotificationList>()
+
+ @Test
+ fun onNotificationListUpdated_itemsAdded_logsNewlyVisibleItems() =
+ testScope.runTest {
+ // WHEN new Notifications are added
+ // AND they're visible
+ val (ranks, locations) = fakeNotificationMaps("key0", "key1")
+ val callable = Callable { locations }
+ underTest.onNotificationListUpdated(callable, ranks)
+ runCurrent()
+
+ // THEN visibility changes are reported
+ verify(mockStatusBarService)
+ .onNotificationVisibilityChanged(visibilityArrayCaptor.capture(), eq(emptyArray()))
+ verify(mockNotificationListenerService)
+ .setNotificationsShown(stringArrayCaptor.capture())
+ val loggedVisibilities = visibilityArrayCaptor.value
+ val loggedKeys = stringArrayCaptor.value
+ assertThat(loggedVisibilities).hasLength(2)
+ assertThat(loggedKeys).hasLength(2)
+ assertThat(loggedVisibilities[0]).apply {
+ isKeyEqualTo("key0")
+ isRankEqualTo(0)
+ isVisible()
+ isInMainArea()
+ isCountEqualTo(2)
+ }
+ assertThat(loggedVisibilities[1]).apply {
+ isKeyEqualTo("key1")
+ isRankEqualTo(1)
+ isVisible()
+ isInMainArea()
+ isCountEqualTo(2)
+ }
+ assertThat(loggedKeys[0]).isEqualTo("key0")
+ assertThat(loggedKeys[1]).isEqualTo("key1")
+ }
+
+ @Test
+ fun onNotificationListUpdated_itemsRemoved_logsNoLongerVisibleItems() =
+ testScope.runTest {
+ // GIVEN some visible Notifications are reported
+ val (ranks, locations) = fakeNotificationMaps("key0", "key1")
+ val callable = Callable { locations }
+ underTest.onNotificationListUpdated(callable, ranks)
+ runCurrent()
+ clearInvocations(mockStatusBarService, mockNotificationListenerService)
+
+ // WHEN the same Notifications are removed
+ val emptyCallable = Callable { emptyMap<String, Int>() }
+ underTest.onNotificationListUpdated(emptyCallable, emptyMap())
+ runCurrent()
+
+ // THEN visibility changes are reported
+ verify(mockStatusBarService)
+ .onNotificationVisibilityChanged(eq(emptyArray()), visibilityArrayCaptor.capture())
+ verifyZeroInteractions(mockNotificationListenerService)
+ val noLongerVisible = visibilityArrayCaptor.value
+ assertThat(noLongerVisible).hasLength(2)
+ assertThat(noLongerVisible[0]).apply {
+ isKeyEqualTo("key0")
+ isRankEqualTo(0)
+ notVisible()
+ isInMainArea()
+ isCountEqualTo(0)
+ }
+ assertThat(noLongerVisible[1]).apply {
+ isKeyEqualTo("key1")
+ isRankEqualTo(1)
+ notVisible()
+ isInMainArea()
+ isCountEqualTo(0)
+ }
+ }
+
+ @Test
+ fun onNotificationListUpdated_itemsBecomeInvisible_logsNoLongerVisibleItems() =
+ testScope.runTest {
+ // GIVEN some visible Notifications are reported
+ val (ranks, locations) = fakeNotificationMaps("key0", "key1")
+ val callable = Callable { locations }
+ underTest.onNotificationListUpdated(callable, ranks)
+ runCurrent()
+ clearInvocations(mockStatusBarService, mockNotificationListenerService)
+
+ // WHEN the same Notifications are becoming invisible
+ val emptyCallable = Callable { emptyMap<String, Int>() }
+ underTest.onNotificationListUpdated(emptyCallable, ranks)
+ runCurrent()
+
+ // THEN visibility changes are reported
+ verify(mockStatusBarService)
+ .onNotificationVisibilityChanged(eq(emptyArray()), visibilityArrayCaptor.capture())
+ verifyZeroInteractions(mockNotificationListenerService)
+ val noLongerVisible = visibilityArrayCaptor.value
+ assertThat(noLongerVisible).hasLength(2)
+ assertThat(noLongerVisible[0]).apply {
+ isKeyEqualTo("key0")
+ isRankEqualTo(0)
+ notVisible()
+ isInMainArea()
+ isCountEqualTo(2)
+ }
+ assertThat(noLongerVisible[1]).apply {
+ isKeyEqualTo("key1")
+ isRankEqualTo(1)
+ notVisible()
+ isInMainArea()
+ isCountEqualTo(2)
+ }
+ }
+
+ @Test
+ fun onNotificationListUpdated_itemsChangedPositions_nothingLogged() =
+ testScope.runTest {
+ // GIVEN some visible Notifications are reported
+ val (ranks, locations) = fakeNotificationMaps("key0", "key1")
+ underTest.onNotificationListUpdated({ locations }, ranks)
+ runCurrent()
+ clearInvocations(mockStatusBarService, mockNotificationListenerService)
+
+ // WHEN the reported Notifications are changing positions
+ val (newRanks, newLocations) = fakeNotificationMaps("key1", "key0")
+ underTest.onNotificationListUpdated({ newLocations }, newRanks)
+ runCurrent()
+
+ // THEN no visibility changes are reported
+ verifyZeroInteractions(mockStatusBarService, mockNotificationListenerService)
+ }
+
+ @Test
+ fun onNotificationListUpdated_calledTwice_usesTheNewCallable() =
+ testScope.runTest {
+ // GIVEN some visible Notifications are reported
+ val (ranks, locations) = fakeNotificationMaps("key0", "key1", "key2")
+ val callable = spy(Callable { locations })
+ underTest.onNotificationListUpdated(callable, ranks)
+ runCurrent()
+ clearInvocations(callable)
+
+ // WHEN a new update comes
+ val otherCallable = spy(Callable { locations })
+ underTest.onNotificationListUpdated(otherCallable, ranks)
+ runCurrent()
+
+ // THEN we call the new Callable
+ verifyZeroInteractions(callable)
+ verify(otherCallable).call()
+ }
+
+ @Test
+ fun onLockscreenOrShadeNotInteractive_logsNoLongerVisibleItems() =
+ testScope.runTest {
+ // GIVEN some visible Notifications are reported
+ val (ranks, locations) = fakeNotificationMaps("key0", "key1")
+ val callable = Callable { locations }
+ underTest.onNotificationListUpdated(callable, ranks)
+ runCurrent()
+ clearInvocations(mockStatusBarService, mockNotificationListenerService)
+
+ // WHEN the Shade becomes non interactive
+ underTest.onLockscreenOrShadeNotInteractive(emptyList())
+ runCurrent()
+
+ // THEN visibility changes are reported
+ verify(mockStatusBarService)
+ .onNotificationVisibilityChanged(eq(emptyArray()), visibilityArrayCaptor.capture())
+ verifyZeroInteractions(mockNotificationListenerService)
+ val noLongerVisible = visibilityArrayCaptor.value
+ assertThat(noLongerVisible).hasLength(2)
+ assertThat(noLongerVisible[0]).apply {
+ isKeyEqualTo("key0")
+ isRankEqualTo(0)
+ notVisible()
+ isInMainArea()
+ isCountEqualTo(0)
+ }
+ assertThat(noLongerVisible[1]).apply {
+ isKeyEqualTo("key1")
+ isRankEqualTo(1)
+ notVisible()
+ isInMainArea()
+ isCountEqualTo(0)
+ }
+ }
+
+ @Test
+ fun onLockscreenOrShadeInteractive_logsPanelShown() =
+ testScope.runTest {
+ // WHEN the Shade becomes interactive
+ underTest.onLockscreenOrShadeInteractive(
+ isOnLockScreen = true,
+ listOf(
+ activeNotificationModel(
+ key = "key0",
+ uid = 0,
+ packageName = "com.android.first"
+ ),
+ activeNotificationModel(
+ key = "key1",
+ uid = 1,
+ packageName = "com.android.second"
+ ),
+ )
+ )
+ runCurrent()
+
+ // THEN the Panel shown event is reported
+ verify(mockPanelLogger).logPanelShown(eq(true), notificationListProtoCaptor.capture())
+ val loggedNotifications = notificationListProtoCaptor.value.notifications
+ assertThat(loggedNotifications.size).isEqualTo(2)
+ with(loggedNotifications[0]) {
+ assertThat(uid).isEqualTo(0)
+ assertThat(packageName).isEqualTo("com.android.first")
+ }
+ with(loggedNotifications[1]) {
+ assertThat(uid).isEqualTo(1)
+ assertThat(packageName).isEqualTo("com.android.second")
+ }
+ }
+
+ @Test
+ fun onNotificationExpanded_visibleLocation_expansionLogged() =
+ testScope.runTest {
+ // WHEN a Notification is expanded
+ underTest.onNotificationExpansionChanged(
+ key = "key",
+ isExpanded = true,
+ location = ExpandableViewState.LOCATION_MAIN_AREA,
+ isUserAction = true
+ )
+ runCurrent()
+
+ // THEN the Expand event is reported
+ verify(mockStatusBarService)
+ .onNotificationExpansionChanged(
+ /* key = */ "key",
+ /* userAction = */ true,
+ /* expanded = */ true,
+ NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal
+ )
+ }
+
+ @Test
+ fun onNotificationExpanded_notVisibleLocation_nothingLogged() =
+ testScope.runTest {
+ // WHEN a NOT visible Notification is expanded
+ underTest.onNotificationExpansionChanged(
+ key = "key",
+ isExpanded = true,
+ location = ExpandableViewState.LOCATION_BOTTOM_STACK_HIDDEN,
+ isUserAction = true
+ )
+ runCurrent()
+
+ // No events are reported
+ verifyZeroInteractions(mockStatusBarService)
+ }
+
+ @Test
+ fun onNotificationExpanded_notVisibleLocation_becomesVisible_expansionLogged() =
+ testScope.runTest {
+ // WHEN a NOT visible Notification is expanded
+ underTest.onNotificationExpansionChanged(
+ key = "key",
+ isExpanded = true,
+ location = ExpandableViewState.LOCATION_GONE,
+ isUserAction = true
+ )
+ runCurrent()
+
+ // AND it becomes visible
+ val (ranks, locations) = fakeNotificationMaps("key")
+ val callable = Callable { locations }
+ underTest.onNotificationListUpdated(callable, ranks)
+ runCurrent()
+
+ // THEN the Expand event is reported
+ verify(mockStatusBarService)
+ .onNotificationExpansionChanged(
+ /* key = */ "key",
+ /* userAction = */ true,
+ /* expanded = */ true,
+ NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal
+ )
+ }
+
+ @Test
+ fun onNotificationCollapsed_isFirstInteraction_nothingLogged() =
+ testScope.runTest {
+ // WHEN a Notification is collapsed, and it is the first interaction
+ underTest.onNotificationExpansionChanged(
+ key = "key",
+ isExpanded = false,
+ location = ExpandableViewState.LOCATION_MAIN_AREA,
+ isUserAction = false
+ )
+ runCurrent()
+
+ // THEN no events are reported, because we consider the Notification initially
+ // collapsed, so only expanded is logged in the first time.
+ verifyZeroInteractions(mockStatusBarService)
+ }
+
+ @Test
+ fun onNotificationExpandedAndCollapsed_expansionChangesLogged() =
+ testScope.runTest {
+ // GIVEN a Notification is expanded
+ underTest.onNotificationExpansionChanged(
+ key = "key",
+ isExpanded = true,
+ location = ExpandableViewState.LOCATION_MAIN_AREA,
+ isUserAction = true
+ )
+ runCurrent()
+
+ // WHEN the Notification is collapsed
+ verify(mockStatusBarService)
+ .onNotificationExpansionChanged(
+ /* key = */ "key",
+ /* userAction = */ true,
+ /* expanded = */ true,
+ NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal
+ )
+
+ // AND the Notification is expanded again
+ underTest.onNotificationExpansionChanged(
+ key = "key",
+ isExpanded = false,
+ location = ExpandableViewState.LOCATION_MAIN_AREA,
+ isUserAction = true
+ )
+ runCurrent()
+
+ // THEN the expansion changes are logged
+ verify(mockStatusBarService)
+ .onNotificationExpansionChanged(
+ /* key = */ "key",
+ /* userAction = */ true,
+ /* expanded = */ false,
+ NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal
+ )
+ }
+
+ private fun fakeNotificationMaps(
+ vararg keys: String
+ ): Pair<Map<String, Int>, Map<String, Int>> {
+ val ranks: Map<String, Int> = keys.mapIndexed { index, key -> key to index }.toMap()
+ val locations: Map<String, Int> =
+ keys.associateWith { ExpandableViewState.LOCATION_MAIN_AREA }
+
+ return Pair(ranks, locations)
+ }
+
+ private fun assertThat(visibility: NotificationVisibility) =
+ NotificationVisibilitySubject(visibility)
+}
+
+private class NotificationVisibilitySubject(private val visibility: NotificationVisibility) {
+ fun isKeyEqualTo(key: String) = assertThat(visibility.key).isEqualTo(key)
+ fun isRankEqualTo(rank: Int) = assertThat(visibility.rank).isEqualTo(rank)
+ fun isCountEqualTo(count: Int) = assertThat(visibility.count).isEqualTo(count)
+ fun isVisible() = assertThat(this.visibility.visible).isTrue()
+ fun notVisible() = assertThat(this.visibility.visible).isFalse()
+ fun isInMainArea() =
+ assertThat(this.visibility.location)
+ .isEqualTo(NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index c17a8ef62a4b..4188c5d34d98 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -38,6 +38,7 @@ import com.android.systemui.res.R
import com.android.systemui.runCurrent
import com.android.systemui.runTest
import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.statusbar.notification.dagger.NotificationStatsLoggerModule
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
@@ -71,6 +72,7 @@ class NotificationListViewModelTest : SysuiTestCase() {
FooterViewModelModule::class,
HeadlessSystemUserModeModule::class,
UnfoldTransitionModule.Bindings::class,
+ NotificationStatsLoggerModule::class,
]
)
interface TestComponent : SysUITestComponent<NotificationListViewModel> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModelTest.kt
new file mode 100644
index 000000000000..e9d88ccb0cbc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModelTest.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.scene.data.repository.windowRootViewVisibilityRepository
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NotificationLoggerViewModelTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+
+ private val testScope = kosmos.testScope
+ private val activeNotificationListRepository = kosmos.activeNotificationListRepository
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
+ private val powerInteractor = kosmos.powerInteractor
+ private val windowRootViewVisibilityRepository = kosmos.windowRootViewVisibilityRepository
+
+ private val underTest = kosmos.notificationListLoggerViewModel
+
+ @Test
+ fun isLockscreenOrShadeInteractive_deviceActiveAndShadeIsInteractive_true() =
+ testScope.runTest {
+ powerInteractor.setAwakeForTest()
+ windowRootViewVisibilityRepository.setIsLockscreenOrShadeVisible(true)
+
+ val actual by collectLastValue(underTest.isLockscreenOrShadeInteractive)
+
+ assertThat(actual).isTrue()
+ }
+
+ @Test
+ fun isLockscreenOrShadeInteractive_deviceIsAsleepAndShadeIsInteractive_false() =
+ testScope.runTest {
+ powerInteractor.setAsleepForTest()
+ windowRootViewVisibilityRepository.setIsLockscreenOrShadeVisible(true)
+
+ val actual by collectLastValue(underTest.isLockscreenOrShadeInteractive)
+
+ assertThat(actual).isFalse()
+ }
+
+ @Test
+ fun isLockscreenOrShadeInteractive_deviceActiveAndShadeIsNotInteractive_false() =
+ testScope.runTest {
+ powerInteractor.setAwakeForTest()
+ windowRootViewVisibilityRepository.setIsLockscreenOrShadeVisible(false)
+
+ val actual by collectLastValue(underTest.isLockscreenOrShadeInteractive)
+
+ assertThat(actual).isFalse()
+ }
+
+ @Test
+ fun activeNotifications_hasNotifications() =
+ testScope.runTest {
+ activeNotificationListRepository.setActiveNotifs(5)
+
+ val notifs by collectLastValue(underTest.activeNotifications)
+
+ assertThat(notifs).hasSize(5)
+ requireNotNull(notifs).forEachIndexed { i, notif ->
+ assertThat(notif.key).isEqualTo("$i")
+ }
+ }
+
+ @Test
+ fun activeNotifications_isEmpty() =
+ testScope.runTest {
+ activeNotificationListRepository.setActiveNotifs(0)
+
+ val notifications by collectLastValue(underTest.activeNotifications)
+
+ assertThat(notifications).isEmpty()
+ }
+
+ @Test
+ fun activeNotificationRanks_hasNotifications() =
+ testScope.runTest {
+ val keys = (0..4).map { "$it" }
+ activeNotificationListRepository.setActiveNotifs(5)
+
+ val rankingsMap by collectLastValue(underTest.activeNotificationRanks)
+
+ assertThat(rankingsMap).hasSize(5)
+ keys.forEachIndexed { rank, key -> assertThat(rankingsMap).containsEntry(key, rank) }
+ }
+
+ @Test
+ fun activeNotificationRanks_isEmpty() =
+ testScope.runTest {
+ activeNotificationListRepository.setActiveNotifs(0)
+
+ val rankingsMap by collectLastValue(underTest.activeNotificationRanks)
+
+ assertThat(rankingsMap).isEmpty()
+ }
+
+ @Test
+ fun isOnLockScreen_true() =
+ testScope.runTest {
+ keyguardRepository.setKeyguardShowing(true)
+
+ val isOnLockScreen by collectLastValue(underTest.isOnLockScreen)
+
+ assertThat(isOnLockScreen).isTrue()
+ }
+ @Test
+ fun isOnLockScreen_false() =
+ testScope.runTest {
+ keyguardRepository.setKeyguardShowing(false)
+
+ val isOnLockScreen by collectLastValue(underTest.isOnLockScreen)
+
+ assertThat(isOnLockScreen).isFalse()
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/android/service/notification/NotificationListenerServiceKosmos.kt b/packages/SystemUI/tests/utils/src/android/service/notification/NotificationListenerServiceKosmos.kt
new file mode 100644
index 000000000000..bff0d0e2ec81
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/service/notification/NotificationListenerServiceKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.notification
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.notificationListenerService by Fixture { mockNotificationListenerService }
+val Kosmos.mockNotificationListenerService by Fixture { mock<NotificationListenerService>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
index fdb9b30e546e..b18859dc4b25 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
@@ -18,9 +18,11 @@ package com.android.systemui
import android.app.ActivityManager
import android.app.admin.DevicePolicyManager
import android.os.UserManager
+import android.service.notification.NotificationListenerService
import android.util.DisplayMetrics
import android.view.LayoutInflater
import com.android.internal.logging.MetricsLogger
+import com.android.internal.statusbar.IStatusBarService
import com.android.keyguard.KeyguardSecurityModel
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardViewController
@@ -49,9 +51,11 @@ 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.logging.NotificationPanelLogger
import com.android.systemui.statusbar.notification.stack.AmbientState
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.phone.LSShadeTransitionLogger
@@ -59,6 +63,7 @@ import com.android.systemui.statusbar.phone.ScreenOffAnimationController
import com.android.systemui.statusbar.phone.ScrimController
import com.android.systemui.statusbar.phone.SystemUIDialogManager
import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.ZenModeController
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
@@ -80,6 +85,7 @@ data class TestMocksModule(
@get:Provides val deviceProvisionedController: DeviceProvisionedController = mock(),
@get:Provides val dozeParameters: DozeParameters = mock(),
@get:Provides val dumpManager: DumpManager = mock(),
+ @get:Provides val headsUpManager: HeadsUpManager = mock(),
@get:Provides val guestResumeSessionReceiver: GuestResumeSessionReceiver = mock(),
@get:Provides val keyguardBypassController: KeyguardBypassController = mock(),
@get:Provides val keyguardSecurityModel: KeyguardSecurityModel = mock(),
@@ -89,8 +95,10 @@ data class TestMocksModule(
val lockscreenShadeTransitionController: LockscreenShadeTransitionController = mock(),
@get:Provides val mediaHierarchyManager: MediaHierarchyManager = mock(),
@get:Provides val notifCollection: NotifCollection = mock(),
+ @get:Provides val notificationListLogger: NotificationStatsLogger = mock(),
@get:Provides val notificationListener: NotificationListener = mock(),
@get:Provides val notificationLockscreenUserManager: NotificationLockscreenUserManager = mock(),
+ @get:Provides val notificationPanelLogger: NotificationPanelLogger = mock(),
@get:Provides val notificationMediaManager: NotificationMediaManager = mock(),
@get:Provides val notificationShadeDepthController: NotificationShadeDepthController = mock(),
@get:Provides
@@ -129,6 +137,10 @@ data class TestMocksModule(
@get:Provides val displayMetrics: DisplayMetrics = mock(),
@get:Provides val metricsLogger: MetricsLogger = mock(),
@get:Provides val userManager: UserManager = mock(),
+
+ // system server mocks
+ @get:Provides val mockStatusBarService: IStatusBarService = mock(),
+ @get:Provides val mockNotificationListenerService: NotificationListenerService = mock(),
) {
@Module
interface Bindings {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifCollectionKosmos.kt
index d98f49684999..9b27a9fa818f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifCollectionKosmos.kt
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.systemui.statusbar.notification.collection
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
import com.android.systemui.util.mockito.mock
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.notifCollection by Fixture { mockNotifCollection }
+val Kosmos.mockNotifCollection by Fixture { mock<NotifCollection>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
index 9851b0ef9e94..9c5c48670ff4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.data.model
import android.graphics.drawable.Icon
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import com.android.systemui.statusbar.notification.stack.BUCKET_UNKNOWN
/** Simple ActiveNotificationModel builder for use in tests. */
fun activeNotificationModel(
@@ -32,6 +33,11 @@ fun activeNotificationModel(
aodIcon: Icon? = null,
shelfIcon: Icon? = null,
statusBarIcon: Icon? = null,
+ uid: Int = 0,
+ instanceId: Int? = null,
+ isGroupSummary: Boolean = false,
+ packageName: String = "pkg",
+ bucket: Int = BUCKET_UNKNOWN,
) =
ActiveNotificationModel(
key = key,
@@ -45,4 +51,9 @@ fun activeNotificationModel(
aodIcon = aodIcon,
shelfIcon = shelfIcon,
statusBarIcon = statusBarIcon,
+ uid = uid,
+ packageName = packageName,
+ instanceId = instanceId,
+ isGroupSummary = isGroupSummary,
+ bucket = bucket,
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
index cb1ba206d110..b40e1e7ab33b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
@@ -20,11 +20,20 @@ import com.android.systemui.statusbar.notification.data.model.activeNotification
/**
* Make the repository hold [count] active notifications for testing. The keys of the notifications
- * are "0", "1", ..., (count - 1).toString().
+ * are "0", "1", ..., (count - 1).toString(). The ranks are the same values in Int.
*/
fun ActiveNotificationListRepository.setActiveNotifs(count: Int) {
this.activeNotifications.value =
ActiveNotificationsStore.Builder()
- .apply { repeat(count) { i -> addEntry(activeNotificationModel(key = i.toString())) } }
+ .apply {
+ val rankingsMap = mutableMapOf<String, Int>()
+ repeat(count) { i ->
+ val key = "$i"
+ addEntry(activeNotificationModel(key = key))
+ rankingsMap[key] = i
+ }
+
+ setRankingsMap(rankingsMap)
+ }
.build()
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt
index 67fecb4d8bcd..acc455f74946 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt
@@ -19,8 +19,8 @@ package com.android.systemui.statusbar.notification.icon.ui.viewbinder
import com.android.systemui.common.ui.configurationState
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.notification.collection.notifCollection
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.notificationIconContainerShelfViewModel
-import com.android.systemui.statusbar.notification.stack.ui.viewbinder.notifCollection
import com.android.systemui.statusbar.ui.systemBarUtilsState
val Kosmos.notificationIconContainerShelfViewBinder by Fixture {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.kt
new file mode 100644
index 000000000000..30fc2f3af987
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.logging
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.notificationPanelLogger by Fixture { mockNotificationPanelLogger }
+val Kosmos.mockNotificationPanelLogger by Fixture { mock<NotificationPanelLogger>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt
new file mode 100644
index 000000000000..de52155dce79
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.view
+
+import android.service.notification.notificationListenerService
+import com.android.internal.statusbar.statusBarService
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.notification.logging.notificationPanelLogger
+
+val Kosmos.notificationStatsLogger by Fixture {
+ NotificationStatsLoggerImpl(
+ applicationScope = testScope,
+ bgDispatcher = testDispatcher,
+ statusBarService = statusBarService,
+ notificationListenerService = notificationListenerService,
+ notificationPanelLogger = notificationPanelLogger,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
index 04716b9c48a3..c6498e48dd52 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
@@ -23,8 +23,10 @@ import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.notificationIconContainerShelfViewBinder
+import com.android.systemui.statusbar.notification.stack.ui.view.notificationStatsLogger
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel
import com.android.systemui.statusbar.phone.notificationIconAreaController
+import java.util.Optional
val Kosmos.notificationListViewBinder by Fixture {
NotificationListViewBinder(
@@ -35,5 +37,6 @@ val Kosmos.notificationListViewBinder by Fixture {
iconAreaController = notificationIconAreaController,
metricsLogger = metricsLogger,
nicBinder = notificationIconContainerShelfViewBinder,
+ loggerOptional = Optional.of(notificationStatsLogger),
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListLoggerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListLoggerViewModelKosmos.kt
new file mode 100644
index 000000000000..08bda463261d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListLoggerViewModelKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
+
+val Kosmos.notificationListLoggerViewModel by Fixture {
+ NotificationLoggerViewModel(
+ keyguardInteractor = keyguardInteractor,
+ windowRootViewVisibilityInteractor = windowRootViewVisibilityInteractor,
+ activeNotificationsInteractor = activeNotificationsInteractor,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
index f5a4c034d836..998e579b14fc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
@@ -33,6 +33,7 @@ val Kosmos.notificationListViewModel by Fixture {
shelf = notificationShelfViewModel,
hideListViewModel = hideListViewModel,
footer = Optional.of(footerViewModel),
+ logger = Optional.of(notificationListLoggerViewModel),
activeNotificationsInteractor = activeNotificationsInteractor,
keyguardTransitionInteractor = keyguardTransitionInteractor,
seenNotificationsInteractor = seenNotificationsInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt
index e4313bb168b8..d9beabb11ad9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt
@@ -19,7 +19,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel
import com.android.systemui.keyguard.data.repository.keyguardRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.data.repository.windowRootViewVisibilityRepository
import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor
@@ -27,7 +27,7 @@ import com.android.systemui.statusbar.policy.headsUpManager
val Kosmos.windowRootViewVisibilityInteractor by Fixture {
WindowRootViewVisibilityInteractor(
- scope = testScope,
+ scope = applicationCoroutineScope,
windowRootViewVisibilityRepository = windowRootViewVisibilityRepository,
keyguardRepository = keyguardRepository,
headsUpManager = headsUpManager,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt
index 1f48d940f91c..11c09ee3d343 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt
@@ -17,6 +17,7 @@ package com.android.systemui.util.concurrency
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.util.time.FakeSystemClock
import dagger.Binds
import dagger.Module
@@ -27,8 +28,9 @@ import java.util.concurrent.Executor
interface FakeExecutorModule {
@Binds @Main @SysUISingleton fun bindMainExecutor(executor: FakeExecutor): Executor
+ @Binds @UiBackground @SysUISingleton fun bindUiBgExecutor(executor: FakeExecutor): Executor
+
companion object {
- @Provides
- fun provideFake(clock: FakeSystemClock) = FakeExecutor(clock)
+ @Provides fun provideFake(clock: FakeSystemClock) = FakeExecutor(clock)
}
}