From accc7f1d19e940c9c9a1f64c65b11035b87aa473 Mon Sep 17 00:00:00 2001 From: Winson Chung <winsonc@google.com> Date: Thu, 15 Feb 2024 06:51:48 +0000 Subject: [PATCH] Add basic implementation of starting an intent on an unhandled drag - Expose drags through DragAndDropController, DesktopModeController will listen for unhandled drag events and consume it to start the associated task Bug: 320797628 Test: atest WMShellUnitTests Change-Id: I9d114f58a3224299f3ccb060543533b6fbf4c9ff --- .../wm/shell/bubbles/BubbleController.java | 7 +- .../wm/shell/dagger/WMShellModule.java | 10 +-- .../desktopmode/DesktopTasksController.kt | 60 ++++++++++++++++- .../draganddrop/DragAndDropController.java | 67 +++++++++++++++++-- .../wm/shell/draganddrop/DragUtils.java | 30 +++++++++ .../shell/draganddrop/GlobalDragListener.kt | 1 + .../desktopmode/DesktopTasksControllerTest.kt | 6 ++ 7 files changed, 170 insertions(+), 11 deletions(-) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index b123c28c0019..e0f0556d03f0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -481,7 +481,12 @@ public class BubbleController implements ConfigurationChangeListener, }); mOneHandedOptional.ifPresent(this::registerOneHandedState); - mDragAndDropController.addListener(this::collapseStack); + mDragAndDropController.addListener(new DragAndDropController.DragAndDropListener() { + @Override + public void onDragStarted() { + collapseStack(); + } + }); // Clear out any persisted bubbles on disk that no longer have a valid user. List<UserInfo> users = mUserManager.getAliveUsers(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index aded1153d778..f757e1c88cb8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -498,6 +498,7 @@ public abstract class WMShellModule { ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + DragAndDropController dragAndDropController, Transitions transitions, EnterDesktopTaskTransitionHandler enterDesktopTransitionHandler, ExitDesktopTaskTransitionHandler exitDesktopTransitionHandler, @@ -506,14 +507,15 @@ public abstract class WMShellModule { @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, LaunchAdjacentController launchAdjacentController, RecentsTransitionHandler recentsTransitionHandler, + MultiInstanceHelper multiInstanceHelper, @ShellMainThread ShellExecutor mainExecutor ) { return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController, displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, - transitions, enterDesktopTransitionHandler, exitDesktopTransitionHandler, - toggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler, - desktopModeTaskRepository, launchAdjacentController, recentsTransitionHandler, - mainExecutor); + dragAndDropController, transitions, enterDesktopTransitionHandler, + exitDesktopTransitionHandler, toggleResizeDesktopTaskTransitionHandler, + dragToDesktopTransitionHandler, desktopModeTaskRepository, launchAdjacentController, + recentsTransitionHandler, multiInstanceHelper, mainExecutor); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 645ff0608608..e41b185787b2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -16,7 +16,10 @@ package com.android.wm.shell.desktopmode +import android.app.ActivityManager import android.app.ActivityManager.RunningTaskInfo +import android.app.ActivityOptions +import android.app.PendingIntent import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM @@ -25,6 +28,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.app.WindowConfiguration.WindowingMode import android.content.Context +import android.content.Intent import android.graphics.Point import android.graphics.PointF import android.graphics.Rect @@ -32,6 +36,7 @@ import android.graphics.Region import android.os.IBinder import android.os.SystemProperties import android.util.DisplayMetrics.DENSITY_DEFAULT +import android.view.Display.DEFAULT_DISPLAY import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_NONE @@ -49,6 +54,8 @@ import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.ExecutorUtils import com.android.wm.shell.common.ExternalInterfaceBinder import com.android.wm.shell.common.LaunchAdjacentController +import com.android.wm.shell.common.MultiInstanceHelper +import com.android.wm.shell.common.MultiInstanceHelper.Companion.getComponent import com.android.wm.shell.common.RemoteCallable import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SingleInstanceRemoteListener @@ -59,7 +66,9 @@ import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOT import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener +import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.recents.RecentTasksController import com.android.wm.shell.recents.RecentsTransitionHandler import com.android.wm.shell.recents.RecentsTransitionStateListener import com.android.wm.shell.splitscreen.SplitScreenController @@ -76,6 +85,7 @@ import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener import java.io.PrintWriter import java.util.concurrent.Executor import java.util.function.Consumer +import java.util.function.Function /** Handles moving tasks in and out of desktop */ class DesktopTasksController( @@ -87,6 +97,7 @@ class DesktopTasksController( private val shellTaskOrganizer: ShellTaskOrganizer, private val syncQueue: SyncTransactionQueue, private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + private val dragAndDropController: DragAndDropController, private val transitions: Transitions, private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler, private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler, @@ -96,8 +107,10 @@ class DesktopTasksController( private val desktopModeTaskRepository: DesktopModeTaskRepository, private val launchAdjacentController: LaunchAdjacentController, private val recentsTransitionHandler: RecentsTransitionHandler, + private val multiInstanceHelper: MultiInstanceHelper, @ShellMainThread private val mainExecutor: ShellExecutor -) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler { +) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, + DragAndDropController.DragAndDropListener { private val desktopMode: DesktopModeImpl private var visualIndicator: DesktopModeVisualIndicator? = null @@ -174,6 +187,7 @@ class DesktopTasksController( } } ) + dragAndDropController.addListener(this) } fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) { @@ -1023,6 +1037,50 @@ class DesktopTasksController( desktopModeTaskRepository.setExclusionRegionListener(listener, callbackExecutor) } + override fun onUnhandledDrag( + launchIntent: PendingIntent, + dragSurface: SurfaceControl, + onFinishCallback: Consumer<Boolean> + ): Boolean { + // TODO(b/320797628): Pass through which display we are dropping onto + val activeTasks = desktopModeTaskRepository.getActiveTasks(DEFAULT_DISPLAY) + if (!activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) { + // Not currently in desktop mode, ignore the drop + return false + } + + val launchComponent = getComponent(launchIntent) + if (!multiInstanceHelper.supportsMultiInstanceSplit(launchComponent)) { + // TODO(b/320797628): Should only return early if there is an existing running task, and + // notify the user as well. But for now, just ignore the drop. + KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "Dropped intent does not support multi-instance") + return false + } + + // Start a new transition to launch the app + val opts = ActivityOptions.makeBasic().apply { + launchWindowingMode = WINDOWING_MODE_FREEFORM + pendingIntentLaunchFlags = + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK + setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) + isPendingIntentBackgroundActivityLaunchAllowedByPermission = true + } + val wct = WindowContainerTransaction() + wct.sendPendingIntent(launchIntent, null, opts.toBundle()) + transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */) + + // Report that this is handled by the listener + onFinishCallback.accept(true) + + // We've assumed responsibility of cleaning up the drag surface, so do that now + // TODO(b/320797628): Do an actual animation here for the drag surface + val t = SurfaceControl.Transaction() + t.remove(dragSurface) + t.apply() + return true + } + private fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("${prefix}DesktopTasksController") diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java index 0fea3e3e219c..1afbdf90eac0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -37,6 +37,7 @@ import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DR import android.app.ActivityManager; import android.app.ActivityTaskManager; +import android.app.PendingIntent; import android.content.ClipDescription; import android.content.ComponentCallbacks2; import android.content.Context; @@ -77,6 +78,8 @@ import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.function.Consumer; +import java.util.function.Function; /** * Handles the global drag and drop handling for the Shell. @@ -103,12 +106,29 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll // Map of displayId -> per-display info private final SparseArray<PerDisplay> mDisplayDropTargets = new SparseArray<>(); + // The current display if a drag is in progress + private int mActiveDragDisplay = -1; + /** - * Listener called during drag events, currently just onDragStarted. + * Listener called during drag events. */ public interface DragAndDropListener { /** Called when a drag has started. */ - void onDragStarted(); + default void onDragStarted() {} + + /** Called when a drag has ended. */ + default void onDragEnded() {} + + /** + * Called when an unhandled drag has occurred. The impl must return true if it decides to + * handled the unhandled drag, and it must also call `onFinishCallback` to complete the + * drag. + */ + default boolean onUnhandledDrag(@NonNull PendingIntent launchIntent, + @NonNull SurfaceControl dragSurface, + @NonNull Consumer<Boolean> onFinishCallback) { + return false; + } } public DragAndDropController(Context context, @@ -180,10 +200,18 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll mListeners.remove(listener); } - private void notifyDragStarted() { + /** + * Notifies all listeners and returns whether any listener handled the callback. + */ + private boolean notifyListeners(Function<DragAndDropListener, Boolean> callback) { for (int i = 0; i < mListeners.size(); i++) { - mListeners.get(i).onDragStarted(); + boolean handled = callback.apply(mListeners.get(i)); + if (handled) { + // Return once the callback reports it has handled it + return true; + } } + return false; } @Override @@ -269,6 +297,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll } if (event.getAction() == ACTION_DRAG_STARTED) { + mActiveDragDisplay = displayId; pd.isHandlingDrag = DragUtils.canHandleDrag(event); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Clip description: handlingDrag=%b itemCount=%d mimeTypes=%s", @@ -294,7 +323,11 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll pd.dragSession.update(); pd.dragLayout.prepare(pd.dragSession, loggerSessionId); setDropTargetWindowVisibility(pd, View.VISIBLE); - notifyDragStarted(); + notifyListeners(l -> { + l.onDragStarted(); + // Return false to continue dispatch to next listener + return false; + }); break; case ACTION_DRAG_ENTERED: pd.dragLayout.show(); @@ -328,6 +361,12 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll }); } mLogger.logEnd(); + mActiveDragDisplay = -1; + notifyListeners(l -> { + l.onDragEnded(); + // Return false to continue dispatch to next listener + return false; + }); break; } return true; @@ -341,6 +380,24 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll mTransitions.startTransition(WindowManager.TRANSIT_TO_FRONT, wct, null); } + @Override + public void onUnhandledDrop(@NonNull DragEvent dragEvent, + @NonNull Consumer<Boolean> onFinishCallback) { + final PendingIntent launchIntent = DragUtils.getLaunchIntent(dragEvent); + if (launchIntent == null) { + // No intent to launch, report that this is unhandled by the listener + onFinishCallback.accept(false); + return; + } + + final boolean handled = notifyListeners( + l -> l.onUnhandledDrag(launchIntent, dragEvent.getDragSurface(), onFinishCallback)); + if (!handled) { + // Nobody handled this, we still have to notify WM + onFinishCallback.accept(false); + } + } + /** * Handles dropping on the drop target. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java index 7c0883d2538f..f7bcc9477aa1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java @@ -20,9 +20,14 @@ import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY; import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; +import android.app.PendingIntent; +import android.content.ClipData; import android.content.ClipDescription; import android.view.DragEvent; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + /** Collection of utility classes for handling drag and drop. */ public class DragUtils { private static final String TAG = "DragUtils"; @@ -44,6 +49,31 @@ public class DragUtils { || description.hasMimeType(MIMETYPE_APPLICATION_TASK); } + /** + * Returns a launchable intent in the given `DragEvent` or `null` if there is none. + */ + @Nullable + public static PendingIntent getLaunchIntent(@NonNull DragEvent dragEvent) { + return getLaunchIntent(dragEvent.getClipData()); + } + + /** + * Returns a launchable intent in the given `ClipData` or `null` if there is none. + */ + @Nullable + public static PendingIntent getLaunchIntent(@NonNull ClipData data) { + for (int i = 0; i < data.getItemCount(); i++) { + final ClipData.Item item = data.getItemAt(i); + if (item.getIntentSender() != null) { + final PendingIntent intent = new PendingIntent(item.getIntentSender().getTarget()); + if (intent != null && intent.isActivity()) { + return intent; + } + } + } + return null; + } + /** * Returns a list of the mime types provided in the clip description. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt index 7f7cfb964e7d..8826141fb406 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt @@ -30,6 +30,7 @@ import java.util.function.Consumer /** * Manages the listener and callbacks for unhandled global drags. + * This is only used by DragAndDropController and should not be used directly by other classes. */ class GlobalDragListener( private val wmService: IWindowManager, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index cb64c52444ac..383621beca22 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -48,12 +48,14 @@ import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.LaunchAdjacentController +import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createHomeTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createSplitScreenTask +import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.recents.RecentsTransitionHandler import com.android.wm.shell.recents.RecentsTransitionStateListener import com.android.wm.shell.splitscreen.SplitScreenController @@ -106,6 +108,8 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock lateinit var desktopModeWindowDecoration: DesktopModeWindowDecoration @Mock lateinit var splitScreenController: SplitScreenController @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler + @Mock lateinit var dragAndDropController: DragAndDropController + @Mock lateinit var multiInstanceHelper: MultiInstanceHelper private lateinit var mockitoSession: StaticMockitoSession private lateinit var controller: DesktopTasksController @@ -148,6 +152,7 @@ class DesktopTasksControllerTest : ShellTestCase() { shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, + dragAndDropController, transitions, enterDesktopTransitionHandler, exitDesktopTransitionHandler, @@ -156,6 +161,7 @@ class DesktopTasksControllerTest : ShellTestCase() { desktopModeTaskRepository, launchAdjacentController, recentsTransitionHandler, + multiInstanceHelper, shellExecutor ) } -- GitLab