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