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