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.shade 18 19 import android.view.View 20 import android.view.ViewGroup 21 import android.view.WindowInsets 22 import androidx.annotation.VisibleForTesting 23 import androidx.constraintlayout.widget.ConstraintSet 24 import androidx.constraintlayout.widget.ConstraintSet.BOTTOM 25 import androidx.constraintlayout.widget.ConstraintSet.END 26 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID 27 import androidx.constraintlayout.widget.ConstraintSet.START 28 import androidx.constraintlayout.widget.ConstraintSet.TOP 29 import com.android.systemui.R 30 import com.android.systemui.dagger.SysUISingleton 31 import com.android.systemui.dagger.qualifiers.Main 32 import com.android.systemui.flags.FeatureFlags 33 import com.android.systemui.flags.Flags 34 import com.android.systemui.fragments.FragmentService 35 import com.android.systemui.navigationbar.NavigationModeController 36 import com.android.systemui.plugins.qs.QS 37 import com.android.systemui.plugins.qs.QSContainerController 38 import com.android.systemui.recents.OverviewProxyService 39 import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener 40 import com.android.systemui.shared.system.QuickStepContract 41 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController 42 import com.android.systemui.util.LargeScreenUtils 43 import com.android.systemui.util.ViewController 44 import com.android.systemui.util.concurrency.DelayableExecutor 45 import java.util.function.Consumer 46 import javax.inject.Inject 47 import kotlin.reflect.KMutableProperty0 48 49 @VisibleForTesting 50 internal const val INSET_DEBOUNCE_MILLIS = 500L 51 52 @SysUISingleton 53 class NotificationsQSContainerController @Inject constructor( 54 view: NotificationsQuickSettingsContainer, 55 private val navigationModeController: NavigationModeController, 56 private val overviewProxyService: OverviewProxyService, 57 private val shadeHeaderController: ShadeHeaderController, 58 private val shadeExpansionStateManager: ShadeExpansionStateManager, 59 private val fragmentService: FragmentService, 60 @Main private val delayableExecutor: DelayableExecutor, 61 private val featureFlags: FeatureFlags, 62 private val 63 notificationStackScrollLayoutController: NotificationStackScrollLayoutController, 64 ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController { 65 66 private var qsExpanded = false 67 private var splitShadeEnabled = false 68 private var isQSDetailShowing = false 69 private var isQSCustomizing = false 70 private var isQSCustomizerAnimating = false 71 72 private var shadeHeaderHeight = 0 73 private var largeScreenShadeHeaderHeight = 0 74 private var largeScreenShadeHeaderActive = false 75 private var notificationsBottomMargin = 0 76 private var scrimShadeBottomMargin = 0 77 private var footerActionsOffset = 0 78 private var bottomStableInsets = 0 79 private var bottomCutoutInsets = 0 80 private var panelMarginHorizontal = 0 81 private var topMargin = 0 82 83 private var isGestureNavigation = true 84 private var taskbarVisible = false 85 private val taskbarVisibilityListener: OverviewProxyListener = object : OverviewProxyListener { 86 override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) { 87 taskbarVisible = visible 88 } 89 } 90 private val shadeQsExpansionListener: ShadeQsExpansionListener = 91 ShadeQsExpansionListener { isQsExpanded -> 92 if (qsExpanded != isQsExpanded) { 93 qsExpanded = isQsExpanded 94 mView.invalidate() 95 } 96 } 97 98 // With certain configuration changes (like light/dark changes), the nav bar will disappear 99 // for a bit, causing `bottomStableInsets` to be unstable for some time. Debounce the value 100 // for 500ms. 101 // All interactions with this object happen in the main thread. 102 private val delayedInsetSetter = object : Runnable, Consumer<WindowInsets> { 103 private var canceller: Runnable? = null 104 private var stableInsets = 0 105 private var cutoutInsets = 0 106 107 override fun accept(insets: WindowInsets) { 108 // when taskbar is visible, stableInsetBottom will include its height 109 stableInsets = insets.stableInsetBottom 110 cutoutInsets = insets.displayCutout?.safeInsetBottom ?: 0 111 canceller?.run() 112 canceller = delayableExecutor.executeDelayed(this, INSET_DEBOUNCE_MILLIS) 113 } 114 115 override fun run() { 116 bottomStableInsets = stableInsets 117 bottomCutoutInsets = cutoutInsets 118 updateBottomSpacing() 119 } 120 } 121 122 override fun onInit() { 123 val currentMode: Int = navigationModeController.addListener { mode: Int -> 124 isGestureNavigation = QuickStepContract.isGesturalMode(mode) 125 } 126 isGestureNavigation = QuickStepContract.isGesturalMode(currentMode) 127 128 mView.setStackScroller(notificationStackScrollLayoutController.getView()) 129 mView.setMigratingNSSL(featureFlags.isEnabled(Flags.MIGRATE_NSSL)) 130 } 131 132 public override fun onViewAttached() { 133 updateResources() 134 overviewProxyService.addCallback(taskbarVisibilityListener) 135 shadeExpansionStateManager.addQsExpansionListener(shadeQsExpansionListener) 136 mView.setInsetsChangedListener(delayedInsetSetter) 137 mView.setQSFragmentAttachedListener { qs: QS -> qs.setContainerController(this) } 138 mView.setConfigurationChangedListener { updateResources() } 139 fragmentService.getFragmentHostManager(mView).addTagListener(QS.TAG, mView) 140 } 141 142 override fun onViewDetached() { 143 overviewProxyService.removeCallback(taskbarVisibilityListener) 144 shadeExpansionStateManager.removeQsExpansionListener(shadeQsExpansionListener) 145 mView.removeOnInsetsChangedListener() 146 mView.removeQSFragmentAttachedListener() 147 mView.setConfigurationChangedListener(null) 148 fragmentService.getFragmentHostManager(mView).removeTagListener(QS.TAG, mView) 149 } 150 151 fun updateResources() { 152 val newSplitShadeEnabled = LargeScreenUtils.shouldUseSplitNotificationShade(resources) 153 val splitShadeEnabledChanged = newSplitShadeEnabled != splitShadeEnabled 154 splitShadeEnabled = newSplitShadeEnabled 155 largeScreenShadeHeaderActive = LargeScreenUtils.shouldUseLargeScreenShadeHeader(resources) 156 notificationsBottomMargin = resources.getDimensionPixelSize( 157 R.dimen.notification_panel_margin_bottom) 158 largeScreenShadeHeaderHeight = calculateLargeShadeHeaderHeight() 159 shadeHeaderHeight = calculateShadeHeaderHeight() 160 panelMarginHorizontal = resources.getDimensionPixelSize( 161 R.dimen.notification_panel_margin_horizontal) 162 topMargin = if (largeScreenShadeHeaderActive) { 163 largeScreenShadeHeaderHeight 164 } else { 165 resources.getDimensionPixelSize(R.dimen.notification_panel_margin_top) 166 } 167 updateConstraints() 168 169 val scrimMarginChanged = ::scrimShadeBottomMargin.setAndReportChange( 170 resources.getDimensionPixelSize(R.dimen.split_shade_notifications_scrim_margin_bottom) 171 ) 172 val footerOffsetChanged = ::footerActionsOffset.setAndReportChange( 173 resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset) + 174 resources.getDimensionPixelSize(R.dimen.qs_footer_actions_bottom_padding) 175 ) 176 val dimensChanged = scrimMarginChanged || footerOffsetChanged 177 178 if (splitShadeEnabledChanged || dimensChanged) { 179 updateBottomSpacing() 180 } 181 } 182 183 private fun calculateLargeShadeHeaderHeight(): Int { 184 return resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height) 185 } 186 187 private fun calculateShadeHeaderHeight(): Int { 188 val minHeight = resources.getDimensionPixelSize(R.dimen.qs_header_height) 189 190 // Following the constraints in xml/qs_header, the total needed height would be the sum of 191 // 1. privacy_container height (R.dimen.large_screen_shade_header_min_height) 192 // 2. carrier_group height (R.dimen.large_screen_shade_header_min_height) 193 // 3. date height (R.dimen.new_qs_header_non_clickable_element_height) 194 val estimatedHeight = 195 2 * resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height) + 196 resources.getDimensionPixelSize(R.dimen.new_qs_header_non_clickable_element_height) 197 return estimatedHeight.coerceAtLeast(minHeight) 198 } 199 200 override fun setCustomizerAnimating(animating: Boolean) { 201 if (isQSCustomizerAnimating != animating) { 202 isQSCustomizerAnimating = animating 203 mView.invalidate() 204 } 205 } 206 207 override fun setCustomizerShowing(showing: Boolean, animationDuration: Long) { 208 if (showing != isQSCustomizing) { 209 isQSCustomizing = showing 210 shadeHeaderController.startCustomizingAnimation(showing, animationDuration) 211 updateBottomSpacing() 212 } 213 } 214 215 override fun setDetailShowing(showing: Boolean) { 216 isQSDetailShowing = showing 217 updateBottomSpacing() 218 } 219 220 private fun updateBottomSpacing() { 221 val (containerPadding, notificationsMargin, qsContainerPadding) = calculateBottomSpacing() 222 mView.setPadding(0, 0, 0, containerPadding) 223 mView.setNotificationsMarginBottom(notificationsMargin) 224 mView.setQSContainerPaddingBottom(qsContainerPadding) 225 } 226 227 private fun calculateBottomSpacing(): Paddings { 228 val containerPadding: Int 229 val stackScrollMargin: Int 230 if (!splitShadeEnabled && (isQSCustomizing || isQSDetailShowing)) { 231 // Clear out bottom paddings/margins so the qs customization can be full height. 232 containerPadding = 0 233 stackScrollMargin = 0 234 } else if (isGestureNavigation) { 235 // only default cutout padding, taskbar always hides 236 containerPadding = bottomCutoutInsets 237 stackScrollMargin = notificationsBottomMargin 238 } else if (taskbarVisible) { 239 // navigation buttons + visible taskbar means we're NOT on homescreen 240 containerPadding = bottomStableInsets 241 stackScrollMargin = notificationsBottomMargin 242 } else { 243 // navigation buttons + hidden taskbar means we're on homescreen 244 containerPadding = 0 245 stackScrollMargin = bottomStableInsets + notificationsBottomMargin 246 } 247 val qsContainerPadding = if (!isQSDetailShowing) { 248 // We also want this padding in the bottom in these cases 249 if (splitShadeEnabled) { 250 stackScrollMargin - scrimShadeBottomMargin - footerActionsOffset 251 } else { 252 bottomStableInsets 253 } 254 } else { 255 0 256 } 257 return Paddings(containerPadding, stackScrollMargin, qsContainerPadding) 258 } 259 260 fun updateConstraints() { 261 // To change the constraints at runtime, all children of the ConstraintLayout must have ids 262 ensureAllViewsHaveIds(mView) 263 val constraintSet = ConstraintSet() 264 constraintSet.clone(mView) 265 setKeyguardStatusViewConstraints(constraintSet) 266 setQsConstraints(constraintSet) 267 setNotificationsConstraints(constraintSet) 268 setLargeScreenShadeHeaderConstraints(constraintSet) 269 mView.applyConstraints(constraintSet) 270 } 271 272 private fun setLargeScreenShadeHeaderConstraints(constraintSet: ConstraintSet) { 273 if (largeScreenShadeHeaderActive) { 274 constraintSet.constrainHeight(R.id.split_shade_status_bar, largeScreenShadeHeaderHeight) 275 } else { 276 constraintSet.constrainHeight(R.id.split_shade_status_bar, shadeHeaderHeight) 277 } 278 } 279 280 private fun setNotificationsConstraints(constraintSet: ConstraintSet) { 281 if (featureFlags.isEnabled(Flags.MIGRATE_NSSL)) { 282 return 283 } 284 val startConstraintId = if (splitShadeEnabled) R.id.qs_edge_guideline else PARENT_ID 285 val nsslId = R.id.notification_stack_scroller 286 constraintSet.apply { 287 connect(nsslId, START, startConstraintId, START) 288 setMargin(nsslId, START, if (splitShadeEnabled) 0 else panelMarginHorizontal) 289 setMargin(nsslId, END, panelMarginHorizontal) 290 setMargin(nsslId, TOP, topMargin) 291 setMargin(nsslId, BOTTOM, notificationsBottomMargin) 292 } 293 } 294 295 private fun setQsConstraints(constraintSet: ConstraintSet) { 296 val endConstraintId = if (splitShadeEnabled) R.id.qs_edge_guideline else PARENT_ID 297 constraintSet.apply { 298 connect(R.id.qs_frame, END, endConstraintId, END) 299 setMargin(R.id.qs_frame, START, if (splitShadeEnabled) 0 else panelMarginHorizontal) 300 setMargin(R.id.qs_frame, END, if (splitShadeEnabled) 0 else panelMarginHorizontal) 301 setMargin(R.id.qs_frame, TOP, topMargin) 302 } 303 } 304 305 private fun setKeyguardStatusViewConstraints(constraintSet: ConstraintSet) { 306 val statusViewMarginHorizontal = resources.getDimensionPixelSize( 307 R.dimen.status_view_margin_horizontal) 308 constraintSet.apply { 309 setMargin(R.id.keyguard_status_view, START, statusViewMarginHorizontal) 310 setMargin(R.id.keyguard_status_view, END, statusViewMarginHorizontal) 311 } 312 } 313 314 private fun ensureAllViewsHaveIds(parentView: ViewGroup) { 315 for (i in 0 until parentView.childCount) { 316 val childView = parentView.getChildAt(i) 317 if (childView.id == View.NO_ID) { 318 childView.id = View.generateViewId() 319 } 320 } 321 } 322 } 323 324 private data class Paddings( 325 val containerPadding: Int, 326 val notificationsMargin: Int, 327 val qsContainerPadding: Int 328 ) 329 330 private fun KMutableProperty0<Int>.setAndReportChange(newValue: Int): Boolean { 331 val oldValue = get() 332 set(newValue) 333 return oldValue != newValue 334 } 335