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