diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index eaf577b8cc..b228fdb1cf 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -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 getSupportedShortcuts() { Stream 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. * diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java index 9d5e7c362c..e8722e26ad 100644 --- a/quickstep/src/com/android/quickstep/TaskViewUtils.java +++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java @@ -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, diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index e9f9d80e01..54c868d8e5 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -298,6 +298,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. * diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java index 3119a7714e..825c721961 100644 --- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java +++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java @@ -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 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 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 callback, boolean freezeTaskList) { + public void launchTasks(GroupedTaskView groupedTaskView, Consumer 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 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 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 mSuccessCallback; - RemoteSplitLaunchTransitionRunner(int initialTaskId, PendingIntent initialTaskPendingIntent, - int secondTaskId, Consumer callback) { + RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId, + Consumer 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 mSuccessCallback; - RemoteSplitLaunchAnimationRunner(int initialTaskId, PendingIntent initialTaskPendingIntent, - int secondTaskId, Consumer successCallback) { + RemoteSplitLaunchAnimationRunner(int initialTaskId, int secondTaskId, + Consumer 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; + } } diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java new file mode 100644 index 0000000000..3587bd1da5 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java @@ -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(); + } + }; +} diff --git a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java index dc1ae520a3..1d421b2081 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java +++ b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java @@ -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; diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index ce96b7168e..e56b7b9fd6 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -4184,7 +4184,7 @@ public abstract class RecentsView 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; } diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index 037a77e44f..694536c287 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -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"); diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java index 05f53fd209..06ba55663a 100644 --- a/src/com/android/launcher3/logging/StatsLogManager.java +++ b/src/com/android/launcher3/logging/StatsLogManager.java @@ -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 diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java index 2a890c3d1f..520f33ca74 100644 --- a/src/com/android/launcher3/statemanager/StatefulActivity.java +++ b/src/com/android/launcher3/statemanager/StatefulActivity.java @@ -231,4 +231,10 @@ public abstract class StatefulActivity> * 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) { } } diff --git a/src/com/android/launcher3/util/SplitConfigurationOptions.java b/src/com/android/launcher3/util/SplitConfigurationOptions.java index 3eff783586..19a39483e1 100644 --- a/src/com/android/launcher3/util/SplitConfigurationOptions.java +++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java @@ -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; + } }