1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui.statusbar.phone 16 17 import android.content.res.Configuration 18 import android.content.res.Configuration.SCREENLAYOUT_LAYOUTDIR_LTR 19 import android.content.res.Configuration.SCREENLAYOUT_LAYOUTDIR_RTL 20 import android.content.res.Configuration.UI_MODE_NIGHT_NO 21 import android.content.res.Configuration.UI_MODE_NIGHT_YES 22 import android.content.res.Configuration.UI_MODE_TYPE_CAR 23 import android.os.LocaleList 24 import android.view.Display 25 import androidx.test.ext.junit.runners.AndroidJUnit4 26 import androidx.test.filters.SmallTest 27 import com.android.systemui.SysuiTestCase 28 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener 29 import com.google.common.truth.Truth.assertThat 30 import java.util.Locale 31 import org.junit.Before 32 import org.junit.Ignore 33 import org.junit.Test 34 import org.junit.runner.RunWith 35 import org.mockito.Mockito.doAnswer 36 import org.mockito.Mockito.mock 37 import org.mockito.Mockito.never 38 import org.mockito.Mockito.verify 39 40 @RunWith(AndroidJUnit4::class) 41 @SmallTest 42 class ConfigurationControllerImplTest : SysuiTestCase() { 43 44 private lateinit var mConfigurationController: ConfigurationControllerImpl 45 46 @Before setUpnull47 fun setUp() { 48 mConfigurationController = ConfigurationControllerImpl(mContext) 49 } 50 51 @Test testThemeChangenull52 fun testThemeChange() { 53 val listener = mock(ConfigurationListener::class.java) 54 mConfigurationController.addCallback(listener) 55 56 mConfigurationController.notifyThemeChanged() 57 verify(listener).onThemeChanged() 58 } 59 60 @Test testRemoveListenerDuringCallbacknull61 fun testRemoveListenerDuringCallback() { 62 val listener = mock(ConfigurationListener::class.java) 63 mConfigurationController.addCallback(listener) 64 val listener2 = mock(ConfigurationListener::class.java) 65 mConfigurationController.addCallback(listener2) 66 67 doAnswer { 68 mConfigurationController.removeCallback(listener2) 69 null 70 } 71 .`when`(listener) 72 .onThemeChanged() 73 74 mConfigurationController.notifyThemeChanged() 75 verify(listener).onThemeChanged() 76 verify(listener2, never()).onThemeChanged() 77 } 78 79 @Test configChanged_listenerNotifiednull80 fun configChanged_listenerNotified() { 81 val config = mContext.resources.configuration 82 config.densityDpi = 12 83 config.smallestScreenWidthDp = 240 84 mConfigurationController.onConfigurationChanged(config) 85 86 val listener = createAndAddListener() 87 88 // WHEN the config is updated 89 config.densityDpi = 20 90 config.smallestScreenWidthDp = 300 91 mConfigurationController.onConfigurationChanged(config) 92 93 // THEN the listener is notified 94 assertThat(listener.changedConfig?.densityDpi).isEqualTo(20) 95 assertThat(listener.changedConfig?.smallestScreenWidthDp).isEqualTo(300) 96 } 97 98 @Test densityChanged_listenerNotifiednull99 fun densityChanged_listenerNotified() { 100 val config = mContext.resources.configuration 101 config.densityDpi = 12 102 mConfigurationController.onConfigurationChanged(config) 103 104 val listener = createAndAddListener() 105 106 // WHEN the density is updated 107 config.densityDpi = 20 108 mConfigurationController.onConfigurationChanged(config) 109 110 // THEN the listener is notified 111 assertThat(listener.densityOrFontScaleChanged).isTrue() 112 } 113 114 @Test fontChanged_listenerNotifiednull115 fun fontChanged_listenerNotified() { 116 val config = mContext.resources.configuration 117 config.fontScale = 1.5f 118 mConfigurationController.onConfigurationChanged(config) 119 120 val listener = createAndAddListener() 121 122 // WHEN the font is updated 123 config.fontScale = 1.4f 124 mConfigurationController.onConfigurationChanged(config) 125 126 // THEN the listener is notified 127 assertThat(listener.densityOrFontScaleChanged).isTrue() 128 } 129 130 @Test isCarAndUiModeChanged_densityListenerNotifiednull131 fun isCarAndUiModeChanged_densityListenerNotified() { 132 val config = mContext.resources.configuration 133 config.uiMode = UI_MODE_TYPE_CAR or UI_MODE_NIGHT_YES 134 // Re-create the controller since we calculate car mode on creation 135 mConfigurationController = ConfigurationControllerImpl(mContext) 136 137 val listener = createAndAddListener() 138 139 // WHEN the ui mode is updated 140 config.uiMode = UI_MODE_TYPE_CAR or UI_MODE_NIGHT_NO 141 mConfigurationController.onConfigurationChanged(config) 142 143 // THEN the listener is notified 144 assertThat(listener.densityOrFontScaleChanged).isTrue() 145 } 146 147 @Test isNotCarAndUiModeChanged_densityListenerNotNotifiednull148 fun isNotCarAndUiModeChanged_densityListenerNotNotified() { 149 val config = mContext.resources.configuration 150 config.uiMode = UI_MODE_NIGHT_YES 151 // Re-create the controller since we calculate car mode on creation 152 mConfigurationController = ConfigurationControllerImpl(mContext) 153 154 val listener = createAndAddListener() 155 156 // WHEN the ui mode is updated 157 config.uiMode = UI_MODE_NIGHT_NO 158 mConfigurationController.onConfigurationChanged(config) 159 160 // THEN the listener is not notified because it's not car mode 161 assertThat(listener.densityOrFontScaleChanged).isFalse() 162 } 163 164 @Test smallestScreenWidthChanged_listenerNotifiednull165 fun smallestScreenWidthChanged_listenerNotified() { 166 val config = mContext.resources.configuration 167 config.smallestScreenWidthDp = 240 168 mConfigurationController.onConfigurationChanged(config) 169 170 val listener = createAndAddListener() 171 172 // WHEN the width is updated 173 config.smallestScreenWidthDp = 300 174 mConfigurationController.onConfigurationChanged(config) 175 176 // THEN the listener is notified 177 assertThat(listener.smallestScreenWidthChanged).isTrue() 178 } 179 180 @Test maxBoundsChange_newConfigObject_listenerNotifiednull181 fun maxBoundsChange_newConfigObject_listenerNotified() { 182 val config = mContext.resources.configuration 183 config.windowConfiguration.setMaxBounds(0, 0, 200, 200) 184 mConfigurationController.onConfigurationChanged(config) 185 186 val listener = createAndAddListener() 187 188 // WHEN a new configuration object with new bounds is sent 189 val newConfig = Configuration() 190 newConfig.windowConfiguration.setMaxBounds(0, 0, 100, 100) 191 mConfigurationController.onConfigurationChanged(newConfig) 192 193 // THEN the listener is notified 194 assertThat(listener.maxBoundsChanged).isTrue() 195 } 196 197 // Regression test for b/245799099 198 @Test maxBoundsChange_sameObject_listenerNotifiednull199 fun maxBoundsChange_sameObject_listenerNotified() { 200 val config = mContext.resources.configuration 201 config.windowConfiguration.setMaxBounds(0, 0, 200, 200) 202 mConfigurationController.onConfigurationChanged(config) 203 204 val listener = createAndAddListener() 205 206 // WHEN the existing config is updated with new bounds 207 config.windowConfiguration.setMaxBounds(0, 0, 100, 100) 208 mConfigurationController.onConfigurationChanged(config) 209 210 // THEN the listener is notified 211 assertThat(listener.maxBoundsChanged).isTrue() 212 } 213 214 @Test localeListChanged_listenerNotifiednull215 fun localeListChanged_listenerNotified() { 216 val config = mContext.resources.configuration 217 config.setLocales(LocaleList(Locale.CANADA, Locale.GERMANY)) 218 mConfigurationController.onConfigurationChanged(config) 219 220 val listener = createAndAddListener() 221 222 // WHEN the locales are updated 223 config.setLocales(LocaleList(Locale.FRANCE, Locale.JAPAN, Locale.CHINESE)) 224 mConfigurationController.onConfigurationChanged(config) 225 226 // THEN the listener is notified 227 assertThat(listener.localeListChanged).isTrue() 228 } 229 230 @Test uiModeChanged_listenerNotifiednull231 fun uiModeChanged_listenerNotified() { 232 val config = mContext.resources.configuration 233 config.uiMode = UI_MODE_NIGHT_YES 234 mConfigurationController.onConfigurationChanged(config) 235 236 val listener = createAndAddListener() 237 238 // WHEN the ui mode is updated 239 config.uiMode = UI_MODE_NIGHT_NO 240 mConfigurationController.onConfigurationChanged(config) 241 242 // THEN the listener is notified 243 assertThat(listener.uiModeChanged).isTrue() 244 } 245 246 @Test layoutDirectionUpdated_listenerNotifiednull247 fun layoutDirectionUpdated_listenerNotified() { 248 val config = mContext.resources.configuration 249 config.screenLayout = SCREENLAYOUT_LAYOUTDIR_LTR 250 mConfigurationController.onConfigurationChanged(config) 251 252 val listener = createAndAddListener() 253 254 // WHEN the layout is updated 255 config.screenLayout = SCREENLAYOUT_LAYOUTDIR_RTL 256 mConfigurationController.onConfigurationChanged(config) 257 258 // THEN the listener is notified 259 assertThat(listener.layoutDirectionChanged).isTrue() 260 } 261 262 @Test assetPathsUpdated_listenerNotifiednull263 fun assetPathsUpdated_listenerNotified() { 264 val config = mContext.resources.configuration 265 config.assetsSeq = 45 266 mConfigurationController.onConfigurationChanged(config) 267 268 val listener = createAndAddListener() 269 270 // WHEN the assets sequence is updated 271 config.assetsSeq = 46 272 mConfigurationController.onConfigurationChanged(config) 273 274 // THEN the listener is notified 275 assertThat(listener.themeChanged).isTrue() 276 } 277 278 @Test orientationUpdated_listenerNotifiednull279 fun orientationUpdated_listenerNotified() { 280 val config = mContext.resources.configuration 281 config.orientation = Configuration.ORIENTATION_LANDSCAPE 282 mConfigurationController.onConfigurationChanged(config) 283 284 val listener = createAndAddListener() 285 286 // WHEN the orientation is updated 287 config.orientation = Configuration.ORIENTATION_PORTRAIT 288 mConfigurationController.onConfigurationChanged(config) 289 290 // THEN the listener is notified 291 assertThat(listener.orientationChanged).isTrue() 292 } 293 294 @Test multipleUpdates_listenerNotifiedOfAllnull295 fun multipleUpdates_listenerNotifiedOfAll() { 296 val config = mContext.resources.configuration 297 config.densityDpi = 14 298 config.windowConfiguration.setMaxBounds(0, 0, 2, 2) 299 config.uiMode = UI_MODE_NIGHT_YES 300 mConfigurationController.onConfigurationChanged(config) 301 302 val listener = createAndAddListener() 303 304 // WHEN multiple fields are updated 305 config.densityDpi = 20 306 config.windowConfiguration.setMaxBounds(0, 0, 3, 3) 307 config.uiMode = UI_MODE_NIGHT_NO 308 mConfigurationController.onConfigurationChanged(config) 309 310 // THEN the listener is notified of all of them 311 assertThat(listener.densityOrFontScaleChanged).isTrue() 312 assertThat(listener.maxBoundsChanged).isTrue() 313 assertThat(listener.uiModeChanged).isTrue() 314 } 315 316 @Test onMovedToDisplay_dispatchedToChildrennull317 fun onMovedToDisplay_dispatchedToChildren() { 318 val config = mContext.resources.configuration 319 val listener = createAndAddListener() 320 321 mConfigurationController.dispatchOnMovedToDisplay(newDisplayId = 1, config) 322 323 assertThat(listener.display).isEqualTo(1) 324 assertThat(listener.changedConfig).isEqualTo(config) 325 } 326 327 @Test 328 @Ignore("b/261408895") equivalentConfigObject_listenerNotNotifiednull329 fun equivalentConfigObject_listenerNotNotified() { 330 val config = mContext.resources.configuration 331 val listener = createAndAddListener() 332 333 // WHEN we update with the new object that has all the same fields 334 mConfigurationController.onConfigurationChanged(Configuration(config)) 335 336 listener.assertNoMethodsCalled() 337 } 338 createAndAddListenernull339 private fun createAndAddListener(): TestListener { 340 val listener = TestListener() 341 mConfigurationController.addCallback(listener) 342 // Adding a listener can trigger some callbacks, so we want to reset the values right 343 // after the listener is added 344 listener.reset() 345 return listener 346 } 347 348 private class TestListener : ConfigurationListener { 349 var changedConfig: Configuration? = null 350 var densityOrFontScaleChanged = false 351 var smallestScreenWidthChanged = false 352 var maxBoundsChanged = false 353 var uiModeChanged = false 354 var themeChanged = false 355 var localeListChanged = false 356 var layoutDirectionChanged = false 357 var orientationChanged = false 358 var display = Display.DEFAULT_DISPLAY 359 onConfigChangednull360 override fun onConfigChanged(newConfig: Configuration?) { 361 changedConfig = newConfig 362 } 363 onDensityOrFontScaleChangednull364 override fun onDensityOrFontScaleChanged() { 365 densityOrFontScaleChanged = true 366 } 367 onSmallestScreenWidthChangednull368 override fun onSmallestScreenWidthChanged() { 369 smallestScreenWidthChanged = true 370 } 371 onMaxBoundsChangednull372 override fun onMaxBoundsChanged() { 373 maxBoundsChanged = true 374 } 375 onUiModeChangednull376 override fun onUiModeChanged() { 377 uiModeChanged = true 378 } 379 onThemeChangednull380 override fun onThemeChanged() { 381 themeChanged = true 382 } 383 onLocaleListChangednull384 override fun onLocaleListChanged() { 385 localeListChanged = true 386 } 387 onLayoutDirectionChangednull388 override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) { 389 layoutDirectionChanged = true 390 } 391 onOrientationChangednull392 override fun onOrientationChanged(orientation: Int) { 393 orientationChanged = true 394 } 395 onMovedToDisplaynull396 override fun onMovedToDisplay(newDisplayId: Int, newConfiguration: Configuration?) { 397 display = newDisplayId 398 changedConfig = newConfiguration 399 } 400 assertNoMethodsCallednull401 fun assertNoMethodsCalled() { 402 assertThat(densityOrFontScaleChanged).isFalse() 403 assertThat(smallestScreenWidthChanged).isFalse() 404 assertThat(maxBoundsChanged).isFalse() 405 assertThat(uiModeChanged).isFalse() 406 assertThat(themeChanged).isFalse() 407 assertThat(localeListChanged).isFalse() 408 assertThat(layoutDirectionChanged).isFalse() 409 } 410 resetnull411 fun reset() { 412 changedConfig = null 413 densityOrFontScaleChanged = false 414 smallestScreenWidthChanged = false 415 maxBoundsChanged = false 416 uiModeChanged = false 417 themeChanged = false 418 localeListChanged = false 419 layoutDirectionChanged = false 420 display = Display.DEFAULT_DISPLAY 421 } 422 } 423 } 424