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.wifi.ui.binder
18 
19 import android.content.res.ColorStateList
20 import android.view.View
21 import android.view.ViewGroup
22 import android.widget.ImageView
23 import androidx.core.view.isVisible
24 import androidx.lifecycle.Lifecycle
25 import androidx.lifecycle.repeatOnLifecycle
26 import com.android.systemui.R
27 import com.android.systemui.common.ui.binder.IconViewBinder
28 import com.android.systemui.lifecycle.repeatWhenAttached
29 import com.android.systemui.statusbar.StatusBarIconView
30 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
31 import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
32 import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper
33 import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
34 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
35 import kotlinx.coroutines.InternalCoroutinesApi
36 import kotlinx.coroutines.awaitCancellation
37 import kotlinx.coroutines.flow.MutableStateFlow
38 import kotlinx.coroutines.flow.distinctUntilChanged
39 import kotlinx.coroutines.launch
40 
41 /**
42  * Binds a wifi icon in the status bar to its view-model.
43  *
44  * To use this properly, users should maintain a one-to-one relationship between the [View] and the
45  * view-binding, binding each view only once. It is okay and expected for the same instance of the
46  * view-model to be reused for multiple view/view-binder bindings.
47  */
48 @OptIn(InternalCoroutinesApi::class)
49 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
50 object WifiViewBinder {
51 
52     /** Binds the view to the view-model, continuing to update the former based on the latter. */
53     @JvmStatic
54     fun bind(
55         view: ViewGroup,
56         viewModel: LocationBasedWifiViewModel,
57     ): ModernStatusBarViewBinding {
58         val groupView = view.requireViewById<ViewGroup>(R.id.wifi_group)
59         val iconView = view.requireViewById<ImageView>(R.id.wifi_signal)
60         val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot)
61         val activityInView = view.requireViewById<ImageView>(R.id.wifi_in)
62         val activityOutView = view.requireViewById<ImageView>(R.id.wifi_out)
63         val activityContainerView = view.requireViewById<View>(R.id.inout_container)
64         val airplaneSpacer = view.requireViewById<View>(R.id.wifi_airplane_spacer)
65         val signalSpacer = view.requireViewById<View>(R.id.wifi_signal_spacer)
66 
67         view.isVisible = true
68         iconView.isVisible = true
69 
70         // TODO(b/238425913): We should log this visibility state.
71         @StatusBarIconView.VisibleState
72         val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN)
73 
74         val iconTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor)
75         val decorTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor)
76 
77         var isCollecting: Boolean = false
78 
79         view.repeatWhenAttached {
80             repeatOnLifecycle(Lifecycle.State.STARTED) {
81                 isCollecting = true
82 
83                 launch {
84                     visibilityState.collect { visibilityState ->
85                         // for b/296864006, we can not hide all the child views if visibilityState
86                         // is STATE_HIDDEN. Because hiding all child views would cause the
87                         // getWidth() of this view return 0, and that would cause the translation
88                         // calculation fails in StatusIconContainer. Therefore, like class
89                         // MobileIconBinder, instead of set the child views visibility to View.GONE,
90                         // we set their visibility to View.INVISIBLE to make them invisible but
91                         // keep the width.
92                         ModernStatusBarViewVisibilityHelper.setVisibilityState(
93                             visibilityState,
94                             groupView,
95                             dotView,
96                         )
97                     }
98                 }
99 
100                 launch {
101                     viewModel.wifiIcon.collect { wifiIcon ->
102                         view.isVisible = wifiIcon is WifiIcon.Visible
103                         if (wifiIcon is WifiIcon.Visible) {
104                             IconViewBinder.bind(wifiIcon.icon, iconView)
105                         }
106                     }
107                 }
108 
109                 launch {
110                     iconTint.collect { tint ->
111                         val tintList = ColorStateList.valueOf(tint)
112                         iconView.imageTintList = tintList
113                         activityInView.imageTintList = tintList
114                         activityOutView.imageTintList = tintList
115                         dotView.setDecorColor(tint)
116                     }
117                 }
118 
119                 launch { decorTint.collect { tint -> dotView.setDecorColor(tint) } }
120 
121                 launch {
122                     viewModel.isActivityInViewVisible.distinctUntilChanged().collect { visible ->
123                         activityInView.isVisible = visible
124                     }
125                 }
126 
127                 launch {
128                     viewModel.isActivityOutViewVisible.distinctUntilChanged().collect { visible ->
129                         activityOutView.isVisible = visible
130                     }
131                 }
132 
133                 launch {
134                     viewModel.isActivityContainerVisible.distinctUntilChanged().collect { visible ->
135                         activityContainerView.isVisible = visible
136                     }
137                 }
138 
139                 launch {
140                     viewModel.isAirplaneSpacerVisible.distinctUntilChanged().collect { visible ->
141                         airplaneSpacer.isVisible = visible
142                     }
143                 }
144 
145                 launch {
146                     viewModel.isSignalSpacerVisible.distinctUntilChanged().collect { visible ->
147                         signalSpacer.isVisible = visible
148                     }
149                 }
150 
151                 try {
152                     awaitCancellation()
153                 } finally {
154                     isCollecting = false
155                 }
156             }
157         }
158 
159         return object : ModernStatusBarViewBinding {
160             override fun getShouldIconBeVisible(): Boolean {
161                 return viewModel.wifiIcon.value is WifiIcon.Visible
162             }
163 
164             override fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) {
165                 visibilityState.value = state
166             }
167 
168             override fun onIconTintChanged(newTint: Int) {
169                 iconTint.value = newTint
170             }
171 
172             override fun onDecorTintChanged(newTint: Int) {
173                 decorTint.value = newTint
174             }
175 
176             override fun isCollecting(): Boolean {
177                 return isCollecting
178             }
179         }
180     }
181 }
182