• 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.R
24 import com.android.systemui.dagger.SysUISingleton
25 import com.android.systemui.dagger.qualifiers.Main
26 import com.android.systemui.statusbar.LockscreenShadeTransitionController
27 import com.android.systemui.statusbar.StatusBarState.KEYGUARD
28 import com.android.systemui.statusbar.SysuiStatusBarStateController
29 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
30 import com.android.systemui.statusbar.notification.row.ExpandableView
31 import com.android.systemui.util.Compile
32 import com.android.systemui.util.children
33 import java.io.PrintWriter
34 import javax.inject.Inject
35 import kotlin.math.max
36 import kotlin.math.min
37 import kotlin.properties.Delegates.notNull
38 
39 private const val TAG = "NotifStackSizeCalc"
40 private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG)
41 private val SPEW = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE)
42 
43 /**
44  * Calculates number of notifications to display and the height of the notification stack.
45  * "Notifications" refers to any ExpandableView that we show on lockscreen, which can include the
46  * media player.
47  */
48 @SysUISingleton
49 class NotificationStackSizeCalculator
50 @Inject
51 constructor(
52     private val statusBarStateController: SysuiStatusBarStateController,
53     private val lockscreenShadeTransitionController: LockscreenShadeTransitionController,
54     @Main private val resources: Resources
55 ) {
56 
57     private lateinit var lastComputeHeightLog : String
58 
59     /**
60      * Maximum # notifications to show on Keyguard; extras will be collapsed in an overflow shelf.
61      * If there are exactly 1 + mMaxKeyguardNotifications, and they fit in the available space
62      * (considering the overflow shelf is not displayed in this case), then all notifications are
63      * shown.
64      */
65     private var maxKeyguardNotifications by notNull<Int>()
66 
67     /** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */
68     private var dividerHeight by notNull<Float>()
69 
70     init {
71         updateResources()
72     }
73 
74     /**
75      * Returns whether notifications and (shelf if visible) can fit in total space available.
76      * [spaceForShelf] is extra vertical space allowed for the shelf to overlap the lock icon.
77      */
78     private fun canStackFitInSpace(
79         stackHeight: StackHeight,
80         spaceForNotifications: Float,
81         spaceForShelf: Float,
82     ): Boolean {
83 
84         val (notificationsHeight, shelfHeightWithSpaceBefore) = stackHeight
85         var canFit: Boolean
86 
87         if (shelfHeightWithSpaceBefore == 0f) {
88             canFit = notificationsHeight <= spaceForNotifications
89             log {
90                 "canStackFitInSpace[$canFit] = notificationsHeight[$notificationsHeight]" +
91                     " <= spaceForNotifications[$spaceForNotifications]"
92             }
93         } else {
94             canFit =
95                 (notificationsHeight + shelfHeightWithSpaceBefore) <=
96                     (spaceForNotifications + spaceForShelf)
97             log {
98                 "canStackFitInSpace[$canFit] = (notificationsHeight[$notificationsHeight]" +
99                     " + shelfHeightWithSpaceBefore[$shelfHeightWithSpaceBefore])" +
100                     " <= (spaceForNotifications[$spaceForNotifications] " +
101                     " + spaceForShelf[$spaceForShelf])"
102             }
103         }
104         return canFit
105     }
106 
107     /**
108      * Given the [spaceForNotifications] and [spaceForShelf] constraints, calculate how many
109      * notifications to show. This number is only valid in keyguard.
110      *
111      * @param totalAvailableSpace space for notifications. This includes the space for the shelf.
112      */
113     fun computeMaxKeyguardNotifications(
114         stack: NotificationStackScrollLayout,
115         spaceForNotifications: Float,
116         spaceForShelf: Float,
117         shelfIntrinsicHeight: Float
118     ): Int {
119         log { "\n" }
120 
121         val stackHeightSequence = computeHeightPerNotificationLimit(stack, shelfIntrinsicHeight,
122             /* computeHeight= */ false)
123 
124         var maxNotifications =
125             stackHeightSequence.lastIndexWhile { heightResult ->
126                 canStackFitInSpace(
127                     heightResult,
128                     spaceForNotifications = spaceForNotifications,
129                     spaceForShelf = spaceForShelf)
130             }
131 
132         if (onLockscreen()) {
133             maxNotifications = min(maxKeyguardNotifications, maxNotifications)
134         }
135 
136         // Could be < 0 if the space available is less than the shelf size. Returns 0 in this case.
137         maxNotifications = max(0, maxNotifications)
138         log {
139             val sequence = if (SPEW) " stackHeightSequence=${stackHeightSequence.toList()}" else ""
140             "computeMaxKeyguardNotifications(" +
141                 " spaceForNotifications=$spaceForNotifications" +
142                 " spaceForShelf=$spaceForShelf" +
143                 " shelfHeight=$shelfIntrinsicHeight) -> $maxNotifications$sequence"
144         }
145         return maxNotifications
146     }
147 
148     /**
149      * Given the [maxNotifications] constraint, calculates the height of the
150      * [NotificationStackScrollLayout]. This might or might not be in keyguard.
151      *
152      * @param stack stack containing notifications as children.
153      * @param maxNotifications Maximum number of notifications. When reached, the others will go
154      * into the shelf.
155      * @param shelfIntrinsicHeight height of the shelf, without any padding. It might be zero.
156      *
157      * @return height of the stack, including shelf height, if needed.
158      */
159     fun computeHeight(
160         stack: NotificationStackScrollLayout,
161         maxNotifications: Int,
162         shelfIntrinsicHeight: Float
163     ): Float {
164         log { "\n" }
165         lastComputeHeightLog = ""
166         val heightPerMaxNotifications =
167             computeHeightPerNotificationLimit(stack, shelfIntrinsicHeight,
168                     /* computeHeight= */ true)
169 
170         val (notificationsHeight, shelfHeightWithSpaceBefore) =
171             heightPerMaxNotifications.elementAtOrElse(maxNotifications) {
172                 heightPerMaxNotifications.last() // Height with all notifications visible.
173             }
174         lastComputeHeightLog += "\ncomputeHeight(maxNotifications=$maxNotifications," +
175                 "shelfIntrinsicHeight=$shelfIntrinsicHeight) -> " +
176                 "${notificationsHeight + shelfHeightWithSpaceBefore}" +
177                 " = ($notificationsHeight + $shelfHeightWithSpaceBefore)"
178         log {
179             lastComputeHeightLog
180         }
181         return notificationsHeight + shelfHeightWithSpaceBefore
182     }
183 
184     private data class StackHeight(
185         // Float height with ith max notifications (not including shelf)
186         val notificationsHeight: Float,
187 
188         // Float height of shelf (0 if shelf is not showing), and space before the shelf that
189         // changes during the lockscreen <=> full shade transition.
190         val shelfHeightWithSpaceBefore: Float
191     )
192 
193     private fun computeHeightPerNotificationLimit(
194         stack: NotificationStackScrollLayout,
195         shelfHeight: Float,
196         computeHeight: Boolean
197     ): Sequence<StackHeight> = sequence {
198         log { "computeHeightPerNotificationLimit" }
199 
200         val children = stack.showableChildren().toList()
201         var notifications = 0f
202         var previous: ExpandableView? = null
203         val onLockscreen = onLockscreen()
204 
205         // Only shelf. This should never happen, since we allow 1 view minimum (EmptyViewState).
206         yield(StackHeight(notificationsHeight = 0f, shelfHeightWithSpaceBefore = shelfHeight))
207 
208         children.forEachIndexed { i, currentNotification ->
209             notifications += spaceNeeded(currentNotification, i, previous, stack, onLockscreen)
210             previous = currentNotification
211 
212             val shelfWithSpaceBefore =
213                 if (i == children.lastIndex) {
214                     0f // No shelf needed.
215                 } else {
216                     val firstViewInShelfIndex = i + 1
217                     val spaceBeforeShelf =
218                         calculateGapAndDividerHeight(
219                             stack,
220                             previous = currentNotification,
221                             current = children[firstViewInShelfIndex],
222                             currentIndex = firstViewInShelfIndex)
223                     spaceBeforeShelf + shelfHeight
224                 }
225 
226             val currentLog = "computeHeight | i=$i notificationsHeight=$notifications " +
227                 "shelfHeightWithSpaceBefore=$shelfWithSpaceBefore"
228             if (computeHeight) {
229                 lastComputeHeightLog += "\n" + currentLog
230             }
231             log {
232                 currentLog
233             }
234             yield(
235                 StackHeight(
236                     notificationsHeight = notifications,
237                     shelfHeightWithSpaceBefore = shelfWithSpaceBefore))
238         }
239     }
240 
241     fun updateResources() {
242         maxKeyguardNotifications =
243             infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count))
244 
245         dividerHeight =
246             max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat())
247     }
248 
249     private val NotificationStackScrollLayout.childrenSequence: Sequence<ExpandableView>
250         get() = children.map { it as ExpandableView }
251 
252     @VisibleForTesting
253     fun onLockscreen(): Boolean {
254         return statusBarStateController.state == KEYGUARD &&
255             lockscreenShadeTransitionController.fractionToShade == 0f
256     }
257 
258     @VisibleForTesting
259     fun spaceNeeded(
260         view: ExpandableView,
261         visibleIndex: Int,
262         previousView: ExpandableView?,
263         stack: NotificationStackScrollLayout,
264         onLockscreen: Boolean
265     ): Float {
266         assert(view.isShowable(onLockscreen))
267         var size =
268             if (onLockscreen) {
269                 view.getMinHeight(/* ignoreTemporaryStates= */ true).toFloat()
270             } else {
271                 view.intrinsicHeight.toFloat()
272             }
273         size += calculateGapAndDividerHeight(stack, previousView, current = view, visibleIndex)
274         return size
275     }
276 
277     fun dump(pw: PrintWriter, args: Array<out String>) {
278         pw.println("NotificationStackSizeCalculator lastComputeHeightLog = $lastComputeHeightLog")
279     }
280 
281     private fun ExpandableView.isShowable(onLockscreen: Boolean): Boolean {
282         if (visibility == GONE || hasNoContentHeight()) return false
283         if (onLockscreen) {
284             when (this) {
285                 is ExpandableNotificationRow -> {
286                     if (!canShowViewOnLockscreen() || isRemoved) {
287                         return false
288                     }
289                 }
290                 is MediaContainerView -> if (intrinsicHeight == 0) return false
291                 else -> return false
292             }
293         }
294         return true
295     }
296 
297     private fun calculateGapAndDividerHeight(
298         stack: NotificationStackScrollLayout,
299         previous: ExpandableView?,
300         current: ExpandableView?,
301         currentIndex: Int
302     ): Float {
303         if (currentIndex == 0) {
304             return 0f
305         }
306         return stack.calculateGapHeight(previous, current, currentIndex) + dividerHeight
307     }
308 
309     private fun NotificationStackScrollLayout.showableChildren() =
310         this.childrenSequence.filter { it.isShowable(onLockscreen()) }
311 
312     /**
313      * Can a view be shown on the lockscreen when calculating the number of allowed notifications to
314      * show?
315      *
316      * @return `true` if it can be shown.
317      */
318     private fun ExpandableView.canShowViewOnLockscreen(): Boolean {
319         if (hasNoContentHeight()) {
320             return false
321         } else if (visibility == GONE) {
322             return false
323         }
324         return true
325     }
326 
327     private inline fun log(s: () -> String) {
328         if (DEBUG) {
329             Log.d(TAG, s())
330         }
331     }
332 
333     /** Returns infinite when [v] is negative. Useful in case a resource doesn't limit when -1. */
334     private fun infiniteIfNegative(v: Int): Int =
335         if (v < 0) {
336             Int.MAX_VALUE
337         } else {
338             v
339         }
340 
341     /** Returns the last index where [predicate] returns true, or -1 if it was always false. */
342     private fun <T> Sequence<T>.lastIndexWhile(predicate: (T) -> Boolean): Int =
343         takeWhile(predicate).count() - 1
344 }
345