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

Commit e01dcc1b authored by Nicolo' Mazzucato's avatar Nicolo' Mazzucato
Browse files

Make SysUiState for external display an override of the default one

SysUI state has several types of flags. Some of them are meant to be
device specific (e.g. SYSUI_STATE_AWAKE), and some of them display
specific (SYSUI_STATE_QUICK_SETTINGS_EXPANDED)

This cl leaves the default display SysUIState instance as it was, but it
 introduces the concept of "SysUIStateOverride" for external displays.

If a bit has been set in the override state, it will override the same
in the default display. All bits that have never been overridden wil
still have the value from the default display.

Bug: 362719719
Bug: 398011576
Test: SysUIStateOverrideTest
Flag: com.android.systemui.shade_window_goes_around
Change-Id: I3927a1fdd45afab4fd85b4bd9a8dc60e93c3ff3d
parent f49f7414
Loading
Loading
Loading
Loading
+105 −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.model

import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.dumpManager
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import org.junit.Before
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.Mockito.never
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.reset
import org.mockito.kotlin.verify

@SmallTest
@RunWith(AndroidJUnit4::class)
class SysUIStateOverrideTest : SysuiTestCase() {
    private val kosmos = testKosmos()

    private val defaultState = kosmos.sysUiState
    private val callbackOnOverride = mock<SysUiState.SysUiStateCallback>()
    private val dumpManager = kosmos.dumpManager

    private val underTest = kosmos.sysUiStateOverrideFactory.invoke(DISPLAY_1)

    @Before
    fun setup() {
        underTest.start()
        underTest.addCallback(callbackOnOverride)
        reset(callbackOnOverride)
    }

    @Test
    fun setFlag_setOnDefaultState_propagatedToOverride() {
        defaultState.setFlag(FLAG_1, true).commitUpdate()

        verify(callbackOnOverride).onSystemUiStateChanged(FLAG_1, Display.DEFAULT_DISPLAY)
        verify(callbackOnOverride).onSystemUiStateChanged(FLAG_1, DISPLAY_1)
    }

    @Test
    fun setFlag_onOverride_overridesDefaultOnes() {
        defaultState.setFlag(FLAG_1, false).setFlag(FLAG_2, true).commitUpdate()
        underTest.setFlag(FLAG_1, true).setFlag(FLAG_2, false).commitUpdate()

        assertThat(underTest.isFlagEnabled(FLAG_1)).isTrue()
        assertThat(underTest.isFlagEnabled(FLAG_2)).isFalse()

        assertThat(defaultState.isFlagEnabled(FLAG_1)).isFalse()
        assertThat(defaultState.isFlagEnabled(FLAG_2)).isTrue()
    }

    @Test
    fun destroy_callbacksForDefaultStateNotReceivedAnymore() {
        defaultState.setFlag(FLAG_1, true).commitUpdate()

        verify(callbackOnOverride).onSystemUiStateChanged(FLAG_1, Display.DEFAULT_DISPLAY)

        reset(callbackOnOverride)
        underTest.destroy()
        defaultState.setFlag(FLAG_1, false).commitUpdate()

        verify(callbackOnOverride, never()).onSystemUiStateChanged(FLAG_1, Display.DEFAULT_DISPLAY)
    }

    @Test
    fun init_registersWithDumpManager() {
        verify(dumpManager).registerNormalDumpable(any(), eq(underTest))
    }

    @Test
    fun destroy_unregistersWithDumpManager() {
        underTest.destroy()

        verify(dumpManager).unregisterDumpable(ArgumentMatchers.anyString())
    }

    private companion object {
        const val DISPLAY_1 = 1
        const val FLAG_1 = 1L
        const val FLAG_2 = 2L
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -140,6 +140,8 @@ public class SysUiStateTest extends SysuiTestCase {

    @Test
    public void init_registersWithDumpManager() {
        mFlagsContainer.start();

        verify(mDumpManager).registerNormalDumpable(any(), eq(mFlagsContainer));
    }

+2 −0
Original line number Diff line number Diff line
@@ -130,6 +130,7 @@ constructor(
        displayRepository.displayIds.collectLatest { displayIds ->
            val toRemove = perDisplayInstances.keys - displayIds
            toRemove.forEach { displayId ->
                Log.d(TAG, "<$debugName> destroying instance for displayId=$displayId.")
                perDisplayInstances.remove(displayId)?.let { instance ->
                    (instanceProvider as? PerDisplayInstanceProviderWithTeardown)?.destroyInstance(
                        instance
@@ -147,6 +148,7 @@ constructor(

        // If it doesn't exist, create it and put it in the map.
        return perDisplayInstances.computeIfAbsent(displayId) { key ->
            Log.d(TAG, "<$debugName> creating instance for displayId=$key, as it wasn't available.")
            val instance =
                traceSection({ "creating instance of $debugName for displayId=$key" }) {
                    instanceProvider.createInstance(key)
+83 −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.model

import android.view.Display
import com.android.systemui.dump.DumpManager
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

/**
 * This class is used to provide per-display overrides for only certain flags.
 *
 * While some of the [SystemUiStateFlags] are per display (e.g. shade expansion, dialog visible),
 * some of them are device specific (e.g. whether it's awake or not). A [SysUIStateOverride] is
 * created for each display that is not [Display.DEFAULT_DISPLAY], and if some flags are set on it,
 * they will override whatever the default display state had in those.
 */
class SysUIStateOverride
@AssistedInject
constructor(
    @Assisted override val displayId: Int,
    private val sceneContainerPlugin: SceneContainerPlugin?,
    dumpManager: DumpManager,
    private val defaultDisplayState: SysUiState,
    private val stateDispatcher: SysUIStateDispatcher,
) : SysUiStateImpl(displayId, sceneContainerPlugin, dumpManager, stateDispatcher) {

    private val override = StateChange()
    private var lastSentFlags = defaultDisplayState.flags

    private val defaultFlagsChangedCallback = { _: Long, otherDisplayId: Int ->
        if (otherDisplayId == Display.DEFAULT_DISPLAY) {
            commitUpdate()
        }
    }

    override fun start() {
        super.start()
        stateDispatcher.registerListener(defaultFlagsChangedCallback)
    }

    override fun destroy() {
        super.destroy()
        stateDispatcher.unregisterListener(defaultFlagsChangedCallback)
    }

    override fun commitUpdate() {
        if (flags != lastSentFlags) {
            stateDispatcher.dispatchSysUIStateChange(flags, displayId)
            lastSentFlags = flags
        }
    }

    override val flags: Long
        get() = override.applyTo(defaultDisplayState.flags)

    override fun setFlag(@SystemUiStateFlags flag: Long, enabled: Boolean): SysUiState {
        val toSet = flagWithOptionalOverrides(flag, enabled, displayId, sceneContainerPlugin)
        override.setFlag(flag, toSet)
        return this
    }

    @AssistedFactory
    interface Factory {
        fun create(displayId: Int): SysUIStateOverride
    }
}
+20 −6
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@
package com.android.systemui.model

import android.util.Log
import android.view.Display
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.display.data.repository.PerDisplayInstanceProviderWithTeardown
@@ -74,6 +75,9 @@ interface SysUiState : Dumpable {
     */
    fun destroy()

    /** Initializes the state after construction. */
    fun start()

    /** The display ID this instances is associated with */
    val displayId: Int

@@ -84,7 +88,7 @@ interface SysUiState : Dumpable {

private const val TAG = "SysUIState"

class SysUiStateImpl
open class SysUiStateImpl
@AssistedInject
constructor(
    @Assisted override val displayId: Int,
@@ -93,9 +97,10 @@ constructor(
    private val stateDispatcher: SysUIStateDispatcher,
) : SysUiState {

    private val debugName = "SysUiStateImpl-ForDisplay=$displayId"
    private val debugName
        get() = "SysUiStateImpl-ForDisplay=$displayId"

    init {
    override fun start() {
        dumpManager.registerNormalDumpable(debugName, this)
    }

@@ -222,10 +227,19 @@ fun flagWithOptionalOverrides(

/** Creates and destroy instances of [SysUiState] */
@SysUISingleton
class SysUIStateInstanceProvider @Inject constructor(private val factory: SysUiStateImpl.Factory) :
    PerDisplayInstanceProviderWithTeardown<SysUiState> {
class SysUIStateInstanceProvider
@Inject
constructor(
    private val factory: SysUiStateImpl.Factory,
    private val overrideFactory: SysUIStateOverride.Factory,
) : PerDisplayInstanceProviderWithTeardown<SysUiState> {
    override fun createInstance(displayId: Int): SysUiState {
        return factory.create(displayId)
        return if (displayId == Display.DEFAULT_DISPLAY) {
                factory.create(displayId)
            } else {
                overrideFactory.create(displayId)
            }
            .apply { start() }
    }

    override fun destroyInstance(instance: SysUiState) {
Loading