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

Commit 76000f3b authored by Jerry Chang's avatar Jerry Chang
Browse files

Support starting a shortcut into multi-instances split

Support appending MULTIPLE_TASK flag when starting a shotcut into
multi-instances split.
Align the fallback logic of canceling split screen from a shortcut and
an intent to prevent broken split screen.

Bug: 265244780
Test: atest WMShellUnitTests
Test: atest WMShellFlickerTests
Test: verify appended flag from log with the repro steps of the bug
Change-Id: I5942eac71389bcbbef55074fbf127470439fc1e8
parent d9964c1f
Loading
Loading
Loading
Loading
+0 −13
Original line number Diff line number Diff line
@@ -699,19 +699,6 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
        return bounds.width() > bounds.height();
    }

    /** Reverse the split position. */
    @SplitPosition
    public static int reversePosition(@SplitPosition int position) {
        switch (position) {
            case SPLIT_POSITION_TOP_OR_LEFT:
                return SPLIT_POSITION_BOTTOM_OR_RIGHT;
            case SPLIT_POSITION_BOTTOM_OR_RIGHT:
                return SPLIT_POSITION_TOP_OR_LEFT;
            default:
                return SPLIT_POSITION_UNDEFINED;
        }
    }

    /**
     * Return if this layout is landscape.
     */
+85 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.common.split;

import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
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 android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.PendingIntent;
import android.content.Intent;

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

/** Helper utility class for split screen components to use. */
public class SplitScreenUtils {
    /** Reverse the split position. */
    @SplitScreenConstants.SplitPosition
    public static int reverseSplitPosition(@SplitScreenConstants.SplitPosition int position) {
        switch (position) {
            case SPLIT_POSITION_TOP_OR_LEFT:
                return SPLIT_POSITION_BOTTOM_OR_RIGHT;
            case SPLIT_POSITION_BOTTOM_OR_RIGHT:
                return SPLIT_POSITION_TOP_OR_LEFT;
            case SPLIT_POSITION_UNDEFINED:
            default:
                return SPLIT_POSITION_UNDEFINED;
        }
    }

    /** Returns true if the task is valid for split screen. */
    public static boolean isValidToSplit(ActivityManager.RunningTaskInfo taskInfo) {
        return taskInfo != null && taskInfo.supportsMultiWindow
                && ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType())
                && ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, taskInfo.getWindowingMode());
    }

    /** Retrieve package name from an intent */
    @Nullable
    public static String getPackageName(Intent intent) {
        if (intent == null || intent.getComponent() == null) {
            return null;
        }
        return intent.getComponent().getPackageName();
    }

    /** Retrieve package name from a PendingIntent */
    @Nullable
    public static String getPackageName(PendingIntent pendingIntent) {
        if (pendingIntent == null || pendingIntent.getIntent() == null) {
            return null;
        }
        return getPackageName(pendingIntent.getIntent());
    }

    /** Retrieve package name from a taskId */
    @Nullable
    public static String getPackageName(int taskId, ShellTaskOrganizer taskOrganizer) {
        final ActivityManager.RunningTaskInfo taskInfo = taskOrganizer.getRunningTaskInfo(taskId);
        return taskInfo != null ? getPackageName(taskInfo.baseIntent) : null;
    }

    /** Returns true if they are the same package. */
    public static boolean samePackage(String packageName1, String packageName2) {
        return packageName1 != null && packageName1.equals(packageName2);
    }
}
+85 −97
Original line number Diff line number Diff line
@@ -28,6 +28,9 @@ import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTas
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.isValidToSplit;
import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
import static com.android.wm.shell.common.split.SplitScreenUtils.samePackage;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN;
@@ -39,11 +42,8 @@ import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.graphics.Rect;
import android.os.Bundle;
@@ -82,8 +82,8 @@ import com.android.wm.shell.common.SingleInstanceRemoteListener;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.annotations.ExternalThread;
import com.android.wm.shell.common.split.SplitLayout;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
import com.android.wm.shell.common.split.SplitScreenUtils;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.draganddrop.DragAndDropPolicy;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -318,10 +318,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
        return mStageCoordinator;
    }

    public boolean isValidToEnterSplitScreen(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
        return mStageCoordinator.isValidToEnterSplitScreen(taskInfo);
    }

    @Nullable
    public ActivityManager.RunningTaskInfo getTaskInfo(@SplitPosition int splitPosition) {
        if (!isSplitScreenVisible() || splitPosition == SPLIT_POSITION_UNDEFINED) {
@@ -480,41 +476,56 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
    @Override
    public void startShortcut(String packageName, String shortcutId, @SplitPosition int position,
            @Nullable Bundle options, UserHandle user) {
        IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
            @Override
            public void onAnimationStart(@WindowManager.TransitionOldType int transit,
                    RemoteAnimationTarget[] apps,
                    RemoteAnimationTarget[] wallpapers,
                    RemoteAnimationTarget[] nonApps,
                    final IRemoteAnimationFinishedCallback finishedCallback) {
                try {
                    finishedCallback.onAnimationFinished();
                } catch (RemoteException e) {
                    Slog.e(TAG, "Failed to invoke onAnimationFinished", e);
                }
                final WindowContainerTransaction evictWct = new WindowContainerTransaction();
                mStageCoordinator.prepareEvictNonOpeningChildTasks(position, apps, evictWct);
                mSyncQueue.queue(evictWct);
        if (options == null) options = new Bundle();
        final ActivityOptions activityOptions = ActivityOptions.fromBundle(options);

        if (samePackage(packageName, getPackageName(reverseSplitPosition(position)))) {
            if (supportMultiInstancesSplit(packageName)) {
                activityOptions.setApplyMultipleTaskFlagForShortcut(true);
                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
            } else if (isSplitScreenVisible()) {
                mStageCoordinator.switchSplitPosition("startShortcut");
                return;
            } else {
                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                        "Cancel entering split as not supporting multi-instances");
                Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
                        Toast.LENGTH_SHORT).show();
                return;
            }
            @Override
            public void onAnimationCancelled(boolean isKeyguardOccluded) {
        }
        };
        options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
                null /* wct */);
        RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(wrapper,
                0 /* duration */, 0 /* statusBarTransitionDelay */);
        ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
        activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
        try {
            LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
            launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */,

        mStageCoordinator.startShortcut(packageName, shortcutId, position,
                activityOptions.toBundle(), user);
        } catch (ActivityNotFoundException e) {
            Slog.e(TAG, "Failed to launch shortcut", e);
    }

    void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo,
            @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
            @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
            InstanceId instanceId) {
        if (options1 == null) options1 = new Bundle();
        final ActivityOptions activityOptions = ActivityOptions.fromBundle(options1);

        final String packageName1 = shortcutInfo.getPackage();
        final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer);
        if (samePackage(packageName1, packageName2)) {
            if (supportMultiInstancesSplit(shortcutInfo.getPackage())) {
                activityOptions.setApplyMultipleTaskFlagForShortcut(true);
                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
            } else {
                taskId = INVALID_TASK_ID;
                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                        "Cancel entering split as not supporting multi-instances");
                Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
                        Toast.LENGTH_SHORT).show();
            }
        }

        mStageCoordinator.startShortcutAndTaskWithLegacyTransition(shortcutInfo,
                activityOptions.toBundle(), taskId, options2, splitPosition, splitRatio, adapter,
                instanceId);
    }

    /**
     * See {@link #startIntent(PendingIntent, Intent, int, Bundle)}
     * @param instanceId to be used by {@link SplitscreenEventLogger}
@@ -530,8 +541,10 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
            @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
            InstanceId instanceId) {
        Intent fillInIntent = null;
        if (launchSameAppAdjacently(pendingIntent, taskId)) {
            if (supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
        final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent);
        final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer);
        if (samePackage(packageName1, packageName2)) {
            if (supportMultiInstancesSplit(packageName1)) {
                fillInIntent = new Intent();
                fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
@@ -551,8 +564,10 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
            int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
            float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
        Intent fillInIntent = null;
        if (launchSameAppAdjacently(pendingIntent, taskId)) {
            if (supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
        final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent);
        final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer);
        if (samePackage(packageName1, packageName2)) {
            if (supportMultiInstancesSplit(packageName1)) {
                fillInIntent = new Intent();
                fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
@@ -573,8 +588,10 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
            float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) {
        Intent fillInIntent1 = null;
        Intent fillInIntent2 = null;
        if (launchSameAppAdjacently(pendingIntent1, pendingIntent2)) {
            if (supportMultiInstancesSplit(pendingIntent1.getIntent().getComponent())) {
        final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1);
        final String packageName2 = SplitScreenUtils.getPackageName(pendingIntent2);
        if (samePackage(packageName1, packageName2)) {
            if (supportMultiInstancesSplit(packageName1)) {
                fillInIntent1 = new Intent();
                fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                fillInIntent2 = new Intent();
@@ -602,13 +619,15 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
        if (fillInIntent == null) fillInIntent = new Intent();
        fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);

        if (launchSameAppAdjacently(position, intent)) {
            final ComponentName launching = intent.getIntent().getComponent();
            if (supportMultiInstancesSplit(launching)) {
        final String packageName1 = SplitScreenUtils.getPackageName(intent);
        final String packageName2 = getPackageName(reverseSplitPosition(position));
        if (SplitScreenUtils.samePackage(packageName1, packageName2)) {
            if (supportMultiInstancesSplit(packageName1)) {
                // To prevent accumulating large number of instances in the background, reuse task
                // in the background with priority.
                final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional
                        .map(recentTasks -> recentTasks.findTaskInBackground(launching))
                        .map(recentTasks -> recentTasks.findTaskInBackground(
                                intent.getIntent().getComponent()))
                        .orElse(null);
                if (taskInfo != null) {
                    startTask(taskInfo.taskId, position, options);
@@ -636,65 +655,34 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
        mStageCoordinator.startIntent(intent, fillInIntent, position, options);
    }

    /** Retrieve package name of a specific split position if split screen is activated, otherwise
     *  returns the package name of the top running task. */
    @Nullable
    private String getPackageName(Intent intent) {
        if (intent == null || intent.getComponent() == null) {
            return null;
        }
        return intent.getComponent().getPackageName();
    }

    private boolean launchSameAppAdjacently(@SplitPosition int position,
            PendingIntent pendingIntent) {
        ActivityManager.RunningTaskInfo adjacentTaskInfo = null;
    private String getPackageName(@SplitPosition int position) {
        ActivityManager.RunningTaskInfo taskInfo;
        if (isSplitScreenVisible()) {
            adjacentTaskInfo = getTaskInfo(SplitLayout.reversePosition(position));
            taskInfo = getTaskInfo(position);
        } else {
            adjacentTaskInfo = mRecentTasksOptional
                    .map(recentTasks -> recentTasks.getTopRunningTask()).orElse(null);
            if (!isValidToEnterSplitScreen(adjacentTaskInfo)) {
                return false;
            }
        }

        if (adjacentTaskInfo == null) {
            return false;
        }

        final String targetPackageName = getPackageName(pendingIntent.getIntent());
        final String adjacentPackageName = getPackageName(adjacentTaskInfo.baseIntent);
        return targetPackageName != null && targetPackageName.equals(adjacentPackageName);
    }

    private boolean launchSameAppAdjacently(PendingIntent pendingIntent, int taskId) {
        final ActivityManager.RunningTaskInfo adjacentTaskInfo =
                mTaskOrganizer.getRunningTaskInfo(taskId);
        if (adjacentTaskInfo == null) {
            return false;
            taskInfo = mRecentTasksOptional
                    .map(recentTasks -> recentTasks.getTopRunningTask())
                    .orElse(null);
            if (!isValidToSplit(taskInfo)) {
                return null;
            }
        final String targetPackageName = getPackageName(pendingIntent.getIntent());
        final String adjacentPackageName = getPackageName(adjacentTaskInfo.baseIntent);
        return targetPackageName != null && targetPackageName.equals(adjacentPackageName);
        }

    private boolean launchSameAppAdjacently(PendingIntent pendingIntent1,
            PendingIntent pendingIntent2) {
        final String targetPackageName = getPackageName(pendingIntent1.getIntent());
        final String adjacentPackageName = getPackageName(pendingIntent2.getIntent());
        return targetPackageName != null && targetPackageName.equals(adjacentPackageName);
        return taskInfo != null ? SplitScreenUtils.getPackageName(taskInfo.baseIntent) : null;
    }

    @VisibleForTesting
    /** Returns {@code true} if the component supports multi-instances split. */
    boolean supportMultiInstancesSplit(@Nullable ComponentName launching) {
        if (launching == null) return false;

        final String packageName = launching.getPackageName();
    boolean supportMultiInstancesSplit(String packageName) {
        if (packageName != null) {
            for (int i = 0; i < mAppsSupportMultiInstances.length; i++) {
                if (mAppsSupportMultiInstances[i].equals(packageName)) {
                    return true;
                }
            }
        }

        return false;
    }
@@ -1011,7 +999,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
                InstanceId instanceId) {
            executeRemoteCallWithTaskPermission(mController,
                    "startShortcutAndTaskWithLegacyTransition", (controller) ->
                            controller.mStageCoordinator.startShortcutAndTaskWithLegacyTransition(
                            controller.startShortcutAndTaskWithLegacyTransition(
                                    shortcutInfo, options1, taskId, options2, splitPosition,
                                    splitRatio, adapter, instanceId));
        }
+74 −12

File changed.

Preview size limit exceeded, changes collapsed.

+0 −4
Original line number Diff line number Diff line
@@ -27,8 +27,6 @@ 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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -201,7 +199,6 @@ public class SplitScreenControllerTests extends ShellTestCase {
        ActivityManager.RunningTaskInfo topRunningTask =
                createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
        doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask();
        doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any());

        mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);

@@ -222,7 +219,6 @@ public class SplitScreenControllerTests extends ShellTestCase {
        ActivityManager.RunningTaskInfo topRunningTask =
                createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
        doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask();
        doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any());
        // Put the same component into a task in the background
        ActivityManager.RecentTaskInfo sameTaskInfo = new ActivityManager.RecentTaskInfo();
        doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any());