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.TableLogBuffer
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.google.common.truth.Truth.assertThat
51 import kotlinx.coroutines.CoroutineScope
52 import kotlinx.coroutines.Dispatchers
53 import kotlinx.coroutines.flow.MutableStateFlow
54 import org.junit.Before
55 import org.junit.Test
56 import org.junit.runner.RunWith
57 import org.mockito.Mock
58 import org.mockito.MockitoAnnotations
59 
60 @SmallTest
61 @RunWith(AndroidTestingRunner::class)
62 @RunWithLooper(setAsMainLooper = true)
63 class ModernStatusBarWifiViewTest : SysuiTestCase() {
64 
65     private lateinit var testableLooper: TestableLooper
66 
67     @Mock private lateinit var tableLogBuffer: TableLogBuffer
68     @Mock private lateinit var connectivityConstants: ConnectivityConstants
69     @Mock private lateinit var wifiConstants: WifiConstants
70     private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
71     private lateinit var connectivityRepository: FakeConnectivityRepository
72     private lateinit var wifiRepository: FakeWifiRepository
73     private lateinit var interactor: WifiInteractor
74     private lateinit var viewModel: LocationBasedWifiViewModel
75     private lateinit var scope: CoroutineScope
76     private lateinit var airplaneModeViewModel: AirplaneModeViewModel
77 
78     @Before
setUpnull79     fun setUp() {
80         MockitoAnnotations.initMocks(this)
81         testableLooper = TestableLooper.get(this)
82 
83         airplaneModeRepository = FakeAirplaneModeRepository()
84         connectivityRepository = FakeConnectivityRepository()
85         wifiRepository = FakeWifiRepository()
86         wifiRepository.setIsWifiEnabled(true)
87         scope = CoroutineScope(Dispatchers.Unconfined)
88         interactor = WifiInteractorImpl(connectivityRepository, wifiRepository, scope)
89         airplaneModeViewModel =
90             AirplaneModeViewModelImpl(
91                 AirplaneModeInteractor(
92                     airplaneModeRepository,
93                     connectivityRepository,
94                     FakeMobileConnectionsRepository(),
95                 ),
96                 tableLogBuffer,
97                 scope,
98             )
99         val viewModelCommon =
100             WifiViewModel(
101                 airplaneModeViewModel,
102                 shouldShowSignalSpacerProvider = { MutableStateFlow(false) },
103                 connectivityConstants,
104                 context,
105                 tableLogBuffer,
106                 interactor,
107                 scope,
108                 wifiConstants,
109             )
110         viewModel =
111             viewModelForLocation(
112                 viewModelCommon,
113                 StatusBarLocation.HOME,
114             )
115     }
116 
117     // Note: The following tests are more like integration tests, since they stand up a full
118     // [WifiViewModel] and test the interactions between the view, view-binder, and view-model.
119 
120     @Test
setVisibleState_icon_iconShownDotHiddennull121     fun setVisibleState_icon_iconShownDotHidden() {
122         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
123 
124         view.setVisibleState(STATE_ICON, /* animate= */ false)
125 
126         ViewUtils.attachView(view)
127         testableLooper.processAllMessages()
128 
129         assertThat(view.getIconGroupView().visibility).isEqualTo(View.VISIBLE)
130         assertThat(view.getDotView().visibility).isEqualTo(View.GONE)
131 
132         ViewUtils.detachView(view)
133     }
134 
135     @Test
setVisibleState_dot_iconHiddenDotShownnull136     fun setVisibleState_dot_iconHiddenDotShown() {
137         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
138 
139         view.setVisibleState(STATE_DOT, /* animate= */ false)
140 
141         ViewUtils.attachView(view)
142         testableLooper.processAllMessages()
143 
144         assertThat(view.getIconGroupView().visibility).isEqualTo(View.INVISIBLE)
145         assertThat(view.getDotView().visibility).isEqualTo(View.VISIBLE)
146 
147         ViewUtils.detachView(view)
148     }
149 
150     @Test
setVisibleState_hidden_iconAndDotHiddennull151     fun setVisibleState_hidden_iconAndDotHidden() {
152         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
153 
154         view.setVisibleState(STATE_HIDDEN, /* animate= */ false)
155 
156         ViewUtils.attachView(view)
157         testableLooper.processAllMessages()
158 
159         assertThat(view.getIconGroupView().visibility).isEqualTo(View.INVISIBLE)
160         assertThat(view.getDotView().visibility).isEqualTo(View.INVISIBLE)
161 
162         ViewUtils.detachView(view)
163     }
164 
165     /* Regression test for b/296864006. When STATE_HIDDEN we need to ensure the wifi view width
166      * would not break the StatusIconContainer translation calculation. */
167     @Test
setVisibleState_hidden_keepWidthnull168     fun setVisibleState_hidden_keepWidth() {
169         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
170 
171         view.setVisibleState(STATE_ICON, /* animate= */ false)
172 
173         // get the view width when it's in visible state
174         ViewUtils.attachView(view)
175         val lp = view.layoutParams
176         lp.width = ViewGroup.LayoutParams.WRAP_CONTENT
177         lp.height = ViewGroup.LayoutParams.WRAP_CONTENT
178         view.layoutParams = lp
179         testableLooper.processAllMessages()
180         val currentWidth = view.width
181 
182         view.setVisibleState(STATE_HIDDEN, /* animate= */ false)
183         testableLooper.processAllMessages()
184 
185         // the view width when STATE_HIDDEN should be at least the width when STATE_ICON. Because
186         // when STATE_HIDDEN the invisible dot view width might be larger than group view width,
187         // then the wifi view width would be enlarged.
188         assertThat(view.width).isAtLeast(currentWidth)
189 
190         ViewUtils.detachView(view)
191     }
192 
193     @Test
isIconVisible_notEnabled_outputsFalsenull194     fun isIconVisible_notEnabled_outputsFalse() {
195         wifiRepository.setIsWifiEnabled(false)
196         wifiRepository.setWifiNetwork(
197             WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 2)
198         )
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(
214             WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 2)
215         )
216 
217         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
218 
219         ViewUtils.attachView(view)
220         testableLooper.processAllMessages()
221 
222         assertThat(view.isIconVisible).isTrue()
223 
224         ViewUtils.detachView(view)
225     }
226 
227     @Test
onDarkChanged_iconHasNewColornull228     fun onDarkChanged_iconHasNewColor() {
229         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
230         ViewUtils.attachView(view)
231         testableLooper.processAllMessages()
232 
233         val color = 0x12345678
234         val contrast = 0x12344321
235         view.onDarkChangedWithContrast(arrayListOf(), color, contrast)
236         testableLooper.processAllMessages()
237 
238         assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color))
239 
240         ViewUtils.detachView(view)
241     }
242 
243     @Test
setStaticDrawableColor_iconHasNewColornull244     fun setStaticDrawableColor_iconHasNewColor() {
245         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
246         ViewUtils.attachView(view)
247         testableLooper.processAllMessages()
248 
249         val color = 0x23456789
250         val contrast = 0x12344321
251         view.setStaticDrawableColor(color, contrast)
252         testableLooper.processAllMessages()
253 
254         assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color))
255 
256         ViewUtils.detachView(view)
257     }
258 
getIconGroupViewnull259     private fun View.getIconGroupView(): View {
260         return this.requireViewById(R.id.wifi_group)
261     }
262 
getIconViewnull263     private fun View.getIconView(): ImageView {
264         return this.requireViewById(R.id.wifi_signal)
265     }
266 
getDotViewnull267     private fun View.getDotView(): View {
268         return this.requireViewById(R.id.status_bar_dot)
269     }
270 }
271 
272 private const val SLOT_NAME = "TestSlotName"
273 private const val NETWORK_ID = 200
274