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