diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index ec287fe106343..ac926db96eac3 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -16,6 +16,8 @@ package android.app; +import static android.app.WindowConfiguration.activityTypeToString; +import static android.app.WindowConfiguration.windowingModeToString; import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; @@ -1732,11 +1734,6 @@ public class ActivityManager { * @hide */ public void dump(PrintWriter pw, String indent) { - final String activityType = WindowConfiguration.activityTypeToString( - configuration.windowConfiguration.getActivityType()); - final String windowingMode = WindowConfiguration.activityTypeToString( - configuration.windowConfiguration.getActivityType()); - pw.println(); pw.print(" "); pw.print(" id="); pw.print(persistentId); pw.print(" stackId="); pw.print(stackId); @@ -1762,8 +1759,8 @@ public class ActivityManager { pw.print(" "); pw.print(" isExcluded="); pw.print(((baseIntent.getFlags() & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0)); - pw.print(" activityType="); pw.print(activityType); - pw.print(" windowingMode="); pw.print(windowingMode); + pw.print(" activityType="); pw.print(activityTypeToString(getActivityType())); + pw.print(" windowingMode="); pw.print(windowingModeToString(getWindowingMode())); pw.print(" supportsSplitScreenMultiWindow="); pw.println(supportsSplitScreenMultiWindow); if (taskDescription != null) { diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java index 43b7722c08645..5dfab6efe4905 100644 --- a/core/java/android/app/TaskInfo.java +++ b/core/java/android/app/TaskInfo.java @@ -256,6 +256,18 @@ public class TaskInfo { return pictureInPictureParams; } + /** @hide */ + @WindowConfiguration.WindowingMode + public int getWindowingMode() { + return configuration.windowConfiguration.getWindowingMode(); + } + + /** @hide */ + @WindowConfiguration.ActivityType + public int getActivityType() { + return configuration.windowConfiguration.getActivityType(); + } + /** @hide */ public void addLaunchCookie(IBinder cookie) { if (cookie == null || launchCookies.contains(cookie)) return; diff --git a/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json b/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json index bee9f4163b048..6342c00f80fa0 100644 --- a/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json +++ b/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json @@ -79,12 +79,6 @@ "group": "WM_SHELL_TASK_ORG", "at": "com\/android\/wm\/shell\/splitscreen\/SplitScreenTaskListener.java" }, - "-712674749": { - "message": "Clip description: %s", - "level": "VERBOSE", - "group": "WM_SHELL_DRAG_AND_DROP", - "at": "com\/android\/wm\/shell\/draganddrop\/DragAndDropController.java" - }, "-710770147": { "message": "Add target: %s", "level": "VERBOSE", @@ -121,6 +115,12 @@ "group": "WM_SHELL_DRAG_AND_DROP", "at": "com\/android\/wm\/shell\/draganddrop\/DropOutlineDrawable.java" }, + "375908576": { + "message": "Clip description: handlingDrag=%b itemCount=%d mimeTypes=%s", + "level": "VERBOSE", + "group": "WM_SHELL_DRAG_AND_DROP", + "at": "com\/android\/wm\/shell\/draganddrop\/DragAndDropController.java" + }, "481673835": { "message": "addListenerForTaskId taskId=%s", "level": "VERBOSE", @@ -169,12 +169,6 @@ "group": "WM_SHELL_DRAG_AND_DROP", "at": "com\/android\/wm\/shell\/draganddrop\/DragLayout.java" }, - "1842752748": { - "message": "Clip description: handlingDrag=%b mimeTypes=%s", - "level": "VERBOSE", - "group": "WM_SHELL_DRAG_AND_DROP", - "at": "com\/android\/wm\/shell\/draganddrop\/DragAndDropController.java" - }, "1862198614": { "message": "Drag event: action=%s x=%f y=%f xOffset=%f yOffset=%f", "level": "VERBOSE", 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 bf5b1d8a4bccd..c77f5943ee0c3 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 @@ -16,17 +16,9 @@ package com.android.wm.shell.draganddrop; -import static android.app.ActivityTaskManager.INVALID_TASK_ID; -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -import static android.content.ClipDescription.EXTRA_ACTIVITY_OPTIONS; -import static android.content.ClipDescription.EXTRA_PENDING_INTENT; 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.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.view.DragEvent.ACTION_DRAG_ENDED; import static android.view.DragEvent.ACTION_DRAG_ENTERED; import static android.view.DragEvent.ACTION_DRAG_EXITED; @@ -42,25 +34,10 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.ActivityOptions; -import android.app.ActivityTaskManager; -import android.app.PendingIntent; -import android.content.ActivityNotFoundException; -import android.content.ClipData; import android.content.ClipDescription; import android.content.Context; -import android.content.Intent; -import android.content.pm.LauncherApps; import android.content.res.Configuration; import android.graphics.PixelFormat; -import android.os.Binder; -import android.os.Bundle; -import android.os.Handler; -import android.os.RemoteException; -import android.os.UserHandle; -import android.util.Slog; import android.util.SparseArray; import android.view.DragEvent; import android.view.LayoutInflater; @@ -76,6 +53,7 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreen; +import java.util.Objects; import java.util.Optional; /** @@ -91,10 +69,12 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange private SplitScreen mSplitScreen; private final SparseArray mDisplayDropTargets = new SparseArray<>(); - private boolean mIsHandlingDrag; - private DragLayout mDragLayout; private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); + // A count of the number of active drags in progress to ensure that we only hide the window when + // all the drag animations have completed + private int mActiveDragCount; + public DragAndDropController(Context context, DisplayController displayController) { mContext = context; mDisplayController = displayController; @@ -124,26 +104,31 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange layoutParams.setFitInsetsTypes(0); layoutParams.setTitle("ShellDropTarget"); - FrameLayout dropTarget = (FrameLayout) LayoutInflater.from(context).inflate( + FrameLayout rootView = (FrameLayout) LayoutInflater.from(context).inflate( R.layout.global_drop_target, null); - dropTarget.setOnDragListener(this); - dropTarget.setVisibility(View.INVISIBLE); - wm.addView(dropTarget, layoutParams); - mDisplayDropTargets.put(displayId, new PerDisplay(displayId, context, wm, dropTarget)); + rootView.setOnDragListener(this); + rootView.setVisibility(View.INVISIBLE); + DragLayout dragLayout = new DragLayout(context, mSplitScreen); + rootView.addView(dragLayout, + new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + wm.addView(rootView, layoutParams); + + mDisplayDropTargets.put(displayId, + new PerDisplay(displayId, context, wm, rootView, dragLayout)); } @Override public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display changed: %d", displayId); final PerDisplay pd = mDisplayDropTargets.get(displayId); - pd.dropTarget.requestApplyInsets(); + pd.rootView.requestApplyInsets(); } @Override public void onDisplayRemoved(int displayId) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display removed: %d", displayId); final PerDisplay pd = mDisplayDropTargets.get(displayId); - pd.wm.removeViewImmediate(pd.dropTarget); + pd.wm.removeViewImmediate(pd.rootView); mDisplayDropTargets.remove(displayId); } @@ -158,32 +143,33 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange final ClipDescription description = event.getClipDescription(); if (event.getAction() == ACTION_DRAG_STARTED) { - final boolean hasValidClipData = description.hasMimeType(MIMETYPE_APPLICATION_ACTIVITY) - || description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT) - || description.hasMimeType(MIMETYPE_APPLICATION_TASK); - mIsHandlingDrag = hasValidClipData; + final boolean hasValidClipData = event.getClipData().getItemCount() > 0 + && (description.hasMimeType(MIMETYPE_APPLICATION_ACTIVITY) + || description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT) + || description.hasMimeType(MIMETYPE_APPLICATION_TASK)); + pd.isHandlingDrag = hasValidClipData; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, - "Clip description: handlingDrag=%b mimeTypes=%s", - mIsHandlingDrag, getMimeTypes(description)); + "Clip description: handlingDrag=%b itemCount=%d mimeTypes=%s", + pd.isHandlingDrag, event.getClipData().getItemCount(), + getMimeTypes(description)); } - if (!mIsHandlingDrag) { + if (!pd.isHandlingDrag) { return false; } switch (event.getAction()) { case ACTION_DRAG_STARTED: - mDragLayout = new DragLayout(pd.context, - mDisplayController.getDisplayLayout(displayId), mSplitScreen); - pd.dropTarget.addView(mDragLayout, - new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + mActiveDragCount++; + pd.dragLayout.prepare(mDisplayController.getDisplayLayout(displayId), + event.getClipData()); setDropTargetWindowVisibility(pd, View.VISIBLE); break; case ACTION_DRAG_ENTERED: - mDragLayout.show(event); + pd.dragLayout.show(); break; case ACTION_DRAG_LOCATION: - mDragLayout.update(event); + pd.dragLayout.update(event); break; case ACTION_DROP: { return handleDrop(event, pd); @@ -191,20 +177,22 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange case ACTION_DRAG_EXITED: { // Either one of DROP or EXITED will happen, and when EXITED we won't consume // the drag surface - mDragLayout.hide(event, null); + pd.dragLayout.hide(event, null); break; } case ACTION_DRAG_ENDED: // TODO(b/169894807): Ensure sure it's not possible to get ENDED without DROP // or EXITED - if (!mDragLayout.hasDropped()) { - final View dragLayout = mDragLayout; - mDragLayout.hide(event, () -> { - setDropTargetWindowVisibility(pd, View.INVISIBLE); - pd.dropTarget.removeView(dragLayout); + if (!pd.dragLayout.hasDropped()) { + mActiveDragCount--; + pd.dragLayout.hide(event, () -> { + if (mActiveDragCount == 0) { + // Hide the window if another drag hasn't been started while animating + // the drag-end + setDropTargetWindowVisibility(pd, View.INVISIBLE); + } }); } - mDragLayout = null; break; } return true; @@ -214,52 +202,14 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange * Handles dropping on the drop target. */ private boolean handleDrop(DragEvent event, PerDisplay pd) { - final ClipData data = event.getClipData(); - final ClipDescription description = event.getClipDescription(); final SurfaceControl dragSurface = event.getDragSurface(); - final View dragLayout = mDragLayout; - final boolean isTask = description.hasMimeType(MIMETYPE_APPLICATION_TASK); - final boolean isShortcut = description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT); - return mDragLayout.drop(event, dragSurface, (dropTargetBounds) -> { - if (dropTargetBounds != null && data.getItemCount() > 0) { - final Intent intent = data.getItemAt(0).getIntent(); - // TODO(b/169894807): Properly handle the drop, for now just launch it - if (isTask) { - int taskId = intent.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID); - try { - ActivityTaskManager.getService().startActivityFromRecents( - taskId, null); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to launch task", e); - } - } else if (isShortcut) { - try { - Bundle opts = intent.hasExtra(EXTRA_ACTIVITY_OPTIONS) - ? intent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS) - : null; - LauncherApps launcherApps = - mContext.getSystemService(LauncherApps.class); - launcherApps.startShortcut( - intent.getStringExtra(EXTRA_PACKAGE_NAME), - intent.getStringExtra(EXTRA_SHORTCUT_ID), - null /* sourceBounds */, opts, - intent.getParcelableExtra(EXTRA_USER)); - } catch (ActivityNotFoundException e) { - Slog.e(TAG, "Failed to launch shortcut", e); - } - } else { - PendingIntent pi = intent.getParcelableExtra(EXTRA_PENDING_INTENT); - try { - pi.send(); - } catch (PendingIntent.CanceledException e) { - Slog.e(TAG, "Failed to launch activity", e); - } - } + mActiveDragCount--; + return pd.dragLayout.drop(event, dragSurface, () -> { + if (mActiveDragCount == 0) { + // Hide the window if another drag hasn't been started while animating the drop + setDropTargetWindowVisibility(pd, View.INVISIBLE); } - setDropTargetWindowVisibility(pd, View.INVISIBLE); - pd.dropTarget.removeView(dragLayout); - // Clean up the drag surface mTransaction.reparent(dragSurface, null); mTransaction.apply(); @@ -270,9 +220,9 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Set drop target window visibility: displayId=%d visibility=%d", pd.displayId, visibility); - pd.dropTarget.setVisibility(visibility); + pd.rootView.setVisibility(visibility); if (visibility == View.VISIBLE) { - pd.dropTarget.requestApplyInsets(); + pd.rootView.requestApplyInsets(); } } @@ -291,13 +241,17 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange final int displayId; final Context context; final WindowManager wm; - final FrameLayout dropTarget; + final FrameLayout rootView; + final DragLayout dragLayout; - PerDisplay(int dispId, Context c, WindowManager w, FrameLayout l) { + boolean isHandlingDrag; + + PerDisplay(int dispId, Context c, WindowManager w, FrameLayout rv, DragLayout dl) { displayId = dispId; context = c; wm = w; - dropTarget = l; + rootView = rv; + dragLayout = dl; } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java new file mode 100644 index 0000000000000..25890bc31b3c5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -0,0 +1,409 @@ +/* + * 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.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.content.ClipDescription.EXTRA_ACTIVITY_OPTIONS; +import static android.content.ClipDescription.EXTRA_PENDING_INTENT; +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.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 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 android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.IActivityTaskManager; +import android.app.PendingIntent; +import android.app.WindowConfiguration; +import android.content.ActivityNotFoundException; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.LauncherApps; +import android.graphics.Insets; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Slog; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.splitscreen.SplitScreen; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; + +/** + * The policy for handling drag and drop operations to shell. + */ +public class DragAndDropPolicy { + + private static final String TAG = DragAndDropPolicy.class.getSimpleName(); + + private final Context mContext; + private final IActivityTaskManager mIActivityTaskManager; + private final Starter mStarter; + private final SplitScreen mSplitScreen; + private final ArrayList mTargets = new ArrayList<>(); + + private DragSession mSession; + + public DragAndDropPolicy(Context context, SplitScreen splitScreen) { + this(context, ActivityTaskManager.getService(), splitScreen, + new DefaultStarter(context, splitScreen)); + } + + @VisibleForTesting + DragAndDropPolicy(Context context, IActivityTaskManager activityTaskManager, + SplitScreen splitScreen, Starter starter) { + mContext = context; + mIActivityTaskManager = activityTaskManager; + mSplitScreen = splitScreen; + mStarter = starter; + } + + /** + * Starts a new drag session with the given initial drag data. + */ + void start(DisplayLayout displayLayout, ClipData data) { + mSession = new DragSession(mContext, mIActivityTaskManager, displayLayout, data); + // TODO(b/169894807): Also update the session data with task stack changes + mSession.update(); + } + + /** + * 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 boolean isVerticalSplit = mSession.isPhone && !mSession.displayLayout.isLandscape(); + if (mSession.dragItemSupportsSplitscreen + && mSession.runningTaskActType == ACTIVITY_TYPE_STANDARD + && mSession.runningTaskWinMode == WINDOWING_MODE_FULLSCREEN + && mSession.runningTaskIsResizeable) { + // Allow splitting when there is a fullscreen standard activity running + if (isVerticalSplit) { + // TODO(b/169894807): For now, only allow splitting to the right/bottom until we + // have split pairs + mTargets.add(new Target(TYPE_FULLSCREEN, + new Rect(0, 0, w, h / 2), + new Rect(l, t, l + iw, t + ih), + new Rect(0, 0, w, h))); + mTargets.add(new Target(TYPE_SPLIT_BOTTOM, + new Rect(0, h / 2, w, h), + new Rect(l, t + ih / 2, l + iw, t + ih), + new Rect(0, h / 2, w, h))); + } else { + mTargets.add(new Target(TYPE_FULLSCREEN, + new Rect(0, 0, w / 2, h), + new Rect(l, t, l + iw, t + ih), + new Rect(0, 0, w, h))); + mTargets.add(new Target(TYPE_SPLIT_RIGHT, + new Rect(w / 2, 0, w, h), + new Rect(l + iw / 2, t, l + iw, t + ih), + new Rect(w / 2, 0, w, h))); + } + } else if (mSession.dragItemSupportsSplitscreen + && mSplitScreen != null + && mSplitScreen.isDividerVisible()) { + // Already split, allow replacing existing split task + // TODO(b/169894807): For now, only allow replacing the non-primary task until we have + // split pairs + final Rect secondarySplitRawBounds = + mSplitScreen.getDividerView().getNonMinimizedSplitScreenSecondaryBounds(); + final Rect secondarySplitBounds = new Rect(secondarySplitRawBounds); + secondarySplitBounds.intersect(new Rect(l, t, l + iw, t + ih)); + if (isVerticalSplit) { + mTargets.add(new Target(TYPE_FULLSCREEN, + new Rect(0, 0, w, secondarySplitRawBounds.top), + new Rect(l, t, l + iw, t + ih), + new Rect(0, 0, w, secondarySplitRawBounds.top))); + } else { + mTargets.add(new Target(TYPE_FULLSCREEN, + new Rect(0, 0, secondarySplitRawBounds.left, h), + new Rect(l, t, l + iw, t + ih), + new Rect(0, 0, w, h))); + } + mTargets.add(new Target(isVerticalSplit ? TYPE_SPLIT_BOTTOM : TYPE_SPLIT_RIGHT, + new Rect(secondarySplitBounds), + new Rect(secondarySplitBounds), + new Rect(secondarySplitBounds))); + } else { + // Otherwise only show the fullscreen target + mTargets.add(new Target(TYPE_FULLSCREEN, + new Rect(0, 0, w, h), + new Rect(l, t, l + iw, t + ih), + new Rect(0, 0, w, h))); + } + return mTargets; + } + + /** + * Returns the target at the given position based on the targets previously calculated. + */ + @Nullable + Target getTargetAtLocation(int x, int y) { + 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; + } + + @VisibleForTesting + void handleDrop(Target target, ClipData data) { + if (target == null || !mTargets.contains(target)) { + return; + } + + final ClipDescription description = data.getDescription(); + final boolean isTask = description.hasMimeType(MIMETYPE_APPLICATION_TASK); + final boolean isShortcut = description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT); + final Intent dragData = mSession.dragData; + + if (target.type == TYPE_FULLSCREEN) { + if (mSplitScreen != null && mSplitScreen.isDividerVisible()) { + // If in split, remove split and launch fullscreen + mStarter.exitSplitScreen(mSession.runningTaskId); + } else { + // Not in split, fall through to launch + } + } else { + if (mSplitScreen != null && mSplitScreen.isDividerVisible()) { + // Split is already visible, just replace the task + // TODO(b/169894807): Since we only allow replacing the non-primary target above + // just fall through and start the activity + } else { + // Not in split, enter split now + mStarter.enterSplitScreen(mSession.runningTaskId, + target.type == TYPE_SPLIT_LEFT || target.type == TYPE_SPLIT_TOP); + } + } + + Bundle opts = dragData.hasExtra(EXTRA_ACTIVITY_OPTIONS) + ? dragData.getBundleExtra(EXTRA_ACTIVITY_OPTIONS) + : null; + if (isTask) { + mStarter.startTask(dragData.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID), opts); + } else if (isShortcut) { + mStarter.startShortcut(dragData.getStringExtra(EXTRA_PACKAGE_NAME), + dragData.getStringExtra(EXTRA_SHORTCUT_ID), + opts, dragData.getParcelableExtra(EXTRA_USER)); + } else { + mStarter.startIntent(dragData.getParcelableExtra(EXTRA_PENDING_INTENT), opts); + } + } + + /** + * Per-drag session data. + */ + private static class DragSession { + private final Context mContext; + private final IActivityTaskManager mIActivityTaskManager; + private final ClipData mInitialDragData; + + final DisplayLayout displayLayout; + Intent dragData; + int runningTaskId; + @WindowConfiguration.WindowingMode + int runningTaskWinMode = WINDOWING_MODE_UNDEFINED; + @WindowConfiguration.ActivityType + int runningTaskActType = ACTIVITY_TYPE_STANDARD; + boolean runningTaskIsResizeable; + boolean dragItemSupportsSplitscreen; + boolean isPhone; + + DragSession(Context context, IActivityTaskManager activityTaskManager, + DisplayLayout dispLayout, ClipData data) { + mContext = context; + mIActivityTaskManager = activityTaskManager; + mInitialDragData = data; + displayLayout = dispLayout; + } + + /** + * Updates the session data based on the current state of the system. + */ + void update() { + final ClipDescription description = mInitialDragData.getDescription(); + try { + List tasks = + mIActivityTaskManager.getFilteredTasks(1, + false /* filterOnlyVisibleRecents */); + if (!tasks.isEmpty()) { + final ActivityManager.RunningTaskInfo task = tasks.get(0); + runningTaskWinMode = task.getWindowingMode(); + runningTaskActType = task.getActivityType(); + runningTaskId = task.taskId; + runningTaskIsResizeable = task.isResizeable; + } + } catch (RemoteException e) { + // Fall through + } + + final ActivityInfo info = mInitialDragData.getItemAt(0).getActivityInfo(); + dragItemSupportsSplitscreen = info == null + || ActivityInfo.isResizeableMode(info.resizeMode); + isPhone = mContext.getResources().getConfiguration().smallestScreenWidthDp < 600; + dragData = mInitialDragData.getItemAt(0).getIntent(); + } + } + + /** + * Interface for actually committing the task launches. + */ + @VisibleForTesting + interface Starter { + void startTask(int taskId, Bundle activityOptions); + void startShortcut(String packageName, String shortcutId, Bundle activityOptions, + UserHandle user); + void startIntent(PendingIntent intent, Bundle activityOptions); + void enterSplitScreen(int taskId, boolean leftOrTop); + void exitSplitScreen(int taskId); + } + + /** + * 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; + private final SplitScreen mSplitScreen; + + public DefaultStarter(Context context, SplitScreen splitScreen) { + mContext = context; + mSplitScreen = splitScreen; + } + + @Override + public void startTask(int taskId, Bundle activityOptions) { + try { + ActivityTaskManager.getService().startActivityFromRecents(taskId, null); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to launch task", e); + } + } + + @Override + public void startShortcut(String packageName, String shortcutId, Bundle activityOptions, + UserHandle user) { + try { + LauncherApps launcherApps = + mContext.getSystemService(LauncherApps.class); + launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */, + activityOptions, user); + } catch (ActivityNotFoundException e) { + Slog.e(TAG, "Failed to launch shortcut", e); + } + } + + @Override + public void startIntent(PendingIntent intent, Bundle activityOptions) { + try { + intent.send(null, 0, null, null, null, null, activityOptions); + } catch (PendingIntent.CanceledException e) { + Slog.e(TAG, "Failed to launch activity", e); + } + } + + @Override + public void enterSplitScreen(int taskId, boolean leftOrTop) { + mSplitScreen.splitPrimaryTask(); + } + + @Override + public void exitSplitScreen(int taskId) { + mSplitScreen.dismissSplitToPrimaryTask(); + } + } + + /** + * 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; + // The + final Rect dropTargetBounds; + + public Target(@Type int t, Rect hit, Rect draw, Rect drop) { + type = t; + hitRegion = hit; + drawRegion = draw; + dropTargetBounds = drop; + } + + @Override + public String toString() { + return "Target {hit=" + hitRegion + " drop=" + dropTargetBounds + "}"; + } + } +} 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 b70036b1cce74..fa857cdd174c3 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 @@ -24,6 +24,7 @@ import static com.android.wm.shell.animation.Interpolators.LINEAR_OUT_SLOW_IN; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; +import android.content.ClipData; import android.content.Context; import android.graphics.Canvas; import android.graphics.Insets; @@ -52,20 +53,19 @@ import java.util.function.Consumer; */ public class DragLayout extends View { - private final DisplayLayout mDisplayLayout; - private final SplitScreen mSplitScreen; + private final DragAndDropPolicy mPolicy; - private final ArrayList mTargets = new ArrayList<>(); - private Target mCurrentTarget = null; + private DragAndDropPolicy.Target mCurrentTarget = null; private DropOutlineDrawable mDropOutline; private int mDisplayMargin; private Insets mInsets = Insets.NONE; + + private boolean mIsShowing; private boolean mHasDropped; - public DragLayout(Context context, DisplayLayout displayLayout, SplitScreen splitscreen) { + public DragLayout(Context context, SplitScreen splitscreen) { super(context); - mDisplayLayout = displayLayout; - mSplitScreen = splitscreen; + mPolicy = new DragAndDropPolicy(context, splitscreen); mDisplayMargin = context.getResources().getDimensionPixelSize( R.dimen.drop_layout_display_margin); mDropOutline = new DropOutlineDrawable(context); @@ -76,7 +76,7 @@ public class DragLayout extends View { @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { mInsets = insets.getInsets(Type.systemBars() | Type.displayCutout()); - calculateDropTargets(); + recomputeDropTargets(); return super.onApplyWindowInsets(insets); } @@ -100,66 +100,41 @@ public class DragLayout extends View { return mHasDropped; } - public void show(DragEvent event) { - calculateDropTargets(); + public void prepare(DisplayLayout displayLayout, ClipData initialData) { + mPolicy.start(displayLayout, initialData); mHasDropped = false; + mCurrentTarget = null; } - private void calculateDropTargets() { - // Calculate all the regions based on split and landscape portrait - // TODO: Filter based on clip data - final float SIDE_MARGIN_PCT = 0.3f; - final int w = mDisplayLayout.width(); - final int h = mDisplayLayout.height(); - final int iw = w - mInsets.left - mInsets.right; - final int ih = h - mInsets.top - mInsets.bottom; - final int l = mInsets.left; - final int t = mInsets.top; - final int r = mInsets.right; - final int b = mInsets.bottom; - mTargets.clear(); - - // Left split - addTarget(new Target( - new Rect(0, 0, - (int) (w * SIDE_MARGIN_PCT), h), - new Rect(l + mDisplayMargin, t + mDisplayMargin, - l + iw / 2 - mDisplayMargin, t + ih - mDisplayMargin), - new Rect(0, 0, w / 2, h))); - - // Fullscreen - addTarget(new Target( - new Rect((int) (w * SIDE_MARGIN_PCT), 0, - w - (int) (w * SIDE_MARGIN_PCT), h), - new Rect(l + mDisplayMargin, t + mDisplayMargin, - l + iw - mDisplayMargin, t + ih - mDisplayMargin), - new Rect(0, 0, w, h))); - - // Right split - addTarget(new Target( - new Rect(w - (int) (w * SIDE_MARGIN_PCT), 0, - w, h), - new Rect(l + iw / 2 + mDisplayMargin, t + mDisplayMargin, - l + iw - mDisplayMargin, t + ih - mDisplayMargin), - new Rect(w / 2, 0, w, h))); + public void show() { + mIsShowing = true; + recomputeDropTargets(); } - private void addTarget(Target t) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Add target: %s", t); - mTargets.add(t); + /** + * Recalculates the drop targets based on the current policy. + */ + private void recomputeDropTargets() { + if (!mIsShowing) { + return; + } + final ArrayList targets = mPolicy.getTargets(mInsets); + for (int i = 0; i < targets.size(); i++) { + final DragAndDropPolicy.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); + } } + /** + * Updates the visible drop target as the user drags. + */ public void update(DragEvent event) { // Find containing region, if the same as mCurrentRegion, then skip, otherwise, animate the // visibility of the current region - Target target = null; - for (int i = mTargets.size() - 1; i >= 0; i--) { - Target t = mTargets.get(i); - if (t.hitRegion.contains((int) event.getX(), (int) event.getY())) { - target = t; - break; - } - } + DragAndDropPolicy.Target target = mPolicy.getTargetAtLocation( + (int) event.getX(), (int) event.getY()); if (target != null && mCurrentTarget != target) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Current target: %s", target); Interpolator boundsInterpolator = FAST_OUT_SLOW_IN; @@ -175,7 +150,11 @@ public class DragLayout extends View { } } + /** + * Hides the drag layout and animates out the visible drop targets. + */ public void hide(DragEvent event, Runnable hideCompleteCallback) { + mIsShowing = false; ObjectAnimator alphaAnimator = mDropOutline.startVisibilityAnimation(false, LINEAR); ObjectAnimator boundsAnimator = null; if (mCurrentTarget != null) { @@ -199,50 +178,19 @@ public class DragLayout extends View { mCurrentTarget = null; } + /** + * Handles the drop onto a target and animates out the visible drop targets. + */ public boolean drop(DragEvent event, SurfaceControl dragSurface, - Consumer dropCompleteCallback) { + Runnable dropCompleteCallback) { + final boolean handledDrop = mCurrentTarget != null; mHasDropped = true; + + // Process the drop + mPolicy.handleDrop(mCurrentTarget, event.getClipData()); + // TODO(b/169894807): Coordinate with dragSurface - final Rect dropRegion = mCurrentTarget != null - ? mCurrentTarget.dropTargetBounds - : null; - - ObjectAnimator alphaAnimator = mDropOutline.startVisibilityAnimation(false, LINEAR); - ObjectAnimator boundsAnimator = null; - if (dropRegion != null) { - Rect finalBounds = new Rect(mCurrentTarget.drawRegion); - finalBounds.inset(mDisplayMargin, mDisplayMargin); - mDropOutline.startBoundsAnimation(finalBounds, FAST_OUT_LINEAR_IN); - } - - if (dropCompleteCallback != null) { - ObjectAnimator lastAnim = boundsAnimator != null - ? boundsAnimator - : alphaAnimator; - lastAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - dropCompleteCallback.accept(dropRegion); - } - }); - } - return dropRegion != null; - } - - private static class Target { - final Rect hitRegion; - final Rect drawRegion; - final Rect dropTargetBounds; - - public Target(Rect hit, Rect draw, Rect drop) { - hitRegion = hit; - drawRegion = draw; - dropTargetBounds = drop; - } - - @Override - public String toString() { - return "Target {hit=" + hitRegion + " drop=" + dropTargetBounds + "}"; - } + hide(event, dropCompleteCallback); + return handledDrop; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java index 08edc2fb35ac4..64f7be5be8139 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java @@ -40,7 +40,7 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.R; /** - * Drawable to draw the region of the + * Drawable to draw the region that the target will have once it is dropped. */ public class DropOutlineDrawable extends Drawable { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java index 985dff20ad321..d1176733deb95 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java @@ -83,4 +83,9 @@ public interface SplitScreen { * @return {@code true} if it successes to split the primary task. */ boolean splitPrimaryTask(); + + /** + * Exits the split to make the primary task fullscreen. + */ + void dismissSplitToPrimaryTask(); } 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 8b616e8fd1eec..341a459902dba 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 @@ -501,6 +501,11 @@ public class SplitScreenController implements SplitScreen, } } + @Override + public void dismissSplitToPrimaryTask() { + startDismissSplit(true /* toPrimaryTask */); + } + /** Notifies the bounds of split screen changed. */ void notifyBoundsChanged(Rect secondaryWindowBounds, Rect secondaryWindowInsets) { synchronized (mBoundsChangedListeners) { @@ -519,8 +524,8 @@ public class SplitScreenController implements SplitScreen, mHomeStackResizable = mWindowManagerProxy.applyEnterSplit(mSplits, mSplitLayout); } - void startDismissSplit() { - mWindowManagerProxy.applyDismissSplit(mSplits, mSplitLayout, true /* dismissOrMaximize */); + void startDismissSplit(boolean toPrimaryTask) { + mWindowManagerProxy.applyDismissSplit(mSplits, mSplitLayout, !toPrimaryTask); updateVisibility(false /* visible */); mMinimized = false; // Resets divider bar position to undefined, so new divider bar will apply default position diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskListener.java index f709fed78b442..64e9d66183900 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskListener.java @@ -271,7 +271,7 @@ class SplitScreenTaskListener implements ShellTaskOrganizer.TaskListener { Log.d(TAG, " was in split, so this means leave it " + mPrimary.topActivityType + " " + mSecondary.topActivityType); } - mSplitScreenController.startDismissSplit(); + mSplitScreenController.startDismissSplit(false /* toPrimaryTask */); } else if (!primaryIsEmpty && primaryWasEmpty && secondaryWasEmpty) { // Wasn't in split-mode (both were empty), but now that the primary split is // populated, we should fully enter split by moving everything else into secondary. 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 new file mode 100644 index 0000000000000..affd7367f4729 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java @@ -0,0 +1,358 @@ +/* + * 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.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; +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 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_RIGHT; + +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.verify; + +import android.app.ActivityManager; +import android.app.IActivityTaskManager; +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipDescription; +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.graphics.Rect; +import android.os.RemoteException; +import android.view.DisplayInfo; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.draganddrop.DragAndDropPolicy.Target; +import com.android.wm.shell.splitscreen.DividerView; +import com.android.wm.shell.splitscreen.SplitScreen; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +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 { + + @Mock + private Context mContext; + + @Mock + private IActivityTaskManager mIActivityTaskManager; + + @Mock + private SplitScreen mSplitScreen; + + @Mock + private DragAndDropPolicy.Starter mStarter; + + private DisplayLayout mDisplayLayout; + private Insets mInsets; + private DragAndDropPolicy mPolicy; + + private ClipData mActivityClipData; + private ClipData mNonResizeableActivityClipData; + private ClipData mTaskClipData; + private ClipData mShortcutClipData; + + private ActivityManager.RunningTaskInfo mHomeTask; + private ActivityManager.RunningTaskInfo mFullscreenAppTask; + private ActivityManager.RunningTaskInfo mNonResizeableFullscreenAppTask; + private ActivityManager.RunningTaskInfo mSplitPrimaryAppTask; + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + + Resources res = mock(Resources.class); + Configuration config = new Configuration(); + doReturn(config).when(res).getConfiguration(); + DisplayInfo info = new DisplayInfo(); + info.logicalWidth = 100; + info.logicalHeight = 100; + mDisplayLayout = new DisplayLayout(info, res, false, false); + mInsets = Insets.of(0, 0, 0, 0); + + DividerView divider = mock(DividerView.class); + doReturn(divider).when(mSplitScreen).getDividerView(); + doReturn(new Rect(50, 0, 100, 100)).when(divider) + .getNonMinimizedSplitScreenSecondaryBounds(); + + mPolicy = new DragAndDropPolicy(mContext, mIActivityTaskManager, mSplitScreen, mStarter); + mActivityClipData = createClipData(MIMETYPE_APPLICATION_ACTIVITY); + mNonResizeableActivityClipData = createClipData(MIMETYPE_APPLICATION_ACTIVITY); + setClipDataResizeable(mNonResizeableActivityClipData, false); + mTaskClipData = createClipData(MIMETYPE_APPLICATION_TASK); + mShortcutClipData = createClipData(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; + mSplitPrimaryAppTask = createTaskInfo(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, + ACTIVITY_TYPE_STANDARD); + + setIsPhone(false); + setInSplitScreen(false); + setRunningTask(mFullscreenAppTask); + } + + /** + * Creates a clip data that is by default resizeable. + */ + private ClipData createClipData(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: + i.putExtra(ClipDescription.EXTRA_PENDING_INTENT, mock(PendingIntent.class)); + 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; + } + + 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; + return info; + } + + private void setRunningTask(ActivityManager.RunningTaskInfo task) throws RemoteException { + doReturn(Collections.singletonList(task)).when(mIActivityTaskManager) + .getFilteredTasks(anyInt(), anyBoolean()); + } + + private void setClipDataResizeable(ClipData data, boolean resizeable) { + data.getItemAt(0).getActivityInfo().resizeMode = resizeable + ? ActivityInfo.RESIZE_MODE_RESIZEABLE + : ActivityInfo.RESIZE_MODE_UNRESIZEABLE; + } + + private void setIsPhone(boolean isPhone) { + Resources res = mock(Resources.class); + Configuration config = mock(Configuration.class); + config.smallestScreenWidthDp = isPhone ? 400 : 800; + doReturn(config).when(res).getConfiguration(); + doReturn(res).when(mContext).getResources(); + } + + private void setInSplitScreen(boolean inSplitscreen) { + doReturn(inSplitscreen).when(mSplitScreen).isDividerVisible(); + } + + @Test + public void testDragAppOverFullscreenHome_expectOnlyFullscreenTarget() throws RemoteException { + setRunningTask(mHomeTask); + mPolicy.start(mDisplayLayout, mActivityClipData); + ArrayList targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_FULLSCREEN); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + verify(mStarter).startIntent(any(), any()); + } + + @Test + public void testDragAppOverFullscreenApp_expectSplitScreenAndFullscreenTargets() + throws RemoteException { + setRunningTask(mFullscreenAppTask); + mPolicy.start(mDisplayLayout, mActivityClipData); + // TODO(b/169894807): For now, only allow splitting to the right/bottom until we have split + // pairs + ArrayList targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_RIGHT); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + verify(mStarter).startIntent(any(), any()); + reset(mStarter); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData); + verify(mStarter).enterSplitScreen(anyInt(), eq(false)); + verify(mStarter).startIntent(any(), any()); + } + + @Test + public void testDragAppOverFullscreenAppPhone_expectVerticalSplitScreenAndFullscreenTargets() + throws RemoteException { + setIsPhone(true); + setRunningTask(mFullscreenAppTask); + mPolicy.start(mDisplayLayout, mActivityClipData); + // TODO(b/169894807): For now, only allow splitting to the right/bottom until we have split + // pairs + ArrayList targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_BOTTOM); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + verify(mStarter).startIntent(any(), any()); + reset(mStarter); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData); + verify(mStarter).enterSplitScreen(anyInt(), eq(false)); + verify(mStarter).startIntent(any(), any()); + } + + @Test + public void testDragAppOverFullscreenNonResizeableApp_expectOnlyFullscreenTargets() + throws RemoteException { + setRunningTask(mNonResizeableFullscreenAppTask); + mPolicy.start(mDisplayLayout, mActivityClipData); + ArrayList targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_FULLSCREEN); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + verify(mStarter).startIntent(any(), any()); + } + + @Test + public void testDragNonResizeableAppOverFullscreenApp_expectOnlyFullscreenTargets() + throws RemoteException { + setRunningTask(mFullscreenAppTask); + mPolicy.start(mDisplayLayout, mNonResizeableActivityClipData); + ArrayList targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_FULLSCREEN); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + verify(mStarter).startIntent(any(), any()); + } + + @Test + public void testDragAppOverSplitApp_expectFullscreenAndSplitTargets() throws RemoteException { + setInSplitScreen(true); + setRunningTask(mSplitPrimaryAppTask); + mPolicy.start(mDisplayLayout, mActivityClipData); + // TODO(b/169894807): For now, only allow splitting to the right/bottom until we have split + // pairs + ArrayList targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_RIGHT); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + verify(mStarter).startIntent(any(), any()); + reset(mStarter); + + // TODO(b/169894807): Just verify starting for the non-docked task until we have app pairs + mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData); + verify(mStarter).startIntent(any(), any()); + } + + @Test + public void testDragAppOverSplitAppPhone_expectFullscreenAndVerticalSplitTargets() + throws RemoteException { + setIsPhone(true); + setInSplitScreen(true); + setRunningTask(mSplitPrimaryAppTask); + mPolicy.start(mDisplayLayout, mActivityClipData); + // TODO(b/169894807): For now, only allow splitting to the right/bottom until we have split + // pairs + ArrayList targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_BOTTOM); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + verify(mStarter).startIntent(any(), any()); + reset(mStarter); + + // TODO(b/169894807): Just verify starting for the non-docked task until we have app pairs + mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData); + verify(mStarter).startIntent(any(), any()); + } + + @Test + public void testTargetHitRects() throws RemoteException { + setRunningTask(mFullscreenAppTask); + mPolicy.start(mDisplayLayout, mActivityClipData); + 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); + } + } + + 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/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 6fbd351648746..28e907fa6109b 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -369,15 +369,24 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { throw new IllegalArgumentException("Clip item must include the shortcut id and " + "the user to launch for."); } + // TODO(b/169894807): Validate and get activity info for shortcut as well } } else if (hasTask) { mService.mAtmService.enforceCallerIsRecentsOrHasPermission(START_TASKS_FROM_RECENTS, "performDrag"); for (int i = 0; i < data.getItemCount(); i++) { + final ClipData.Item item = data.getItemAt(i); final Intent intent = data.getItemAt(i).getIntent(); - if (intent.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID) == INVALID_TASK_ID) { + final int taskId = intent.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID); + if (taskId == INVALID_TASK_ID) { throw new IllegalArgumentException("Clip item must include the task id."); } + final Task task = mService.mRoot.anyTaskForId(taskId); + final ActivityRecord rootActivity = task != null ? task.getRootActivity() : null; + if (rootActivity == null) { + throw new IllegalArgumentException("Invalid task id."); + } + item.setActivityInfo(rootActivity.info); } } } diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java index 2c39c2b4a98ea..05c990b02754b 100644 --- a/services/core/java/com/android/server/wm/TaskOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java @@ -489,8 +489,7 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { mTmpTaskInfo.positionInParent, lastInfo.positionInParent) || mTmpTaskInfo.pictureInPictureParams != lastInfo.pictureInPictureParams - || mTmpTaskInfo.getConfiguration().windowConfiguration.getWindowingMode() - != lastInfo.getConfiguration().windowConfiguration.getWindowingMode() + || mTmpTaskInfo.getWindowingMode() != lastInfo.getWindowingMode() || !TaskDescription.equals(mTmpTaskInfo.taskDescription, lastInfo.taskDescription); if (!changed) { int cfgChanges = mTmpTaskInfo.configuration.diff(lastInfo.configuration);