1 /*
2  * Copyright (C) 2023 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 package com.android.systemui.statusbar.notification.icon.ui.viewbinder
17 
18 import android.content.Context
19 import android.graphics.Color
20 import android.graphics.Rect
21 import android.os.Bundle
22 import android.os.Trace
23 import android.view.LayoutInflater
24 import android.view.View
25 import android.widget.FrameLayout
26 import androidx.annotation.ColorInt
27 import androidx.annotation.VisibleForTesting
28 import androidx.collection.ArrayMap
29 import com.android.app.animation.Interpolators
30 import com.android.internal.statusbar.StatusBarIcon
31 import com.android.internal.util.ContrastColorUtil
32 import com.android.settingslib.Utils
33 import com.android.systemui.R
34 import com.android.systemui.dagger.SysUISingleton
35 import com.android.systemui.demomode.DemoMode
36 import com.android.systemui.demomode.DemoModeController
37 import com.android.systemui.flags.FeatureFlags
38 import com.android.systemui.flags.Flags
39 import com.android.systemui.flags.ViewRefactorFlag
40 import com.android.systemui.plugins.DarkIconDispatcher
41 import com.android.systemui.plugins.statusbar.StatusBarStateController
42 import com.android.systemui.statusbar.CrossFadeHelper
43 import com.android.systemui.statusbar.NotificationListener
44 import com.android.systemui.statusbar.NotificationMediaManager
45 import com.android.systemui.statusbar.NotificationShelfController
46 import com.android.systemui.statusbar.StatusBarIconView
47 import com.android.systemui.statusbar.StatusBarState
48 import com.android.systemui.statusbar.notification.NotificationUtils
49 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
50 import com.android.systemui.statusbar.notification.collection.ListEntry
51 import com.android.systemui.statusbar.notification.collection.NotificationEntry
52 import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
53 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
54 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
55 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
56 import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl
57 import com.android.systemui.statusbar.phone.DozeParameters
58 import com.android.systemui.statusbar.phone.KeyguardBypassController
59 import com.android.systemui.statusbar.phone.NotificationIconAreaController
60 import com.android.systemui.statusbar.phone.NotificationIconContainer
61 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
62 import com.android.systemui.statusbar.window.StatusBarWindowController
63 import com.android.wm.shell.bubbles.Bubbles
64 import java.util.Optional
65 import java.util.function.Function
66 import javax.inject.Inject
67 import kotlinx.coroutines.DisposableHandle
68 
69 /**
70  * Controller class for [NotificationIconContainer]. This implementation serves as a temporary
71  * wrapper around [NotificationIconContainerViewBinder], so that external code can continue to
72  * depend on the [NotificationIconAreaController] interface. Once
73  * [LegacyNotificationIconAreaControllerImpl] is removed, this class can go away and the ViewBinder
74  * can be used directly.
75  */
76 @SysUISingleton
77 class NotificationIconAreaControllerViewBinderWrapperImpl
78 @Inject
79 constructor(
80     private val context: Context,
81     private val statusBarStateController: StatusBarStateController,
82     private val wakeUpCoordinator: NotificationWakeUpCoordinator,
83     private val bypassController: KeyguardBypassController,
84     private val mediaManager: NotificationMediaManager,
85     notificationListener: NotificationListener,
86     private val dozeParameters: DozeParameters,
87     private val sectionStyleProvider: SectionStyleProvider,
88     private val bubblesOptional: Optional<Bubbles>,
89     demoModeController: DemoModeController,
90     darkIconDispatcher: DarkIconDispatcher,
91     featureFlags: FeatureFlags,
92     private val statusBarWindowController: StatusBarWindowController,
93     private val screenOffAnimationController: ScreenOffAnimationController,
94     private val shelfIconsViewModel: NotificationIconContainerShelfViewModel,
95     private val statusBarIconsViewModel: NotificationIconContainerStatusBarViewModel,
96     private val aodIconsViewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
97 ) :
98     NotificationIconAreaController,
99     DarkIconDispatcher.DarkReceiver,
100     StatusBarStateController.StateListener,
101     NotificationWakeUpCoordinator.WakeUpListener,
102     DemoMode {
103 
104     private val contrastColorUtil: ContrastColorUtil = ContrastColorUtil.getInstance(context)
105     private val updateStatusBarIcons = Runnable { updateStatusBarIcons() }
106     private val shelfRefactor = ViewRefactorFlag(featureFlags, Flags.NOTIFICATION_SHELF_REFACTOR)
107     private val tintAreas = ArrayList<Rect>()
108 
109     private var iconSize = 0
110     private var iconHPadding = 0
111     private var iconTint = Color.WHITE
112     private var notificationEntries = listOf<ListEntry>()
113     private var notificationIconArea: View? = null
114     private var notificationIcons: NotificationIconContainer? = null
115     private var shelfIcons: NotificationIconContainer? = null
116     private var aodIcons: NotificationIconContainer? = null
117     private var aodBindJob: DisposableHandle? = null
118     private var aodIconAppearTranslation = 0
119     private var animationsEnabled = false
120     private var aodIconTint = 0
121     private var aodIconsVisible = false
122     private var showLowPriority = true
123 
124     @VisibleForTesting
125     val settingsListener: NotificationListener.NotificationSettingsListener =
126         object : NotificationListener.NotificationSettingsListener {
127             override fun onStatusBarIconsBehaviorChanged(hideSilentStatusIcons: Boolean) {
128                 showLowPriority = !hideSilentStatusIcons
129                 updateStatusBarIcons()
130             }
131         }
132 
133     init {
134         statusBarStateController.addCallback(this)
135         wakeUpCoordinator.addListener(this)
136         demoModeController.addCallback(this)
137         notificationListener.addNotificationSettingsListener(settingsListener)
138         initializeNotificationAreaViews(context)
139         reloadAodColor()
140         darkIconDispatcher.addDarkReceiver(this)
141     }
142 
143     @VisibleForTesting
144     fun shouldShowLowPriorityIcons(): Boolean {
145         return showLowPriority
146     }
147 
148     /** Called by the Keyguard*ViewController whose view contains the aod icons. */
149     override fun setupAodIcons(aodIcons: NotificationIconContainer) {
150         val changed = this.aodIcons != null && aodIcons !== this.aodIcons
151         if (changed) {
152             this.aodIcons!!.setAnimationsEnabled(false)
153             this.aodIcons!!.removeAllViews()
154             aodBindJob?.dispose()
155         }
156         this.aodIcons = aodIcons
157         this.aodIcons!!.setOnLockScreen(true)
158         aodBindJob = NotificationIconContainerViewBinder.bind(aodIcons, aodIconsViewModel)
159         updateAodIconsVisibility(animate = false, forceUpdate = changed)
160         updateAnimations()
161         if (changed) {
162             updateAodNotificationIcons()
163         }
164         updateIconLayoutParams(context)
165     }
166 
167     override fun setupShelf(notificationShelfController: NotificationShelfController) =
168         NotificationShelfViewBinderWrapperControllerImpl.unsupported
169 
170     override fun setShelfIcons(icons: NotificationIconContainer) {
171         if (shelfRefactor.expectEnabled()) {
172             NotificationIconContainerViewBinder.bind(icons, shelfIconsViewModel)
173             shelfIcons = icons
174         }
175     }
176 
177     override fun onDensityOrFontScaleChanged(context: Context) {
178         updateIconLayoutParams(context)
179     }
180 
181     /** Returns the view that represents the notification area. */
182     override fun getNotificationInnerAreaView(): View? {
183         return notificationIconArea
184     }
185 
186     /**
187      * See [com.android.systemui.statusbar.policy.DarkIconDispatcher.setIconsDarkArea]. Sets the
188      * color that should be used to tint any icons in the notification area.
189      *
190      * @param tintAreas the areas in which to tint the icons, specified in screen coordinates
191      * @param darkIntensity
192      */
193     override fun onDarkChanged(tintAreas: ArrayList<Rect>, darkIntensity: Float, iconTint: Int) {
194         this.tintAreas.clear()
195         this.tintAreas.addAll(tintAreas)
196         if (DarkIconDispatcher.isInAreas(tintAreas, notificationIconArea)) {
197             this.iconTint = iconTint
198         }
199         applyNotificationIconsTint()
200     }
201 
202     /** Updates the notifications with the given list of notifications to display. */
203     override fun updateNotificationIcons(entries: List<ListEntry>) {
204         notificationEntries = entries
205         updateNotificationIcons()
206     }
207 
208     private fun updateStatusBarIcons() {
209         updateIconsForLayout(
210             { entry: NotificationEntry -> entry.icons.statusBarIcon },
211             notificationIcons,
212             showAmbient = false /* showAmbient */,
213             showLowPriority = showLowPriority,
214             hideDismissed = true /* hideDismissed */,
215             hideRepliedMessages = true /* hideRepliedMessages */,
216             hideCurrentMedia = false /* hideCurrentMedia */,
217             hidePulsing = false /* hidePulsing */
218         )
219     }
220 
221     override fun updateAodNotificationIcons() {
222         if (aodIcons == null) {
223             return
224         }
225         updateIconsForLayout(
226             { entry: NotificationEntry -> entry.icons.aodIcon },
227             aodIcons,
228             showAmbient = false /* showAmbient */,
229             showLowPriority = true /* showLowPriority */,
230             hideDismissed = true /* hideDismissed */,
231             hideRepliedMessages = true /* hideRepliedMessages */,
232             hideCurrentMedia = true /* hideCurrentMedia */,
233             hidePulsing = bypassController.bypassEnabled /* hidePulsing */
234         )
235     }
236 
237     override fun showIconIsolated(icon: StatusBarIconView?, animated: Boolean) {
238         notificationIcons!!.showIconIsolated(icon, animated)
239     }
240 
241     override fun setIsolatedIconLocation(iconDrawingRect: Rect, requireStateUpdate: Boolean) {
242         notificationIcons!!.setIsolatedIconLocation(iconDrawingRect, requireStateUpdate)
243     }
244 
245     override fun onDozingChanged(isDozing: Boolean) {
246         if (aodIcons == null) {
247             return
248         }
249         val animate = (dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking)
250         aodIcons!!.setDozing(isDozing, animate, 0)
251     }
252 
253     override fun setAnimationsEnabled(enabled: Boolean) {
254         animationsEnabled = enabled
255         updateAnimations()
256     }
257 
258     override fun onStateChanged(newState: Int) {
259         updateAodIconsVisibility(animate = false, forceUpdate = false)
260         updateAnimations()
261     }
262 
263     override fun onThemeChanged() {
264         reloadAodColor()
265         updateAodIconColors()
266     }
267 
268     override fun getHeight(): Int {
269         return if (aodIcons == null) 0 else aodIcons!!.height
270     }
271 
272     @VisibleForTesting
273     fun appearAodIcons() {
274         if (aodIcons == null) {
275             return
276         }
277         if (screenOffAnimationController.shouldAnimateAodIcons()) {
278             aodIcons!!.translationY = -aodIconAppearTranslation.toFloat()
279             aodIcons!!.alpha = 0f
280             animateInAodIconTranslation()
281             aodIcons!!
282                 .animate()
283                 .alpha(1f)
284                 .setInterpolator(Interpolators.LINEAR)
285                 .setDuration(AOD_ICONS_APPEAR_DURATION)
286                 .start()
287         } else {
288             aodIcons!!.alpha = 1.0f
289             aodIcons!!.translationY = 0f
290         }
291     }
292 
293     override fun onFullyHiddenChanged(isFullyHidden: Boolean) {
294         var animate = true
295         if (!bypassController.bypassEnabled) {
296             animate = dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking
297             // We only want the appear animations to happen when the notifications get fully hidden,
298             // since otherwise the unhide animation overlaps
299             animate = animate and isFullyHidden
300         }
301         updateAodIconsVisibility(animate, false /* force */)
302         updateAodNotificationIcons()
303         updateAodIconColors()
304     }
305 
306     override fun onPulseExpansionChanged(expandingChanged: Boolean) {
307         if (expandingChanged) {
308             updateAodIconsVisibility(animate = true, forceUpdate = false)
309         }
310     }
311 
312     override fun demoCommands(): List<String> {
313         val commands = ArrayList<String>()
314         commands.add(DemoMode.COMMAND_NOTIFICATIONS)
315         return commands
316     }
317 
318     override fun dispatchDemoCommand(command: String, args: Bundle) {
319         if (notificationIconArea != null) {
320             val visible = args.getString("visible")
321             val vis = if ("false" == visible) View.INVISIBLE else View.VISIBLE
322             notificationIconArea?.visibility = vis
323         }
324     }
325 
326     override fun onDemoModeFinished() {
327         if (notificationIconArea != null) {
328             notificationIconArea?.visibility = View.VISIBLE
329         }
330     }
331 
332     private fun inflateIconArea(inflater: LayoutInflater): View {
333         return inflater.inflate(R.layout.notification_icon_area, null)
334     }
335 
336     /** Initializes the views that will represent the notification area. */
337     private fun initializeNotificationAreaViews(context: Context) {
338         reloadDimens(context)
339         val layoutInflater = LayoutInflater.from(context)
340         notificationIconArea = inflateIconArea(layoutInflater)
341         notificationIcons = notificationIconArea?.findViewById(R.id.notificationIcons)
342         NotificationIconContainerViewBinder.bind(notificationIcons!!, statusBarIconsViewModel)
343     }
344 
345     private fun updateIconLayoutParams(context: Context) {
346         reloadDimens(context)
347         val params = generateIconLayoutParams()
348         for (i in 0 until notificationIcons!!.childCount) {
349             val child = notificationIcons!!.getChildAt(i)
350             child.layoutParams = params
351         }
352         if (shelfIcons != null) {
353             for (i in 0 until shelfIcons!!.childCount) {
354                 val child = shelfIcons!!.getChildAt(i)
355                 child.layoutParams = params
356             }
357         }
358         if (aodIcons != null) {
359             for (i in 0 until aodIcons!!.childCount) {
360                 val child = aodIcons!!.getChildAt(i)
361                 child.layoutParams = params
362             }
363         }
364     }
365 
366     private fun generateIconLayoutParams(): FrameLayout.LayoutParams {
367         return FrameLayout.LayoutParams(
368             iconSize + 2 * iconHPadding,
369             statusBarWindowController.statusBarHeight
370         )
371     }
372 
373     private fun reloadDimens(context: Context) {
374         val res = context.resources
375         iconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size_sp)
376         iconHPadding = res.getDimensionPixelSize(R.dimen.status_bar_icon_horizontal_margin)
377         aodIconAppearTranslation = res.getDimensionPixelSize(R.dimen.shelf_appear_translation)
378     }
379 
380     private fun shouldShowNotificationIcon(
381         entry: NotificationEntry,
382         showAmbient: Boolean,
383         showLowPriority: Boolean,
384         hideDismissed: Boolean,
385         hideRepliedMessages: Boolean,
386         hideCurrentMedia: Boolean,
387         hidePulsing: Boolean
388     ): Boolean {
389         if (!showAmbient && sectionStyleProvider.isMinimized(entry)) {
390             return false
391         }
392         if (hideCurrentMedia && entry.key == mediaManager.mediaNotificationKey) {
393             return false
394         }
395         if (!showLowPriority && sectionStyleProvider.isSilent(entry)) {
396             return false
397         }
398         if (entry.isRowDismissed && hideDismissed) {
399             return false
400         }
401         if (hideRepliedMessages && entry.isLastMessageFromReply) {
402             return false
403         }
404         // showAmbient == show in shade but not shelf
405         if (!showAmbient && entry.shouldSuppressStatusBar()) {
406             return false
407         }
408         if (
409             hidePulsing &&
410                 entry.showingPulsing() &&
411                 (!wakeUpCoordinator.notificationsFullyHidden || !entry.isPulseSuppressed)
412         ) {
413             return false
414         }
415         return if (bubblesOptional.isPresent && bubblesOptional.get().isBubbleExpanded(entry.key)) {
416             false
417         } else true
418     }
419 
420     private fun updateNotificationIcons() {
421         Trace.beginSection("NotificationIconAreaController.updateNotificationIcons")
422         updateStatusBarIcons()
423         updateShelfIcons()
424         updateAodNotificationIcons()
425         applyNotificationIconsTint()
426         Trace.endSection()
427     }
428 
429     private fun updateShelfIcons() {
430         if (shelfIcons == null) {
431             return
432         }
433         updateIconsForLayout(
434             { entry: NotificationEntry -> entry.icons.shelfIcon },
435             shelfIcons,
436             showAmbient = true,
437             showLowPriority = true,
438             hideDismissed = false,
439             hideRepliedMessages = false,
440             hideCurrentMedia = false,
441             hidePulsing = false
442         )
443     }
444 
445     /**
446      * Updates the notification icons for a host layout. This will ensure that the notification host
447      * layout will have the same icons like the ones in here.
448      *
449      * @param function A function to look up an icon view based on an entry
450      * @param hostLayout which layout should be updated
451      * @param showAmbient should ambient notification icons be shown
452      * @param showLowPriority should icons from silent notifications be shown
453      * @param hideDismissed should dismissed icons be hidden
454      * @param hideRepliedMessages should messages that have been replied to be hidden
455      * @param hidePulsing should pulsing notifications be hidden
456      */
457     private fun updateIconsForLayout(
458         function: Function<NotificationEntry, StatusBarIconView?>,
459         hostLayout: NotificationIconContainer?,
460         showAmbient: Boolean,
461         showLowPriority: Boolean,
462         hideDismissed: Boolean,
463         hideRepliedMessages: Boolean,
464         hideCurrentMedia: Boolean,
465         hidePulsing: Boolean,
466     ) {
467         val toShow = ArrayList<StatusBarIconView>(notificationEntries.size)
468         // Filter out ambient notifications and notification children.
469         for (i in notificationEntries.indices) {
470             val entry = notificationEntries[i].representativeEntry
471             if (entry != null && entry.row != null) {
472                 if (
473                     shouldShowNotificationIcon(
474                         entry,
475                         showAmbient,
476                         showLowPriority,
477                         hideDismissed,
478                         hideRepliedMessages,
479                         hideCurrentMedia,
480                         hidePulsing
481                     )
482                 ) {
483                     val iconView = function.apply(entry)
484                     if (iconView != null) {
485                         toShow.add(iconView)
486                     }
487                 }
488             }
489         }
490 
491         // In case we are changing the suppression of a group, the replacement shouldn't flicker
492         // and it should just be replaced instead. We therefore look for notifications that were
493         // just replaced by the child or vice-versa to suppress this.
494         val replacingIcons = ArrayMap<String, ArrayList<StatusBarIcon>>()
495         val toRemove = ArrayList<View>()
496         for (i in 0 until hostLayout!!.childCount) {
497             val child = hostLayout.getChildAt(i) as? StatusBarIconView ?: continue
498             if (!toShow.contains(child)) {
499                 var iconWasReplaced = false
500                 val removedGroupKey = child.notification.groupKey
501                 for (j in toShow.indices) {
502                     val candidate = toShow[j]
503                     if (
504                         candidate.sourceIcon.sameAs(child.sourceIcon) &&
505                             candidate.notification.groupKey == removedGroupKey
506                     ) {
507                         if (!iconWasReplaced) {
508                             iconWasReplaced = true
509                         } else {
510                             iconWasReplaced = false
511                             break
512                         }
513                     }
514                 }
515                 if (iconWasReplaced) {
516                     var statusBarIcons = replacingIcons[removedGroupKey]
517                     if (statusBarIcons == null) {
518                         statusBarIcons = ArrayList()
519                         replacingIcons[removedGroupKey] = statusBarIcons
520                     }
521                     statusBarIcons.add(child.statusBarIcon)
522                 }
523                 toRemove.add(child)
524             }
525         }
526         // removing all duplicates
527         val duplicates = ArrayList<String?>()
528         for (key in replacingIcons.keys) {
529             val statusBarIcons = replacingIcons[key]!!
530             if (statusBarIcons.size != 1) {
531                 duplicates.add(key)
532             }
533         }
534         replacingIcons.removeAll(duplicates)
535         hostLayout.setReplacingIcons(replacingIcons)
536         val toRemoveCount = toRemove.size
537         for (i in 0 until toRemoveCount) {
538             hostLayout.removeView(toRemove[i])
539         }
540         val params = generateIconLayoutParams()
541         for (i in toShow.indices) {
542             val v = toShow[i]
543             // The view might still be transiently added if it was just removed and added again
544             hostLayout.removeTransientView(v)
545             if (v.parent == null) {
546                 if (hideDismissed) {
547                     v.setOnDismissListener(updateStatusBarIcons)
548                 }
549                 hostLayout.addView(v, i, params)
550             }
551         }
552         hostLayout.setChangingViewPositions(true)
553         // Re-sort notification icons
554         val childCount = hostLayout.childCount
555         for (i in 0 until childCount) {
556             val actual = hostLayout.getChildAt(i)
557             val expected = toShow[i]
558             if (actual === expected) {
559                 continue
560             }
561             hostLayout.removeView(expected)
562             hostLayout.addView(expected, i)
563         }
564         hostLayout.setChangingViewPositions(false)
565         hostLayout.setReplacingIcons(null)
566     }
567 
568     /** Applies [.mIconTint] to the notification icons. */
569     private fun applyNotificationIconsTint() {
570         for (i in 0 until notificationIcons!!.childCount) {
571             val iv = notificationIcons!!.getChildAt(i) as StatusBarIconView
572             if (iv.width != 0) {
573                 updateTintForIcon(iv, iconTint)
574             } else {
575                 iv.executeOnLayout { updateTintForIcon(iv, iconTint) }
576             }
577         }
578         updateAodIconColors()
579     }
580 
581     private fun updateTintForIcon(v: StatusBarIconView, tint: Int) {
582         val isPreL = java.lang.Boolean.TRUE == v.getTag(R.id.icon_is_pre_L)
583         var color = StatusBarIconView.NO_COLOR
584         val colorize = !isPreL || NotificationUtils.isGrayscale(v, contrastColorUtil)
585         if (colorize) {
586             color = DarkIconDispatcher.getTint(tintAreas, v, tint)
587         }
588         v.staticDrawableColor = color
589         v.setDecorColor(tint)
590     }
591 
592     private fun updateAnimations() {
593         val inShade = statusBarStateController.state == StatusBarState.SHADE
594         if (aodIcons != null) {
595             aodIcons!!.setAnimationsEnabled(animationsEnabled && !inShade)
596         }
597         notificationIcons!!.setAnimationsEnabled(animationsEnabled && inShade)
598     }
599 
600     private fun animateInAodIconTranslation() {
601         aodIcons!!
602             .animate()
603             .setInterpolator(Interpolators.DECELERATE_QUINT)
604             .translationY(0f)
605             .setDuration(AOD_ICONS_APPEAR_DURATION)
606             .start()
607     }
608 
609     private fun reloadAodColor() {
610         aodIconTint =
611             Utils.getColorAttrDefaultColor(
612                 context,
613                 R.attr.wallpaperTextColor,
614                 DEFAULT_AOD_ICON_COLOR
615             )
616     }
617 
618     private fun updateAodIconColors() {
619         if (aodIcons != null) {
620             for (i in 0 until aodIcons!!.childCount) {
621                 val iv = aodIcons!!.getChildAt(i) as StatusBarIconView
622                 if (iv.width != 0) {
623                     updateTintForIcon(iv, aodIconTint)
624                 } else {
625                     iv.executeOnLayout { updateTintForIcon(iv, aodIconTint) }
626                 }
627             }
628         }
629     }
630 
631     private fun updateAodIconsVisibility(animate: Boolean, forceUpdate: Boolean) {
632         if (aodIcons == null) {
633             return
634         }
635         var visible = (bypassController.bypassEnabled || wakeUpCoordinator.notificationsFullyHidden)
636 
637         // Hide the AOD icons if we're not in the KEYGUARD state unless the screen off animation is
638         // playing, in which case we want them to be visible since we're animating in the AOD UI and
639         // will be switching to KEYGUARD shortly.
640         if (
641             statusBarStateController.state != StatusBarState.KEYGUARD &&
642                 !screenOffAnimationController.shouldShowAodIconsWhenShade()
643         ) {
644             visible = false
645         }
646         if (visible && wakeUpCoordinator.isPulseExpanding() && !bypassController.bypassEnabled) {
647             visible = false
648         }
649         if (aodIconsVisible != visible || forceUpdate) {
650             aodIconsVisible = visible
651             aodIcons!!.animate().cancel()
652             if (animate) {
653                 val wasFullyInvisible = aodIcons!!.visibility != View.VISIBLE
654                 if (aodIconsVisible) {
655                     if (wasFullyInvisible) {
656                         // No fading here, let's just appear the icons instead!
657                         aodIcons!!.visibility = View.VISIBLE
658                         aodIcons!!.alpha = 1.0f
659                         appearAodIcons()
660                     } else {
661                         // Let's make sure the icon are translated to 0, since we cancelled it above
662                         animateInAodIconTranslation()
663                         // We were fading out, let's fade in instead
664                         CrossFadeHelper.fadeIn(aodIcons)
665                     }
666                 } else {
667                     // Let's make sure the icon are translated to 0, since we cancelled it above
668                     animateInAodIconTranslation()
669                     CrossFadeHelper.fadeOut(aodIcons)
670                 }
671             } else {
672                 aodIcons!!.alpha = 1.0f
673                 aodIcons!!.translationY = 0f
674                 aodIcons!!.visibility = if (visible) View.VISIBLE else View.INVISIBLE
675             }
676         }
677     }
678 
679     companion object {
680         private const val AOD_ICONS_APPEAR_DURATION: Long = 200
681 
682         @ColorInt private val DEFAULT_AOD_ICON_COLOR = -0x1
683     }
684 }
685