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