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

Commit 9bb7006b authored by Jorge Gil's avatar Jorge Gil
Browse files

[4/N] WindowDecorViewHost: Add SurfaceControlViewHostAdapter

Spiritual revert^2 of I08111bfd4728e5223ed078916255313b13a4093f, but
broken down into smaller changes.

Introduces an adapter around SurfaceControlViewHost that handles the
setup needed to start using one: creating the backing surface and
windowless window manager. It also wraps #setView and #relayout into a
single #updateView function that internally checks which is needed and
whether an illegal view swap is being attempted.
This adapter abstracts some common utils out of
DefaultWindowDecorViewHost so that they can be shared between it and the
upcoming ReusableWindowDecorViewHost in a follow up CL.

This is also a pure refactor, so no behavior or performance changes are
expected with this CL alone.

Bug: 360452034
Flag: EXEMPT refactor
Test: atest WMShellUnitTests
Change-Id: I9738a6caf4f2efb25b1e30e5fee46ff6b150cf1b
parent 66b19001
Loading
Loading
Loading
Loading
+13 −64
Original line number Diff line number Diff line
@@ -20,10 +20,8 @@ import android.content.res.Configuration
import android.graphics.Region
import android.view.Display
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
import android.view.View
import android.view.WindowManager
import android.view.WindowlessWindowManager
import androidx.tracing.Trace
import com.android.internal.annotations.VisibleForTesting
import com.android.wm.shell.shared.annotations.ShellMainThread
@@ -31,41 +29,23 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

typealias SurfaceControlViewHostFactory =
    (Context, Display, WindowlessWindowManager, String) -> SurfaceControlViewHost

/**
 * A default implementation of [WindowDecorViewHost] backed by a [SurfaceControlViewHost].
 * A default implementation of [WindowDecorViewHost] backed by a [SurfaceControlViewHostAdapter].
 *
 * It does not support swapping the root view added to the VRI of the [SurfaceControlViewHost], and
 * any attempts to do will throw, which means that once a [View] is added using [updateView] or
 * [updateViewAsync], only its properties and binding may be changed, its children views may be
 * added, removed or changed and its [WindowManager.LayoutParams] may be changed. It also supports
 * asynchronously updating the view hierarchy using [updateViewAsync], in which case the update work
 * will be posted on the [ShellMainThread] with no delay.
 * It supports asynchronously updating the view hierarchy using [updateViewAsync], in which
 * case the update work will be posted on the [ShellMainThread] with no delay.
 */
class DefaultWindowDecorViewHost(
    private val context: Context,
    context: Context,
    @ShellMainThread private val mainScope: CoroutineScope,
    private val display: Display,
    private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory = { c, d, wwm, s ->
        SurfaceControlViewHost(c, d, wwm, s)
    },
    display: Display,
    @VisibleForTesting val viewHostAdapter: SurfaceControlViewHostAdapter =
        SurfaceControlViewHostAdapter(context, display),
) : WindowDecorViewHost {

    private val rootSurface: SurfaceControl =
        SurfaceControl.Builder()
            .setName("DefaultWindowDecorViewHost surface")
            .setContainerLayer()
            .setCallsite("DefaultWindowDecorViewHost#init")
            .build()

    private var wwm: WindowDecorWindowlessWindowManager? = null
    @VisibleForTesting var viewHost: SurfaceControlViewHost? = null
    private var currentUpdateJob: Job? = null

    override val surfaceControl: SurfaceControl
        get() = rootSurface
        get() = viewHostAdapter.rootSurface

    override fun updateView(
        view: View,
@@ -103,8 +83,7 @@ class DefaultWindowDecorViewHost(

    override fun release(t: SurfaceControl.Transaction) {
        clearCurrentUpdateJob()
        viewHost?.release()
        t.remove(rootSurface)
        viewHostAdapter.release(t)
    }

    private fun updateViewHost(
@@ -115,33 +94,11 @@ class DefaultWindowDecorViewHost(
        onDrawTransaction: SurfaceControl.Transaction?,
    ) {
        Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost")
        if (wwm == null) {
            wwm = WindowDecorWindowlessWindowManager(configuration, rootSurface)
        }
        if (viewHost == null) {
            viewHost =
                surfaceControlViewHostFactory.invoke(
                    context,
                    display,
                    requireWindowlessWindowManager(),
                    "DefaultWindowDecorViewHost#updateViewHost",
                )
        }
        requireWindowlessWindowManager().apply {
            setConfiguration(configuration)
            setTouchRegion(requireViewHost(), touchableRegion)
        }
        onDrawTransaction?.let { requireViewHost().rootSurfaceControl.applyTransactionOnDraw(it) }
        if (requireViewHost().view == null) {
            Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost-setView")
            requireViewHost().setView(view, attrs)
            Trace.endSection()
        } else {
            check(requireViewHost().view == view) { "Changing view is not allowed" }
            Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost-relayout")
            requireViewHost().relayout(attrs)
            Trace.endSection()
        viewHostAdapter.prepareViewHost(configuration, touchableRegion)
        onDrawTransaction?.let {
            viewHostAdapter.applyTransactionOnDraw(it)
        }
        viewHostAdapter.updateView(view, attrs)
        Trace.endSection()
    }

@@ -149,12 +106,4 @@ class DefaultWindowDecorViewHost(
        currentUpdateJob?.cancel()
        currentUpdateJob = null
    }

    private fun requireWindowlessWindowManager(): WindowDecorWindowlessWindowManager {
        return wwm ?: error("Expected non-null windowless window manager")
    }

    private fun requireViewHost(): SurfaceControlViewHost {
        return viewHost ?: error("Expected non-null view host")
    }
}
+120 −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.wm.shell.windowdecor.common.viewhost

import android.content.Context
import android.content.res.Configuration
import android.graphics.Region
import android.view.AttachedSurfaceControl
import android.view.Display
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
import android.view.View
import android.view.WindowManager
import android.view.WindowlessWindowManager
import androidx.tracing.Trace
import com.android.internal.annotations.VisibleForTesting

typealias SurfaceControlViewHostFactory =
    (Context, Display, WindowlessWindowManager, String) -> SurfaceControlViewHost

/**
 * Adapter for a [SurfaceControlViewHost] and its backing [SurfaceControl].
 *
 * It does not support swapping the root view added to the VRI of the [SurfaceControlViewHost], and
 * any attempts to do will throw, which means that once a [View] is added using [updateView], only
 * its properties and binding may be changed, children views may be added, removed or changed
 * and its [WindowManager.LayoutParams] may be changed.
 */
class SurfaceControlViewHostAdapter(
    private val context: Context,
    private val display: Display,
    private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory = { c, d, wwm, s ->
        SurfaceControlViewHost(c, d, wwm, s)
    },
) {
    val rootSurface: SurfaceControl =
        SurfaceControl.Builder()
            .setName("SurfaceControlViewHostAdapter surface")
            .setContainerLayer()
            .setCallsite("SurfaceControlViewHostAdapter#init")
            .build()

    private var wwm: WindowDecorWindowlessWindowManager? = null
    @VisibleForTesting var viewHost: SurfaceControlViewHost? = null

    /**
     * Initialize or updates the [SurfaceControlViewHost].
     */
    fun prepareViewHost(
        configuration: Configuration,
        touchableRegion: Region?
    ) {
        if (wwm == null) {
            wwm = WindowDecorWindowlessWindowManager(configuration, rootSurface)
        }
        if (viewHost == null) {
            viewHost =
                surfaceControlViewHostFactory.invoke(
                    context,
                    display,
                    requireWindowlessWindowManager(),
                    "SurfaceControlViewHostAdapter#prepareViewHost",
                )
        }
        requireWindowlessWindowManager().setConfiguration(configuration)
        requireWindowlessWindowManager().setTouchRegion(requireViewHost(), touchableRegion)
    }

    /**
     * Request to apply the transaction atomically with the next draw of the view hierarchy. See
     * [AttachedSurfaceControl.applyTransactionOnDraw].
     */
    fun applyTransactionOnDraw(t: SurfaceControl.Transaction) {
        requireViewHost().rootSurfaceControl.applyTransactionOnDraw(t)
    }

    /** Update the view hierarchy of the view host. */
    fun updateView(view: View, attrs: WindowManager.LayoutParams) {
        if (requireViewHost().view == null) {
            Trace.beginSection("SurfaceControlViewHostAdapter#updateView-setView")
            requireViewHost().setView(view, attrs)
            Trace.endSection()
        } else {
            check(requireViewHost().view == view) { "Changing view is not allowed" }
            Trace.beginSection("SurfaceControlViewHostAdapter#updateView-relayout")
            requireViewHost().relayout(attrs)
            Trace.endSection()
        }
    }

    /** Release the view host and remove the backing surface. */
    fun release(t: SurfaceControl.Transaction) {
        viewHost?.release()
        t.remove(rootSurface)
    }

    /** Whether the view host has had a view hierarchy set. */
    fun isInitialized(): Boolean = viewHost?.view != null

    private fun requireWindowlessWindowManager(): WindowDecorWindowlessWindowManager {
        return wwm ?: error("Expected non-null windowless window manager")
    }

    private fun requireViewHost(): SurfaceControlViewHost {
        return viewHost ?: error("Expected non-null view host")
    }
}
+15 −66
Original line number Diff line number Diff line
@@ -18,7 +18,6 @@ package com.android.wm.shell.windowdecor.common.viewhost
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
import android.view.View
import android.view.WindowManager
import androidx.test.filters.SmallTest
@@ -28,7 +27,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
@@ -57,54 +55,8 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() {
            onDrawTransaction = null,
        )

        assertThat(windowDecorViewHost.viewHost).isNotNull()
        assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(view)
    }

    @Test
    fun updateView_alreadyLaidOut_relayouts() = runTest {
        val windowDecorViewHost = createDefaultViewHost()
        val view = View(context)
        windowDecorViewHost.updateView(
            view = view,
            attrs = WindowManager.LayoutParams(100, 100),
            configuration = context.resources.configuration,
            onDrawTransaction = null,
        )

        val otherParams = WindowManager.LayoutParams(200, 200)
        windowDecorViewHost.updateView(
            view = view,
            attrs = otherParams,
            configuration = context.resources.configuration,
            onDrawTransaction = null,
        )

        assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(view)
        assertThat(windowDecorViewHost.viewHost!!.view!!.layoutParams.width)
            .isEqualTo(otherParams.width)
    }

    @Test
    fun updateView_replacingView_throws() = runTest {
        val windowDecorViewHost = createDefaultViewHost()
        val view = View(context)
        windowDecorViewHost.updateView(
            view = view,
            attrs = WindowManager.LayoutParams(100, 100),
            configuration = context.resources.configuration,
            onDrawTransaction = null,
        )

        val otherView = View(context)
        assertThrows(Exception::class.java) {
            windowDecorViewHost.updateView(
                view = otherView,
                attrs = WindowManager.LayoutParams(100, 100),
                configuration = context.resources.configuration,
                onDrawTransaction = null,
            )
        }
        assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue()
        assertThat(windowDecorViewHost.view()).isEqualTo(view)
    }

    @OptIn(ExperimentalCoroutinesApi::class)
@@ -123,7 +75,7 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() {
        )

        // No view host yet, since the coroutine hasn't run.
        assertThat(windowDecorViewHost.viewHost).isNull()
        assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isFalse()

        windowDecorViewHost.updateView(
            view = syncView,
@@ -135,14 +87,13 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() {
        // Would run coroutine if it hadn't been cancelled.
        advanceUntilIdle()

        assertThat(windowDecorViewHost.viewHost).isNotNull()
        assertThat(windowDecorViewHost.viewHost!!.view).isNotNull()
        assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue()
        assertThat(windowDecorViewHost.view()).isNotNull()
        // View host view/attrs should match the ones from the sync call, plus, since the
        // sync/async were made with different views, if the job hadn't been cancelled there
        // would've been an exception thrown as replacing views isn't allowed.
        assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(syncView)
        assertThat(windowDecorViewHost.viewHost!!.view!!.layoutParams.width)
            .isEqualTo(syncAttrs.width)
        assertThat(windowDecorViewHost.view()).isEqualTo(syncView)
        assertThat(windowDecorViewHost.view()!!.layoutParams.width).isEqualTo(syncAttrs.width)
    }

    @OptIn(ExperimentalCoroutinesApi::class)
@@ -158,11 +109,11 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() {
            configuration = context.resources.configuration,
        )

        assertThat(windowDecorViewHost.viewHost).isNull()
        assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isFalse()

        advanceUntilIdle()

        assertThat(windowDecorViewHost.viewHost).isNotNull()
        assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue()
    }

    @OptIn(ExperimentalCoroutinesApi::class)
@@ -185,9 +136,8 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() {

        advanceUntilIdle()

        assertThat(windowDecorViewHost.viewHost).isNotNull()
        assertThat(windowDecorViewHost.viewHost!!.view).isNotNull()
        assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(otherView)
        assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue()
        assertThat(windowDecorViewHost.view()).isEqualTo(otherView)
    }

    @Test
@@ -205,8 +155,7 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() {
        val t = mock(SurfaceControl.Transaction::class.java)
        windowDecorViewHost.release(t)

        verify(windowDecorViewHost.viewHost!!).release()
        verify(t).remove(windowDecorViewHost.surfaceControl)
        verify(windowDecorViewHost.viewHostAdapter).release(t)
    }

    private fun CoroutineScope.createDefaultViewHost() =
@@ -214,8 +163,8 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() {
            context = context,
            mainScope = this,
            display = context.display,
            surfaceControlViewHostFactory = { c, d, wwm, s ->
                spy(SurfaceControlViewHost(c, d, wwm, s))
            },
            viewHostAdapter = spy(SurfaceControlViewHostAdapter(context, context.display)),
        )

    private fun DefaultWindowDecorViewHost.view(): View? = viewHostAdapter.viewHost?.view
}
+144 −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.wm.shell.windowdecor.common.viewhost

import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
import android.view.View
import android.view.WindowManager
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.verify

/**
 * Tests for [SurfaceControlViewHostAdapter].
 *
 * Build/Install/Run:
 * atest WMShellUnitTests:SurfaceControlViewHostAdapterTest
 */
@SmallTest
@TestableLooper.RunWithLooper
@RunWith(AndroidTestingRunner::class)
class SurfaceControlViewHostAdapterTest : ShellTestCase() {

    private lateinit var adapter: SurfaceControlViewHostAdapter

    @Before
    fun setUp() {
        adapter = SurfaceControlViewHostAdapter(
            context,
            context.display,
            surfaceControlViewHostFactory = { c, d, wwm, s ->
                spy(SurfaceControlViewHost(c, d, wwm, s))
            }
        )
    }

    @Test
    fun prepareViewHost() {
        adapter.prepareViewHost(context.resources.configuration, touchableRegion = null)

        assertThat(adapter.viewHost).isNotNull()
    }

    @Test
    fun prepareViewHost_alreadyCreated_skips() {
        adapter.prepareViewHost(context.resources.configuration, touchableRegion = null)

        val viewHost = adapter.viewHost!!

        adapter.prepareViewHost(context.resources.configuration, touchableRegion = null)

        assertThat(adapter.viewHost).isEqualTo(viewHost)
    }

    @Test
    fun updateView_layoutInViewHost() {
        val view = View(context)
        adapter.prepareViewHost(context.resources.configuration, touchableRegion = null)

        adapter.updateView(
            view = view,
            attrs = WindowManager.LayoutParams(100, 100)
        )

        assertThat(adapter.isInitialized()).isTrue()
        assertThat(adapter.view()).isEqualTo(view)
    }

    @Test
    fun updateView_alreadyLaidOut_relayouts() {
        val view = View(context)
        adapter.prepareViewHost(context.resources.configuration, touchableRegion = null)
        adapter.updateView(
            view = view,
            attrs = WindowManager.LayoutParams(100, 100)
        )

        val otherParams = WindowManager.LayoutParams(200, 200)
        adapter.updateView(
            view = view,
            attrs = otherParams
        )

        assertThat(adapter.view()).isEqualTo(view)
        assertThat(adapter.view()!!.layoutParams.width).isEqualTo(otherParams.width)
    }

    @Test
    fun updateView_replacingView_throws() {
        val view = View(context)
        adapter.prepareViewHost(context.resources.configuration, touchableRegion = null)
        adapter.updateView(
            view = view,
            attrs = WindowManager.LayoutParams(100, 100)
        )

        val otherView = View(context)
        assertThrows(Exception::class.java) {
            adapter.updateView(
                view = otherView,
                attrs = WindowManager.LayoutParams(100, 100)
            )
        }
    }

    @Test
    fun release() {
        adapter.prepareViewHost(context.resources.configuration, touchableRegion = null)
        adapter.updateView(
            view = View(context),
            attrs = WindowManager.LayoutParams(100, 100)
        )

        val mockT = mock(SurfaceControl.Transaction::class.java)
        adapter.release(mockT)

        verify(adapter.viewHost!!).release()
        verify(mockT).remove(adapter.rootSurface)
    }

    private fun SurfaceControlViewHostAdapter.view(): View? = viewHost?.view
}
 No newline at end of file