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