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

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

Channel SysUI state updates through SysUIStateDispatcher

After this change, any listener registered to any SysUIState instance will receive updates for all display IDs.

The logic to handle callbacks is removed from SysUIState and moved to the dispatcher (that is also thread safe now).

This is needed as various parts of the code will change the state for different displays, and we want each listener to get events for all displays.

In a follow up it's possible to entirely remove callback registration logic from SysUIState and have it only from a repository, but it was a bigger refactor, and I prefer to keep this cl small.

Bug: 362719719
Bug: 398011576
Test: SysUIStateDispatcherTest
Flag: com.android.systemui.shade_window_goes_around
Change-Id: I465b207a3d2ab28d7cd66f09e98c013160532d1d
parent dcc945ed
Loading
Loading
Loading
Loading
+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.model

import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.view.Display
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.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class SysUIStateDispatcherTest : SysuiTestCase() {

    private val kosmos = testKosmos()

    private val stateFactory = kosmos.sysUiStateFactory
    private val state0 = stateFactory.create(Display.DEFAULT_DISPLAY)
    private val state1 = stateFactory.create(DISPLAY_1)
    private val state2 = stateFactory.create(DISPLAY_2)
    private val underTest = kosmos.sysUIStateDispatcher

    private val flagsChanges = mutableMapOf<Int, Long>() // display id -> flag value
    private val callback =
        SysUiState.SysUiStateCallback { sysUiFlags, displayId ->
            flagsChanges[displayId] = sysUiFlags
        }

    @Test
    @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
    fun registerUnregisterListener_notifiedOfChanges_receivedForAllDisplayIdsWithOneCallback() {
        underTest.registerListener(callback)

        state1.setFlag(FLAG_1, true).commitUpdate()
        state2.setFlag(FLAG_2, true).commitUpdate()

        assertThat(flagsChanges).containsExactly(DISPLAY_1, FLAG_1, DISPLAY_2, FLAG_2)

        underTest.unregisterListener(callback)

        state1.setFlag(0, true).commitUpdate()

        // Didn't change
        assertThat(flagsChanges).containsExactly(DISPLAY_1, FLAG_1, DISPLAY_2, FLAG_2)
    }

    @Test
    @DisableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
    fun registerUnregisterListener_notifiedOfChangesForNonDefaultDisplay_NotPropagated() {
        underTest.registerListener(callback)

        state1.setFlag(FLAG_1, true).commitUpdate()

        assertThat(flagsChanges).isEmpty()

        state0.setFlag(FLAG_1, true).commitUpdate()

        assertThat(flagsChanges).containsExactly(Display.DEFAULT_DISPLAY, FLAG_1)
    }

    private companion object {
        const val DISPLAY_1 = 1
        const val DISPLAY_2 = 2
        const val FLAG_1 = 10L
        const val FLAG_2 = 20L
    }
}
+4 −1
Original line number Diff line number Diff line
@@ -55,9 +55,11 @@ public class SysUiStateTest extends SysuiTestCase {
    private SysUiState mFlagsContainer;
    private SceneContainerPlugin mSceneContainerPlugin;
    private DumpManager mDumpManager;
    private SysUIStateDispatcher mSysUIStateDispatcher;

    private SysUiState createInstance(int displayId) {
        var sysuiState = new SysUiStateImpl(displayId, mSceneContainerPlugin, mDumpManager);
        var sysuiState = new SysUiStateImpl(displayId, mSceneContainerPlugin, mDumpManager,
                mSysUIStateDispatcher);
        sysuiState.addCallback(mCallback);
        return sysuiState;
    }
@@ -69,6 +71,7 @@ public class SysUiStateTest extends SysuiTestCase {
        mSceneContainerPlugin = mKosmos.getSceneContainerPlugin();
        mCallback = mock(SysUiState.SysUiStateCallback.class);
        mDumpManager = mock(DumpManager.class);
        mSysUIStateDispatcher = mKosmos.getSysUIStateDispatcher();
        mFlagsContainer = createInstance(DEFAULT_DISPLAY);
    }

+77 −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.dagger.SysUISingleton
import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject

/**
 * Channels changes from several [SysUiState]s to a single callback.
 *
 * There are several [SysUiState]s (one per display). This class allows for listeners to listen to
 * sysui state updates from any of those [SysUiState] instances.
 *
 *                      ┌────────────────────┐
 *                      │ SysUIStateOverride │
 *                      │ displayId=2        │
 *                      └┬───────────────────┘
 *                       │  ▲
 * ┌───────────────┐     │  │  ┌────────────────────┐
 * │ SysUIState    │     │  │  │ SysUIStateOverride │
 * │ displayId=0   │     │  │  │ displayId=1        │
 * └────────────┬──┘     │  │  └┬───────────────────┘
 *              │        │  │   │ ▲
 *              ▼        ▼  │   ▼ │
 *            ┌─────────────┴─────┴─┐
 *            │SysUiStateDispatcher │
 *            └────────┬────────────┘
 *                     │
 *                     ▼
 *             ┌──────────────────┐
 *             │ listeners for    │
 *             │ all displays     │
 *             └──────────────────┘
 */
@SysUISingleton
class SysUIStateDispatcher @Inject constructor() {

    private val listeners = CopyOnWriteArrayList<SysUiState.SysUiStateCallback>()

    /** Called from each [SysUiState] to propagate new state changes. */
    fun dispatchSysUIStateChange(sysUiFlags: Long, displayId: Int) {
        if (displayId != Display.DEFAULT_DISPLAY && !ShadeWindowGoesAround.isEnabled) return;
        listeners.forEach { listener ->
            listener.onSystemUiStateChanged(sysUiFlags = sysUiFlags, displayId = displayId)
        }
    }

    /**
     * Registers a listener to listen for system UI state changes.
     *
     * Listeners will have [SysUiState.SysUiStateCallback.onSystemUiStateChanged] called whenever a
     * system UI state changes.
     */
    fun registerListener(listener: SysUiState.SysUiStateCallback) {
        listeners += listener
    }

    fun unregisterListener(listener: SysUiState.SysUiStateCallback) {
        listeners -= listener
    }
}
+36 −31
Original line number Diff line number Diff line
@@ -43,7 +43,9 @@ interface SysUiState : Dumpable {
    fun removeCallback(callback: SysUiStateCallback)

    /** Returns whether a flag is enabled in this state. */
    fun isFlagEnabled(@SystemUiStateFlags flag: Long): Boolean
    fun isFlagEnabled(@SystemUiStateFlags flag: Long): Boolean {
        return (flags and flag) != 0L
    }

    /** Returns the current sysui state flags. */
    val flags: Long
@@ -58,11 +60,8 @@ interface SysUiState : Dumpable {
    /** Call to save all the flags updated from [setFlag]. */
    fun commitUpdate()

    /** Notify all those who are registered that the state has changed. */
    fun notifyAndSetSystemUiStateChanged(newFlags: Long, oldFlags: Long)

    /** Callback to be notified whenever system UI state flags are changed. */
    interface SysUiStateCallback {
    fun interface SysUiStateCallback {

        /** To be called when any SysUiStateFlag gets updated for a specific [displayId]. */
        fun onSystemUiStateChanged(@SystemUiStateFlags sysUiFlags: Long, displayId: Int)
@@ -88,6 +87,7 @@ constructor(
    @Assisted private val displayId: Int,
    private val sceneContainerPlugin: SceneContainerPlugin?,
    private val dumpManager: DumpManager,
    private val stateDispatcher: SysUIStateDispatcher,
) : SysUiState {

    private val debugName = "SysUiStateImpl-ForDisplay=$displayId"
@@ -103,45 +103,30 @@ constructor(
        get() = _flags

    private var _flags: Long = 0
    private val callbacks: MutableList<SysUiStateCallback> = ArrayList()
    private var flagsToSet: Long = 0
    private var flagsToClear: Long = 0

    /**
     * Add listener to be notified of changes made to SysUI state. The callback will also be called
     * as part of this function.
     *
     * Note that the listener would receive updates for all displays.
     */
    override fun addCallback(callback: SysUiStateCallback) {
        callbacks.add(callback)
        stateDispatcher.registerListener(callback)
        callback.onSystemUiStateChanged(flags, displayId)
    }

    /** Callback will no longer receive events on state change */
    override fun removeCallback(callback: SysUiStateCallback) {
        callbacks.remove(callback)
    }

    override fun isFlagEnabled(@SystemUiStateFlags flag: Long): Boolean {
        return (flags and flag) != 0L
        stateDispatcher.unregisterListener(callback)
    }

    /** Methods to this call can be chained together before calling [.commitUpdate]. */
    override fun setFlag(@SystemUiStateFlags flag: Long, enabled: Boolean): SysUiState {
        var enabled = enabled
        val overrideOrNull =
            sceneContainerPlugin?.flagValueOverride(flag = flag, displayId = displayId)
        if (overrideOrNull != null && enabled != overrideOrNull) {
            if (SysUiState.DEBUG) {
                Log.d(
                    TAG,
                    "setFlag for flag $flag and value $enabled overridden to $overrideOrNull by scene container plugin",
                )
            }
        val toSet = flagWithOptionalOverrides(flag, enabled, displayId, sceneContainerPlugin)

            enabled = overrideOrNull
        }

        if (enabled) {
        if (toSet) {
            flagsToSet = flagsToSet or flag
        } else {
            flagsToClear = flagsToClear or flag
@@ -172,16 +157,13 @@ constructor(
    }

    /** Notify all those who are registered that the state has changed. */
    override fun notifyAndSetSystemUiStateChanged(newFlags: Long, oldFlags: Long) {
    private fun notifyAndSetSystemUiStateChanged(newFlags: Long, oldFlags: Long) {
        if (SysUiState.DEBUG) {
            Log.d(TAG, "SysUiState changed: old=$oldFlags new=$newFlags")
        }
        if (newFlags != oldFlags) {
            callbacks.forEach { callback: SysUiStateCallback ->
                callback.onSystemUiStateChanged(newFlags, displayId)
            }

            _flags = newFlags
            stateDispatcher.dispatchSysUIStateChange(newFlags, displayId)
        }
    }

@@ -212,6 +194,29 @@ constructor(
    }
}

/** Returns the flag value taking into account [SceneContainerPlugin] potential overrides. */
fun flagWithOptionalOverrides(
    flag: Long,
    enabled: Boolean,
    displayId: Int,
    sceneContainerPlugin: SceneContainerPlugin?,
): Boolean {
    var toSet = enabled
    val overrideOrNull = sceneContainerPlugin?.flagValueOverride(flag = flag, displayId = displayId)
    if (overrideOrNull != null && toSet != overrideOrNull) {
        if (SysUiState.DEBUG) {
            Log.d(
                TAG,
                "setFlag for flag $flag and value $toSet overridden to " +
                    "$overrideOrNull by scene container plugin",
            )
        }

        toSet = overrideOrNull
    }
    return toSet
}

/** Creates and destroy instances of [SysUiState] */
@SysUISingleton
class SysUIStateInstanceProvider @Inject constructor(private val factory: SysUiStateImpl.Factory) :
+2 −0
Original line number Diff line number Diff line
@@ -52,6 +52,7 @@ import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor
import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel
import com.android.systemui.model.sceneContainerPlugin
import com.android.systemui.model.sysUIStateDispatcher
import com.android.systemui.model.sysUiState
import com.android.systemui.plugins.statusbar.statusBarStateController
import com.android.systemui.power.data.repository.fakePowerRepository
@@ -198,4 +199,5 @@ class KosmosJavaAdapter() {
    val windowRootViewBlurInteractor by lazy { kosmos.windowRootViewBlurInteractor }
    val sysuiState by lazy { kosmos.sysUiState }
    val displayTracker by lazy { kosmos.displayTracker }
    val sysUIStateDispatcher by lazy { kosmos.sysUIStateDispatcher }
}
Loading