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