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.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.widget.FrameLayout
26 import android.widget.ImageView
27 import androidx.test.filters.SmallTest
28 import com.android.systemui.SysuiTestCase
29 import com.android.systemui.flags.FakeFeatureFlagsClassic
30 import com.android.systemui.flags.Flags
31 import com.android.systemui.log.table.logcatTableLogBuffer
32 import com.android.systemui.res.R
33 import com.android.systemui.statusbar.StatusBarIconView
34 import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
35 import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
36 import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
37 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
38 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
39 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
40 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
41 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.QsMobileIconViewModel
42 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
43 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
44 import com.android.systemui.testKosmos
45 import com.android.systemui.util.mockito.whenever
46 import com.google.common.truth.Truth.assertThat
47 import kotlinx.coroutines.ExperimentalCoroutinesApi
48 import kotlinx.coroutines.test.TestScope
49 import kotlinx.coroutines.test.UnconfinedTestDispatcher
50 import kotlinx.coroutines.test.runTest
51 import org.junit.Before
52 import org.junit.Test
53 import org.junit.runner.RunWith
54 import org.mockito.Mock
55 import org.mockito.MockitoAnnotations
56 
57 @SmallTest
58 @RunWith(AndroidTestingRunner::class)
59 @RunWithLooper(setAsMainLooper = true)
60 @OptIn(ExperimentalCoroutinesApi::class)
61 class ModernStatusBarMobileViewTest : SysuiTestCase() {
62     private val kosmos = testKosmos()
63 
64     private lateinit var testableLooper: TestableLooper
65     private val testDispatcher = UnconfinedTestDispatcher()
66     private val testScope = TestScope(testDispatcher)
<lambda>null67     private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) }
68 
69     @Mock private lateinit var viewLogger: MobileViewLogger
70     @Mock private lateinit var constants: ConnectivityConstants
71     private lateinit var interactor: FakeMobileIconInteractor
72     private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
73     private lateinit var airplaneModeInteractor: AirplaneModeInteractor
74 
75     private lateinit var viewModelCommon: MobileIconViewModel
76     private lateinit var viewModel: LocationBasedMobileViewModel
77 
78     @Before
setUpnull79     fun setUp() {
80         MockitoAnnotations.initMocks(this)
81         // This line was necessary to make the onDarkChanged and setStaticDrawableColor tests pass.
82         // But, it maybe *shouldn't* be necessary.
83         whenever(constants.hasDataCapabilities).thenReturn(true)
84 
85         testableLooper = TestableLooper.get(this)
86 
87         airplaneModeRepository = FakeAirplaneModeRepository()
88         airplaneModeInteractor =
89             AirplaneModeInteractor(
90                 airplaneModeRepository,
91                 FakeConnectivityRepository(),
92                 kosmos.fakeMobileConnectionsRepository,
93             )
94 
95         interactor =
96             FakeMobileIconInteractor(logcatTableLogBuffer(kosmos, "ModernStatusBarMobileViewTest"))
97         createViewModel()
98     }
99 
100     // Note: The following tests are more like integration tests, since they stand up a full
101     // [MobileIconViewModel] and test the interactions between the view, view-binder, and
102     // view-model.
103 
104     @Test
setVisibleState_icon_iconShownDotHiddennull105     fun setVisibleState_icon_iconShownDotHidden() {
106         val view =
107             ModernStatusBarMobileView.constructAndBind(
108                 context,
109                 viewLogger,
110                 SLOT_NAME,
111                 viewModel,
112             )
113 
114         view.setVisibleState(StatusBarIconView.STATE_ICON, /* animate= */ false)
115 
116         ViewUtils.attachView(view)
117         testableLooper.processAllMessages()
118 
119         assertThat(view.getGroupView().visibility).isEqualTo(View.VISIBLE)
120         assertThat(view.getDotView().visibility).isEqualTo(View.GONE)
121 
122         ViewUtils.detachView(view)
123     }
124 
125     @Test
setVisibleState_dot_iconHiddenDotShownnull126     fun setVisibleState_dot_iconHiddenDotShown() {
127         val view =
128             ModernStatusBarMobileView.constructAndBind(
129                 context,
130                 viewLogger,
131                 SLOT_NAME,
132                 viewModel,
133             )
134         view.setVisibleState(StatusBarIconView.STATE_DOT, /* animate= */ false)
135 
136         ViewUtils.attachView(view)
137         testableLooper.processAllMessages()
138 
139         assertThat(view.getGroupView().visibility).isEqualTo(View.INVISIBLE)
140         assertThat(view.getDotView().visibility).isEqualTo(View.VISIBLE)
141 
142         ViewUtils.detachView(view)
143     }
144 
145     @Test
setVisibleState_hidden_iconAndDotHiddennull146     fun setVisibleState_hidden_iconAndDotHidden() {
147         val view =
148             ModernStatusBarMobileView.constructAndBind(
149                 context,
150                 viewLogger,
151                 SLOT_NAME,
152                 viewModel,
153             )
154         view.setVisibleState(StatusBarIconView.STATE_HIDDEN, /* animate= */ false)
155 
156         ViewUtils.attachView(view)
157         testableLooper.processAllMessages()
158 
159         assertThat(view.getGroupView().visibility).isEqualTo(View.INVISIBLE)
160         assertThat(view.getDotView().visibility).isEqualTo(View.INVISIBLE)
161 
162         ViewUtils.detachView(view)
163     }
164 
165     @Test
isIconVisible_noData_outputsFalsenull166     fun isIconVisible_noData_outputsFalse() {
167         whenever(constants.hasDataCapabilities).thenReturn(false)
168         createViewModel()
169 
170         val view =
171             ModernStatusBarMobileView.constructAndBind(
172                 context,
173                 viewLogger,
174                 SLOT_NAME,
175                 viewModel,
176             )
177         ViewUtils.attachView(view)
178         testableLooper.processAllMessages()
179 
180         assertThat(view.isIconVisible).isFalse()
181 
182         ViewUtils.detachView(view)
183     }
184 
185     @Test
isIconVisible_hasData_outputsTruenull186     fun isIconVisible_hasData_outputsTrue() {
187         whenever(constants.hasDataCapabilities).thenReturn(true)
188         createViewModel()
189 
190         val view =
191             ModernStatusBarMobileView.constructAndBind(
192                 context,
193                 viewLogger,
194                 SLOT_NAME,
195                 viewModel,
196             )
197         ViewUtils.attachView(view)
198         testableLooper.processAllMessages()
199 
200         assertThat(view.isIconVisible).isTrue()
201 
202         ViewUtils.detachView(view)
203     }
204 
205     @Test
<lambda>null206     fun isIconVisible_notAirplaneMode_outputsTrue() = runTest {
207         airplaneModeRepository.setIsAirplaneMode(false)
208 
209         val view =
210             ModernStatusBarMobileView.constructAndBind(
211                 context,
212                 viewLogger,
213                 SLOT_NAME,
214                 viewModel,
215             )
216         ViewUtils.attachView(view)
217         testableLooper.processAllMessages()
218 
219         assertThat(view.isIconVisible).isTrue()
220 
221         ViewUtils.detachView(view)
222     }
223 
224     @Test
<lambda>null225     fun isIconVisible_airplaneMode_outputsTrue() = runTest {
226         airplaneModeRepository.setIsAirplaneMode(true)
227 
228         val view =
229             ModernStatusBarMobileView.constructAndBind(
230                 context,
231                 viewLogger,
232                 SLOT_NAME,
233                 viewModel,
234             )
235         ViewUtils.attachView(view)
236         testableLooper.processAllMessages()
237 
238         assertThat(view.isIconVisible).isFalse()
239 
240         ViewUtils.detachView(view)
241     }
242 
243     @Test
onDarkChanged_iconHasNewColornull244     fun onDarkChanged_iconHasNewColor() {
245         val view =
246             ModernStatusBarMobileView.constructAndBind(
247                 context,
248                 viewLogger,
249                 SLOT_NAME,
250                 viewModel,
251             )
252         ViewUtils.attachView(view)
253         testableLooper.processAllMessages()
254 
255         val color = 0x12345678
256         val contrast = 0x12344321
257         view.onDarkChangedWithContrast(arrayListOf(), color, contrast)
258         testableLooper.processAllMessages()
259 
260         assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color))
261 
262         ViewUtils.detachView(view)
263     }
264 
265     @Test
setStaticDrawableColor_iconHasNewColornull266     fun setStaticDrawableColor_iconHasNewColor() {
267         val view =
268             ModernStatusBarMobileView.constructAndBind(
269                 context,
270                 viewLogger,
271                 SLOT_NAME,
272                 viewModel,
273             )
274         ViewUtils.attachView(view)
275         testableLooper.processAllMessages()
276 
277         val color = 0x23456789
278         val contrast = 0x12344321
279         view.setStaticDrawableColor(color, contrast)
280         testableLooper.processAllMessages()
281 
282         assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color))
283 
284         ViewUtils.detachView(view)
285     }
286 
287     @Test
colorChange_layersUpdateWithContrastnull288     fun colorChange_layersUpdateWithContrast() {
289         // Allow the slice, and set it to visible. This cause us to use special color logic
290         flags.set(Flags.NEW_NETWORK_SLICE_UI, true)
291         interactor.showSliceAttribution.value = true
292         createViewModel()
293 
294         val view =
295             ModernStatusBarMobileView.constructAndBind(
296                 context,
297                 viewLogger,
298                 SLOT_NAME,
299                 viewModel,
300             )
301         ViewUtils.attachView(view)
302         testableLooper.processAllMessages()
303 
304         val color = 0x23456789
305         val contrast = 0x12344321
306         view.setStaticDrawableColor(color, contrast)
307 
308         testableLooper.processAllMessages()
309 
310         assertThat(view.getNetTypeContainer().backgroundTintList).isEqualTo(color.colorState())
311         assertThat(view.getNetTypeView().imageTintList).isEqualTo(contrast.colorState())
312 
313         ViewUtils.detachView(view)
314     }
315 
getGroupViewnull316     private fun View.getGroupView(): View {
317         return this.requireViewById(R.id.mobile_group)
318     }
319 
getIconViewnull320     private fun View.getIconView(): ImageView {
321         return this.requireViewById(R.id.mobile_signal)
322     }
323 
getNetTypeContainernull324     private fun View.getNetTypeContainer(): FrameLayout {
325         return this.requireViewById(R.id.mobile_type_container)
326     }
327 
getNetTypeViewnull328     private fun View.getNetTypeView(): ImageView {
329         return this.requireViewById(R.id.mobile_type)
330     }
331 
getDotViewnull332     private fun View.getDotView(): View {
333         return this.requireViewById(R.id.status_bar_dot)
334     }
335 
colorStatenull336     private fun Int.colorState() = ColorStateList.valueOf(this)
337 
338     private fun createViewModel() {
339         viewModelCommon =
340             MobileIconViewModel(
341                 subscriptionId = 1,
342                 interactor,
343                 airplaneModeInteractor,
344                 constants,
345                 flags,
346                 testScope.backgroundScope,
347             )
348         viewModel = QsMobileIconViewModel(viewModelCommon)
349     }
350 }
351 
352 private const val SLOT_NAME = "TestSlotName"
353