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