1 /* <lambda>null2 * Copyright (C) 2019 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 package com.android.systemui.statusbar.notification.stack 17 18 import android.annotation.ColorInt 19 import android.util.Log 20 import android.view.View 21 import com.android.internal.annotations.VisibleForTesting 22 import com.android.systemui.media.controls.ui.controller.KeyguardMediaController 23 import com.android.systemui.shade.ShadeDisplayAware 24 import com.android.systemui.statusbar.notification.SourceType 25 import com.android.systemui.statusbar.notification.collection.NotificationClassificationFlag 26 import com.android.systemui.statusbar.notification.collection.render.MediaContainerController 27 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController 28 import com.android.systemui.statusbar.notification.dagger.AlertingHeader 29 import com.android.systemui.statusbar.notification.dagger.IncomingHeader 30 import com.android.systemui.statusbar.notification.dagger.NewsHeader 31 import com.android.systemui.statusbar.notification.dagger.PeopleHeader 32 import com.android.systemui.statusbar.notification.dagger.PromoHeader 33 import com.android.systemui.statusbar.notification.dagger.RecsHeader 34 import com.android.systemui.statusbar.notification.dagger.SilentHeader 35 import com.android.systemui.statusbar.notification.dagger.SocialHeader 36 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow 37 import com.android.systemui.statusbar.notification.row.ExpandableView 38 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi 39 import com.android.systemui.statusbar.notification.stack.PriorityBucket 40 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.SectionProvider 41 import com.android.systemui.statusbar.policy.ConfigurationController 42 import com.android.systemui.util.foldToSparseArray 43 import javax.inject.Inject 44 45 /** 46 * Manages section headers in the NSSL. 47 * 48 * TODO: Move remaining sections logic from NSSL into this class. 49 */ 50 class NotificationSectionsManager 51 @Inject 52 internal constructor( 53 @ShadeDisplayAware private val configurationController: ConfigurationController, 54 private val keyguardMediaController: KeyguardMediaController, 55 private val mediaContainerController: MediaContainerController, 56 private val notificationRoundnessManager: NotificationRoundnessManager, 57 @IncomingHeader private val incomingHeaderController: SectionHeaderController, 58 @PeopleHeader private val peopleHeaderController: SectionHeaderController, 59 @AlertingHeader private val alertingHeaderController: SectionHeaderController, 60 @SilentHeader private val silentHeaderController: SectionHeaderController, 61 @NewsHeader private val newsHeaderController: SectionHeaderController, 62 @SocialHeader private val socialHeaderController: SectionHeaderController, 63 @RecsHeader private val recsHeaderController: SectionHeaderController, 64 @PromoHeader private val promoHeaderController: SectionHeaderController, 65 ) : SectionProvider { 66 67 private val configurationListener = 68 object : ConfigurationController.ConfigurationListener { 69 override fun onLocaleListChanged() { 70 reinflateViews() 71 } 72 } 73 74 private lateinit var parent: NotificationStackScrollLayout 75 private var initialized = false 76 77 @VisibleForTesting 78 val silentHeaderView: SectionHeaderView? 79 get() = silentHeaderController.headerView 80 81 @VisibleForTesting 82 val alertingHeaderView: SectionHeaderView? 83 get() = alertingHeaderController.headerView 84 85 @VisibleForTesting 86 val incomingHeaderView: SectionHeaderView? 87 get() = incomingHeaderController.headerView 88 89 @VisibleForTesting 90 val peopleHeaderView: SectionHeaderView? 91 get() = peopleHeaderController.headerView 92 93 @VisibleForTesting 94 val mediaControlsView: MediaContainerView? 95 get() = mediaContainerController.mediaContainerView 96 97 @VisibleForTesting 98 val newsHeaderView: SectionHeaderView? 99 get() = newsHeaderController.headerView 100 101 @VisibleForTesting 102 val socialHeaderView: SectionHeaderView? 103 get() = socialHeaderController.headerView 104 105 @VisibleForTesting 106 val recsHeaderView: SectionHeaderView? 107 get() = recsHeaderController.headerView 108 109 @VisibleForTesting 110 val promoHeaderView: SectionHeaderView? 111 get() = promoHeaderController.headerView 112 113 /** Must be called before use. */ 114 fun initialize(parent: NotificationStackScrollLayout) { 115 check(!initialized) { "NotificationSectionsManager already initialized" } 116 initialized = true 117 this.parent = parent 118 reinflateViews() 119 configurationController.addCallback(configurationListener) 120 } 121 122 fun createSectionsForBuckets(): Array<NotificationSection> = 123 PriorityBucket 124 .getAllInOrder() 125 .map { NotificationSection(it) } 126 .toTypedArray() 127 128 /** Reinflates the entire notification header, including all decoration views. */ 129 fun reinflateViews() { 130 silentHeaderController.reinflateView(parent) 131 alertingHeaderController.reinflateView(parent) 132 peopleHeaderController.reinflateView(parent) 133 incomingHeaderController.reinflateView(parent) 134 mediaContainerController.reinflateView(parent) 135 keyguardMediaController.attachSinglePaneContainer(mediaControlsView) 136 if (NotificationClassificationFlag.isEnabled) { 137 newsHeaderController.reinflateView(parent) 138 socialHeaderController.reinflateView(parent) 139 recsHeaderController.reinflateView(parent) 140 promoHeaderController.reinflateView(parent) 141 } 142 } 143 144 override fun beginsSection(view: View, previous: View?): Boolean = 145 view === silentHeaderView || 146 view === mediaControlsView || 147 view === peopleHeaderView || 148 view === alertingHeaderView || 149 view === incomingHeaderView || 150 (NotificationClassificationFlag.isEnabled && 151 (view === newsHeaderView || 152 view === socialHeaderView || 153 view === recsHeaderView || 154 view === promoHeaderView)) || 155 getBucket(view) != getBucket(previous) 156 157 private fun getBucket(view: View?): Int? = 158 when { 159 view === silentHeaderView -> BUCKET_SILENT 160 view === incomingHeaderView -> BUCKET_HEADS_UP 161 view === mediaControlsView -> BUCKET_MEDIA_CONTROLS 162 view === peopleHeaderView -> BUCKET_PEOPLE 163 view === alertingHeaderView -> BUCKET_ALERTING 164 view === newsHeaderView -> BUCKET_NEWS 165 view === socialHeaderView -> BUCKET_SOCIAL 166 view === recsHeaderView -> BUCKET_RECS 167 view === promoHeaderView -> BUCKET_PROMO 168 view is ExpandableNotificationRow -> 169 if (NotificationBundleUi.isEnabled) view.entryAdapter?.sectionBucket 170 else view.entryLegacy.bucket 171 else -> null 172 } 173 174 private sealed class SectionBounds { 175 176 data class Many(val first: ExpandableView, val last: ExpandableView) : SectionBounds() 177 178 data class One(val lone: ExpandableView) : SectionBounds() 179 180 object None : SectionBounds() 181 182 fun addNotif(notif: ExpandableView): SectionBounds = 183 when (this) { 184 is None -> One(notif) 185 is One -> Many(lone, notif) 186 is Many -> copy(last = notif) 187 } 188 189 fun updateSection(section: NotificationSection): Boolean = 190 when (this) { 191 is None -> section.setFirstAndLastVisibleChildren(null, null) 192 is One -> section.setFirstAndLastVisibleChildren(lone, lone) 193 is Many -> section.setFirstAndLastVisibleChildren(first, last) 194 } 195 196 private fun NotificationSection.setFirstAndLastVisibleChildren( 197 first: ExpandableView?, 198 last: ExpandableView?, 199 ): Boolean { 200 val firstChanged = setFirstVisibleChild(first) 201 val lastChanged = setLastVisibleChild(last) 202 return firstChanged || lastChanged 203 } 204 } 205 206 /** 207 * Updates the boundaries (as tracked by their first and last views) of the priority sections. 208 * 209 * @return `true` If the last view in the top section changed (so we need to animate). 210 */ 211 fun updateFirstAndLastViewsForAllSections( 212 sections: Array<NotificationSection>, 213 children: List<ExpandableView>, 214 ): Boolean { 215 // Create mapping of bucket to section 216 val sectionBounds = 217 children 218 .asSequence() 219 // Group children by bucket 220 .groupingBy { 221 getBucket(it) 222 ?: throw IllegalArgumentException("Cannot find section bucket for view") 223 } 224 // Combine each bucket into a SectionBoundary 225 .foldToSparseArray( 226 SectionBounds.None, 227 size = sections.size, 228 operation = SectionBounds::addNotif, 229 ) 230 231 // Build a set of the old first/last Views of the sections 232 val oldFirstChildren = sections.mapNotNull { it.firstVisibleChild }.toSet().toMutableSet() 233 val oldLastChildren = sections.mapNotNull { it.lastVisibleChild }.toSet().toMutableSet() 234 235 // Update each section with the associated boundary, tracking if there was a change 236 val changed = 237 sections.fold(false) { changed, section -> 238 val bounds = sectionBounds[section.bucket] ?: SectionBounds.None 239 val isSectionChanged = bounds.updateSection(section) 240 isSectionChanged || changed 241 } 242 243 val newFirstChildren = sections.mapNotNull { it.firstVisibleChild } 244 val newLastChildren = sections.mapNotNull { it.lastVisibleChild } 245 246 // Update the roundness of Views that weren't already in the first/last position 247 newFirstChildren.forEach { firstChild -> 248 val wasFirstChild = oldFirstChildren.remove(firstChild) 249 if (!wasFirstChild) { 250 val notAnimatedChild = !notificationRoundnessManager.isAnimatedChild(firstChild) 251 val animated = firstChild.isShown && notAnimatedChild 252 firstChild.requestTopRoundness(1f, SECTION, animated) 253 } 254 } 255 newLastChildren.forEach { lastChild -> 256 val wasLastChild = oldLastChildren.remove(lastChild) 257 if (!wasLastChild) { 258 val notAnimatedChild = !notificationRoundnessManager.isAnimatedChild(lastChild) 259 val animated = lastChild.isShown && notAnimatedChild 260 lastChild.requestBottomRoundness(1f, SECTION, animated) 261 } 262 } 263 264 // The Views left in the set are no longer in the first/last position 265 oldFirstChildren.forEach { noMoreFirstChild -> 266 noMoreFirstChild.requestTopRoundness(0f, SECTION) 267 } 268 oldLastChildren.forEach { noMoreLastChild -> 269 noMoreLastChild.requestBottomRoundness(0f, SECTION) 270 } 271 272 if (DEBUG) { 273 logSections(sections) 274 } 275 return changed 276 } 277 278 private fun logSections(sections: Array<NotificationSection>) { 279 for (i in sections.indices) { 280 val s = sections[i] 281 val fs = 282 when (val first = s.firstVisibleChild) { 283 null -> "(null)" 284 is ExpandableNotificationRow -> first.loggingKey 285 else -> Integer.toHexString(System.identityHashCode(first)) 286 } 287 val ls = 288 when (val last = s.lastVisibleChild) { 289 null -> "(null)" 290 is ExpandableNotificationRow -> last.loggingKey 291 else -> Integer.toHexString(System.identityHashCode(last)) 292 } 293 Log.d(TAG, "updateSections: f=$fs s=$i") 294 Log.d(TAG, "updateSections: l=$ls s=$i") 295 } 296 } 297 298 fun setHeaderForegroundColors(@ColorInt onSurface: Int, @ColorInt onSurfaceVariant: Int) { 299 peopleHeaderView?.setForegroundColors(onSurface, onSurfaceVariant) 300 silentHeaderView?.setForegroundColors(onSurface, onSurfaceVariant) 301 alertingHeaderView?.setForegroundColors(onSurface, onSurfaceVariant) 302 if (NotificationClassificationFlag.isEnabled) { 303 newsHeaderView?.setForegroundColors(onSurface, onSurfaceVariant) 304 socialHeaderView?.setForegroundColors(onSurface, onSurfaceVariant) 305 recsHeaderView?.setForegroundColors(onSurface, onSurfaceVariant) 306 promoHeaderView?.setForegroundColors(onSurface, onSurfaceVariant) 307 } 308 } 309 310 companion object { 311 private const val TAG = "NotifSectionsManager" 312 private const val DEBUG = false 313 private val SECTION = SourceType.from("Section") 314 } 315 } 316