blob: 723d8623ca667de09b8bed83009e2fff0c3d4c58 [file] [log] [blame]
/*
* Copyright 2021 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.quickstep.util;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT;
import static android.app.PendingIntent.FLAG_MUTABLE;
import static com.android.launcher3.Utilities.postAsyncCallback;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO;
import static com.android.launcher3.util.SplitConfigurationOptions.getOppositeStagePosition;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.ActivityThread;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.util.Pair;
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.window.IRemoteTransition;
import android.window.IRemoteTransitionFinishedCallback;
import android.window.RemoteTransition;
import android.window.TransitionInfo;
import androidx.annotation.Nullable;
import com.android.internal.logging.InstanceId;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.statehandlers.DepthController;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TaskAnimationManager;
import com.android.quickstep.TaskViewUtils;
import com.android.quickstep.views.FloatingTaskView;
import com.android.quickstep.views.GroupedTaskView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
import java.util.function.Consumer;
/**
* Represent data needed for the transient state when user has selected one app for split screen
* and is in the process of either a) selecting a second app or b) exiting intention to invoke split
*/
public class SplitSelectStateController {
private static final String TAG = "SplitSelectStateCtor";
private final Context mContext;
private final Handler mHandler;
private final RecentsModel mRecentTasksModel;
private final SplitAnimationController mSplitAnimationController;
private final AppPairsController mAppPairsController;
private StatsLogManager mStatsLogManager;
private final SystemUiProxy mSystemUiProxy;
private final StateManager mStateManager;
@Nullable
private DepthController mDepthController;
private @StagePosition int mInitialStagePosition;
private ItemInfo mItemInfo;
/** {@link #mInitialTaskIntent} and {@link #mInitialUser} (the user of the Intent) are set
* together when split is initiated from an Intent. */
private Intent mInitialTaskIntent;
private UserHandle mInitialUser;
private int mInitialTaskId = INVALID_TASK_ID;
/** {@link #mSecondTaskIntent} and {@link #mSecondUser} (the user of the Intent) are set
* together when split is confirmed with an Intent. */
private Intent mSecondTaskIntent;
private UserHandle mSecondUser;
private int mSecondTaskId = INVALID_TASK_ID;
private boolean mRecentsAnimationRunning;
/** If {@code true}, animates the existing task view split placeholder view */
private boolean mAnimateCurrentTaskDismissal;
/**
* Acts as a subset of {@link #mAnimateCurrentTaskDismissal}, we can't be dismissing from a
* split pair task view without wanting to animate current task dismissal overall
*/
private boolean mDismissingFromSplitPair;
/** If not null, this is the TaskView we want to launch from */
@Nullable
private GroupedTaskView mLaunchingTaskView;
/** Represents where split is intended to be invoked from. */
private StatsLogManager.EventEnum mSplitEvent;
private FloatingTaskView mFirstFloatingTaskView;
public SplitSelectStateController(Context context, Handler handler, StateManager stateManager,
DepthController depthController, StatsLogManager statsLogManager,
SystemUiProxy systemUiProxy, RecentsModel recentsModel) {
mContext = context;
mHandler = handler;
mStatsLogManager = statsLogManager;
mSystemUiProxy = systemUiProxy;
mStateManager = stateManager;
mDepthController = depthController;
mRecentTasksModel = recentsModel;
mSplitAnimationController = new SplitAnimationController(this);
mAppPairsController = new AppPairsController(context, this);
}
/**
* @param alreadyRunningTask if set to {@link android.app.ActivityTaskManager#INVALID_TASK_ID}
* then @param intent will be used to launch the initial task
* @param intent will be ignored if @param alreadyRunningTask is set
*/
public void setInitialTaskSelect(@Nullable Intent intent, @StagePosition int stagePosition,
@NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent,
int alreadyRunningTask) {
if (alreadyRunningTask != INVALID_TASK_ID) {
mInitialTaskId = alreadyRunningTask;
} else {
mInitialTaskIntent = intent;
mInitialUser = itemInfo.user;
}
setInitialData(stagePosition, splitEvent, itemInfo);
}
/**
* To be called after first task selected from using a split shortcut from the fullscreen
* running app.
*/
public void setInitialTaskSelect(ActivityManager.RunningTaskInfo info,
@StagePosition int stagePosition, @NonNull ItemInfo itemInfo,
StatsLogManager.EventEnum splitEvent) {
mInitialTaskId = info.taskId;
setInitialData(stagePosition, splitEvent, itemInfo);
}
private void setInitialData(@StagePosition int stagePosition,
StatsLogManager.EventEnum splitEvent, ItemInfo itemInfo) {
mItemInfo = itemInfo;
mInitialStagePosition = stagePosition;
mSplitEvent = splitEvent;
}
/**
* Pulls the list of active Tasks from RecentsModel, and finds the most recently active Task
* matching a given ComponentName. Then uses that Task (which could be null) with the given
* callback.
*
* Used in various task-switching or splitscreen operations when we need to check if there is a
* currently running Task of a certain type and use the most recent one.
*/
public void findLastActiveTaskAndRunCallback(ComponentKey componentKey,
Consumer<Task> callback) {
mRecentTasksModel.getTasks(taskGroups -> {
Task lastActiveTask = null;
// Loop through tasks in reverse, since they are ordered with most-recent tasks last.
for (int i = taskGroups.size() - 1; i >= 0; i--) {
GroupTask groupTask = taskGroups.get(i);
Task task1 = groupTask.task1;
if (isInstanceOfComponent(task1, componentKey)) {
lastActiveTask = task1;
break;
}
Task task2 = groupTask.task2;
if (isInstanceOfComponent(task2, componentKey)) {
lastActiveTask = task2;
break;
}
}
callback.accept(lastActiveTask);
});
}
/**
* Checks if a given Task is the most recently-active Task of type componentName. Used for
* selecting already-running Tasks for splitscreen.
*/
public boolean isInstanceOfComponent(@Nullable Task task, ComponentKey componentKey) {
// Exclude the task that is already staged
if (task == null || task.key.id == mInitialTaskId) {
return false;
}
return task.key.baseIntent.getComponent().equals(componentKey.componentName)
&& task.key.userId == componentKey.user.getIdentifier();
}
/**
* To be called when the actual tasks ({@link #mInitialTaskId}, {@link #mSecondTaskId}) are
* to be launched. Call after launcher side animations are complete.
*/
public void launchSplitTasks(Consumer<Boolean> callback) {
Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds =
LogUtils.getShellShareableInstanceId();
launchTasks(mInitialTaskId, mInitialTaskIntent, mSecondTaskId, mSecondTaskIntent,
mInitialStagePosition, callback, false /* freezeTaskList */, DEFAULT_SPLIT_RATIO,
instanceIds.first);
mStatsLogManager.logger()
.withItemInfo(mItemInfo)
.withInstanceId(instanceIds.second)
.log(mSplitEvent);
}
/**
* To be called as soon as user selects the second task (even if animations aren't complete)
* @param task The second task that will be launched.
*/
public void setSecondTask(Task task) {
mSecondTaskId = task.key.id;
}
/**
* To be called as soon as user selects the second app (even if animations aren't complete)
* @param intent The second intent that will be launched.
* @param user The user of that intent.
*/
public void setSecondTask(Intent intent, UserHandle user) {
mSecondTaskIntent = intent;
mSecondUser = user;
}
/**
* To be called when we want to launch split pairs from an existing GroupedTaskView.
*/
public void launchTasks(GroupedTaskView groupedTaskView, Consumer<Boolean> callback,
boolean freezeTaskList) {
mLaunchingTaskView = groupedTaskView;
TaskView.TaskIdAttributeContainer[] taskIdAttributeContainers =
groupedTaskView.getTaskIdAttributeContainers();
launchTasks(taskIdAttributeContainers[0].getTask().key.id,
taskIdAttributeContainers[1].getTask().key.id,
taskIdAttributeContainers[0].getStagePosition(), callback, freezeTaskList,
groupedTaskView.getSplitRatio());
}
/**
* To be called when we want to launch split pairs from Overview when split is initiated from
* Overview.
*/
public void launchTasks(int taskId1, int taskId2, @StagePosition int stagePosition,
Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio) {
launchTasks(taskId1, null /* intent1 */, taskId2, null /* intent2 */, stagePosition,
callback, freezeTaskList, splitRatio, null);
}
/**
* To be called when we want to launch split pairs from Overview. Split can be initiated from
* either Overview or home, or all apps. Either both taskIds are set, or a pending intent + a
* fill in intent with a taskId2 are set.
* @param intent1 is null when split is initiated from Overview
* @param stagePosition representing location of task1
* @param shellInstanceId loggingId to be used by shell, will be non-null for actions that
* create a split instance, null for cases that bring existing instaces to the
* foreground (quickswitch, launching previous pairs from overview)
*/
public void launchTasks(int taskId1, @Nullable Intent intent1, int taskId2,
@Nullable Intent intent2, @StagePosition int stagePosition,
Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio,
@Nullable InstanceId shellInstanceId) {
TestLogging.recordEvent(
TestProtocol.SEQUENCE_MAIN, "launchSplitTasks");
final ActivityOptions options1 = ActivityOptions.makeBasic();
if (freezeTaskList) {
options1.setFreezeRecentTasksReordering();
}
if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
final RemoteSplitLaunchTransitionRunner animationRunner =
new RemoteSplitLaunchTransitionRunner(taskId1, taskId2, callback);
final RemoteTransition remoteTransition = new RemoteTransition(animationRunner,
ActivityThread.currentActivityThread().getApplicationThread(),
"LaunchSplitPair");
if (intent1 == null && intent2 == null) {
mSystemUiProxy.startTasks(taskId1, options1.toBundle(), taskId2,
null /* options2 */, stagePosition, splitRatio, remoteTransition,
shellInstanceId);
} else if (intent2 == null) {
launchIntentOrShortcut(intent1, mInitialUser, options1, taskId2, stagePosition,
splitRatio, remoteTransition, shellInstanceId);
} else if (intent1 == null) {
launchIntentOrShortcut(intent2, mSecondUser, options1, taskId1,
getOppositeStagePosition(stagePosition), splitRatio, remoteTransition,
shellInstanceId);
} else {
mSystemUiProxy.startIntents(getPendingIntent(intent1, mInitialUser),
options1.toBundle(), getPendingIntent(intent2, mSecondUser),
null /* options2 */, stagePosition, splitRatio, remoteTransition,
shellInstanceId);
}
} else {
final RemoteSplitLaunchAnimationRunner animationRunner =
new RemoteSplitLaunchAnimationRunner(taskId1, taskId2, callback);
final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
animationRunner, 300, 150,
ActivityThread.currentActivityThread().getApplicationThread());
if (intent1 == null && intent2 == null) {
mSystemUiProxy.startTasksWithLegacyTransition(taskId1, options1.toBundle(),
taskId2, null /* options2 */, stagePosition, splitRatio, adapter,
shellInstanceId);
} else if (intent2 == null) {
launchIntentOrShortcutLegacy(intent1, mInitialUser, options1, taskId2,
stagePosition, splitRatio, adapter, shellInstanceId);
} else if (intent1 == null) {
launchIntentOrShortcutLegacy(intent2, mSecondUser, options1, taskId1,
getOppositeStagePosition(stagePosition), splitRatio, adapter,
shellInstanceId);
} else {
mSystemUiProxy.startIntentsWithLegacyTransition(
getPendingIntent(intent1, mInitialUser),
getShortcutInfo(intent1, mInitialUser), options1.toBundle(),
getPendingIntent(intent2, mSecondUser),
getShortcutInfo(intent2, mSecondUser), null /* options2 */, stagePosition,
splitRatio, adapter, shellInstanceId);
}
}
}
private void launchIntentOrShortcut(Intent intent, UserHandle user, ActivityOptions options1,
int taskId, @StagePosition int stagePosition, float splitRatio,
RemoteTransition remoteTransition, @Nullable InstanceId shellInstanceId) {
final ShortcutInfo shortcutInfo = getShortcutInfo(intent, user);
if (shortcutInfo != null) {
mSystemUiProxy.startShortcutAndTask(shortcutInfo,
options1.toBundle(), taskId, null /* options2 */, stagePosition,
splitRatio, remoteTransition, shellInstanceId);
} else {
mSystemUiProxy.startIntentAndTask(getPendingIntent(intent, user),
options1.toBundle(), taskId, null /* options2 */, stagePosition, splitRatio,
remoteTransition, shellInstanceId);
}
}
private void launchIntentOrShortcutLegacy(Intent intent, UserHandle user,
ActivityOptions options1, int taskId, @StagePosition int stagePosition,
float splitRatio, RemoteAnimationAdapter adapter,
@Nullable InstanceId shellInstanceId) {
final ShortcutInfo shortcutInfo = getShortcutInfo(intent, user);
if (shortcutInfo != null) {
mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(shortcutInfo,
options1.toBundle(), taskId, null /* options2 */, stagePosition,
splitRatio, adapter, shellInstanceId);
} else {
mSystemUiProxy.startIntentAndTaskWithLegacyTransition(
getPendingIntent(intent, user), options1.toBundle(), taskId,
null /* options2 */, stagePosition, splitRatio, adapter, shellInstanceId);
}
}
private PendingIntent getPendingIntent(Intent intent, UserHandle user) {
return intent == null ? null : (user != null
? PendingIntent.getActivityAsUser(mContext, 0, intent,
FLAG_MUTABLE | FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT, null /* options */, user)
: PendingIntent.getActivity(mContext, 0, intent,
FLAG_MUTABLE | FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT));
}
public @StagePosition int getActiveSplitStagePosition() {
return mInitialStagePosition;
}
public StatsLogManager.EventEnum getSplitEvent() {
return mSplitEvent;
}
public void setRecentsAnimationRunning(boolean running) {
mRecentsAnimationRunning = running;
}
@Nullable
private ShortcutInfo getShortcutInfo(Intent intent, UserHandle user) {
if (intent == null || intent.getPackage() == null) {
return null;
}
final String shortcutId = intent.getStringExtra(ShortcutKey.EXTRA_SHORTCUT_ID);
if (shortcutId == null) {
return null;
}
try {
final Context context = mContext.createPackageContextAsUser(
intent.getPackage(), 0 /* flags */, user);
return new ShortcutInfo.Builder(context, shortcutId).build();
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Failed to create a ShortcutInfo for " + intent.getPackage());
}
return null;
}
public boolean isAnimateCurrentTaskDismissal() {
return mAnimateCurrentTaskDismissal;
}
public void setAnimateCurrentTaskDismissal(boolean animateCurrentTaskDismissal) {
mAnimateCurrentTaskDismissal = animateCurrentTaskDismissal;
}
public boolean isDismissingFromSplitPair() {
return mDismissingFromSplitPair;
}
public void setDismissingFromSplitPair(boolean dismissingFromSplitPair) {
mDismissingFromSplitPair = dismissingFromSplitPair;
}
public SplitAnimationController getSplitAnimationController() {
return mSplitAnimationController;
}
/**
* Requires Shell Transitions
*/
private class RemoteSplitLaunchTransitionRunner extends IRemoteTransition.Stub {
private final int mInitialTaskId;
private final int mSecondTaskId;
private final Consumer<Boolean> mSuccessCallback;
RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId,
Consumer<Boolean> callback) {
mInitialTaskId = initialTaskId;
mSecondTaskId = secondTaskId;
mSuccessCallback = callback;
}
@Override
public void startAnimation(IBinder transition, TransitionInfo info,
SurfaceControl.Transaction t,
IRemoteTransitionFinishedCallback finishedCallback) {
final Runnable finishAdapter = () -> {
try {
finishedCallback.onTransitionFinished(null /* wct */, null /* sct */);
} catch (RemoteException e) {
Log.e(TAG, "Failed to call transition finished callback", e);
}
};
MAIN_EXECUTOR.execute(() -> {
TaskViewUtils.composeRecentsSplitLaunchAnimator(mLaunchingTaskView, mStateManager,
mDepthController, mInitialTaskId, mSecondTaskId, info, t, () -> {
finishAdapter.run();
if (mSuccessCallback != null) {
mSuccessCallback.accept(true);
}
});
// After successful launch, call resetState
resetState();
});
}
@Override
public void mergeAnimation(IBinder transition, TransitionInfo info,
SurfaceControl.Transaction t, IBinder mergeTarget,
IRemoteTransitionFinishedCallback finishedCallback) { }
}
/**
* LEGACY
* Remote animation runner for animation to launch an app.
*/
private class RemoteSplitLaunchAnimationRunner extends RemoteAnimationRunnerCompat {
private final int mInitialTaskId;
private final int mSecondTaskId;
private final Consumer<Boolean> mSuccessCallback;
RemoteSplitLaunchAnimationRunner(int initialTaskId, int secondTaskId,
Consumer<Boolean> successCallback) {
mInitialTaskId = initialTaskId;
mSecondTaskId = secondTaskId;
mSuccessCallback = successCallback;
}
@Override
public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
Runnable finishedCallback) {
postAsyncCallback(mHandler,
() -> TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy(
mLaunchingTaskView, mInitialTaskId, mSecondTaskId, apps, wallpapers,
nonApps, mStateManager, mDepthController, () -> {
finishedCallback.run();
if (mSuccessCallback != null) {
mSuccessCallback.accept(true);
}
resetState();
}));
}
@Override
public void onAnimationCancelled(boolean isKeyguardOccluded) {
postAsyncCallback(mHandler, () -> {
if (mSuccessCallback != null) {
// Launching legacy tasks while recents animation is running will always cause
// onAnimationCancelled to be called (should be fixed w/ shell transitions?)
mSuccessCallback.accept(mRecentsAnimationRunning);
}
resetState();
});
}
}
/**
* To be called if split select was cancelled
*/
public void resetState() {
mInitialTaskId = INVALID_TASK_ID;
mInitialTaskIntent = null;
mSecondTaskId = INVALID_TASK_ID;
mSecondTaskIntent = null;
mInitialUser = null;
mSecondUser = null;
mInitialStagePosition = SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
mRecentsAnimationRunning = false;
mLaunchingTaskView = null;
mItemInfo = null;
mSplitEvent = null;
mAnimateCurrentTaskDismissal = false;
mDismissingFromSplitPair = false;
}
/**
* @return {@code true} if first task has been selected and waiting for the second task to be
* chosen
*/
public boolean isSplitSelectActive() {
return isInitialTaskIntentSet() && !isSecondTaskIntentSet();
}
/**
* @return {@code true} if the first and second task have been chosen and split is waiting to
* be launched
*/
public boolean isBothSplitAppsConfirmed() {
return isInitialTaskIntentSet() && isSecondTaskIntentSet();
}
private boolean isInitialTaskIntentSet() {
return (mInitialTaskId != INVALID_TASK_ID || mInitialTaskIntent != null);
}
public int getInitialTaskId() {
return mInitialTaskId;
}
public int getSecondTaskId() {
return mSecondTaskId;
}
private boolean isSecondTaskIntentSet() {
return (mSecondTaskId != INVALID_TASK_ID || mSecondTaskIntent != null);
}
public void setFirstFloatingTaskView(FloatingTaskView floatingTaskView) {
mFirstFloatingTaskView = floatingTaskView;
}
public FloatingTaskView getFirstFloatingTaskView() {
return mFirstFloatingTaskView;
}
public AppPairsController getAppPairsController() {
return mAppPairsController;
}
}