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

Commit e92bb92a authored by Chris Göllner's avatar Chris Göllner
Browse files

Create DisplayWindowPropertiesRepository

The repository will provide per display window properties. These
properties are display specific and window type related.

If for example someone wants to add a window of type TYPE_STATUS_BAR to
an external display, the properties will contain the correct Context and
WindowManager to be able to do so.

Test: DisplayWindowPropertiesRepositoryImplTest.kt
Bug: 367592591
Flag: com.android.systemui.status_bar_connected_displays
Change-Id: I5da93401522d46eebe30a534bc67abf7ec4382d0
parent 9228b084
Loading
Loading
Loading
Loading
+21 −0
Original line number Diff line number Diff line
@@ -24,6 +24,8 @@ import com.android.systemui.display.data.repository.DisplayRepository
import com.android.systemui.display.data.repository.DisplayRepositoryImpl
import com.android.systemui.display.data.repository.DisplayScopeRepository
import com.android.systemui.display.data.repository.DisplayScopeRepositoryImpl
import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository
import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepositoryImpl
import com.android.systemui.display.data.repository.FocusedDisplayRepository
import com.android.systemui.display.data.repository.FocusedDisplayRepositoryImpl
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
@@ -58,6 +60,11 @@ interface DisplayModule {

    @Binds fun displayScopeRepository(impl: DisplayScopeRepositoryImpl): DisplayScopeRepository

    @Binds
    fun displayWindowPropertiesRepository(
        impl: DisplayWindowPropertiesRepositoryImpl
    ): DisplayWindowPropertiesRepository

    companion object {
        @Provides
        @SysUISingleton
@@ -72,5 +79,19 @@ interface DisplayModule {
                CoreStartable.NOP
            }
        }

        @Provides
        @SysUISingleton
        @IntoMap
        @ClassKey(DisplayWindowPropertiesRepository::class)
        fun displayWindowPropertiesRepoAsCoreStartable(
            repoLazy: Lazy<DisplayWindowPropertiesRepositoryImpl>
        ): CoreStartable {
            return if (StatusBarConnectedDisplays.isEnabled) {
                return repoLazy.get()
            } else {
                CoreStartable.NOP
            }
        }
    }
}
+115 −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.display.data.repository

import android.annotation.SuppressLint
import android.content.Context
import android.view.Display
import android.view.WindowManager
import com.android.systemui.CoreStartable
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.statusbar.core.StatusBarConnectedDisplays
import com.google.common.collect.HashBasedTable
import com.google.common.collect.Table
import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

/** Provides per display instances of [DisplayWindowProperties]. */
interface DisplayWindowPropertiesRepository {

    /**
     * Returns a [DisplayWindowProperties] instance for a given display id and window type.
     *
     * @throws IllegalArgumentException if no display with the given display id exists.
     */
    fun get(
        displayId: Int,
        @WindowManager.LayoutParams.WindowType windowType: Int,
    ): DisplayWindowProperties
}

@SysUISingleton
class DisplayWindowPropertiesRepositoryImpl
@Inject
constructor(
    @Background private val backgroundApplicationScope: CoroutineScope,
    private val globalContext: Context,
    private val globalWindowManager: WindowManager,
    private val displayRepository: DisplayRepository,
) : DisplayWindowPropertiesRepository, CoreStartable {

    init {
        StatusBarConnectedDisplays.assertInNewMode()
    }

    private val properties: Table<Int, Int, DisplayWindowProperties> = HashBasedTable.create()

    override fun get(
        displayId: Int,
        @WindowManager.LayoutParams.WindowType windowType: Int,
    ): DisplayWindowProperties {
        val display =
            displayRepository.getDisplay(displayId)
                ?: throw IllegalArgumentException("Display with id $displayId doesn't exist")
        return properties.get(displayId, windowType)
            ?: create(display, windowType).also { properties.put(displayId, windowType, it) }
    }

    override fun start() {
        backgroundApplicationScope.launch(
            CoroutineName("DisplayWindowPropertiesRepositoryImpl#start")
        ) {
            displayRepository.displayRemovalEvent.collect { removedDisplayId ->
                properties.row(removedDisplayId).clear()
            }
        }
    }

    private fun create(display: Display, windowType: Int): DisplayWindowProperties {
        val displayId = display.displayId
        return if (displayId == Display.DEFAULT_DISPLAY) {
            // For the default display, we can just reuse the global/application properties.
            // Creating a window context is expensive, therefore we avoid it.
            DisplayWindowProperties(
                displayId = displayId,
                windowType = windowType,
                context = globalContext,
                windowManager = globalWindowManager,
            )
        } else {
            val context = createWindowContext(display, windowType)
            @SuppressLint("NonInjectedService") // Need to manually get the service
            val windowManager = context.getSystemService(WindowManager::class.java) as WindowManager
            DisplayWindowProperties(displayId, windowType, context, windowManager)
        }
    }

    private fun createWindowContext(display: Display, windowType: Int): Context =
        globalContext.createWindowContext(display, windowType, /* options= */ null).also {
            it.setTheme(R.style.Theme_SystemUI)
        }

    override fun dump(pw: PrintWriter, args: Array<out String>) {
        pw.write("perDisplayContexts: $properties")
    }
}
+43 −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.display.shared.model

import android.content.Context
import android.view.WindowManager

/** Represents a display specific group of window related properties. */
data class DisplayWindowProperties(
    /** The id of the display associated with this instance. */
    val displayId: Int,
    /**
     * The window type that was used to create the [Context] in this instance, using
     * [Context.createWindowContext]. This is the window type that can be used when adding views to
     * the [WindowManager] associated with this instance.
     */
    @WindowManager.LayoutParams.WindowType val windowType: Int,
    /**
     * The display specific [Context] created using [Context.createWindowContext] with window type
     * associated with this instance.
     */
    val context: Context,

    /**
     * The display specific [WindowManager] instance to be used when adding windows of the type
     * associated with this instance.
     */
    val windowManager: WindowManager,
)
+158 −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.display.data.repository

import android.content.testableContext
import android.platform.test.annotations.EnableFlags
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.display.shared.model.DisplayWindowProperties
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.unconfinedTestDispatcher
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock

@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
@RunWith(AndroidJUnit4::class)
@SmallTest
class DisplayWindowPropertiesRepositoryImplTest : SysuiTestCase() {

    private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
    private val fakeDisplayRepository = kosmos.displayRepository
    private val testScope = kosmos.testScope

    private val applicationContext = kosmos.testableContext
    private val applicationWindowManager = kosmos.mockWindowManager

    private val repo =
        DisplayWindowPropertiesRepositoryImpl(
            kosmos.applicationCoroutineScope,
            applicationContext,
            applicationWindowManager,
            fakeDisplayRepository,
        )

    @Before
    fun start() {
        repo.start()
    }

    @Before
    fun addDisplays() = runBlocking {
        fakeDisplayRepository.addDisplay(createDisplay(DEFAULT_DISPLAY_ID))
        fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID))
    }

    @Test
    fun get_defaultDisplayId_returnsDefaultProperties() =
        testScope.runTest {
            val displayContext = repo.get(DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)

            assertThat(displayContext)
                .isEqualTo(
                    DisplayWindowProperties(
                        displayId = DEFAULT_DISPLAY_ID,
                        windowType = WINDOW_TYPE_FOO,
                        context = applicationContext,
                        windowManager = applicationWindowManager,
                    )
                )
        }

    @Test
    fun get_nonDefaultDisplayId_returnsNewStatusBarContext() =
        testScope.runTest {
            val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)

            assertThat(displayContext.context).isNotSameInstanceAs(applicationContext)
        }

    @Test
    fun get_nonDefaultDisplayId_returnsNewWindowManager() =
        testScope.runTest {
            val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)

            assertThat(displayContext.windowManager).isNotSameInstanceAs(applicationWindowManager)
        }

    @Test
    fun get_multipleCallsForDefaultDisplay_returnsSameInstance() =
        testScope.runTest {
            val displayContext = repo.get(DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)

            assertThat(repo.get(DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO))
                .isSameInstanceAs(displayContext)
        }

    @Test
    fun get_multipleCallsForNonDefaultDisplay_returnsSameInstance() =
        testScope.runTest {
            val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)

            assertThat(repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO))
                .isSameInstanceAs(displayContext)
        }

    @Test
    fun get_multipleCalls_differentType_returnsNewInstance() =
        testScope.runTest {
            val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)

            assertThat(repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_BAR))
                .isNotSameInstanceAs(displayContext)
        }

    @Test
    fun get_afterDisplayRemoved_returnsNewInstance() =
        testScope.runTest {
            val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)

            fakeDisplayRepository.removeDisplay(NON_DEFAULT_DISPLAY_ID)
            fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID))

            assertThat(repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO))
                .isNotSameInstanceAs(displayContext)
        }

    @Test(expected = IllegalArgumentException::class)
    fun get_nonExistingDisplayId_throws() =
        testScope.runTest { repo.get(NON_EXISTING_DISPLAY_ID, WINDOW_TYPE_FOO) }

    private fun createDisplay(displayId: Int) =
        mock<Display> { on { getDisplayId() } doReturn displayId }

    companion object {
        private const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY
        private const val NON_DEFAULT_DISPLAY_ID = DEFAULT_DISPLAY_ID + 1
        private const val NON_EXISTING_DISPLAY_ID = DEFAULT_DISPLAY_ID + 2
        private const val WINDOW_TYPE_FOO = 123
        private const val WINDOW_TYPE_BAR = 321
    }
}
+26 −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.display.data.repository

import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher

val Kosmos.fakeDisplayScopeRepository by
    Kosmos.Fixture { FakeDisplayScopeRepository(testDispatcher) }

var Kosmos.displayScopeRepository: DisplayScopeRepository by
    Kosmos.Fixture { fakeDisplayScopeRepository }
Loading