• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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