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