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