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

Commit b579795a authored by Jerry Chang's avatar Jerry Chang Committed by Automerger Merge Worker
Browse files

Merge "Support to launch multi-instance when long press to split on an app...

Merge "Support to launch multi-instance when long press to split on an app icon" into tm-qpr-dev am: 312e15b0 am: 729976c5

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19008926



Change-Id: Iae16815d8248d4e776fdb9f3f3364ed7803ffb1e
Signed-off-by: default avatarAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
parents 14da2a47 729976c5
Loading
Loading
Loading
Loading
+1 −43
Original line number Diff line number Diff line
@@ -45,12 +45,10 @@ import android.app.WindowConfiguration;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.ResolveInfo;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Bundle;
@@ -64,19 +62,15 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.android.internal.logging.InstanceId;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.split.SplitLayout;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * The policy for handling drag and drop operations to shell.
@@ -269,46 +263,10 @@ public class DragAndDropPolicy {
            mStarter.startShortcut(packageName, id, position, opts, user);
        } else {
            final PendingIntent launchIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT);
            mStarter.startIntent(launchIntent, getStartIntentFillInIntent(launchIntent, position),
                    position, opts);
            mStarter.startIntent(launchIntent, null /* fillIntent */, position, opts);
        }
    }

    /**
     * Returns the fill-in intent to use when starting an app from a drop.
     */
    @VisibleForTesting
    Intent getStartIntentFillInIntent(PendingIntent launchIntent, @SplitPosition int position) {
        // Get the drag app
        final List<ResolveInfo> infos = launchIntent.queryIntentComponents(0 /* flags */);
        final ComponentName dragIntentActivity = !infos.isEmpty()
                ? infos.get(0).activityInfo.getComponentName()
                : null;

        // Get the current app (either fullscreen or the remaining app post-drop if in splitscreen)
        final boolean inSplitScreen = mSplitScreen != null
                && mSplitScreen.isSplitScreenVisible();
        final ComponentName currentActivity;
        if (!inSplitScreen) {
            currentActivity = mSession.runningTaskInfo != null
                    ? mSession.runningTaskInfo.baseIntent.getComponent()
                    : null;
        } else {
            final ActivityManager.RunningTaskInfo nonReplacedTaskInfo =
                    mSplitScreen.getTaskInfo(SplitLayout.reversePosition(position));
            currentActivity = nonReplacedTaskInfo.baseIntent.getComponent();
        }

        if (Objects.equals(currentActivity, dragIntentActivity)) {
            // Only apply MULTIPLE_TASK if we are dragging the same activity
            final Intent fillInIntent = new Intent();
            fillInIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Adding MULTIPLE_TASK");
            return fillInIntent;
        }
        return null;
    }

    /**
     * Per-drag session data.
     */
+64 −12
Original line number Diff line number Diff line
@@ -18,6 +18,9 @@ package com.android.wm.shell.splitscreen;

import static android.app.ActivityManager.START_SUCCESS;
import static android.app.ActivityManager.START_TASK_TO_FRONT;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
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;
@@ -58,7 +61,9 @@ import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.InstanceId;
import com.android.internal.protolog.common.ProtoLog;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -74,6 +79,7 @@ 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.draganddrop.DragAndDropPolicy;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.splitscreen.SplitScreen.StageType;
import com.android.wm.shell.transition.LegacyTransitions;
@@ -95,7 +101,7 @@ import java.util.concurrent.Executor;
 */
// TODO(b/198577848): Implement split screen flicker test to consolidate CUJ of split screen.
public class SplitScreenController implements DragAndDropPolicy.Starter,
        RemoteCallable<SplitScreenController> {
        RemoteCallable<SplitScreenController>, ShellTaskOrganizer.FocusListener {
    private static final String TAG = SplitScreenController.class.getSimpleName();

    static final int EXIT_REASON_UNKNOWN = 0;
@@ -143,6 +149,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
    // outside the bounds of the roots by being reparented into a higher level fullscreen container
    private SurfaceControl mSplitTasksContainerLayer;

    private ActivityManager.RunningTaskInfo mFocusingTaskInfo;

    public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer,
            SyncTransactionQueue syncQueue, Context context,
            RootTaskDisplayAreaOrganizer rootTDAOrganizer,
@@ -164,6 +172,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
        mLogger = new SplitscreenEventLogger();
        mIconProvider = iconProvider;
        mRecentTasksOptional = recentTasks;
        mTaskOrganizer.addFocusListener(this);
    }

    public SplitScreen asSplitScreen() {
@@ -180,6 +189,11 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
        return mMainExecutor;
    }

    @Override
    public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
        mFocusingTaskInfo = taskInfo;
    }

    public void onOrganizerRegistered() {
        if (mStageCoordinator == null) {
            // TODO: Multi-display
@@ -341,14 +355,21 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
            options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
                    null /* wct */);

            // Flag this as a no-user-action launch to prevent sending user leaving event to the
            // current top activity since it's going to be put into another side of the split. This
            // prevents the current top activity from going into pip mode due to user leaving event.
            if (fillInIntent == null) {
                fillInIntent = new Intent();
            }
            // Flag this as a no-user-action launch to prevent sending user leaving event to the
            // current top activity since it's going to be put into another side of the split. This
            // prevents the current top activity from going into pip mode due to user leaving event.
            fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);

            // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of the
            // split.
            if (isLaunchingAdjacently(intent.getIntent(), position)) {
                fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
            }

            intent.send(mContext, 0, fillInIntent, null /* onFinished */, null /* handler */,
                    null /* requiredPermission */, options);
        } catch (PendingIntent.CanceledException e) {
@@ -358,6 +379,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,

    private void startIntentLegacy(PendingIntent intent, @Nullable Intent fillInIntent,
            @SplitPosition int position, @Nullable Bundle options) {
        boolean startSameActivityAdjacently = isLaunchingAdjacently(intent.getIntent(), position);

        final WindowContainerTransaction evictWct = new WindowContainerTransaction();
        mStageCoordinator.prepareEvictChildTasks(position, evictWct);

@@ -368,14 +391,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
                    IRemoteAnimationFinishedCallback finishedCallback,
                    SurfaceControl.Transaction t) {
                if (apps == null || apps.length == 0) {
                    final ActivityManager.RunningTaskInfo pairedTaskInfo =
                            getTaskInfo(SplitLayout.reversePosition(position));
                    final ComponentName pairedActivity = pairedTaskInfo != null
                            ? pairedTaskInfo.baseIntent.getComponent() : null;
                    final ComponentName intentActivity = intent.getIntent() != null
                            ? intent.getIntent().getComponent() : null;

                    if (Objects.equals(pairedActivity, intentActivity)) {
                    if (startSameActivityAdjacently) {
                        // Switch split position if dragging the same activity to another side.
                        setSideStagePosition(SplitLayout.reversePosition(
                                mStageCoordinator.getSideStagePosition()));
@@ -417,11 +433,47 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
            fillInIntent = new Intent();
        }
        fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
        if (startSameActivityAdjacently) {
            fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
        }

        wct.sendPendingIntent(intent, fillInIntent, options);
        mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct);
    }

    /** Returns {@code true} if it's launching the same component on both sides of the split. */
    @VisibleForTesting
    boolean isLaunchingAdjacently(@Nullable Intent startIntent,
            @SplitPosition int position) {
        if (startIntent == null) {
            return false;
        }

        final ComponentName launchingActivity = startIntent.getComponent();
        if (launchingActivity == null) {
            return false;
        }

        if (isSplitScreenVisible()) {
            final ActivityManager.RunningTaskInfo pairedTaskInfo =
                    getTaskInfo(SplitLayout.reversePosition(position));
            final ComponentName pairedActivity = pairedTaskInfo != null
                    ? pairedTaskInfo.baseIntent.getComponent() : null;
            return Objects.equals(launchingActivity, pairedActivity);
        }

        if (mFocusingTaskInfo != null
                // TODO (b/238032411): have an API to determine whether an activity is valid for
                //  split screen or not.
                && mFocusingTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
                && mFocusingTaskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) {
            return Objects.equals(mFocusingTaskInfo.baseIntent.getComponent(), launchingActivity);
        }

        return false;
    }

    RemoteAnimationTarget[] onGoingToRecentsLegacy(RemoteAnimationTarget[] apps) {
        if (isSplitScreenVisible()) {
            // Evict child tasks except the top visible one under split root to ensure it could be
+6 −5
Original line number Diff line number Diff line
@@ -200,11 +200,6 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
    @Override
    @CallSuper
    public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
        if (!taskInfo.supportsMultiWindow) {
            // Leave split screen if the task no longer supports multi window.
            mCallbacks.onNoLongerSupportMultiWindow();
            return;
        }
        if (mRootTaskInfo.taskId == taskInfo.taskId) {
            // Inflates split decor view only when the root task is visible.
            if (mRootTaskInfo.isVisible != taskInfo.isVisible) {
@@ -217,6 +212,12 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
            }
            mRootTaskInfo = taskInfo;
        } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
            if (!taskInfo.supportsMultiWindow) {
                // Leave split screen if the task no longer supports multi window.
                mCallbacks.onNoLongerSupportMultiWindow();
                return;
            }

            mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);
            mCallbacks.onChildTaskStatusChanged(taskInfo.taskId, true /* present */,
                    taskInfo.isVisible);
+0 −58
Original line number Diff line number Diff line
@@ -34,7 +34,6 @@ import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPL
import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT;
import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP;

import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;

@@ -57,7 +56,6 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Insets;
@@ -265,62 +263,6 @@ public class DragAndDropPolicyTest {
        }
    }

    @Test
    public void testLaunchMultipleTask_differentActivity() {
        setRunningTask(mFullscreenAppTask);
        mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
        Intent fillInIntent = mPolicy.getStartIntentFillInIntent(mock(PendingIntent.class), 0);
        assertNull(fillInIntent);
    }

    @Test
    public void testLaunchMultipleTask_differentActivity_inSplitscreen() {
        setRunningTask(mFullscreenAppTask);
        doReturn(true).when(mSplitScreenStarter).isSplitScreenVisible();
        doReturn(mFullscreenAppTask).when(mSplitScreenStarter).getTaskInfo(anyInt());
        mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
        Intent fillInIntent = mPolicy.getStartIntentFillInIntent(mock(PendingIntent.class), 0);
        assertNull(fillInIntent);
    }

    @Test
    public void testLaunchMultipleTask_sameActivity() {
        setRunningTask(mFullscreenAppTask);

        // Replace the mocked drag pending intent and ensure it resolves to the same activity
        PendingIntent launchIntent = mock(PendingIntent.class);
        ResolveInfo launchInfo = new ResolveInfo();
        launchInfo.activityInfo = mFullscreenAppTask.topActivityInfo;
        doReturn(Collections.singletonList(launchInfo))
                .when(launchIntent).queryIntentComponents(anyInt());
        mActivityClipData.getItemAt(0).getIntent().putExtra(ClipDescription.EXTRA_PENDING_INTENT,
                launchIntent);

        mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
        Intent fillInIntent = mPolicy.getStartIntentFillInIntent(launchIntent, 0);
        assertTrue((fillInIntent.getFlags() & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0);
    }

    @Test
    public void testLaunchMultipleTask_sameActivity_inSplitScreen() {
        setRunningTask(mFullscreenAppTask);

        // Replace the mocked drag pending intent and ensure it resolves to the same activity
        PendingIntent launchIntent = mock(PendingIntent.class);
        ResolveInfo launchInfo = new ResolveInfo();
        launchInfo.activityInfo = mFullscreenAppTask.topActivityInfo;
        doReturn(Collections.singletonList(launchInfo))
                .when(launchIntent).queryIntentComponents(anyInt());
        mActivityClipData.getItemAt(0).getIntent().putExtra(ClipDescription.EXTRA_PENDING_INTENT,
                launchIntent);

        doReturn(true).when(mSplitScreenStarter).isSplitScreenVisible();
        doReturn(mFullscreenAppTask).when(mSplitScreenStarter).getTaskInfo(anyInt());
        mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
        Intent fillInIntent = mPolicy.getStartIntentFillInIntent(launchIntent, 0);
        assertTrue((fillInIntent.getFlags() & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0);
    }

    private Target filterTargetByType(ArrayList<Target> targets, int type) {
        for (Target t : targets) {
            if (type == t.type) {
+152 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.splitscreen;

import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;

import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;

import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.transition.Transitions;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Optional;

/**
 * Tests for {@link SplitScreenController}
 */
@SmallTest
@RunWith(AndroidJUnit4.class)
public class SplitScreenControllerTests extends ShellTestCase {

    @Mock ShellTaskOrganizer mTaskOrganizer;
    @Mock SyncTransactionQueue mSyncQueue;
    @Mock RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
    @Mock ShellExecutor mMainExecutor;
    @Mock DisplayController mDisplayController;
    @Mock DisplayImeController mDisplayImeController;
    @Mock DisplayInsetsController mDisplayInsetsController;
    @Mock Transitions mTransitions;
    @Mock TransactionPool mTransactionPool;
    @Mock IconProvider mIconProvider;
    @Mock Optional<RecentTasksController> mRecentTasks;

    private SplitScreenController mSplitScreenController;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        mSplitScreenController = spy(new SplitScreenController(mTaskOrganizer, mSyncQueue, mContext,
                mRootTDAOrganizer, mMainExecutor, mDisplayController, mDisplayImeController,
                mDisplayInsetsController, mTransitions, mTransactionPool, mIconProvider,
                mRecentTasks));
    }

    @Test
    public void testIsLaunchingAdjacently_notInSplitScreen() {
        doReturn(false).when(mSplitScreenController).isSplitScreenVisible();

        // Verify launching the same activity returns true.
        Intent startIntent = createStartIntent("startActivity");
        ActivityManager.RunningTaskInfo focusTaskInfo =
                createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
        mSplitScreenController.onFocusTaskChanged(focusTaskInfo);
        assertTrue(mSplitScreenController.isLaunchingAdjacently(
                startIntent, SPLIT_POSITION_TOP_OR_LEFT));

        // Verify launching different activity returns false.
        Intent diffIntent = createStartIntent("diffActivity");
        focusTaskInfo =
                createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, diffIntent);
        mSplitScreenController.onFocusTaskChanged(focusTaskInfo);
        assertFalse(mSplitScreenController.isLaunchingAdjacently(
                startIntent, SPLIT_POSITION_TOP_OR_LEFT));
    }

    @Test
    public void testIsLaunchingAdjacently_inSplitScreen() {
        doReturn(true).when(mSplitScreenController).isSplitScreenVisible();

        // Verify launching the same activity returns true.
        Intent startIntent = createStartIntent("startActivity");
        ActivityManager.RunningTaskInfo pairingTaskInfo =
                createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent);
        doReturn(pairingTaskInfo).when(mSplitScreenController).getTaskInfo(anyInt());
        assertTrue(mSplitScreenController.isLaunchingAdjacently(
                startIntent, SPLIT_POSITION_TOP_OR_LEFT));

        // Verify launching different activity returns false.
        Intent diffIntent = createStartIntent("diffActivity");
        pairingTaskInfo =
                createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, diffIntent);
        doReturn(pairingTaskInfo).when(mSplitScreenController).getTaskInfo(anyInt());
        assertFalse(mSplitScreenController.isLaunchingAdjacently(
                startIntent, SPLIT_POSITION_TOP_OR_LEFT));
    }

    private Intent createStartIntent(String activityName) {
        Intent intent = new Intent();
        intent.setComponent(new ComponentName(mContext, activityName));
        return intent;
    }

    private ActivityManager.RunningTaskInfo createTaskInfo(int winMode, int actType,
            Intent strIntent) {
        ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
        info.configuration.windowConfiguration.setActivityType(actType);
        info.configuration.windowConfiguration.setWindowingMode(winMode);
        info.supportsMultiWindow = true;
        info.baseIntent = strIntent;
        info.baseActivity = strIntent.getComponent();
        ActivityInfo activityInfo = new ActivityInfo();
        activityInfo.packageName = info.baseActivity.getPackageName();
        activityInfo.name = info.baseActivity.getClassName();
        info.topActivityInfo = activityInfo;
        return info;
    }
}