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