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 androidx.lifecycle.lifecycleScope
21 import com.android.app.tracing.TraceUtils.traceAsync
22 import com.android.internal.logging.MetricsLogger
23 import com.android.internal.logging.nano.MetricsProto
24 import com.android.systemui.common.ui.ConfigurationState
25 import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo
26 import com.android.systemui.dagger.qualifiers.Background
27 import com.android.systemui.lifecycle.repeatWhenAttached
28 import com.android.systemui.plugins.FalsingManager
29 import com.android.systemui.res.R
30 import com.android.systemui.statusbar.NotificationShelf
31 import com.android.systemui.statusbar.notification.NotificationActivityStarter
32 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController
33 import com.android.systemui.statusbar.notification.dagger.SilentHeader
34 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
35 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
36 import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
37 import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
38 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
39 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
40 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
41 import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
42 import com.android.systemui.statusbar.notification.stack.DisplaySwitchNotificationsHiderTracker
43 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
44 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
45 import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger
46 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList
47 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
48 import com.android.systemui.statusbar.notification.ui.viewbinder.HeadsUpNotificationViewBinder
49 import com.android.systemui.statusbar.phone.NotificationIconAreaController
50 import com.android.systemui.util.kotlin.awaitCancellationThenDispose
51 import com.android.systemui.util.kotlin.getOrNull
52 import com.android.systemui.util.ui.isAnimating
53 import com.android.systemui.util.ui.value
54 import java.util.Optional
55 import javax.inject.Inject
56 import javax.inject.Provider
57 import kotlinx.coroutines.CoroutineDispatcher
58 import kotlinx.coroutines.DisposableHandle
59 import kotlinx.coroutines.awaitCancellation
60 import kotlinx.coroutines.coroutineScope
61 import kotlinx.coroutines.flow.StateFlow
62 import kotlinx.coroutines.flow.collectLatest
63 import kotlinx.coroutines.flow.combine
64 import kotlinx.coroutines.flow.flowOn
65 import kotlinx.coroutines.flow.stateIn
66 import kotlinx.coroutines.launch
67 
68 /** Binds a [NotificationStackScrollLayout] to its [view model][NotificationListViewModel]. */
69 class NotificationListViewBinder
70 @Inject
71 constructor(
72     @Background private val backgroundDispatcher: CoroutineDispatcher,
73     private val hiderTracker: DisplaySwitchNotificationsHiderTracker,
74     private val configuration: ConfigurationState,
75     private val falsingManager: FalsingManager,
76     private val hunBinder: HeadsUpNotificationViewBinder,
77     private val iconAreaController: NotificationIconAreaController,
78     private val loggerOptional: Optional<NotificationStatsLogger>,
79     private val metricsLogger: MetricsLogger,
80     private val nicBinder: NotificationIconContainerShelfViewBinder,
81     // Using a provider to avoid a circular dependency.
82     private val notificationActivityStarter: Provider<NotificationActivityStarter>,
83     @SilentHeader private val silentHeaderController: SectionHeaderController,
84     private val viewModel: NotificationListViewModel,
85 ) {
86 
87     fun bindWhileAttached(
88         view: NotificationStackScrollLayout,
89         viewController: NotificationStackScrollLayoutController
90     ) {
91         val shelf =
92             LayoutInflater.from(view.context)
93                 .inflate(R.layout.status_bar_notification_shelf, view, false) as NotificationShelf
94         view.setShelf(shelf)
95 
96         view.repeatWhenAttached {
97             lifecycleScope.launch {
98                 if (NotificationsHeadsUpRefactor.isEnabled) {
99                     launch { hunBinder.bindHeadsUpNotifications(view) }
100                 }
101                 launch { bindShelf(shelf) }
102                 bindHideList(viewController, viewModel, hiderTracker)
103 
104                 if (FooterViewRefactor.isEnabled) {
105                     val hasNonClearableSilentNotifications: StateFlow<Boolean> =
106                         viewModel.hasNonClearableSilentNotifications.stateIn(this)
107                     launch { reinflateAndBindFooter(view, hasNonClearableSilentNotifications) }
108                     launch { bindEmptyShade(view) }
109                     launch {
110                         bindSilentHeaderClickListener(view, hasNonClearableSilentNotifications)
111                     }
112                     launch {
113                         viewModel.isImportantForAccessibility.collect { isImportantForAccessibility
114                             ->
115                             view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
116                         }
117                     }
118                 }
119 
120                 launch { bindLogger(view) }
121             }
122         }
123     }
124 
125     private suspend fun bindShelf(shelf: NotificationShelf) {
126         NotificationShelfViewBinder.bind(
127             shelf,
128             viewModel.shelf,
129             falsingManager,
130             nicBinder,
131             iconAreaController,
132         )
133     }
134 
135     private suspend fun reinflateAndBindFooter(
136         parentView: NotificationStackScrollLayout,
137         hasNonClearableSilentNotifications: StateFlow<Boolean>
138     ) {
139         viewModel.footer.getOrNull()?.let { footerViewModel ->
140             // The footer needs to be re-inflated every time the theme or the font size changes.
141             configuration
142                 .inflateLayout<FooterView>(
143                     R.layout.status_bar_notification_footer,
144                     parentView,
145                     attachToRoot = false,
146                 )
147                 .flowOn(backgroundDispatcher)
148                 .collectLatest { footerView: FooterView ->
149                     traceAsync("bind FooterView") {
150                         parentView.setFooterView(footerView)
151                         bindFooter(
152                             footerView,
153                             footerViewModel,
154                             parentView,
155                             hasNonClearableSilentNotifications
156                         )
157                     }
158                 }
159         }
160     }
161 
162     /**
163      * Binds the footer (including its visibility) and dispose of the [DisposableHandle] when done.
164      */
165     private suspend fun bindFooter(
166         footerView: FooterView,
167         footerViewModel: FooterViewModel,
168         parentView: NotificationStackScrollLayout,
169         hasNonClearableSilentNotifications: StateFlow<Boolean>
170     ): Unit = coroutineScope {
171         val disposableHandle =
172             FooterViewBinder.bindWhileAttached(
173                 footerView,
174                 footerViewModel,
175                 clearAllNotifications = {
176                     clearAllNotifications(
177                         parentView,
178                         // Hide the silent section header (if present) if there will be
179                         // no remaining silent notifications upon clearing.
180                         hideSilentSection = !hasNonClearableSilentNotifications.value,
181                     )
182                 },
183                 launchNotificationSettings = { view ->
184                     notificationActivityStarter
185                         .get()
186                         .startHistoryIntent(view, /* showHistory = */ false)
187                 },
188                 launchNotificationHistory = { view ->
189                     notificationActivityStarter
190                         .get()
191                         .startHistoryIntent(view, /* showHistory = */ true)
192                 },
193             )
194         launch {
195             viewModel.shouldIncludeFooterView.collect { animatedVisibility ->
196                 footerView.setVisible(
197                     /* visible = */ animatedVisibility.value,
198                     /* animate = */ animatedVisibility.isAnimating,
199                 )
200             }
201         }
202         launch { viewModel.shouldHideFooterView.collect { footerView.setShouldBeHidden(it) } }
203         disposableHandle.awaitCancellationThenDispose()
204     }
205 
206     private suspend fun bindEmptyShade(parentView: NotificationStackScrollLayout) {
207         combine(
208                 viewModel.shouldShowEmptyShadeView,
209                 viewModel.areNotificationsHiddenInShade,
210                 viewModel.hasFilteredOutSeenNotifications,
211                 ::Triple
212             )
213             .collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) ->
214                 parentView.updateEmptyShadeView(
215                     shouldShow,
216                     areNotifsHidden,
217                     hasFilteredNotifs,
218                 )
219             }
220     }
221 
222     private suspend fun bindSilentHeaderClickListener(
223         parentView: NotificationStackScrollLayout,
224         hasNonClearableSilentNotifications: StateFlow<Boolean>,
225     ): Unit = coroutineScope {
226         val hasClearableAlertingNotifications: StateFlow<Boolean> =
227             viewModel.hasClearableAlertingNotifications.stateIn(this)
228         silentHeaderController.setOnClearSectionClickListener {
229             clearSilentNotifications(
230                 view = parentView,
231                 // Leave the shade open if there will be other notifs left over to clear.
232                 closeShade = !hasClearableAlertingNotifications.value,
233                 // Hide the silent section header itself, if there will be no remaining silent
234                 // notifications upon clearing.
235                 hideSilentSection = !hasNonClearableSilentNotifications.value,
236             )
237         }
238         try {
239             awaitCancellation()
240         } finally {
241             silentHeaderController.setOnClearSectionClickListener {}
242         }
243     }
244 
245     private fun clearAllNotifications(
246         view: NotificationStackScrollLayout,
247         hideSilentSection: Boolean,
248     ) {
249         metricsLogger.action(MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES)
250         view.clearAllNotifications(hideSilentSection)
251     }
252 
253     private fun clearSilentNotifications(
254         view: NotificationStackScrollLayout,
255         closeShade: Boolean,
256         hideSilentSection: Boolean
257     ) {
258         view.clearSilentNotifications(closeShade, hideSilentSection)
259     }
260 
261     private suspend fun bindLogger(view: NotificationStackScrollLayout) {
262         if (NotificationsLiveDataStoreRefactor.isEnabled) {
263             viewModel.logger.getOrNull()?.let { viewModel ->
264                 loggerOptional.getOrNull()?.let { logger ->
265                     NotificationStatsLoggerBinder.bindLogger(
266                         view,
267                         logger,
268                         viewModel,
269                     )
270                 }
271             }
272         }
273     }
274 }
275