1 /*
2  * Copyright (C) 2022 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.view.View
21 import android.view.View.GONE
22 import android.view.View.VISIBLE
23 import android.view.ViewGroup
24 import android.widget.ImageView
25 import android.widget.Space
26 import androidx.core.view.isVisible
27 import androidx.lifecycle.Lifecycle
28 import androidx.lifecycle.lifecycleScope
29 import androidx.lifecycle.repeatOnLifecycle
30 import com.android.settingslib.graph.SignalDrawable
31 import com.android.systemui.R
32 import com.android.systemui.common.ui.binder.ContentDescriptionViewBinder
33 import com.android.systemui.common.ui.binder.IconViewBinder
34 import com.android.systemui.lifecycle.repeatWhenAttached
35 import com.android.systemui.statusbar.StatusBarIconView
36 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
37 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
38 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
39 import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
40 import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper
41 import kotlinx.coroutines.awaitCancellation
42 import kotlinx.coroutines.flow.MutableStateFlow
43 import kotlinx.coroutines.flow.distinctUntilChanged
44 import kotlinx.coroutines.launch
45 
46 object MobileIconBinder {
47     /** Binds the view to the view-model, continuing to update the former based on the latter */
48     @JvmStatic
49     fun bind(
50         view: ViewGroup,
51         viewModel: LocationBasedMobileViewModel,
52         @StatusBarIconView.VisibleState initialVisibilityState: Int = STATE_HIDDEN,
53         logger: MobileViewLogger,
54     ): ModernStatusBarViewBinding {
55         val mobileGroupView = view.requireViewById<ViewGroup>(R.id.mobile_group)
56         val activityContainer = view.requireViewById<View>(R.id.inout_container)
57         val activityIn = view.requireViewById<ImageView>(R.id.mobile_in)
58         val activityOut = view.requireViewById<ImageView>(R.id.mobile_out)
59         val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type)
60         val iconView = view.requireViewById<ImageView>(R.id.mobile_signal)
61         val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) }
62         val roamingView = view.requireViewById<ImageView>(R.id.mobile_roaming)
63         val roamingSpace = view.requireViewById<Space>(R.id.mobile_roaming_space)
64         val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot)
65 
66         view.isVisible = viewModel.isVisible.value
67         iconView.isVisible = true
68 
69         // TODO(b/238425913): We should log this visibility state.
70         @StatusBarIconView.VisibleState
71         val visibilityState: MutableStateFlow<Int> = MutableStateFlow(initialVisibilityState)
72 
73         val iconTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor)
74         val decorTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor)
75 
76         var isCollecting = false
77 
78         view.repeatWhenAttached {
79             lifecycleScope.launch {
80                 repeatOnLifecycle(Lifecycle.State.CREATED) {
81                     // isVisible controls the visibility state of the outer group, and thus it needs
82                     // to run in the CREATED lifecycle so it can continue to watch while invisible
83                     // See (b/291031862) for details
84                     launch {
85                         viewModel.isVisible.collect { isVisible ->
86                             viewModel.verboseLogger?.logBinderReceivedVisibility(
87                                 view,
88                                 viewModel.subscriptionId,
89                                 isVisible
90                             )
91                             view.isVisible = isVisible
92                             // [StatusIconContainer] can get out of sync sometimes. Make sure to
93                             // request another layout when this changes.
94                             view.requestLayout()
95                         }
96                     }
97                 }
98             }
99 
100             lifecycleScope.launch {
101                 repeatOnLifecycle(Lifecycle.State.STARTED) {
102                     logger.logCollectionStarted(view, viewModel)
103                     isCollecting = true
104 
105                     launch {
106                         visibilityState.collect { state ->
107                             ModernStatusBarViewVisibilityHelper.setVisibilityState(
108                                 state,
109                                 mobileGroupView,
110                                 dotView,
111                             )
112                         }
113                     }
114 
115                     // Set the icon for the triangle
116                     launch {
117                         viewModel.icon.distinctUntilChanged().collect { icon ->
118                             viewModel.verboseLogger?.logBinderReceivedSignalIcon(
119                                 view,
120                                 viewModel.subscriptionId,
121                                 icon,
122                             )
123                             mobileDrawable.level = icon.toSignalDrawableState()
124                         }
125                     }
126 
127                     launch {
128                         viewModel.contentDescription.distinctUntilChanged().collect {
129                             ContentDescriptionViewBinder.bind(it, view)
130                         }
131                     }
132 
133                     // Set the network type icon
134                     launch {
135                         viewModel.networkTypeIcon.distinctUntilChanged().collect { dataTypeId ->
136                             viewModel.verboseLogger?.logBinderReceivedNetworkTypeIcon(
137                                 view,
138                                 viewModel.subscriptionId,
139                                 dataTypeId,
140                             )
141                             dataTypeId?.let { IconViewBinder.bind(dataTypeId, networkTypeView) }
142                             networkTypeView.visibility = if (dataTypeId != null) VISIBLE else GONE
143                         }
144                     }
145 
146                     // Set the roaming indicator
147                     launch {
148                         viewModel.roaming.distinctUntilChanged().collect { isRoaming ->
149                             roamingView.isVisible = isRoaming
150                             roamingSpace.isVisible = isRoaming
151                         }
152                     }
153 
154                     // Set the activity indicators
155                     launch { viewModel.activityInVisible.collect { activityIn.isVisible = it } }
156 
157                     launch { viewModel.activityOutVisible.collect { activityOut.isVisible = it } }
158 
159                     launch {
160                         viewModel.activityContainerVisible.collect {
161                             activityContainer.isVisible = it
162                         }
163                     }
164 
165                     // Set the tint
166                     launch {
167                         iconTint.collect { tint ->
168                             val tintList = ColorStateList.valueOf(tint)
169                             iconView.imageTintList = tintList
170                             networkTypeView.imageTintList = tintList
171                             roamingView.imageTintList = tintList
172                             activityIn.imageTintList = tintList
173                             activityOut.imageTintList = tintList
174                             dotView.setDecorColor(tint)
175                         }
176                     }
177 
178                     launch { decorTint.collect { tint -> dotView.setDecorColor(tint) } }
179 
180                     try {
181                         awaitCancellation()
182                     } finally {
183                         isCollecting = false
184                         logger.logCollectionStopped(view, viewModel)
185                     }
186                 }
187             }
188         }
189 
190         return object : ModernStatusBarViewBinding {
191             override fun getShouldIconBeVisible(): Boolean {
192                 return viewModel.isVisible.value
193             }
194 
195             override fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) {
196                 visibilityState.value = state
197             }
198 
199             override fun onIconTintChanged(newTint: Int) {
200                 iconTint.value = newTint
201             }
202 
203             override fun onDecorTintChanged(newTint: Int) {
204                 decorTint.value = newTint
205             }
206 
207             override fun isCollecting(): Boolean {
208                 return isCollecting
209             }
210         }
211     }
212 }
213