• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2025 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.mobile.ui.binder
18 
19 import android.content.res.ColorStateList
20 import android.graphics.Color
21 import android.view.View
22 import android.view.ViewGroup
23 import android.widget.FrameLayout
24 import android.widget.ImageView
25 import android.widget.Space
26 import androidx.core.view.isVisible
27 import com.android.settingslib.graph.SignalDrawable
28 import com.android.systemui.Flags
29 import com.android.systemui.common.ui.binder.IconViewBinder
30 import com.android.systemui.kairos.BuildScope
31 import com.android.systemui.kairos.BuildSpec
32 import com.android.systemui.kairos.ExperimentalKairosApi
33 import com.android.systemui.kairos.KairosNetwork
34 import com.android.systemui.kairos.MutableState
35 import com.android.systemui.kairos.effect
36 import com.android.systemui.lifecycle.repeatWhenAttachedToWindow
37 import com.android.systemui.lifecycle.repeatWhenWindowIsVisible
38 import com.android.systemui.plugins.DarkIconDispatcher
39 import com.android.systemui.res.R
40 import com.android.systemui.statusbar.StatusBarIconView
41 import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
42 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
43 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModelKairos
44 import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
45 import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper
46 import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarViewBinderConstants
47 import kotlinx.coroutines.CoroutineScope
48 import kotlinx.coroutines.Job
49 import kotlinx.coroutines.awaitCancellation
50 import kotlinx.coroutines.launch
51 
52 object MobileIconBinderKairos {
53 
54     @ExperimentalKairosApi
55     fun bind(
56         view: ViewGroup,
57         viewModel: BuildSpec<LocationBasedMobileViewModelKairos>,
58         @StatusBarIconView.VisibleState
59         initialVisibilityState: Int = StatusBarIconView.STATE_HIDDEN,
60         logger: MobileViewLogger,
61         scope: CoroutineScope,
62         kairosNetwork: KairosNetwork,
63     ): Pair<ModernStatusBarViewBinding, Job> {
64         val binding = ModernStatusBarViewBindingKairosImpl(kairosNetwork, initialVisibilityState)
65         return binding to
66             scope.launch {
67                 view.repeatWhenAttachedToWindow {
68                     kairosNetwork.activateSpec {
69                         bind(
70                             view = view,
71                             viewModel = viewModel.applySpec(),
72                             logger = logger,
73                             binding = binding,
74                         )
75                     }
76                 }
77             }
78     }
79 
80     @ExperimentalKairosApi
81     private class ModernStatusBarViewBindingKairosImpl(
82         kairosNetwork: KairosNetwork,
83         initialVisibilityState: Int,
84     ) : ModernStatusBarViewBinding {
85 
86         @JvmField var shouldIconBeVisible: Boolean = false
87         @JvmField var isCollecting: Boolean = false
88 
89         // TODO(b/238425913): We should log this visibility state.
90         val visibility = MutableState(kairosNetwork, initialVisibilityState)
91         val iconTint =
92             MutableState(
93                 kairosNetwork,
94                 MobileIconColors(
95                     tint = DarkIconDispatcher.DEFAULT_ICON_TINT,
96                     contrast = DarkIconDispatcher.DEFAULT_INVERSE_ICON_TINT,
97                 ),
98             )
99         val decorTint = MutableState(kairosNetwork, Color.WHITE)
100 
101         override fun getShouldIconBeVisible(): Boolean = shouldIconBeVisible
102 
103         override fun onVisibilityStateChanged(state: Int) {
104             visibility.setValue(state)
105         }
106 
107         override fun onIconTintChanged(newTint: Int, contrastTint: Int) {
108             iconTint.setValue(MobileIconColors(tint = newTint, contrast = contrastTint))
109         }
110 
111         override fun onDecorTintChanged(newTint: Int) {
112             decorTint.setValue(newTint)
113         }
114 
115         override fun isCollecting(): Boolean = isCollecting
116     }
117 
118     @ExperimentalKairosApi
119     private fun BuildScope.bind(
120         view: ViewGroup,
121         viewModel: LocationBasedMobileViewModelKairos,
122         logger: MobileViewLogger,
123         binding: ModernStatusBarViewBindingKairosImpl,
124     ) {
125         viewModel.isVisible.observe { binding.shouldIconBeVisible = it }
126 
127         val mobileGroupView = view.requireViewById<ViewGroup>(R.id.mobile_group)
128         val activityContainer = view.requireViewById<View>(R.id.inout_container)
129         val activityIn = view.requireViewById<ImageView>(R.id.mobile_in)
130         val activityOut = view.requireViewById<ImageView>(R.id.mobile_out)
131         val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type)
132         val networkTypeContainer = view.requireViewById<FrameLayout>(R.id.mobile_type_container)
133         val iconView = view.requireViewById<ImageView>(R.id.mobile_signal)
134         val mobileDrawable = SignalDrawable(view.context)
135         val roamingView = view.requireViewById<ImageView>(R.id.mobile_roaming)
136         val roamingSpace = view.requireViewById<Space>(R.id.mobile_roaming_space)
137         val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot)
138 
139         effect {
140             view.isVisible = viewModel.isVisible.sample()
141             iconView.isVisible = true
142             launch {
143                 view.repeatWhenAttachedToWindow {
144                     // isVisible controls the visibility state of the outer group, and thus it needs
145                     // to run in the CREATED lifecycle so it can continue to watch while invisible
146                     // See (b/291031862) for details
147                     kairosNetwork.activateSpec {
148                         viewModel.isVisible.observe { isVisible ->
149                             viewModel.verboseLogger?.logBinderReceivedVisibility(
150                                 view,
151                                 viewModel.subscriptionId,
152                                 isVisible,
153                             )
154                             view.isVisible = isVisible
155                             // [StatusIconContainer] can get out of sync sometimes. Make sure to
156                             // request another layout when this changes.
157                             view.requestLayout()
158                         }
159                     }
160                 }
161             }
162             launch {
163                 view.repeatWhenWindowIsVisible {
164                     logger.logCollectionStarted(view, viewModel)
165                     binding.isCollecting = true
166                     kairosNetwork.activateSpec {
167                         binding.visibility.observe { state ->
168                             ModernStatusBarViewVisibilityHelper.setVisibilityState(
169                                 state,
170                                 mobileGroupView,
171                                 dotView,
172                             )
173                             view.requestLayout()
174                         }
175 
176                         // Set the icon for the triangle
177                         viewModel.icon.observe { icon ->
178                             viewModel.verboseLogger?.logBinderReceivedSignalIcon(
179                                 view,
180                                 viewModel.subscriptionId,
181                                 icon,
182                             )
183                             if (icon is SignalIconModel.Cellular) {
184                                 iconView.setImageDrawable(mobileDrawable)
185                                 mobileDrawable.level = icon.toSignalDrawableState()
186                             } else if (icon is SignalIconModel.Satellite) {
187                                 IconViewBinder.bind(icon.icon, iconView)
188                             }
189                         }
190 
191                         viewModel.contentDescription.observe {
192                             MobileContentDescriptionViewBinder.bind(it, view)
193                         }
194 
195                         // Set the network type icon
196                         viewModel.networkTypeIcon.observe { dataTypeId ->
197                             viewModel.verboseLogger?.logBinderReceivedNetworkTypeIcon(
198                                 view,
199                                 viewModel.subscriptionId,
200                                 dataTypeId,
201                             )
202                             dataTypeId?.let { IconViewBinder.bind(dataTypeId, networkTypeView) }
203                             val prevVis = networkTypeContainer.visibility
204                             networkTypeContainer.visibility =
205                                 if (dataTypeId != null) View.VISIBLE else View.GONE
206 
207                             if (prevVis != networkTypeContainer.visibility) {
208                                 view.requestLayout()
209                             }
210                         }
211 
212                         // Set the network type background
213                         viewModel.networkTypeBackground.observe { background ->
214                             networkTypeContainer.setBackgroundResource(background?.res ?: 0)
215 
216                             // Tint will invert when this bit changes
217                             if (background?.res != null) {
218                                 networkTypeContainer.backgroundTintList =
219                                     ColorStateList.valueOf(binding.iconTint.sample().tint)
220                                 networkTypeView.imageTintList =
221                                     ColorStateList.valueOf(binding.iconTint.sample().contrast)
222                             } else {
223                                 networkTypeView.imageTintList =
224                                     ColorStateList.valueOf(binding.iconTint.sample().tint)
225                             }
226                         }
227 
228                         // Set the roaming indicator
229                         viewModel.roaming.observe { isRoaming ->
230                             roamingView.isVisible = isRoaming
231                             roamingSpace.isVisible = isRoaming
232                         }
233 
234                         if (Flags.statusBarStaticInoutIndicators()) {
235                             // Set the opacity of the activity indicators
236                             viewModel.activityInVisible.observe { visible ->
237                                 activityIn.imageAlpha =
238                                     (if (visible) StatusBarViewBinderConstants.ALPHA_ACTIVE
239                                     else StatusBarViewBinderConstants.ALPHA_INACTIVE)
240                             }
241                             viewModel.activityOutVisible.observe { visible ->
242                                 activityOut.imageAlpha =
243                                     (if (visible) StatusBarViewBinderConstants.ALPHA_ACTIVE
244                                     else StatusBarViewBinderConstants.ALPHA_INACTIVE)
245                             }
246                         } else {
247                             // Set the activity indicators
248                             viewModel.activityInVisible.observe { activityIn.isVisible = it }
249                             viewModel.activityOutVisible.observe { activityOut.isVisible = it }
250                         }
251 
252                         viewModel.activityContainerVisible.observe {
253                             activityContainer.isVisible = it
254                         }
255 
256                         // Set the tint
257                         binding.iconTint.observe { colors ->
258                             val tint = ColorStateList.valueOf(colors.tint)
259                             val contrast = ColorStateList.valueOf(colors.contrast)
260 
261                             iconView.imageTintList = tint
262 
263                             // If the bg is visible, tint it and use the contrast for the fg
264                             if (viewModel.networkTypeBackground.sample() != null) {
265                                 networkTypeContainer.backgroundTintList = tint
266                                 networkTypeView.imageTintList = contrast
267                             } else {
268                                 networkTypeView.imageTintList = tint
269                             }
270 
271                             roamingView.imageTintList = tint
272                             activityIn.imageTintList = tint
273                             activityOut.imageTintList = tint
274                             dotView.setDecorColor(colors.tint)
275                         }
276 
277                         binding.decorTint.observe { tint -> dotView.setDecorColor(tint) }
278                     }
279 
280                     try {
281                         awaitCancellation()
282                     } finally {
283                         binding.isCollecting = false
284                         logger.logCollectionStopped(view, viewModel)
285                     }
286                 }
287             }
288         }
289     }
290 }
291