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

Commit 246ee62f authored by Winson Chung's avatar Winson Chung
Browse files

Add property for apps to request support for showing multi-instance entry points

- Currently, this is controlled by an allow-list, but we want to ensure
  that all apps can request to be launched in multiple instances in
  cases where SystemUI desires

Bug: 262864589
Test: atest WMShellUnitTests
Change-Id: Ie7d2fa637d741623763a070000696d4d95215b4d
parent c4a0eca0
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -53961,6 +53961,7 @@ package android.view {
    field public static final String PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS = "android.window.PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS";
    field public static final String PROPERTY_COMPAT_ENABLE_FAKE_FOCUS = "android.window.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS";
    field public static final String PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION = "android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION";
    field @FlaggedApi("com.android.window.flags.supports_multi_instance_system_ui") public static final String PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI = "android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI";
  }
  public static class WindowManager.BadTokenException extends java.lang.RuntimeException {
+24 −0
Original line number Diff line number Diff line
@@ -1496,6 +1496,30 @@ public interface WindowManager extends ViewManager {
    String PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED =
            "android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED";

    /**
     * Activity or Application level {@link android.content.pm.PackageManager.Property
     * PackageManager.Property} for an app to declare that System UI should be shown for this
     * app/component to allow it to be launched as multiple instances.  This property only affects
     * SystemUI behavior and does _not_ affect whether a component can actually be launched into
     * multiple instances, which is determined by the Activity's {@code launchMode} or the launching
     * Intent's flags.  If the property is set on the Application, then all components within that
     * application will use that value unless specified per component.
     *
     * The value must be a boolean string.
     *
     * <p><b>Syntax:</b>
     * <pre>
     * &lt;activity&gt;
     *   &lt;property
     *     android:name="android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI"
     *     android:value="true|false"/&gt;
     * &lt;/activity&gt;
     * </pre>
     */
    @FlaggedApi(Flags.FLAG_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI)
    public static final String PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI =
            "android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI";

    /**
     * Request for app's keyboard shortcuts to be retrieved asynchronously.
     *
+8 −0
Original line number Diff line number Diff line
@@ -75,3 +75,11 @@ flag {
    bug: "259497289"
    is_fixed_read_only: true
}

flag {
    name: "supports_multi_instance_system_ui"
    namespace: "multitasking"
    description: "Feature flag to enable a multi-instance system ui component property."
    bug: "262864589"
    is_fixed_read_only: true
}
 No newline at end of file
+39 −3
Original line number Diff line number Diff line
@@ -16,7 +16,10 @@

package com.android.wm.shell.common.split;

import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_PERSONS_DATA;
import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED;
import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC;
import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER;

import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
@@ -24,19 +27,27 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;

import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
import android.os.UserHandle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.internal.util.ArrayUtils;
import com.android.wm.shell.Flags;
import com.android.wm.shell.ShellTaskOrganizer;

import java.util.Arrays;
import java.util.List;

/** Helper utility class for split screen components to use. */
public class SplitScreenUtils {
    /** Reverse the split position. */
@@ -135,4 +146,29 @@ public class SplitScreenUtils {
            return isLandscape;
        }
    }

    /** Returns the component from a PendingIntent */
    @Nullable
    public static ComponentName getComponent(@Nullable PendingIntent pendingIntent) {
        if (pendingIntent == null || pendingIntent.getIntent() == null) {
            return null;
        }
        return pendingIntent.getIntent().getComponent();
    }

    /** Returns the component from a shortcut */
    @Nullable
    public static ComponentName getShortcutComponent(@NonNull String packageName, String shortcutId,
            @NonNull UserHandle user, @NonNull LauncherApps launcherApps) {
        LauncherApps.ShortcutQuery query = new LauncherApps.ShortcutQuery();
        query.setPackage(packageName);
        query.setShortcutIds(Arrays.asList(shortcutId));
        query.setQueryFlags(FLAG_MATCH_DYNAMIC | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
                | FLAG_MATCH_CACHED | FLAG_GET_PERSONS_DATA);
        List<ShortcutInfo> shortcuts = launcherApps.getShortcuts(query, user);
        ShortcutInfo info = shortcuts != null && shortcuts.size() > 0
                ? shortcuts.get(0)
                : null;
        return info != null ? info.getActivity() : null;
    }
}
+81 −21
Original line number Diff line number Diff line
@@ -23,12 +23,15 @@ import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI;

import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.common.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
import static com.android.wm.shell.common.split.SplitScreenUtils.getComponent;
import static com.android.wm.shell.common.split.SplitScreenUtils.getShortcutComponent;
import static com.android.wm.shell.common.split.SplitScreenUtils.isValidToSplit;
import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
import static com.android.wm.shell.common.split.SplitScreenUtils.samePackage;
@@ -47,6 +50,8 @@ import android.app.TaskInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.graphics.Rect;
import android.os.Bundle;
@@ -171,6 +176,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
    private final ShellTaskOrganizer mTaskOrganizer;
    private final SyncTransactionQueue mSyncQueue;
    private final Context mContext;
    private final PackageManager mPackageManager;
    private final LauncherApps mLauncherApps;
    private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
    private final ShellExecutor mMainExecutor;
    private final SplitScreenImpl mImpl = new SplitScreenImpl();
@@ -186,7 +193,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
    private final Optional<WindowDecorViewModel> mWindowDecorViewModel;
    private final Optional<DesktopTasksController> mDesktopTasksController;
    private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler;
    private final String[] mAppsSupportMultiInstances;
    // A static allow list of apps which support multi-instance
    private final String[] mAppsSupportingMultiInstance;

    @VisibleForTesting
    StageCoordinator mStageCoordinator;
@@ -220,6 +228,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
        mTaskOrganizer = shellTaskOrganizer;
        mSyncQueue = syncQueue;
        mContext = context;
        mPackageManager = context.getPackageManager();
        mLauncherApps = context.getSystemService(LauncherApps.class);
        mRootTDAOrganizer = rootTDAOrganizer;
        mMainExecutor = mainExecutor;
        mDisplayController = displayController;
@@ -242,7 +252,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,

        // TODO(255224696): Remove the config once having a way for client apps to opt-in
        //                  multi-instances split.
        mAppsSupportMultiInstances = mContext.getResources()
        mAppsSupportingMultiInstance = mContext.getResources()
                .getStringArray(R.array.config_appsSupportMultiInstancesSplit);
    }

@@ -266,12 +276,15 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
            WindowDecorViewModel windowDecorViewModel,
            DesktopTasksController desktopTasksController,
            ShellExecutor mainExecutor,
            StageCoordinator stageCoordinator) {
            StageCoordinator stageCoordinator,
            String[] appsSupportingMultiInstance) {
        mShellCommandHandler = shellCommandHandler;
        mShellController = shellController;
        mTaskOrganizer = shellTaskOrganizer;
        mSyncQueue = syncQueue;
        mContext = context;
        mPackageManager = context.getPackageManager();
        mLauncherApps = context.getSystemService(LauncherApps.class);
        mRootTDAOrganizer = rootTDAOrganizer;
        mMainExecutor = mainExecutor;
        mDisplayController = displayController;
@@ -288,8 +301,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
        mStageCoordinator = stageCoordinator;
        mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this);
        shellInit.addInitCallback(this::onInit, this);
        mAppsSupportMultiInstances = mContext.getResources()
                .getStringArray(R.array.config_appsSupportMultiInstancesSplit);
        mAppsSupportingMultiInstance = appsSupportingMultiInstance;
    }

    public SplitScreen asSplitScreen() {
@@ -588,7 +600,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,

        if (samePackage(packageName, getPackageName(reverseSplitPosition(position)),
                user.getIdentifier(), getUserId(reverseSplitPosition(position)))) {
            if (supportMultiInstancesSplit(packageName)) {
            if (supportsMultiInstanceSplit(getShortcutComponent(packageName, shortcutId, user,
                    mLauncherApps))) {
                activityOptions.setApplyMultipleTaskFlagForShortcut(true);
                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
            } else if (isSplitScreenVisible()) {
@@ -609,7 +622,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
                activityOptions.toBundle(), user);
    }

    void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo,
    void startShortcutAndTaskWithLegacyTransition(@NonNull ShortcutInfo shortcutInfo,
            @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
            @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition,
            RemoteAnimationAdapter adapter, InstanceId instanceId) {
@@ -621,7 +634,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
        final int userId1 = shortcutInfo.getUserId();
        final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
        if (samePackage(packageName1, packageName2, userId1, userId2)) {
            if (supportMultiInstancesSplit(shortcutInfo.getPackage())) {
            if (supportsMultiInstanceSplit(shortcutInfo.getActivity())) {
                activityOptions.setApplyMultipleTaskFlagForShortcut(true);
                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
            } else {
@@ -640,7 +653,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
                instanceId);
    }

    void startShortcutAndTask(ShortcutInfo shortcutInfo, @Nullable Bundle options1,
    void startShortcutAndTask(@NonNull ShortcutInfo shortcutInfo, @Nullable Bundle options1,
            int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
            @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition,
            InstanceId instanceId) {
@@ -653,7 +666,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
        final int userId1 = shortcutInfo.getUserId();
        final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
        if (samePackage(packageName1, packageName2, userId1, userId2)) {
            if (supportMultiInstancesSplit(packageName1)) {
            if (supportsMultiInstanceSplit(shortcutInfo.getActivity())) {
                activityOptions.setApplyMultipleTaskFlagForShortcut(true);
                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
            } else {
@@ -692,7 +705,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
        final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer);
        final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
        if (samePackage(packageName1, packageName2, userId1, userId2)) {
            if (supportMultiInstancesSplit(packageName1)) {
            if (supportsMultiInstanceSplit(getComponent(pendingIntent))) {
                fillInIntent = new Intent();
                fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
@@ -722,7 +735,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
        final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
        boolean setSecondIntentMultipleTask = false;
        if (samePackage(packageName1, packageName2, userId1, userId2)) {
            if (supportMultiInstancesSplit(packageName1)) {
            if (supportsMultiInstanceSplit(getComponent(pendingIntent))) {
                setSecondIntentMultipleTask = true;
                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
            } else {
@@ -757,7 +770,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
        final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1);
        final String packageName2 = SplitScreenUtils.getPackageName(pendingIntent2);
        if (samePackage(packageName1, packageName2, userId1, userId2)) {
            if (supportMultiInstancesSplit(packageName1)) {
            if (supportsMultiInstanceSplit(getComponent(pendingIntent1))) {
                fillInIntent1 = new Intent();
                fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                fillInIntent2 = new Intent();
@@ -794,7 +807,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
                ? ActivityOptions.fromBundle(options2) : ActivityOptions.makeBasic();
        boolean setSecondIntentMultipleTask = false;
        if (samePackage(packageName1, packageName2, userId1, userId2)) {
            if (supportMultiInstancesSplit(packageName1)) {
            if (supportsMultiInstanceSplit(getComponent(pendingIntent1))) {
                fillInIntent1 = new Intent();
                fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                setSecondIntentMultipleTask = true;
@@ -856,7 +869,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
            return;
        }
        if (samePackage(packageName1, packageName2, userId1, userId2)) {
            if (supportMultiInstancesSplit(packageName1)) {
            if (supportsMultiInstanceSplit(getComponent(intent))) {
                // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of
                // the split and there is no reusable background task.
                fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
@@ -915,16 +928,63 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
        return taskInfo != null ? taskInfo.userId : -1;
    }

    /**
     * Returns whether a specific component desires to be launched in multiple instances for
     * split screen.
     */
    @VisibleForTesting
    boolean supportMultiInstancesSplit(String packageName) {
        if (packageName != null) {
            for (int i = 0; i < mAppsSupportMultiInstances.length; i++) {
                if (mAppsSupportMultiInstances[i].equals(packageName)) {
    boolean supportsMultiInstanceSplit(@Nullable ComponentName componentName) {
        if (componentName == null || componentName.getPackageName() == null) {
            // TODO(b/262864589): Handle empty component case
            return false;
        }

        // Check the pre-defined allow list
        final String packageName = componentName.getPackageName();
        for (int i = 0; i < mAppsSupportingMultiInstance.length; i++) {
            if (mAppsSupportingMultiInstance[i].equals(packageName)) {
                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                        "application=%s in allowlist supports multi-instance", packageName);
                return true;
            }
        }

        // Check the activity property first
        try {
            final PackageManager.Property activityProp = mPackageManager.getProperty(
                    PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, componentName);
            // If the above call doesn't throw a NameNotFoundException, then the activity property
            // should override the application property value
            if (activityProp.isBoolean()) {
                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                        "activity=%s supports multi-instance", componentName);
                return activityProp.getBoolean();
            } else {
                ProtoLog.w(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                        "Warning: property=%s for activity=%s has non-bool type=%d",
                        PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName,
                        activityProp.getType());
            }
        } catch (PackageManager.NameNotFoundException nnfe) {
            // Not specified in the activity, fall through
        }

        // Check the application property otherwise
        try {
            final PackageManager.Property appProp = mPackageManager.getProperty(
                    PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName);
            if (appProp.isBoolean()) {
                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                        "application=%s supports multi-instance", packageName);
                return appProp.getBoolean();
            } else {
                ProtoLog.w(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                        "Warning: property=%s for application=%s has non-bool type=%d",
                        PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName, appProp.getType());
            }
        } catch (PackageManager.NameNotFoundException nnfe) {
            // Not specified in either application or activity
        }
        return false;
    }

Loading