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