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

Commit 52448946 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Introduce ShadeDisplaysInteractor" into main

parents 9c1543b0 69f17cbb
Loading
Loading
Loading
Loading
+159 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.shade.domain.interactor

import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.view.Display
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.data.repository.FakeDisplayWindowPropertiesRepository
import com.android.systemui.display.shared.model.DisplayWindowProperties
import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.data.repository.FakeShadePositionRepository
import com.android.systemui.statusbar.phone.ConfigurationForwarder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
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() {

    private val shadeRootview = mock<WindowRootView>()
    private val positionRepository = FakeShadePositionRepository()
    private val defaultContext = mock<Context>()
    private val secondaryContext = 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 resources = mock<Resources>()
    private val configuration = mock<Configuration>()
    private val display = mock<Display>()

    private val interactor =
        ShadeDisplaysInteractor(
            shadeRootview,
            positionRepository,
            defaultContext,
            contextStore,
            testScope,
            configurationForwarder,
            testScope.coroutineContext,
        )

    @Before
    fun setup() {
        whenever(shadeRootview.display).thenReturn(display)
        whenever(display.displayId).thenReturn(0)

        whenever(resources.configuration).thenReturn(configuration)
        whenever(resources.configuration).thenReturn(configuration)

        whenever(defaultContext.displayId).thenReturn(0)
        whenever(defaultContext.getSystemService(any())).thenReturn(defaultWm)
        whenever(defaultContext.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,
                layoutInflater = mock(),
            )
        )
    }

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

        verifyNoMoreInteractions(defaultWm)
        verifyNoMoreInteractions(secondaryWm)
    }

    @Test
    fun start_shadeInWrongPosition_changes() {
        whenever(display.displayId).thenReturn(0)
        positionRepository.setDisplayId(1)
        interactor.start()
        testScope.advanceUntilIdle()

        verify(defaultWm).removeView(eq(shadeRootview))
        verify(secondaryWm).addView(eq(shadeRootview), any())
    }

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

        positionRepository.setDisplayId(1)
        testScope.advanceUntilIdle()

        verify(defaultWm).removeView(eq(shadeRootview))
        verify(secondaryWm).addView(eq(shadeRootview), any())
    }

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

        positionRepository.setDisplayId(1)
        testScope.advanceUntilIdle()

        verify(configurationForwarder).onConfigurationChanged(eq(configuration))
    }
}
+5 −1
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.display.shared.model.DisplayWindowProperties
import com.android.systemui.res.R
import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.google.common.collect.HashBasedTable
import com.google.common.collect.Table
@@ -60,7 +61,10 @@ constructor(
) : DisplayWindowPropertiesRepository, CoreStartable {

    init {
        StatusBarConnectedDisplays.assertInNewMode()
        check(StatusBarConnectedDisplays.isEnabled || ShadeWindowGoesAround.isEnabled) {
            "This should be instantiated only when wither StatusBarConnectedDisplays or " +
                "ShadeWindowGoesAround are enabled."
        }
    }

    private val properties: Table<Int, Int, DisplayWindowProperties> = HashBasedTable.create()
+1 −2
Original line number Diff line number Diff line
@@ -17,13 +17,12 @@
package com.android.systemui.display.data.repository

import android.view.Display
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.qualifiers.Background
import java.io.PrintWriter
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import com.android.app.tracing.coroutines.launchTraced as launch

/** Provides per display instances of [T]. */
interface PerDisplayStore<T> {
+110 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.shade.domain.interactor

import android.content.Context
import android.util.Log
import android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE
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.ShadePositionRepository
import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import com.android.systemui.statusbar.phone.ConfigurationForwarder
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext

/** Handles Shade window display change when [ShadePositionRepository.displayId] changes. */
@SysUISingleton
class ShadeDisplaysInteractor
@Inject
constructor(
    private val shadeRootView: WindowRootView,
    private val shadePositionRepository: ShadePositionRepository,
    @ShadeDisplayAware private val shadeContext: Context,
    private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository,
    @Background private val bgScope: CoroutineScope,
    @ShadeDisplayAware private val configurationForwarder: ConfigurationForwarder,
    @Main private val mainContext: CoroutineContext,
) : CoreStartable {

    override fun start() {
        ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()
        bgScope.launchTraced(TAG) {
            shadePositionRepository.displayId.collect { displayId -> moveShadeWindowTo(displayId) }
        }
    }

    /** Tries to move the shade. If anything wrong happens, fails gracefully without crashing. */
    private suspend fun moveShadeWindowTo(destinationDisplayId: Int) {
        val currentId = shadeRootView.display.displayId
        if (currentId == destinationDisplayId) {
            Log.w(TAG, "Trying to move the shade to a display it was already in")
            return
        }
        try {
            moveShadeWindow(fromId = currentId, toId = destinationDisplayId)
        } catch (e: IllegalStateException) {
            Log.e(
                TAG,
                "Unable to move the shade window from display $currentId to $destinationDisplayId",
                e,
            )
        }
    }

    private suspend fun moveShadeWindow(fromId: Int, toId: Int) {
        val sourceProperties = getDisplayWindowProperties(fromId)
        val destinationProperties = getDisplayWindowProperties(toId)
        traceSection({ "MovingShadeWindow from $fromId to $toId" }) {
            withContext(mainContext) {
                traceSection("removeView") {
                    sourceProperties.windowManager.removeView(shadeRootView)
                }
                traceSection("addView") {
                    destinationProperties.windowManager.addView(
                        shadeRootView,
                        ShadeWindowLayoutParams.create(shadeContext),
                    )
                }
            }
        }
        traceSection("SecondaryShadeInteractor#onConfigurationChanged") {
            configurationForwarder.onConfigurationChanged(
                destinationProperties.context.resources.configuration
            )
        }
    }

    private fun getDisplayWindowProperties(displayId: Int): DisplayWindowProperties {
        return displayWindowPropertiesRepository.get(displayId, TYPE_NOTIFICATION_SHADE)
    }

    private companion object {
        const val TAG = "SecondaryShadeInteractor"
    }
}
+5 −0
Original line number Diff line number Diff line
@@ -35,4 +35,9 @@ class FakeDisplayWindowPropertiesRepository : DisplayWindowPropertiesRepository
                )
                .also { properties.put(displayId, windowType, it) }
    }

    /** Sets an instance, just for testing purposes. */
    fun insert(instance: DisplayWindowProperties) {
        properties.put(instance.displayId, instance.windowType, instance)
    }
}