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

Commit 69fa28de authored by Eghosa Ewansiha-Vlachavas's avatar Eghosa Ewansiha-Vlachavas
Browse files

[1/n] Force incompatible activities to fullscreen via desktop launch params

Currently incompatible activities are only forced to fullscreen via the
request handling in `DesktopTasksController`. However, desktop taskbar
launches are not handled by `DesktopTaskController` after the intent in
started in Shell.

This CL moves 'DesktopModeCompatPolicy` to Core so it can be used by
both `DesktopTaksController` in Shell and
`DesktopModeLaunchParamsModifier` in Core.

Flag: com.android.window.flags.handle_incompatible_tasks_in_desktop_launch_params
Fixes: 409489637
Fixes: 418181855
Test: atest WmTests:DesktopModeLaunchParamsModifierTests,
atest WMShellUnitTests:DesktopModeCompatPolicyTest,
atest WMShellUnitTests:DesktopTasksControllerTest,
atest WMShellUnitTests:SystemModalsTransitionHandlerTest,
atest WMShellUnitTests:DesktopModeWindowDecorationTests,
atest WMShellUnitTests:DesktopModeWindowDecorViewModelTests

Change-Id: I7b549b0547c443fb0aa149c0b7b3d0251595e9ce
parent c8929114
Loading
Loading
Loading
Loading
+266 −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.internal.policy;

import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.TaskInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.window.DesktopExperienceFlags;
import android.window.DesktopModeFlags;

import com.android.internal.R;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;

public class DesktopModeCompatPolicy {
    @NonNull
    private final Context mContext;
    @NonNull
    private final String mSystemUiPackage;
    private final Map<String, Boolean> mPackageInfoCache = new HashMap<>();
    private PackageManager mPackageManager = null;

    public Supplier<String> mDefaultHomePackageSupplier;

    public DesktopModeCompatPolicy(@NonNull Context context) {
        mContext = context;
        mSystemUiPackage = context.getResources().getString(R.string.config_systemUi);
        mDefaultHomePackageSupplier = () -> {
            final ComponentName homeActivities = getPackageManager().getHomeActivities(
                    new ArrayList<>());
            if (homeActivities != null) {
                return homeActivities.getPackageName();
            }
            return null;
        };
    }

    public void setDefaultHomePackageSupplier(
            @NonNull Supplier<String> defaultHomePackageSupplier) {
        mDefaultHomePackageSupplier = defaultHomePackageSupplier;
    }

    @NonNull
    private PackageManager getPackageManager() {
        if (mPackageManager != null) {
            return mPackageManager;
        }

        mPackageManager = mContext.getPackageManager();
        return mPackageManager;
    }

    private String getDefaultHomePackage() {
        if (mDefaultHomePackageSupplier != null) {
            return mDefaultHomePackageSupplier.get();
        }

        final ComponentName homeActivities = getPackageManager().getHomeActivities(
                new ArrayList<>());
        if (homeActivities != null) {
            return homeActivities.getPackageName();
        }
        return null;
    }

    /**
     * @see #isTopActivityExemptFromDesktopWindowing(ComponentName, boolean, boolean, int, int,
     * ActivityInfo)
     */
    public boolean isTopActivityExemptFromDesktopWindowing(@NonNull TaskInfo task) {
        return isTopActivityExemptFromDesktopWindowing(task.baseActivity,
                task.isTopActivityNoDisplay, task.isActivityStackTransparent, task.numActivities,
                task.userId, task.topActivityInfo);
    }

    /**
     * If the top activity should be exempt from desktop windowing and forced back to fullscreen.
     * Currently includes all system ui, default home and transparent stack activities with the
     * relevant permission or signature. However if the top activity is not being displayed,
     * regardless of its configuration, we will not exempt it as to remain in the desktop windowing
     * environment.
     */
    public boolean isTopActivityExemptFromDesktopWindowing(@Nullable ComponentName baseActivity,
            boolean isTopActivityNoDisplay, boolean isActivityStackTransparent, int numActivities,
            int userId, ActivityInfo info) {
        final String packageName = baseActivity != null ? baseActivity.getPackageName() : null;
        if (packageName == null) {
            return false;
        }

        if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue()) {
            return false;
        }
        // If activity is not being displayed, window mode change has no visual affect so leave
        // unchanged.
        if (isTopActivityNoDisplay) {
            return false;
        }
        // If activity belongs to system ui package, safe to force out of desktop.
        if (isSystemUiTask(packageName)) {
            return true;
        }
        // If activity belongs to default home package, safe to force out of desktop.
        if (isPartOfDefaultHomePackageOrNoHomeAvailable(packageName)) {
            return true;
        }
        // If all activities in task stack are transparent AND package has the relevant
        // fullscreen transparent permission OR is signed with platform key, safe to force out
        // of desktop.
        return isTransparentTask(isActivityStackTransparent, numActivities)
                && (hasFullscreenTransparentPermission(packageName, userId)
                || hasPlatformSignature(info));
    }

    /** @see #shouldDisableDesktopEntryPoints(String, int, boolean, boolean) */
    public boolean shouldDisableDesktopEntryPoints(@NonNull TaskInfo task) {
        final String packageName = task.baseActivity != null ? task.baseActivity.getPackageName() :
                null;
        return shouldDisableDesktopEntryPoints(
                packageName,
                task.numActivities,
                task.isTopActivityNoDisplay,
                task.isActivityStackTransparent
        );
    }

    /**
     * Whether all desktop entry points should be disabled for a given activity. Currently includes
     * all system ui, default home, transparent stack and no display activities.
     */
    public boolean shouldDisableDesktopEntryPoints(
            @Nullable String packageName,
            int numActivities,
            boolean isTopActivityNoDisplay,
            boolean isActivityStackTransparent) {
        // Activity will not be displayed, no need to show desktop entry point.
        if (isTopActivityNoDisplay) {
            return true;
        }
        // If activity belongs to system ui package, hide desktop entry point.
        if (isSystemUiTask(packageName)) {
            return true;
        }
        // If activity belongs to default home package, safe to force out of desktop.
        if (isPartOfDefaultHomePackageOrNoHomeAvailable(packageName)) {
            return true;
        }
        // If all activities in task stack are transparent AND package has the relevant fullscreen
        // transparent permission, safe to force out of desktop.
        return isTransparentTask(isActivityStackTransparent, numActivities);
    }


    /** @see DesktopModeCompatUtils#shouldExcludeCaptionFromAppBounds */
    public boolean shouldExcludeCaptionFromAppBounds(@NonNull TaskInfo taskInfo) {
        if (taskInfo.topActivityInfo != null) {
            return DesktopModeCompatUtils.shouldExcludeCaptionFromAppBounds(
                    taskInfo.topActivityInfo,
                    taskInfo.isResizeable,
                    taskInfo.appCompatTaskInfo != null
                            && taskInfo.appCompatTaskInfo.hasOptOutEdgeToEdge()
            );
        }
        return false;
    }

    /**
     * Returns true if all activities in a tasks stack are transparent. If there are no activities
     * will return false.
     */
    public boolean isTransparentTask(@NonNull TaskInfo task) {
        return isTransparentTask(task.isActivityStackTransparent, task.numActivities);
    }

    private boolean isTransparentTask(boolean isActivityStackTransparent, int numActivities) {
        return isActivityStackTransparent && numActivities > 0;
    }

    private boolean isSystemUiTask(@Nullable String packageName) {
        return Objects.equals(packageName, mSystemUiPackage);
    }

    // Checks if the app for the given package has the SYSTEM_ALERT_WINDOW permission.
    private boolean hasFullscreenTransparentPermission(@NonNull String packageName, int userId) {
        if (!DesktopModeFlags.ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS.isTrue()) {
            // If the ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS flag is disabled, make neutral
            // condition
            // dependant on the ENABLE_MODALS_FULLSCREEN_WITH_PLATFORM_SIGNATURE flag.
            return !DesktopExperienceFlags.ENABLE_MODALS_FULLSCREEN_WITH_PLATFORM_SIGNATURE
                    .isTrue();
        }

        final String cacheKey = userId + "@" + packageName;
        if (mPackageInfoCache.containsKey(cacheKey)) {
            return mPackageInfoCache.get(cacheKey);
        }

        boolean hasPermission = false;
        try {
            PackageInfo packageInfo = getPackageManager().getPackageInfoAsUser(
                    packageName,
                    PackageManager.GET_PERMISSIONS,
                    userId
            );
            if (packageInfo != null && packageInfo.requestedPermissions != null) {
                for (String permission : packageInfo.requestedPermissions) {
                    if (Objects.equals(permission, Manifest.permission.SYSTEM_ALERT_WINDOW)) {
                        hasPermission = true;
                        break;
                    }
                }
            }
        } catch (PackageManager.NameNotFoundException e) {
            // Package not found, so no permission
        }
        mPackageInfoCache.put(cacheKey, hasPermission);
        return hasPermission;

    }

    // Checks if the app is signed with the platform signature.
    private boolean hasPlatformSignature(@Nullable ActivityInfo info) {
        if (DesktopExperienceFlags.ENABLE_MODALS_FULLSCREEN_WITH_PLATFORM_SIGNATURE.isTrue()) {
            return info != null
                    && info.applicationInfo != null
                    && info.applicationInfo.isSignedWithPlatformKey();
        }
        // If the ENABLE_MODALS_FULLSCREEN_WITH_PLATFORM_SIGNATURE flag is disabled, make neutral
        // condition dependant on the ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS flag.
        return !DesktopModeFlags.ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS.isTrue();
    }

    /**
     * Returns true if the tasks base activity is part of the default home package, or there is
     * currently no default home package available.
     */
    private boolean isPartOfDefaultHomePackageOrNoHomeAvailable(@Nullable String packageName) {
        final String defaultHomePackage = mDefaultHomePackageSupplier.get();
        return defaultHomePackage == null || (packageName != null
                && packageName.equals(defaultHomePackage));
    }
}
+0 −154
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.shared.desktopmode

import android.Manifest.permission.SYSTEM_ALERT_WINDOW
import android.app.TaskInfo
import android.content.Context
import android.content.pm.PackageManager
import android.window.DesktopExperienceFlags
import android.window.DesktopModeFlags
import com.android.internal.R
import com.android.internal.policy.DesktopModeCompatUtils
import java.util.function.Supplier

/**
 * Class to decide whether to apply app compat policies in desktop mode.
 */
// TODO(b/347289970): Consider replacing with API
class DesktopModeCompatPolicy(private val context: Context) {

    private val systemUiPackage: String = context.resources.getString(R.string.config_systemUi)
    private val pkgManager: PackageManager
        get() = context.getPackageManager()
    private val defaultHomePackage: String?
        get() = defaultHomePackageSupplier?.get()
            ?: pkgManager.getHomeActivities(ArrayList())?.packageName
    private val packageInfoCache = mutableMapOf<String, Boolean>()

    var defaultHomePackageSupplier: Supplier<String?>? = null

    /**
     * If the top activity should be exempt from desktop windowing and forced back to fullscreen.
     * Currently includes all system ui, default home and transparent stack activities with the
     * relevant permission or signature. However if the top activity is not being displayed,
     * regardless of its configuration, we will not exempt it as to remain in the desktop windowing
     * environment.
     */
    fun isTopActivityExemptFromDesktopWindowing(task: TaskInfo): Boolean {
        val packageName = task.baseActivity?.packageName ?: return false

        return when {
            !DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue -> false
            // If activity is not being displayed, window mode change has no visual affect so leave
            // unchanged.
            task.isTopActivityNoDisplay -> false
            // If activity belongs to system ui package, safe to force out of desktop.
            isSystemUiTask(packageName) -> true
            // If activity belongs to default home package, safe to force out of desktop.
            isPartOfDefaultHomePackageOrNoHomeAvailable(packageName) -> true
            // If all activities in task stack are transparent AND package has the relevant
            // fullscreen transparent permission OR is signed with platform key, safe to force out
            // of desktop.
            isTransparentTask(task.isActivityStackTransparent, task.numActivities) &&
                    (hasFullscreenTransparentPermission(packageName, task.userId) ||
                            hasPlatformSignature(task)) -> true

            else -> false
        }
    }

    fun shouldDisableDesktopEntryPoints(task: TaskInfo) = shouldDisableDesktopEntryPoints(
        task.baseActivity?.packageName, task.numActivities, task.isTopActivityNoDisplay,
        task.isActivityStackTransparent)

    fun shouldDisableDesktopEntryPoints(
        packageName: String?,
        numActivities: Int,
        isTopActivityNoDisplay: Boolean,
        isActivityStackTransparent: Boolean,
    ) = when {
        // Activity will not be displayed, no need to show desktop entry point.
        isTopActivityNoDisplay -> true
        // If activity belongs to system ui package, hide desktop entry point.
        isSystemUiTask(packageName) -> true
        // If activity belongs to default home package, safe to force out of desktop.
        isPartOfDefaultHomePackageOrNoHomeAvailable(packageName) -> true
        // If all activities in task stack are transparent AND package has the relevant fullscreen
        // transparent permission, safe to force out of desktop.
        isTransparentTask(isActivityStackTransparent, numActivities) -> true
        else -> false
    }


    /** @see DesktopModeCompatUtils.shouldExcludeCaptionFromAppBounds */
    fun shouldExcludeCaptionFromAppBounds(taskInfo: TaskInfo): Boolean =
        taskInfo.topActivityInfo?.let {
            DesktopModeCompatUtils.shouldExcludeCaptionFromAppBounds(it, taskInfo.isResizeable,
                taskInfo.appCompatTaskInfo.hasOptOutEdgeToEdge())
        } ?: false

    /**
     * Returns true if all activities in a tasks stack are transparent. If there are no activities
     * will return false.
     */
    fun isTransparentTask(task: TaskInfo): Boolean =
        isTransparentTask(task.isActivityStackTransparent, task.numActivities)

    private fun isTransparentTask(isActivityStackTransparent: Boolean, numActivities: Int) =
        isActivityStackTransparent && numActivities > 0

    private fun isSystemUiTask(packageName: String?) = packageName == systemUiPackage

    // Checks if the app for the given package has the SYSTEM_ALERT_WINDOW permission.
    private fun hasFullscreenTransparentPermission(packageName: String, userId: Int): Boolean {
        if (DesktopModeFlags.ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS.isTrue) {
            return packageInfoCache.getOrPut("$userId@$packageName") {
                try {
                    val packageInfo = pkgManager.getPackageInfoAsUser(
                        packageName,
                        PackageManager.GET_PERMISSIONS,
                        userId
                    )
                    packageInfo?.requestedPermissions?.contains(SYSTEM_ALERT_WINDOW) == true
                } catch (e: PackageManager.NameNotFoundException) {
                    false // Package not found
                }
            }
        }
        // If the ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS flag is disabled, make neutral condition
        // dependant on the ENABLE_MODALS_FULLSCREEN_WITH_PLATFORM_SIGNATURE flag.
        return !DesktopExperienceFlags.ENABLE_MODALS_FULLSCREEN_WITH_PLATFORM_SIGNATURE.isTrue
    }

    // Checks if the app is signed with the platform signature.
    private fun hasPlatformSignature(task: TaskInfo): Boolean {
        if (DesktopExperienceFlags.ENABLE_MODALS_FULLSCREEN_WITH_PLATFORM_SIGNATURE.isTrue) {
            return task.topActivityInfo?.applicationInfo?.isSignedWithPlatformKey ?: false
        }
        // If the ENABLE_MODALS_FULLSCREEN_WITH_PLATFORM_SIGNATURE flag is disabled, make neutral
        // condition dependant on the ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS flag.
        return !DesktopModeFlags.ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS.isTrue
    }

    /**
     * Returns true if the tasks base activity is part of the default home package, or there is
     * currently no default home package available.
     */
    private fun isPartOfDefaultHomePackageOrNoHomeAvailable(packageName: String?) =
        defaultHomePackage == null || (packageName != null && packageName == defaultHomePackage)
}
+1 −1
Original line number Diff line number Diff line
@@ -38,6 +38,7 @@ import android.window.DesktopModeFlags;
import android.window.SystemPerformanceHinter;

import com.android.internal.logging.UiEventLogger;
import com.android.internal.policy.DesktopModeCompatPolicy;
import com.android.launcher3.icons.IconProvider;
import com.android.window.flags.Flags;
import com.android.wm.shell.ProtoLogController;
@@ -121,7 +122,6 @@ import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.shared.annotations.ShellSplashscreenThread;
import com.android.wm.shell.shared.desktopmode.DesktopConfig;
import com.android.wm.shell.shared.desktopmode.DesktopConfigImpl;
import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy;
import com.android.wm.shell.shared.desktopmode.DesktopState;
import com.android.wm.shell.shared.desktopmode.DesktopStateImpl;
import com.android.wm.shell.splitscreen.SplitScreen;
+1 −1
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ import androidx.annotation.OptIn;
import com.android.app.viewcapture.ViewCaptureAwareWindowManagerFactory;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.policy.DesktopModeCompatPolicy;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.util.LatencyTracker;
import com.android.launcher3.icons.IconProvider;
@@ -163,7 +164,6 @@ import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellDesktopThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.shared.desktopmode.DesktopConfig;
import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy;
import com.android.wm.shell.shared.desktopmode.DesktopState;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellCommandHandler;
+1 −1
Original line number Diff line number Diff line
@@ -87,6 +87,7 @@ import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.policy.DesktopModeCompatPolicy
import com.android.internal.policy.SystemBarUtils.getDesktopViewAppHeaderHeightPx
import com.android.internal.protolog.ProtoLog
import com.android.internal.util.LatencyTracker
@@ -146,7 +147,6 @@ import com.android.wm.shell.shared.annotations.ShellDesktopThread
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.shared.desktopmode.DesktopConfig
import com.android.wm.shell.shared.desktopmode.DesktopFirstListener
import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
import com.android.wm.shell.shared.desktopmode.DesktopState
import com.android.wm.shell.shared.desktopmode.DesktopTaskToFrontReason
Loading