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

Commit 882f4b5c authored by Johannes Gallmann's avatar Johannes Gallmann
Browse files

Extract top-ui logic into TopUiController

Currently, NotificationShadeWindowController acts as the central entity
to handle top-ui requests. This CL extracts that logic into a standalone
TopUiController. Components that need to request top-ui do not have to
inject NotificationShadeWindowController anymore in the future, but can
directly use TopUiController instead.

As a first step, TopUiController is introduced behind a refactor flag.
For now, all components must check the flag value to decide where to
request top-ui. If the refactor flag is enabled, the request needs to be
sent to TopUiController. Otherwise the request needs to be sent to
NotificationShadeWindowController.

Once the flag reaches nextfood, all components using top-ui can be
refactored to use TopUiController only.

Bug: 411061512
Test: TopUiControllerImplTest
Flag: com.android.systemui.enable_top_ui_controller
Change-Id: I7113ad66275f2da5d350a5829f91ce5deec66f4e
parent 125368e1
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -2080,6 +2080,16 @@ flag {
    bug: "411452237"
}

flag {
    name: "enable_top_ui_controller"
    namespace: "systemui"
    description: "Extracts top ui requests into TopUiController"
    bug: "411061512"
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}

flag {
    name: "qs_tile_transition_interaction_refinement"
    namespace: "systemui"
+2 −1
Original line number Diff line number Diff line
@@ -177,7 +177,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase {
                mUserTracker,
                mKosmos.getNotificationShadeWindowModel(),
                mKosmos::getCommunalInteractor,
                mKosmos.getShadeLayoutParams());
                mKosmos.getShadeLayoutParams(),
                mKosmos.getTopUiController());
        mNotificationShadeWindowController.setScrimsVisibilityListener((visibility) -> {});
        mNotificationShadeWindowController.fetchWindowRootView();

+195 −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.topui

import android.app.activityManagerInterface
import android.platform.test.annotations.EnableFlags
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.dump.dumpManager
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.testKosmos
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions

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

    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val iActivityManager = kosmos.activityManagerInterface

    private lateinit var underTest: TopUiControllerImpl

    @Before
    fun setUp() {
        underTest = TopUiControllerImpl(iActivityManager, kosmos.testDispatcher, kosmos.dumpManager)
    }

    @Test
    fun initialState_noActivityManagerCall() =
        kosmos.runTest {
            // No calls should have happened during initialization
            verifyNoMoreInteractions(activityManagerInterface)
        }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_TOP_UI_CONTROLLER)
    fun firstRequest_setsHasTopUiTrue() =
        kosmos.runTest {
            underTest.setRequestTopUi(true, "tag1")

            // Verify setHasTopUi(true) was called exactly once
            verify(activityManagerInterface, times(1)).setHasTopUi(true)
            verifyNoMoreInteractions(activityManagerInterface) // Ensure no other calls
        }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_TOP_UI_CONTROLLER)
    fun duplicateRequest_noExtraCall() =
        kosmos.runTest {
            // Initial request
            underTest.setRequestTopUi(true, "tag1")
            verify(activityManagerInterface, times(1)).setHasTopUi(true)

            // Duplicate request
            underTest.setRequestTopUi(true, "tag1")

            // No *new* calls should happen
            verifyNoMoreInteractions(activityManagerInterface)
        }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_TOP_UI_CONTROLLER)
    fun secondRequest_differentTag_noExtraCall() =
        kosmos.runTest {
            // Initial request
            underTest.setRequestTopUi(true, "tag1")
            verify(activityManagerInterface, times(1)).setHasTopUi(true)

            // Second request from different component
            underTest.setRequestTopUi(true, "tag2")

            // State is already true, no new calls expected
            verifyNoMoreInteractions(activityManagerInterface)
        }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_TOP_UI_CONTROLLER)
    fun releaseOneOfTwo_noCall() =
        kosmos.runTest {
            // Setup with two requesters
            underTest.setRequestTopUi(true, "tag1")
            verify(activityManagerInterface, times(1)).setHasTopUi(true)
            underTest.setRequestTopUi(true, "tag2")
            verifyNoMoreInteractions(activityManagerInterface) // No new call for tag2

            // Release first tag
            underTest.setRequestTopUi(false, "tag1")

            // State should remain true (tag2 still active), no new calls
            verifyNoMoreInteractions(activityManagerInterface)
        }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_TOP_UI_CONTROLLER)
    fun releaseLast_setsHasTopUiFalse() =
        kosmos.runTest {
            // Setup with two requesters
            underTest.setRequestTopUi(true, "tag1")
            verify(activityManagerInterface, times(1)).setHasTopUi(true)
            underTest.setRequestTopUi(true, "tag2")
            verifyNoMoreInteractions(activityManagerInterface)

            // Release first
            underTest.setRequestTopUi(false, "tag1")
            verifyNoMoreInteractions(activityManagerInterface) // Still true

            // Release second (last)
            underTest.setRequestTopUi(false, "tag2")

            // Should now call setHasTopUi(false) exactly once
            verify(activityManagerInterface, times(1)).setHasTopUi(false)
            verifyNoMoreInteractions(activityManagerInterface)
        }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_TOP_UI_CONTROLLER)
    fun duplicateRelease_noExtraCall() =
        kosmos.runTest {
            // Setup and release all
            underTest.setRequestTopUi(true, "tag1")
            verify(activityManagerInterface).setHasTopUi(true) // Use default times(1)
            underTest.setRequestTopUi(false, "tag1")
            verify(activityManagerInterface).setHasTopUi(false) // Use default times(1)

            // Duplicate release
            underTest.setRequestTopUi(false, "tag1")

            // No new calls expected
            verifyNoMoreInteractions(activityManagerInterface)
        }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_TOP_UI_CONTROLLER)
    fun releaseNonExistent_noCall() =
        kosmos.runTest {
            // Setup with one requester
            underTest.setRequestTopUi(true, "tag1")
            verify(activityManagerInterface).setHasTopUi(true)

            // Release a tag that never requested
            underTest.setRequestTopUi(false, "tagNonExistent")

            // No change expected
            verifyNoMoreInteractions(activityManagerInterface)

            // Release the actual tag should still work
            underTest.setRequestTopUi(false, "tag1")
            verify(activityManagerInterface).setHasTopUi(false)
            verifyNoMoreInteractions(activityManagerInterface)
        }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_TOP_UI_CONTROLLER)
    fun requestReleaseRequest_correctCalls() =
        kosmos.runTest {
            // Request
            underTest.setRequestTopUi(true, "tag1")
            verify(activityManagerInterface, times(1)).setHasTopUi(true)
            verifyNoMoreInteractions(activityManagerInterface)

            // Release
            underTest.setRequestTopUi(false, "tag1")
            verify(activityManagerInterface, times(1)).setHasTopUi(false)
            verifyNoMoreInteractions(activityManagerInterface)

            // Request again
            underTest.setRequestTopUi(true, "tag1")
            // Need to verify setHasTopUi(true) was called a *second* time overall
            verify(activityManagerInterface, times(2)).setHasTopUi(true)
            verifyNoMoreInteractions(activityManagerInterface)
        }
}
+13 −5
Original line number Diff line number Diff line
@@ -30,8 +30,11 @@ import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.notificationShadeWindowController
import com.android.systemui.testKosmos
import com.android.systemui.topui.TopUiController
import com.android.systemui.topui.TopUiControllerRefactor
import com.android.systemui.topui.topUiController
import com.android.systemui.topwindoweffects.data.repository.DEFAULT_INITIAL_DELAY_MILLIS
import com.android.systemui.topwindoweffects.data.repository.DEFAULT_LONG_PRESS_POWER_DURATION_MILLIS
import com.android.systemui.topwindoweffects.data.repository.fakeSqueezeEffectRepository
@@ -63,7 +66,7 @@ class TopLevelWindowEffectsTest : SysuiTestCase() {

    @Mock private lateinit var windowManager: WindowManager

    @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
    @Mock private lateinit var topUiController: TopUiController

    @Mock private lateinit var viewModelFactory: SqueezeEffectViewModel.Factory

@@ -79,7 +82,8 @@ class TopLevelWindowEffectsTest : SysuiTestCase() {
                squeezeEffectInteractor =
                    SqueezeEffectInteractor(squeezeEffectRepository = fakeSqueezeEffectRepository),
                appZoomOutOptional = Optional.empty(),
                notificationShadeWindowController = notificationShadeWindowController,
                notificationShadeWindowController = kosmos.notificationShadeWindowController,
                topUiController = kosmos.topUiController,
                interactionJankMonitor = kosmos.interactionJankMonitor,
            )
        }
@@ -224,7 +228,11 @@ class TopLevelWindowEffectsTest : SysuiTestCase() {

    private fun verifyAddViewAndTopUi(mode: VerificationMode) {
        verify(windowManager, mode).addView(any<View>(), any<WindowManager.LayoutParams>())
        verify(notificationShadeWindowController, mode)
        if (TopUiControllerRefactor.isEnabled) {
            verify(topUiController, mode).setRequestTopUi(true, TopLevelWindowEffects.TAG)
        } else {
            verify(kosmos.notificationShadeWindowController, mode)
                .setRequestTopUi(true, TopLevelWindowEffects.TAG)
        }
    }
}
+5 −0
Original line number Diff line number Diff line
@@ -126,6 +126,7 @@ import com.android.systemui.startable.Dependencies;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.topui.TopUiController;
import com.android.systemui.statusbar.chips.StatusBarChipsModule;
import com.android.systemui.statusbar.connectivity.ConnectivityModule;
import com.android.systemui.statusbar.dagger.StatusBarModule;
@@ -158,6 +159,7 @@ import com.android.systemui.statusbar.ui.binder.StatusBarViewBinderModule;
import com.android.systemui.statusbar.window.StatusBarWindowModule;
import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule;
import com.android.systemui.temporarydisplay.dagger.TemporaryDisplayModule;
import com.android.systemui.topui.TopUiModule;
import com.android.systemui.touchpad.TouchpadModule;
import com.android.systemui.tuner.dagger.TunerModule;
import com.android.systemui.user.UserModule;
@@ -284,6 +286,7 @@ import javax.inject.Named;
        TelephonyRepositoryModule.class,
        TemporaryDisplayModule.class,
        ShadeDisplayAwareModule.class,
        TopUiModule.class,
        TouchpadModule.class,
        TunerModule.class,
        UserDomainLayerModule.class,
@@ -410,6 +413,7 @@ public abstract class SystemUIModule {
    static Optional<BubblesManager> provideBubblesManager(Context context,
            Optional<Bubbles> bubblesOptional,
            NotificationShadeWindowController notificationShadeWindowController,
            TopUiController topUiController,
            KeyguardStateController keyguardStateController,
            ShadeController shadeController,
            @Nullable IStatusBarService statusBarService,
@@ -430,6 +434,7 @@ public abstract class SystemUIModule {
        return Optional.ofNullable(BubblesManager.create(context,
                bubblesOptional,
                notificationShadeWindowController,
                topUiController,
                keyguardStateController,
                shadeController,
                statusBarService,
Loading