• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.notification.stack.ui.viewbinder
18 
19 import android.view.LayoutInflater
20 import android.view.View
21 import androidx.lifecycle.lifecycleScope
22 import com.android.app.tracing.TraceUtils.traceAsync
23 import com.android.app.tracing.coroutines.launchTraced as launch
24 import com.android.internal.logging.MetricsLogger
25 import com.android.internal.logging.nano.MetricsProto
26 import com.android.systemui.common.ui.ConfigurationState
27 import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo
28 import com.android.systemui.dagger.qualifiers.NotifInflation
29 import com.android.systemui.lifecycle.repeatWhenAttached
30 import com.android.systemui.lifecycle.repeatWhenAttachedToWindow
31 import com.android.systemui.plugins.FalsingManager
32 import com.android.systemui.res.R
33 import com.android.systemui.scene.shared.flag.SceneContainerFlag
34 import com.android.systemui.shade.ShadeDisplayAware
35 import com.android.systemui.statusbar.NotificationShelf
36 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
37 import com.android.systemui.statusbar.notification.NotificationActivityStarter
38 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController
39 import com.android.systemui.statusbar.notification.dagger.SilentHeader
40 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
41 import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView
42 import com.android.systemui.statusbar.notification.emptyshade.ui.viewbinder.EmptyShadeViewBinder
43 import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
44 import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter
45 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
46 import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
47 import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
48 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
49 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
50 import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
51 import com.android.systemui.statusbar.notification.stack.DisplaySwitchNotificationsHiderTracker
52 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
53 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
54 import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger
55 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList
56 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
57 import com.android.systemui.statusbar.notification.ui.viewbinder.HeadsUpNotificationViewBinder
58 import com.android.systemui.util.kotlin.awaitCancellationThenDispose
59 import com.android.systemui.util.kotlin.getOrNull
60 import com.android.systemui.util.ui.isAnimating
61 import com.android.systemui.util.ui.stopAnimating
62 import com.android.systemui.util.ui.value
63 import java.util.Optional
64 import javax.inject.Inject
65 import javax.inject.Provider
66 import kotlinx.coroutines.CoroutineDispatcher
67 import kotlinx.coroutines.DisposableHandle
68 import kotlinx.coroutines.awaitCancellation
69 import kotlinx.coroutines.coroutineScope
70 import kotlinx.coroutines.flow.StateFlow
71 import kotlinx.coroutines.flow.collectLatest
72 import kotlinx.coroutines.flow.combine
73 import kotlinx.coroutines.flow.flowOn
74 import kotlinx.coroutines.flow.stateIn
75 
76 /** Binds a [NotificationStackScrollLayout] to its [view model][NotificationListViewModel]. */
77 class NotificationListViewBinder
78 @Inject
79 constructor(
80     @NotifInflation private val inflationDispatcher: CoroutineDispatcher,
81     private val hiderTracker: DisplaySwitchNotificationsHiderTracker,
82     @ShadeDisplayAware private val configuration: ConfigurationState,
83     private val falsingManager: FalsingManager,
84     private val hunBinder: HeadsUpNotificationViewBinder,
85     private val loggerOptional: Optional<NotificationStatsLogger>,
86     private val metricsLogger: MetricsLogger,
87     private val nicBinder: NotificationIconContainerShelfViewBinder,
88     // Using a provider to avoid a circular dependency.
89     private val notificationActivityStarter: Provider<NotificationActivityStarter>,
90     @SilentHeader private val silentHeaderController: SectionHeaderController,
91     private val viewModel: NotificationListViewModel,
92 ) {
93 
94     fun bindWhileAttached(
95         view: NotificationStackScrollLayout,
96         viewController: NotificationStackScrollLayoutController,
97     ) {
98         val shelf =
99             LayoutInflater.from(view.context)
100                 .inflate(R.layout.status_bar_notification_shelf, view, false) as NotificationShelf
101         view.setShelf(shelf)
102 
103         // Create viewModels once, and only when needed.
104         val footerViewModel by lazy { viewModel.footerViewModelFactory.create() }
105         val emptyShadeViewModel by lazy { viewModel.emptyShadeViewModelFactory.create() }
106         view.repeatWhenAttached {
107             lifecycleScope.launch {
108                 if (SceneContainerFlag.isEnabled) {
109                     launch { hunBinder.bindHeadsUpNotifications(view) }
110                 }
111                 launch { bindShelf(shelf) }
112                 bindHideList(viewController, viewModel, hiderTracker)
113 
114                 val hasNonClearableSilentNotifications: StateFlow<Boolean> =
115                     viewModel.hasNonClearableSilentNotifications.stateIn(this)
116                 launch {
117                     reinflateAndBindFooter(
118                         footerViewModel,
119                         view,
120                         hasNonClearableSilentNotifications,
121                     )
122                 }
123                 launch {
124                     if (ModesEmptyShadeFix.isEnabled) {
125                         reinflateAndBindEmptyShade(emptyShadeViewModel, view)
126                     } else {
127                         bindEmptyShadeLegacy(emptyShadeViewModel, view)
128                     }
129                 }
130                 launch { bindSilentHeaderClickListener(view, hasNonClearableSilentNotifications) }
131                 launch {
132                     viewModel.isImportantForAccessibility.collect { isImportantForAccessibility ->
133                         view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
134                     }
135                 }
136 
137                 if (StatusBarNotifChips.isEnabled) {
138                     launch {
139                         viewModel.visibleStatusBarChipKeys.collect { keys ->
140                             viewController.updateStatusBarChipKeys(keys)
141                         }
142                     }
143                 }
144 
145                 launch { bindLogger(view) }
146             }
147         }
148     }
149 
150     private suspend fun bindShelf(shelf: NotificationShelf) {
151         NotificationShelfViewBinder.bind(shelf, viewModel.shelf, falsingManager, nicBinder)
152     }
153 
154     private suspend fun reinflateAndBindFooter(
155         footerViewModel: FooterViewModel,
156         parentView: NotificationStackScrollLayout,
157         hasNonClearableSilentNotifications: StateFlow<Boolean>,
158     ) {
159         // The footer needs to be re-inflated every time the theme or the font size changes.
160         configuration
161             .inflateLayout<FooterView>(
162                 if (NotifRedesignFooter.isEnabled) R.layout.notification_2025_footer
163                 else R.layout.status_bar_notification_footer,
164                 parentView,
165                 attachToRoot = false,
166             )
167             .flowOn(inflationDispatcher)
168             .collectLatest { footerView: FooterView ->
169                 traceAsync("bind FooterView") {
170                     parentView.setFooterView(footerView)
171                     bindFooter(
172                         footerView,
173                         footerViewModel,
174                         parentView,
175                         hasNonClearableSilentNotifications,
176                     )
177                 }
178             }
179     }
180 
181     /**
182      * Binds the footer (including its visibility) and dispose of the [DisposableHandle] when done.
183      */
184     private suspend fun bindFooter(
185         footerView: FooterView,
186         footerViewModel: FooterViewModel,
187         parentView: NotificationStackScrollLayout,
188         hasNonClearableSilentNotifications: StateFlow<Boolean>,
189     ): Unit = coroutineScope {
190         val disposableHandle =
191             FooterViewBinder.bindWhileAttached(
192                 footerView,
193                 footerViewModel,
194                 {
195                     clearAllNotifications(
196                         parentView,
197                         // Hide the silent section header (if present) if there will be
198                         // no remaining silent notifications upon clearing.
199                         hideSilentSection = !hasNonClearableSilentNotifications.value,
200                     )
201                 },
202                 launchNotificationSettings,
203                 launchNotificationHistory,
204                 notificationActivityStarter.get(),
205             )
206         if (SceneContainerFlag.isEnabled) {
207             launch {
208                 viewModel.shouldShowFooterView.collect { animatedVisibility ->
209                     footerView.setVisible(
210                         /* visible = */ animatedVisibility.value,
211                         /* animate = */ animatedVisibility.isAnimating,
212                     ) {
213                         animatedVisibility.stopAnimating()
214                     }
215                 }
216             }
217         } else {
218             launch {
219                 viewModel.shouldIncludeFooterView.collect { animatedVisibility ->
220                     footerView.setVisible(
221                         /* visible = */ animatedVisibility.value,
222                         /* animate = */ animatedVisibility.isAnimating,
223                     )
224                 }
225             }
226             launch { viewModel.shouldHideFooterView.collect { footerView.setShouldBeHidden(it) } }
227         }
228         disposableHandle.awaitCancellationThenDispose()
229     }
230 
231     private val launchNotificationSettings: (View) -> Unit = { view: View ->
232         notificationActivityStarter.get().startHistoryIntent(view, /* showHistory= */ false)
233     }
234 
235     private val launchNotificationHistory: (View) -> Unit = { view ->
236         notificationActivityStarter.get().startHistoryIntent(view, /* showHistory= */ true)
237     }
238 
239     private suspend fun reinflateAndBindEmptyShade(
240         emptyShadeViewModel: EmptyShadeViewModel,
241         parentView: NotificationStackScrollLayout,
242     ) {
243         ModesEmptyShadeFix.unsafeAssertInNewMode()
244         // The empty shade needs to be re-inflated every time the theme or the font size
245         // changes.
246         configuration
247             .inflateLayout<EmptyShadeView>(
248                 R.layout.status_bar_no_notifications,
249                 parentView,
250                 attachToRoot = false,
251             )
252             .flowOn(inflationDispatcher)
253             .collectLatest { emptyShadeView: EmptyShadeView ->
254                 traceAsync("bind EmptyShadeView") {
255                     parentView.setEmptyShadeView(emptyShadeView)
256                     bindEmptyShade(emptyShadeView, emptyShadeViewModel)
257                 }
258             }
259     }
260 
261     private suspend fun bindEmptyShadeLegacy(
262         emptyShadeViewModel: EmptyShadeViewModel,
263         parentView: NotificationStackScrollLayout,
264     ) {
265         ModesEmptyShadeFix.assertInLegacyMode()
266         combine(
267                 viewModel.shouldShowEmptyShadeView,
268                 emptyShadeViewModel.areNotificationsHiddenInShade,
269                 emptyShadeViewModel.hasFilteredOutSeenNotifications,
270                 ::Triple,
271             )
272             .collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) ->
273                 parentView.updateEmptyShadeView(shouldShow, areNotifsHidden, hasFilteredNotifs)
274             }
275     }
276 
277     private suspend fun bindEmptyShade(
278         emptyShadeView: EmptyShadeView,
279         emptyShadeViewModel: EmptyShadeViewModel,
280     ): Unit = coroutineScope {
281         ModesEmptyShadeFix.unsafeAssertInNewMode()
282         launch {
283             emptyShadeView.repeatWhenAttachedToWindow {
284                 EmptyShadeViewBinder.bind(
285                     emptyShadeView,
286                     emptyShadeViewModel,
287                     notificationActivityStarter.get(),
288                 )
289             }
290         }
291         launch {
292             viewModel.shouldShowEmptyShadeViewAnimated.collect { shouldShow ->
293                 emptyShadeView.setVisible(shouldShow.value, shouldShow.isAnimating) {
294                     shouldShow.stopAnimating()
295                 }
296             }
297         }
298     }
299 
300     private suspend fun bindSilentHeaderClickListener(
301         parentView: NotificationStackScrollLayout,
302         hasNonClearableSilentNotifications: StateFlow<Boolean>,
303     ): Unit = coroutineScope {
304         val hasClearableAlertingNotifications: StateFlow<Boolean> =
305             viewModel.hasClearableAlertingNotifications.stateIn(this)
306         silentHeaderController.setOnClearSectionClickListener {
307             clearSilentNotifications(
308                 view = parentView,
309                 // Leave the shade open if there will be other notifs left over to clear.
310                 closeShade = !hasClearableAlertingNotifications.value,
311                 // Hide the silent section header itself, if there will be no remaining silent
312                 // notifications upon clearing.
313                 hideSilentSection = !hasNonClearableSilentNotifications.value,
314             )
315         }
316         try {
317             awaitCancellation()
318         } finally {
319             silentHeaderController.setOnClearSectionClickListener {}
320         }
321     }
322 
323     private fun clearAllNotifications(
324         view: NotificationStackScrollLayout,
325         hideSilentSection: Boolean,
326     ) {
327         metricsLogger.action(MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES)
328         view.clearAllNotifications(hideSilentSection)
329     }
330 
331     private fun clearSilentNotifications(
332         view: NotificationStackScrollLayout,
333         closeShade: Boolean,
334         hideSilentSection: Boolean,
335     ) {
336         view.clearSilentNotifications(closeShade, hideSilentSection)
337     }
338 
339     private suspend fun bindLogger(view: NotificationStackScrollLayout) {
340         if (NotificationsLiveDataStoreRefactor.isEnabled) {
341             viewModel.logger.getOrNull()?.let { viewModel ->
342                 loggerOptional.getOrNull()?.let { logger ->
343                     NotificationStatsLoggerBinder.bindLogger(view, logger, viewModel)
344                 }
345             }
346         }
347     }
348 }
349