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.annotation.LayoutRes 20 import android.util.Log 21 import android.view.LayoutInflater 22 import android.view.View 23 import com.android.internal.annotations.VisibleForTesting 24 import com.android.systemui.R 25 import com.android.systemui.media.KeyguardMediaController 26 import com.android.systemui.plugins.statusbar.StatusBarStateController 27 import com.android.systemui.statusbar.StatusBarState 28 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager 29 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController 30 import com.android.systemui.statusbar.notification.collection.render.ShadeViewManager 31 import com.android.systemui.statusbar.notification.dagger.AlertingHeader 32 import com.android.systemui.statusbar.notification.dagger.IncomingHeader 33 import com.android.systemui.statusbar.notification.dagger.PeopleHeader 34 import com.android.systemui.statusbar.notification.dagger.SilentHeader 35 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow 36 import com.android.systemui.statusbar.notification.row.ExpandableView 37 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView 38 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.SectionProvider 39 import com.android.systemui.statusbar.policy.ConfigurationController 40 import com.android.systemui.util.children 41 import com.android.systemui.util.foldToSparseArray 42 import com.android.systemui.util.takeUntil 43 import javax.inject.Inject 44 45 /** 46 * Manages the boundaries of the notification sections (incoming, conversations, high priority, and 47 * low priority). 48 * 49 * In the legacy notification pipeline, this is responsible for correctly positioning all section 50 * headers after the [NotificationStackScrollLayout] has had notifications added/removed/changed. In 51 * the new pipeline, this is handled as part of the [ShadeViewManager]. 52 * 53 * TODO: Move remaining sections logic from NSSL into this class. 54 */ 55 class NotificationSectionsManager @Inject internal constructor( 56 private val statusBarStateController: StatusBarStateController, 57 private val configurationController: ConfigurationController, 58 private val keyguardMediaController: KeyguardMediaController, 59 private val sectionsFeatureManager: NotificationSectionsFeatureManager, 60 private val logger: NotificationSectionsLogger, 61 @IncomingHeader private val incomingHeaderController: SectionHeaderController, 62 @PeopleHeader private val peopleHeaderController: SectionHeaderController, 63 @AlertingHeader private val alertingHeaderController: SectionHeaderController, 64 @SilentHeader private val silentHeaderController: SectionHeaderController 65 ) : SectionProvider { 66 67 private val configurationListener = object : ConfigurationController.ConfigurationListener { 68 override fun onLocaleListChanged() { 69 reinflateViews(LayoutInflater.from(parent.context)) 70 } 71 } 72 73 private lateinit var parent: NotificationStackScrollLayout 74 private var initialized = false 75 76 @VisibleForTesting 77 val silentHeaderView: SectionHeaderView? 78 get() = silentHeaderController.headerView 79 80 @VisibleForTesting 81 val alertingHeaderView: SectionHeaderView? 82 get() = alertingHeaderController.headerView 83 84 @VisibleForTesting 85 val incomingHeaderView: SectionHeaderView? 86 get() = incomingHeaderController.headerView 87 88 @VisibleForTesting 89 val peopleHeaderView: SectionHeaderView? 90 get() = peopleHeaderController.headerView 91 92 @get:VisibleForTesting 93 var mediaControlsView: MediaHeaderView? = null 94 private set 95 96 /** Must be called before use. */ 97 fun initialize(parent: NotificationStackScrollLayout, layoutInflater: LayoutInflater) { 98 check(!initialized) { "NotificationSectionsManager already initialized" } 99 initialized = true 100 this.parent = parent 101 reinflateViews(layoutInflater) 102 configurationController.addCallback(configurationListener) 103 } 104 105 private fun <T : ExpandableView> reinflateView( 106 view: T?, 107 layoutInflater: LayoutInflater, 108 @LayoutRes layoutResId: Int 109 ): T { 110 var oldPos = -1 111 view?.let { 112 view.transientContainer?.removeView(view) 113 if (view.parent === parent) { 114 oldPos = parent.indexOfChild(view) 115 parent.removeView(view) 116 } 117 } 118 val inflated = layoutInflater.inflate(layoutResId, parent, false) as T 119 if (oldPos != -1) { 120 parent.addView(inflated, oldPos) 121 } 122 return inflated 123 } 124 125 fun createSectionsForBuckets(): Array<NotificationSection> = 126 sectionsFeatureManager.getNotificationBuckets() 127 .map { NotificationSection(parent, it) } 128 .toTypedArray() 129 130 /** 131 * Reinflates the entire notification header, including all decoration views. 132 */ 133 fun reinflateViews(layoutInflater: LayoutInflater) { 134 silentHeaderController.reinflateView(parent) 135 alertingHeaderController.reinflateView(parent) 136 peopleHeaderController.reinflateView(parent) 137 incomingHeaderController.reinflateView(parent) 138 mediaControlsView = 139 reinflateView(mediaControlsView, layoutInflater, R.layout.keyguard_media_header) 140 keyguardMediaController.attachSinglePaneContainer(mediaControlsView) 141 } 142 143 override fun beginsSection(view: View, previous: View?): Boolean = 144 view === silentHeaderView || 145 view === mediaControlsView || 146 view === peopleHeaderView || 147 view === alertingHeaderView || 148 view === incomingHeaderView || 149 getBucket(view) != getBucket(previous) 150 151 private fun getBucket(view: View?): Int? = when { 152 view === silentHeaderView -> BUCKET_SILENT 153 view === incomingHeaderView -> BUCKET_HEADS_UP 154 view === mediaControlsView -> BUCKET_MEDIA_CONTROLS 155 view === peopleHeaderView -> BUCKET_PEOPLE 156 view === alertingHeaderView -> BUCKET_ALERTING 157 view is ExpandableNotificationRow -> view.entry.bucket 158 else -> null 159 } 160 161 private fun logShadeChild(i: Int, child: View) { 162 when { 163 child === incomingHeaderView -> logger.logIncomingHeader(i) 164 child === mediaControlsView -> logger.logMediaControls(i) 165 child === peopleHeaderView -> logger.logConversationsHeader(i) 166 child === alertingHeaderView -> logger.logAlertingHeader(i) 167 child === silentHeaderView -> logger.logSilentHeader(i) 168 child !is ExpandableNotificationRow -> logger.logOther(i, child.javaClass) 169 else -> { 170 val isHeadsUp = child.isHeadsUp 171 when (child.entry.bucket) { 172 BUCKET_HEADS_UP -> logger.logHeadsUp(i, isHeadsUp) 173 BUCKET_PEOPLE -> logger.logConversation(i, isHeadsUp) 174 BUCKET_ALERTING -> logger.logAlerting(i, isHeadsUp) 175 BUCKET_SILENT -> logger.logSilent(i, isHeadsUp) 176 } 177 } 178 } 179 } 180 private fun logShadeContents() = parent.children.forEachIndexed(::logShadeChild) 181 182 private val isUsingMultipleSections: Boolean 183 get() = sectionsFeatureManager.getNumberOfBuckets() > 1 184 185 @VisibleForTesting 186 fun updateSectionBoundaries() = updateSectionBoundaries("test") 187 188 private interface SectionUpdateState<out T : ExpandableView> { 189 val header: T 190 var currentPosition: Int? 191 var targetPosition: Int? 192 fun adjustViewPosition() 193 } 194 195 private fun <T : ExpandableView> expandableViewHeaderState(header: T): SectionUpdateState<T> = 196 object : SectionUpdateState<T> { 197 override val header = header 198 override var currentPosition: Int? = null 199 override var targetPosition: Int? = null 200 201 override fun adjustViewPosition() { 202 val target = targetPosition 203 val current = currentPosition 204 if (target == null) { 205 if (current != null) { 206 parent.removeView(header) 207 } 208 } else { 209 if (current == null) { 210 // If the header is animating away, it will still have a parent, so 211 // detach it first 212 // TODO: We should really cancel the active animations here. This will 213 // happen automatically when the view's intro animation starts, but 214 // it's a fragile link. 215 header.transientContainer?.removeTransientView(header) 216 header.transientContainer = null 217 parent.addView(header, target) 218 } else { 219 parent.changeViewPosition(header, target) 220 } 221 } 222 } 223 } 224 225 private fun <T : StackScrollerDecorView> decorViewHeaderState( 226 header: T 227 ): SectionUpdateState<T> { 228 val inner = expandableViewHeaderState(header) 229 return object : SectionUpdateState<T> by inner { 230 override fun adjustViewPosition() { 231 inner.adjustViewPosition() 232 if (targetPosition != null && currentPosition == null) { 233 header.isContentVisible = true 234 } 235 } 236 } 237 } 238 239 /** 240 * Should be called whenever notifs are added, removed, or updated. Updates section boundary 241 * bookkeeping and adds/moves/removes section headers if appropriate. 242 */ 243 fun updateSectionBoundaries(reason: String) { 244 if (!isUsingMultipleSections) { 245 return 246 } 247 logger.logStartSectionUpdate(reason) 248 249 // The overall strategy here is to iterate over the current children of mParent, looking 250 // for where the sections headers are currently positioned, and where each section begins. 251 // Then, once we find the start of a new section, we track that position as the "target" for 252 // the section header, adjusted for the case where existing headers are in front of that 253 // target, but won't be once they are moved / removed after the pass has completed. 254 255 val showHeaders = statusBarStateController.state != StatusBarState.KEYGUARD 256 val usingMediaControls = sectionsFeatureManager.isMediaControlsEnabled() 257 258 val mediaState = mediaControlsView?.let(::expandableViewHeaderState) 259 val incomingState = incomingHeaderView?.let(::decorViewHeaderState) 260 val peopleState = peopleHeaderView?.let(::decorViewHeaderState) 261 val alertingState = alertingHeaderView?.let(::decorViewHeaderState) 262 val gentleState = silentHeaderView?.let(::decorViewHeaderState) 263 264 fun getSectionState(view: View): SectionUpdateState<ExpandableView>? = when { 265 view === mediaControlsView -> mediaState 266 view === incomingHeaderView -> incomingState 267 view === peopleHeaderView -> peopleState 268 view === alertingHeaderView -> alertingState 269 view === silentHeaderView -> gentleState 270 else -> null 271 } 272 273 val headersOrdered = sequenceOf( 274 mediaState, incomingState, peopleState, alertingState, gentleState 275 ).filterNotNull() 276 277 var peopleNotifsPresent = false 278 var nextBucket: Int? = null 279 var inIncomingSection = false 280 281 // Iterating backwards allows for easier construction of the Incoming section, as opposed 282 // to backtracking when a discontinuity in the sections is discovered. 283 // Iterating to -1 in order to support the case where a header is at the very top of the 284 // shade. 285 for (i in parent.childCount - 1 downTo -1) { 286 val child: View? = parent.getChildAt(i) 287 288 child?.let { 289 logShadeChild(i, child) 290 // If this child is a header, update the tracked positions 291 getSectionState(child)?.let { state -> 292 state.currentPosition = i 293 // If headers that should appear above this one in the shade already have a 294 // target index, then we need to decrement them in order to account for this one 295 // being either removed, or moved below them. 296 headersOrdered.takeUntil { it === state } 297 .forEach { it.targetPosition = it.targetPosition?.minus(1) } 298 } 299 } 300 301 val row = (child as? ExpandableNotificationRow) 302 ?.takeUnless { it.visibility == View.GONE } 303 304 // Is there a section discontinuity? This usually occurs due to HUNs 305 inIncomingSection = inIncomingSection || nextBucket?.let { next -> 306 row?.entry?.bucket?.let { curr -> next < curr } 307 } == true 308 309 if (inIncomingSection) { 310 // Update the bucket to reflect that it's being placed in the Incoming section 311 row?.entry?.bucket = BUCKET_HEADS_UP 312 } 313 314 // Insert a header in front of the next row, if there's a boundary between it and this 315 // row, or if it is the topmost row. 316 val isSectionBoundary = nextBucket != null && 317 (child == null || row != null && nextBucket != row.entry.bucket) 318 if (isSectionBoundary && showHeaders) { 319 when (nextBucket) { 320 BUCKET_SILENT -> gentleState?.targetPosition = i + 1 321 } 322 } 323 324 row ?: continue 325 326 // Check if there are any people notifications 327 peopleNotifsPresent = peopleNotifsPresent || row.entry.bucket == BUCKET_PEOPLE 328 nextBucket = row.entry.bucket 329 } 330 331 mediaState?.targetPosition = if (usingMediaControls) 0 else null 332 333 logger.logStr("New header target positions:") 334 logger.logMediaControls(mediaState?.targetPosition ?: -1) 335 logger.logIncomingHeader(incomingState?.targetPosition ?: -1) 336 logger.logConversationsHeader(peopleState?.targetPosition ?: -1) 337 logger.logAlertingHeader(alertingState?.targetPosition ?: -1) 338 logger.logSilentHeader(gentleState?.targetPosition ?: -1) 339 340 // Update headers in reverse order to preserve indices, otherwise movements earlier in the 341 // list will affect the target indices of the headers later in the list. 342 headersOrdered.asIterable().reversed().forEach { it.adjustViewPosition() } 343 344 logger.logStr("Final order:") 345 logShadeContents() 346 logger.logStr("Section boundary update complete") 347 348 // Update headers to reflect state of section contents 349 silentHeaderView?.run { 350 val hasActiveClearableNotifications = this@NotificationSectionsManager.parent 351 .hasActiveClearableNotifications(NotificationStackScrollLayout.ROWS_GENTLE) 352 setClearSectionButtonEnabled(hasActiveClearableNotifications) 353 } 354 } 355 356 private sealed class SectionBounds { 357 358 data class Many( 359 val first: ExpandableView, 360 val last: ExpandableView 361 ) : SectionBounds() 362 363 data class One(val lone: ExpandableView) : SectionBounds() 364 object None : SectionBounds() 365 366 fun addNotif(notif: ExpandableView): SectionBounds = when (this) { 367 is None -> One(notif) 368 is One -> Many(lone, notif) 369 is Many -> copy(last = notif) 370 } 371 372 fun updateSection(section: NotificationSection): Boolean = when (this) { 373 is None -> section.setFirstAndLastVisibleChildren(null, null) 374 is One -> section.setFirstAndLastVisibleChildren(lone, lone) 375 is Many -> section.setFirstAndLastVisibleChildren(first, last) 376 } 377 378 private fun NotificationSection.setFirstAndLastVisibleChildren( 379 first: ExpandableView?, 380 last: ExpandableView? 381 ): Boolean { 382 val firstChanged = setFirstVisibleChild(first) 383 val lastChanged = setLastVisibleChild(last) 384 return firstChanged || lastChanged 385 } 386 } 387 388 /** 389 * Updates the boundaries (as tracked by their first and last views) of the priority sections. 390 * 391 * @return `true` If the last view in the top section changed (so we need to animate). 392 */ 393 fun updateFirstAndLastViewsForAllSections( 394 sections: Array<NotificationSection>, 395 children: List<ExpandableView> 396 ): Boolean { 397 // Create mapping of bucket to section 398 val sectionBounds = children.asSequence() 399 // Group children by bucket 400 .groupingBy { 401 getBucket(it) 402 ?: throw IllegalArgumentException("Cannot find section bucket for view") 403 } 404 // Combine each bucket into a SectionBoundary 405 .foldToSparseArray( 406 SectionBounds.None, 407 size = sections.size, 408 operation = SectionBounds::addNotif 409 ) 410 // Update each section with the associated boundary, tracking if there was a change 411 val changed = sections.fold(false) { changed, section -> 412 val bounds = sectionBounds[section.bucket] ?: SectionBounds.None 413 bounds.updateSection(section) || changed 414 } 415 if (DEBUG) { 416 logSections(sections) 417 } 418 return changed 419 } 420 421 private fun logSections(sections: Array<NotificationSection>) { 422 for (i in sections.indices) { 423 val s = sections[i] 424 val fs = when (val first = s.firstVisibleChild) { 425 null -> "(null)" 426 is ExpandableNotificationRow -> first.entry.key 427 else -> Integer.toHexString(System.identityHashCode(first)) 428 } 429 val ls = when (val last = s.lastVisibleChild) { 430 null -> "(null)" 431 is ExpandableNotificationRow -> last.entry.key 432 else -> Integer.toHexString(System.identityHashCode(last)) 433 } 434 Log.d(TAG, "updateSections: f=$fs s=$i") 435 Log.d(TAG, "updateSections: l=$ls s=$i") 436 } 437 } 438 439 fun setHeaderForegroundColor(@ColorInt color: Int) { 440 peopleHeaderView?.setForegroundColor(color) 441 silentHeaderView?.setForegroundColor(color) 442 alertingHeaderView?.setForegroundColor(color) 443 } 444 445 companion object { 446 private const val TAG = "NotifSectionsManager" 447 private const val DEBUG = false 448 } 449 } 450