• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2021 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.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.annotation.IdRes
22 import android.app.PendingIntent
23 import android.app.StatusBarManager
24 import android.content.Context
25 import android.content.Intent
26 import android.content.res.Configuration
27 import android.graphics.Insets
28 import android.os.Bundle
29 import android.os.Trace
30 import android.os.Trace.TRACE_TAG_APP
31 import android.provider.AlarmClock
32 import android.view.DisplayCutout
33 import android.view.View
34 import android.view.ViewGroup
35 import android.view.WindowInsets
36 import android.widget.TextView
37 import androidx.annotation.VisibleForTesting
38 import androidx.compose.foundation.layout.height
39 import androidx.compose.foundation.layout.wrapContentWidth
40 import androidx.compose.runtime.getValue
41 import androidx.compose.ui.Modifier
42 import androidx.compose.ui.platform.ComposeView
43 import androidx.compose.ui.unit.dp
44 import androidx.constraintlayout.motion.widget.MotionLayout
45 import androidx.core.view.doOnLayout
46 import androidx.core.view.isVisible
47 import androidx.lifecycle.compose.collectAsStateWithLifecycle
48 import com.android.app.animation.Interpolators
49 import com.android.settingslib.Utils
50 import com.android.systemui.Dumpable
51 import com.android.systemui.animation.ShadeInterpolation
52 import com.android.systemui.battery.BatteryMeterView
53 import com.android.systemui.battery.BatteryMeterView.MODE_ESTIMATE
54 import com.android.systemui.battery.BatteryMeterViewController
55 import com.android.systemui.dagger.SysUISingleton
56 import com.android.systemui.demomode.DemoMode
57 import com.android.systemui.demomode.DemoModeController
58 import com.android.systemui.dump.DumpManager
59 import com.android.systemui.plugins.ActivityStarter
60 import com.android.systemui.qs.ChipVisibilityListener
61 import com.android.systemui.qs.HeaderPrivacyIconsController
62 import com.android.systemui.res.R
63 import com.android.systemui.shade.ShadeHeaderController.Companion.HEADER_TRANSITION_ID
64 import com.android.systemui.shade.ShadeHeaderController.Companion.LARGE_SCREEN_HEADER_CONSTRAINT
65 import com.android.systemui.shade.ShadeHeaderController.Companion.LARGE_SCREEN_HEADER_TRANSITION_ID
66 import com.android.systemui.shade.ShadeHeaderController.Companion.QQS_HEADER_CONSTRAINT
67 import com.android.systemui.shade.ShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
68 import com.android.systemui.shade.ShadeViewProviderModule.Companion.SHADE_HEADER
69 import com.android.systemui.shade.carrier.ShadeCarrierGroup
70 import com.android.systemui.shade.carrier.ShadeCarrierGroupController
71 import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
72 import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
73 import com.android.systemui.statusbar.core.NewStatusBarIcons
74 import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
75 import com.android.systemui.statusbar.phone.StatusBarLocation
76 import com.android.systemui.statusbar.phone.StatusIconContainer
77 import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory
78 import com.android.systemui.statusbar.phone.ui.StatusBarIconController
79 import com.android.systemui.statusbar.phone.ui.TintedIconManager
80 import com.android.systemui.statusbar.pipeline.battery.ui.composable.BatteryWithEstimate
81 import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel
82 import com.android.systemui.statusbar.policy.Clock
83 import com.android.systemui.statusbar.policy.ConfigurationController
84 import com.android.systemui.statusbar.policy.NextAlarmController
85 import com.android.systemui.statusbar.policy.VariableDateView
86 import com.android.systemui.statusbar.policy.VariableDateViewController
87 import com.android.systemui.util.ViewController
88 import dagger.Lazy
89 import java.io.PrintWriter
90 import javax.inject.Inject
91 import javax.inject.Named
92 import kotlinx.coroutines.flow.MutableStateFlow
93 
94 /**
95  * Controller for QS header.
96  *
97  * [header] is a [MotionLayout] that has two transitions:
98  * * [HEADER_TRANSITION_ID]: [QQS_HEADER_CONSTRAINT] <-> [QS_HEADER_CONSTRAINT] for portrait
99  *   handheld device configuration.
100  * * [LARGE_SCREEN_HEADER_TRANSITION_ID]: [LARGE_SCREEN_HEADER_CONSTRAINT] for all other
101  *   configurations
102  */
103 @SysUISingleton
104 class ShadeHeaderController
105 @Inject
106 constructor(
107     @Named(SHADE_HEADER) private val header: MotionLayout,
108     private val statusBarIconController: StatusBarIconController,
109     private val tintedIconManagerFactory: TintedIconManager.Factory,
110     private val privacyIconsController: HeaderPrivacyIconsController,
111     private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore,
112     @ShadeDisplayAware private val configurationController: ConfigurationController,
113     @ShadeDisplayAware private val context: Context,
114     private val shadeDisplaysRepositoryLazy: Lazy<ShadeDisplaysRepository>,
115     private val variableDateViewControllerFactory: VariableDateViewController.Factory,
116     @Named(SHADE_HEADER) private val batteryMeterViewController: BatteryMeterViewController,
117     private val batteryViewModelFactory: BatteryViewModel.Factory,
118     private val dumpManager: DumpManager,
119     private val shadeCarrierGroupControllerBuilder: ShadeCarrierGroupController.Builder,
120     private val combinedShadeHeadersConstraintManager: CombinedShadeHeadersConstraintManager,
121     private val demoModeController: DemoModeController,
122     private val qsBatteryModeController: QsBatteryModeController,
123     private val nextAlarmController: NextAlarmController,
124     private val activityStarter: ActivityStarter,
125     private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
126 ) : ViewController<View>(header), Dumpable {
127 
128     private val statusBarContentInsetsProvider
129         get() =
130             statusBarContentInsetsProviderStore.forDisplay(
131                 if (ShadeWindowGoesAround.isEnabled) {
132                     // ShadeDisplaysRepository is the source of truth for display id when
133                     // ShadeWindowGoesAround.isEnabled
134                     shadeDisplaysRepositoryLazy.get().displayId.value
135                 } else {
136                     context.displayId
137                 }
138             )
139 
140     companion object {
141         /** IDs for transitions and constraints for the [MotionLayout]. */
142         @VisibleForTesting internal val HEADER_TRANSITION_ID = R.id.header_transition
143         @VisibleForTesting
144         internal val LARGE_SCREEN_HEADER_TRANSITION_ID = R.id.large_screen_header_transition
145         @VisibleForTesting internal val QQS_HEADER_CONSTRAINT = R.id.qqs_header_constraint
146         @VisibleForTesting internal val QS_HEADER_CONSTRAINT = R.id.qs_header_constraint
147         @VisibleForTesting
148         internal val LARGE_SCREEN_HEADER_CONSTRAINT = R.id.large_screen_header_constraint
149 
150         @VisibleForTesting internal val DEFAULT_CLOCK_INTENT = Intent(AlarmClock.ACTION_SHOW_ALARMS)
151 
152         private fun Int.stateToString() =
153             when (this) {
154                 QQS_HEADER_CONSTRAINT -> "QQS Header"
155                 QS_HEADER_CONSTRAINT -> "QS Header"
156                 LARGE_SCREEN_HEADER_CONSTRAINT -> "Large Screen Header"
157                 else -> "Unknown state $this"
158             }
159     }
160 
161     var shadeCollapseAction: Runnable? = null
162 
163     private lateinit var iconManager: TintedIconManager
164     private lateinit var carrierIconSlots: List<String>
165     private lateinit var mShadeCarrierGroupController: ShadeCarrierGroupController
166 
167     private val batteryIcon: BatteryMeterView = header.requireViewById(R.id.batteryRemainingIcon)
168     private val clock: Clock = header.requireViewById(R.id.clock)
169     private val date: TextView = header.requireViewById(R.id.date)
170     private val iconContainer: StatusIconContainer = header.requireViewById(R.id.statusIcons)
171     private val mShadeCarrierGroup: ShadeCarrierGroup = header.requireViewById(R.id.carrier_group)
172     private val systemIconsHoverContainer: View =
173         header.requireViewById(R.id.hover_system_icons_container)
174 
175     private var roundedCorners = 0
176     private var cutout: DisplayCutout? = null
177     private var lastInsets: WindowInsets? = null
178     private var nextAlarmIntent: PendingIntent? = null
179 
180     private val showBatteryEstimate = MutableStateFlow(false)
181 
182     private var qsDisabled = false
183     private var visible = false
184         set(value) {
185             if (field == value) {
186                 return
187             }
188             field = value
189             updateListeners()
190         }
191 
192     private var customizing = false
193         set(value) {
194             if (field != value) {
195                 field = value
196                 updateVisibility()
197             }
198         }
199 
200     /**
201      * Whether the QQS/QS part of the shade is visible. This is particularly important in
202      * Lockscreen, as the shade is visible but QS is not.
203      */
204     var qsVisible = false
205         set(value) {
206             if (field == value) {
207                 return
208             }
209             field = value
210             onShadeExpandedChanged()
211         }
212 
213     /**
214      * Whether we are in a configuration with large screen width. In this case, the header is a
215      * single line.
216      */
217     var largeScreenActive = false
218         set(value) {
219             if (field == value) {
220                 return
221             }
222             field = value
223             onHeaderStateChanged()
224         }
225 
226     /** Expansion fraction of the QQS/QS shade. This is not the expansion between QQS <-> QS. */
227     var shadeExpandedFraction = -1f
228         set(value) {
229             if (qsVisible && field != value) {
230                 header.alpha = ShadeInterpolation.getContentAlpha(value)
231                 field = value
232                 updateIgnoredSlots()
233             }
234         }
235 
236     /** Expansion fraction of the QQS <-> QS animation. */
237     var qsExpandedFraction = -1f
238         set(value) {
239             if (visible && field != value) {
240                 field = value
241                 iconContainer.setQsExpansionTransitioning(value > 0f && value < 1.0f)
242                 updatePosition()
243                 updateIgnoredSlots()
244             }
245         }
246 
247     /** Current scroll of QS. */
248     var qsScrollY = 0
249         set(value) {
250             if (field != value) {
251                 field = value
252                 updateScrollY()
253             }
254         }
255 
256     private val insetListener =
257         View.OnApplyWindowInsetsListener { view, insets ->
258             val windowInsets = WindowInsets(insets)
259             if (windowInsets != lastInsets) {
260                 updateConstraintsForInsets(view as MotionLayout, insets)
261                 lastInsets = windowInsets
262                 view.onApplyWindowInsets(insets)
263             } else {
264                 insets
265             }
266         }
267 
268     private var singleCarrier = false
269 
270     private val demoModeReceiver =
271         object : DemoMode {
272             override fun demoCommands() = listOf(DemoMode.COMMAND_CLOCK)
273 
274             override fun dispatchDemoCommand(command: String, args: Bundle) =
275                 clock.dispatchDemoCommand(command, args)
276 
277             override fun onDemoModeStarted() = clock.onDemoModeStarted()
278 
279             override fun onDemoModeFinished() = clock.onDemoModeFinished()
280         }
281 
282     private val chipVisibilityListener: ChipVisibilityListener =
283         object : ChipVisibilityListener {
284             override fun onChipVisibilityRefreshed(visible: Boolean) {
285                 // If the privacy chip is visible, we hide the status icons and battery remaining
286                 // icon, only in QQS.
287                 val update =
288                     combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(visible)
289                 header.updateAllConstraints(update)
290             }
291         }
292 
293     private val configurationControllerListener =
294         object : ConfigurationController.ConfigurationListener {
295             override fun onConfigChanged(newConfig: Configuration?) {
296                 val left =
297                     header.resources.getDimensionPixelSize(
298                         R.dimen.large_screen_shade_header_left_padding
299                     )
300                 header.setPadding(
301                     left,
302                     header.paddingTop,
303                     header.paddingRight,
304                     header.paddingBottom,
305                 )
306                 systemIconsHoverContainer.setPaddingRelative(
307                     resources.getDimensionPixelSize(
308                         R.dimen.hover_system_icons_container_padding_start
309                     ),
310                     resources.getDimensionPixelSize(
311                         R.dimen.hover_system_icons_container_padding_top
312                     ),
313                     resources.getDimensionPixelSize(
314                         R.dimen.hover_system_icons_container_padding_end
315                     ),
316                     resources.getDimensionPixelSize(
317                         R.dimen.hover_system_icons_container_padding_bottom
318                     ),
319                 )
320             }
321 
322             override fun onDensityOrFontScaleChanged() {
323                 clock.setTextAppearance(R.style.TextAppearance_QS_Status)
324                 date.setTextAppearance(R.style.TextAppearance_QS_Status)
325                 mShadeCarrierGroup.updateTextAppearance(R.style.TextAppearance_QS_Status)
326                 loadConstraints()
327                 header.minHeight =
328                     resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height)
329                 lastInsets?.let { updateConstraintsForInsets(header, it) }
330                 updateResources()
331                 updateCarrierGroupPadding()
332                 clock.onDensityOrFontScaleChanged()
333             }
334         }
335 
336     private val nextAlarmCallback =
337         NextAlarmController.NextAlarmChangeCallback { nextAlarm ->
338             nextAlarmIntent = nextAlarm?.showIntent
339         }
340 
341     override fun onInit() {
342         variableDateViewControllerFactory.create(date as VariableDateView).init()
343 
344         val fgColor =
345             Utils.getColorAttrDefaultColor(header.context, android.R.attr.textColorPrimary)
346         val bgColor =
347             Utils.getColorAttrDefaultColor(header.context, android.R.attr.textColorPrimaryInverse)
348 
349         iconManager = tintedIconManagerFactory.create(iconContainer, StatusBarLocation.QS)
350         iconManager.setTint(fgColor, bgColor)
351 
352         if (!NewStatusBarIcons.isEnabled) {
353             batteryMeterViewController.init()
354 
355             // battery settings same as in QS icons
356             batteryMeterViewController.ignoreTunerUpdates()
357 
358             batteryIcon.isVisible = true
359             batteryIcon.updateColors(
360                 fgColor /* foreground */,
361                 bgColor /* background */,
362                 fgColor, /* single tone (current default) */
363             )
364         } else {
365             // Configure the compose battery view
366             val batteryComposeView =
367                 ComposeView(mView.context).apply {
368                     setContent {
369                         id = R.id.battery_meter_composable_view
370                         val showBatteryEstimate by showBatteryEstimate.collectAsStateWithLifecycle()
371                         BatteryWithEstimate(
372                             modifier = Modifier.height(17.dp).wrapContentWidth(),
373                             viewModelFactory = batteryViewModelFactory,
374                             isDark = { true },
375                             showEstimate = showBatteryEstimate,
376                         )
377                     }
378                 }
379             mView.requireViewById<ViewGroup>(R.id.hover_system_icons_container).apply {
380                 addView(batteryComposeView, -1)
381             }
382         }
383 
384         carrierIconSlots =
385             listOf(header.context.getString(com.android.internal.R.string.status_bar_mobile))
386         mShadeCarrierGroupController =
387             shadeCarrierGroupControllerBuilder.setShadeCarrierGroup(mShadeCarrierGroup).build()
388 
389         privacyIconsController.onParentVisible()
390     }
391 
392     override fun onViewAttached() {
393         privacyIconsController.chipVisibilityListener = chipVisibilityListener
394         updateVisibility()
395         updateTransition()
396         updateCarrierGroupPadding()
397 
398         header.setOnApplyWindowInsetsListener(insetListener)
399 
400         clock.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
401             val newPivot = if (v.isLayoutRtl) v.width.toFloat() else 0f
402             v.pivotX = newPivot
403             v.pivotY = v.height.toFloat() / 2
404         }
405         clock.setOnClickListener { launchClockActivity() }
406 
407         dumpManager.registerDumpable(this)
408         configurationController.addCallback(configurationControllerListener)
409         demoModeController.addCallback(demoModeReceiver)
410         statusBarIconController.addIconGroup(iconManager)
411         nextAlarmController.addCallback(nextAlarmCallback)
412         systemIconsHoverContainer.setOnHoverListener(
413             statusOverlayHoverListenerFactory.createListener(systemIconsHoverContainer)
414         )
415     }
416 
417     override fun onViewDetached() {
418         clock.setOnClickListener(null)
419         privacyIconsController.chipVisibilityListener = null
420         dumpManager.unregisterDumpable(this::class.java.simpleName)
421         configurationController.removeCallback(configurationControllerListener)
422         demoModeController.removeCallback(demoModeReceiver)
423         statusBarIconController.removeIconGroup(iconManager)
424         nextAlarmController.removeCallback(nextAlarmCallback)
425         systemIconsHoverContainer.setOnHoverListener(null)
426     }
427 
428     fun disable(state1: Int, state2: Int, animate: Boolean) {
429         val disabled = state2 and StatusBarManager.DISABLE2_QUICK_SETTINGS != 0
430         if (disabled == qsDisabled) return
431         qsDisabled = disabled
432         updateVisibility()
433     }
434 
435     fun startCustomizingAnimation(show: Boolean, duration: Long) {
436         header
437             .animate()
438             .setDuration(duration)
439             .alpha(if (show) 0f else 1f)
440             .setInterpolator(if (show) Interpolators.ALPHA_OUT else Interpolators.ALPHA_IN)
441             .setListener(CustomizerAnimationListener(show))
442             .start()
443     }
444 
445     @VisibleForTesting
446     internal fun launchClockActivity() {
447         if (nextAlarmIntent != null) {
448             activityStarter.postStartActivityDismissingKeyguard(nextAlarmIntent)
449         } else {
450             activityStarter.postStartActivityDismissingKeyguard(DEFAULT_CLOCK_INTENT, 0 /*delay */)
451         }
452     }
453 
454     private fun loadConstraints() {
455         // Use resources.getXml instead of passing the resource id due to bug b/205018300
456         header
457             .getConstraintSet(QQS_HEADER_CONSTRAINT)
458             .load(context, resources.getXml(R.xml.qqs_header))
459         header
460             .getConstraintSet(QS_HEADER_CONSTRAINT)
461             .load(context, resources.getXml(R.xml.qs_header))
462         header
463             .getConstraintSet(LARGE_SCREEN_HEADER_CONSTRAINT)
464             .load(context, resources.getXml(R.xml.large_screen_shade_header))
465     }
466 
467     private fun updateCarrierGroupPadding() {
468         clock.doOnLayout {
469             val maxClockWidth =
470                 (clock.width * resources.getFloat(R.dimen.qqs_expand_clock_scale)).toInt()
471             mShadeCarrierGroup.setPaddingRelative(maxClockWidth, 0, 0, 0)
472         }
473     }
474 
475     private fun updateConstraintsForInsets(view: MotionLayout, insets: WindowInsets) {
476         val insetsProvider = statusBarContentInsetsProvider ?: return
477         val cutout = insets.displayCutout.also { this.cutout = it }
478 
479         val sbInsets: Insets = insetsProvider.getStatusBarContentInsetsForCurrentRotation()
480         val cutoutLeft = sbInsets.left
481         val cutoutRight = sbInsets.right
482         val hasCornerCutout: Boolean = insetsProvider.currentRotationHasCornerCutout()
483         updateQQSPaddings()
484         // Set these guides as the left/right limits for content that lives in the top row, using
485         // cutoutLeft and cutoutRight
486         var changes =
487             combinedShadeHeadersConstraintManager.edgesGuidelinesConstraints(
488                 if (view.isLayoutRtl) cutoutRight else cutoutLeft,
489                 header.paddingStart,
490                 if (view.isLayoutRtl) cutoutLeft else cutoutRight,
491                 header.paddingEnd,
492             )
493 
494         if (cutout != null) {
495             val topCutout = cutout.boundingRectTop
496             if (topCutout.isEmpty || hasCornerCutout) {
497                 changes += combinedShadeHeadersConstraintManager.emptyCutoutConstraints()
498             } else {
499                 changes +=
500                     combinedShadeHeadersConstraintManager.centerCutoutConstraints(
501                         view.isLayoutRtl,
502                         (view.width - view.paddingLeft - view.paddingRight - topCutout.width()) / 2,
503                     )
504             }
505         } else {
506             changes += combinedShadeHeadersConstraintManager.emptyCutoutConstraints()
507         }
508 
509         view.setPadding(view.paddingLeft, sbInsets.top, view.paddingRight, view.paddingBottom)
510         view.updateAllConstraints(changes)
511         updateBatteryMode()
512     }
513 
514     private fun updateBatteryMode() {
515         qsBatteryModeController.getBatteryMode(cutout, qsExpandedFraction)?.let {
516             if (NewStatusBarIcons.isEnabled) {
517                 showBatteryEstimate.value = it == MODE_ESTIMATE
518             } else {
519                 batteryIcon.setPercentShowMode(it)
520             }
521         }
522     }
523 
524     private fun updateScrollY() {
525         if (!largeScreenActive) {
526             header.scrollY = qsScrollY
527         }
528     }
529 
530     private fun onShadeExpandedChanged() {
531         if (qsVisible) {
532             privacyIconsController.startListening()
533         } else {
534             privacyIconsController.stopListening()
535         }
536         updateVisibility()
537         updatePosition()
538     }
539 
540     private fun onHeaderStateChanged() {
541         updateTransition()
542     }
543 
544     /**
545      * If not using [combinedHeaders] this should only be visible on large screen. Else, it should
546      * be visible any time the QQS/QS shade is open.
547      */
548     private fun updateVisibility() {
549         val visibility =
550             if (qsDisabled) {
551                 View.GONE
552             } else if (qsVisible && !customizing) {
553                 View.VISIBLE
554             } else {
555                 View.INVISIBLE
556             }
557         if (header.visibility != visibility) {
558             header.visibility = visibility
559             visible = visibility == View.VISIBLE
560         }
561     }
562 
563     private fun updateTransition() {
564         if (largeScreenActive) {
565             logInstantEvent("Large screen constraints set")
566             header.setTransition(LARGE_SCREEN_HEADER_TRANSITION_ID)
567             systemIconsHoverContainer.isClickable = true
568             systemIconsHoverContainer.setOnClickListener { shadeCollapseAction?.run() }
569         } else {
570             logInstantEvent("Small screen constraints set")
571             header.setTransition(HEADER_TRANSITION_ID)
572             systemIconsHoverContainer.setOnClickListener(null)
573             systemIconsHoverContainer.isClickable = false
574         }
575 
576         lastInsets?.let { updateConstraintsForInsets(header, it) }
577 
578         header.jumpToState(header.startState)
579         updatePosition()
580         updateScrollY()
581     }
582 
583     private fun updatePosition() {
584         if (!largeScreenActive && visible) {
585             logInstantEvent("updatePosition: $qsExpandedFraction")
586             header.progress = qsExpandedFraction
587             updateBatteryMode()
588         }
589     }
590 
591     private fun logInstantEvent(message: String) {
592         Trace.instantForTrack(TRACE_TAG_APP, "LargeScreenHeaderController", message)
593     }
594 
595     private fun updateListeners() {
596         mShadeCarrierGroupController.setListening(visible)
597         if (visible) {
598             singleCarrier = mShadeCarrierGroupController.isSingleCarrier
599             updateIgnoredSlots()
600             mShadeCarrierGroupController.setOnSingleCarrierChangedListener {
601                 singleCarrier = it
602                 updateIgnoredSlots()
603             }
604         } else {
605             mShadeCarrierGroupController.setOnSingleCarrierChangedListener(null)
606         }
607     }
608 
609     private fun updateIgnoredSlots() {
610         // switching from QQS to QS state halfway through the transition
611         if (singleCarrier || (!largeScreenActive && qsExpandedFraction < 0.5)) {
612             iconContainer.removeIgnoredSlots(carrierIconSlots)
613         } else {
614             iconContainer.addIgnoredSlots(carrierIconSlots)
615         }
616     }
617 
618     private fun updateResources() {
619         roundedCorners = resources.getDimensionPixelSize(R.dimen.rounded_corner_content_padding)
620         val padding = resources.getDimensionPixelSize(R.dimen.qs_panel_padding)
621         header.setPadding(padding, header.paddingTop, padding, header.paddingBottom)
622         updateQQSPaddings()
623         qsBatteryModeController.updateResources()
624     }
625 
626     private fun updateQQSPaddings() {
627         val clockPaddingStart =
628             resources.getDimensionPixelSize(R.dimen.status_bar_left_clock_starting_padding)
629         val clockPaddingEnd =
630             resources.getDimensionPixelSize(R.dimen.status_bar_left_clock_end_padding)
631         clock.setPaddingRelative(
632             clockPaddingStart,
633             clock.paddingTop,
634             clockPaddingEnd,
635             clock.paddingBottom,
636         )
637     }
638 
639     override fun dump(pw: PrintWriter, args: Array<out String>) {
640         pw.println("visible: $visible")
641         pw.println("shadeExpanded: $qsVisible")
642         pw.println("shadeExpandedFraction: $shadeExpandedFraction")
643         pw.println("active: $largeScreenActive")
644         pw.println("qsExpandedFraction: $qsExpandedFraction")
645         pw.println("qsScrollY: $qsScrollY")
646         pw.println("currentState: ${header.currentState.stateToString()}")
647     }
648 
649     private fun MotionLayout.updateConstraints(@IdRes state: Int, update: ConstraintChange) {
650         val constraints = getConstraintSet(state)
651         constraints.update()
652         updateState(state, constraints)
653     }
654 
655     /**
656      * Updates the [ConstraintSet] for the case of combined headers.
657      *
658      * Only non-`null` changes are applied to reduce the number of rebuilding in the [MotionLayout].
659      */
660     private fun MotionLayout.updateAllConstraints(updates: ConstraintsChanges) {
661         if (updates.qqsConstraintsChanges != null) {
662             updateConstraints(QQS_HEADER_CONSTRAINT, updates.qqsConstraintsChanges)
663         }
664         if (updates.qsConstraintsChanges != null) {
665             updateConstraints(QS_HEADER_CONSTRAINT, updates.qsConstraintsChanges)
666         }
667         if (updates.largeScreenConstraintsChanges != null) {
668             updateConstraints(LARGE_SCREEN_HEADER_CONSTRAINT, updates.largeScreenConstraintsChanges)
669         }
670     }
671 
672     @VisibleForTesting internal fun simulateViewDetached() = this.onViewDetached()
673 
674     inner class CustomizerAnimationListener(private val enteringCustomizing: Boolean) :
675         AnimatorListenerAdapter() {
676         override fun onAnimationEnd(animation: Animator) {
677             super.onAnimationEnd(animation)
678             header.animate().setListener(null)
679             if (enteringCustomizing) {
680                 customizing = true
681             }
682         }
683 
684         override fun onAnimationStart(animation: Animator) {
685             super.onAnimationStart(animation)
686             if (!enteringCustomizing) {
687                 customizing = false
688             }
689         }
690     }
691 }
692