• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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 
17 package com.android.systemui.statusbar.pipeline.shared.ui.binder
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.view.View
22 import androidx.core.view.isVisible
23 import androidx.lifecycle.Lifecycle
24 import androidx.lifecycle.lifecycleScope
25 import androidx.lifecycle.repeatOnLifecycle
26 import com.android.app.animation.Interpolators
27 import com.android.systemui.dagger.SysUISingleton
28 import com.android.systemui.lifecycle.repeatWhenAttached
29 import com.android.systemui.res.R
30 import com.android.systemui.scene.shared.flag.SceneContainerFlag
31 import com.android.systemui.statusbar.chips.mediaprojection.domain.model.MediaProjectionStopDialogModel
32 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
33 import com.android.systemui.statusbar.chips.ui.binder.OngoingActivityChipBinder
34 import com.android.systemui.statusbar.chips.ui.binder.OngoingActivityChipViewBinding
35 import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModelLegacy
36 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
37 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
38 import com.android.systemui.statusbar.core.StatusBarRootModernization
39 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState
40 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingIn
41 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingOut
42 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.RunningChipAnim
43 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ConnectedDisplaysStatusBarNotificationIconViewStore
44 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
45 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment
46 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
47 import com.android.systemui.statusbar.pipeline.shared.ui.model.VisibilityModel
48 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel
49 import javax.inject.Inject
50 import kotlinx.coroutines.flow.combine
51 import kotlinx.coroutines.flow.distinctUntilChanged
52 import kotlinx.coroutines.launch
53 
54 /**
55  * Interface to assist with binding the [CollapsedStatusBarFragment] to [HomeStatusBarViewModel].
56  * Used only to enable easy testing of [CollapsedStatusBarFragment].
57  */
58 interface HomeStatusBarViewBinder {
59     /**
60      * Binds the view to the view-model. [listener] will be notified whenever an event that may
61      * change the status bar visibility occurs.
62      *
63      * Null chip animations are used when [StatusBarRootModernization] is off (i.e., when we are
64      * binding from the fragment). If non-null, they control the animation of the system icon area
65      * to support the chip animations.
66      */
67     fun bind(
68         displayId: Int,
69         view: View,
70         viewModel: HomeStatusBarViewModel,
71         systemEventChipAnimateIn: ((View) -> Unit)?,
72         systemEventChipAnimateOut: ((View) -> Unit)?,
73         listener: StatusBarVisibilityChangeListener?,
74     )
75 }
76 
77 @SysUISingleton
78 class HomeStatusBarViewBinderImpl
79 @Inject
80 constructor(
81     private val viewStoreFactory: ConnectedDisplaysStatusBarNotificationIconViewStore.Factory
82 ) : HomeStatusBarViewBinder {
bindnull83     override fun bind(
84         displayId: Int,
85         view: View,
86         viewModel: HomeStatusBarViewModel,
87         systemEventChipAnimateIn: ((View) -> Unit)?,
88         systemEventChipAnimateOut: ((View) -> Unit)?,
89         listener: StatusBarVisibilityChangeListener?,
90     ) {
91         // Set some top-level views to gone before we get started
92         val primaryChipView: View = view.requireViewById(R.id.ongoing_activity_chip_primary)
93         val systemInfoView = view.requireViewById<View>(R.id.status_bar_end_side_content)
94         val clockView = view.requireViewById<View>(R.id.clock)
95         val notificationIconsArea = view.requireViewById<View>(R.id.notificationIcons)
96 
97         // CollapsedStatusBarFragment doesn't need this
98         if (StatusBarRootModernization.isEnabled) {
99             // GONE because this shouldn't take space in the layout
100             primaryChipView.hideInitially(state = View.GONE)
101             systemInfoView.hideInitially()
102             clockView.hideInitially()
103             notificationIconsArea.hideInitially()
104         }
105 
106         view.repeatWhenAttached {
107             repeatOnLifecycle(Lifecycle.State.CREATED) {
108                 val iconViewStore =
109                     if (StatusBarConnectedDisplays.isEnabled) {
110                         viewStoreFactory.create(displayId).also {
111                             lifecycleScope.launch { it.activate() }
112                         }
113                     } else {
114                         null
115                     }
116                 listener?.let { listener ->
117                     launch {
118                         viewModel.isTransitioningFromLockscreenToOccluded.collect {
119                             listener.onStatusBarVisibilityMaybeChanged()
120                         }
121                     }
122                 }
123 
124                 listener?.let { listener ->
125                     launch {
126                         viewModel.transitionFromLockscreenToDreamStartedEvent.collect {
127                             listener.onTransitionFromLockscreenToDreamStarted()
128                         }
129                     }
130                 }
131 
132                 if (NotificationsLiveDataStoreRefactor.isEnabled) {
133                     val lightsOutView: View = view.requireViewById(R.id.notification_lights_out)
134                     launch {
135                         viewModel.areNotificationsLightsOut.collect { show ->
136                             animateLightsOutView(lightsOutView, show)
137                         }
138                     }
139                 }
140 
141                 if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) {
142                     launch {
143                         viewModel.mediaProjectionStopDialogDueToCallEndedState.collect { stopDialog
144                             ->
145                             if (stopDialog is MediaProjectionStopDialogModel.Shown) {
146                                 stopDialog.createAndShowDialog()
147                             }
148                         }
149                     }
150                 }
151 
152                 if (!StatusBarNotifChips.isEnabled && !StatusBarChipsModernization.isEnabled) {
153                     val primaryChipViewBinding =
154                         OngoingActivityChipBinder.createBinding(primaryChipView)
155 
156                     launch {
157                         combine(
158                                 viewModel.primaryOngoingActivityChip,
159                                 viewModel.canShowOngoingActivityChips,
160                                 ::Pair,
161                             )
162                             .distinctUntilChanged()
163                             .collect { (primaryChipModel, areChipsAllowed) ->
164                                 OngoingActivityChipBinder.bind(
165                                     primaryChipModel,
166                                     primaryChipViewBinding,
167                                     iconViewStore,
168                                 )
169 
170                                 if (StatusBarRootModernization.isEnabled) {
171                                     bindLegacyPrimaryOngoingActivityChipWithVisibility(
172                                         areChipsAllowed,
173                                         primaryChipModel,
174                                         primaryChipViewBinding,
175                                     )
176                                 } else {
177                                     when (primaryChipModel) {
178                                         is OngoingActivityChipModel.Active ->
179                                             listener?.onOngoingActivityStatusChanged(
180                                                 hasPrimaryOngoingActivity = true,
181                                                 hasSecondaryOngoingActivity = false,
182                                                 shouldAnimate = true,
183                                             )
184 
185                                         is OngoingActivityChipModel.Inactive ->
186                                             listener?.onOngoingActivityStatusChanged(
187                                                 hasPrimaryOngoingActivity = false,
188                                                 hasSecondaryOngoingActivity = false,
189                                                 shouldAnimate = primaryChipModel.shouldAnimate,
190                                             )
191                                     }
192                                 }
193                             }
194                     }
195                 }
196 
197                 if (StatusBarNotifChips.isEnabled && !StatusBarChipsModernization.isEnabled) {
198                     // Create view bindings here so we don't keep re-fetching child views each time
199                     // the chip model changes.
200                     val primaryChipViewBinding =
201                         OngoingActivityChipBinder.createBinding(primaryChipView)
202                     val secondaryChipViewBinding =
203                         OngoingActivityChipBinder.createBinding(
204                             view.requireViewById(R.id.ongoing_activity_chip_secondary)
205                         )
206                     OngoingActivityChipBinder.updateTypefaces(primaryChipViewBinding)
207                     OngoingActivityChipBinder.updateTypefaces(secondaryChipViewBinding)
208                     launch {
209                         combine(
210                                 viewModel.ongoingActivityChipsLegacy,
211                                 viewModel.canShowOngoingActivityChips,
212                                 ::Pair,
213                             )
214                             .distinctUntilChanged()
215                             .collect { (chips, areChipsAllowed) ->
216                                 OngoingActivityChipBinder.bind(
217                                     chips.primary,
218                                     primaryChipViewBinding,
219                                     iconViewStore,
220                                 )
221                                 OngoingActivityChipBinder.bind(
222                                     chips.secondary,
223                                     secondaryChipViewBinding,
224                                     iconViewStore,
225                                 )
226                                 if (StatusBarRootModernization.isEnabled) {
227                                     bindOngoingActivityChipsWithVisibility(
228                                         areChipsAllowed,
229                                         chips,
230                                         primaryChipViewBinding,
231                                         secondaryChipViewBinding,
232                                     )
233                                 } else {
234                                     listener?.onOngoingActivityStatusChanged(
235                                         hasPrimaryOngoingActivity =
236                                             chips.primary is OngoingActivityChipModel.Active,
237                                         hasSecondaryOngoingActivity =
238                                             chips.secondary is OngoingActivityChipModel.Active,
239                                         // TODO(b/364653005): Figure out the animation story here.
240                                         shouldAnimate = true,
241                                     )
242                                 }
243                             }
244                     }
245                     launch {
246                         viewModel.contentArea.collect { _ ->
247                             OngoingActivityChipBinder.resetPrimaryChipWidthRestrictions(
248                                 primaryChipViewBinding,
249                                 viewModel.ongoingActivityChipsLegacy.value.primary,
250                             )
251                             OngoingActivityChipBinder.resetSecondaryChipWidthRestrictions(
252                                 secondaryChipViewBinding,
253                                 viewModel.ongoingActivityChipsLegacy.value.secondary,
254                             )
255                             view.requestLayout()
256                         }
257                     }
258                 }
259 
260                 if (SceneContainerFlag.isEnabled) {
261                     listener?.let { listener ->
262                         launch {
263                             viewModel.isHomeStatusBarAllowed.collect {
264                                 listener.onIsHomeStatusBarAllowedBySceneChanged(it)
265                             }
266                         }
267                     }
268                 }
269 
270                 if (StatusBarRootModernization.isEnabled) {
271                     // TODO(b/393445203): figure out the best story for this stub view. This crashes
272                     // if we move it up to the top of [bind]
273                     val operatorNameView = view.requireViewById<View>(R.id.operator_name_frame)
274                     operatorNameView.isVisible = false
275 
276                     StatusBarOperatorNameViewBinder.bind(
277                         operatorNameView,
278                         viewModel.operatorNameViewModel,
279                         viewModel.areaTint,
280                     )
281                     launch {
282                         viewModel.shouldShowOperatorNameView.collect {
283                             operatorNameView.isVisible = it
284                         }
285                     }
286 
287                     launch { viewModel.isClockVisible.collect { clockView.adjustVisibility(it) } }
288 
289                     launch {
290                         viewModel.isNotificationIconContainerVisible.collect {
291                             notificationIconsArea.adjustVisibility(it)
292                         }
293                     }
294 
295                     launch {
296                         viewModel.systemInfoCombinedVis.collect { (baseVis, animState) ->
297                             // Broadly speaking, the baseVis controls the view.visibility, and
298                             // the animation state uses only alpha to achieve its effect. This
299                             // means that we can always modify the visibility, and if we're
300                             // animating we can use the animState to handle it. If we are not
301                             // animating, then we can use the baseVis default animation
302                             if (animState.isAnimatingChip()) {
303                                 // Just apply the visibility of the view, but don't animate
304                                 systemInfoView.visibility = baseVis.visibility
305                                 // Now apply the animation state, with its animator
306                                 when (animState) {
307                                     AnimatingIn -> {
308                                         systemEventChipAnimateIn?.invoke(systemInfoView)
309                                     }
310                                     AnimatingOut -> {
311                                         systemEventChipAnimateOut?.invoke(systemInfoView)
312                                     }
313                                     else -> {
314                                         // Nothing to do here
315                                     }
316                                 }
317                             } else {
318                                 systemInfoView.adjustVisibility(baseVis)
319                             }
320                         }
321                     }
322                 }
323             }
324         }
325     }
326 
327     /** Bind the (legacy) single primary ongoing activity chip with the status bar visibility */
bindLegacyPrimaryOngoingActivityChipWithVisibilitynull328     private fun bindLegacyPrimaryOngoingActivityChipWithVisibility(
329         areChipsAllowed: Boolean,
330         primaryChipModel: OngoingActivityChipModel,
331         primaryChipViewBinding: OngoingActivityChipViewBinding,
332     ) {
333         if (!areChipsAllowed) {
334             primaryChipViewBinding.rootView.hide(shouldAnimateChange = false)
335         } else {
336             when (primaryChipModel) {
337                 is OngoingActivityChipModel.Active -> {
338                     primaryChipViewBinding.rootView.show(shouldAnimateChange = true)
339                 }
340 
341                 is OngoingActivityChipModel.Inactive -> {
342                     primaryChipViewBinding.rootView.hide(
343                         state = View.GONE,
344                         shouldAnimateChange = primaryChipModel.shouldAnimate,
345                     )
346                 }
347             }
348         }
349     }
350 
351     /** Bind the primary/secondary chips along with the home status bar's visibility */
bindOngoingActivityChipsWithVisibilitynull352     private fun bindOngoingActivityChipsWithVisibility(
353         areChipsAllowed: Boolean,
354         chips: MultipleOngoingActivityChipsModelLegacy,
355         primaryChipViewBinding: OngoingActivityChipViewBinding,
356         secondaryChipViewBinding: OngoingActivityChipViewBinding,
357     ) {
358         if (!areChipsAllowed) {
359             primaryChipViewBinding.rootView.hide(shouldAnimateChange = false)
360             secondaryChipViewBinding.rootView.hide(shouldAnimateChange = false)
361         } else {
362             primaryChipViewBinding.rootView.adjustVisibility(chips.primary.toVisibilityModel())
363             secondaryChipViewBinding.rootView.adjustVisibility(chips.secondary.toVisibilityModel())
364         }
365     }
366 
isAnimatingChipnull367     private fun SystemEventAnimationState.isAnimatingChip() =
368         when (this) {
369             AnimatingIn,
370             AnimatingOut,
371             RunningChipAnim -> true
372             else -> false
373         }
374 
OngoingActivityChipModelnull375     private fun OngoingActivityChipModel.toVisibilityModel(): VisibilityModel {
376         return VisibilityModel(
377             visibility = if (this is OngoingActivityChipModel.Active) View.VISIBLE else View.GONE,
378             // TODO(b/364653005): Figure out the animation story here.
379             shouldAnimateChange = true,
380         )
381     }
382 
animateLightsOutViewnull383     private fun animateLightsOutView(view: View, visible: Boolean) {
384         view.animate().cancel()
385 
386         val alpha = if (visible) 1f else 0f
387         val duration = if (visible) 750L else 250L
388         val visibility = if (visible) View.VISIBLE else View.GONE
389 
390         if (visible) {
391             view.alpha = 0f
392             view.visibility = View.VISIBLE
393         }
394 
395         view
396             .animate()
397             .alpha(alpha)
398             .setDuration(duration)
399             .setListener(
400                 object : AnimatorListenerAdapter() {
401                     override fun onAnimationEnd(animation: Animator) {
402                         view.alpha = alpha
403                         view.visibility = visibility
404                         // Unset the listener, otherwise this may persist for
405                         // another view property animation
406                         view.animate().setListener(null)
407                     }
408                 }
409             )
410             .start()
411     }
412 
adjustVisibilitynull413     private fun View.adjustVisibility(model: VisibilityModel) {
414         if (model.visibility == View.VISIBLE) {
415             this.show(model.shouldAnimateChange)
416         } else {
417             this.hide(model.visibility, model.shouldAnimateChange)
418         }
419     }
420 
421     /**
422      * Hide the view for initialization, but skip if it's already hidden and does not cancel
423      * animations.
424      */
Viewnull425     private fun View.hideInitially(state: Int = View.INVISIBLE) {
426         if (visibility == View.INVISIBLE || visibility == View.GONE) {
427             return
428         }
429         alpha = 0f
430         visibility = state
431     }
432 
433     // See CollapsedStatusBarFragment#hide.
hidenull434     private fun View.hide(state: Int = View.INVISIBLE, shouldAnimateChange: Boolean) {
435         animate().cancel()
436 
437         if (
438             (visibility == View.INVISIBLE && state == View.INVISIBLE) ||
439                 (visibility == View.GONE && state == View.GONE)
440         ) {
441             return
442         }
443         val isAlreadyHidden = visibility == View.INVISIBLE || visibility == View.GONE
444         if (!shouldAnimateChange || isAlreadyHidden) {
445             alpha = 0f
446             visibility = state
447             return
448         }
449 
450         animate()
451             .alpha(0f)
452             .setDuration(CollapsedStatusBarFragment.FADE_OUT_DURATION.toLong())
453             .setStartDelay(0)
454             .setInterpolator(Interpolators.ALPHA_OUT)
455             .withEndAction { visibility = state }
456     }
457 
458     // See CollapsedStatusBarFragment#show.
shownull459     private fun View.show(shouldAnimateChange: Boolean) {
460         animate().cancel()
461         if (visibility == View.VISIBLE && alpha >= 1f) {
462             return
463         }
464         visibility = View.VISIBLE
465         if (!shouldAnimateChange) {
466             alpha = 1f
467             return
468         }
469         animate()
470             .alpha(1f)
471             .setDuration(CollapsedStatusBarFragment.FADE_IN_DURATION.toLong())
472             .setInterpolator(Interpolators.ALPHA_IN)
473             .setStartDelay(CollapsedStatusBarFragment.FADE_IN_DELAY.toLong())
474             // We need to clean up any pending end action from animateHide if we call both hide and
475             // show in the same frame before the animation actually gets started.
476             // cancel() doesn't really remove the end action.
477             .withEndAction(null)
478 
479         // TODO(b/364360986): Synchronize the motion with the Keyguard fading if necessary.
480     }
481 }
482 
483 /** Listener for various events that may affect the status bar's visibility. */
484 interface StatusBarVisibilityChangeListener {
485     /**
486      * Called when the status bar visibility might have changed due to the device moving to a
487      * different state.
488      */
onStatusBarVisibilityMaybeChangednull489     fun onStatusBarVisibilityMaybeChanged()
490 
491     /** Called when a transition from lockscreen to dream has started. */
492     fun onTransitionFromLockscreenToDreamStarted()
493 
494     /**
495      * Called when the status of the ongoing activity chip (active or not active) has changed.
496      *
497      * @param shouldAnimate true if the chip should animate in/out, and false if the chip should
498      *   immediately appear/disappear.
499      */
500     fun onOngoingActivityStatusChanged(
501         hasPrimaryOngoingActivity: Boolean,
502         hasSecondaryOngoingActivity: Boolean,
503         shouldAnimate: Boolean,
504     )
505 
506     /**
507      * Called when the scene state has changed such that the home status bar is newly allowed or no
508      * longer allowed. See [HomeStatusBarViewModel.isHomeStatusBarAllowed].
509      */
510     fun onIsHomeStatusBarAllowedBySceneChanged(isHomeStatusBarAllowedByScene: Boolean)
511 }
512