Loading libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +15 −0 Original line number Diff line number Diff line Loading @@ -161,6 +161,21 @@ public class DesktopModeStatus { context.getResources().getInteger(R.integer.config_maxDesktopWindowingActiveTasks)); } /** * Return the maximum size of the window decoration surface control view host pool, or zero if * there should be no pooling. */ public static int getWindowDecorScvhPoolSize(@NonNull Context context) { if (!Flags.enableDesktopWindowingScvhCacheBugFix()) return 0; final int maxTaskLimit = getMaxTaskLimit(context); if (maxTaskLimit > 0) { return maxTaskLimit; } // TODO: b/368032552 - task limit equal to 0 means unlimited. Figure out what the pool // size should be in that case. return 0; } /** * Return {@code true} if the current device supports desktop mode. */ Loading libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +6 −0 Original line number Diff line number Diff line Loading @@ -152,6 +152,7 @@ import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel; import com.android.wm.shell.windowdecor.WindowDecorViewModel; import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer; import com.android.wm.shell.windowdecor.common.viewhost.DefaultWindowDecorViewHostSupplier; import com.android.wm.shell.windowdecor.common.viewhost.PooledWindowDecorViewHostSupplier; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier; import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromoController; Loading Loading @@ -347,7 +348,12 @@ public abstract class WMShellModule { @WMSingleton @Provides static WindowDecorViewHostSupplier<WindowDecorViewHost> provideWindowDecorViewHostSupplier( @NonNull Context context, @ShellMainThread @NonNull CoroutineScope mainScope) { final int poolSize = DesktopModeStatus.getWindowDecorScvhPoolSize(context); if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context) && poolSize > 0) { return new PooledWindowDecorViewHostSupplier(mainScope, poolSize); } return new DefaultWindowDecorViewHostSupplier(mainScope); } Loading libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplier.kt 0 → 100644 +70 −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.os.Trace import android.util.Pools import android.view.Display import android.view.SurfaceControl import com.android.wm.shell.shared.annotations.ShellMainThread import kotlinx.coroutines.CoroutineScope /** * A [WindowDecorViewHostSupplier] backed by a pool to allow recycling view hosts which may be * expensive to recreate for each new or updated window decoration. * * Callers can obtain a [WindowDecorViewHost] using [acquire], which will return a pooled * object if available, or create a new instance and return it if needed. When finished using a * [WindowDecorViewHost], it must be released using [release] to allow it to be sent back * into the pool and reused later on. */ class PooledWindowDecorViewHostSupplier( @ShellMainThread private val mainScope: CoroutineScope, maxPoolSize: Int, ) : WindowDecorViewHostSupplier<WindowDecorViewHost> { private val pool: Pools.Pool<WindowDecorViewHost> = Pools.SynchronizedPool(maxPoolSize) private var nextDecorViewHostId = 0 override fun acquire(context: Context, display: Display): WindowDecorViewHost { val pooledViewHost = pool.acquire() if (pooledViewHost != null) { return pooledViewHost } Trace.beginSection("PooledWindowDecorViewHostSupplier#acquire-newInstance") val newDecorViewHost = newInstance(context, display) Trace.endSection() return newDecorViewHost } override fun release(viewHost: WindowDecorViewHost, t: SurfaceControl.Transaction) { val pooled = pool.release(viewHost) if (!pooled) { viewHost.release(t) } } private fun newInstance(context: Context, display: Display): ReusableWindowDecorViewHost { // Use a reusable window decor view host, as it allows swapping the entire view hierarchy. return ReusableWindowDecorViewHost( context = context, mainScope = mainScope, display = display, id = nextDecorViewHostId++ ) } } libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt 0 → 100644 +118 −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.Display import android.view.SurfaceControl import android.view.View import android.view.WindowManager import android.widget.FrameLayout import androidx.tracing.Trace import com.android.internal.annotations.VisibleForTesting import com.android.wm.shell.shared.annotations.ShellMainThread import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch /** * An implementation of [WindowDecorViewHost] that supports: * 1) Replacing the root [View], meaning [WindowDecorViewHost.updateView] maybe be called with * different [View] instances. This is useful when reusing [WindowDecorViewHost]s instances for * vastly different view hierarchies, such as Desktop Windowing's App Handles and App Headers. */ class ReusableWindowDecorViewHost( private val context: Context, @ShellMainThread private val mainScope: CoroutineScope, display: Display, val id: Int, @VisibleForTesting val viewHostAdapter: SurfaceControlViewHostAdapter = SurfaceControlViewHostAdapter(context, display), ) : WindowDecorViewHost { @VisibleForTesting val rootView = FrameLayout(context) private var currentUpdateJob: Job? = null override val surfaceControl: SurfaceControl get() = viewHostAdapter.rootSurface override fun updateView( view: View, attrs: WindowManager.LayoutParams, configuration: Configuration, touchableRegion: Region?, onDrawTransaction: SurfaceControl.Transaction?, ) { Trace.beginSection("ReusableWindowDecorViewHost#updateView") clearCurrentUpdateJob() updateViewHost(view, attrs, configuration, touchableRegion, onDrawTransaction) Trace.endSection() } override fun updateViewAsync( view: View, attrs: WindowManager.LayoutParams, configuration: Configuration, touchableRegion: Region?, ) { Trace.beginSection("ReusableWindowDecorViewHost#updateViewAsync") clearCurrentUpdateJob() currentUpdateJob = mainScope.launch { updateViewHost( view, attrs, configuration, touchableRegion, onDrawTransaction = null, ) } Trace.endSection() } override fun release(t: SurfaceControl.Transaction) { clearCurrentUpdateJob() viewHostAdapter.release(t) } private fun updateViewHost( view: View, attrs: WindowManager.LayoutParams, configuration: Configuration, touchableRegion: Region?, onDrawTransaction: SurfaceControl.Transaction?, ) { Trace.beginSection("ReusableWindowDecorViewHost#updateViewHost") viewHostAdapter.prepareViewHost(configuration, touchableRegion) onDrawTransaction?.let { viewHostAdapter.applyTransactionOnDraw(it) } rootView.removeAllViews() rootView.addView(view) viewHostAdapter.updateView(rootView, attrs) Trace.endSection() } private fun clearCurrentUpdateJob() { currentUpdateJob?.cancel() currentUpdateJob = null } companion object { private const val TAG = "ReusableWindowDecorViewHost" } } libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplierTest.kt 0 → 100644 +129 −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.res.Configuration import android.graphics.Region import android.testing.AndroidTestingRunner import android.view.SurfaceControl import android.view.View import android.view.WindowManager import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase import com.android.wm.shell.util.StubTransaction import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.MockitoAnnotations import org.mockito.kotlin.mock /** * Tests for [PooledWindowDecorViewHostSupplier]. * * Build/Install/Run: atest WMShellUnitTests:PooledWindowDecorViewHostSupplierTest */ @SmallTest @RunWith(AndroidTestingRunner::class) class PooledWindowDecorViewHostSupplierTest : ShellTestCase() { private lateinit var supplier: PooledWindowDecorViewHostSupplier @Test fun setUp() { MockitoAnnotations.initMocks(this) } @Test fun acquire_poolBelowLimit_caches() = runTest { supplier = createSupplier(maxPoolSize = 5) val viewHost = FakeWindowDecorViewHost() supplier.release(viewHost, StubTransaction()) assertThat(supplier.acquire(context, context.display)).isEqualTo(viewHost) } @Test fun release_poolBelowLimit_doesNotReleaseViewHost() = runTest { supplier = createSupplier(maxPoolSize = 5) val viewHost = FakeWindowDecorViewHost() val mockT = mock<SurfaceControl.Transaction>() supplier.release(viewHost, mockT) assertThat(viewHost.released).isFalse() } @Test fun release_poolAtLimit_doesNotCache() = runTest { supplier = createSupplier(maxPoolSize = 1) val viewHost = FakeWindowDecorViewHost() supplier.release(viewHost, StubTransaction()) // Maxes pool. val viewHost2 = FakeWindowDecorViewHost() supplier.release(viewHost2, StubTransaction()) // Beyond limit. assertThat(supplier.acquire(context, context.display)).isEqualTo(viewHost) // Second one wasn't cached, so the acquired one should've been a new instance. assertThat(supplier.acquire(context, context.display)).isNotEqualTo(viewHost2) } @Test fun release_poolAtLimit_releasesViewHost() = runTest { supplier = createSupplier(maxPoolSize = 1) val viewHost = FakeWindowDecorViewHost() supplier.release(viewHost, StubTransaction()) // Maxes pool. val viewHost2 = FakeWindowDecorViewHost() val mockT = mock<SurfaceControl.Transaction>() supplier.release(viewHost2, mockT) // Beyond limit. // Second one doesn't fit, so it needs to be released. assertThat(viewHost2.released).isTrue() } private fun CoroutineScope.createSupplier(maxPoolSize: Int) = PooledWindowDecorViewHostSupplier(this, maxPoolSize) private class FakeWindowDecorViewHost : WindowDecorViewHost { var released = false private set override val surfaceControl: SurfaceControl get() = SurfaceControl() override fun updateView( view: View, attrs: WindowManager.LayoutParams, configuration: Configuration, touchableRegion: Region?, onDrawTransaction: SurfaceControl.Transaction?, ) {} override fun updateViewAsync( view: View, attrs: WindowManager.LayoutParams, configuration: Configuration, touchableRegion: Region?, ) {} override fun release(t: SurfaceControl.Transaction) { released = true } } } Loading
libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +15 −0 Original line number Diff line number Diff line Loading @@ -161,6 +161,21 @@ public class DesktopModeStatus { context.getResources().getInteger(R.integer.config_maxDesktopWindowingActiveTasks)); } /** * Return the maximum size of the window decoration surface control view host pool, or zero if * there should be no pooling. */ public static int getWindowDecorScvhPoolSize(@NonNull Context context) { if (!Flags.enableDesktopWindowingScvhCacheBugFix()) return 0; final int maxTaskLimit = getMaxTaskLimit(context); if (maxTaskLimit > 0) { return maxTaskLimit; } // TODO: b/368032552 - task limit equal to 0 means unlimited. Figure out what the pool // size should be in that case. return 0; } /** * Return {@code true} if the current device supports desktop mode. */ Loading
libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +6 −0 Original line number Diff line number Diff line Loading @@ -152,6 +152,7 @@ import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel; import com.android.wm.shell.windowdecor.WindowDecorViewModel; import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer; import com.android.wm.shell.windowdecor.common.viewhost.DefaultWindowDecorViewHostSupplier; import com.android.wm.shell.windowdecor.common.viewhost.PooledWindowDecorViewHostSupplier; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier; import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromoController; Loading Loading @@ -347,7 +348,12 @@ public abstract class WMShellModule { @WMSingleton @Provides static WindowDecorViewHostSupplier<WindowDecorViewHost> provideWindowDecorViewHostSupplier( @NonNull Context context, @ShellMainThread @NonNull CoroutineScope mainScope) { final int poolSize = DesktopModeStatus.getWindowDecorScvhPoolSize(context); if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context) && poolSize > 0) { return new PooledWindowDecorViewHostSupplier(mainScope, poolSize); } return new DefaultWindowDecorViewHostSupplier(mainScope); } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplier.kt 0 → 100644 +70 −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.os.Trace import android.util.Pools import android.view.Display import android.view.SurfaceControl import com.android.wm.shell.shared.annotations.ShellMainThread import kotlinx.coroutines.CoroutineScope /** * A [WindowDecorViewHostSupplier] backed by a pool to allow recycling view hosts which may be * expensive to recreate for each new or updated window decoration. * * Callers can obtain a [WindowDecorViewHost] using [acquire], which will return a pooled * object if available, or create a new instance and return it if needed. When finished using a * [WindowDecorViewHost], it must be released using [release] to allow it to be sent back * into the pool and reused later on. */ class PooledWindowDecorViewHostSupplier( @ShellMainThread private val mainScope: CoroutineScope, maxPoolSize: Int, ) : WindowDecorViewHostSupplier<WindowDecorViewHost> { private val pool: Pools.Pool<WindowDecorViewHost> = Pools.SynchronizedPool(maxPoolSize) private var nextDecorViewHostId = 0 override fun acquire(context: Context, display: Display): WindowDecorViewHost { val pooledViewHost = pool.acquire() if (pooledViewHost != null) { return pooledViewHost } Trace.beginSection("PooledWindowDecorViewHostSupplier#acquire-newInstance") val newDecorViewHost = newInstance(context, display) Trace.endSection() return newDecorViewHost } override fun release(viewHost: WindowDecorViewHost, t: SurfaceControl.Transaction) { val pooled = pool.release(viewHost) if (!pooled) { viewHost.release(t) } } private fun newInstance(context: Context, display: Display): ReusableWindowDecorViewHost { // Use a reusable window decor view host, as it allows swapping the entire view hierarchy. return ReusableWindowDecorViewHost( context = context, mainScope = mainScope, display = display, id = nextDecorViewHostId++ ) } }
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt 0 → 100644 +118 −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.Display import android.view.SurfaceControl import android.view.View import android.view.WindowManager import android.widget.FrameLayout import androidx.tracing.Trace import com.android.internal.annotations.VisibleForTesting import com.android.wm.shell.shared.annotations.ShellMainThread import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch /** * An implementation of [WindowDecorViewHost] that supports: * 1) Replacing the root [View], meaning [WindowDecorViewHost.updateView] maybe be called with * different [View] instances. This is useful when reusing [WindowDecorViewHost]s instances for * vastly different view hierarchies, such as Desktop Windowing's App Handles and App Headers. */ class ReusableWindowDecorViewHost( private val context: Context, @ShellMainThread private val mainScope: CoroutineScope, display: Display, val id: Int, @VisibleForTesting val viewHostAdapter: SurfaceControlViewHostAdapter = SurfaceControlViewHostAdapter(context, display), ) : WindowDecorViewHost { @VisibleForTesting val rootView = FrameLayout(context) private var currentUpdateJob: Job? = null override val surfaceControl: SurfaceControl get() = viewHostAdapter.rootSurface override fun updateView( view: View, attrs: WindowManager.LayoutParams, configuration: Configuration, touchableRegion: Region?, onDrawTransaction: SurfaceControl.Transaction?, ) { Trace.beginSection("ReusableWindowDecorViewHost#updateView") clearCurrentUpdateJob() updateViewHost(view, attrs, configuration, touchableRegion, onDrawTransaction) Trace.endSection() } override fun updateViewAsync( view: View, attrs: WindowManager.LayoutParams, configuration: Configuration, touchableRegion: Region?, ) { Trace.beginSection("ReusableWindowDecorViewHost#updateViewAsync") clearCurrentUpdateJob() currentUpdateJob = mainScope.launch { updateViewHost( view, attrs, configuration, touchableRegion, onDrawTransaction = null, ) } Trace.endSection() } override fun release(t: SurfaceControl.Transaction) { clearCurrentUpdateJob() viewHostAdapter.release(t) } private fun updateViewHost( view: View, attrs: WindowManager.LayoutParams, configuration: Configuration, touchableRegion: Region?, onDrawTransaction: SurfaceControl.Transaction?, ) { Trace.beginSection("ReusableWindowDecorViewHost#updateViewHost") viewHostAdapter.prepareViewHost(configuration, touchableRegion) onDrawTransaction?.let { viewHostAdapter.applyTransactionOnDraw(it) } rootView.removeAllViews() rootView.addView(view) viewHostAdapter.updateView(rootView, attrs) Trace.endSection() } private fun clearCurrentUpdateJob() { currentUpdateJob?.cancel() currentUpdateJob = null } companion object { private const val TAG = "ReusableWindowDecorViewHost" } }
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplierTest.kt 0 → 100644 +129 −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.res.Configuration import android.graphics.Region import android.testing.AndroidTestingRunner import android.view.SurfaceControl import android.view.View import android.view.WindowManager import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase import com.android.wm.shell.util.StubTransaction import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.MockitoAnnotations import org.mockito.kotlin.mock /** * Tests for [PooledWindowDecorViewHostSupplier]. * * Build/Install/Run: atest WMShellUnitTests:PooledWindowDecorViewHostSupplierTest */ @SmallTest @RunWith(AndroidTestingRunner::class) class PooledWindowDecorViewHostSupplierTest : ShellTestCase() { private lateinit var supplier: PooledWindowDecorViewHostSupplier @Test fun setUp() { MockitoAnnotations.initMocks(this) } @Test fun acquire_poolBelowLimit_caches() = runTest { supplier = createSupplier(maxPoolSize = 5) val viewHost = FakeWindowDecorViewHost() supplier.release(viewHost, StubTransaction()) assertThat(supplier.acquire(context, context.display)).isEqualTo(viewHost) } @Test fun release_poolBelowLimit_doesNotReleaseViewHost() = runTest { supplier = createSupplier(maxPoolSize = 5) val viewHost = FakeWindowDecorViewHost() val mockT = mock<SurfaceControl.Transaction>() supplier.release(viewHost, mockT) assertThat(viewHost.released).isFalse() } @Test fun release_poolAtLimit_doesNotCache() = runTest { supplier = createSupplier(maxPoolSize = 1) val viewHost = FakeWindowDecorViewHost() supplier.release(viewHost, StubTransaction()) // Maxes pool. val viewHost2 = FakeWindowDecorViewHost() supplier.release(viewHost2, StubTransaction()) // Beyond limit. assertThat(supplier.acquire(context, context.display)).isEqualTo(viewHost) // Second one wasn't cached, so the acquired one should've been a new instance. assertThat(supplier.acquire(context, context.display)).isNotEqualTo(viewHost2) } @Test fun release_poolAtLimit_releasesViewHost() = runTest { supplier = createSupplier(maxPoolSize = 1) val viewHost = FakeWindowDecorViewHost() supplier.release(viewHost, StubTransaction()) // Maxes pool. val viewHost2 = FakeWindowDecorViewHost() val mockT = mock<SurfaceControl.Transaction>() supplier.release(viewHost2, mockT) // Beyond limit. // Second one doesn't fit, so it needs to be released. assertThat(viewHost2.released).isTrue() } private fun CoroutineScope.createSupplier(maxPoolSize: Int) = PooledWindowDecorViewHostSupplier(this, maxPoolSize) private class FakeWindowDecorViewHost : WindowDecorViewHost { var released = false private set override val surfaceControl: SurfaceControl get() = SurfaceControl() override fun updateView( view: View, attrs: WindowManager.LayoutParams, configuration: Configuration, touchableRegion: Region?, onDrawTransaction: SurfaceControl.Transaction?, ) {} override fun updateViewAsync( view: View, attrs: WindowManager.LayoutParams, configuration: Configuration, touchableRegion: Region?, ) {} override fun release(t: SurfaceControl.Transaction) { released = true } } }