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