From fd46900b969f7ffc55fcbc26a2f0c50506e8765b Mon Sep 17 00:00:00 2001 From: Vinit Nayak Date: Tue, 18 Apr 2023 17:54:14 -0700 Subject: [PATCH] Support splitting from workspace with Widgets * Need to insert widget's icon in animation * Launching w/ same package app + widget is broken Test: Launched apps from predicted apps + widget, hotseat apps + widget Flag: ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE Bug: 276361926 Change-Id: I3e30189e56536371ebd0acfbdd2c073a882cc731 --- .../QuickstepInteractionHandler.java | 4 ++ .../uioverrides/QuickstepLauncher.java | 4 +- .../util/SplitSelectStateController.java | 55 ++++++++++++++++--- .../util/SplitToWorkspaceController.java | 52 ++++++++++++++++-- .../util/SplitSelectStateControllerTest.kt | 12 ++++ 5 files changed, 111 insertions(+), 16 deletions(-) diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java index 08d147f7cf..163c36fa24 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java @@ -56,6 +56,10 @@ class QuickstepInteractionHandler implements RemoteViews.InteractionHandler { return RemoteViews.startPendingIntent(hostView, pendingIntent, remoteResponse.getLaunchOptions(view)); } + if (mLauncher.getSplitToWorkspaceController().handleSecondWidgetSelectionForSplit(view, + pendingIntent)) { + return true; + } Pair options = remoteResponse.getLaunchOptions(view); ActivityOptionsWrapper activityOptions = mLauncher.getAppTransitionManager() .getActivityLaunchOptions(hostView); diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index b2b062344e..5b90527c18 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -970,8 +970,8 @@ public class QuickstepLauncher extends Launcher { return mTaskbarUIController; } - public SplitSelectStateController getSplitSelectStateController() { - return mSplitSelectStateController; + public SplitToWorkspaceController getSplitToWorkspaceController() { + return mSplitToWorkspaceController; } public T getActionsView() { diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java index d44d7f637d..9fc6ad6eaa 100644 --- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java +++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java @@ -97,8 +97,15 @@ public class SplitSelectStateController { 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. */ + * together when split is confirmed with an Intent. Either this or {@link #mSecondPendingIntent} + * will be set, but not both + */ private Intent mSecondTaskIntent; + /** + * Set when split is confirmed via a widget. Either this or {@link #mSecondTaskIntent} will be + * set, but not both + */ + private PendingIntent mSecondPendingIntent; private UserHandle mSecondUser; private int mSecondTaskId = INVALID_TASK_ID; private boolean mRecentsAnimationRunning; @@ -246,6 +253,16 @@ public class SplitSelectStateController { mSecondUser = user; } + /** + * To be called as soon as user selects the second app (even if animations aren't complete) + * Sets {@link #mSecondUser} from that of the pendingIntent + * @param pendingIntent The second PendingIntent that will be launched. + */ + public void setSecondTask(PendingIntent pendingIntent) { + mSecondPendingIntent = pendingIntent; + mSecondUser = pendingIntent.getCreatorUserHandle(); + } + /** * To be called when we want to launch split pairs from an existing GroupedTaskView. */ @@ -290,17 +307,18 @@ public class SplitSelectStateController { if (freezeTaskList) { options1.setFreezeRecentTasksReordering(); } + boolean hasSecondaryPendingIntent = mSecondPendingIntent != null; 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) { + if (intent1 == null && (intent2 == null && !hasSecondaryPendingIntent)) { mSystemUiProxy.startTasks(taskId1, options1.toBundle(), taskId2, null /* options2 */, stagePosition, splitRatio, remoteTransition, shellInstanceId); - } else if (intent2 == null) { + } else if (intent2 == null && !hasSecondaryPendingIntent) { launchIntentOrShortcut(intent1, mInitialUser, options1, taskId2, stagePosition, splitRatio, remoteTransition, shellInstanceId); } else if (intent1 == null) { @@ -310,7 +328,9 @@ public class SplitSelectStateController { } else { mSystemUiProxy.startIntents(getPendingIntent(intent1, mInitialUser), getShortcutInfo(intent1, mInitialUser), options1.toBundle(), - getPendingIntent(intent2, mSecondUser), + hasSecondaryPendingIntent + ? mSecondPendingIntent + : getPendingIntent(intent2, mSecondUser), getShortcutInfo(intent2, mSecondUser), null /* options2 */, stagePosition, splitRatio, remoteTransition, shellInstanceId); } @@ -321,11 +341,11 @@ public class SplitSelectStateController { animationRunner, 300, 150, ActivityThread.currentActivityThread().getApplicationThread()); - if (intent1 == null && intent2 == null) { + if (intent1 == null && (intent2 == null && !hasSecondaryPendingIntent)) { mSystemUiProxy.startTasksWithLegacyTransition(taskId1, options1.toBundle(), taskId2, null /* options2 */, stagePosition, splitRatio, adapter, shellInstanceId); - } else if (intent2 == null) { + } else if (intent2 == null && !hasSecondaryPendingIntent) { launchIntentOrShortcutLegacy(intent1, mInitialUser, options1, taskId2, stagePosition, splitRatio, adapter, shellInstanceId); } else if (intent1 == null) { @@ -336,7 +356,9 @@ public class SplitSelectStateController { mSystemUiProxy.startIntentsWithLegacyTransition( getPendingIntent(intent1, mInitialUser), getShortcutInfo(intent1, mInitialUser), options1.toBundle(), - getPendingIntent(intent2, mSecondUser), + hasSecondaryPendingIntent + ? mSecondPendingIntent + : getPendingIntent(intent2, mSecondUser), getShortcutInfo(intent2, mSecondUser), null /* options2 */, stagePosition, splitRatio, adapter, shellInstanceId); } @@ -374,7 +396,22 @@ public class SplitSelectStateController { } } + /** + * We treat launching by intents as grouped in two ways, + * If {@param intent} represents the first app, we always convert the intent to pending intent + * It it represents second app, either the second intent OR mSecondPendingIntent will be used + * convert second intent to a pendingIntent OR return mSecondPendingIntent as is + */ private PendingIntent getPendingIntent(Intent intent, UserHandle user) { + boolean isParamFirstIntent = intent != null && intent == mInitialTaskIntent; + if (!isParamFirstIntent && mSecondPendingIntent != null) { + // Because mSecondPendingIntent and mSecondTaskIntent can't both be set, we know we need + // to be using mSecondPendingIntent + return mSecondPendingIntent; + } + + // intent param must either be mInitialTaskIntent or mSecondTaskIntent, convert either to + // a new PendingIntent return intent == null ? null : (user != null ? PendingIntent.getActivityAsUser(mContext, 0, intent, FLAG_MUTABLE | FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT, null /* options */, user) @@ -546,6 +583,7 @@ public class SplitSelectStateController { mSplitEvent = null; mAnimateCurrentTaskDismissal = false; mDismissingFromSplitPair = false; + mSecondPendingIntent = null; } /** @@ -577,7 +615,8 @@ public class SplitSelectStateController { } private boolean isSecondTaskIntentSet() { - return (mSecondTaskId != INVALID_TASK_ID || mSecondTaskIntent != null); + return (mSecondTaskId != INVALID_TASK_ID || mSecondTaskIntent != null + || mSecondPendingIntent != null); } public void setFirstFloatingTaskView(FloatingTaskView floatingTaskView) { diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java index dd10c2da5d..148a45a386 100644 --- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java +++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java @@ -21,9 +21,15 @@ import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_WORKSP import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.PendingIntent; import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.RectF; +import android.graphics.drawable.Drawable; import android.os.UserHandle; import android.view.View; @@ -55,14 +61,38 @@ public class SplitToWorkspaceController { R.dimen.multi_window_task_divider_size) / 2; } + /** + * Handles widget selection from staged split. + * @param view Original widget view + * @param pendingIntent Provided by widget via InteractionHandler + * @return {@code true} if we can attempt launch the widget into split, {@code false} otherwise + * to allow launcher to handle the click + */ + public boolean handleSecondWidgetSelectionForSplit(View view, PendingIntent pendingIntent) { + if (shouldIgnoreSecondSplitLaunch()) { + return false; + } + + // Convert original widgetView into bitmap to use for animation + // TODO(b/276361926) get the icon for this widget via PackageManager? + int width = view.getWidth(); + int height = view.getHeight(); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + view.draw(canvas); + + mController.setSecondTask(pendingIntent); + + startWorkspaceAnimation(view, bitmap, null /*icon*/); + return true; + } + /** * 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() - && !ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) - || !mController.isSplitSelectActive()) { + if (shouldIgnoreSecondSplitLaunch()) { return false; } Object tag = view.getTag(); @@ -86,6 +116,12 @@ public class SplitToWorkspaceController { mController.setSecondTask(intent, user); + startWorkspaceAnimation(view, null /*bitmap*/, bitmapInfo.newIcon(mLauncher)); + return true; + } + + private void startWorkspaceAnimation(@NonNull View view, @Nullable Bitmap bitmap, + @Nullable Drawable icon) { boolean isTablet = mLauncher.getDeviceProfile().isTablet; SplitAnimationTimings timings = AnimUtils.getDeviceSplitToConfirmTimings(isTablet); PendingAnimation pendingAnimation = new PendingAnimation(timings.getDuration()); @@ -107,8 +143,7 @@ public class SplitToWorkspaceController { false /* fadeWithThumbnail */, true /* isStagedTask */); FloatingTaskView secondFloatingTaskView = FloatingTaskView.getFloatingTaskView(mLauncher, - view, null /* thumbnail */, bitmapInfo.newIcon(mLauncher), - secondTaskStartingBounds); + view, bitmap, icon, secondTaskStartingBounds); secondFloatingTaskView.setAlpha(1); secondFloatingTaskView.addConfirmAnimation(pendingAnimation, secondTaskStartingBounds, secondTaskEndingBounds, true /* fadeWithThumbnail */, false /* isStagedTask */); @@ -138,6 +173,11 @@ public class SplitToWorkspaceController { } }); pendingAnimation.buildAnim().start(); - return true; + } + + private boolean shouldIgnoreSecondSplitLaunch() { + return (!ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS.get() + && !ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) + || !mController.isSplitSelectActive(); } } diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt index 512df8e3d0..acfd54c4d5 100644 --- a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt +++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt @@ -18,6 +18,7 @@ package com.android.quickstep.util import android.app.ActivityManager +import android.app.PendingIntent import android.content.ComponentName import android.content.Context import android.content.Intent @@ -32,6 +33,8 @@ import com.android.launcher3.statehandlers.DepthController import com.android.launcher3.statemanager.StateManager import com.android.launcher3.util.ComponentKey import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.SplitConfigurationOptions.StagePosition +import com.android.launcher3.util.mock import com.android.launcher3.util.withArgCaptor import com.android.quickstep.RecentsModel import com.android.quickstep.SystemUiProxy @@ -59,6 +62,7 @@ class SplitSelectStateControllerTest { @Mock lateinit var handler: Handler @Mock lateinit var context: Context @Mock lateinit var recentsModel: RecentsModel + @Mock lateinit var pendingIntent: PendingIntent lateinit var splitSelectStateController: SplitSelectStateController @@ -348,6 +352,14 @@ class SplitSelectStateControllerTest { assertFalse(splitSelectStateController.isSplitSelectActive) } + @Test + fun secondPendingIntentSet() { + val itemInfo = ItemInfo() + splitSelectStateController.setInitialTaskSelect(null, 0, itemInfo, null, 1) + splitSelectStateController.setSecondTask(pendingIntent) + assertTrue(splitSelectStateController.isBothSplitAppsConfirmed) + } + // Generate GroupTask with default userId. private fun generateGroupTask( task1ComponentName: ComponentName,