Merge "Support split from fullscreen with shortcuts" into tm-qpr-dev

This commit is contained in:
Tracy Zhou
2022-11-01 17:17:33 +00:00
committed by Android (Google) Code Review
12 changed files with 417 additions and 92 deletions
@@ -70,6 +70,7 @@ import android.view.View;
import android.view.WindowManagerGlobal;
import android.window.SplashScreen;
import androidx.annotation.BinderThread;
import androidx.annotation.Nullable;
import com.android.app.viewcapture.ViewCapture;
@@ -135,6 +136,7 @@ import com.android.quickstep.util.QuickstepOnboardingPrefs;
import com.android.quickstep.util.RemoteAnimationProvider;
import com.android.quickstep.util.RemoteFadeOutAnimationListener;
import com.android.quickstep.util.SplitSelectStateController;
import com.android.quickstep.util.SplitWithKeyboardShortcutController;
import com.android.quickstep.util.TISBindHelper;
import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
@@ -179,6 +181,8 @@ public class QuickstepLauncher extends Launcher {
private @Nullable UnfoldTransitionProgressProvider mUnfoldTransitionProgressProvider;
private @Nullable RotationChangeProvider mRotationChangeProvider;
private @Nullable LauncherUnfoldAnimationController mLauncherUnfoldAnimationController;
private SplitWithKeyboardShortcutController mSplitWithKeyboardShortcutController;
/**
* If Launcher restarted while in the middle of an Overview split select, it needs this data to
* recover. In all other cases this will remain null.
@@ -194,11 +198,13 @@ public class QuickstepLauncher extends Launcher {
super.setupViews();
mActionsView = findViewById(R.id.overview_actions_view);
RecentsView overviewPanel = (RecentsView) getOverviewPanel();
RecentsView overviewPanel = getOverviewPanel();
SplitSelectStateController controller =
new SplitSelectStateController(this, mHandler, getStateManager(),
getDepthController(), getStatsLogManager());
overviewPanel.init(mActionsView, controller);
mSplitWithKeyboardShortcutController = new SplitWithKeyboardShortcutController(this,
controller);
mActionsView.updateDimension(getDeviceProfile(), overviewPanel.getLastComputedTaskSize());
mActionsView.updateVerticalMargin(DisplayController.getNavigationMode(this));
@@ -321,6 +327,17 @@ public class QuickstepLauncher extends Launcher {
super.showAllAppsFromIntent(alreadyOnHome);
}
protected void onItemClicked(View view) {
if (!mSplitWithKeyboardShortcutController.handleSecondAppSelectionForSplit(view)) {
QuickstepLauncher.super.getItemOnClickListener().onClick(view);
}
}
@Override
public View.OnClickListener getItemOnClickListener() {
return this::onItemClicked;
}
@Override
public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
Stream<SystemShortcut.Factory> base = Stream.of(WellbeingModel.SHORTCUT_FACTORY);
@@ -402,6 +419,7 @@ public class QuickstepLauncher extends Launcher {
super.onDestroy();
mHotseatPredictionController.destroy();
mSplitWithKeyboardShortcutController.onDestroy();
if (mViewCapture != null) mViewCapture.close();
}
@@ -832,6 +850,12 @@ public class QuickstepLauncher extends Launcher {
return activityOptions;
}
@Override
@BinderThread
public void enterStageSplitFromRunningApp(boolean leftOrTop) {
mSplitWithKeyboardShortcutController.enterStageSplit(leftOrTop);
}
/**
* Adds a new launch cookie for the activity launch if supported.
*
@@ -43,7 +43,6 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Matrix;
@@ -399,9 +398,8 @@ public final class TaskViewUtils {
*/
public static void composeRecentsSplitLaunchAnimator(GroupedTaskView launchingTaskView,
@NonNull StateManager stateManager, @Nullable DepthController depthController,
int initialTaskId, @Nullable PendingIntent initialTaskPendingIntent, int secondTaskId,
@NonNull TransitionInfo transitionInfo, SurfaceControl.Transaction t,
@NonNull Runnable finishCallback) {
int initialTaskId, int secondTaskId, @NonNull TransitionInfo transitionInfo,
SurfaceControl.Transaction t, @NonNull Runnable finishCallback) {
if (launchingTaskView != null) {
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.addListener(new AnimatorListenerAdapter() {
@@ -491,8 +489,7 @@ public final class TaskViewUtils {
* If it is null, then it will simply fade in the starting apps and fade out launcher (for the
* case where launcher handles animating starting split tasks from app icon) */
public static void composeRecentsSplitLaunchAnimatorLegacy(
@Nullable GroupedTaskView launchingTaskView, int initialTaskId,
@Nullable PendingIntent initialTaskPendingIntent, int secondTaskId,
@Nullable GroupedTaskView launchingTaskView, int initialTaskId, int secondTaskId,
@NonNull RemoteAnimationTarget[] appTargets,
@NonNull RemoteAnimationTarget[] wallpaperTargets,
@NonNull RemoteAnimationTarget[] nonAppTargets,
@@ -294,6 +294,16 @@ public class TouchInteractionService extends Service
MAIN_EXECUTOR.execute(ProxyScreenStatusProvider.INSTANCE::onScreenTurningOff);
}
@BinderThread
@Override
public void enterStageSplitFromRunningApp(boolean leftOrTop) {
StatefulActivity activity =
mOverviewComponentObserver.getActivityInterface().getCreatedActivity();
if (activity != null) {
activity.enterStageSplitFromRunningApp(leftOrTop);
}
}
/**
* Preloads the Overview activity.
*
@@ -22,8 +22,10 @@ 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;
@@ -57,6 +59,7 @@ import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
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;
@@ -84,6 +87,8 @@ public class SplitSelectStateController {
private ItemInfo mItemInfo;
private Intent mInitialTaskIntent;
private int mInitialTaskId = INVALID_TASK_ID;
private String mInitialTaskPackageName;
private Intent mSecondTaskIntent;
private int mSecondTaskId = INVALID_TASK_ID;
private String mSecondTaskPackageName;
private boolean mRecentsAnimationRunning;
@@ -95,6 +100,8 @@ public class SplitSelectStateController {
/** 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) {
mContext = context;
@@ -106,19 +113,36 @@ public class SplitSelectStateController {
}
/**
* To be called after first task selected
* To be called after first task selected in Overview.
*/
public void setInitialTaskSelect(int taskId, @StagePosition int stagePosition,
public void setInitialTaskSelect(Task task, @StagePosition int stagePosition,
StatsLogManager.EventEnum splitEvent, ItemInfo itemInfo) {
mInitialTaskId = taskId;
mInitialTaskId = task.key.id;
mInitialTaskPackageName = task.getTopComponent().getPackageName();
setInitialData(stagePosition, splitEvent, itemInfo);
}
/**
* To be called after first task selected from home or all apps.
*/
public void setInitialTaskSelect(Intent intent, @StagePosition int stagePosition,
@NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent) {
mInitialTaskIntent = intent;
mUser = itemInfo.user;
mItemInfo = itemInfo;
mInitialTaskPackageName = intent.getComponent().getPackageName();
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;
mInitialTaskPackageName = info.topActivity.getPackageName();
setInitialData(stagePosition, splitEvent, itemInfo);
}
@@ -134,27 +158,11 @@ public class SplitSelectStateController {
* to be launched. Call after launcher side animations are complete.
*/
public void launchSplitTasks(Consumer<Boolean> callback) {
final Intent fillInIntent;
if (mInitialTaskIntent != null) {
fillInIntent = new Intent();
if (TextUtils.equals(mInitialTaskIntent.getComponent().getPackageName(),
mSecondTaskPackageName)) {
fillInIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
}
} else {
fillInIntent = null;
}
final PendingIntent pendingIntent = mInitialTaskIntent == null ? null : (mUser != null
? PendingIntent.getActivityAsUser(mContext, 0, mInitialTaskIntent,
FLAG_MUTABLE, null /* options */, mUser)
: PendingIntent.getActivity(mContext, 0, mInitialTaskIntent, FLAG_MUTABLE));
Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds =
LogUtils.getShellShareableInstanceId();
launchTasks(mInitialTaskId, pendingIntent, fillInIntent, mSecondTaskId, mStagePosition,
callback, false /* freezeTaskList */, DEFAULT_SPLIT_RATIO,
instanceIds.first);
launchTasks(mInitialTaskId, mInitialTaskIntent, mInitialTaskPackageName, mSecondTaskId,
mSecondTaskIntent, mSecondTaskPackageName, mStagePosition, callback,
false /* freezeTaskList */, DEFAULT_SPLIT_RATIO, instanceIds.first);
mStatsLogManager.logger()
.withItemInfo(mItemInfo)
@@ -162,23 +170,25 @@ public class SplitSelectStateController {
.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;
if (mInitialTaskIntent != null) {
mSecondTaskPackageName = task.getTopComponent().getPackageName();
}
mSecondTaskPackageName = task.getTopComponent().getPackageName();
}
public void setSecondTask(Intent intent) {
mSecondTaskIntent = intent;
mSecondTaskPackageName = intent.getComponent().getPackageName();
}
/**
* To be called when we want to launch split pairs from an existing GroupedTaskView.
*/
public void launchTasks(GroupedTaskView groupedTaskView,
Consumer<Boolean> callback, boolean freezeTaskList) {
public void launchTasks(GroupedTaskView groupedTaskView, Consumer<Boolean> callback,
boolean freezeTaskList) {
mLaunchingTaskView = groupedTaskView;
TaskView.TaskIdAttributeContainer[] taskIdAttributeContainers =
groupedTaskView.getTaskIdAttributeContainers();
@@ -194,22 +204,23 @@ public class SplitSelectStateController {
*/
public void launchTasks(int taskId1, int taskId2, @StagePosition int stagePosition,
Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio) {
launchTasks(taskId1, null /* taskPendingIntent */, null /* fillInIntent */, taskId2,
stagePosition, callback, freezeTaskList, splitRatio, null);
launchTasks(taskId1, null /* intent1 */, null /* packageName1 */, taskId2,
null /* intent2 */, null /* packageName2 */, 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 taskPendingIntent is null when split is initiated from Overview
* @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 PendingIntent taskPendingIntent,
@Nullable Intent fillInIntent, int taskId2, @StagePosition int stagePosition,
public void launchTasks(int taskId1, @Nullable Intent intent1, String packageName1, int taskId2,
@Nullable Intent intent2, String packageName2, @StagePosition int stagePosition,
Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio,
@Nullable InstanceId shellInstanceId) {
TestLogging.recordEvent(
@@ -220,57 +231,107 @@ public class SplitSelectStateController {
}
if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
final RemoteSplitLaunchTransitionRunner animationRunner =
new RemoteSplitLaunchTransitionRunner(taskId1, taskPendingIntent, taskId2,
callback);
new RemoteSplitLaunchTransitionRunner(taskId1, taskId2, callback);
final RemoteTransitionCompat remoteTransition = new RemoteTransitionCompat(
animationRunner, MAIN_EXECUTOR,
ActivityThread.currentActivityThread().getApplicationThread());
if (taskPendingIntent == null) {
if (intent1 == null && intent2 == null) {
mSystemUiProxy.startTasks(taskId1, options1.toBundle(), taskId2,
null /* options2 */, stagePosition, splitRatio, remoteTransition,
shellInstanceId);
} else if (intent2 == null) {
launchIntentOrShortcut(intent1, packageName2, options1, taskId2, stagePosition,
splitRatio, remoteTransition, shellInstanceId);
} else if (intent1 == null) {
launchIntentOrShortcut(intent2, packageName1, options1, taskId1,
getOppositeStagePosition(stagePosition), splitRatio, remoteTransition,
shellInstanceId);
} else {
final ShortcutInfo shortcutInfo = getShortcutInfo(mInitialTaskIntent,
taskPendingIntent.getCreatorUserHandle());
if (shortcutInfo != null) {
mSystemUiProxy.startShortcutAndTask(shortcutInfo,
options1.toBundle(), taskId2, null /* options2 */, stagePosition,
splitRatio, remoteTransition, shellInstanceId);
} else {
mSystemUiProxy.startIntentAndTask(taskPendingIntent,
fillInIntent, options1.toBundle(), taskId2, null /* options2 */,
stagePosition, splitRatio, remoteTransition, shellInstanceId);
}
// TODO: the case when both split apps are started from an intent.
}
} else {
final RemoteSplitLaunchAnimationRunner animationRunner =
new RemoteSplitLaunchAnimationRunner(taskId1, taskPendingIntent, taskId2,
callback);
new RemoteSplitLaunchAnimationRunner(taskId1, taskId2, callback);
final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
RemoteAnimationAdapterCompat.wrapRemoteAnimationRunner(animationRunner),
300, 150,
ActivityThread.currentActivityThread().getApplicationThread());
if (taskPendingIntent == null) {
if (intent1 == null && intent2 == null) {
mSystemUiProxy.startTasksWithLegacyTransition(taskId1, options1.toBundle(),
taskId2, null /* options2 */, stagePosition, splitRatio, adapter,
shellInstanceId);
} else if (intent2 == null) {
launchIntentOrShortcutLegacy(intent1, packageName2, options1, taskId2,
stagePosition, splitRatio, adapter, shellInstanceId);
} else if (intent1 == null) {
launchIntentOrShortcutLegacy(intent2, packageName1, options1, taskId1,
getOppositeStagePosition(stagePosition), splitRatio, adapter,
shellInstanceId);
} else {
final ShortcutInfo shortcutInfo = getShortcutInfo(mInitialTaskIntent,
taskPendingIntent.getCreatorUserHandle());
if (shortcutInfo != null) {
mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(shortcutInfo,
options1.toBundle(), taskId2, null /* options2 */, stagePosition,
splitRatio, adapter, shellInstanceId);
} else {
mSystemUiProxy.startIntentAndTaskWithLegacyTransition(taskPendingIntent,
fillInIntent, options1.toBundle(), taskId2, null /* options2 */,
stagePosition, splitRatio, adapter, shellInstanceId);
}
// TODO: the case when both split apps are started from an intent.
}
}
}
private void launchIntentOrShortcut(Intent intent, String otherTaskPackageName,
ActivityOptions options1, int taskId, @StagePosition int stagePosition,
float splitRatio, RemoteTransitionCompat remoteTransition,
@Nullable InstanceId shellInstanceId) {
PendingIntent pendingIntent = getPendingIntent(intent);
final ShortcutInfo shortcutInfo = getShortcutInfo(intent,
pendingIntent.getCreatorUserHandle());
if (shortcutInfo != null) {
mSystemUiProxy.startShortcutAndTask(shortcutInfo,
options1.toBundle(), taskId, null /* options2 */, stagePosition,
splitRatio, remoteTransition, shellInstanceId);
} else {
mSystemUiProxy.startIntentAndTask(pendingIntent,
getFillInIntent(intent, otherTaskPackageName), options1.toBundle(), taskId,
null /* options2 */, stagePosition, splitRatio, remoteTransition,
shellInstanceId);
}
}
private void launchIntentOrShortcutLegacy(Intent intent, String otherTaskPackageName,
ActivityOptions options1, int taskId, @StagePosition int stagePosition,
float splitRatio, RemoteAnimationAdapter adapter,
@Nullable InstanceId shellInstanceId) {
PendingIntent pendingIntent = getPendingIntent(intent);
final ShortcutInfo shortcutInfo = getShortcutInfo(intent,
pendingIntent.getCreatorUserHandle());
if (shortcutInfo != null) {
mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(shortcutInfo,
options1.toBundle(), taskId, null /* options2 */, stagePosition,
splitRatio, adapter, shellInstanceId);
} else {
mSystemUiProxy.startIntentAndTaskWithLegacyTransition(pendingIntent,
getFillInIntent(intent, otherTaskPackageName), options1.toBundle(), taskId,
null /* options2 */, stagePosition, splitRatio, adapter,
shellInstanceId);
}
}
private PendingIntent getPendingIntent(Intent intent) {
return intent == null ? null : (mUser != null
? PendingIntent.getActivityAsUser(mContext, 0, intent,
FLAG_MUTABLE, null /* options */, mUser)
: PendingIntent.getActivity(mContext, 0, intent, FLAG_MUTABLE));
}
private Intent getFillInIntent(Intent intent, String otherTaskPackageName) {
if (intent == null) {
return null;
}
Intent fillInIntent = new Intent();
if (TextUtils.equals(intent.getComponent().getPackageName(), otherTaskPackageName)) {
fillInIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
}
return fillInIntent;
}
public @StagePosition int getActiveSplitStagePosition() {
return mStagePosition;
}
@@ -280,7 +341,7 @@ public class SplitSelectStateController {
}
public void setRecentsAnimationRunning(boolean running) {
this.mRecentsAnimationRunning = running;
mRecentsAnimationRunning = running;
}
@Nullable
@@ -311,14 +372,12 @@ public class SplitSelectStateController {
private class RemoteSplitLaunchTransitionRunner implements RemoteTransitionRunner {
private final int mInitialTaskId;
private final PendingIntent mInitialTaskPendingIntent;
private final int mSecondTaskId;
private final Consumer<Boolean> mSuccessCallback;
RemoteSplitLaunchTransitionRunner(int initialTaskId, PendingIntent initialTaskPendingIntent,
int secondTaskId, Consumer<Boolean> callback) {
RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId,
Consumer<Boolean> callback) {
mInitialTaskId = initialTaskId;
mInitialTaskPendingIntent = initialTaskPendingIntent;
mSecondTaskId = secondTaskId;
mSuccessCallback = callback;
}
@@ -327,12 +386,11 @@ public class SplitSelectStateController {
public void startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull Runnable finishCallback) {
TaskViewUtils.composeRecentsSplitLaunchAnimator(mLaunchingTaskView, mStateManager,
mDepthController, mInitialTaskId, mInitialTaskPendingIntent, mSecondTaskId,
info, t, () -> {
finishCallback.run();
if (mSuccessCallback != null) {
mSuccessCallback.accept(true);
}
mDepthController, mInitialTaskId, mSecondTaskId, info, t, () -> {
finishCallback.run();
if (mSuccessCallback != null) {
mSuccessCallback.accept(true);
}
});
// After successful launch, call resetState
resetState();
@@ -346,14 +404,12 @@ public class SplitSelectStateController {
private class RemoteSplitLaunchAnimationRunner implements RemoteAnimationRunnerCompat {
private final int mInitialTaskId;
private final PendingIntent mInitialTaskPendingIntent;
private final int mSecondTaskId;
private final Consumer<Boolean> mSuccessCallback;
RemoteSplitLaunchAnimationRunner(int initialTaskId, PendingIntent initialTaskPendingIntent,
int secondTaskId, Consumer<Boolean> successCallback) {
RemoteSplitLaunchAnimationRunner(int initialTaskId, int secondTaskId,
Consumer<Boolean> successCallback) {
mInitialTaskId = initialTaskId;
mInitialTaskPendingIntent = initialTaskPendingIntent;
mSecondTaskId = secondTaskId;
mSuccessCallback = successCallback;
}
@@ -364,9 +420,8 @@ public class SplitSelectStateController {
Runnable finishedCallback) {
postAsyncCallback(mHandler,
() -> TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy(
mLaunchingTaskView, mInitialTaskId, mInitialTaskPendingIntent,
mSecondTaskId, apps, wallpapers, nonApps, mStateManager,
mDepthController, () -> {
mLaunchingTaskView, mInitialTaskId, mSecondTaskId, apps, wallpapers,
nonApps, mStateManager, mDepthController, () -> {
finishedCallback.run();
if (mSuccessCallback != null) {
mSuccessCallback.accept(true);
@@ -394,7 +449,10 @@ public class SplitSelectStateController {
public void resetState() {
mInitialTaskId = INVALID_TASK_ID;
mInitialTaskIntent = null;
mInitialTaskPackageName = null;
mSecondTaskId = INVALID_TASK_ID;
mSecondTaskIntent = null;
mSecondTaskPackageName = null;
mStagePosition = SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
mRecentsAnimationRunning = false;
mLaunchingTaskView = null;
@@ -407,7 +465,7 @@ public class SplitSelectStateController {
* chosen
*/
public boolean isSplitSelectActive() {
return isInitialTaskIntentSet() && mSecondTaskId == INVALID_TASK_ID;
return isInitialTaskIntentSet() && !isSecondTaskIntentSet();
}
/**
@@ -415,7 +473,7 @@ public class SplitSelectStateController {
* be launched
*/
public boolean isBothSplitAppsConfirmed() {
return isInitialTaskIntentSet() && mSecondTaskId != INVALID_TASK_ID;
return isInitialTaskIntentSet() && isSecondTaskIntentSet();
}
private boolean isInitialTaskIntentSet() {
@@ -425,4 +483,16 @@ public class SplitSelectStateController {
public int getInitialTaskId() {
return mInitialTaskId;
}
private boolean isSecondTaskIntentSet() {
return (mSecondTaskId != INVALID_TASK_ID || mSecondTaskIntent != null);
}
public void setFirstFloatingTaskView(FloatingTaskView floatingTaskView) {
mFirstFloatingTaskView = floatingTaskView;
}
public FloatingTaskView getFirstFloatingTaskView() {
return mFirstFloatingTaskView;
}
}
@@ -0,0 +1,193 @@
/*
* 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.quickstep.util;
import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_RIGHT_BOTTOM;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.ActivityManager;
import android.content.Intent;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.SystemClock;
import android.os.UserHandle;
import android.view.View;
import androidx.annotation.BinderThread;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.quickstep.OverviewCommandHelper;
import com.android.quickstep.OverviewComponentObserver;
import com.android.quickstep.RecentsAnimationCallbacks;
import com.android.quickstep.RecentsAnimationController;
import com.android.quickstep.RecentsAnimationDeviceState;
import com.android.quickstep.RecentsAnimationTargets;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.views.FloatingTaskView;
import com.android.quickstep.views.RecentsView;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.ActivityManagerWrapper;
/** Transitions app from fullscreen to stage split when triggered from keyboard shortcuts. */
public class SplitWithKeyboardShortcutController {
private final QuickstepLauncher mLauncher;
private final SplitSelectStateController mController;
private final OverviewComponentObserver mOverviewComponentObserver;
private final int mSplitPlaceholderSize;
private final int mSplitPlaceholderInset;
public SplitWithKeyboardShortcutController(QuickstepLauncher launcher,
SplitSelectStateController controller) {
mLauncher = launcher;
mController = controller;
RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(
launcher.getApplicationContext());
mOverviewComponentObserver = new OverviewComponentObserver(launcher.getApplicationContext(),
deviceState);
mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize(
R.dimen.split_placeholder_size);
mSplitPlaceholderInset = mLauncher.getResources().getDimensionPixelSize(
R.dimen.split_placeholder_inset);
}
@BinderThread
public void enterStageSplit(boolean leftOrTop) {
if (!ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS.get()) {
return;
}
RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks(
SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()),
false /* allowMinimizeSplitScreen */);
SplitWithKeyboardShortcutRecentsAnimationListener listener =
new SplitWithKeyboardShortcutRecentsAnimationListener(leftOrTop);
MAIN_EXECUTOR.execute(() -> {
callbacks.addListener(listener);
UI_HELPER_EXECUTOR.execute(
// Transition from fullscreen app to enter stage split in launcher with
// recents animation.
() -> ActivityManagerWrapper.getInstance().startRecentsActivity(
mOverviewComponentObserver.getOverviewIntent(),
SystemClock.uptimeMillis(), callbacks, null, null));
});
}
/**
* Handles second app selection from stage split. If the item can't be opened in split or
* it's not in stage split state, we pass it onto Launcher's default item click handler.
*/
public boolean handleSecondAppSelectionForSplit(View view) {
if (!ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS.get()
|| !mController.isSplitSelectActive()) {
return false;
}
Object tag = view.getTag();
Intent intent;
if (tag instanceof WorkspaceItemInfo) {
final WorkspaceItemInfo workspaceItemInfo = (WorkspaceItemInfo) tag;
intent = workspaceItemInfo.intent;
} else if (tag instanceof com.android.launcher3.model.data.AppInfo) {
final com.android.launcher3.model.data.AppInfo appInfo =
(com.android.launcher3.model.data.AppInfo) tag;
intent = appInfo.intent;
} else {
return false;
}
mController.setSecondTask(intent);
mController.launchSplitTasks(aBoolean -> mLauncher.getDragLayer().removeView(
mController.getFirstFloatingTaskView()));
return true;
}
public void onDestroy() {
mOverviewComponentObserver.onDestroy();
}
private class SplitWithKeyboardShortcutRecentsAnimationListener implements
RecentsAnimationCallbacks.RecentsAnimationListener {
private final boolean mLeftOrTop;
private final Rect mTempRect = new Rect();
private SplitWithKeyboardShortcutRecentsAnimationListener(boolean leftOrTop) {
mLeftOrTop = leftOrTop;
}
@Override
public void onRecentsAnimationStart(RecentsAnimationController controller,
RecentsAnimationTargets targets) {
ActivityManager.RunningTaskInfo runningTaskInfo =
ActivityManagerWrapper.getInstance().getRunningTask();
mController.setInitialTaskSelect(runningTaskInfo,
mLeftOrTop ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT,
null /* itemInfo */,
mLeftOrTop ? LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP
: LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_RIGHT_BOTTOM);
RecentsView recentsView = mLauncher.getOverviewPanel();
recentsView.getPagedOrientationHandler().getInitialSplitPlaceholderBounds(
mSplitPlaceholderSize, mSplitPlaceholderInset, mLauncher.getDeviceProfile(),
mController.getActiveSplitStagePosition(), mTempRect);
PendingAnimation anim = new PendingAnimation(
SplitAnimationTimings.TABLET_HOME_TO_SPLIT.getDuration());
RectF startingTaskRect = new RectF();
final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView(
mLauncher, mLauncher.getDragLayer(),
controller.screenshotTask(runningTaskInfo.taskId).thumbnail,
null /* icon */, startingTaskRect);
RecentsModel.INSTANCE.get(mLauncher.getApplicationContext())
.getIconCache()
.updateIconInBackground(
Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo,
false /* isLocked */),
(task) -> {
if (task.thumbnail != null) {
floatingTaskView.setIcon(task.thumbnail.thumbnail);
}
});
floatingTaskView.setAlpha(1);
floatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect,
false /* fadeWithThumbnail */, true /* isStagedTask */);
mController.setFirstFloatingTaskView(floatingTaskView);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
controller.finish(true /* toRecents */, null /* onFinishComplete */,
false /* sendUserLeaveHint */);
}
});
anim.buildAnim().start();
}
};
}
@@ -11,6 +11,7 @@ import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.FloatProperty;
@@ -74,6 +75,7 @@ public class FloatingTaskView extends FrameLayout {
}
};
private int mSplitHolderSize;
private FloatingTaskThumbnailView mThumbnailView;
private SplitPlaceholderView mSplitPlaceholderView;
private RectF mStartingPosition;
@@ -97,6 +99,9 @@ public class FloatingTaskView extends FrameLayout {
mActivity = BaseActivity.fromContext(context);
mIsRtl = Utilities.isRtl(getResources());
mFullscreenParams = new FullscreenDrawParams(context);
mSplitHolderSize = context.getResources().getDimensionPixelSize(
R.dimen.split_placeholder_icon_size);
}
@Override
@@ -126,8 +131,7 @@ public class FloatingTaskView extends FrameLayout {
RecentsView recentsView = launcher.getOverviewPanel();
mOrientationHandler = recentsView.getPagedOrientationHandler();
mStagePosition = recentsView.getSplitSelectController().getActiveSplitStagePosition();
mSplitPlaceholderView.setIcon(icon,
mContext.getResources().getDimensionPixelSize(R.dimen.split_placeholder_icon_size));
mSplitPlaceholderView.setIcon(icon, mSplitHolderSize);
mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated());
}
@@ -193,6 +197,10 @@ public class FloatingTaskView extends FrameLayout {
mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated());
}
public void setIcon(Bitmap icon) {
mSplitPlaceholderView.setIcon(new BitmapDrawable(icon), mSplitHolderSize);
}
protected void initPosition(RectF pos, InsettableFrameLayout.LayoutParams lp) {
mStartingPosition.set(pos);
lp.ignoreInsets = true;
@@ -4184,7 +4184,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
public void initiateSplitSelect(TaskView taskView, @StagePosition int stagePosition,
StatsLogManager.EventEnum splitEvent) {
mSplitHiddenTaskView = taskView;
mSplitSelectStateController.setInitialTaskSelect(taskView.getTask().key.id,
mSplitSelectStateController.setInitialTaskSelect(taskView.getTask(),
stagePosition, splitEvent, taskView.getItemInfo());
mSplitHiddenTaskViewIndex = indexOfChild(taskView);
finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
+1 -1
View File
@@ -1332,7 +1332,7 @@ public class Launcher extends StatefulActivity<LauncherState>
BubbleTextView favorite = (BubbleTextView) LayoutInflater.from(parent.getContext())
.inflate(R.layout.app_icon, parent, false);
favorite.applyFromWorkspaceItem(info);
favorite.setOnClickListener(ItemClickHandler.INSTANCE);
favorite.setOnClickListener(getItemOnClickListener());
favorite.setOnFocusChangeListener(mFocusHandler);
return favorite;
}
@@ -253,6 +253,10 @@ public final class FeatureFlags {
"ENABLE_SPLIT_FROM_WORKSPACE", true,
"Enable initiating split screen from workspace.");
public static final BooleanFlag ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS =
getDebugFlag("ENABLE_SPLIT_FROM_FULLSCREEN_SHORTCUT", false,
"Enable splitting from fullscreen app with keyboard shortcuts");
public static final BooleanFlag ENABLE_NEW_MIGRATION_LOGIC = getDebugFlag(
"ENABLE_NEW_MIGRATION_LOGIC", true,
"Enable the new grid migration logic, keeping pages when src < dest");
@@ -617,6 +617,11 @@ public class StatsLogManager implements ResourceBasedOverride {
@UiEvent(doc = "Number of apps in A-Z list (personal and work profile)")
LAUNCHER_ALLAPPS_COUNT(1225),
@UiEvent(doc = "User has invoked split to right half with a keyboard shortcut.")
LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_RIGHT_BOTTOM(1232),
@UiEvent(doc = "User has invoked split to left half with a keyboard shortcut.")
LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP(1233)
;
// ADD MORE
@@ -231,4 +231,10 @@ public abstract class StatefulActivity<STATE_TYPE extends BaseState<STATE_TYPE>>
* etc.)
*/
protected abstract void onHandleConfigurationChanged();
/**
* Enter staged split directly from the current running app.
* @param leftOrTop if the staged split will be positioned left or top.
*/
public void enterStageSplitFromRunningApp(boolean leftOrTop) { }
}
@@ -182,4 +182,12 @@ public final class SplitConfigurationOptions {
? LAUNCHER_APP_ICON_MENU_SPLIT_LEFT_TOP
: LAUNCHER_APP_ICON_MENU_SPLIT_RIGHT_BOTTOM;
}
public static @StagePosition int getOppositeStagePosition(@StagePosition int position) {
if (position == STAGE_POSITION_UNDEFINED) {
return position;
}
return position == STAGE_POSITION_TOP_OR_LEFT ? STAGE_POSITION_BOTTOM_OR_RIGHT
: STAGE_POSITION_TOP_OR_LEFT;
}
}