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

Commit 7d55651b authored by Nicolo' Mazzucato's avatar Nicolo' Mazzucato
Browse files

Propagate Notification and QS SysUI flags per display

NPVC now propagates SysUIstate changes through SysUIStateDisplaysInteractor. This guarantees those flags are only on for a specific display, and off for all the others.

The case when SceneContainer flag is on has been handled already by one of the cls this is based on.

Bug: 362719719
Bug: 398011576
Test: SysUIStateDisplaysInteractorTest, NotificationPanelViewControllerTest
Flag: com.android.systemui.shade_window_goes_around
Change-Id: I0d1044718fae3933cc790d3bc4ca01dbb19ecad0
parent 21f53177
Loading
Loading
Loading
Loading
+112 −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.domain.interactor

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.model.fakeSysUIStatePerDisplayRepository
import com.android.systemui.model.sysUiStateFactory
import com.android.systemui.model.sysuiStateInteractor
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

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

    private val kosmos = testKosmos()

    val stateRepository = kosmos.fakeSysUIStatePerDisplayRepository
    val state0 = kosmos.sysUiStateFactory.create(0)
    val state1 = kosmos.sysUiStateFactory.create(1)
    val state2 = kosmos.sysUiStateFactory.create(2)

    val underTest = kosmos.sysuiStateInteractor

    @Before
    fun setup() {
        stateRepository.apply {
            add(0, state0)
            add(1, state1)
            add(2, state2)
        }
    }

    @Test
    fun setFlagsExclusivelyToDisplay_setsFlagsOnTargetStateAndClearsTheOthers() {
        val targetDisplayId = 0
        val stateChange = StateChange().setFlag(1L, true)

        underTest.setFlagsExclusivelyToDisplay(targetDisplayId, stateChange)

        assertThat(state0.isFlagEnabled(1)).isTrue()
        assertThat(state1.isFlagEnabled(1)).isFalse()
        assertThat(state2.isFlagEnabled(1)).isFalse()

        underTest.setFlagsExclusivelyToDisplay(1, stateChange)

        assertThat(state0.isFlagEnabled(1)).isFalse()
        assertThat(state1.isFlagEnabled(1)).isTrue()
        assertThat(state2.isFlagEnabled(1)).isFalse()

        underTest.setFlagsExclusivelyToDisplay(2, stateChange)

        assertThat(state0.isFlagEnabled(1)).isFalse()
        assertThat(state1.isFlagEnabled(1)).isFalse()
        assertThat(state2.isFlagEnabled(1)).isTrue()

        underTest.setFlagsExclusivelyToDisplay(3, stateChange)

        assertThat(state0.isFlagEnabled(1)).isFalse()
        assertThat(state1.isFlagEnabled(1)).isFalse()
        assertThat(state2.isFlagEnabled(1)).isFalse()
    }

    @Test
    fun setFlagsExclusivelyToDisplay_multipleFlags_setsFlagsOnTargetStateAndClearsTheOthers() {
        val stateChange = StateChange().setFlag(1L, true).setFlag(2L, true)

        underTest.setFlagsExclusivelyToDisplay(1, stateChange)

        assertThat(state0.isFlagEnabled(1)).isFalse()
        assertThat(state0.isFlagEnabled(2)).isFalse()
        assertThat(state1.isFlagEnabled(1)).isTrue()
        assertThat(state1.isFlagEnabled(2)).isTrue()
        assertThat(state2.isFlagEnabled(1)).isFalse()
        assertThat(state2.isFlagEnabled(1)).isFalse()
    }

    @Test
    fun setFlagsExclusivelyToDisplay_clearsFlags() {
        state0.setFlag(1, true).setFlag(2, true).commitUpdate()
        state1.setFlag(1, true).setFlag(2, true).commitUpdate()
        state2.setFlag(1, true).setFlag(2, true).commitUpdate()

        val stateChange = StateChange().setFlag(1L, false)

        underTest.setFlagsExclusivelyToDisplay(1, stateChange)

        // Sets it as false in display 1, but also the others.
        assertThat(state0.isFlagEnabled(1)).isFalse()
        assertThat(state1.isFlagEnabled(1)).isFalse()
        assertThat(state2.isFlagEnabled(1)).isFalse()
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -17,7 +17,9 @@
package com.android.systemui.shade;

import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.view.Display.TYPE_INTERNAL;

import static com.android.systemui.display.data.repository.FakeDisplayRepositoryKt.display;
import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;

import static com.google.common.truth.Truth.assertThat;
@@ -47,6 +49,7 @@ import android.os.Looper;
import android.os.PowerManager;
import android.os.UserManager;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
@@ -70,6 +73,7 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.common.domain.interactor.SysUIStateDisplaysInteractor;
import com.android.systemui.common.ui.view.TouchHandlingView;
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor;
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
@@ -291,6 +295,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
    @Mock private NaturalScrollingSettingObserver mNaturalScrollingSettingObserver;
    @Mock private LargeScreenHeaderHelper mLargeScreenHeaderHelper;
    @Mock private StatusBarLongPressGestureDetector mStatusBarLongPressGestureDetector;
    @Mock protected SysUIStateDisplaysInteractor mSysUIStateDisplaysInteractor;
    protected final int mMaxUdfpsBurnInOffsetY = 5;
    protected FakeFeatureFlagsClassic mFeatureFlags = new FakeFeatureFlagsClassic();
    protected KeyguardClockInteractor mKeyguardClockInteractor;
@@ -435,6 +440,9 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
            return null;
        }).when(mView).setOnTouchListener(any(NotificationPanelViewController.TouchHandler.class));

        var displayMock = display(TYPE_INTERNAL, /* flags= */ 0, /* id= */Display.DEFAULT_DISPLAY,
                /* state= */ null);
        when(mView.getDisplay()).thenReturn(displayMock);
        // Any edge transition
        when(mKeyguardTransitionInteractor.transition(any()))
                .thenReturn(emptyFlow());
@@ -565,6 +573,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
                mShadeRepository,
                mSysUIUnfoldComponent,
                mSysUiState,
                mSysUIStateDisplaysInteractor,
                mKeyguardUnlockAnimationController,
                mKeyguardIndicationController,
                mNotificationListContainer,
+31 −0
Original line number Diff line number Diff line
@@ -16,10 +16,16 @@

package com.android.systemui.shade;

import static android.view.Display.TYPE_INTERNAL;

import static com.android.systemui.display.data.repository.FakeDisplayRepositoryKt.display;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -28,6 +34,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.os.Build;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.testing.TestableLooper;
import android.view.HapticFeedbackConstants;
@@ -38,6 +45,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import com.android.systemui.DejankUtils;
import com.android.systemui.Flags;
import com.android.systemui.flags.DisableSceneContainer;

import com.google.android.msdl.data.model.MSDLToken;
@@ -182,4 +190,27 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo

        assertThat(mMSDLPlayer.getLatestTokenPlayed()).isEqualTo(MSDLToken.FAILURE);
    }

    @Test
    @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
    public void updateSystemUiStateFlags_updatesSysuiStateInteractor() {
        var DISPLAY_ID = 10;
        var displayMock = display(TYPE_INTERNAL, /* flags= */ 0, /* id= */DISPLAY_ID,
                /* state= */ null);
        when(mView.getDisplay()).thenReturn(displayMock);

        mNotificationPanelViewController.updateSystemUiStateFlags();

        verify(mSysUIStateDisplaysInteractor).setFlagsExclusivelyToDisplay(eq(DISPLAY_ID), any());
    }

    @Test
    @DisableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
    public void updateSystemUiStateFlags_flagOff_doesNotUpdateSysuiStateInteractor() {
        mNotificationPanelViewController.updateSystemUiStateFlags();

        verify(mSysUIStateDisplaysInteractor, never()).setFlagsExclusivelyToDisplay(anyInt(),
                any());
    }

}
+82 −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.domain.interactor

import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.display.data.repository.PerDisplayRepository
import com.android.systemui.model.SysUiState
import javax.inject.Inject

/** Handles [SysUiState] changes between displays. */
@SysUISingleton
class SysUIStateDisplaysInteractor
@Inject
constructor(private val sysUIStateRepository: PerDisplayRepository<SysUiState>) {

    /**
     * Sets the flags on the given [targetDisplayId] based on the [stateChanges], while making sure
     * that those flags are not set in any other display.
     */
    fun setFlagsExclusivelyToDisplay(targetDisplayId: Int, stateChanges: StateChange) {
        sysUIStateRepository.forEachInstance { displayId, instance ->
            if (displayId == targetDisplayId) {
                stateChanges.applyTo(instance)
            } else {
                stateChanges.clearAllChangedFlagsIn(instance)
            }
        }
    }
}

/**
 * Represents a set of state changes. A bit can either be set to `true` or `false`.
 *
 * This is used in [SysUIStateDisplaysInteractor] to selectively change bits.
 */
class StateChange {
    private val changes = mutableMapOf<Long, Boolean>()

    /**
     * Sets the [state] of the given [bit].
     *
     * @return `this` for chaining purposes
     */
    fun setFlag(bit: Long, state: Boolean): StateChange {
        changes[bit] = state
        return this
    }

    /**
     * Gets the value of a given [bit] or false if not present.
     *
     * @param bit the bit to query
     * @return the value of the bit or false if not present.
     */
    fun get(bit: Long): Boolean = changes[bit] ?: false

    /** Applies all changed flags to [sysUiState]. */
    fun applyTo(sysUiState: SysUiState) {
        changes.forEach { (bit, state) -> sysUiState.setFlag(bit, state) }
        sysUiState.commitUpdate()
    }

    /** Clears all the flags changed in a [sysUiState] */
    fun clearAllChangedFlagsIn(sysUiState: SysUiState) {
        changes.forEach { (bit, _) -> sysUiState.setFlag(bit, false) }
        sysUiState.commitUpdate()
    }
}
+40 −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.data.repository

class FakePerDisplayRepository<T> : PerDisplayRepository<T> {

    private val instances = mutableMapOf<Int, T>()

    fun add(displayId: Int, instance: T) {
        instances[displayId] = instance
    }

    fun remove(displayId: Int) {
        instances.remove(displayId)
    }

    override fun get(displayId: Int): T? {
        return instances[displayId]
    }

    override val displayIds: Set<Int>
        get() = instances.keys

    override val debugName: String
        get() = "FakePerDisplayRepository"
}
Loading