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

Commit 513caf7c authored by Jorge Gil's avatar Jorge Gil
Browse files

[5/N] WindowDecorViewHost: Add pooled supplier & reusable ViewHost

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

Adds a ReusableWindowDecorViewHost that is able to swap view hierarchies
(e.g. App Handle <-> App Header) without having to release and create a
new SCVH. This is done by putting the desired view hierarchy inside a
root View that never changes.
Also adds a Pooled supplier that uses reusable view hosts with a
capacity equal to the desktop task limit.

Bug: 360452034
Flag: com.android.window.flags.enable_desktop_windowing_scvh_cache_bug_fix
Test: check perfetto trace for reused SCVHs

Change-Id: I30555d86ec6995decbca8a47c1008f1b79020899
parent 9bb7006b
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -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.
     */
+6 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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);
    }

+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++
        )
    }
}
+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"
    }
}
+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