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