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