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

Commit 789ae288 authored by Nicolo' Mazzucato's avatar Nicolo' Mazzucato
Browse files

Enable WindowContext to move to another display

This refactors the way the ShadeWindow is moved between display using WindowContext.updateDisplay instead of using a MutableContextWrapper.

This approach is better as it automatically updates existing resource instances that might have been cached, and the configuration change is propagated to the root view without the need to rely on ComponentCallbacks.

Bug: 362719719
Bug: 374267505
Test: ShadeDisplaysInteractorTest, WindowContextTest
Flag: com.android.systemui.shade_window_goes_around
Change-Id: Ib1070f30a50c3ec5c85596cbbb9c15ad69319c8d
parent cf51487f
Loading
Loading
Loading
Loading
+17 −0
Original line number Diff line number Diff line
@@ -94,6 +94,23 @@ public class WindowContext extends ContextWrapper implements WindowProvider {
        mController.attachToDisplayArea(mType, getDisplayId(), mOptions);
    }

    /**
     * Updates this context to a new displayId.
     * <p>
     * Note that this doesn't re-parent previously attached windows (they should be removed and
     * re-added manually after this is called). Resources associated with this context will have
     * the correct value and configuration for the new display after this is called.
     */
    @Override
    public void updateDisplay(int displayId) {
        if (displayId == getDisplayId()) {
            return;
        }
        super.updateDisplay(displayId);
        mController.detachIfNeeded();
        mController.attachToDisplayArea(mType, displayId, mOptions);
    }

    @Override
    public Object getSystemService(String name) {
        if (WINDOW_SERVICE.equals(name)) {
+33 −0
Original line number Diff line number Diff line
@@ -29,6 +29,11 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import android.app.Activity;
import android.app.EmptyActivity;
@@ -60,6 +65,8 @@ import androidx.test.rule.ActivityTestRule;

import com.android.frameworks.coretests.R;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -88,9 +95,21 @@ public class WindowContextTest {
    private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
    private final WindowContext mWindowContext = createWindowContext();
    private final IWindowManager mWms = WindowManagerGlobal.getWindowManagerService();
    private WindowTokenClientController mOriginalWindowTokenClientController;

    private static final int TIMEOUT_IN_SECONDS = 4;

    @Before
    public void setUp() {
        // Keeping the original to set it back after each test, in case they applied any override.
        mOriginalWindowTokenClientController = WindowTokenClientController.getInstance();
    }

    @After
    public void tearDown() {
        WindowTokenClientController.overrideForTesting(mOriginalWindowTokenClientController);
    }

    @Test
    public void testCreateWindowContextWindowManagerAttachClientToken() {
        final WindowManager windowContextWm = WindowManagerImpl
@@ -320,6 +339,20 @@ public class WindowContextTest {
        }
    }

    @Test
    public void updateDisplay_wasAttached_detachThenAttachedPropagatedToTokenController() {
        final WindowTokenClientController mockWindowTokenClientController =
                mock(WindowTokenClientController.class);
        WindowTokenClientController.overrideForTesting(mockWindowTokenClientController);

        mWindowContext.updateDisplay(DEFAULT_DISPLAY + 1);

        verify(mockWindowTokenClientController).detachIfNeeded(any());
        verify(mockWindowTokenClientController).attachToDisplayArea(any(),
                anyInt(), /* displayId= */ eq(DEFAULT_DISPLAY + 1),
                any());
    }

    private WindowContext createWindowContext() {
        return createWindowContext(TYPE_APPLICATION_OVERLAY);
    }
+19 −46
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
package com.android.systemui.shade.domain.interactor

import android.content.Context
import android.content.MutableContextWrapper
import android.content.res.Configuration
import android.content.res.Resources
import android.view.Display
@@ -30,7 +29,6 @@ import com.android.systemui.display.data.repository.FakeDisplayWindowPropertiesR
import com.android.systemui.display.shared.model.DisplayWindowProperties
import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.data.repository.FakeShadeDisplayRepository
import com.android.systemui.statusbar.phone.ConfigurationForwarder
import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
@@ -39,6 +37,7 @@ import kotlinx.coroutines.test.advanceUntilIdle
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.verify
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
@@ -53,13 +52,10 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {

    private val shadeRootview = mock<WindowRootView>()
    private val positionRepository = FakeShadeDisplayRepository()
    private val defaultContext = mock<Context>()
    private val secondaryContext = mock<Context>()
    private val shadeContext = mock<Context>()
    private val contextStore = FakeDisplayWindowPropertiesRepository()
    private val testScope = TestScope(UnconfinedTestDispatcher())
    private val configurationForwarder = mock<ConfigurationForwarder>()
    private val defaultWm = mock<WindowManager>()
    private val secondaryWm = mock<WindowManager>()
    private val shadeWm = mock<WindowManager>()
    private val resources = mock<Resources>()
    private val configuration = mock<Configuration>()
    private val display = mock<Display>()
@@ -68,11 +64,9 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {
        ShadeDisplaysInteractor(
            Optional.of(shadeRootview),
            positionRepository,
            MutableContextWrapper(defaultContext),
            resources,
            contextStore,
            shadeContext,
            shadeWm,
            testScope.backgroundScope,
            configurationForwarder,
            testScope.backgroundScope.coroutineContext,
        )

@@ -83,28 +77,15 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {

        whenever(resources.configuration).thenReturn(configuration)

        whenever(defaultContext.displayId).thenReturn(0)
        whenever(defaultContext.getSystemService(any())).thenReturn(defaultWm)
        whenever(defaultContext.resources).thenReturn(resources)
        whenever(shadeContext.displayId).thenReturn(0)
        whenever(shadeContext.getSystemService(any())).thenReturn(shadeWm)
        whenever(shadeContext.resources).thenReturn(resources)
        contextStore.insert(
            DisplayWindowProperties(
                displayId = 0,
                windowType = TYPE_NOTIFICATION_SHADE,
                context = defaultContext,
                windowManager = defaultWm,
                layoutInflater = mock(),
            )
        )

        whenever(secondaryContext.displayId).thenReturn(1)
        whenever(secondaryContext.getSystemService(any())).thenReturn(secondaryWm)
        whenever(secondaryContext.resources).thenReturn(resources)
        contextStore.insert(
            DisplayWindowProperties(
                displayId = 1,
                windowType = TYPE_NOTIFICATION_SHADE,
                context = secondaryContext,
                windowManager = secondaryWm,
                context = shadeContext,
                windowManager = shadeWm,
                layoutInflater = mock(),
            )
        )
@@ -117,8 +98,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {
        interactor.start()
        testScope.advanceUntilIdle()

        verifyNoMoreInteractions(defaultWm)
        verifyNoMoreInteractions(secondaryWm)
        verifyNoMoreInteractions(shadeWm)
    }

    @Test
@@ -127,8 +107,10 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {
        positionRepository.setDisplayId(1)
        interactor.start()

        verify(defaultWm).removeView(eq(shadeRootview))
        verify(secondaryWm).addView(eq(shadeRootview), any())
        inOrder(shadeWm).apply {
            verify(shadeWm).removeView(eq(shadeRootview))
            verify(shadeWm).addView(eq(shadeRootview), any())
        }
    }

    @Test
@@ -139,18 +121,9 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {

        positionRepository.setDisplayId(1)

        verify(defaultWm).removeView(eq(shadeRootview))
        verify(secondaryWm).addView(eq(shadeRootview), any())
        inOrder(shadeWm).apply {
            verify(shadeWm).removeView(eq(shadeRootview))
            verify(shadeWm).addView(eq(shadeRootview), any())
        }

    @Test
    fun start_shadePositionChanges_newConfigPropagated() {
        whenever(display.displayId).thenReturn(0)
        positionRepository.setDisplayId(0)
        interactor.start()

        positionRepository.setDisplayId(1)

        verify(configurationForwarder).onConfigurationChanged(eq(configuration))
    }
}
+20 −2
Original line number Diff line number Diff line
@@ -17,9 +17,10 @@
package com.android.systemui.shade

import android.content.Context
import android.content.MutableContextWrapper
import android.content.res.Resources
import android.view.LayoutInflater
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE
import com.android.systemui.CoreStartable
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.common.ui.ConfigurationStateImpl
@@ -29,6 +30,7 @@ import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImp
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractorImpl
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.res.R
import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
import com.android.systemui.shade.data.repository.ShadeDisplaysRepositoryImpl
@@ -65,12 +67,28 @@ object ShadeDisplayAwareModule {
    @SysUISingleton
    fun provideShadeDisplayAwareContext(context: Context): Context {
        return if (ShadeWindowGoesAround.isEnabled) {
            MutableContextWrapper(context)
            context
                .createWindowContext(context.display, TYPE_NOTIFICATION_SHADE, /* options= */ null)
                .apply { setTheme(R.style.Theme_SystemUI) }
        } else {
            context
        }
    }

    @Provides
    @ShadeDisplayAware
    @SysUISingleton
    fun provideShadeWindowManager(
        defaultWindowManager: WindowManager,
        @ShadeDisplayAware context: Context,
    ): WindowManager {
        return if (ShadeWindowGoesAround.isEnabled) {
            context.getSystemService(WindowManager::class.java) as WindowManager
        } else {
            defaultWindowManager
        }
    }

    @Provides
    @ShadeDisplayAware
    @SysUISingleton
+21 −72
Original line number Diff line number Diff line
@@ -16,28 +16,21 @@

package com.android.systemui.shade.domain.interactor

import android.content.ComponentCallbacks
import android.content.Context
import android.content.MutableContextWrapper
import android.content.res.Configuration
import android.content.res.Resources
import android.util.Log
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE
import androidx.annotation.UiThread
import com.android.app.tracing.coroutines.launchTraced
import com.android.app.tracing.traceSection
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository
import com.android.systemui.display.shared.model.DisplayWindowProperties
import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.ShadeDisplayAware
import com.android.systemui.shade.ShadeWindowLayoutParams
import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import com.android.systemui.statusbar.phone.ConfigurationForwarder
import com.android.systemui.util.kotlin.getOrNull
import java.util.Optional
import javax.inject.Inject
@@ -53,13 +46,14 @@ constructor(
    optionalShadeRootView: Optional<WindowRootView>,
    private val shadePositionRepository: ShadeDisplaysRepository,
    @ShadeDisplayAware private val shadeContext: Context,
    @ShadeDisplayAware private val shadeResources: Resources,
    private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository,
    @ShadeDisplayAware private val wm: WindowManager,
    @Background private val bgScope: CoroutineScope,
    @ShadeDisplayAware private val shadeConfigurationForwarder: ConfigurationForwarder,
    @Main private val mainThreadContext: CoroutineContext,
) : CoreStartable {

    private val shadeLayoutParams: WindowManager.LayoutParams =
        ShadeWindowLayoutParams.create(shadeContext)

    private val shadeRootView =
        optionalShadeRootView.getOrNull()
            ?: error(
@@ -69,9 +63,6 @@ constructor(
            """
                    .trimIndent()
            )
    // TODO: b/362719719 - Get rid of this callback as the root view should automatically get the
    //  correct configuration once it's moved to another window.
    private var unregisterConfigChangedCallbacks: (() -> Unit)? = null

    override fun start() {
        ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()
@@ -94,7 +85,7 @@ constructor(
            return
        }
        try {
            moveShadeWindow(fromId = currentId, toId = destinationId)
            withContext(mainThreadContext) { moveShadeWindow(toId = destinationId) }
        } catch (e: IllegalStateException) {
            Log.e(
                TAG,
@@ -104,68 +95,26 @@ constructor(
        }
    }

    private suspend fun moveShadeWindow(fromId: Int, toId: Int) {
        val (_, _, _, sourceWm) = getDisplayWindowProperties(fromId)
        val (_, _, destContext, destWm) = getDisplayWindowProperties(toId)
        withContext(mainThreadContext) {
            traceSection({ "MovingShadeWindow from $fromId to $toId" }) {
                removeShade(sourceWm)
                addShade(destWm)
                overrideContextAndResources(newContext = destContext)
                registerConfigurationChange(destContext)
            }
            traceSection("ShadeDisplaysInteractor#onConfigurationChanged") {
                dispatchConfigurationChanged(destContext.resources.configuration)
            }
        }
    }

    private fun removeShade(wm: WindowManager): Unit =
        traceSection("removeView") { wm.removeView(shadeRootView) }

    private fun addShade(wm: WindowManager): Unit =
        traceSection("addView") {
            wm.addView(shadeRootView, ShadeWindowLayoutParams.create(shadeContext))
        }

    private fun overrideContextAndResources(newContext: Context) {
        val contextWrapper =
            shadeContext as? MutableContextWrapper
                ?: error("Shade context is not a MutableContextWrapper!")
        contextWrapper.baseContext = newContext
        // Override needed in case someone is keeping a reference to the resources from the old
        // context.
        // TODO: b/362719719 - This shouldn't be needed, as resources should be updated when the
        //  window is moved to the new display automatically.
        shadeResources.impl = shadeContext.resources.impl
    @UiThread
    private fun moveShadeWindow(toId: Int) {
        traceSection({ "moveShadeWindow  to $toId" }) {
            removeShadeWindow()
            updateContextDisplay(toId)
            addShadeWindow()
        }

    private fun dispatchConfigurationChanged(newConfig: Configuration) {
        shadeConfigurationForwarder.onConfigurationChanged(newConfig)
        shadeRootView.dispatchConfigurationChanged(newConfig)
        shadeRootView.requestLayout()
    }

    private fun registerConfigurationChange(context: Context) {
        // we should keep only one at the time.
        unregisterConfigChangedCallbacks?.invoke()
        val callback =
            object : ComponentCallbacks {
                override fun onConfigurationChanged(newConfig: Configuration) {
                    dispatchConfigurationChanged(newConfig)
                }
    @UiThread
    private fun removeShadeWindow(): Unit =
        traceSection("removeShadeWindow") { wm.removeView(shadeRootView) }

                override fun onLowMemory() {}
            }
        context.registerComponentCallbacks(callback)
        unregisterConfigChangedCallbacks = {
            context.unregisterComponentCallbacks(callback)
            unregisterConfigChangedCallbacks = null
        }
    }
    @UiThread
    private fun addShadeWindow(): Unit =
        traceSection("addShadeWindow") { wm.addView(shadeRootView, shadeLayoutParams) }

    private fun getDisplayWindowProperties(displayId: Int): DisplayWindowProperties {
        return displayWindowPropertiesRepository.get(displayId, TYPE_NOTIFICATION_SHADE)
    @UiThread
    private fun updateContextDisplay(newDisplayId: Int) {
        traceSection("updateContextDisplay") { shadeContext.updateDisplay(newDisplayId) }
    }

    private companion object {