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.testing.AndroidTestingRunner 25 import androidx.test.filters.SmallTest 26 import com.android.systemui.SysuiTestCase 27 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener 28 import com.google.common.truth.Truth.assertThat 29 import org.junit.Before 30 import org.junit.Ignore 31 import org.junit.Test 32 import org.junit.runner.RunWith 33 import org.mockito.Mockito.doAnswer 34 import org.mockito.Mockito.mock 35 import org.mockito.Mockito.never 36 import org.mockito.Mockito.verify 37 import java.util.Locale 38 39 @RunWith(AndroidTestingRunner::class) 40 @SmallTest 41 class ConfigurationControllerImplTest : SysuiTestCase() { 42 43 private lateinit var mConfigurationController: ConfigurationControllerImpl 44 45 @Before setUpnull46 fun setUp() { 47 mConfigurationController = ConfigurationControllerImpl(mContext) 48 } 49 50 @Test testThemeChangenull51 fun testThemeChange() { 52 val listener = mock(ConfigurationListener::class.java) 53 mConfigurationController.addCallback(listener) 54 55 mConfigurationController.notifyThemeChanged() 56 verify(listener).onThemeChanged() 57 } 58 59 @Test testRemoveListenerDuringCallbacknull60 fun testRemoveListenerDuringCallback() { 61 val listener = mock(ConfigurationListener::class.java) 62 mConfigurationController.addCallback(listener) 63 val listener2 = mock(ConfigurationListener::class.java) 64 mConfigurationController.addCallback(listener2) 65 66 doAnswer { 67 mConfigurationController.removeCallback(listener2) 68 null 69 }.`when`(listener).onThemeChanged() 70 71 mConfigurationController.notifyThemeChanged() 72 verify(listener).onThemeChanged() 73 verify(listener2, never()).onThemeChanged() 74 } 75 76 @Test configChanged_listenerNotifiednull77 fun configChanged_listenerNotified() { 78 val config = mContext.resources.configuration 79 config.densityDpi = 12 80 config.smallestScreenWidthDp = 240 81 mConfigurationController.onConfigurationChanged(config) 82 83 val listener = createAndAddListener() 84 85 // WHEN the config is updated 86 config.densityDpi = 20 87 config.smallestScreenWidthDp = 300 88 mConfigurationController.onConfigurationChanged(config) 89 90 // THEN the listener is notified 91 assertThat(listener.changedConfig?.densityDpi).isEqualTo(20) 92 assertThat(listener.changedConfig?.smallestScreenWidthDp).isEqualTo(300) 93 } 94 95 @Test densityChanged_listenerNotifiednull96 fun densityChanged_listenerNotified() { 97 val config = mContext.resources.configuration 98 config.densityDpi = 12 99 mConfigurationController.onConfigurationChanged(config) 100 101 val listener = createAndAddListener() 102 103 // WHEN the density is updated 104 config.densityDpi = 20 105 mConfigurationController.onConfigurationChanged(config) 106 107 // THEN the listener is notified 108 assertThat(listener.densityOrFontScaleChanged).isTrue() 109 } 110 111 @Test fontChanged_listenerNotifiednull112 fun fontChanged_listenerNotified() { 113 val config = mContext.resources.configuration 114 config.fontScale = 1.5f 115 mConfigurationController.onConfigurationChanged(config) 116 117 val listener = createAndAddListener() 118 119 // WHEN the font is updated 120 config.fontScale = 1.4f 121 mConfigurationController.onConfigurationChanged(config) 122 123 // THEN the listener is notified 124 assertThat(listener.densityOrFontScaleChanged).isTrue() 125 } 126 127 @Test isCarAndUiModeChanged_densityListenerNotifiednull128 fun isCarAndUiModeChanged_densityListenerNotified() { 129 val config = mContext.resources.configuration 130 config.uiMode = UI_MODE_TYPE_CAR or UI_MODE_NIGHT_YES 131 // Re-create the controller since we calculate car mode on creation 132 mConfigurationController = ConfigurationControllerImpl(mContext) 133 134 val listener = createAndAddListener() 135 136 // WHEN the ui mode is updated 137 config.uiMode = UI_MODE_TYPE_CAR or UI_MODE_NIGHT_NO 138 mConfigurationController.onConfigurationChanged(config) 139 140 // THEN the listener is notified 141 assertThat(listener.densityOrFontScaleChanged).isTrue() 142 } 143 144 @Test isNotCarAndUiModeChanged_densityListenerNotNotifiednull145 fun isNotCarAndUiModeChanged_densityListenerNotNotified() { 146 val config = mContext.resources.configuration 147 config.uiMode = UI_MODE_NIGHT_YES 148 // Re-create the controller since we calculate car mode on creation 149 mConfigurationController = ConfigurationControllerImpl(mContext) 150 151 val listener = createAndAddListener() 152 153 // WHEN the ui mode is updated 154 config.uiMode = UI_MODE_NIGHT_NO 155 mConfigurationController.onConfigurationChanged(config) 156 157 // THEN the listener is not notified because it's not car mode 158 assertThat(listener.densityOrFontScaleChanged).isFalse() 159 } 160 161 @Test smallestScreenWidthChanged_listenerNotifiednull162 fun smallestScreenWidthChanged_listenerNotified() { 163 val config = mContext.resources.configuration 164 config.smallestScreenWidthDp = 240 165 mConfigurationController.onConfigurationChanged(config) 166 167 val listener = createAndAddListener() 168 169 // WHEN the width is updated 170 config.smallestScreenWidthDp = 300 171 mConfigurationController.onConfigurationChanged(config) 172 173 // THEN the listener is notified 174 assertThat(listener.smallestScreenWidthChanged).isTrue() 175 } 176 177 @Test maxBoundsChange_newConfigObject_listenerNotifiednull178 fun maxBoundsChange_newConfigObject_listenerNotified() { 179 val config = mContext.resources.configuration 180 config.windowConfiguration.setMaxBounds(0, 0, 200, 200) 181 mConfigurationController.onConfigurationChanged(config) 182 183 val listener = createAndAddListener() 184 185 // WHEN a new configuration object with new bounds is sent 186 val newConfig = Configuration() 187 newConfig.windowConfiguration.setMaxBounds(0, 0, 100, 100) 188 mConfigurationController.onConfigurationChanged(newConfig) 189 190 // THEN the listener is notified 191 assertThat(listener.maxBoundsChanged).isTrue() 192 } 193 194 // Regression test for b/245799099 195 @Test maxBoundsChange_sameObject_listenerNotifiednull196 fun maxBoundsChange_sameObject_listenerNotified() { 197 val config = mContext.resources.configuration 198 config.windowConfiguration.setMaxBounds(0, 0, 200, 200) 199 mConfigurationController.onConfigurationChanged(config) 200 201 val listener = createAndAddListener() 202 203 // WHEN the existing config is updated with new bounds 204 config.windowConfiguration.setMaxBounds(0, 0, 100, 100) 205 mConfigurationController.onConfigurationChanged(config) 206 207 // THEN the listener is notified 208 assertThat(listener.maxBoundsChanged).isTrue() 209 } 210 211 212 @Test localeListChanged_listenerNotifiednull213 fun localeListChanged_listenerNotified() { 214 val config = mContext.resources.configuration 215 config.locales = LocaleList(Locale.CANADA, Locale.GERMANY) 216 mConfigurationController.onConfigurationChanged(config) 217 218 val listener = createAndAddListener() 219 220 // WHEN the locales are updated 221 config.locales = LocaleList(Locale.FRANCE, Locale.JAPAN, Locale.CHINESE) 222 mConfigurationController.onConfigurationChanged(config) 223 224 // THEN the listener is notified 225 assertThat(listener.localeListChanged).isTrue() 226 } 227 228 @Test uiModeChanged_listenerNotifiednull229 fun uiModeChanged_listenerNotified() { 230 val config = mContext.resources.configuration 231 config.uiMode = UI_MODE_NIGHT_YES 232 mConfigurationController.onConfigurationChanged(config) 233 234 val listener = createAndAddListener() 235 236 // WHEN the ui mode is updated 237 config.uiMode = UI_MODE_NIGHT_NO 238 mConfigurationController.onConfigurationChanged(config) 239 240 // THEN the listener is notified 241 assertThat(listener.uiModeChanged).isTrue() 242 } 243 244 @Test layoutDirectionUpdated_listenerNotifiednull245 fun layoutDirectionUpdated_listenerNotified() { 246 val config = mContext.resources.configuration 247 config.screenLayout = SCREENLAYOUT_LAYOUTDIR_LTR 248 mConfigurationController.onConfigurationChanged(config) 249 250 val listener = createAndAddListener() 251 252 // WHEN the layout is updated 253 config.screenLayout = SCREENLAYOUT_LAYOUTDIR_RTL 254 mConfigurationController.onConfigurationChanged(config) 255 256 // THEN the listener is notified 257 assertThat(listener.layoutDirectionChanged).isTrue() 258 } 259 260 @Test assetPathsUpdated_listenerNotifiednull261 fun assetPathsUpdated_listenerNotified() { 262 val config = mContext.resources.configuration 263 config.assetsSeq = 45 264 mConfigurationController.onConfigurationChanged(config) 265 266 val listener = createAndAddListener() 267 268 // WHEN the assets sequence is updated 269 config.assetsSeq = 46 270 mConfigurationController.onConfigurationChanged(config) 271 272 // THEN the listener is notified 273 assertThat(listener.themeChanged).isTrue() 274 } 275 276 @Test multipleUpdates_listenerNotifiedOfAllnull277 fun multipleUpdates_listenerNotifiedOfAll() { 278 val config = mContext.resources.configuration 279 config.densityDpi = 14 280 config.windowConfiguration.setMaxBounds(0, 0, 2, 2) 281 config.uiMode = UI_MODE_NIGHT_YES 282 mConfigurationController.onConfigurationChanged(config) 283 284 val listener = createAndAddListener() 285 286 // WHEN multiple fields are updated 287 config.densityDpi = 20 288 config.windowConfiguration.setMaxBounds(0, 0, 3, 3) 289 config.uiMode = UI_MODE_NIGHT_NO 290 mConfigurationController.onConfigurationChanged(config) 291 292 // THEN the listener is notified of all of them 293 assertThat(listener.densityOrFontScaleChanged).isTrue() 294 assertThat(listener.maxBoundsChanged).isTrue() 295 assertThat(listener.uiModeChanged).isTrue() 296 } 297 298 @Test 299 @Ignore("b/261408895") equivalentConfigObject_listenerNotNotifiednull300 fun equivalentConfigObject_listenerNotNotified() { 301 val config = mContext.resources.configuration 302 val listener = createAndAddListener() 303 304 // WHEN we update with the new object that has all the same fields 305 mConfigurationController.onConfigurationChanged(Configuration(config)) 306 307 listener.assertNoMethodsCalled() 308 } 309 createAndAddListenernull310 private fun createAndAddListener(): TestListener { 311 val listener = TestListener() 312 mConfigurationController.addCallback(listener) 313 // Adding a listener can trigger some callbacks, so we want to reset the values right 314 // after the listener is added 315 listener.reset() 316 return listener 317 } 318 319 private class TestListener : ConfigurationListener { 320 var changedConfig: Configuration? = null 321 var densityOrFontScaleChanged = false 322 var smallestScreenWidthChanged = false 323 var maxBoundsChanged = false 324 var uiModeChanged = false 325 var themeChanged = false 326 var localeListChanged = false 327 var layoutDirectionChanged = false 328 onConfigChangednull329 override fun onConfigChanged(newConfig: Configuration?) { 330 changedConfig = newConfig 331 } onDensityOrFontScaleChangednull332 override fun onDensityOrFontScaleChanged() { 333 densityOrFontScaleChanged = true 334 } onSmallestScreenWidthChangednull335 override fun onSmallestScreenWidthChanged() { 336 smallestScreenWidthChanged = true 337 } onMaxBoundsChangednull338 override fun onMaxBoundsChanged() { 339 maxBoundsChanged = true 340 } onUiModeChangednull341 override fun onUiModeChanged() { 342 uiModeChanged = true 343 } onThemeChangednull344 override fun onThemeChanged() { 345 themeChanged = true 346 } onLocaleListChangednull347 override fun onLocaleListChanged() { 348 localeListChanged = true 349 } onLayoutDirectionChangednull350 override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) { 351 layoutDirectionChanged = true 352 } 353 assertNoMethodsCallednull354 fun assertNoMethodsCalled() { 355 assertThat(densityOrFontScaleChanged).isFalse() 356 assertThat(smallestScreenWidthChanged).isFalse() 357 assertThat(maxBoundsChanged).isFalse() 358 assertThat(uiModeChanged).isFalse() 359 assertThat(themeChanged).isFalse() 360 assertThat(localeListChanged).isFalse() 361 assertThat(layoutDirectionChanged).isFalse() 362 } 363 resetnull364 fun reset() { 365 changedConfig = null 366 densityOrFontScaleChanged = false 367 smallestScreenWidthChanged = false 368 maxBoundsChanged = false 369 uiModeChanged = false 370 themeChanged = false 371 localeListChanged = false 372 layoutDirectionChanged = false 373 } 374 } 375 } 376