From 23fbdffd90ad2e94316a07b74139540fac4e3da2 Mon Sep 17 00:00:00 2001 From: Vinit Nayak Date: Fri, 6 Sep 2024 17:19:40 -0700 Subject: Add interfaces to abstract out Shell DragAndDrop behavior * No logical changes. Only non interface change is now the rootView in DragAndDropController is passed to drag layout to add what it needs instead of assuming that DragLayout is both the controller and the view Bug: 349828130 Test: Works same w/ flag on or off Flag: com.android.wm.shell.enable_flexible_split Change-Id: Iae870dcc56f9e4fe2c7b8224b1eb1491279216c1 --- .../shell/draganddrop/DragAndDropController.java | 12 +- .../wm/shell/draganddrop/DragAndDropPolicy.java | 457 -------------------- .../android/wm/shell/draganddrop/DragLayout.java | 36 +- .../wm/shell/draganddrop/DragLayoutProvider.kt | 80 ++++ .../com/android/wm/shell/draganddrop/DropTarget.kt | 56 +++ .../wm/shell/draganddrop/SplitDragPolicy.java | 460 +++++++++++++++++++++ .../shell/splitscreen/SplitScreenController.java | 4 +- .../shell/draganddrop/DragAndDropPolicyTest.java | 386 ----------------- .../wm/shell/draganddrop/SplitDragPolicyTest.java | 386 +++++++++++++++++ 9 files changed, 1011 insertions(+), 866 deletions(-) delete mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayoutProvider.kt create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropTarget.kt create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java delete mode 100644 libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java create mode 100644 libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java 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 cf02fb5fde8e..22e8dc186e9b 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 @@ -26,7 +26,6 @@ import static android.view.DragEvent.ACTION_DROP; import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; -import static android.view.WindowManager.LayoutParams.MATCH_PARENT; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; @@ -247,9 +246,8 @@ public class DragAndDropController implements RemoteCallable mTargets = new ArrayList<>(); - private final RectF mDisallowHitRegion = new RectF(); - - private InstanceId mLoggerSessionId; - private DragSession mSession; - - public DragAndDropPolicy(Context context, SplitScreenController splitScreen) { - this(context, splitScreen, new DefaultStarter(context)); - } - - @VisibleForTesting - DragAndDropPolicy(Context context, SplitScreenController splitScreen, - Starter fullscreenStarter) { - mContext = context; - mSplitScreen = splitScreen; - mFullscreenStarter = fullscreenStarter; - mSplitscreenStarter = splitScreen; - } - - /** - * Starts a new drag session with the given initial drag data. - */ - void start(DragSession session, InstanceId loggerSessionId) { - mLoggerSessionId = loggerSessionId; - mSession = session; - RectF disallowHitRegion = mSession.appData != null - ? (RectF) mSession.appData.getExtra(EXTRA_DISALLOW_HIT_REGION) - : null; - if (disallowHitRegion == null) { - mDisallowHitRegion.setEmpty(); - } else { - mDisallowHitRegion.set(disallowHitRegion); - } - } - - /** - * Returns the number of targets. - */ - int getNumTargets() { - return mTargets.size(); - } - - /** - * Returns the target's regions based on the current state of the device and display. - */ - @NonNull - ArrayList getTargets(Insets insets) { - mTargets.clear(); - if (mSession == null) { - // Return early if this isn't an app drag - return mTargets; - } - - final int w = mSession.displayLayout.width(); - final int h = mSession.displayLayout.height(); - final int iw = w - insets.left - insets.right; - final int ih = h - insets.top - insets.bottom; - final int l = insets.left; - final int t = insets.top; - final Rect displayRegion = new Rect(l, t, l + iw, t + ih); - final Rect fullscreenDrawRegion = new Rect(displayRegion); - final Rect fullscreenHitRegion = new Rect(displayRegion); - final boolean isLeftRightSplit = mSplitScreen != null && mSplitScreen.isLeftRightSplit(); - final boolean inSplitScreen = mSplitScreen != null && mSplitScreen.isSplitScreenVisible(); - final float dividerWidth = mContext.getResources().getDimensionPixelSize( - R.dimen.split_divider_bar_width); - // We allow splitting if we are already in split-screen or the running task is a standard - // task in fullscreen mode. - final boolean allowSplit = inSplitScreen - || (mSession.runningTaskActType == ACTIVITY_TYPE_STANDARD - && mSession.runningTaskWinMode == WINDOWING_MODE_FULLSCREEN); - if (allowSplit) { - // Already split, allow replacing existing split task - final Rect topOrLeftBounds = new Rect(); - final Rect bottomOrRightBounds = new Rect(); - mSplitScreen.getStageBounds(topOrLeftBounds, bottomOrRightBounds); - topOrLeftBounds.intersect(displayRegion); - bottomOrRightBounds.intersect(displayRegion); - - if (isLeftRightSplit) { - final Rect leftHitRegion = new Rect(); - final Rect rightHitRegion = new Rect(); - - // If we have existing split regions use those bounds, otherwise split it 50/50 - if (inSplitScreen) { - // The bounds of the existing split will have a divider bar, the hit region - // should include that space. Find the center of the divider bar: - float centerX = topOrLeftBounds.right + (dividerWidth / 2); - // Now set the hit regions using that center. - leftHitRegion.set(displayRegion); - leftHitRegion.right = (int) centerX; - rightHitRegion.set(displayRegion); - rightHitRegion.left = (int) centerX; - } else { - displayRegion.splitVertically(leftHitRegion, rightHitRegion); - } - - mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, topOrLeftBounds)); - mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, bottomOrRightBounds)); - - } else { - final Rect topHitRegion = new Rect(); - final Rect bottomHitRegion = new Rect(); - - // If we have existing split regions use those bounds, otherwise split it 50/50 - if (inSplitScreen) { - // The bounds of the existing split will have a divider bar, the hit region - // should include that space. Find the center of the divider bar: - float centerX = topOrLeftBounds.bottom + (dividerWidth / 2); - // Now set the hit regions using that center. - topHitRegion.set(displayRegion); - topHitRegion.bottom = (int) centerX; - bottomHitRegion.set(displayRegion); - bottomHitRegion.top = (int) centerX; - } else { - displayRegion.splitHorizontally(topHitRegion, bottomHitRegion); - } - - mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topOrLeftBounds)); - mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomOrRightBounds)); - } - } else { - // Split-screen not allowed, so only show the fullscreen target - mTargets.add(new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion)); - } - return mTargets; - } - - /** - * Returns the target at the given position based on the targets previously calculated. - */ - @Nullable - Target getTargetAtLocation(int x, int y) { - if (mDisallowHitRegion.contains(x, y)) { - return null; - } - for (int i = mTargets.size() - 1; i >= 0; i--) { - DragAndDropPolicy.Target t = mTargets.get(i); - if (t.hitRegion.contains(x, y)) { - return t; - } - } - return null; - } - - /** - * Handles the drop on a given {@param target}. If a {@param hideTaskToken} is set, then the - * handling of the drop will attempt to hide the given task as a part of the same window - * container transaction if possible. - */ - @VisibleForTesting - void handleDrop(Target target, @Nullable WindowContainerToken hideTaskToken) { - if (target == null || !mTargets.contains(target)) { - return; - } - - final boolean leftOrTop = target.type == TYPE_SPLIT_TOP || target.type == TYPE_SPLIT_LEFT; - - @SplitPosition int position = SPLIT_POSITION_UNDEFINED; - if (target.type != TYPE_FULLSCREEN && mSplitScreen != null) { - // Update launch options for the split side we are targeting. - position = leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT; - // Add some data for logging splitscreen once it is invoked - mSplitScreen.onDroppedToSplit(position, mLoggerSessionId); - } - - final Starter starter = target.type == TYPE_FULLSCREEN - ? mFullscreenStarter - : mSplitscreenStarter; - if (mSession.appData != null) { - launchApp(mSession, starter, position, hideTaskToken); - } else { - launchIntent(mSession, starter, position, hideTaskToken); - } - } - - /** - * Launches an app provided by SysUI. - */ - private void launchApp(DragSession session, Starter starter, @SplitPosition int position, - @Nullable WindowContainerToken hideTaskToken) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Launching app data at position=%d", - position); - final ClipDescription description = session.getClipDescription(); - final boolean isTask = description.hasMimeType(MIMETYPE_APPLICATION_TASK); - final boolean isShortcut = description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT); - final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic(); - baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true); - // Put BAL flags to avoid activity start aborted. - baseActivityOpts.setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); - final Bundle opts = baseActivityOpts.toBundle(); - if (session.appData.hasExtra(EXTRA_ACTIVITY_OPTIONS)) { - opts.putAll(session.appData.getBundleExtra(EXTRA_ACTIVITY_OPTIONS)); - } - final UserHandle user = session.appData.getParcelableExtra(EXTRA_USER); - - if (isTask) { - final int taskId = session.appData.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID); - starter.startTask(taskId, position, opts, hideTaskToken); - } else if (isShortcut) { - if (hideTaskToken != null) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, - "Can not hide task token with starting shortcut"); - } - final String packageName = session.appData.getStringExtra(EXTRA_PACKAGE_NAME); - final String id = session.appData.getStringExtra(EXTRA_SHORTCUT_ID); - starter.startShortcut(packageName, id, position, opts, user); - } else { - final PendingIntent launchIntent = - session.appData.getParcelableExtra(EXTRA_PENDING_INTENT); - if (Build.IS_DEBUGGABLE) { - if (!user.equals(launchIntent.getCreatorUserHandle())) { - Log.e(TAG, "Expected app intent's EXTRA_USER to match pending intent user"); - } - } - starter.startIntent(launchIntent, user.getIdentifier(), null /* fillIntent */, - position, opts, hideTaskToken); - } - } - - /** - * Launches an intent sender provided by an application. - */ - private void launchIntent(DragSession session, Starter starter, @SplitPosition int position, - @Nullable WindowContainerToken hideTaskToken) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Launching intent at position=%d", - position); - final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic(); - baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true); - baseActivityOpts.setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_DENIED); - // TODO(b/255649902): Rework this so that SplitScreenController can always use the options - // instead of a fillInIntent since it's assuming that the PendingIntent is mutable - baseActivityOpts.setPendingIntentLaunchFlags(FLAG_ACTIVITY_NEW_TASK - | FLAG_ACTIVITY_MULTIPLE_TASK); - - final Bundle opts = baseActivityOpts.toBundle(); - starter.startIntent(session.launchableIntent, - session.launchableIntent.getCreatorUserHandle().getIdentifier(), - null /* fillIntent */, position, opts, hideTaskToken); - } - - /** - * Interface for actually committing the task launches. - */ - public interface Starter { - void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options, - @Nullable WindowContainerToken hideTaskToken); - void startShortcut(String packageName, String shortcutId, @SplitPosition int position, - @Nullable Bundle options, UserHandle user); - void startIntent(PendingIntent intent, int userId, Intent fillInIntent, - @SplitPosition int position, @Nullable Bundle options, - @Nullable WindowContainerToken hideTaskToken); - void enterSplitScreen(int taskId, boolean leftOrTop); - - /** - * Exits splitscreen, with an associated exit trigger from the SplitscreenUIChanged proto - * for logging. - */ - void exitSplitScreen(int toTopTaskId, int exitTrigger); - } - - /** - * Default implementation of the starter which calls through the system services to launch the - * tasks. - */ - private static class DefaultStarter implements Starter { - private final Context mContext; - - public DefaultStarter(Context context) { - mContext = context; - } - - @Override - public void startTask(int taskId, int position, @Nullable Bundle options, - @Nullable WindowContainerToken hideTaskToken) { - if (hideTaskToken != null) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, - "Default starter does not support hide task token"); - } - try { - ActivityTaskManager.getService().startActivityFromRecents(taskId, options); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to launch task", e); - } - } - - @Override - public void startShortcut(String packageName, String shortcutId, int position, - @Nullable Bundle options, UserHandle user) { - try { - LauncherApps launcherApps = - mContext.getSystemService(LauncherApps.class); - launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */, - options, user); - } catch (ActivityNotFoundException e) { - Slog.e(TAG, "Failed to launch shortcut", e); - } - } - - @Override - public void startIntent(PendingIntent intent, int userId, @Nullable Intent fillInIntent, - int position, @Nullable Bundle options, - @Nullable WindowContainerToken hideTaskToken) { - if (hideTaskToken != null) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, - "Default starter does not support hide task token"); - } - try { - intent.send(mContext, 0, fillInIntent, null, null, null, options); - } catch (PendingIntent.CanceledException e) { - Slog.e(TAG, "Failed to launch activity", e); - } - } - - @Override - public void enterSplitScreen(int taskId, boolean leftOrTop) { - throw new UnsupportedOperationException("enterSplitScreen not implemented by starter"); - } - - @Override - public void exitSplitScreen(int toTopTaskId, int exitTrigger) { - throw new UnsupportedOperationException("exitSplitScreen not implemented by starter"); - } - } - - /** - * Represents a drop target. - */ - static class Target { - static final int TYPE_FULLSCREEN = 0; - static final int TYPE_SPLIT_LEFT = 1; - static final int TYPE_SPLIT_TOP = 2; - static final int TYPE_SPLIT_RIGHT = 3; - static final int TYPE_SPLIT_BOTTOM = 4; - @IntDef(value = { - TYPE_FULLSCREEN, - TYPE_SPLIT_LEFT, - TYPE_SPLIT_TOP, - TYPE_SPLIT_RIGHT, - TYPE_SPLIT_BOTTOM - }) - @Retention(RetentionPolicy.SOURCE) - @interface Type{} - - final @Type int type; - - // The actual hit region for this region - final Rect hitRegion; - // The approximate visual region for where the task will start - final Rect drawRegion; - - public Target(@Type int t, Rect hit, Rect draw) { - type = t; - hitRegion = hit; - drawRegion = draw; - } - - @Override - public String toString() { - return "Target {type=" + type + " hit=" + hitRegion + " draw=" + drawRegion + "}"; - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java index 3fecbe7fff74..dfa24370590a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java @@ -23,10 +23,10 @@ import static android.content.pm.ActivityInfo.CONFIG_UI_MODE; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_BOTTOM; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_LEFT; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_RIGHT; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_TOP; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; @@ -47,6 +47,7 @@ import android.graphics.Region; import android.graphics.drawable.Drawable; import android.view.DragEvent; import android.view.SurfaceControl; +import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.WindowInsets.Type; @@ -66,13 +67,13 @@ import com.android.wm.shell.shared.animation.Interpolators; import com.android.wm.shell.splitscreen.SplitScreenController; import java.io.PrintWriter; -import java.util.ArrayList; +import java.util.List; /** * Coordinates the visible drop targets for the current drag within a single display. */ public class DragLayout extends LinearLayout - implements ViewTreeObserver.OnComputeInternalInsetsListener { + implements ViewTreeObserver.OnComputeInternalInsetsListener, DragLayoutProvider { // While dragging the status bar is hidden. private static final int HIDE_STATUS_BAR_FLAGS = StatusBarManager.DISABLE_NOTIFICATION_ICONS @@ -80,7 +81,7 @@ public class DragLayout extends LinearLayout | StatusBarManager.DISABLE_CLOCK | StatusBarManager.DISABLE_SYSTEM_INFO; - private final DragAndDropPolicy mPolicy; + private final DropTarget mPolicy; private final SplitScreenController mSplitScreenController; private final IconProvider mIconProvider; private final StatusBarManager mStatusBarManager; @@ -91,7 +92,7 @@ public class DragLayout extends LinearLayout // Whether the device is currently in left/right split mode private boolean mIsLeftRightSplit; - private DragAndDropPolicy.Target mCurrentTarget = null; + private SplitDragPolicy.Target mCurrentTarget = null; private DropZoneView mDropZoneView1; private DropZoneView mDropZoneView2; @@ -113,7 +114,7 @@ public class DragLayout extends LinearLayout super(context); mSplitScreenController = splitScreenController; mIconProvider = iconProvider; - mPolicy = new DragAndDropPolicy(context, splitScreenController); + mPolicy = new SplitDragPolicy(context, splitScreenController); mStatusBarManager = context.getSystemService(StatusBarManager.class); mLastConfiguration.setTo(context.getResources().getConfiguration()); @@ -387,6 +388,13 @@ public class DragLayout extends LinearLayout recomputeDropTargets(); } + @NonNull + @Override + public void addDraggingView(ViewGroup rootView) { + // TODO(b/349828130) We need to separate out view + logic here + rootView.addView(this, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + } + /** * Recalculates the drop targets based on the current policy. */ @@ -394,9 +402,9 @@ public class DragLayout extends LinearLayout if (!mIsShowing) { return; } - final ArrayList targets = mPolicy.getTargets(mInsets); + final List targets = mPolicy.getTargets(mInsets); for (int i = 0; i < targets.size(); i++) { - final DragAndDropPolicy.Target target = targets.get(i); + final SplitDragPolicy.Target target = targets.get(i); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Add target: %s", target); // Inset the draw region by a little bit target.drawRegion.inset(mDisplayMargin, mDisplayMargin); @@ -419,7 +427,7 @@ public class DragLayout extends LinearLayout } // Find containing region, if the same as mCurrentRegion, then skip, otherwise, animate the // visibility of the current region - DragAndDropPolicy.Target target = mPolicy.getTargetAtLocation(x, y); + SplitDragPolicy.Target target = mPolicy.getTargetAtLocation(x, y); if (mCurrentTarget != target) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Current target: %s", target); if (target == null) { @@ -493,7 +501,7 @@ public class DragLayout extends LinearLayout mHasDropped = true; // Process the drop - mPolicy.handleDrop(mCurrentTarget, hideTaskToken); + mPolicy.onDropped(mCurrentTarget, hideTaskToken); // Start animating the drop UI out with the drag surface hide(event, dropCompleteCallback); @@ -576,7 +584,7 @@ public class DragLayout extends LinearLayout } } - private void animateHighlight(DragAndDropPolicy.Target target) { + private void animateHighlight(SplitDragPolicy.Target target) { if (target.type == TYPE_SPLIT_LEFT || target.type == TYPE_SPLIT_TOP) { mDropZoneView1.setShowingHighlight(true); mDropZoneView2.setShowingHighlight(false); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayoutProvider.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayoutProvider.kt new file mode 100644 index 000000000000..3d408242f5f8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayoutProvider.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 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.wm.shell.draganddrop + +import android.content.res.Configuration +import android.view.DragEvent +import android.view.SurfaceControl +import android.view.ViewGroup +import android.window.WindowContainerToken +import com.android.internal.logging.InstanceId +import java.io.PrintWriter + +/** Interface to be implemented by any controllers providing a layout for DragAndDrop in Shell */ +interface DragLayoutProvider { + /** + * Updates the drag layout based on the given drag session. + */ + fun updateSession(session: DragSession) + /** + * Called when a new drag is started. + */ + fun prepare(session: DragSession, loggerSessionId: InstanceId?) + + /** + * Shows the drag layout. + */ + fun show() + + /** + * Updates the visible drop target as the user drags. + */ + fun update(event: DragEvent?) + + /** + * Hides the drag layout and animates out the visible drop targets. + */ + fun hide(event: DragEvent?, hideCompleteCallback: Runnable?) + + /** + * Whether target has already been dropped or not + */ + fun hasDropped(): Boolean + + /** + * Handles the drop onto a target and animates out the visible drop targets. + */ + fun drop( + event: DragEvent?, dragSurface: SurfaceControl, + hideTaskToken: WindowContainerToken?, dropCompleteCallback: Runnable? + ): Boolean + + /** + * Dumps information about this drag layout. + */ + fun dump(pw: PrintWriter, prefix: String?) + + /** + * @return a View which will be added to the global root view for drag and drop + */ + fun addDraggingView(viewGroup: ViewGroup) + + /** + * Called when the configuration changes. + */ + fun onConfigChanged(newConfig: Configuration?) +} \ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropTarget.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropTarget.kt new file mode 100644 index 000000000000..122a105dbf8d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropTarget.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 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.wm.shell.draganddrop + +import android.graphics.Insets +import android.window.WindowContainerToken +import com.android.internal.logging.InstanceId + +/** + * Interface to be implemented by classes which want to provide drop targets + * for DragAndDrop in Shell + */ +interface DropTarget { + // TODO(b/349828130) Delete after flexible split launches + /** + * Called at the start of a Drag, before input events are processed. + */ + fun start(dragSession: DragSession, logSessionId: InstanceId) + /** + * @return [SplitDragPolicy.Target] corresponding to the given coords in display bounds. + */ + fun getTargetAtLocation(x: Int, y: Int) : SplitDragPolicy.Target + /** + * @return total number of drop targets for the current drag session. + */ + fun getNumTargets() : Int + // TODO(b/349828130) + + /** + * @return [List] to show for the current drag session. + */ + fun getTargets(insets: Insets) : List + /** + * Called when user is hovering Drag object over the given Target + */ + fun onHoveringOver(target: SplitDragPolicy.Target) {} + /** + * Called when the user has dropped the provided target (need not be the same target as + * [onHoveringOver]) + */ + fun onDropped(target: SplitDragPolicy.Target, hideTaskToken: WindowContainerToken) +} \ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java new file mode 100644 index 000000000000..2a19d6512b56 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java @@ -0,0 +1,460 @@ +/* + * Copyright (C) 2020 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.wm.shell.draganddrop; + +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.ClipDescription.EXTRA_ACTIVITY_OPTIONS; +import static android.content.ClipDescription.EXTRA_PENDING_INTENT; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; +import static android.content.Intent.EXTRA_PACKAGE_NAME; +import static android.content.Intent.EXTRA_SHORTCUT_ID; +import static android.content.Intent.EXTRA_TASK_ID; +import static android.content.Intent.EXTRA_USER; +import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; + +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_FULLSCREEN; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_BOTTOM; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_LEFT; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_RIGHT; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_TOP; +import static com.android.wm.shell.shared.draganddrop.DragAndDropConstants.EXTRA_DISALLOW_HIT_REGION; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; + +import android.app.ActivityOptions; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.content.ActivityNotFoundException; +import android.content.ClipDescription; +import android.content.Context; +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.graphics.Insets; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Build; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Log; +import android.util.Slog; +import android.window.WindowContainerToken; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.internal.logging.InstanceId; +import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; +import com.android.wm.shell.splitscreen.SplitScreenController; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; + +/** + * The policy for handling drag and drop operations to shell. + */ +public class SplitDragPolicy implements DropTarget { + + private static final String TAG = SplitDragPolicy.class.getSimpleName(); + + private final Context mContext; + // Used only for launching a fullscreen task (or as a fallback if there is no split starter) + private final Starter mFullscreenStarter; + // Used for launching tasks into splitscreen + private final Starter mSplitscreenStarter; + private final SplitScreenController mSplitScreen; + private final ArrayList mTargets = new ArrayList<>(); + private final RectF mDisallowHitRegion = new RectF(); + + private InstanceId mLoggerSessionId; + private DragSession mSession; + + public SplitDragPolicy(Context context, SplitScreenController splitScreen) { + this(context, splitScreen, new DefaultStarter(context)); + } + + @VisibleForTesting + SplitDragPolicy(Context context, SplitScreenController splitScreen, + Starter fullscreenStarter) { + mContext = context; + mSplitScreen = splitScreen; + mFullscreenStarter = fullscreenStarter; + mSplitscreenStarter = splitScreen; + } + + /** + * Starts a new drag session with the given initial drag data. + */ + public void start(DragSession session, InstanceId loggerSessionId) { + mLoggerSessionId = loggerSessionId; + mSession = session; + RectF disallowHitRegion = mSession.appData != null + ? (RectF) mSession.appData.getExtra(EXTRA_DISALLOW_HIT_REGION) + : null; + if (disallowHitRegion == null) { + mDisallowHitRegion.setEmpty(); + } else { + mDisallowHitRegion.set(disallowHitRegion); + } + } + + /** + * Returns the number of targets. + */ + @Override + public int getNumTargets() { + return mTargets.size(); + } + + /** + * Returns the target's regions based on the current state of the device and display. + */ + @NonNull + @Override + public ArrayList getTargets(@NonNull Insets insets) { + mTargets.clear(); + if (mSession == null) { + // Return early if this isn't an app drag + return mTargets; + } + + final int w = mSession.displayLayout.width(); + final int h = mSession.displayLayout.height(); + final int iw = w - insets.left - insets.right; + final int ih = h - insets.top - insets.bottom; + final int l = insets.left; + final int t = insets.top; + final Rect displayRegion = new Rect(l, t, l + iw, t + ih); + final Rect fullscreenDrawRegion = new Rect(displayRegion); + final Rect fullscreenHitRegion = new Rect(displayRegion); + final boolean isLeftRightSplit = mSplitScreen != null && mSplitScreen.isLeftRightSplit(); + final boolean inSplitScreen = mSplitScreen != null && mSplitScreen.isSplitScreenVisible(); + final float dividerWidth = mContext.getResources().getDimensionPixelSize( + R.dimen.split_divider_bar_width); + // We allow splitting if we are already in split-screen or the running task is a standard + // task in fullscreen mode. + final boolean allowSplit = inSplitScreen + || (mSession.runningTaskActType == ACTIVITY_TYPE_STANDARD + && mSession.runningTaskWinMode == WINDOWING_MODE_FULLSCREEN); + if (allowSplit) { + // Already split, allow replacing existing split task + final Rect topOrLeftBounds = new Rect(); + final Rect bottomOrRightBounds = new Rect(); + mSplitScreen.getStageBounds(topOrLeftBounds, bottomOrRightBounds); + topOrLeftBounds.intersect(displayRegion); + bottomOrRightBounds.intersect(displayRegion); + + if (isLeftRightSplit) { + final Rect leftHitRegion = new Rect(); + final Rect rightHitRegion = new Rect(); + + // If we have existing split regions use those bounds, otherwise split it 50/50 + if (inSplitScreen) { + // The bounds of the existing split will have a divider bar, the hit region + // should include that space. Find the center of the divider bar: + float centerX = topOrLeftBounds.right + (dividerWidth / 2); + // Now set the hit regions using that center. + leftHitRegion.set(displayRegion); + leftHitRegion.right = (int) centerX; + rightHitRegion.set(displayRegion); + rightHitRegion.left = (int) centerX; + } else { + displayRegion.splitVertically(leftHitRegion, rightHitRegion); + } + + mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, topOrLeftBounds)); + mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, bottomOrRightBounds)); + + } else { + final Rect topHitRegion = new Rect(); + final Rect bottomHitRegion = new Rect(); + + // If we have existing split regions use those bounds, otherwise split it 50/50 + if (inSplitScreen) { + // The bounds of the existing split will have a divider bar, the hit region + // should include that space. Find the center of the divider bar: + float centerX = topOrLeftBounds.bottom + (dividerWidth / 2); + // Now set the hit regions using that center. + topHitRegion.set(displayRegion); + topHitRegion.bottom = (int) centerX; + bottomHitRegion.set(displayRegion); + bottomHitRegion.top = (int) centerX; + } else { + displayRegion.splitHorizontally(topHitRegion, bottomHitRegion); + } + + mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topOrLeftBounds)); + mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomOrRightBounds)); + } + } else { + // Split-screen not allowed, so only show the fullscreen target + mTargets.add(new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion)); + } + return mTargets; + } + + /** + * Returns the target at the given position based on the targets previously calculated. + */ + @Nullable + public Target getTargetAtLocation(int x, int y) { + if (mDisallowHitRegion.contains(x, y)) { + return null; + } + for (int i = mTargets.size() - 1; i >= 0; i--) { + SplitDragPolicy.Target t = mTargets.get(i); + if (t.hitRegion.contains(x, y)) { + return t; + } + } + return null; + } + + /** + * Handles the drop on a given {@param target}. If a {@param hideTaskToken} is set, then the + * handling of the drop will attempt to hide the given task as a part of the same window + * container transaction if possible. + */ + @VisibleForTesting + public void onDropped(Target target, @Nullable WindowContainerToken hideTaskToken) { + if (target == null || !mTargets.contains(target)) { + return; + } + + final boolean leftOrTop = target.type == TYPE_SPLIT_TOP || target.type == TYPE_SPLIT_LEFT; + + @SplitPosition int position = SPLIT_POSITION_UNDEFINED; + if (target.type != TYPE_FULLSCREEN && mSplitScreen != null) { + // Update launch options for the split side we are targeting. + position = leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT; + // Add some data for logging splitscreen once it is invoked + mSplitScreen.onDroppedToSplit(position, mLoggerSessionId); + } + + final Starter starter = target.type == TYPE_FULLSCREEN + ? mFullscreenStarter + : mSplitscreenStarter; + if (mSession.appData != null) { + launchApp(mSession, starter, position, hideTaskToken); + } else { + launchIntent(mSession, starter, position, hideTaskToken); + } + } + + /** + * Launches an app provided by SysUI. + */ + private void launchApp(DragSession session, Starter starter, @SplitPosition int position, + @Nullable WindowContainerToken hideTaskToken) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Launching app data at position=%d", + position); + final ClipDescription description = session.getClipDescription(); + final boolean isTask = description.hasMimeType(MIMETYPE_APPLICATION_TASK); + final boolean isShortcut = description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT); + final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic(); + baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true); + // Put BAL flags to avoid activity start aborted. + baseActivityOpts.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); + final Bundle opts = baseActivityOpts.toBundle(); + if (session.appData.hasExtra(EXTRA_ACTIVITY_OPTIONS)) { + opts.putAll(session.appData.getBundleExtra(EXTRA_ACTIVITY_OPTIONS)); + } + final UserHandle user = session.appData.getParcelableExtra(EXTRA_USER); + + if (isTask) { + final int taskId = session.appData.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID); + starter.startTask(taskId, position, opts, hideTaskToken); + } else if (isShortcut) { + if (hideTaskToken != null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Can not hide task token with starting shortcut"); + } + final String packageName = session.appData.getStringExtra(EXTRA_PACKAGE_NAME); + final String id = session.appData.getStringExtra(EXTRA_SHORTCUT_ID); + starter.startShortcut(packageName, id, position, opts, user); + } else { + final PendingIntent launchIntent = + session.appData.getParcelableExtra(EXTRA_PENDING_INTENT); + if (Build.IS_DEBUGGABLE) { + if (!user.equals(launchIntent.getCreatorUserHandle())) { + Log.e(TAG, "Expected app intent's EXTRA_USER to match pending intent user"); + } + } + starter.startIntent(launchIntent, user.getIdentifier(), null /* fillIntent */, + position, opts, hideTaskToken); + } + } + + /** + * Launches an intent sender provided by an application. + */ + private void launchIntent(DragSession session, Starter starter, @SplitPosition int position, + @Nullable WindowContainerToken hideTaskToken) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Launching intent at position=%d", + position); + final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic(); + baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true); + baseActivityOpts.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_DENIED); + // TODO(b/255649902): Rework this so that SplitScreenController can always use the options + // instead of a fillInIntent since it's assuming that the PendingIntent is mutable + baseActivityOpts.setPendingIntentLaunchFlags(FLAG_ACTIVITY_NEW_TASK + | FLAG_ACTIVITY_MULTIPLE_TASK); + + final Bundle opts = baseActivityOpts.toBundle(); + starter.startIntent(session.launchableIntent, + session.launchableIntent.getCreatorUserHandle().getIdentifier(), + null /* fillIntent */, position, opts, hideTaskToken); + } + + /** + * Interface for actually committing the task launches. + */ + public interface Starter { + void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options, + @Nullable WindowContainerToken hideTaskToken); + void startShortcut(String packageName, String shortcutId, @SplitPosition int position, + @Nullable Bundle options, UserHandle user); + void startIntent(PendingIntent intent, int userId, Intent fillInIntent, + @SplitPosition int position, @Nullable Bundle options, + @Nullable WindowContainerToken hideTaskToken); + void enterSplitScreen(int taskId, boolean leftOrTop); + + /** + * Exits splitscreen, with an associated exit trigger from the SplitscreenUIChanged proto + * for logging. + */ + void exitSplitScreen(int toTopTaskId, int exitTrigger); + } + + /** + * Default implementation of the starter which calls through the system services to launch the + * tasks. + */ + private static class DefaultStarter implements Starter { + private final Context mContext; + + public DefaultStarter(Context context) { + mContext = context; + } + + @Override + public void startTask(int taskId, int position, @Nullable Bundle options, + @Nullable WindowContainerToken hideTaskToken) { + if (hideTaskToken != null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Default starter does not support hide task token"); + } + try { + ActivityTaskManager.getService().startActivityFromRecents(taskId, options); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to launch task", e); + } + } + + @Override + public void startShortcut(String packageName, String shortcutId, int position, + @Nullable Bundle options, UserHandle user) { + try { + LauncherApps launcherApps = + mContext.getSystemService(LauncherApps.class); + launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */, + options, user); + } catch (ActivityNotFoundException e) { + Slog.e(TAG, "Failed to launch shortcut", e); + } + } + + @Override + public void startIntent(PendingIntent intent, int userId, @Nullable Intent fillInIntent, + int position, @Nullable Bundle options, + @Nullable WindowContainerToken hideTaskToken) { + if (hideTaskToken != null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Default starter does not support hide task token"); + } + try { + intent.send(mContext, 0, fillInIntent, null, null, null, options); + } catch (PendingIntent.CanceledException e) { + Slog.e(TAG, "Failed to launch activity", e); + } + } + + @Override + public void enterSplitScreen(int taskId, boolean leftOrTop) { + throw new UnsupportedOperationException("enterSplitScreen not implemented by starter"); + } + + @Override + public void exitSplitScreen(int toTopTaskId, int exitTrigger) { + throw new UnsupportedOperationException("exitSplitScreen not implemented by starter"); + } + } + + /** + * Represents a drop target. + * TODO(b/349828130): Move this into {@link DropTarget} + */ + public static class Target { + static final int TYPE_FULLSCREEN = 0; + static final int TYPE_SPLIT_LEFT = 1; + static final int TYPE_SPLIT_TOP = 2; + static final int TYPE_SPLIT_RIGHT = 3; + static final int TYPE_SPLIT_BOTTOM = 4; + @IntDef(value = { + TYPE_FULLSCREEN, + TYPE_SPLIT_LEFT, + TYPE_SPLIT_TOP, + TYPE_SPLIT_RIGHT, + TYPE_SPLIT_BOTTOM + }) + @Retention(RetentionPolicy.SOURCE) + @interface Type{} + + final @Type int type; + + // The actual hit region for this region + final Rect hitRegion; + // The approximate visual region for where the task will start + final Rect drawRegion; + + public Target(@Type int t, Rect hit, Rect draw) { + type = t; + hitRegion = hit; + drawRegion = draw; + } + + @Override + public String toString() { + return "Target {type=" + type + " hit=" + hitRegion + " draw=" + drawRegion + "}"; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 793e2aa757a3..87b661d340ed 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -92,7 +92,7 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.split.SplitScreenUtils; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.draganddrop.DragAndDropController; -import com.android.wm.shell.draganddrop.DragAndDropPolicy; +import com.android.wm.shell.draganddrop.SplitDragPolicy; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.shared.TransactionPool; @@ -121,7 +121,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * @see StageCoordinator */ // TODO(b/198577848): Implement split screen flicker test to consolidate CUJ of split screen. -public class SplitScreenController implements DragAndDropPolicy.Starter, +public class SplitScreenController implements SplitDragPolicy.Starter, RemoteCallable, KeyguardChangeListener { private static final String TAG = SplitScreenController.class.getSimpleName(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java deleted file mode 100644 index 645b296930f8..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java +++ /dev/null @@ -1,386 +0,0 @@ -/* - * Copyright (C) 2020 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.wm.shell.draganddrop; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -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 static android.content.ClipDescription.MIMETYPE_TEXT_INTENT; - -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; -import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_FULLSCREEN; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP; - -import static junit.framework.Assert.assertTrue; -import static junit.framework.Assert.fail; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.quality.Strictness.LENIENT; - -import android.app.ActivityManager; -import android.app.ActivityTaskManager; -import android.app.PendingIntent; -import android.content.ClipData; -import android.content.ClipDescription; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Insets; -import android.os.RemoteException; -import android.view.DisplayInfo; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.internal.logging.InstanceId; -import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.draganddrop.DragAndDropPolicy.Target; -import com.android.wm.shell.splitscreen.SplitScreenController; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.MockitoSession; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; - -/** - * Tests for the drag and drop policy. - */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class DragAndDropPolicyTest extends ShellTestCase { - - @Mock - private Context mContext; - - @Mock - private ActivityTaskManager mActivityTaskManager; - - // Both the split-screen and start interface. - @Mock - private SplitScreenController mSplitScreenStarter; - @Mock - private DragAndDropPolicy.Starter mFullscreenStarter; - - @Mock - private InstanceId mLoggerSessionId; - - private DisplayLayout mLandscapeDisplayLayout; - private DisplayLayout mPortraitDisplayLayout; - private Insets mInsets; - private DragAndDropPolicy mPolicy; - - private ClipData mActivityClipData; - private PendingIntent mLaunchableIntentPendingIntent; - private ClipData mLaunchableIntentClipData; - private ClipData mNonResizeableActivityClipData; - private ClipData mTaskClipData; - private ClipData mShortcutClipData; - - private ActivityManager.RunningTaskInfo mHomeTask; - private ActivityManager.RunningTaskInfo mFullscreenAppTask; - private ActivityManager.RunningTaskInfo mNonResizeableFullscreenAppTask; - - private MockitoSession mMockitoSession; - - @Before - public void setUp() throws RemoteException { - MockitoAnnotations.initMocks(this); - mMockitoSession = mockitoSession() - .strictness(LENIENT) - .mockStatic(DragUtils.class) - .startMocking(); - when(DragUtils.canHandleDrag(any())).thenReturn(true); - - Resources res = mock(Resources.class); - Configuration config = new Configuration(); - doReturn(config).when(res).getConfiguration(); - doReturn(res).when(mContext).getResources(); - DisplayInfo info = new DisplayInfo(); - info.logicalWidth = 200; - info.logicalHeight = 100; - mLandscapeDisplayLayout = new DisplayLayout(info, res, false, false); - DisplayInfo info2 = new DisplayInfo(); - info.logicalWidth = 100; - info.logicalHeight = 200; - mPortraitDisplayLayout = new DisplayLayout(info2, res, false, false); - mInsets = Insets.of(0, 0, 0, 0); - - mPolicy = spy(new DragAndDropPolicy(mContext, mSplitScreenStarter, mFullscreenStarter)); - mActivityClipData = createAppClipData(MIMETYPE_APPLICATION_ACTIVITY); - mLaunchableIntentPendingIntent = mock(PendingIntent.class); - when(mLaunchableIntentPendingIntent.getCreatorUserHandle()) - .thenReturn(android.os.Process.myUserHandle()); - mLaunchableIntentClipData = createIntentClipData(mLaunchableIntentPendingIntent); - mNonResizeableActivityClipData = createAppClipData(MIMETYPE_APPLICATION_ACTIVITY); - setClipDataResizeable(mNonResizeableActivityClipData, false); - mTaskClipData = createAppClipData(MIMETYPE_APPLICATION_TASK); - mShortcutClipData = createAppClipData(MIMETYPE_APPLICATION_SHORTCUT); - - mHomeTask = createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME); - mFullscreenAppTask = createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); - mNonResizeableFullscreenAppTask = - createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); - mNonResizeableFullscreenAppTask.isResizeable = false; - - setRunningTask(mFullscreenAppTask); - } - - @After - public void tearDown() { - mMockitoSession.finishMocking(); - } - - /** - * Creates an app-based clip data that is by default resizeable. - */ - private ClipData createAppClipData(String mimeType) { - ClipDescription clipDescription = new ClipDescription(mimeType, new String[] { mimeType }); - Intent i = new Intent(); - switch (mimeType) { - case MIMETYPE_APPLICATION_SHORTCUT: - i.putExtra(Intent.EXTRA_PACKAGE_NAME, "package"); - i.putExtra(Intent.EXTRA_SHORTCUT_ID, "shortcut_id"); - break; - case MIMETYPE_APPLICATION_TASK: - i.putExtra(Intent.EXTRA_TASK_ID, 12345); - break; - case MIMETYPE_APPLICATION_ACTIVITY: - final PendingIntent pi = mock(PendingIntent.class); - doReturn(android.os.Process.myUserHandle()).when(pi).getCreatorUserHandle(); - i.putExtra(ClipDescription.EXTRA_PENDING_INTENT, pi); - break; - } - i.putExtra(Intent.EXTRA_USER, android.os.Process.myUserHandle()); - ClipData.Item item = new ClipData.Item(i); - item.setActivityInfo(new ActivityInfo()); - ClipData data = new ClipData(clipDescription, item); - setClipDataResizeable(data, true); - return data; - } - - /** - * Creates an intent-based clip data that is by default resizeable. - */ - private ClipData createIntentClipData(PendingIntent intent) { - ClipDescription clipDescription = new ClipDescription("Intent", - new String[] { MIMETYPE_TEXT_INTENT }); - ClipData.Item item = new ClipData.Item.Builder() - .setIntentSender(intent.getIntentSender()) - .build(); - ClipData data = new ClipData(clipDescription, item); - return data; - } - - private ActivityManager.RunningTaskInfo createTaskInfo(int winMode, int actType) { - ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo(); - info.configuration.windowConfiguration.setActivityType(actType); - info.configuration.windowConfiguration.setWindowingMode(winMode); - info.isResizeable = true; - info.baseActivity = new ComponentName(getInstrumentation().getContext(), - ".ActivityWithMode" + winMode); - info.baseIntent = new Intent(); - info.baseIntent.setComponent(info.baseActivity); - ActivityInfo activityInfo = new ActivityInfo(); - activityInfo.packageName = info.baseActivity.getPackageName(); - activityInfo.name = info.baseActivity.getClassName(); - info.topActivityInfo = activityInfo; - return info; - } - - private void setRunningTask(ActivityManager.RunningTaskInfo task) { - doReturn(Collections.singletonList(task)).when(mActivityTaskManager) - .getTasks(anyInt(), anyBoolean()); - } - - private void setClipDataResizeable(ClipData data, boolean resizeable) { - data.getItemAt(0).getActivityInfo().resizeMode = resizeable - ? ActivityInfo.RESIZE_MODE_RESIZEABLE - : ActivityInfo.RESIZE_MODE_UNRESIZEABLE; - } - - @Test - public void testDragAppOverFullscreenHome_expectOnlyFullscreenTarget() { - dragOverFullscreenHome_expectOnlyFullscreenTarget(mActivityClipData); - } - - @Test - public void testDragAppOverFullscreenApp_expectSplitScreenTargets() { - dragOverFullscreenApp_expectSplitScreenTargets(mActivityClipData); - } - - @Test - public void testDragAppOverFullscreenAppPhone_expectVerticalSplitScreenTargets() { - dragOverFullscreenAppPhone_expectVerticalSplitScreenTargets(mActivityClipData); - } - - @Test - public void testDragIntentOverFullscreenHome_expectOnlyFullscreenTarget() { - when(DragUtils.getLaunchIntent((ClipData) any(), anyInt())).thenReturn( - mLaunchableIntentPendingIntent); - dragOverFullscreenHome_expectOnlyFullscreenTarget(mLaunchableIntentClipData); - } - - @Test - public void testDragIntentOverFullscreenApp_expectSplitScreenTargets() { - when(DragUtils.getLaunchIntent((ClipData) any(), anyInt())).thenReturn( - mLaunchableIntentPendingIntent); - dragOverFullscreenApp_expectSplitScreenTargets(mLaunchableIntentClipData); - } - - @Test - public void testDragIntentOverFullscreenAppPhone_expectVerticalSplitScreenTargets() { - when(DragUtils.getLaunchIntent((ClipData) any(), anyInt())).thenReturn( - mLaunchableIntentPendingIntent); - dragOverFullscreenAppPhone_expectVerticalSplitScreenTargets(mLaunchableIntentClipData); - } - - private void dragOverFullscreenHome_expectOnlyFullscreenTarget(ClipData data) { - doReturn(true).when(mSplitScreenStarter).isLeftRightSplit(); - setRunningTask(mHomeTask); - DragSession dragSession = new DragSession(mActivityTaskManager, - mLandscapeDisplayLayout, data, 0 /* dragFlags */); - dragSession.initialize(); - mPolicy.start(dragSession, mLoggerSessionId); - ArrayList targets = assertExactTargetTypes( - mPolicy.getTargets(mInsets), TYPE_FULLSCREEN); - - mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), null /* hideTaskToken */); - verify(mFullscreenStarter).startIntent(any(), anyInt(), any(), - eq(SPLIT_POSITION_UNDEFINED), any(), any()); - } - - private void dragOverFullscreenApp_expectSplitScreenTargets(ClipData data) { - doReturn(true).when(mSplitScreenStarter).isLeftRightSplit(); - setRunningTask(mFullscreenAppTask); - DragSession dragSession = new DragSession(mActivityTaskManager, - mLandscapeDisplayLayout, data, 0 /* dragFlags */); - dragSession.initialize(); - mPolicy.start(dragSession, mLoggerSessionId); - ArrayList targets = assertExactTargetTypes( - mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); - - mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_LEFT), null /* hideTaskToken */); - verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), - eq(SPLIT_POSITION_TOP_OR_LEFT), any(), any()); - reset(mSplitScreenStarter); - - mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), null /* hideTaskToken */); - verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), - eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any(), any()); - } - - private void dragOverFullscreenAppPhone_expectVerticalSplitScreenTargets(ClipData data) { - doReturn(false).when(mSplitScreenStarter).isLeftRightSplit(); - setRunningTask(mFullscreenAppTask); - DragSession dragSession = new DragSession(mActivityTaskManager, - mPortraitDisplayLayout, data, 0 /* dragFlags */); - dragSession.initialize(); - mPolicy.start(dragSession, mLoggerSessionId); - ArrayList targets = assertExactTargetTypes( - mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); - - mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_TOP), null /* hideTaskToken */); - verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), - eq(SPLIT_POSITION_TOP_OR_LEFT), any(), any()); - reset(mSplitScreenStarter); - - mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), - null /* hideTaskToken */); - verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), - eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any(), any()); - } - - @Test - public void testTargetHitRects() { - setRunningTask(mFullscreenAppTask); - DragSession dragSession = new DragSession(mActivityTaskManager, - mLandscapeDisplayLayout, mActivityClipData, 0 /* dragFlags */); - dragSession.initialize(); - mPolicy.start(dragSession, mLoggerSessionId); - ArrayList targets = mPolicy.getTargets(mInsets); - for (Target t : targets) { - assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.left, t.hitRegion.top) == t); - assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.right - 1, t.hitRegion.top) == t); - assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.right - 1, t.hitRegion.bottom - 1) - == t); - assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.left, t.hitRegion.bottom - 1) - == t); - } - } - - @Test - public void testDisallowLaunchIntentWithoutDelegationFlag() { - assertTrue(DragUtils.getLaunchIntent(mLaunchableIntentClipData, 0) == null); - } - - private Target filterTargetByType(ArrayList targets, int type) { - for (Target t : targets) { - if (type == t.type) { - return t; - } - } - fail("Target with type: " + type + " not found"); - return null; - } - - private ArrayList assertExactTargetTypes(ArrayList targets, - int... expectedTargetTypes) { - HashSet expected = new HashSet<>(); - for (int t : expectedTargetTypes) { - expected.add(t); - } - for (Target t : targets) { - if (!expected.contains(t.type)) { - fail("Found unexpected target type: " + t.type); - } - expected.remove(t.type); - } - assertTrue(expected.isEmpty()); - return targets; - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java new file mode 100644 index 000000000000..46b60499a01d --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2020 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.wm.shell.draganddrop; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +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 static android.content.ClipDescription.MIMETYPE_TEXT_INTENT; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_FULLSCREEN; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_BOTTOM; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_LEFT; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_RIGHT; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_TOP; + +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.quality.Strictness.LENIENT; + +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Insets; +import android.os.RemoteException; +import android.view.DisplayInfo; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.logging.InstanceId; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.draganddrop.SplitDragPolicy.Target; +import com.android.wm.shell.splitscreen.SplitScreenController; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; + +/** + * Tests for the drag and drop policy. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SplitDragPolicyTest extends ShellTestCase { + + @Mock + private Context mContext; + + @Mock + private ActivityTaskManager mActivityTaskManager; + + // Both the split-screen and start interface. + @Mock + private SplitScreenController mSplitScreenStarter; + @Mock + private SplitDragPolicy.Starter mFullscreenStarter; + + @Mock + private InstanceId mLoggerSessionId; + + private DisplayLayout mLandscapeDisplayLayout; + private DisplayLayout mPortraitDisplayLayout; + private Insets mInsets; + private SplitDragPolicy mPolicy; + + private ClipData mActivityClipData; + private PendingIntent mLaunchableIntentPendingIntent; + private ClipData mLaunchableIntentClipData; + private ClipData mNonResizeableActivityClipData; + private ClipData mTaskClipData; + private ClipData mShortcutClipData; + + private ActivityManager.RunningTaskInfo mHomeTask; + private ActivityManager.RunningTaskInfo mFullscreenAppTask; + private ActivityManager.RunningTaskInfo mNonResizeableFullscreenAppTask; + + private MockitoSession mMockitoSession; + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + mMockitoSession = mockitoSession() + .strictness(LENIENT) + .mockStatic(DragUtils.class) + .startMocking(); + when(DragUtils.canHandleDrag(any())).thenReturn(true); + + Resources res = mock(Resources.class); + Configuration config = new Configuration(); + doReturn(config).when(res).getConfiguration(); + doReturn(res).when(mContext).getResources(); + DisplayInfo info = new DisplayInfo(); + info.logicalWidth = 200; + info.logicalHeight = 100; + mLandscapeDisplayLayout = new DisplayLayout(info, res, false, false); + DisplayInfo info2 = new DisplayInfo(); + info.logicalWidth = 100; + info.logicalHeight = 200; + mPortraitDisplayLayout = new DisplayLayout(info2, res, false, false); + mInsets = Insets.of(0, 0, 0, 0); + + mPolicy = spy(new SplitDragPolicy(mContext, mSplitScreenStarter, mFullscreenStarter)); + mActivityClipData = createAppClipData(MIMETYPE_APPLICATION_ACTIVITY); + mLaunchableIntentPendingIntent = mock(PendingIntent.class); + when(mLaunchableIntentPendingIntent.getCreatorUserHandle()) + .thenReturn(android.os.Process.myUserHandle()); + mLaunchableIntentClipData = createIntentClipData(mLaunchableIntentPendingIntent); + mNonResizeableActivityClipData = createAppClipData(MIMETYPE_APPLICATION_ACTIVITY); + setClipDataResizeable(mNonResizeableActivityClipData, false); + mTaskClipData = createAppClipData(MIMETYPE_APPLICATION_TASK); + mShortcutClipData = createAppClipData(MIMETYPE_APPLICATION_SHORTCUT); + + mHomeTask = createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME); + mFullscreenAppTask = createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); + mNonResizeableFullscreenAppTask = + createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); + mNonResizeableFullscreenAppTask.isResizeable = false; + + setRunningTask(mFullscreenAppTask); + } + + @After + public void tearDown() { + mMockitoSession.finishMocking(); + } + + /** + * Creates an app-based clip data that is by default resizeable. + */ + private ClipData createAppClipData(String mimeType) { + ClipDescription clipDescription = new ClipDescription(mimeType, new String[] { mimeType }); + Intent i = new Intent(); + switch (mimeType) { + case MIMETYPE_APPLICATION_SHORTCUT: + i.putExtra(Intent.EXTRA_PACKAGE_NAME, "package"); + i.putExtra(Intent.EXTRA_SHORTCUT_ID, "shortcut_id"); + break; + case MIMETYPE_APPLICATION_TASK: + i.putExtra(Intent.EXTRA_TASK_ID, 12345); + break; + case MIMETYPE_APPLICATION_ACTIVITY: + final PendingIntent pi = mock(PendingIntent.class); + doReturn(android.os.Process.myUserHandle()).when(pi).getCreatorUserHandle(); + i.putExtra(ClipDescription.EXTRA_PENDING_INTENT, pi); + break; + } + i.putExtra(Intent.EXTRA_USER, android.os.Process.myUserHandle()); + ClipData.Item item = new ClipData.Item(i); + item.setActivityInfo(new ActivityInfo()); + ClipData data = new ClipData(clipDescription, item); + setClipDataResizeable(data, true); + return data; + } + + /** + * Creates an intent-based clip data that is by default resizeable. + */ + private ClipData createIntentClipData(PendingIntent intent) { + ClipDescription clipDescription = new ClipDescription("Intent", + new String[] { MIMETYPE_TEXT_INTENT }); + ClipData.Item item = new ClipData.Item.Builder() + .setIntentSender(intent.getIntentSender()) + .build(); + ClipData data = new ClipData(clipDescription, item); + return data; + } + + private ActivityManager.RunningTaskInfo createTaskInfo(int winMode, int actType) { + ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo(); + info.configuration.windowConfiguration.setActivityType(actType); + info.configuration.windowConfiguration.setWindowingMode(winMode); + info.isResizeable = true; + info.baseActivity = new ComponentName(getInstrumentation().getContext(), + ".ActivityWithMode" + winMode); + info.baseIntent = new Intent(); + info.baseIntent.setComponent(info.baseActivity); + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.packageName = info.baseActivity.getPackageName(); + activityInfo.name = info.baseActivity.getClassName(); + info.topActivityInfo = activityInfo; + return info; + } + + private void setRunningTask(ActivityManager.RunningTaskInfo task) { + doReturn(Collections.singletonList(task)).when(mActivityTaskManager) + .getTasks(anyInt(), anyBoolean()); + } + + private void setClipDataResizeable(ClipData data, boolean resizeable) { + data.getItemAt(0).getActivityInfo().resizeMode = resizeable + ? ActivityInfo.RESIZE_MODE_RESIZEABLE + : ActivityInfo.RESIZE_MODE_UNRESIZEABLE; + } + + @Test + public void testDragAppOverFullscreenHome_expectOnlyFullscreenTarget() { + dragOverFullscreenHome_expectOnlyFullscreenTarget(mActivityClipData); + } + + @Test + public void testDragAppOverFullscreenApp_expectSplitScreenTargets() { + dragOverFullscreenApp_expectSplitScreenTargets(mActivityClipData); + } + + @Test + public void testDragAppOverFullscreenAppPhone_expectVerticalSplitScreenTargets() { + dragOverFullscreenAppPhone_expectVerticalSplitScreenTargets(mActivityClipData); + } + + @Test + public void testDragIntentOverFullscreenHome_expectOnlyFullscreenTarget() { + when(DragUtils.getLaunchIntent((ClipData) any(), anyInt())).thenReturn( + mLaunchableIntentPendingIntent); + dragOverFullscreenHome_expectOnlyFullscreenTarget(mLaunchableIntentClipData); + } + + @Test + public void testDragIntentOverFullscreenApp_expectSplitScreenTargets() { + when(DragUtils.getLaunchIntent((ClipData) any(), anyInt())).thenReturn( + mLaunchableIntentPendingIntent); + dragOverFullscreenApp_expectSplitScreenTargets(mLaunchableIntentClipData); + } + + @Test + public void testDragIntentOverFullscreenAppPhone_expectVerticalSplitScreenTargets() { + when(DragUtils.getLaunchIntent((ClipData) any(), anyInt())).thenReturn( + mLaunchableIntentPendingIntent); + dragOverFullscreenAppPhone_expectVerticalSplitScreenTargets(mLaunchableIntentClipData); + } + + private void dragOverFullscreenHome_expectOnlyFullscreenTarget(ClipData data) { + doReturn(true).when(mSplitScreenStarter).isLeftRightSplit(); + setRunningTask(mHomeTask); + DragSession dragSession = new DragSession(mActivityTaskManager, + mLandscapeDisplayLayout, data, 0 /* dragFlags */); + dragSession.initialize(); + mPolicy.start(dragSession, mLoggerSessionId); + ArrayList targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_FULLSCREEN); + + mPolicy.onDropped(filterTargetByType(targets, TYPE_FULLSCREEN), null /* hideTaskToken */); + verify(mFullscreenStarter).startIntent(any(), anyInt(), any(), + eq(SPLIT_POSITION_UNDEFINED), any(), any()); + } + + private void dragOverFullscreenApp_expectSplitScreenTargets(ClipData data) { + doReturn(true).when(mSplitScreenStarter).isLeftRightSplit(); + setRunningTask(mFullscreenAppTask); + DragSession dragSession = new DragSession(mActivityTaskManager, + mLandscapeDisplayLayout, data, 0 /* dragFlags */); + dragSession.initialize(); + mPolicy.start(dragSession, mLoggerSessionId); + ArrayList targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); + + mPolicy.onDropped(filterTargetByType(targets, TYPE_SPLIT_LEFT), null /* hideTaskToken */); + verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), + eq(SPLIT_POSITION_TOP_OR_LEFT), any(), any()); + reset(mSplitScreenStarter); + + mPolicy.onDropped(filterTargetByType(targets, TYPE_SPLIT_RIGHT), null /* hideTaskToken */); + verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any(), any()); + } + + private void dragOverFullscreenAppPhone_expectVerticalSplitScreenTargets(ClipData data) { + doReturn(false).when(mSplitScreenStarter).isLeftRightSplit(); + setRunningTask(mFullscreenAppTask); + DragSession dragSession = new DragSession(mActivityTaskManager, + mPortraitDisplayLayout, data, 0 /* dragFlags */); + dragSession.initialize(); + mPolicy.start(dragSession, mLoggerSessionId); + ArrayList targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); + + mPolicy.onDropped(filterTargetByType(targets, TYPE_SPLIT_TOP), null /* hideTaskToken */); + verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), + eq(SPLIT_POSITION_TOP_OR_LEFT), any(), any()); + reset(mSplitScreenStarter); + + mPolicy.onDropped(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), + null /* hideTaskToken */); + verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any(), any()); + } + + @Test + public void testTargetHitRects() { + setRunningTask(mFullscreenAppTask); + DragSession dragSession = new DragSession(mActivityTaskManager, + mLandscapeDisplayLayout, mActivityClipData, 0 /* dragFlags */); + dragSession.initialize(); + mPolicy.start(dragSession, mLoggerSessionId); + ArrayList targets = mPolicy.getTargets(mInsets); + for (Target t : targets) { + assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.left, t.hitRegion.top) == t); + assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.right - 1, t.hitRegion.top) == t); + assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.right - 1, t.hitRegion.bottom - 1) + == t); + assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.left, t.hitRegion.bottom - 1) + == t); + } + } + + @Test + public void testDisallowLaunchIntentWithoutDelegationFlag() { + assertTrue(DragUtils.getLaunchIntent(mLaunchableIntentClipData, 0) == null); + } + + private Target filterTargetByType(ArrayList targets, int type) { + for (Target t : targets) { + if (type == t.type) { + return t; + } + } + fail("Target with type: " + type + " not found"); + return null; + } + + private ArrayList assertExactTargetTypes(ArrayList targets, + int... expectedTargetTypes) { + HashSet expected = new HashSet<>(); + for (int t : expectedTargetTypes) { + expected.add(t); + } + for (Target t : targets) { + if (!expected.contains(t.type)) { + fail("Found unexpected target type: " + t.type); + } + expected.remove(t.type); + } + assertTrue(expected.isEmpty()); + return targets; + } +} -- cgit v1.2.3-59-g8ed1b