• 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.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