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 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
setAndReportChangenull330 private fun KMutableProperty0<Int>.setAndReportChange(newValue: Int): Boolean {
331 val oldValue = get()
332 set(newValue)
333 return oldValue != newValue
334 }
335