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.content.Intent
22 import android.provider.Settings
23 import android.util.Log
24 import android.view.LayoutInflater
25 import android.view.View
26 import com.android.internal.annotations.VisibleForTesting
27 import com.android.systemui.R
28 import com.android.systemui.media.KeyguardMediaController
29 import com.android.systemui.plugins.ActivityStarter
30 import com.android.systemui.plugins.statusbar.StatusBarStateController
31 import com.android.systemui.statusbar.StatusBarState
32 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager
33 import com.android.systemui.statusbar.notification.people.DataListener
34 import com.android.systemui.statusbar.notification.people.PeopleHubViewAdapter
35 import com.android.systemui.statusbar.notification.people.PeopleHubViewBoundary
36 import com.android.systemui.statusbar.notification.people.PersonViewModel
37 import com.android.systemui.statusbar.notification.people.Subscription
38 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
39 import com.android.systemui.statusbar.notification.row.ExpandableView
40 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView
41 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.SectionProvider
42 import com.android.systemui.statusbar.policy.ConfigurationController
43 import com.android.systemui.util.children
44 import com.android.systemui.util.takeUntil
45 import com.android.systemui.util.foldToSparseArray
46 import javax.inject.Inject
47 
48 /**
49  * Manages the boundaries of the two notification sections (high priority and low priority). Also
50  * shows/hides the headers for those sections where appropriate.
51  *
52  * TODO: Move remaining sections logic from NSSL into this class.
53  */
54 class NotificationSectionsManager @Inject internal constructor(
55     private val activityStarter: ActivityStarter,
56     private val statusBarStateController: StatusBarStateController,
57     private val configurationController: ConfigurationController,
58     private val peopleHubViewAdapter: PeopleHubViewAdapter,
59     private val keyguardMediaController: KeyguardMediaController,
60     private val sectionsFeatureManager: NotificationSectionsFeatureManager,
61     private val logger: NotificationSectionsLogger
62 ) : SectionProvider {
63 
64     private val configurationListener = object : ConfigurationController.ConfigurationListener {
65         override fun onLocaleListChanged() {
66             reinflateViews(LayoutInflater.from(parent.context))
67         }
68     }
69 
70     private val peopleHubViewBoundary: PeopleHubViewBoundary = object : PeopleHubViewBoundary {
71         override fun setVisible(isVisible: Boolean) {
72             if (peopleHubVisible != isVisible) {
73                 peopleHubVisible = isVisible
74                 if (initialized) {
75                     updateSectionBoundaries("PeopleHub visibility changed")
76                 }
77             }
78         }
79 
80         override val associatedViewForClickAnimation: View
81             get() = peopleHeaderView!!
82 
83         override val personViewAdapters: Sequence<DataListener<PersonViewModel?>>
84             get() = peopleHeaderView!!.personViewAdapters
85     }
86 
87     private lateinit var parent: NotificationStackScrollLayout
88     private var initialized = false
89     private var onClearSilentNotifsClickListener: View.OnClickListener? = null
90 
91     @get:VisibleForTesting
92     var silentHeaderView: SectionHeaderView? = null
93         private set
94 
95     @get:VisibleForTesting
96     var alertingHeaderView: SectionHeaderView? = null
97         private set
98 
99     @get:VisibleForTesting
100     var incomingHeaderView: SectionHeaderView? = null
101         private set
102 
103     @get:VisibleForTesting
104     var peopleHeaderView: PeopleHubView? = null
105         private set
106 
107     @set:VisibleForTesting
108     var peopleHubVisible = false
109     private var peopleHubSubscription: Subscription? = null
110 
111     @get:VisibleForTesting
112     var mediaControlsView: MediaHeaderView? = null
113         private set
114 
115     /** Must be called before use.  */
116     fun initialize(parent: NotificationStackScrollLayout, layoutInflater: LayoutInflater) {
117         check(!initialized) { "NotificationSectionsManager already initialized" }
118         initialized = true
119         this.parent = parent
120         reinflateViews(layoutInflater)
121         configurationController.addCallback(configurationListener)
122     }
123 
124     private fun <T : ExpandableView> reinflateView(
125         view: T?,
126         layoutInflater: LayoutInflater,
127         @LayoutRes layoutResId: Int
128     ): T {
129         var oldPos = -1
130         view?.let {
131             view.transientContainer?.removeView(view)
132             if (view.parent === parent) {
133                 oldPos = parent.indexOfChild(view)
134                 parent.removeView(view)
135             }
136         }
137         val inflated = layoutInflater.inflate(layoutResId, parent, false) as T
138         if (oldPos != -1) {
139             parent.addView(inflated, oldPos)
140         }
141         return inflated
142     }
143 
144     fun createSectionsForBuckets(): Array<NotificationSection> =
145             sectionsFeatureManager.getNotificationBuckets()
146                     .map { NotificationSection(parent, it) }
147                     .toTypedArray()
148 
149     /**
150      * Reinflates the entire notification header, including all decoration views.
151      */
152     fun reinflateViews(layoutInflater: LayoutInflater) {
153         silentHeaderView = reinflateView(
154                 silentHeaderView, layoutInflater, R.layout.status_bar_notification_section_header
155         ).apply {
156             setHeaderText(R.string.notification_section_header_gentle)
157             setOnHeaderClickListener { onGentleHeaderClick() }
158             setOnClearAllClickListener { onClearGentleNotifsClick(it) }
159         }
160         alertingHeaderView = reinflateView(
161                 alertingHeaderView, layoutInflater, R.layout.status_bar_notification_section_header
162         ).apply {
163             setHeaderText(R.string.notification_section_header_alerting)
164             setOnHeaderClickListener { onGentleHeaderClick() }
165         }
166         peopleHubSubscription?.unsubscribe()
167         peopleHubSubscription = null
168         peopleHeaderView = reinflateView(peopleHeaderView, layoutInflater, R.layout.people_strip)
169         if (ENABLE_SNOOZED_CONVERSATION_HUB) {
170             peopleHubSubscription = peopleHubViewAdapter.bindView(peopleHubViewBoundary)
171         }
172         incomingHeaderView = reinflateView(
173                 incomingHeaderView, layoutInflater, R.layout.status_bar_notification_section_header
174         ).apply {
175             setHeaderText(R.string.notification_section_header_incoming)
176             setOnHeaderClickListener { onGentleHeaderClick() }
177         }
178         mediaControlsView =
179                 reinflateView(mediaControlsView, layoutInflater, R.layout.keyguard_media_header)
180                         .also(keyguardMediaController::attach)
181     }
182 
183     override fun beginsSection(view: View, previous: View?): Boolean =
184             view === silentHeaderView ||
185             view === mediaControlsView ||
186             view === peopleHeaderView ||
187             view === alertingHeaderView ||
188             view === incomingHeaderView ||
189             getBucket(view) != getBucket(previous)
190 
191     private fun getBucket(view: View?): Int? = when {
192         view === silentHeaderView -> BUCKET_SILENT
193         view === incomingHeaderView -> BUCKET_HEADS_UP
194         view === mediaControlsView -> BUCKET_MEDIA_CONTROLS
195         view === peopleHeaderView -> BUCKET_PEOPLE
196         view === alertingHeaderView -> BUCKET_ALERTING
197         view is ExpandableNotificationRow -> view.entry.bucket
198         else -> null
199     }
200 
201     private fun logShadeChild(i: Int, child: View) {
202         when {
203             child === incomingHeaderView -> logger.logIncomingHeader(i)
204             child === mediaControlsView -> logger.logMediaControls(i)
205             child === peopleHeaderView -> logger.logConversationsHeader(i)
206             child === alertingHeaderView -> logger.logAlertingHeader(i)
207             child === silentHeaderView -> logger.logSilentHeader(i)
208             child !is ExpandableNotificationRow -> logger.logOther(i, child.javaClass)
209             else -> {
210                 val isHeadsUp = child.isHeadsUp
211                 when (child.entry.bucket) {
212                     BUCKET_HEADS_UP -> logger.logHeadsUp(i, isHeadsUp)
213                     BUCKET_PEOPLE -> logger.logConversation(i, isHeadsUp)
214                     BUCKET_ALERTING -> logger.logAlerting(i, isHeadsUp)
215                     BUCKET_SILENT -> logger.logSilent(i, isHeadsUp)
216                 }
217             }
218         }
219     }
220     private fun logShadeContents() = parent.children.forEachIndexed(::logShadeChild)
221 
222     private val isUsingMultipleSections: Boolean
223         get() = sectionsFeatureManager.getNumberOfBuckets() > 1
224 
225     @VisibleForTesting
226     fun updateSectionBoundaries() = updateSectionBoundaries("test")
227 
228     private interface SectionUpdateState<out T : ExpandableView> {
229         val header: T
230         var currentPosition: Int?
231         var targetPosition: Int?
232         fun adjustViewPosition()
233     }
234 
235     private fun <T : ExpandableView> expandableViewHeaderState(header: T): SectionUpdateState<T> =
236             object : SectionUpdateState<T> {
237                 override val header = header
238                 override var currentPosition: Int? = null
239                 override var targetPosition: Int? = null
240 
241                 override fun adjustViewPosition() {
242                     val target = targetPosition
243                     val current = currentPosition
244                     if (target == null) {
245                         if (current != null) {
246                             parent.removeView(header)
247                         }
248                     } else {
249                         if (current == null) {
250                             // If the header is animating away, it will still have a parent, so
251                             // detach it first
252                             // TODO: We should really cancel the active animations here. This will
253                             //  happen automatically when the view's intro animation starts, but
254                             //  it's a fragile link.
255                             header.transientContainer?.removeTransientView(header)
256                             header.transientContainer = null
257                             parent.addView(header, target)
258                         } else {
259                             parent.changeViewPosition(header, target)
260                         }
261                     }
262                 }
263     }
264 
265     private fun <T : StackScrollerDecorView> decorViewHeaderState(
266         header: T
267     ): SectionUpdateState<T> {
268         val inner = expandableViewHeaderState(header)
269         return object : SectionUpdateState<T> by inner {
270             override fun adjustViewPosition() {
271                 inner.adjustViewPosition()
272                 if (targetPosition != null && currentPosition == null) {
273                     header.isContentVisible = true
274                 }
275             }
276         }
277     }
278 
279     /**
280      * Should be called whenever notifs are added, removed, or updated. Updates section boundary
281      * bookkeeping and adds/moves/removes section headers if appropriate.
282      */
283     fun updateSectionBoundaries(reason: String) {
284         if (!isUsingMultipleSections) {
285             return
286         }
287         logger.logStartSectionUpdate(reason)
288 
289         // The overall strategy here is to iterate over the current children of mParent, looking
290         // for where the sections headers are currently positioned, and where each section begins.
291         // Then, once we find the start of a new section, we track that position as the "target" for
292         // the section header, adjusted for the case where existing headers are in front of that
293         // target, but won't be once they are moved / removed after the pass has completed.
294 
295         val showHeaders = statusBarStateController.state != StatusBarState.KEYGUARD
296         val usingPeopleFiltering = sectionsFeatureManager.isFilteringEnabled()
297         val usingMediaControls = sectionsFeatureManager.isMediaControlsEnabled()
298 
299         val mediaState = mediaControlsView?.let(::expandableViewHeaderState)
300         val incomingState = incomingHeaderView?.let(::decorViewHeaderState)
301         val peopleState = peopleHeaderView?.let(::decorViewHeaderState)
302         val alertingState = alertingHeaderView?.let(::decorViewHeaderState)
303         val gentleState = silentHeaderView?.let(::decorViewHeaderState)
304 
305         fun getSectionState(view: View): SectionUpdateState<ExpandableView>? = when {
306             view === mediaControlsView -> mediaState
307             view === incomingHeaderView -> incomingState
308             view === peopleHeaderView -> peopleState
309             view === alertingHeaderView -> alertingState
310             view === silentHeaderView -> gentleState
311             else -> null
312         }
313 
314         val headersOrdered = sequenceOf(
315                 mediaState, incomingState, peopleState, alertingState, gentleState
316         ).filterNotNull()
317 
318         var peopleNotifsPresent = false
319         var lastNotifIndex = 0
320         var nextBucket: Int? = null
321         var inIncomingSection = false
322 
323         // Iterating backwards allows for easier construction of the Incoming section, as opposed
324         // to backtracking when a discontinuity in the sections is discovered.
325         // Iterating to -1 in order to support the case where a header is at the very top of the
326         // shade.
327         for (i in parent.childCount - 1 downTo -1) {
328             val child: View? = parent.getChildAt(i)
329 
330             child?.let {
331                 logShadeChild(i, child)
332                 // If this child is a header, update the tracked positions
333                 getSectionState(child)?.let { state ->
334                     state.currentPosition = i
335                     // If headers that should appear above this one in the shade already have a
336                     // target index, then we need to decrement them in order to account for this one
337                     // being either removed, or moved below them.
338                     headersOrdered.takeUntil { it === state }
339                             .forEach { it.targetPosition = it.targetPosition?.minus(1) }
340                 }
341             }
342 
343             val row = (child as? ExpandableNotificationRow)
344                     ?.takeUnless { it.visibility == View.GONE }
345 
346             // Is there a section discontinuity? This usually occurs due to HUNs
347             inIncomingSection = inIncomingSection || nextBucket?.let { next ->
348                 row?.entry?.bucket?.let { curr -> next < curr }
349             } == true
350 
351             if (inIncomingSection) {
352                 // Update the bucket to reflect that it's being placed in the Incoming section
353                 row?.entry?.bucket = BUCKET_HEADS_UP
354             }
355 
356             // Insert a header in front of the next row, if there's a boundary between it and this
357             // row, or if it is the topmost row.
358             val isSectionBoundary = nextBucket != null &&
359                     (child == null || row != null && nextBucket != row.entry.bucket)
360             if (isSectionBoundary && showHeaders) {
361                 when (nextBucket) {
362                     BUCKET_HEADS_UP -> incomingState?.targetPosition = i + 1
363                     BUCKET_PEOPLE -> peopleState?.targetPosition = i + 1
364                     BUCKET_ALERTING -> alertingState?.targetPosition = i + 1
365                     BUCKET_SILENT -> gentleState?.targetPosition = i + 1
366                 }
367             }
368 
369             row ?: continue
370 
371             // Check if there are any people notifications
372             peopleNotifsPresent = peopleNotifsPresent || row.entry.bucket == BUCKET_PEOPLE
373 
374             if (nextBucket == null) {
375                 lastNotifIndex = i
376             }
377             nextBucket = row.entry.bucket
378         }
379 
380         if (showHeaders && usingPeopleFiltering && peopleHubVisible) {
381             peopleState?.targetPosition = peopleState?.targetPosition
382                     // Insert the people header even if there are no people visible, in order to
383                     // show the hub. Put it directly above the next header.
384                     ?: alertingState?.targetPosition
385                     ?: gentleState?.targetPosition
386                     // Put it at the end of the list.
387                     ?: lastNotifIndex
388 
389             // Offset the target to account for the current position of the people header.
390             peopleState?.targetPosition = peopleState?.currentPosition?.let { current ->
391                 peopleState.targetPosition?.let { target ->
392                     if (current < target) target - 1 else target
393                 }
394             }
395         }
396 
397         mediaState?.targetPosition = if (usingMediaControls) 0 else null
398 
399         logger.logStr("New header target positions:")
400         logger.logMediaControls(mediaState?.targetPosition ?: -1)
401         logger.logIncomingHeader(incomingState?.targetPosition ?: -1)
402         logger.logConversationsHeader(peopleState?.targetPosition ?: -1)
403         logger.logAlertingHeader(alertingState?.targetPosition ?: -1)
404         logger.logSilentHeader(gentleState?.targetPosition ?: -1)
405 
406         // Update headers in reverse order to preserve indices, otherwise movements earlier in the
407         // list will affect the target indices of the headers later in the list.
408         headersOrdered.asIterable().reversed().forEach { it.adjustViewPosition() }
409 
410         logger.logStr("Final order:")
411         logShadeContents()
412         logger.logStr("Section boundary update complete")
413 
414         // Update headers to reflect state of section contents
415         silentHeaderView?.run {
416             val hasActiveClearableNotifications = this@NotificationSectionsManager.parent
417                     .hasActiveClearableNotifications(NotificationStackScrollLayout.ROWS_GENTLE)
418             setAreThereDismissableGentleNotifs(hasActiveClearableNotifications)
419         }
420         peopleHeaderView?.run {
421             canSwipe = showHeaders && peopleHubVisible && !peopleNotifsPresent
422             peopleState?.targetPosition?.let { targetPosition ->
423                 if (targetPosition != peopleState.currentPosition) {
424                     resetTranslation()
425                 }
426             }
427         }
428     }
429 
430     private sealed class SectionBounds {
431 
432         data class Many(
433             val first: ExpandableView,
434             val last: ExpandableView
435         ) : SectionBounds()
436 
437         data class One(val lone: ExpandableView) : SectionBounds()
438         object None : SectionBounds()
439 
440         fun addNotif(notif: ExpandableView): SectionBounds = when (this) {
441             is None -> One(notif)
442             is One -> Many(lone, notif)
443             is Many -> copy(last = notif)
444         }
445 
446         fun updateSection(section: NotificationSection): Boolean = when (this) {
447             is None -> section.setFirstAndLastVisibleChildren(null, null)
448             is One -> section.setFirstAndLastVisibleChildren(lone, lone)
449             is Many -> section.setFirstAndLastVisibleChildren(first, last)
450         }
451 
452         private fun NotificationSection.setFirstAndLastVisibleChildren(
453             first: ExpandableView?,
454             last: ExpandableView?
455         ): Boolean {
456             val firstChanged = setFirstVisibleChild(first)
457             val lastChanged = setLastVisibleChild(last)
458             return firstChanged || lastChanged
459         }
460     }
461 
462     /**
463      * Updates the boundaries (as tracked by their first and last views) of the priority sections.
464      *
465      * @return `true` If the last view in the top section changed (so we need to animate).
466      */
467     fun updateFirstAndLastViewsForAllSections(
468         sections: Array<NotificationSection>,
469         children: List<ExpandableView>
470     ): Boolean {
471         // Create mapping of bucket to section
472         val sectionBounds = children.asSequence()
473                 // Group children by bucket
474                 .groupingBy {
475                     getBucket(it)
476                             ?: throw IllegalArgumentException("Cannot find section bucket for view")
477                 }
478                 // Combine each bucket into a SectionBoundary
479                 .foldToSparseArray(
480                         SectionBounds.None,
481                         size = sections.size,
482                         operation = SectionBounds::addNotif
483                 )
484         // Update each section with the associated boundary, tracking if there was a change
485         val changed = sections.fold(false) { changed, section ->
486             val bounds = sectionBounds[section.bucket] ?: SectionBounds.None
487             bounds.updateSection(section) || changed
488         }
489         if (DEBUG) {
490             logSections(sections)
491         }
492         return changed
493     }
494 
495     private fun logSections(sections: Array<NotificationSection>) {
496         for (i in sections.indices) {
497             val s = sections[i]
498             val fs = when (val first = s.firstVisibleChild) {
499                 null -> "(null)"
500                 is ExpandableNotificationRow -> first.entry.key
501                 else -> Integer.toHexString(System.identityHashCode(first))
502             }
503             val ls = when (val last = s.lastVisibleChild) {
504                 null -> "(null)"
505                 is ExpandableNotificationRow -> last.entry.key
506                 else -> Integer.toHexString(System.identityHashCode(last))
507             }
508             Log.d(TAG, "updateSections: f=$fs s=$i")
509             Log.d(TAG, "updateSections: l=$ls s=$i")
510         }
511     }
512 
513     private fun onGentleHeaderClick() {
514         val intent = Intent(Settings.ACTION_NOTIFICATION_SETTINGS)
515         activityStarter.startActivity(
516                 intent,
517                 true,
518                 true,
519                 Intent.FLAG_ACTIVITY_SINGLE_TOP)
520     }
521 
522     private fun onClearGentleNotifsClick(v: View) {
523         onClearSilentNotifsClickListener?.onClick(v)
524     }
525 
526     /** Listener for when the "clear all" button is clicked on the gentle notification header. */
527     fun setOnClearSilentNotifsClickListener(listener: View.OnClickListener) {
528         onClearSilentNotifsClickListener = listener
529     }
530 
531     fun hidePeopleRow() {
532         peopleHubVisible = false
533         updateSectionBoundaries("PeopleHub dismissed")
534     }
535 
536     fun setHeaderForegroundColor(@ColorInt color: Int) {
537         peopleHeaderView?.setTextColor(color)
538         silentHeaderView?.setForegroundColor(color)
539         alertingHeaderView?.setForegroundColor(color)
540     }
541 
542     companion object {
543         private const val TAG = "NotifSectionsManager"
544         private const val DEBUG = false
545         private const val ENABLE_SNOOZED_CONVERSATION_HUB = false
546     }
547 }
548 
549 /**
550  * For now, declare the available notification buckets (sections) here so that other
551  * presentation code can decide what to do based on an entry's buckets
552  */
553 @Retention(AnnotationRetention.SOURCE)
554 @IntDef(
555         prefix = ["BUCKET_"],
556         value = [
557             BUCKET_UNKNOWN, BUCKET_MEDIA_CONTROLS, BUCKET_HEADS_UP, BUCKET_FOREGROUND_SERVICE,
558             BUCKET_PEOPLE, BUCKET_ALERTING, BUCKET_SILENT
559         ]
560 )
561 annotation class PriorityBucket
562 
563 const val BUCKET_UNKNOWN = 0
564 const val BUCKET_MEDIA_CONTROLS = 1
565 const val BUCKET_HEADS_UP = 2
566 const val BUCKET_FOREGROUND_SERVICE = 3
567 const val BUCKET_PEOPLE = 4
568 const val BUCKET_ALERTING = 5
569 const val BUCKET_SILENT = 6
570