Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 367d0f9e authored by Nicolò Mazzucato's avatar Nicolò Mazzucato Committed by Android (Google) Code Review
Browse files

Merge "Provide a configuration controller for each display and use it for...

Merge "Provide a configuration controller for each display and use it for DisplayStateRepository (isWideScreen)" into main
parents 05f50a97 28270e75
Loading
Loading
Loading
Loading
+99 −42
Original line number Diff line number Diff line
@@ -17,6 +17,9 @@
package com.android.systemui.display.data.repository

import android.content.res.Configuration
import android.content.testableContext
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.util.DisplayMetrics
import android.util.Size
import android.view.Display
@@ -25,65 +28,54 @@ import android.view.DisplayInfo
import android.view.Surface
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState
import com.android.systemui.display.shared.model.DisplayRotation
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.spy

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class DisplayStateRepositoryImplTest : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val display = mock<Display>()
    private val testScope = TestScope(StandardTestDispatcher())
    private val fakeDeviceStateRepository = FakeDeviceStateRepository()
    private val fakeDisplayRepository = FakeDisplayRepository()
    private val configuration = Configuration()
    private val context = kosmos.testableContext

    private lateinit var underTest: DisplayStateRepository
    private val underTest by lazy { kosmos.realDisplayStateRepository }

    @Before
    fun setUp() {
        mContext.orCreateTestableResources.addOverride(
            com.android.internal.R.bool.config_reverseDefaultRotation,
            false,
        )
        context.display = display
        context.orCreateTestableResources.apply {
            addOverride(com.android.internal.R.bool.config_reverseDefaultRotation, false)
            overrideConfiguration(configuration)
        }

        // Set densityDpi such that pixels and DP are the same; Makes it easier to read and write
        // tests.
        configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT

        mContext = spy(mContext)
        whenever(mContext.display).thenReturn(display)
        whenever(mContext.resources.configuration).thenReturn(configuration)

        underTest =
            DisplayStateRepositoryImpl(
                testScope.backgroundScope,
                mContext,
                fakeDeviceStateRepository,
                fakeDisplayRepository,
            )
    }

    @Test
    fun updatesIsInRearDisplayMode_whenRearDisplayStateChanges() =
        testScope.runTest {
        kosmos.runTest {
            val isInRearDisplayMode by collectLastValue(underTest.isInRearDisplayMode)
            runCurrent()

            fakeDeviceStateRepository.emit(DeviceState.FOLDED)
            assertThat(isInRearDisplayMode).isFalse()
@@ -94,9 +86,8 @@ class DisplayStateRepositoryImplTest : SysuiTestCase() {

    @Test
    fun updatesCurrentRotation_whenDisplayStateChanges() =
        testScope.runTest {
        kosmos.runTest {
            val currentRotation by collectLastValue(underTest.currentRotation)
            runCurrent()

            whenever(display.getDisplayInfo(any())).then {
                val info = it.getArgument<DisplayInfo>(0)
@@ -104,7 +95,7 @@ class DisplayStateRepositoryImplTest : SysuiTestCase() {
                return@then true
            }

            fakeDisplayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            displayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            assertThat(currentRotation).isEqualTo(DisplayRotation.ROTATION_90)

            whenever(display.getDisplayInfo(any())).then {
@@ -113,15 +104,14 @@ class DisplayStateRepositoryImplTest : SysuiTestCase() {
                return@then true
            }

            fakeDisplayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            displayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            assertThat(currentRotation).isEqualTo(DisplayRotation.ROTATION_180)
        }

    @Test
    fun updatesCurrentSize_whenDisplayStateChanges() =
        testScope.runTest {
        kosmos.runTest {
            val currentSize by collectLastValue(underTest.currentDisplaySize)
            runCurrent()

            whenever(display.getDisplayInfo(any())).then {
                val info = it.getArgument<DisplayInfo>(0)
@@ -130,7 +120,7 @@ class DisplayStateRepositoryImplTest : SysuiTestCase() {
                info.logicalHeight = 200
                return@then true
            }
            fakeDisplayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            displayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            assertThat(currentSize).isEqualTo(Size(100, 200))

            whenever(display.getDisplayInfo(any())).then {
@@ -140,15 +130,15 @@ class DisplayStateRepositoryImplTest : SysuiTestCase() {
                info.logicalHeight = 200
                return@then true
            }
            fakeDisplayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            displayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            assertThat(currentSize).isEqualTo(Size(200, 100))
        }

    @Test
    @DisableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
    fun updatesIsLargeScreen_whenDisplayStateChanges() =
        testScope.runTest {
        kosmos.runTest {
            val isLargeScreen by collectLastValue(underTest.isLargeScreen)
            runCurrent()

            whenever(display.getDisplayInfo(any())).then {
                val info = it.getArgument<DisplayInfo>(0)
@@ -157,7 +147,7 @@ class DisplayStateRepositoryImplTest : SysuiTestCase() {
                info.logicalHeight = 700
                return@then true
            }
            fakeDisplayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            displayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            assertThat(isLargeScreen).isFalse()

            whenever(display.getDisplayInfo(any())).then {
@@ -167,15 +157,15 @@ class DisplayStateRepositoryImplTest : SysuiTestCase() {
                info.logicalHeight = 700
                return@then true
            }
            fakeDisplayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            displayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            assertThat(isLargeScreen).isTrue()
        }

    @Test
    @DisableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
    fun updatesIsWideScreen_whenDisplayStateChanges() =
        testScope.runTest {
        kosmos.runTest {
            val isWideScreen by collectLastValue(underTest.isWideScreen)
            runCurrent()

            whenever(display.getDisplayInfo(any())).then {
                val info = it.getArgument<DisplayInfo>(0)
@@ -184,7 +174,7 @@ class DisplayStateRepositoryImplTest : SysuiTestCase() {
                info.logicalHeight = 700
                return@then true
            }
            fakeDisplayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            displayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            assertThat(isWideScreen).isFalse()

            whenever(display.getDisplayInfo(any())).then {
@@ -194,7 +184,74 @@ class DisplayStateRepositoryImplTest : SysuiTestCase() {
                info.logicalHeight = 200
                return@then true
            }
            fakeDisplayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            displayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            assertThat(isWideScreen).isTrue()
        }

    @Test
    @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
    fun currentRotation_anotherDisplaychanged_noChange() =
        kosmos.runTest {
            val currentRotation by collectLastValue(underTest.currentRotation)

            whenever(display.getDisplayInfo(any())).then {
                val info = it.getArgument<DisplayInfo>(0)
                info.rotation = Surface.ROTATION_90
                return@then true
            }

            displayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
            assertThat(currentRotation).isEqualTo(DisplayRotation.ROTATION_90)

            whenever(display.getDisplayInfo(any())).then {
                val info = it.getArgument<DisplayInfo>(0)
                info.rotation = Surface.ROTATION_180
                return@then true
            }

            displayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY + 1)
            // Still the previous one!
            assertThat(currentRotation).isEqualTo(DisplayRotation.ROTATION_90)
        }

    @Test
    @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
    fun isWideScreen_fromConfiguration() =
        kosmos.runTest {
            val isWideScreen by collectLastValue(underTest.isWideScreen)

            val smallScreenConfig = Configuration().apply { screenWidthDp = SMALL_SCREEN_WIDTH_DP }
            kosmos.fakeConfigurationRepository.onConfigurationChange(smallScreenConfig)

            assertThat(isWideScreen).isFalse()

            val wideScreenConfig = Configuration().apply { screenWidthDp = LARGE_SCREEN_WIDTH_DP }
            kosmos.fakeConfigurationRepository.onConfigurationChange(wideScreenConfig)

            assertThat(isWideScreen).isTrue()
        }

    @Test
    @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
    fun isLargeScreen_fromConfiguration() =
        kosmos.runTest {
            val isLargeScreen by collectLastValue(underTest.isLargeScreen)

            val smallScreenConfig =
                Configuration().apply { smallestScreenWidthDp = SMALL_SCREEN_WIDTH_DP }
            kosmos.fakeConfigurationRepository.onConfigurationChange(smallScreenConfig)

            assertThat(isLargeScreen).isFalse()

            val wideScreenConfig =
                Configuration().apply { smallestScreenWidthDp = LARGE_SCREEN_WIDTH_DP }
            kosmos.fakeConfigurationRepository.onConfigurationChange(wideScreenConfig)

            assertThat(isLargeScreen).isTrue()
        }

    private companion object {
        const val SMALL_SCREEN_WIDTH_DP = 1
        const val LARGE_SCREEN_WIDTH_DP = 1000000
    }
}
+87 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.common.ui

import android.content.ComponentCallbacks
import android.content.res.Configuration
import android.window.WindowContext
import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
import com.android.systemui.statusbar.phone.ConfigurationForwarder
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.ConfigurationControllerDelegate
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

/**
 * Simple proxy class that acts as a [ConfigurationController] using [ComponentCallbacks] for a
 * [WindowContext].
 *
 * This is usually needed if we want to receive configuration changes associated to a specific
 * window.
 */
interface WindowContextConfigurationController : ConfigurationController {
    /** Starts listening and propagating config changes. */
    fun start()

    /** Stops listening and propagating config changes. */
    fun stop()
}

class WindowContextConfigurationControllerImpl
@AssistedInject
constructor(
    @Assisted private val windowContext: WindowContext,
    @Assisted private val delegate: ConfigurationControllerDelegate,
    configurationControllerFactory: ConfigurationControllerImpl.Factory,
) : WindowContextConfigurationController, ConfigurationController by delegate {

    init {
        delegate.setDelegate(configurationControllerFactory.create(windowContext))
    }

    private val configurationForwarder: ConfigurationForwarder = delegate

    private val componentCallback =
        object : ComponentCallbacks {
            override fun onConfigurationChanged(newConfig: Configuration) {
                configurationForwarder.onConfigurationChanged(newConfig)
            }

            @Deprecated("See ComponentCallbacks") override fun onLowMemory() {}
        }

    override fun start() {
        windowContext.registerComponentCallbacks(componentCallback)
    }

    override fun stop() {
        windowContext.unregisterComponentCallbacks(componentCallback)
    }

    @AssistedFactory
    interface Factory {
        /**
         * Creates a [com.android.systemui.common.ui.WindowContextConfigurationControllerImpl] that
         * gets config changes using [WindowContext.registerComponentCallbacks].
         */
        fun create(
            windowContext: WindowContext,
            delegate: ConfigurationControllerDelegate = ConfigurationControllerDelegate(),
        ): WindowContextConfigurationControllerImpl
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -38,7 +38,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope

/** Module providing common dependencies for per-display singletons. */
@Module(includes = [StatusBarPerDisplayModule::class])
@Module(includes = [StatusBarPerDisplayModule::class, PerDisplayConfigurationModule::class])
interface PerDisplayCommonModule {

    @Multibinds
+164 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.display.dagger

import android.view.Display
import android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR
import android.window.WindowContext
import com.android.systemui.common.ui.WindowContextConfigurationController
import com.android.systemui.common.ui.WindowContextConfigurationControllerImpl
import com.android.systemui.common.ui.data.repository.ConfigurationRepository
import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractorImpl
import com.android.systemui.display.dagger.SystemUIDisplaySubcomponent.DisplayAware
import com.android.systemui.display.dagger.SystemUIDisplaySubcomponent.DisplayId
import com.android.systemui.display.dagger.SystemUIDisplaySubcomponent.PerDisplaySingleton
import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository
import com.android.systemui.statusbar.policy.ConfigurationController
import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet

/**
 * Additional module to provide a [ConfigurationController] related to a specific display.
 *
 * While this is not tied to any UI object, it but uses a [TYPE_STATUS_BAR] window context type, and
 * gets the configuration using [android.content.ComponentCallbacks].
 *
 * The [WindowContext] bound here is from [DisplayWindowPropertiesRepository], and it's essentially
 * the same as the one used by the status bar (so, no new context creation happen as part of this).
 *
 * Note: for the default display it just binds the global configuration objects with the
 * `@DisplayAware` annotation, to keep the same behaviour as before and avoiding creating new
 * instances.
 */
@Module
class PerDisplayConfigurationModule {

    @Provides
    @PerDisplaySingleton
    @DisplayAware
    fun provideStatusBarWindowContext(
        @DisplayId displayId: Int,
        displayPropertiesRepository: DisplayWindowPropertiesRepository,
    ): WindowContext {
        if (displayId == Display.DEFAULT_DISPLAY) {
            error(
                """If you're receiving this error it either means something in
                    | PerDisplayConfigurationModule is wrong, or that you're injecting a
                    | @DisplayAware window context in a class used by the default display. This is
                    | not possible as the statusbar window context is used for this binding, but for
                    | the default display we're not creating a new window context."""
                    .trimMargin()
            )
        }
        return displayPropertiesRepository.get(displayId, TYPE_STATUS_BAR)?.context
            as? WindowContext
            ?: error(
                """Unable to cast status bar context to WindowContext. I
                    |f the statusbar is not using WindowContext, this will not work and you should
                    | remove PerDisplayConfigurationModule from your dagger graph and any dependency
                    | on its classes."""
                    .trimMargin()
            )
    }

    @Provides
    @PerDisplaySingleton
    @DisplayAware
    fun provideWindowContextDisplayConfigurationController(
        @DisplayAware statusbarWindowContext: WindowContext,
        windowContextConfigurationController: WindowContextConfigurationControllerImpl.Factory,
    ): WindowContextConfigurationController =
        windowContextConfigurationController.create(statusbarWindowContext)

    @Provides
    @PerDisplaySingleton
    @DisplayAware
    fun provideDisplayConfigurationController(
        @DisplayAware displayConfigurationController: Lazy<WindowContextConfigurationController>,
        globalConfigController: ConfigurationController,
        @DisplayAware displayId: Int,
    ): ConfigurationController {
        // We should remove this condition and just create also the instance for the default display
        // in the same way. This is not possible right now as we're not using a WindowContext for
        // the default display statusbar.
        return if (displayId == Display.DEFAULT_DISPLAY) {
            globalConfigController
        } else {
            displayConfigurationController.get()
        }
    }

    /**
     * The lifecycle listener is only needed if we're on an external display, as we can assume the
     * default display will always be there.
     */
    @Provides
    @PerDisplaySingleton
    @DisplayAware
    @IntoSet
    fun provideDisplayWindowContextConfigurationControllerLifecycleObserver(
        @DisplayAware displayConfigurationController: Lazy<WindowContextConfigurationController>,
        @DisplayAware displayId: Int,
    ): SystemUIDisplaySubcomponent.LifecycleListener =
        object : SystemUIDisplaySubcomponent.LifecycleListener {
            override fun start() {
                if (displayId != Display.DEFAULT_DISPLAY) {
                    displayConfigurationController.get().start()
                }
            }

            override fun stop() {
                if (displayId != Display.DEFAULT_DISPLAY) {
                    displayConfigurationController.get().stop()
                }
            }
        }

    @Provides
    @PerDisplaySingleton
    @DisplayAware
    fun provideConfigurationRepository(
        @DisplayAware configurationController: Lazy<ConfigurationController>,
        @DisplayAware context: Lazy<WindowContext>,
        configurationRepositoryFactory: ConfigurationRepositoryImpl.Factory,
        @DisplayAware displayId: Int,
        globalConfigurationRepository: ConfigurationRepository,
    ): ConfigurationRepository =
        if (displayId == Display.DEFAULT_DISPLAY) {
            globalConfigurationRepository
        } else {
            configurationRepositoryFactory.create(context.get(), configurationController.get())
        }

    @Provides
    @PerDisplaySingleton
    @DisplayAware
    fun provideConfigurationInteractor(
        @DisplayAware configurationRepository: Lazy<ConfigurationRepository>,
        @DisplayAware displayId: Int,
        globalConfigurationInteractor: ConfigurationInteractor,
    ): ConfigurationInteractor =
        if (displayId == Display.DEFAULT_DISPLAY) {
            globalConfigurationInteractor
        } else {
            ConfigurationInteractorImpl(configurationRepository.get())
        }
}
+26 −10
Original line number Diff line number Diff line
@@ -20,16 +20,19 @@ import android.content.Context
import android.util.DisplayMetrics
import android.util.Size
import android.view.DisplayInfo
import com.android.systemui.common.ui.data.repository.ConfigurationRepository
import com.android.systemui.display.dagger.SystemUIDisplaySubcomponent.DisplayAware
import com.android.systemui.display.dagger.SystemUIDisplaySubcomponent.PerDisplaySingleton
import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState.REAR_DISPLAY
import com.android.systemui.display.shared.model.DisplayRotation
import com.android.systemui.display.shared.model.toDisplayRotation
import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import javax.inject.Inject
import kotlin.math.min
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

@@ -74,6 +77,7 @@ class DisplayStateRepositoryImpl
constructor(
    @DisplayAware bgDisplayScope: CoroutineScope,
    @DisplayAware val context: Context,
    @DisplayAware val configurationRepository: ConfigurationRepository,
    deviceStateRepository: DeviceStateRepository,
    displayRepository: DisplayRepository,
) : DisplayStateRepository {
@@ -86,7 +90,11 @@ constructor(
            .stateIn(bgDisplayScope, started = SharingStarted.Eagerly, initialValue = false)

    private val currentDisplayInfo: StateFlow<DisplayInfo> =
        if (ShadeWindowGoesAround.isEnabled) {
                displayRepository.displayChangeEvent.filter { it == context.displayId }
            } else {
                displayRepository.displayChangeEvent
            }
            .map { getDisplayInfo() }
            .stateIn(
                bgDisplayScope,
@@ -116,20 +124,28 @@ constructor(
                    ),
            )

    // TODO: b/417956803 - This should use the configuration instead
    override val isLargeScreen: StateFlow<Boolean> =
        currentDisplayInfo
            .map {
        if (ShadeWindowGoesAround.isEnabled) {
                configurationRepository.configurationValues.map {
                    it.smallestScreenWidthDp >= LARGE_SCREEN_MIN_DPS
                }
            } else {
                currentDisplayInfo.map {
                    // copied from systemui/shared/...Utilities.java
                    val smallestWidth = min(it.logicalWidth, it.logicalHeight).toDpi()
                    smallestWidth >= LARGE_SCREEN_MIN_DPS
                }
            }
            .stateIn(bgDisplayScope, started = SharingStarted.Eagerly, initialValue = false)

    // TODO: b/417956803 - This should use the configuration instead
    override val isWideScreen: StateFlow<Boolean> =
        currentDisplayInfo
            .map { it.logicalWidth.toDpi() >= LARGE_SCREEN_MIN_DPS }
        if (ShadeWindowGoesAround.isEnabled) {
                configurationRepository.configurationValues.map {
                    it.screenWidthDp >= LARGE_SCREEN_MIN_DPS
                }
            } else {
                currentDisplayInfo.map { it.logicalWidth.toDpi() >= LARGE_SCREEN_MIN_DPS }
            }
            .stateIn(bgDisplayScope, started = SharingStarted.Eagerly, initialValue = false)

    private fun getDisplayInfo(): DisplayInfo {
Loading