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

Commit 0b0c24c5 authored by Nicolo' Mazzucato's avatar Nicolo' Mazzucato Committed by Nicolò Mazzucato
Browse files

Use reparentToDisplayId to move the shade between displays

This uses the new API from display manager to reparent non-activity
windows without triggering onAttached/Detached.

As SysUI heavily relies on getting those callbacks only once (for some
windows), this makes the flexiglass refactor work without additional
changes even when the shade window moves.

Also, the shade move is much faster now: from the 600ms of the previous
approach to the <100ms of the current one (as all "onAttached" callbacks
are not being triggered)

After the change of window, only a configuration change in the shade
root view is received now, and propagated though configurationForwarder
to the other classes interested.

Bug: 362719719
Bug: 381258683
Bug: 381075014
Test: ShadeDisplaysInteractorTest
Flag: com.android.systemui.shade_window_goes_around
Flag: com.android.window.flags.reparent_window_token_api
Change-Id: I85dbc219eea894ec5c9e81fe378cdd5cc361cfde
parent 5054fff0
Loading
Loading
Loading
Loading
+6 −32
Original line number Diff line number Diff line
@@ -16,32 +16,26 @@

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

import android.content.mockedContext
import android.content.res.Configuration
import android.content.res.mockResources
import android.view.Display
import android.view.mockWindowManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.scene.ui.view.mockShadeRootView
import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository
import com.android.systemui.testKosmos
import kotlinx.coroutines.ExperimentalCoroutinesApi
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.never
import org.mockito.Mockito.verify
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
@SmallTest
class ShadeDisplaysInteractorTest : SysuiTestCase() {
@@ -49,9 +43,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {

    private val shadeRootview = kosmos.mockShadeRootView
    private val positionRepository = kosmos.fakeShadeDisplaysRepository
    private val shadeContext = kosmos.mockedContext
    private val testScope = kosmos.testScope
    private val shadeWm = kosmos.mockWindowManager
    private val shadeContext = kosmos.mockedWindowContext
    private val resources = kosmos.mockResources
    private val configuration = mock<Configuration>()
    private val display = mock<Display>()
@@ -66,8 +58,8 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {
        whenever(resources.configuration).thenReturn(configuration)

        whenever(shadeContext.displayId).thenReturn(0)
        whenever(shadeContext.getSystemService(any())).thenReturn(shadeWm)
        whenever(shadeContext.resources).thenReturn(resources)
        whenever(shadeContext.display).thenReturn(display)
    }

    @Test
@@ -77,7 +69,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {

        underTest.start()

        verifyNoMoreInteractions(shadeWm)
        verify(shadeContext, never()).reparentToDisplay(any())
    }

    @Test
@@ -87,24 +79,6 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {

        underTest.start()

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

    @Test
    fun start_shadePositionChanges_removedThenAdded() {
        whenever(display.displayId).thenReturn(0)
        positionRepository.setDisplayId(0)
        underTest.start()

        positionRepository.setDisplayId(1)
        testScope.advanceUntilIdle()

        inOrder(shadeWm).apply {
            verify(shadeWm).removeView(eq(shadeRootview))
            verify(shadeWm).addView(eq(shadeRootview), any())
        }
        verify(shadeContext).reparentToDisplay(eq(1))
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -165,6 +165,15 @@ public class NotificationShadeWindowView extends WindowRootView {
        return handled;
    }

    @Override
    public void onMovedToDisplay(int displayId, Configuration config) {
        super.onMovedToDisplay(displayId, config);
        ShadeWindowGoesAround.isUnexpectedlyInLegacyMode();
        // When the window is moved we're only receiving a call to this method instead of the
        // onConfigurationChange itself. Let's just trigegr a normal config change.
        onConfigurationChanged(config);
    }

    @Override
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
+17 −1
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.view.LayoutInflater
import android.view.WindowManager
import android.view.WindowManager.LayoutParams
import android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE
import android.window.WindowContext
import com.android.systemui.CoreStartable
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.common.ui.ConfigurationStateImpl
@@ -78,6 +79,19 @@ object ShadeDisplayAwareModule {
        }
    }

    @Provides
    @ShadeDisplayAware
    @SysUISingleton
    fun provideShadeDisplayAwareWindowContext(@ShadeDisplayAware context: Context): WindowContext {
        ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()
        // We rely on the fact context is a WindowContext as the API to reparent windows is only
        // available there.
        return (context as? WindowContext)
            ?: error(
                "ShadeDisplayAware context must be a window context to allow window reparenting."
            )
    }

    @Provides
    @ShadeDisplayAware
    @SysUISingleton
@@ -203,7 +217,9 @@ object ShadeDisplayAwareModule {
    @Provides
    @IntoMap
    @ClassKey(ShadePrimaryDisplayCommand::class)
    fun provideShadePrimaryDisplayCommand(impl: Provider<ShadePrimaryDisplayCommand>): CoreStartable {
    fun provideShadePrimaryDisplayCommand(
        impl: Provider<ShadePrimaryDisplayCommand>
    ): CoreStartable {
        return if (ShadeWindowGoesAround.isEnabled) {
            impl.get()
        } else {
+10 −27
Original line number Diff line number Diff line
@@ -16,10 +16,8 @@

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

import android.content.Context
import android.util.Log
import android.view.WindowManager
import android.view.WindowManager.LayoutParams
import android.window.WindowContext
import androidx.annotation.UiThread
import com.android.app.tracing.coroutines.launchTraced
import com.android.app.tracing.traceSection
@@ -45,9 +43,7 @@ class ShadeDisplaysInteractor
constructor(
    optionalShadeRootView: Optional<WindowRootView>,
    private val shadePositionRepository: ShadeDisplaysRepository,
    @ShadeDisplayAware private val shadeContext: Context,
    @ShadeDisplayAware private val shadeLayoutParams: LayoutParams,
    @ShadeDisplayAware private val wm: WindowManager,
    @ShadeDisplayAware private val shadeContext: WindowContext,
    @Background private val bgScope: CoroutineScope,
    @Main private val mainThreadContext: CoroutineContext,
) : CoreStartable {
@@ -72,7 +68,11 @@ constructor(
    /** Tries to move the shade. If anything wrong happens, fails gracefully without crashing. */
    private suspend fun moveShadeWindowTo(destinationId: Int) {
        Log.d(TAG, "Trying to move shade window to display with id $destinationId")
        val currentDisplay = shadeRootView.display
        // Why using the shade context here instead of the view's Display?
        // The context's display is updated before the view one, so it is a better indicator of
        // which display the shade is supposed to be at. The View display is updated after the first
        // rendering with the new config.
        val currentDisplay = shadeContext.display
        if (currentDisplay == null) {
            Log.w(TAG, "Current shade display is null")
            return
@@ -83,7 +83,7 @@ constructor(
            return
        }
        try {
            withContext(mainThreadContext) { moveShadeWindow(toId = destinationId) }
            withContext(mainThreadContext) { reparentToDisplayId(id = destinationId) }
        } catch (e: IllegalStateException) {
            Log.e(
                TAG,
@@ -94,25 +94,8 @@ constructor(
    }

    @UiThread
    private fun moveShadeWindow(toId: Int) {
        traceSection({ "moveShadeWindow  to $toId" }) {
            removeShadeWindow()
            updateContextDisplay(toId)
            addShadeWindow()
        }
    }

    @UiThread
    private fun removeShadeWindow(): Unit =
        traceSection("removeShadeWindow") { wm.removeView(shadeRootView) }

    @UiThread
    private fun addShadeWindow(): Unit =
        traceSection("addShadeWindow") { wm.addView(shadeRootView, shadeLayoutParams) }

    @UiThread
    private fun updateContextDisplay(newDisplayId: Int) {
        traceSection("updateContextDisplay") { shadeContext.updateDisplay(newDisplayId) }
    private fun reparentToDisplayId(id: Int) {
        traceSection({ "reparentToDisplayId(id=$id)" }) { shadeContext.reparentToDisplay(id) }
    }

    private companion object {
+6 −7
Original line number Diff line number Diff line
@@ -17,25 +17,24 @@
package com.android.systemui.shade.domain.interactor

import android.content.mockedContext
import android.view.mockWindowManager
import android.window.WindowContext
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.ui.view.mockShadeRootView
import com.android.systemui.shade.ShadeWindowLayoutParams
import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository
import java.util.Optional
import org.mockito.kotlin.mock

val Kosmos.shadeLayoutParams by Kosmos.Fixture {
    ShadeWindowLayoutParams.create(mockedContext)
}
val Kosmos.shadeLayoutParams by Kosmos.Fixture { ShadeWindowLayoutParams.create(mockedContext) }

val Kosmos.mockedWindowContext by Kosmos.Fixture { mock<WindowContext>() }
val Kosmos.shadeDisplaysInteractor by
    Kosmos.Fixture {
        ShadeDisplaysInteractor(
            Optional.of(mockShadeRootView),
            fakeShadeDisplaysRepository,
            mockedContext,
            shadeLayoutParams,
            mockWindowManager,
            mockedWindowContext,
            testScope.backgroundScope,
            testScope.backgroundScope.coroutineContext,
        )