Loading libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/DefaultWindowDecorViewHost.kt +13 −64 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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, Loading Loading @@ -103,8 +83,7 @@ class DefaultWindowDecorViewHost( override fun release(t: SurfaceControl.Transaction) { clearCurrentUpdateJob() viewHost?.release() t.remove(rootSurface) viewHostAdapter.release(t) } private fun updateViewHost( Loading @@ -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() } Loading @@ -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") } } libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/SurfaceControlViewHostAdapter.kt 0 → 100644 +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") } } libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/DefaultWindowDecorViewHostTest.kt +15 −66 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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) Loading @@ -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, Loading @@ -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) Loading @@ -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) Loading @@ -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 Loading @@ -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() = Loading @@ -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 } libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/SurfaceControlViewHostAdapterTest.kt 0 → 100644 +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 Loading
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/DefaultWindowDecorViewHost.kt +13 −64 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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, Loading Loading @@ -103,8 +83,7 @@ class DefaultWindowDecorViewHost( override fun release(t: SurfaceControl.Transaction) { clearCurrentUpdateJob() viewHost?.release() t.remove(rootSurface) viewHostAdapter.release(t) } private fun updateViewHost( Loading @@ -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() } Loading @@ -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") } }
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/SurfaceControlViewHostAdapter.kt 0 → 100644 +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") } }
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/DefaultWindowDecorViewHostTest.kt +15 −66 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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) Loading @@ -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, Loading @@ -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) Loading @@ -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) Loading @@ -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 Loading @@ -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() = Loading @@ -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 }
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/SurfaceControlViewHostAdapterTest.kt 0 → 100644 +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