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