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.view
18 
19 import android.content.res.ColorStateList
20 import android.testing.AndroidTestingRunner
21 import android.testing.TestableLooper
22 import android.testing.TestableLooper.RunWithLooper
23 import android.testing.ViewUtils
24 import android.view.View
25 import android.view.ViewGroup
26 import android.widget.ImageView
27 import androidx.test.filters.SmallTest
28 import com.android.systemui.SysuiTestCase
29 import com.android.systemui.log.table.logcatTableLogBuffer
30 import com.android.systemui.res.R
31 import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
32 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
33 import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
34 import com.android.systemui.statusbar.phone.StatusBarLocation
35 import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
36 import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
37 import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
38 import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl
39 import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
40 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
41 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
42 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
43 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
44 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
45 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
46 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
47 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
48 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel.Companion.viewModelForLocation
49 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
50 import com.android.systemui.testKosmos
51 import com.google.common.truth.Truth.assertThat
52 import kotlinx.coroutines.CoroutineScope
53 import kotlinx.coroutines.Dispatchers
54 import kotlinx.coroutines.flow.MutableStateFlow
55 import org.junit.Before
56 import org.junit.Test
57 import org.junit.runner.RunWith
58 import org.mockito.Mock
59 import org.mockito.MockitoAnnotations
60 
61 @SmallTest
62 @RunWith(AndroidTestingRunner::class)
63 @RunWithLooper(setAsMainLooper = true)
64 class ModernStatusBarWifiViewTest : SysuiTestCase() {
65     private val kosmos = testKosmos()
66 
67     private lateinit var testableLooper: TestableLooper
68 
69     private val tableLogBuffer = logcatTableLogBuffer(kosmos, "ModernStatusBarWifiViewTest")
70     @Mock private lateinit var connectivityConstants: ConnectivityConstants
71     @Mock private lateinit var wifiConstants: WifiConstants
72     private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
73     private lateinit var connectivityRepository: FakeConnectivityRepository
74     private lateinit var wifiRepository: FakeWifiRepository
75     private lateinit var interactor: WifiInteractor
76     private lateinit var viewModel: LocationBasedWifiViewModel
77     private lateinit var scope: CoroutineScope
78     private lateinit var airplaneModeViewModel: AirplaneModeViewModel
79 
80     @Before
setUpnull81     fun setUp() {
82         MockitoAnnotations.initMocks(this)
83         testableLooper = TestableLooper.get(this)
84 
85         airplaneModeRepository = FakeAirplaneModeRepository()
86         connectivityRepository = FakeConnectivityRepository()
87         wifiRepository = FakeWifiRepository()
88         wifiRepository.setIsWifiEnabled(true)
89         scope = CoroutineScope(Dispatchers.Unconfined)
90         interactor = WifiInteractorImpl(connectivityRepository, wifiRepository, scope)
91         airplaneModeViewModel =
92             AirplaneModeViewModelImpl(
93                 AirplaneModeInteractor(
94                     airplaneModeRepository,
95                     connectivityRepository,
96                     kosmos.fakeMobileConnectionsRepository,
97                 ),
98                 tableLogBuffer,
99                 scope,
100             )
101         val viewModelCommon =
102             WifiViewModel(
103                 airplaneModeViewModel,
104                 shouldShowSignalSpacerProvider = { MutableStateFlow(false) },
105                 connectivityConstants,
106                 context,
107                 tableLogBuffer,
108                 interactor,
109                 scope,
110                 wifiConstants,
111             )
112         viewModel =
113             viewModelForLocation(
114                 viewModelCommon,
115                 StatusBarLocation.HOME,
116             )
117     }
118 
119     // Note: The following tests are more like integration tests, since they stand up a full
120     // [WifiViewModel] and test the interactions between the view, view-binder, and view-model.
121 
122     @Test
setVisibleState_icon_iconShownDotHiddennull123     fun setVisibleState_icon_iconShownDotHidden() {
124         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
125 
126         view.setVisibleState(STATE_ICON, /* animate= */ false)
127 
128         ViewUtils.attachView(view)
129         testableLooper.processAllMessages()
130 
131         assertThat(view.getIconGroupView().visibility).isEqualTo(View.VISIBLE)
132         assertThat(view.getDotView().visibility).isEqualTo(View.GONE)
133 
134         ViewUtils.detachView(view)
135     }
136 
137     @Test
setVisibleState_dot_iconHiddenDotShownnull138     fun setVisibleState_dot_iconHiddenDotShown() {
139         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
140 
141         view.setVisibleState(STATE_DOT, /* animate= */ false)
142 
143         ViewUtils.attachView(view)
144         testableLooper.processAllMessages()
145 
146         assertThat(view.getIconGroupView().visibility).isEqualTo(View.INVISIBLE)
147         assertThat(view.getDotView().visibility).isEqualTo(View.VISIBLE)
148 
149         ViewUtils.detachView(view)
150     }
151 
152     @Test
setVisibleState_hidden_iconAndDotHiddennull153     fun setVisibleState_hidden_iconAndDotHidden() {
154         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
155 
156         view.setVisibleState(STATE_HIDDEN, /* animate= */ false)
157 
158         ViewUtils.attachView(view)
159         testableLooper.processAllMessages()
160 
161         assertThat(view.getIconGroupView().visibility).isEqualTo(View.INVISIBLE)
162         assertThat(view.getDotView().visibility).isEqualTo(View.INVISIBLE)
163 
164         ViewUtils.detachView(view)
165     }
166 
167     /* Regression test for b/296864006. When STATE_HIDDEN we need to ensure the wifi view width
168      * would not break the StatusIconContainer translation calculation. */
169     @Test
setVisibleState_hidden_keepWidthnull170     fun setVisibleState_hidden_keepWidth() {
171         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
172 
173         view.setVisibleState(STATE_ICON, /* animate= */ false)
174 
175         // get the view width when it's in visible state
176         ViewUtils.attachView(view)
177         val lp = view.layoutParams
178         lp.width = ViewGroup.LayoutParams.WRAP_CONTENT
179         lp.height = ViewGroup.LayoutParams.WRAP_CONTENT
180         view.layoutParams = lp
181         testableLooper.processAllMessages()
182         val currentWidth = view.width
183 
184         view.setVisibleState(STATE_HIDDEN, /* animate= */ false)
185         testableLooper.processAllMessages()
186 
187         // the view width when STATE_HIDDEN should be at least the width when STATE_ICON. Because
188         // when STATE_HIDDEN the invisible dot view width might be larger than group view width,
189         // then the wifi view width would be enlarged.
190         assertThat(view.width).isAtLeast(currentWidth)
191 
192         ViewUtils.detachView(view)
193     }
194 
195     @Test
isIconVisible_notEnabled_outputsFalsenull196     fun isIconVisible_notEnabled_outputsFalse() {
197         wifiRepository.setIsWifiEnabled(false)
198         wifiRepository.setWifiNetwork(WifiNetworkModel.Active.of(isValidated = true, level = 2))
199 
200         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
201 
202         ViewUtils.attachView(view)
203         testableLooper.processAllMessages()
204 
205         assertThat(view.isIconVisible).isFalse()
206 
207         ViewUtils.detachView(view)
208     }
209 
210     @Test
isIconVisible_enabled_outputsTruenull211     fun isIconVisible_enabled_outputsTrue() {
212         wifiRepository.setIsWifiEnabled(true)
213         wifiRepository.setWifiNetwork(WifiNetworkModel.Active.of(isValidated = true, level = 2))
214 
215         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
216 
217         ViewUtils.attachView(view)
218         testableLooper.processAllMessages()
219 
220         assertThat(view.isIconVisible).isTrue()
221 
222         ViewUtils.detachView(view)
223     }
224 
225     @Test
onDarkChanged_iconHasNewColornull226     fun onDarkChanged_iconHasNewColor() {
227         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
228         ViewUtils.attachView(view)
229         testableLooper.processAllMessages()
230 
231         val color = 0x12345678
232         val contrast = 0x12344321
233         view.onDarkChangedWithContrast(arrayListOf(), color, contrast)
234         testableLooper.processAllMessages()
235 
236         assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color))
237 
238         ViewUtils.detachView(view)
239     }
240 
241     @Test
setStaticDrawableColor_iconHasNewColornull242     fun setStaticDrawableColor_iconHasNewColor() {
243         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
244         ViewUtils.attachView(view)
245         testableLooper.processAllMessages()
246 
247         val color = 0x23456789
248         val contrast = 0x12344321
249         view.setStaticDrawableColor(color, contrast)
250         testableLooper.processAllMessages()
251 
252         assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color))
253 
254         ViewUtils.detachView(view)
255     }
256 
getIconGroupViewnull257     private fun View.getIconGroupView(): View {
258         return this.requireViewById(R.id.wifi_group)
259     }
260 
getIconViewnull261     private fun View.getIconView(): ImageView {
262         return this.requireViewById(R.id.wifi_signal)
263     }
264 
getDotViewnull265     private fun View.getDotView(): View {
266         return this.requireViewById(R.id.status_bar_dot)
267     }
268 }
269 
270 private const val SLOT_NAME = "TestSlotName"
271