• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2022 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 
17 package com.android.systemui.statusbar.notification.stack
18 
19 import android.content.res.Resources
20 import android.util.Log
21 import android.view.View.GONE
22 import androidx.annotation.VisibleForTesting
23 import com.android.systemui.dagger.SysUISingleton
24 import com.android.systemui.dagger.qualifiers.Main
25 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
26 import com.android.systemui.res.R
27 import com.android.systemui.statusbar.LockscreenShadeTransitionController
28 import com.android.systemui.statusbar.StatusBarState.KEYGUARD
29 import com.android.systemui.statusbar.SysuiStatusBarStateController
30 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
31 import com.android.systemui.statusbar.notification.row.ExpandableView
32 import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
33 import com.android.systemui.statusbar.policy.SplitShadeStateController
34 import com.android.systemui.util.Compile
35 import com.android.systemui.util.children
36 import java.io.PrintWriter
37 import javax.inject.Inject
38 import kotlin.math.max
39 import kotlin.math.min
40 import kotlin.properties.Delegates.notNull
41 
42 private const val TAG = "NotifStackSizeCalc"
43 private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG)
44 private val SPEW = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE)
45 
46 /**
47  * Calculates number of notifications to display and the height of the notification stack.
48  * "Notifications" refers to any ExpandableView that we show on lockscreen, which can include the
49  * media player.
50  */
51 @SysUISingleton
52 class NotificationStackSizeCalculator
53 @Inject
54 constructor(
55     private val statusBarStateController: SysuiStatusBarStateController,
56     private val lockscreenShadeTransitionController: LockscreenShadeTransitionController,
57     private val mediaDataManager: MediaDataManager,
58     @Main private val resources: Resources,
59     private val splitShadeStateController: SplitShadeStateController
60 ) {
61 
62     /**
63      * Maximum # notifications to show on Keyguard; extras will be collapsed in an overflow shelf.
64      * If there are exactly 1 + mMaxKeyguardNotifications, and they fit in the available space
65      * (considering the overflow shelf is not displayed in this case), then all notifications are
66      * shown.
67      */
68     private var maxKeyguardNotifications by notNull<Int>()
69 
70     /**
71      * Whether [maxKeyguardNotifications] will have 1 added to it when media is shown in the stack.
72      */
73     private var maxNotificationsExcludesMedia = false
74 
75     /** Whether we allow keyguard to show less important notifications above the shelf. */
76     private val limitLockScreenToOneImportant
77         get() = NotificationMinimalismPrototype.V2.isEnabled
78 
79     /** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */
80     private var dividerHeight by notNull<Float>()
81 
82     /**
83      * True when there is not enough vertical space to show at least one notification with heads up
84      * layout. When true, notifications always show collapsed layout.
85      */
86     private var saveSpaceOnLockscreen = false
87 
88     init {
89         updateResources()
90     }
91 
92     private fun allowedByPolicy(stackHeight: StackHeight): Boolean =
93         if (stackHeight.shouldForceIntoShelf) {
94             log { "\tallowedByPolicy = false" }
95             false
96         } else {
97             true
98         }
99 
100     /**
101      * Returns whether notifications and (shelf if visible) can fit in total space available.
102      * [shelfSpace] is extra vertical space allowed for the shelf to overlap the lock icon.
103      */
104     private fun canStackFitInSpace(
105         stackHeight: StackHeight,
106         notifSpace: Float,
107         shelfSpace: Float,
108     ): FitResult {
109         val (notifHeight, notifHeightSaveSpace, shelfHeightWithSpaceBefore) = stackHeight
110 
111         if (shelfHeightWithSpaceBefore == 0f) {
112             if (notifHeight <= notifSpace) {
113                 log {
114                     "\tcanStackFitInSpace[FIT] = notifHeight[$notifHeight]" +
115                         " <= notifSpace[$notifSpace]"
116                 }
117                 return FitResult.FIT
118             }
119             if (notifHeightSaveSpace <= notifSpace) {
120                 log {
121                     "\tcanStackFitInSpace[FIT_IF_SAVE_SPACE]" +
122                         " = notifHeightSaveSpace[$notifHeightSaveSpace]" +
123                         " <= notifSpace[$notifSpace]"
124                 }
125                 return FitResult.FIT_IF_SAVE_SPACE
126             }
127             log {
128                 "\tcanStackFitInSpace[NO_FIT]" +
129                     " = notifHeightSaveSpace[$notifHeightSaveSpace] > notifSpace[$notifSpace]"
130             }
131             return FitResult.NO_FIT
132         } else {
133             if ((notifHeight + shelfHeightWithSpaceBefore) <= (notifSpace + shelfSpace)) {
134                 log {
135                     "\tcanStackFitInSpace[FIT] = (notifHeight[$notifHeight]" +
136                         " + shelfHeightWithSpaceBefore[$shelfHeightWithSpaceBefore])" +
137                         " <= (notifSpace[$notifSpace] " +
138                         " + spaceForShelf[$shelfSpace])"
139                 }
140                 return FitResult.FIT
141             } else if (
142                 (notifHeightSaveSpace + shelfHeightWithSpaceBefore) <= (notifSpace + shelfSpace)
143             ) {
144                 log {
145                     "\tcanStackFitInSpace[FIT_IF_SAVE_SPACE]" +
146                         " = (notifHeightSaveSpace[$notifHeightSaveSpace]" +
147                         " + shelfHeightWithSpaceBefore[$shelfHeightWithSpaceBefore])" +
148                         " <= (notifSpace[$notifSpace] + shelfSpace[$shelfSpace])"
149                 }
150                 return FitResult.FIT_IF_SAVE_SPACE
151             } else {
152                 log {
153                     "\tcanStackFitInSpace[NO_FIT]" +
154                         " = (notifHeightSaveSpace[$notifHeightSaveSpace]" +
155                         " + shelfHeightWithSpaceBefore[$shelfHeightWithSpaceBefore])" +
156                         " > (notifSpace[$notifSpace] + shelfSpace[$shelfSpace])"
157                 }
158                 return FitResult.NO_FIT
159             }
160         }
161     }
162 
163     /**
164      * Given the [notifSpace] and [shelfSpace] constraints, calculate how many notifications to
165      * show. This number is only valid in keyguard.
166      *
167      * @param totalAvailableSpace space for notifications. This includes the space for the shelf.
168      */
169     fun computeMaxKeyguardNotifications(
170         stack: NotificationStackScrollLayout,
171         notifSpace: Float,
172         shelfSpace: Float,
173         shelfHeight: Float,
174     ): Int {
175         log { "\n " }
176         log {
177             "computeMaxKeyguardNotifications ---" +
178                 "\n\tnotifSpace $notifSpace" +
179                 "\n\tspaceForShelf $shelfSpace" +
180                 "\n\tshelfIntrinsicHeight $shelfHeight"
181         }
182         if (notifSpace + shelfSpace <= 0f) {
183             log { "--- No space to show anything. maxNotifs=0" }
184             return 0
185         }
186         log { "\n" }
187 
188         val stackHeightSequence = computeHeightPerNotificationLimit(stack, shelfHeight)
189 
190         // TODO: Avoid making this split shade assumption by simply checking the stack for media
191         val isMediaShowing = mediaDataManager.hasActiveMediaOrRecommendation()
192         val isMediaShowingInStack =
193             isMediaShowing && !splitShadeStateController.shouldUseSplitNotificationShade(resources)
194 
195         log { "\tGet maxNotifWithoutSavingSpace ---" }
196         val maxNotifWithoutSavingSpace =
197             stackHeightSequence.lastIndexWhile { heightResult ->
198                 allowedByPolicy(heightResult) &&
199                     canStackFitInSpace(
200                         heightResult,
201                         notifSpace = notifSpace,
202                         shelfSpace = shelfSpace
203                     ) == FitResult.FIT
204             }
205 
206         // How many notifications we can show at heightWithoutLockscreenConstraints
207         val minCountAtHeightWithoutConstraints = if (isMediaShowingInStack) 2 else 1
208         log {
209             "\t---maxNotifWithoutSavingSpace=$maxNotifWithoutSavingSpace " +
210                 "isMediaShowing=$isMediaShowing" +
211                 "isMediaShowingInStack=$isMediaShowingInStack" +
212                 "minCountAtHeightWithoutConstraints=$minCountAtHeightWithoutConstraints"
213         }
214         log { "\n" }
215 
216         var maxNotifications: Int
217         if (maxNotifWithoutSavingSpace >= minCountAtHeightWithoutConstraints) {
218             saveSpaceOnLockscreen = false
219             maxNotifications = maxNotifWithoutSavingSpace
220             log {
221                 "\tDo NOT save space. maxNotifications=maxNotifWithoutSavingSpace=$maxNotifications"
222             }
223         } else {
224             log { "\tSAVE space ---" }
225             saveSpaceOnLockscreen = true
226             maxNotifications =
227                 stackHeightSequence.lastIndexWhile { heightResult ->
228                     allowedByPolicy(heightResult) &&
229                         canStackFitInSpace(
230                             heightResult,
231                             notifSpace = notifSpace,
232                             shelfSpace = shelfSpace
233                         ) != FitResult.NO_FIT
234                 }
235             log { "\t--- maxNotifications=$maxNotifications" }
236         }
237 
238         // Must update views immediately to avoid mismatches between initial HUN layout height
239         // and the height adapted to lockscreen space constraints, which causes jump cuts.
240         stack.showableChildren().toList().forEach { currentNotification ->
241             run {
242                 if (currentNotification is ExpandableNotificationRow) {
243                     currentNotification.saveSpaceOnLockscreen = saveSpaceOnLockscreen
244                 }
245             }
246         }
247 
248         if (onLockscreen()) {
249             val increaseMaxForMedia = maxNotificationsExcludesMedia && isMediaShowingInStack
250             val lockscreenMax = maxKeyguardNotifications.safeIncrementIf(increaseMaxForMedia)
251             maxNotifications = min(lockscreenMax, maxNotifications)
252         }
253 
254         // Could be < 0 if the space available is less than the shelf size. Returns 0 in this case.
255         maxNotifications = max(0, maxNotifications)
256         log {
257             val sequence = if (SPEW) " stackHeightSequence=${stackHeightSequence.toList()}" else ""
258             "--- computeMaxKeyguardNotifications(" +
259                 " notifSpace=$notifSpace" +
260                 " shelfSpace=$shelfSpace" +
261                 " shelfHeight=$shelfHeight) -> $maxNotifications$sequence"
262         }
263         log { "\n" }
264         return maxNotifications
265     }
266 
267     /**
268      * Given the [maxNotifs] constraint, calculates the height of the
269      * [NotificationStackScrollLayout]. This might or might not be in keyguard.
270      *
271      * @param stack stack containing notifications as children.
272      * @param maxNotifs Maximum number of notifications. When reached, the others will go into the
273      *   shelf.
274      * @param shelfHeight height of the shelf, without any padding. It might be zero.
275      * @return height of the stack, including shelf height, if needed.
276      */
277     fun computeHeight(
278         stack: NotificationStackScrollLayout,
279         maxNotifs: Int,
280         shelfHeight: Float
281     ): Float {
282         log { "\n" }
283         log { "computeHeight ---" }
284 
285         val stackHeightSequence = computeHeightPerNotificationLimit(stack, shelfHeight)
286 
287         val (notifsHeight, notifsHeightSavingSpace, shelfHeightWithSpaceBefore) =
288             stackHeightSequence.elementAtOrElse(maxNotifs) {
289                 stackHeightSequence.last() // Height with all notifications visible.
290             }
291 
292         var height: Float
293         if (saveSpaceOnLockscreen) {
294             height = notifsHeightSavingSpace + shelfHeightWithSpaceBefore
295             log {
296                 "--- computeHeight(maxNotifs=$maxNotifs, shelfHeight=$shelfHeight)" +
297                     " -> $height=($notifsHeightSavingSpace+$shelfHeightWithSpaceBefore)," +
298                     " | saveSpaceOnLockscreen=$saveSpaceOnLockscreen"
299             }
300         } else {
301             height = notifsHeight + shelfHeightWithSpaceBefore
302             log {
303                 "--- computeHeight(maxNotifs=$maxNotifs, shelfHeight=$shelfHeight)" +
304                     " -> $height=($notifsHeight+$shelfHeightWithSpaceBefore)" +
305                     " | saveSpaceOnLockscreen=$saveSpaceOnLockscreen"
306             }
307         }
308         return height
309     }
310 
311     private enum class FitResult {
312         FIT,
313         FIT_IF_SAVE_SPACE,
314         NO_FIT
315     }
316 
317     data class SpaceNeeded(
318         // Float height of spaceNeeded when showing heads up layout for FSI HUNs.
319         val whenEnoughSpace: Float,
320 
321         // Float height of space needed when showing collapsed layout for FSI HUNs.
322         val whenSavingSpace: Float
323     )
324 
325     private data class StackHeight(
326         // Float height with ith max notifications (not including shelf)
327         val notifsHeight: Float,
328 
329         // Float height with ith max notifications
330         // (not including shelf, using collapsed layout for FSI HUN)
331         val notifsHeightSavingSpace: Float,
332 
333         // Float height of shelf (0 if shelf is not showing), and space before the shelf that
334         // changes during the lockscreen <=> full shade transition.
335         val shelfHeightWithSpaceBefore: Float,
336 
337         /** Whether the stack should actually be forced into the shelf before this height. */
338         val shouldForceIntoShelf: Boolean
339     )
340 
341     private fun computeHeightPerNotificationLimit(
342         stack: NotificationStackScrollLayout,
343         shelfHeight: Float,
344     ): Sequence<StackHeight> = sequence {
345         val children = stack.showableChildren().toList()
346         var notifications = 0f
347         var notifsWithCollapsedHun = 0f
348         var previous: ExpandableView? = null
349         val onLockscreen = onLockscreen()
350 
351         val counter = if (limitLockScreenToOneImportant) BucketTypeCounter() else null
352 
353         // Only shelf. This should never happen, since we allow 1 view minimum (EmptyViewState).
354         yield(
355             StackHeight(
356                 notifsHeight = 0f,
357                 notifsHeightSavingSpace = 0f,
358                 shelfHeightWithSpaceBefore = shelfHeight,
359                 shouldForceIntoShelf = false,
360             )
361         )
362 
363         children.forEachIndexed { i, currentNotification ->
364             val space = getSpaceNeeded(currentNotification, i, previous, stack, onLockscreen)
365             notifications += space.whenEnoughSpace
366             notifsWithCollapsedHun += space.whenSavingSpace
367 
368             previous = currentNotification
369 
370             val shelfWithSpaceBefore =
371                 if (i == children.lastIndex) {
372                     0f // No shelf needed.
373                 } else {
374                     val firstViewInShelfIndex = i + 1
375                     val spaceBeforeShelf =
376                         calculateGapAndDividerHeight(
377                             stack,
378                             previous = currentNotification,
379                             current = children[firstViewInShelfIndex],
380                             currentIndex = firstViewInShelfIndex
381                         )
382                     spaceBeforeShelf + shelfHeight
383                 }
384 
385             if (counter != null) {
386                 val entry = (currentNotification as? ExpandableNotificationRow)?.entry
387                 counter.incrementForBucket(entry?.bucket)
388             }
389 
390             log {
391                 "\tcomputeHeightPerNotificationLimit i=$i notifs=$notifications " +
392                     "notifsHeightSavingSpace=$notifsWithCollapsedHun" +
393                     " shelfWithSpaceBefore=$shelfWithSpaceBefore"
394             }
395             yield(
396                 StackHeight(
397                     notifsHeight = notifications,
398                     notifsHeightSavingSpace = notifsWithCollapsedHun,
399                     shelfHeightWithSpaceBefore = shelfWithSpaceBefore,
400                     shouldForceIntoShelf = counter?.shouldForceIntoShelf() ?: false
401                 )
402             )
403         }
404     }
405 
406     fun updateResources() {
407         maxKeyguardNotifications =
408             infiniteIfNegative(
409                 if (NotificationMinimalismPrototype.V1.isEnabled) {
410                     NotificationMinimalismPrototype.V1.maxNotifs
411                 } else {
412                     resources.getInteger(R.integer.keyguard_max_notification_count)
413                 }
414             )
415         maxNotificationsExcludesMedia =
416             NotificationMinimalismPrototype.V1.isEnabled ||
417                 NotificationMinimalismPrototype.V2.isEnabled
418 
419         dividerHeight =
420             max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat())
421     }
422 
423     private val NotificationStackScrollLayout.childrenSequence: Sequence<ExpandableView>
424         get() = children.map { it as ExpandableView }
425 
426     @VisibleForTesting
427     fun onLockscreen(): Boolean {
428         return statusBarStateController.state == KEYGUARD &&
429             lockscreenShadeTransitionController.fractionToShade == 0f
430     }
431 
432     @VisibleForTesting
433     fun getSpaceNeeded(
434         view: ExpandableView,
435         visibleIndex: Int,
436         previousView: ExpandableView?,
437         stack: NotificationStackScrollLayout,
438         onLockscreen: Boolean,
439     ): SpaceNeeded {
440         assert(view.isShowable(onLockscreen))
441 
442         // Must use heightWithoutLockscreenConstraints because intrinsicHeight references
443         // mSaveSpaceOnLockscreen and using intrinsicHeight here will result in stack overflow.
444         val height = view.heightWithoutLockscreenConstraints.toFloat()
445         val gapAndDividerHeight =
446             calculateGapAndDividerHeight(stack, previousView, current = view, visibleIndex)
447 
448         var size =
449             if (onLockscreen) {
450                 if (view is ExpandableNotificationRow && view.entry.isStickyAndNotDemoted) {
451                     height
452                 } else {
453                     view.getMinHeight(/* ignoreTemporaryStates= */ true).toFloat()
454                 }
455             } else {
456                 height
457             }
458         size += gapAndDividerHeight
459 
460         var sizeWhenSavingSpace =
461             if (onLockscreen) {
462                 view.getMinHeight(/* ignoreTemporaryStates= */ true).toFloat()
463             } else {
464                 height
465             }
466         sizeWhenSavingSpace += gapAndDividerHeight
467 
468         return SpaceNeeded(size, sizeWhenSavingSpace)
469     }
470 
471     fun dump(pw: PrintWriter, args: Array<out String>) {
472         pw.println("NotificationStackSizeCalculator saveSpaceOnLockscreen=$saveSpaceOnLockscreen")
473     }
474 
475     private fun ExpandableView.isShowable(onLockscreen: Boolean): Boolean {
476         if (visibility == GONE || hasNoContentHeight()) return false
477         if (onLockscreen) {
478             when (this) {
479                 is ExpandableNotificationRow -> {
480                     if (!canShowViewOnLockscreen() || isRemoved) {
481                         return false
482                     }
483                 }
484                 is MediaContainerView -> if (intrinsicHeight == 0) return false
485                 else -> return false
486             }
487         }
488         return true
489     }
490 
491     private fun calculateGapAndDividerHeight(
492         stack: NotificationStackScrollLayout,
493         previous: ExpandableView?,
494         current: ExpandableView?,
495         currentIndex: Int
496     ): Float {
497         if (currentIndex == 0) {
498             return 0f
499         }
500         return stack.calculateGapHeight(previous, current, currentIndex) + dividerHeight
501     }
502 
503     private fun NotificationStackScrollLayout.showableChildren() =
504         this.childrenSequence.filter { it.isShowable(onLockscreen()) }
505 
506     /**
507      * Can a view be shown on the lockscreen when calculating the number of allowed notifications to
508      * show?
509      *
510      * @return `true` if it can be shown.
511      */
512     private fun ExpandableView.canShowViewOnLockscreen(): Boolean {
513         if (hasNoContentHeight()) {
514             return false
515         } else if (visibility == GONE) {
516             return false
517         }
518         return true
519     }
520 
521     private inline fun log(s: () -> String) {
522         if (DEBUG) {
523             Log.d(TAG, s())
524         }
525     }
526 
527     /** Returns infinite when [v] is negative. Useful in case a resource doesn't limit when -1. */
528     private fun infiniteIfNegative(v: Int): Int =
529         if (v < 0) {
530             Int.MAX_VALUE
531         } else {
532             v
533         }
534 
535     private fun Int.safeIncrementIf(condition: Boolean): Int =
536         if (condition && this != Int.MAX_VALUE) {
537             this + 1
538         } else {
539             this
540         }
541 
542     /** Returns the last index where [predicate] returns true, or -1 if it was always false. */
543     private fun <T> Sequence<T>.lastIndexWhile(predicate: (T) -> Boolean): Int =
544         takeWhile(predicate).count() - 1
545 
546     /** Counts the number of notifications for each type of bucket */
547     data class BucketTypeCounter(
548         var ongoing: Int = 0,
549         var important: Int = 0,
550         var other: Int = 0,
551     ) {
552         fun incrementForBucket(@PriorityBucket bucket: Int?) {
553             when (bucket) {
554                 BUCKET_MEDIA_CONTROLS,
555                 null -> Unit // not counted as notifications at all
556                 BUCKET_TOP_ONGOING -> ongoing++
557                 BUCKET_HEADS_UP -> important++
558                 BUCKET_TOP_UNSEEN -> important++
559                 else -> other++
560             }
561         }
562 
563         fun shouldForceIntoShelf(): Boolean = ongoing > 1 || important > 1 || other > 0
564     }
565 }
566