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

Commit c296db8e authored by Steven Ng's avatar Steven Ng
Browse files

Show an empty presentation in connected displays during device provisioning

When a device is provisioned, showing wallpaper on connected displays requires a specific approach. Normally, two components handle this:

1. DesktopWallpaperActivity: This translucent activity acts as the launcher for connected displays, responsible for showing the wallpaper.
2. ConnectedDisplayKeyguardPresentation: This is a keyguard dialog that appears on the connected display when the device's keyguard is active. It uses the FLAG_SHOW_WALLPAPER flag to display the lock screen wallpaper.

However, these mechanisms don't activate during the device provisioning process. To address this, a new translucent window (a new presentation) has been introduced specifically for showing wallpaper during provisioning.
A new class WallpaperPresentationManager is introduced to control the lifecycle of SysUi wallpaper presentations.

I considered moving the presentation to SetupWizard, but decided against it due to:
1. SetupWizard currently lacks the necessary display management mechanisms to effectively control the lifecycle of such a presentation.
2. Managing wallpaper is more appropriately handled by SysUI than by SetupWizard

Test: SystemUITests:DisplayWallpaperPresentationInteractorTest
Test: SystemUITests:WallpaperPresentationManagerTest
Test: manual - In setupwizard, connect the device to an external monitor to confirm that the default wallpaper is shown in the connected display.
Flag: com.android.window.flags.enable_connected_displays_wallpaper_presentations
Fix: 429395688
Change-Id: I6005dad28e32efbd5f3a702c0440af63ac7923d1
parent ae2ba8a6
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -90,6 +90,7 @@ class KeyguardDisplayManagerTest : SysuiTestCase() {
                presentationFactory,
                { shadePositionRepository },
                testScope.backgroundScope,
                /* isCentralizedWallpaperPresentationEnabled= */ false,
            )
        whenever(presentationFactory.create(any())).doReturn(connectedDisplayKeyguardPresentation)

+184 −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.wallpapers

import android.app.Presentation
import android.hardware.display.DisplayManagerGlobal
import android.view.Display
import android.view.DisplayAdjustments
import android.view.DisplayInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.wallpapers.domain.interactor.DisplayWallpaperPresentationInteractor.WallpaperPresentationType.KEYGUARD
import com.android.systemui.wallpapers.domain.interactor.DisplayWallpaperPresentationInteractor.WallpaperPresentationType.NONE
import com.android.systemui.wallpapers.domain.interactor.DisplayWallpaperPresentationInteractor.WallpaperPresentationType.PROVISIONING
import com.android.systemui.wallpapers.domain.interactor.fakeDisplayWallpaperPresentationInteractor
import com.android.systemui.wallpapers.ui.presentation.KeyguardWallpaperPresentationFactory
import com.android.systemui.wallpapers.ui.presentation.ProvisioningWallpaperPresentationFactory
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.verifyNoMoreInteractions

@SmallTest
@RunWith(AndroidJUnit4::class)
class WallpaperPresentationManagerTest : SysuiTestCase() {
    private val kosmos = Kosmos().useUnconfinedTestDispatcher()
    private val testScope = kosmos.testScope
    private val fakeWallpaperPresentationInteractor =
        kosmos.fakeDisplayWallpaperPresentationInteractor
    private val testDisplay: Display =
        Display(
            DisplayManagerGlobal.getInstance(),
            /* displayId= */ 2,
            DisplayInfo(),
            DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS,
        )

    private val provisioningPresentation: Presentation = mock()
    private val provisioningPresentationFactory: ProvisioningWallpaperPresentationFactory = mock {
        on { create(any()) }.doReturn(provisioningPresentation)
    }
    private val keyguardPresentation: Presentation = mock()
    private val keyguardPresentationFactory: KeyguardWallpaperPresentationFactory = mock {
        on { create(any()) }.doReturn(keyguardPresentation)
    }

    private val wallpaperPresentationManager =
        WallpaperPresentationManager(
            display = testDisplay,
            displayCoroutineScope = testScope.backgroundScope,
            presentationInteractor = kosmos.fakeDisplayWallpaperPresentationInteractor,
            presentationFactories =
                mapOf(
                    PROVISIONING to provisioningPresentationFactory,
                    KEYGUARD to keyguardPresentationFactory,
                ),
            appCoroutineScope = testScope,
            mainDispatcher = kosmos.testDispatcher,
        )

    @Test
    fun emitProvisioning_createProvisioningPresentation() =
        kosmos.runTest {
            wallpaperPresentationManager.start()

            fakeWallpaperPresentationInteractor._presentationFactoryFlow.emit(PROVISIONING)

            verify(provisioningPresentationFactory).create(eq(testDisplay))
            verify(provisioningPresentation).show()
            verifyNoMoreInteractions(provisioningPresentation)
            verifyNoInteractions(keyguardPresentationFactory)
        }

    @Test
    fun emitProvisioningThenNone_hideProvisioningPresentation() =
        kosmos.runTest {
            wallpaperPresentationManager.start()

            fakeWallpaperPresentationInteractor._presentationFactoryFlow.emit(PROVISIONING)
            fakeWallpaperPresentationInteractor._presentationFactoryFlow.emit(NONE)

            verify(provisioningPresentation).hide()
            verifyNoInteractions(keyguardPresentationFactory)
        }

    @Test
    fun emitProvisioningThenKeyguard_hideProvisioningAndShowKeyguardPresentation() =
        kosmos.runTest {
            wallpaperPresentationManager.start()

            fakeWallpaperPresentationInteractor._presentationFactoryFlow.emit(PROVISIONING)
            fakeWallpaperPresentationInteractor._presentationFactoryFlow.emit(KEYGUARD)

            verify(provisioningPresentation).hide()
            verify(keyguardPresentationFactory).create(eq(testDisplay))
            verify(keyguardPresentation).show()
            verifyNoMoreInteractions(keyguardPresentation)
        }

    @Test
    fun emitKeyguard_showKeyguardPresentation() =
        kosmos.runTest {
            wallpaperPresentationManager.start()

            fakeWallpaperPresentationInteractor._presentationFactoryFlow.emit(KEYGUARD)

            verify(keyguardPresentationFactory).create(eq(testDisplay))
            verify(keyguardPresentation).show()
            verifyNoMoreInteractions(keyguardPresentation)
            verifyNoInteractions(provisioningPresentationFactory)
        }

    @Test
    fun emitKeyguardThenNone_hideKeyguardPresentation() =
        kosmos.runTest {
            wallpaperPresentationManager.start()

            fakeWallpaperPresentationInteractor._presentationFactoryFlow.emit(KEYGUARD)
            fakeWallpaperPresentationInteractor._presentationFactoryFlow.emit(NONE)

            verify(keyguardPresentation).hide()
            verifyNoInteractions(provisioningPresentationFactory)
        }

    @Test
    fun emitKeyguardThenProvisioning_hideKeyguardAndShowProvisioningPresentation() =
        kosmos.runTest {
            wallpaperPresentationManager.start()

            fakeWallpaperPresentationInteractor._presentationFactoryFlow.emit(KEYGUARD)
            fakeWallpaperPresentationInteractor._presentationFactoryFlow.emit(PROVISIONING)

            verify(keyguardPresentation).hide()
            verify(provisioningPresentation).show()
            verifyNoMoreInteractions(provisioningPresentation)
        }

    @Test
    fun stop_hidePreviouslyShownProvisioningPresentation() =
        kosmos.runTest {
            wallpaperPresentationManager.start()
            fakeWallpaperPresentationInteractor._presentationFactoryFlow.emit(PROVISIONING)

            wallpaperPresentationManager.stop()

            verify(provisioningPresentation).hide()
        }

    @Test
    fun stop_hidePreviouslyShownKeyguardPresentation() =
        kosmos.runTest {
            wallpaperPresentationManager.start()
            fakeWallpaperPresentationInteractor._presentationFactoryFlow.emit(KEYGUARD)

            wallpaperPresentationManager.stop()

            verify(keyguardPresentation).hide()
        }
}
+136 −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.wallpapers.domain.interactor

import android.hardware.display.DisplayManagerGlobal
import android.view.Display
import android.view.DisplayAdjustments
import android.view.DisplayInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.keyguardDisplayManager
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository
import com.android.systemui.testKosmos
import com.android.systemui.wallpapers.domain.interactor.DisplayWallpaperPresentationInteractor.WallpaperPresentationType.KEYGUARD
import com.android.systemui.wallpapers.domain.interactor.DisplayWallpaperPresentationInteractor.WallpaperPresentationType.NONE
import com.android.systemui.wallpapers.domain.interactor.DisplayWallpaperPresentationInteractor.WallpaperPresentationType.PROVISIONING
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Before
import org.junit.runner.RunWith

@ExperimentalCoroutinesApi
@SmallTest
@RunWith(AndroidJUnit4::class)
class DisplayWallpaperPresentationInteractorTest : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val deviceUnlockedInteractor = kosmos.keyguardInteractor
    private val fakeKeyguardRepository = kosmos.fakeKeyguardRepository
    private val deviceProvisioningRepository = kosmos.fakeDeviceProvisioningRepository
    private val keyguardDisplayManager = kosmos.keyguardDisplayManager
    private val testDisplayInfo = DisplayInfo()
    private val testDisplay: Display =
        Display(
            DisplayManagerGlobal.getInstance(),
            /* displayId= */ 2,
            testDisplayInfo,
            DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS,
        )
    private val wallpaperPresentationInteractor =
        DisplayWallpaperPresentationInteractorImpl(
            display = testDisplay,
            displayCoroutineScope = kosmos.testScope.backgroundScope,
            keyguardInteractor = { deviceUnlockedInteractor },
            deviceProvisioningRepository = { deviceProvisioningRepository },
            keyguardDisplayManager = { keyguardDisplayManager },
        )

    @Before
    fun setUp() {
        fakeKeyguardRepository.setKeyguardDismissible(isUnlocked = true)
        deviceProvisioningRepository.setDeviceProvisioned(true)
    }

    @Test
    fun presentationFactoryFlow_unlocked_provisioned_none() =
        kosmos.runTest {
            fakeKeyguardRepository.setKeyguardDismissible(isUnlocked = true)
            deviceProvisioningRepository.setDeviceProvisioned(true)

            val actual by collectLastValue(wallpaperPresentationInteractor.presentationFactoryFlow)
            assertThat(actual).isEqualTo(NONE)
        }

    @Test
    fun presentationFactoryFlow_locked_provisioned_displayCompatible_keyguard() =
        kosmos.runTest {
            fakeKeyguardRepository.setKeyguardDismissible(isUnlocked = false)
            deviceProvisioningRepository.setDeviceProvisioned(true)

            val actual by collectLastValue(wallpaperPresentationInteractor.presentationFactoryFlow)
            assertThat(actual).isEqualTo(KEYGUARD)
        }

    @Test
    fun presentationFactoryFlow_locked_provisioned_displayIncompatible_none() =
        kosmos.runTest {
            testDisplayInfo.flags = Display.FLAG_PRIVATE
            fakeKeyguardRepository.setKeyguardDismissible(isUnlocked = false)
            deviceProvisioningRepository.setDeviceProvisioned(true)

            val actual by collectLastValue(wallpaperPresentationInteractor.presentationFactoryFlow)
            assertThat(actual).isEqualTo(NONE)
        }

    @Test
    fun presentationFactoryFlow_provisioning_locked_displayCompatible_provisioning() =
        kosmos.runTest {
            deviceProvisioningRepository.setDeviceProvisioned(false)
            fakeKeyguardRepository.setKeyguardDismissible(isUnlocked = false)

            val actual by collectLastValue(wallpaperPresentationInteractor.presentationFactoryFlow)
            assertThat(actual).isEqualTo(PROVISIONING)
        }

    @Test
    fun presentationFactoryFlow_provisioning_unlocked_displayCompatible_provisioning() =
        kosmos.runTest {
            deviceProvisioningRepository.setDeviceProvisioned(false)
            fakeKeyguardRepository.setKeyguardDismissible(isUnlocked = true)

            val actual by collectLastValue(wallpaperPresentationInteractor.presentationFactoryFlow)
            assertThat(actual).isEqualTo(PROVISIONING)
        }

    @Test
    fun presentationFactoryFlow_provisioning_displayIncompatible_none() =
        kosmos.runTest {
            testDisplayInfo.flags = Display.FLAG_PRIVATE
            deviceProvisioningRepository.setDeviceProvisioned(false)

            val actual by collectLastValue(wallpaperPresentationInteractor.presentationFactoryFlow)
            assertThat(actual).isEqualTo(NONE)
        }
}
+8 −0
Original line number Diff line number Diff line
@@ -203,4 +203,12 @@
      </item>
      <item name="android:textSize">@dimen/bouncer_user_switcher_item_text_size</item>
    </style>

    <style name="Theme.SystemUI.WallpaperPresentation">
        <item name="android:windowActionBar">false</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowFullscreen">true</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:colorBackground">@android:color/transparent</item>
    </style>
</resources>
+30 −8
Original line number Diff line number Diff line
@@ -46,6 +46,7 @@ import com.android.systemui.shade.ShadeDisplayAware;
import com.android.systemui.shade.data.repository.ShadeDisplaysRepository;
import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.wallpapers.WallpaperPresentationEnabled;

import dagger.Lazy;

@@ -71,6 +72,7 @@ public class KeyguardDisplayManager {
    private final Provider<ShadeDisplaysRepository> mShadePositionRepositoryProvider;
    private final ConnectedDisplayKeyguardPresentationFactory
            mConnectedDisplayKeyguardPresentationFactory;
    private final Boolean mIsCentralizedWallpaperPresentationEnabled;
    private final Context mContext;

    private boolean mShowing;
@@ -116,7 +118,8 @@ public class KeyguardDisplayManager {
            ConnectedDisplayKeyguardPresentationFactory
                    connectedDisplayKeyguardPresentationFactory,
            Provider<ShadeDisplaysRepository> shadePositionRepositoryProvider,
            @Application CoroutineScope appScope) {
            @Application CoroutineScope appScope,
            @WallpaperPresentationEnabled Boolean isCentralizedWallpaperPresentationEnabled) {
        mContext = context;
        mNavigationBarControllerLazy = navigationBarControllerLazy;
        mShadePositionRepositoryProvider = shadePositionRepositoryProvider;
@@ -127,6 +130,7 @@ public class KeyguardDisplayManager {
        mDeviceStateHelper = deviceStateHelper;
        mKeyguardStateController = keyguardStateController;
        mConnectedDisplayKeyguardPresentationFactory = connectedDisplayKeyguardPresentationFactory;
        mIsCentralizedWallpaperPresentationEnabled = isCentralizedWallpaperPresentationEnabled;
        if (ShadeWindowGoesAround.isEnabled()) {
            collectFlow(appScope, shadePositionRepositoryProvider.get().getDisplayId(),
                    (id) -> onShadeWindowMovedToDisplayId(id));
@@ -140,7 +144,10 @@ public class KeyguardDisplayManager {
        }
    }

    private boolean isKeyguardShowable(Display display) {
    /**
     * Returns `true` if the keyguard can be shown for a given {@code display}. Otherwise, `false`.
     */
    public boolean isKeyguardShowable(Display display) {
        if (display == null) {
            Log.i(TAG, "Cannot show Keyguard on null display");
            return false;
@@ -193,6 +200,10 @@ public class KeyguardDisplayManager {
     *         was already there.
     */
    private boolean showPresentation(Display display) {
        if (mIsCentralizedWallpaperPresentationEnabled) {
            // Handled in WallpaperPresentationManager.
            return false;
        }
        if (!isKeyguardShowable(display)) return false;
        Log.i(TAG, "Keyguard enabled on display: " + display);
        final int displayId = display.getDisplayId();
@@ -227,6 +238,10 @@ public class KeyguardDisplayManager {
     * @param displayId The id of the display to hide the presentation off.
     */
    private void hidePresentation(int displayId) {
        if (mIsCentralizedWallpaperPresentationEnabled) {
            // Handled in WallpaperPresentationManager.
            return;
        }
        final Presentation presentation = mPresentations.get(displayId);
        if (presentation != null) {
            presentation.dismiss();
@@ -289,6 +304,12 @@ public class KeyguardDisplayManager {
                updateNavigationBarVisibility(displayId, false /* navBarVisible */);
                changed |= showPresentation(display);
            }
        } else {
            if (mIsCentralizedWallpaperPresentationEnabled) {
                for (Display display : mDisplayTracker.getAllDisplays()) {
                    int displayId = display.getDisplayId();
                    updateNavigationBarVisibility(displayId, true /* navBarVisible */);
                }
            } else {
                changed = mPresentations.size() > 0;
                for (int i = mPresentations.size() - 1; i >= 0; i--) {
@@ -298,6 +319,7 @@ public class KeyguardDisplayManager {
                }
                mPresentations.clear();
            }
        }
        return changed;
    }

Loading