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