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

Commit 52e16ad2 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add reusable WindowDecorViewHosts with a pool-backed supplier" into main

parents 0cf5f30d 8c2eacb4
Loading
Loading
Loading
Loading
+17 −0
Original line number Diff line number Diff line
@@ -90,6 +90,9 @@ public class DesktopModeStatus {
    /** The maximum override density allowed for tasks inside the desktop. */
    private static final int DESKTOP_DENSITY_MAX = 1000;

    /** The number of [WindowDecorViewHost] instances to warm up on system start. */
    private static final int WINDOW_DECOR_PRE_WARM_SIZE = 2;

    /**
     * Sysprop declaring the maximum number of Tasks to show in Desktop Mode at any one time.
     *
@@ -101,6 +104,14 @@ public class DesktopModeStatus {
     */
    private static final String MAX_TASK_LIMIT_SYS_PROP = "persist.wm.debug.desktop_max_task_limit";

    /**
     * Sysprop declaring the number of [WindowDecorViewHost] instances to warm up on system start.
     *
     * <p>If it is not defined, then [WINDOW_DECOR_PRE_WARM_SIZE] is used.
     */
    private static final String WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP =
            "persist.wm.debug.desktop_window_decor_pre_warm_size";

    /**
     * Return {@code true} if veiled resizing is active. If false, fluid resizing is used.
     */
@@ -141,6 +152,12 @@ public class DesktopModeStatus {
                context.getResources().getInteger(R.integer.config_maxDesktopWindowingActiveTasks));
    }

    /** The number of [WindowDecorViewHost] instances to warm up on system start. */
    public static int getWindowDecorPreWarmSize() {
        return SystemProperties.getInt(WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP,
                WINDOW_DECOR_PRE_WARM_SIZE);
    }

    /**
     * Return {@code true} if the current device supports desktop mode.
     */
+15 −2
Original line number Diff line number Diff line
@@ -116,6 +116,8 @@ import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel;
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import com.android.wm.shell.windowdecor.viewhost.DefaultWindowDecorViewHostSupplier;
import com.android.wm.shell.windowdecor.viewhost.PooledWindowDecorViewHostSupplier;
import com.android.wm.shell.windowdecor.viewhost.ReusableWindowDecorViewHost;
import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier;

import dagger.Binds;
@@ -380,9 +382,20 @@ public abstract class WMShellModule {
    @WMSingleton
    @Provides
    static WindowDecorViewHostSupplier provideWindowDecorViewHostSupplier(
            @ShellMainThread @NonNull CoroutineScope mainScope) {
            @NonNull Context context,
            @ShellMainThread @NonNull CoroutineScope mainScope,
            @NonNull ShellInit shellInit) {
        if (DesktopModeStatus.canEnterDesktopMode(context)
                && Flags.enableDesktopWindowingScvhCache()) {
            final int maxPoolSize = DesktopModeStatus.getMaxTaskLimit(context);
            final int preWarmSize = DesktopModeStatus.getWindowDecorPreWarmSize();
            return new PooledWindowDecorViewHostSupplier(
                    context, mainScope, shellInit,
                    ReusableWindowDecorViewHost.DefaultFactory.INSTANCE, maxPoolSize, preWarmSize);
        } else {
            return new DefaultWindowDecorViewHostSupplier(mainScope);
        }
    }

    //
    // One handed mode
+11 −60
Original line number Diff line number Diff line
@@ -19,51 +19,33 @@ import android.content.Context
import android.content.res.Configuration
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
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
 * 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: WindowlessWindowManager? = 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,
@@ -92,8 +74,7 @@ class DefaultWindowDecorViewHost(

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

    private fun updateViewHost(
@@ -102,45 +83,15 @@ class DefaultWindowDecorViewHost(
        configuration: Configuration,
        onDrawTransaction: SurfaceControl.Transaction?
    ) {
        Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost")
        if (wwm == null) {
            wwm = WindowlessWindowManager(configuration, rootSurface, null)
        }
        requireWindowlessWindowManager().setConfiguration(configuration)
        if (viewHost == null) {
            viewHost = surfaceControlViewHostFactory.invoke(
                context,
                display,
                requireWindowlessWindowManager(),
                "DefaultWindowDecorViewHost#updateViewHost"
            )
        }
        viewHostAdapter.prepareViewHost(configuration)
        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.applyTransactionOnDraw(it)
        }
        Trace.endSection()
        viewHostAdapter.updateView(view, attrs)
    }

    private fun clearCurrentUpdateJob() {
        currentUpdateJob?.cancel()
        currentUpdateJob = null
    }

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

    private fun requireViewHost(): SurfaceControlViewHost {
        return viewHost ?: error("Expected non-null view host")
    }
}
+105 −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.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 com.android.wm.shell.sysui.ShellInit
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

/**
 * A [WindowDecorViewHostSupplier] backed by a pool to allow recycling view hosts which may be
 * expensive to recreate for each new/updated window decoration.
 *
 * Callers can obtain [ReusableWindowDecorViewHost] using [acquire], which will return a pooled
 * object if available, or create a new instance and return it if needed. When done using a
 * [ReusableWindowDecorViewHost], it must be released using [release] to allow it to be sent back
 * into the pool and reused later on.
 *
 * This class also supports pre-warming [ReusableWindowDecorViewHost] instances, which will be put
 * into the pool immediately after creation.
 */
class PooledWindowDecorViewHostSupplier(
    private val context: Context,
    @ShellMainThread private val mainScope: CoroutineScope,
    shellInit: ShellInit,
    private val viewHostFactory: ReusableWindowDecorViewHost.Factory =
        ReusableWindowDecorViewHost.DefaultFactory,
    maxPoolSize: Int,
    private val preWarmSize: Int,
) : WindowDecorViewHostSupplier<ReusableWindowDecorViewHost> {

    private val pool: Pools.Pool<ReusableWindowDecorViewHost> = Pools.SynchronizedPool(maxPoolSize)
    private var nextDecorViewHostId = 0

    init {
        require(preWarmSize <= maxPoolSize) { "Pre-warm size should not exceed pool size" }
        shellInit.addInitCallback(this::onShellInit, this)
    }

    private fun onShellInit() {
        if (preWarmSize <= 0) {
            return
        }
        preWarmViewHosts(preWarmSize)
    }

    private fun preWarmViewHosts(preWarmSize: Int) {
        mainScope.launch {
            // Applying isn't needed, as the surface was never actually shown.
            val t = SurfaceControl.Transaction()
            repeat(preWarmSize) {
                val warmedViewHost = create(context, context.display).apply {
                    warmUp()
                }
                // Put the warmed view host in the pool by releasing it.
                release(warmedViewHost, t)
            }
        }
    }

    override fun acquire(context: Context, display: Display): ReusableWindowDecorViewHost {
        val reusedDecorViewHost = pool.acquire()
        if (reusedDecorViewHost != null) {
            return reusedDecorViewHost
        }
        Trace.beginSection("WindowDecorViewHostPool#acquire-new")
        val newDecorViewHost = create(context, display)
        Trace.endSection()
        return newDecorViewHost
    }

    override fun release(viewHost: ReusableWindowDecorViewHost, t: SurfaceControl.Transaction) {
        val cached = pool.release(viewHost)
        if (!cached) {
            viewHost.release(t)
        }
    }

    private fun create(context: Context, display: Display): ReusableWindowDecorViewHost {
        return viewHostFactory.create(
            context,
            mainScope,
            display,
            nextDecorViewHostId++
        )
    }
}
+161 −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.viewhost

import android.content.Context
import android.content.res.Configuration
import android.graphics.PixelFormat
import android.os.Trace
import android.view.Display
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
import android.view.View
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
import android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
import android.view.WindowManager.LayoutParams.TYPE_APPLICATION
import android.widget.FrameLayout
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.
 *  2) Pre-warming of the underlying [SurfaceControlViewHost]s. Useful because their creation and
 *    first root view assignment are expensive, which is undesirable in latency-sensitive code
 *    paths like during a shell transition.
 */
class ReusableWindowDecorViewHost(
    private val context: Context,
    @ShellMainThread private val mainScope: CoroutineScope,
    display: Display,
    val id: Int,
    @VisibleForTesting val viewHostAdapter: SurfaceControlViewHostAdapter =
        SurfaceControlViewHostAdapter(context, display)
) : WindowDecorViewHost, Warmable {

    @VisibleForTesting
    val rootView = FrameLayout(context)

    private var currentUpdateJob: Job? = null

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

    override fun warmUp() {
        if (viewHostAdapter.isInitialized()) {
            // Already warmed up.
            return
        }
        Trace.beginSection("$TAG#warmUp")
        viewHostAdapter.prepareViewHost(context.resources.configuration)
        viewHostAdapter.updateView(
            rootView,
            WindowManager.LayoutParams(
                0 /* width*/,
                0 /* height */,
                TYPE_APPLICATION,
                FLAG_NOT_FOCUSABLE or FLAG_SPLIT_TOUCH,
                PixelFormat.TRANSPARENT
            ).apply {
                setTitle("View root of $TAG#$id")
                setTrustedOverlay()
            }
        )
        Trace.endSection()
    }

    override fun updateView(
        view: View,
        attrs: WindowManager.LayoutParams,
        configuration: Configuration,
        onDrawTransaction: SurfaceControl.Transaction?
    ) {
        clearCurrentUpdateJob()
        updateViewHost(view, attrs, configuration, onDrawTransaction)
    }

    override fun updateViewAsync(
        view: View,
        attrs: WindowManager.LayoutParams,
        configuration: Configuration
    ) {
        clearCurrentUpdateJob()
        currentUpdateJob = mainScope.launch {
            updateViewHost(view, attrs, configuration, onDrawTransaction = null)
        }
    }

    override fun release(t: SurfaceControl.Transaction) {
        clearCurrentUpdateJob()
        viewHostAdapter.release(t)
    }

    private fun updateViewHost(
        view: View,
        attrs: WindowManager.LayoutParams,
        configuration: Configuration,
        onDrawTransaction: SurfaceControl.Transaction?
    ) {
        viewHostAdapter.prepareViewHost(configuration)
        onDrawTransaction?.let {
            viewHostAdapter.applyTransactionOnDraw(it)
        }
        rootView.removeAllViews()
        rootView.addView(view)
        viewHostAdapter.updateView(rootView, attrs)
    }

    private fun clearCurrentUpdateJob() {
        currentUpdateJob?.cancel()
        currentUpdateJob = null
    }

    interface Factory {
        fun create(
            context: Context,
            @ShellMainThread mainScope: CoroutineScope,
            display: Display,
            id: Int
        ): ReusableWindowDecorViewHost
    }

    object DefaultFactory : Factory {
        override fun create(
            context: Context,
            @ShellMainThread mainScope: CoroutineScope,
            display: Display,
            id: Int
        ): ReusableWindowDecorViewHost {
            return ReusableWindowDecorViewHost(
                context,
                mainScope,
                display,
                id
            )
        }
    }

    companion object {
        private const val TAG = "ReusableWindowDecorViewHost"
    }
}
 No newline at end of file
Loading