diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt index 34cd1ceeffc71..7dcdc0bdb3832 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt @@ -33,7 +33,7 @@ class ConfigurationControllerImpl @Inject constructor(context: Context) : Config private val lastConfig = Configuration() private var density: Int = 0 private var smallestScreenWidth: Int = 0 - private var maxBounds: Rect? = null + private var maxBounds = Rect() private var fontScale: Float = 0.toFloat() private val inCarMode: Boolean private var uiMode: Int = 0 @@ -47,6 +47,7 @@ class ConfigurationControllerImpl @Inject constructor(context: Context) : Config fontScale = currentConfig.fontScale density = currentConfig.densityDpi smallestScreenWidth = currentConfig.smallestScreenWidthDp + maxBounds.set(currentConfig.windowConfiguration.maxBounds) inCarMode = currentConfig.uiMode and Configuration.UI_MODE_TYPE_MASK == Configuration.UI_MODE_TYPE_CAR uiMode = currentConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK @@ -92,7 +93,11 @@ class ConfigurationControllerImpl @Inject constructor(context: Context) : Config val maxBounds = newConfig.windowConfiguration.maxBounds if (maxBounds != this.maxBounds) { - this.maxBounds = maxBounds + // Update our internal rect to have the same bounds, instead of using + // `this.maxBounds = maxBounds` directly. Setting it directly means that `maxBounds` + // would be a direct reference to windowConfiguration.maxBounds, so the if statement + // above would always fail. See b/245799099 for more information. + this.maxBounds.set(maxBounds) listeners.filterForEach({ this.listeners.contains(it) }) { it.onMaxBoundsChanged() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt index fee3ccb217922..038af8ff53967 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt @@ -14,23 +14,37 @@ package com.android.systemui.statusbar.phone -import androidx.test.filters.SmallTest +import android.content.res.Configuration +import android.content.res.Configuration.SCREENLAYOUT_LAYOUTDIR_LTR +import android.content.res.Configuration.SCREENLAYOUT_LAYOUTDIR_RTL +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.content.res.Configuration.UI_MODE_TYPE_CAR +import android.os.LocaleList import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener +import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.doAnswer import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.verify +import java.util.Locale @RunWith(AndroidTestingRunner::class) @SmallTest class ConfigurationControllerImplTest : SysuiTestCase() { - private val mConfigurationController = - com.android.systemui.statusbar.phone.ConfigurationControllerImpl(mContext) + private lateinit var mConfigurationController: ConfigurationControllerImpl + + @Before + fun setUp() { + mConfigurationController = ConfigurationControllerImpl(mContext) + } @Test fun testThemeChange() { @@ -57,4 +71,303 @@ class ConfigurationControllerImplTest : SysuiTestCase() { verify(listener).onThemeChanged() verify(listener2, never()).onThemeChanged() } + + @Test + fun configChanged_listenerNotified() { + val config = mContext.resources.configuration + config.densityDpi = 12 + config.smallestScreenWidthDp = 240 + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the config is updated + config.densityDpi = 20 + config.smallestScreenWidthDp = 300 + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.changedConfig?.densityDpi).isEqualTo(20) + assertThat(listener.changedConfig?.smallestScreenWidthDp).isEqualTo(300) + } + + @Test + fun densityChanged_listenerNotified() { + val config = mContext.resources.configuration + config.densityDpi = 12 + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the density is updated + config.densityDpi = 20 + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.densityOrFontScaleChanged).isTrue() + } + + @Test + fun fontChanged_listenerNotified() { + val config = mContext.resources.configuration + config.fontScale = 1.5f + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the font is updated + config.fontScale = 1.4f + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.densityOrFontScaleChanged).isTrue() + } + + @Test + fun isCarAndUiModeChanged_densityListenerNotified() { + val config = mContext.resources.configuration + config.uiMode = UI_MODE_TYPE_CAR or UI_MODE_NIGHT_YES + // Re-create the controller since we calculate car mode on creation + mConfigurationController = ConfigurationControllerImpl(mContext) + + val listener = createAndAddListener() + + // WHEN the ui mode is updated + config.uiMode = UI_MODE_TYPE_CAR or UI_MODE_NIGHT_NO + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.densityOrFontScaleChanged).isTrue() + } + + @Test + fun isNotCarAndUiModeChanged_densityListenerNotNotified() { + val config = mContext.resources.configuration + config.uiMode = UI_MODE_NIGHT_YES + // Re-create the controller since we calculate car mode on creation + mConfigurationController = ConfigurationControllerImpl(mContext) + + val listener = createAndAddListener() + + // WHEN the ui mode is updated + config.uiMode = UI_MODE_NIGHT_NO + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is not notified because it's not car mode + assertThat(listener.densityOrFontScaleChanged).isFalse() + } + + @Test + fun smallestScreenWidthChanged_listenerNotified() { + val config = mContext.resources.configuration + config.smallestScreenWidthDp = 240 + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the width is updated + config.smallestScreenWidthDp = 300 + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.smallestScreenWidthChanged).isTrue() + } + + @Test + fun maxBoundsChange_newConfigObject_listenerNotified() { + val config = mContext.resources.configuration + config.windowConfiguration.setMaxBounds(0, 0, 200, 200) + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN a new configuration object with new bounds is sent + val newConfig = Configuration() + newConfig.windowConfiguration.setMaxBounds(0, 0, 100, 100) + mConfigurationController.onConfigurationChanged(newConfig) + + // THEN the listener is notified + assertThat(listener.maxBoundsChanged).isTrue() + } + + // Regression test for b/245799099 + @Test + fun maxBoundsChange_sameObject_listenerNotified() { + val config = mContext.resources.configuration + config.windowConfiguration.setMaxBounds(0, 0, 200, 200) + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the existing config is updated with new bounds + config.windowConfiguration.setMaxBounds(0, 0, 100, 100) + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.maxBoundsChanged).isTrue() + } + + + @Test + fun localeListChanged_listenerNotified() { + val config = mContext.resources.configuration + config.locales = LocaleList(Locale.CANADA, Locale.GERMANY) + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the locales are updated + config.locales = LocaleList(Locale.FRANCE, Locale.JAPAN, Locale.CHINESE) + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.localeListChanged).isTrue() + } + + @Test + fun uiModeChanged_listenerNotified() { + val config = mContext.resources.configuration + config.uiMode = UI_MODE_NIGHT_YES + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the ui mode is updated + config.uiMode = UI_MODE_NIGHT_NO + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.uiModeChanged).isTrue() + } + + @Test + fun layoutDirectionUpdated_listenerNotified() { + val config = mContext.resources.configuration + config.screenLayout = SCREENLAYOUT_LAYOUTDIR_LTR + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the layout is updated + config.screenLayout = SCREENLAYOUT_LAYOUTDIR_RTL + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.layoutDirectionChanged).isTrue() + } + + @Test + fun assetPathsUpdated_listenerNotified() { + val config = mContext.resources.configuration + config.assetsSeq = 45 + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the assets sequence is updated + config.assetsSeq = 46 + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.themeChanged).isTrue() + } + + @Test + fun multipleUpdates_listenerNotifiedOfAll() { + val config = mContext.resources.configuration + config.densityDpi = 14 + config.windowConfiguration.setMaxBounds(0, 0, 2, 2) + config.uiMode = UI_MODE_NIGHT_YES + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN multiple fields are updated + config.densityDpi = 20 + config.windowConfiguration.setMaxBounds(0, 0, 3, 3) + config.uiMode = UI_MODE_NIGHT_NO + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified of all of them + assertThat(listener.densityOrFontScaleChanged).isTrue() + assertThat(listener.maxBoundsChanged).isTrue() + assertThat(listener.uiModeChanged).isTrue() + } + + @Test + fun equivalentConfigObject_listenerNotNotified() { + val config = mContext.resources.configuration + val listener = createAndAddListener() + + // WHEN we update with the new object that has all the same fields + mConfigurationController.onConfigurationChanged(Configuration(config)) + + listener.assertNoMethodsCalled() + } + + private fun createAndAddListener(): TestListener { + val listener = TestListener() + mConfigurationController.addCallback(listener) + // Adding a listener can trigger some callbacks, so we want to reset the values right + // after the listener is added + listener.reset() + return listener + } + + private class TestListener : ConfigurationListener { + var changedConfig: Configuration? = null + var densityOrFontScaleChanged = false + var smallestScreenWidthChanged = false + var maxBoundsChanged = false + var uiModeChanged = false + var themeChanged = false + var localeListChanged = false + var layoutDirectionChanged = false + + override fun onConfigChanged(newConfig: Configuration?) { + changedConfig = newConfig + } + override fun onDensityOrFontScaleChanged() { + densityOrFontScaleChanged = true + } + override fun onSmallestScreenWidthChanged() { + smallestScreenWidthChanged = true + } + override fun onMaxBoundsChanged() { + maxBoundsChanged = true + } + override fun onUiModeChanged() { + uiModeChanged = true + } + override fun onThemeChanged() { + themeChanged = true + } + override fun onLocaleListChanged() { + localeListChanged = true + } + override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) { + layoutDirectionChanged = true + } + + fun assertNoMethodsCalled() { + assertThat(densityOrFontScaleChanged).isFalse() + assertThat(smallestScreenWidthChanged).isFalse() + assertThat(maxBoundsChanged).isFalse() + assertThat(uiModeChanged).isFalse() + assertThat(themeChanged).isFalse() + assertThat(localeListChanged).isFalse() + assertThat(layoutDirectionChanged).isFalse() + } + + fun reset() { + changedConfig = null + densityOrFontScaleChanged = false + smallestScreenWidthChanged = false + maxBoundsChanged = false + uiModeChanged = false + themeChanged = false + localeListChanged = false + layoutDirectionChanged = false + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt index e86676b81f8eb..1759fb794bd6e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt @@ -19,9 +19,9 @@ package com.android.systemui.statusbar.phone import android.content.Context import android.content.res.Configuration import android.graphics.Rect -import android.test.suitebuilder.annotation.SmallTest import android.view.Display import android.view.DisplayCutout +import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.policy.ConfigurationController @@ -463,16 +463,10 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { val provider = StatusBarContentInsetsProvider(contextMock, configurationController, mock(DumpManager::class.java)) - givenDisplay( - screenBounds = Rect(0, 0, 1080, 2160), - displayUniqueId = "1" - ) + configuration.windowConfiguration.maxBounds = Rect(0, 0, 1080, 2160) val firstDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE) - givenDisplay( - screenBounds = Rect(0, 0, 800, 600), - displayUniqueId = "2" - ) - configurationController.onConfigurationChanged(configuration) + + configuration.windowConfiguration.maxBounds = Rect(0, 0, 800, 600) // WHEN: get insets on the second display val secondDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE) @@ -487,23 +481,15 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { // get insets and switch back val provider = StatusBarContentInsetsProvider(contextMock, configurationController, mock(DumpManager::class.java)) - givenDisplay( - screenBounds = Rect(0, 0, 1080, 2160), - displayUniqueId = "1" - ) + + configuration.windowConfiguration.maxBounds = Rect(0, 0, 1080, 2160) val firstDisplayInsetsFirstCall = provider .getStatusBarContentAreaForRotation(ROTATION_NONE) - givenDisplay( - screenBounds = Rect(0, 0, 800, 600), - displayUniqueId = "2" - ) - configurationController.onConfigurationChanged(configuration) + + configuration.windowConfiguration.maxBounds = Rect(0, 0, 800, 600) provider.getStatusBarContentAreaForRotation(ROTATION_NONE) - givenDisplay( - screenBounds = Rect(0, 0, 1080, 2160), - displayUniqueId = "1" - ) - configurationController.onConfigurationChanged(configuration) + + configuration.windowConfiguration.maxBounds = Rect(0, 0, 1080, 2160) // WHEN: get insets on the first display again val firstDisplayInsetsSecondCall = provider @@ -513,9 +499,70 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { assertThat(firstDisplayInsetsFirstCall).isEqualTo(firstDisplayInsetsSecondCall) } - private fun givenDisplay(screenBounds: Rect, displayUniqueId: String) { - `when`(display.uniqueId).thenReturn(displayUniqueId) - configuration.windowConfiguration.maxBounds = screenBounds + // Regression test for b/245799099 + @Test + fun onMaxBoundsChanged_listenerNotified() { + // Start out with an existing configuration with bounds + configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100) + configurationController.onConfigurationChanged(configuration) + val provider = StatusBarContentInsetsProvider(contextMock, configurationController, + mock(DumpManager::class.java)) + val listener = object : StatusBarContentInsetsChangedListener { + var triggered = false + + override fun onStatusBarContentInsetsChanged() { + triggered = true + } + } + provider.addCallback(listener) + + // WHEN the config is updated with new bounds + configuration.windowConfiguration.setMaxBounds(0, 0, 456, 789) + configurationController.onConfigurationChanged(configuration) + + // THEN the listener is notified + assertThat(listener.triggered).isTrue() + } + + @Test + fun onDensityOrFontScaleChanged_listenerNotified() { + configuration.densityDpi = 12 + val provider = StatusBarContentInsetsProvider(contextMock, configurationController, + mock(DumpManager::class.java)) + val listener = object : StatusBarContentInsetsChangedListener { + var triggered = false + + override fun onStatusBarContentInsetsChanged() { + triggered = true + } + } + provider.addCallback(listener) + + // WHEN the config is updated + configuration.densityDpi = 20 + configurationController.onConfigurationChanged(configuration) + + // THEN the listener is notified + assertThat(listener.triggered).isTrue() + } + + @Test + fun onThemeChanged_listenerNotified() { + val provider = StatusBarContentInsetsProvider(contextMock, configurationController, + mock(DumpManager::class.java)) + val listener = object : StatusBarContentInsetsChangedListener { + var triggered = false + + override fun onStatusBarContentInsetsChanged() { + triggered = true + } + } + provider.addCallback(listener) + + configurationController.notifyThemeChanged() + + // THEN the listener is notified + assertThat(listener.triggered).isTrue() } private fun assertRects(