diff options
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) } } |