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