• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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