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

Commit 47a6106b authored by Pierre Barbier de Reuille's avatar Pierre Barbier de Reuille
Browse files

Replaces DesktopModeStatus with two objects.

Replaces a set of static methods with two objects:

1) DesktopModeConfig, which should only be used within WM Shell and
   contains constants needed for Desktop Experiences.
2) DesktopModeFeatures, which will be shared between WM Shell, System UI
   and Launcher, containing properties and methods to check what is
   enabled or not within Desktop Experiences.

Bug: 395863348
Test: atest DesktopModeConfigImplTest
Test: atest DesktopModeFeaturesImplTest
Flag: EXEMPT (refactoring)
Change-Id: I5c7d2903169a42579284344f5d87a36669f14410
parent 98424bf3
Loading
Loading
Loading
Loading
+91 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.shared.desktopmode

import android.app.TaskInfo
import android.content.Context
import com.android.internal.annotations.VisibleForTesting
import java.io.PrintWriter

/**
 * Configuration of the desktop mode. Defines the parameters used by various features.
 *
 * This class shouldn't be used outside of WM Shell.
 */
@Suppress("INAPPLICABLE_JVM_NAME")
interface DesktopConfig {
    /**
     * Whether a window should be maximized when it's dragged to the top edge of the screen.
     */
    val shouldMaximizeWhenDragToTopEdge: Boolean

    /** Whether the override desktop density is enabled and valid. */
    @get:JvmName("useDesktopOverrideDensity")
    val useDesktopOverrideDensity: Boolean

    /** The number of [WindowDecorViewHost] instances to warm up on system start. */
    val windowDecorPreWarmSize: Int

    /**
     * The maximum size of the window decoration surface control view host pool, or zero if there
     * should be no pooling.
     */
    val windowDecorScvhPoolSize: Int

    /**
     * Whether veiled resizing is enabled.
     */
    val isVeiledResizeEnabled: Boolean

    /** Returns `true` if the app-to-web feature is using the build-time generic links list. */
    @get:JvmName("useAppToWebBuildTimeGenericLinks")
    val useAppToWebBuildTimeGenericLinks: Boolean

    /** Returns whether to use rounded corners for windows. */
    @get:JvmName("useRoundedCorners")
    val useRoundedCorners: Boolean

    /**
     * Returns whether to use window shadows, [isFocusedWindow] indicating whether or not the window
     * currently holds the focus.
     */
    fun useWindowShadow(isFocusedWindow: Boolean): Boolean

    /**
     * Whether we set opaque background for all freeform tasks.
     *
     * This might be done to prevent freeform tasks below from being visible if freeform task window
     * above is translucent. Otherwise if fluid resize is enabled, add a background to freeform
     * tasks.
     */
    fun shouldSetBackground(taskInfo: TaskInfo): Boolean

    /** Returns the maximum limit on the number of tasks to show in on a desk at any one time. */
    val maxTaskLimit: Int

    /** Override density for tasks when they're inside the desktop.  */
    val desktopDensityOverride: Int

    /** Dumps DesktopModeStatus flags and configs. */
    fun dump(pw: PrintWriter, prefix: String)

    companion object {
        /** Create a [DesktopConfig] from a context. Should only be used for testing. */
        @VisibleForTesting
        fun fromContext(context: Context): DesktopConfig = DesktopConfigImpl(context)
    }
}
+180 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.shared.desktopmode

import android.app.TaskInfo
import android.content.Context
import android.os.SystemProperties
import android.util.IndentingPrintWriter
import android.window.DesktopExperienceFlags
import android.window.DesktopModeFlags
import com.android.internal.R
import com.android.internal.annotations.VisibleForTesting
import com.android.wm.shell.shared.desktopmode.DesktopConfigImpl.Companion.WINDOW_DECOR_PRE_WARM_SIZE
import java.io.PrintWriter

@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
class DesktopConfigImpl(
    private val context: Context,
    private val desktopState: DesktopState,
) : DesktopConfig {

    constructor(context: Context) : this(context, DesktopState.fromContext(context))

    override val shouldMaximizeWhenDragToTopEdge: Boolean
        get() {
            if (!DesktopExperienceFlags.ENABLE_DRAG_TO_MAXIMIZE.isTrue) return false
            return SystemProperties.getBoolean(
                ENABLE_DRAG_TO_MAXIMIZE_SYS_PROP,
                context.getResources().getBoolean(R.bool.config_dragToMaximizeInDesktopMode),
            )
        }

    override val useDesktopOverrideDensity: Boolean =
        DESKTOP_DENSITY_OVERRIDE_ENABLED && isValidDesktopDensityOverrideSet()

    /** Return `true` if the override desktop density is set and within a valid range. */
    private fun isValidDesktopDensityOverrideSet() =
        DESKTOP_DENSITY_OVERRIDE >= DESKTOP_DENSITY_MIN &&
            DESKTOP_DENSITY_OVERRIDE <= DESKTOP_DENSITY_MAX

    override val windowDecorPreWarmSize: Int =
        SystemProperties.getInt(WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP, WINDOW_DECOR_PRE_WARM_SIZE)

    override val windowDecorScvhPoolSize: Int
        get() {
            if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_SCVH_CACHE.isTrue) return 0

            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
        }

    override val isVeiledResizeEnabled: Boolean =
        SystemProperties.getBoolean("persist.wm.debug.desktop_veiled_resizing", true)

    override val useAppToWebBuildTimeGenericLinks: Boolean =
        SystemProperties.getBoolean(
            "persist.wm.debug.use_app_to_web_build_time_generic_links",
            true,
        )

    override val useRoundedCorners: Boolean =
        SystemProperties.getBoolean("persist.wm.debug.desktop_use_rounded_corners", true)

    override fun useWindowShadow(isFocusedWindow: Boolean): Boolean =
        if (isFocusedWindow) USE_WINDOW_SHADOWS_FOCUSED_WINDOW else USE_WINDOW_SHADOWS

    override fun shouldSetBackground(taskInfo: TaskInfo): Boolean =
        taskInfo.isFreeform &&
            (!isVeiledResizeEnabled ||
                DesktopModeFlags.ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS.isTrue)

    override val maxTaskLimit: Int =
        SystemProperties.getInt(
            MAX_TASK_LIMIT_SYS_PROP,
            context.getResources().getInteger(R.integer.config_maxDesktopWindowingActiveTasks),
        )

    override val desktopDensityOverride: Int =
        SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 284)

    override fun dump(pw: PrintWriter, prefix: String) {
        val ipw = IndentingPrintWriter(pw, /* singleIndent= */ "  ", /* prefix= */ prefix)
        ipw.increaseIndent()
        pw.println(TAG)
        pw.println("maxTaskLimit=$maxTaskLimit")

        pw.print(
            "maxTaskLimit config override=${
                context.getResources()
                    .getInteger(R.integer.config_maxDesktopWindowingActiveTasks)
            }"
        )

        val maxTaskLimitHandle = SystemProperties.find(MAX_TASK_LIMIT_SYS_PROP)
        pw.println("maxTaskLimit sysprop=${maxTaskLimitHandle?.getInt( /* def= */-1) ?: "null"}")

        pw.println("showAppHandle config override=${desktopState.overridesShowAppHandle}")
    }

    companion object {
        private const val TAG: String = "DesktopConfig"

        /** The minimum override density allowed for tasks inside the desktop. */
        private const val DESKTOP_DENSITY_MIN: Int = 100

        /** The maximum override density allowed for tasks inside the desktop. */
        private const val DESKTOP_DENSITY_MAX: Int = 1000

        /** The number of [WindowDecorViewHost] instances to warm up on system start. */
        private const val WINDOW_DECOR_PRE_WARM_SIZE: Int = 2

        /**
         * Sysprop declaring the number of [WindowDecorViewHost] instances to warm up on system
         * start.
         *
         * If it is not defined, then [WINDOW_DECOR_PRE_WARM_SIZE] is used.
         */
        private const val WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP =
            "persist.wm.debug.desktop_window_decor_pre_warm_size"

        /**
         * Sysprop declaring the maximum number of Tasks to show in Desktop Mode at any one time.
         *
         * If it is not defined, then `R.integer.config_maxDesktopWindowingActiveTasks` is used.
         *
         * The limit does NOT affect Picture-in-Picture, Bubbles, or System Modals (like a screen
         * recording window, or Bluetooth pairing window).
         */
        private const val MAX_TASK_LIMIT_SYS_PROP = "persist.wm.debug.desktop_max_task_limit"

        /**
         * Sysprop declaring whether to enable drag-to-maximize for desktop windows.
         *
         * If it is not defined, then `R.integer.config_dragToMaximizeInDesktopMode`
         * is used.
         */
        private const val ENABLE_DRAG_TO_MAXIMIZE_SYS_PROP =
            "persist.wm.debug.enable_drag_to_maximize"

        /** Flag to indicate whether to apply shadows to windows in desktop mode. */
        private val USE_WINDOW_SHADOWS =
            SystemProperties.getBoolean("persist.wm.debug.desktop_use_window_shadows", true)

        /**
         * Flag to indicate whether to apply shadows to the focused window in desktop mode.
         *
         * Note: this flag is only relevant if USE_WINDOW_SHADOWS is false.
         */
        private val USE_WINDOW_SHADOWS_FOCUSED_WINDOW =
            SystemProperties.getBoolean(
                "persist.wm.debug.desktop_use_window_shadows_focused_window",
                false,
            )

        /** Whether the desktop density override is enabled. */
        private val DESKTOP_DENSITY_OVERRIDE_ENABLED =
            SystemProperties.getBoolean("persist.wm.debug.desktop_mode_density_enabled", false)

        /** Override density for tasks when they're inside the desktop. */
        private val DESKTOP_DENSITY_OVERRIDE: Int =
            SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 284)
    }
}
+3 −1
Original line number Diff line number Diff line
@@ -42,8 +42,10 @@ import java.util.Arrays;

/**
 * Constants for desktop mode feature
 *
 * @deprecated Use {@link DesktopState} or {@link DesktopConfig} instead.
 */
// TODO(b/237575897): Move this file to the `com.android.wm.shell.shared.desktopmode` package
@Deprecated(forRemoval = true)
public class DesktopModeStatus {

    private static final String TAG = "DesktopModeStatus";
+97 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.shared.desktopmode

import android.content.Context
import android.view.Display

/**
 * Interface defining which features are available on the device.
 *
 * A feature may be specific to a task or a display and may change over time (e.g.
 * [isDesktopModeSupportedOnDisplay] depends on user settings).
 *
 * This class is meant to be used in WM Shell, System UI and Launcher so they all understand what
 * features are enabled on the current device.
 */
@Suppress("INAPPLICABLE_JVM_NAME")
interface DesktopState {
    /** Returns if desktop mode is enabled and can be entered on the current device. */
    @get:JvmName("canEnterDesktopMode")
    val canEnterDesktopMode: Boolean

    /**
     * Whether desktop mode is enabled or app handles should be shown for other reasons.
     */
    @get:JvmName("canEnterDesktopModeOrShowAppHandle")
    val canEnterDesktopModeOrShowAppHandle: Boolean
        get() = canEnterDesktopMode || overridesShowAppHandle

    /** Whether desktop experience dev option should be shown on current device. */
    @get:JvmName("canShowDesktopExperienceDevOption")
    val canShowDesktopExperienceDevOption: Boolean

    /** Whether desktop mode dev option should be shown on current device. */
    @get:JvmName("canShowDesktopModeDevOption")
    val canShowDesktopModeDevOption: Boolean

    /**
     * Whether a display should enter desktop mode by default when the windowing mode of the
     * display's root [TaskDisplayArea] is set to `WINDOWING_MODE_FREEFORM`.
     */
    @get:JvmName("enterDesktopByDefaultOnFreeformDisplay")
    val enterDesktopByDefaultOnFreeformDisplay: Boolean

    /** Whether desktop mode is unrestricted and is supported on the device. */
    val isDeviceEligibleForDesktopMode: Boolean

    /**
     * Whether the multiple desktops feature is enabled for this device (both backend and
     * frontend implementations).
     */
    @get:JvmName("enableMultipleDesktops")
    val enableMultipleDesktops: Boolean

    /**
     * Checks if the display with id [displayId] should have desktop mode enabled or not. Internal
     * and external displays have separate logic.
     */
    fun isDesktopModeSupportedOnDisplay(displayId: Int): Boolean

    /**
     * Checks if [display] should have desktop mode enabled or not. Internal and external displays
     * have separate logic.
     */
    fun isDesktopModeSupportedOnDisplay(display: Display): Boolean

    /**
     * Whether the app handle should be shown on this device.
     */
    @get:JvmName("overridesShowAppHandle")
    val overridesShowAppHandle: Boolean

    /**
     * Whether freeform windowing is enabled on the system.
     */
    val isFreeformEnabled: Boolean

    companion object {
        /** Creates a new [DesktopState] from a context. */
        @JvmStatic
        fun fromContext(context: Context): DesktopState = DesktopStateImpl(context)
    }
}
+156 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.shared.desktopmode

import android.content.Context
import android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT
import android.hardware.display.DisplayManager
import android.os.SystemProperties
import android.provider.Settings
import android.view.Display
import android.view.WindowManager
import android.window.DesktopExperienceFlags
import android.window.DesktopModeFlags
import com.android.internal.R
import com.android.internal.annotations.VisibleForTesting
import com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement
import com.android.window.flags.Flags
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper

@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
class DesktopStateImpl(context: Context) : DesktopState {

    private val windowManager = context.getSystemService(WindowManager::class.java)
    private val displayManager = context.getSystemService(DisplayManager::class.java)

    private val enforceDeviceRestrictions =
        SystemProperties.getBoolean(ENFORCE_DEVICE_RESTRICTIONS_SYS_PROP, true)

    private val isDesktopModeDevOptionSupported =
        context.getResources().getBoolean(R.bool.config_isDesktopModeDevOptionSupported)

    private val isDesktopModeSupported =
        context.getResources().getBoolean(R.bool.config_isDesktopModeSupported)

    private val canInternalDisplayHostDesktops =
        context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops)

    private val isDeviceEligibleForDesktopModeDevOption =
        if (!enforceDeviceRestrictions) {
            true
        } else {
            val desktopModeSupportedOnInternalDisplay =
                isDesktopModeSupported && canInternalDisplayHostDesktops
            desktopModeSupportedOnInternalDisplay || isDesktopModeDevOptionSupported
        }

    override val canShowDesktopModeDevOption: Boolean =
        isDeviceEligibleForDesktopModeDevOption && Flags.showDesktopWindowingDevOption()

    private val isDesktopModeEnabledByDevOption =
        DesktopModeFlags.isDesktopModeForcedEnabled() && canShowDesktopModeDevOption

    override val canEnterDesktopMode: Boolean = run {
        val desktopModeEnabled =
            isDeviceEligibleForDesktopMode && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue
        desktopModeEnabled || isDesktopModeEnabledByDevOption
    }

    override val canShowDesktopExperienceDevOption: Boolean =
        Flags.showDesktopExperienceDevOption() && isDeviceEligibleForDesktopMode

    override val enterDesktopByDefaultOnFreeformDisplay: Boolean =
        DesktopExperienceFlags.ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS.isTrue &&
            SystemProperties.getBoolean(
                ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP,
                context
                    .getResources()
                    .getBoolean(R.bool.config_enterDesktopByDefaultOnFreeformDisplay),
            )

    override val isDeviceEligibleForDesktopMode: Boolean
        get() {
            if (!enforceDeviceRestrictions) return true

            // If projected display is enabled, [canInternalDisplayHostDesktops] is no longer a
            // requirement.
            val desktopModeSupported =
                if (DesktopExperienceFlags.ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE.isTrue) {
                    isDesktopModeSupported
                } else {
                    isDesktopModeSupported && canInternalDisplayHostDesktops
                }
            val desktopModeSupportedByDevOptions =
                Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionSupported
            return desktopModeSupported || desktopModeSupportedByDevOptions
        }

    override val enableMultipleDesktops: Boolean =
        DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue &&
            Flags.enableMultipleDesktopsFrontend() &&
            canEnterDesktopMode

    override fun isDesktopModeSupportedOnDisplay(displayId: Int): Boolean =
        displayManager.getDisplay(displayId)?.let { isDesktopModeSupportedOnDisplay(it) } ?: false

    override fun isDesktopModeSupportedOnDisplay(display: Display): Boolean {
        if (!canEnterDesktopMode) return false
        if (!enforceDeviceRestrictions) return true
        if (display.type == Display.TYPE_INTERNAL) return canInternalDisplayHostDesktops

        // TODO (b/395014779): Change this to use WM API
        if (
            (display.type == Display.TYPE_EXTERNAL || display.type == Display.TYPE_OVERLAY) &&
                enableDisplayContentModeManagement()
        ) {
            return windowManager?.shouldShowSystemDecors(display.displayId) ?: false
        }

        return false
    }

    private val deviceHasLargeScreen =
        displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)
            ?.filter { display -> display.type == Display.TYPE_INTERNAL }
            ?.any { display ->
                display.minSizeDimensionDp >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP
            } ?: false

    override val overridesShowAppHandle: Boolean =
        (Flags.showAppHandleLargeScreens() ||
            BubbleAnythingFlagHelper.enableBubbleToFullscreen()) && deviceHasLargeScreen

    private val hasFreeformFeature =
        context.getPackageManager().hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT)
    private val hasFreeformDevOption =
        Settings.Global.getInt(
            context.getContentResolver(),
            Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT,
            0
        ) != 0
    override val isFreeformEnabled: Boolean = hasFreeformFeature || hasFreeformDevOption

    companion object {
        @VisibleForTesting
        const val ENFORCE_DEVICE_RESTRICTIONS_SYS_PROP =
            "persist.wm.debug.desktop_mode_enforce_device_restrictions"

        @VisibleForTesting
        const val ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP =
            "persist.wm.debug.enter_desktop_by_default_on_freeform_display"
    }
}
Loading