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