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