Couple fixes for drag and drop

- Add simple fade of the drag surface when dropping and when canceling a
  drag from the shade (since the shade has closed, the default animation
  of animating to the start position is wrong)
- Add case for dragging over home/recents, show the highlighted drop zone
  as fullscreen since there is nothing to split with
- Fix issue with drag listener not being removed after a notification
  drag
- For now, add a toast when attempting to drag a notification that does
  not have a valid intent
- Also don't wait for DRAG_START to start collapsing the shade if the
  call to startDrag() returns true

Bug: 217972792
Bug: 218791595
Bug: 219757067
Test: atest WMShellUnitTests
Test: atest SystemUITests

Change-Id: Ic53de0d8a999d4ae39dc207bfc2927466ad2ba7c
This commit is contained in:
Winson Chung
2022-02-17 18:39:50 +00:00
parent b70d7493c6
commit 93f75de6dd
7 changed files with 243 additions and 39 deletions

View File

@@ -35,6 +35,9 @@ 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.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.ClipDescription;
import android.content.Context;
import android.content.res.Configuration;
@@ -54,6 +57,7 @@ import com.android.internal.logging.UiEventLogger;
import com.android.internal.protolog.common.ProtoLog;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.R;
import com.android.wm.shell.animation.Interpolators;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -205,6 +209,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
break;
case ACTION_DRAG_ENTERED:
pd.dragLayout.show();
pd.dragLayout.update(event);
break;
case ACTION_DRAG_LOCATION:
pd.dragLayout.update(event);
@@ -250,10 +255,6 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
// Hide the window if another drag hasn't been started while animating the drop
setDropTargetWindowVisibility(pd, View.INVISIBLE);
}
// Clean up the drag surface
mTransaction.reparent(dragSurface, null);
mTransaction.apply();
});
}

View File

@@ -122,6 +122,13 @@ public class DragAndDropPolicy {
return mSession.runningTaskInfo;
}
/**
* 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.
*/

View File

@@ -17,6 +17,7 @@
package com.android.wm.shell.draganddrop;
import static android.app.StatusBarManager.DISABLE_NONE;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
@@ -24,6 +25,7 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.StatusBarManager;
@@ -44,6 +46,7 @@ import com.android.internal.logging.InstanceId;
import com.android.internal.protolog.common.ProtoLog;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.R;
import com.android.wm.shell.animation.Interpolators;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -135,6 +138,12 @@ public class DragLayout extends LinearLayout {
}
}
private void updateContainerMarginsForSingleTask() {
mDropZoneView1.setContainerMargin(
mDisplayMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin);
mDropZoneView2.setContainerMargin(0, 0, 0, 0);
}
private void updateContainerMargins(int orientation) {
final float halfMargin = mDisplayMargin / 2f;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
@@ -165,11 +174,20 @@ public class DragLayout extends LinearLayout {
if (!alreadyInSplit) {
ActivityManager.RunningTaskInfo taskInfo1 = mPolicy.getLatestRunningTask();
if (taskInfo1 != null) {
Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo);
int bgColor1 = getResizingBackgroundColor(taskInfo1);
mDropZoneView1.setAppInfo(bgColor1, icon1);
mDropZoneView2.setAppInfo(bgColor1, icon1);
updateDropZoneSizes(null, null); // passing null splits the views evenly
final int activityType = taskInfo1.getActivityType();
if (activityType == ACTIVITY_TYPE_STANDARD) {
Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo);
int bgColor1 = getResizingBackgroundColor(taskInfo1);
mDropZoneView1.setAppInfo(bgColor1, icon1);
mDropZoneView2.setAppInfo(bgColor1, icon1);
updateDropZoneSizes(null, null); // passing null splits the views evenly
} else {
// We use the first drop zone to show the fullscreen highlight, and don't need
// to set additional info
mDropZoneView1.setForceIgnoreBottomMargin(true);
updateDropZoneSizesForSingleTask();
updateContainerMarginsForSingleTask();
}
}
} else {
// We're already in split so get taskInfo from the controller to populate icon / color.
@@ -195,6 +213,21 @@ public class DragLayout extends LinearLayout {
}
}
private void updateDropZoneSizesForSingleTask() {
final LinearLayout.LayoutParams dropZoneView1 =
(LayoutParams) mDropZoneView1.getLayoutParams();
final LinearLayout.LayoutParams dropZoneView2 =
(LayoutParams) mDropZoneView2.getLayoutParams();
dropZoneView1.width = MATCH_PARENT;
dropZoneView1.height = MATCH_PARENT;
dropZoneView2.width = 0;
dropZoneView2.height = 0;
dropZoneView1.weight = 1;
dropZoneView2.weight = 0;
mDropZoneView1.setLayoutParams(dropZoneView1);
mDropZoneView2.setLayoutParams(dropZoneView2);
}
/**
* Sets the size of the two drop zones based on the provided bounds. The divider sits between
* the views and its size is included in the calculations.
@@ -265,9 +298,12 @@ public class DragLayout extends LinearLayout {
// Animating to no target
animateSplitContainers(false, null /* animCompleteCallback */);
} else if (mCurrentTarget == null) {
// Animating to first target
animateSplitContainers(true, null /* animCompleteCallback */);
animateHighlight(target);
if (mPolicy.getNumTargets() == 1) {
animateFullscreenContainer(true);
} else {
animateSplitContainers(true, null /* animCompleteCallback */);
animateHighlight(target);
}
} else {
// Switching between targets
mDropZoneView1.animateSwitch();
@@ -283,6 +319,10 @@ public class DragLayout extends LinearLayout {
public void hide(DragEvent event, Runnable hideCompleteCallback) {
mIsShowing = false;
animateSplitContainers(false, hideCompleteCallback);
// Reset the state if we previously force-ignore the bottom margin
mDropZoneView1.setForceIgnoreBottomMargin(false);
mDropZoneView2.setForceIgnoreBottomMargin(false);
updateContainerMargins(getResources().getConfiguration().orientation);
mCurrentTarget = null;
}
@@ -297,11 +337,63 @@ public class DragLayout extends LinearLayout {
// Process the drop
mPolicy.handleDrop(mCurrentTarget, event.getClipData());
// TODO(b/169894807): Coordinate with dragSurface
// Start animating the drop UI out with the drag surface
hide(event, dropCompleteCallback);
hideDragSurface(dragSurface);
return handledDrop;
}
private void hideDragSurface(SurfaceControl dragSurface) {
final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
final ValueAnimator dragSurfaceAnimator = ValueAnimator.ofFloat(0f, 1f);
// Currently the splash icon animation runs with the default ValueAnimator duration of
// 300ms
dragSurfaceAnimator.setDuration(300);
dragSurfaceAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
dragSurfaceAnimator.addUpdateListener(animation -> {
float t = animation.getAnimatedFraction();
float alpha = 1f - t;
// TODO: Scale the drag surface as well once we make all the source surfaces
// consistent
tx.setAlpha(dragSurface, alpha);
tx.apply();
});
dragSurfaceAnimator.addListener(new AnimatorListenerAdapter() {
private boolean mCanceled = false;
@Override
public void onAnimationCancel(Animator animation) {
cleanUpSurface();
mCanceled = true;
}
@Override
public void onAnimationEnd(Animator animation) {
if (mCanceled) {
// Already handled above
return;
}
cleanUpSurface();
}
private void cleanUpSurface() {
// Clean up the drag surface
tx.remove(dragSurface);
tx.apply();
}
});
dragSurfaceAnimator.start();
}
private void animateFullscreenContainer(boolean visible) {
mStatusBarManager.disable(visible
? HIDE_STATUS_BAR_FLAGS
: DISABLE_NONE);
// We're only using the first drop zone if there is one fullscreen target
mDropZoneView1.setShowingMargin(visible);
mDropZoneView1.setShowingHighlight(visible);
}
private void animateSplitContainers(boolean visible, Runnable animCompleteCallback) {
mStatusBarManager.disable(visible
? HIDE_STATUS_BAR_FLAGS

View File

@@ -65,6 +65,7 @@ public class DropZoneView extends FrameLayout {
private final float[] mContainerMargin = new float[4];
private float mCornerRadius;
private float mBottomInset;
private boolean mIgnoreBottomMargin;
private int mMarginColor; // i.e. color used for negative space like the container insets
private boolean mShowingHighlight;
@@ -141,6 +142,14 @@ public class DropZoneView extends FrameLayout {
}
}
/** Ignores the bottom margin provided by the insets. */
public void setForceIgnoreBottomMargin(boolean ignoreBottomMargin) {
mIgnoreBottomMargin = ignoreBottomMargin;
if (mMarginPercent > 0) {
mMarginView.invalidate();
}
}
/** Sets the bottom inset so the drop zones are above bottom navigation. */
public void setBottomInset(float bottom) {
mBottomInset = bottom;
@@ -257,7 +266,8 @@ public class DropZoneView extends FrameLayout {
mPath.addRoundRect(mContainerMargin[0] * mMarginPercent,
mContainerMargin[1] * mMarginPercent,
getWidth() - (mContainerMargin[2] * mMarginPercent),
getHeight() - (mContainerMargin[3] * mMarginPercent) - mBottomInset,
getHeight() - (mContainerMargin[3] * mMarginPercent)
- (mIgnoreBottomMargin ? 0 : mBottomInset),
mCornerRadius * mMarginPercent,
mCornerRadius * mMarginPercent,
Path.Direction.CW);

View File

@@ -2407,4 +2407,7 @@
<string name="add_user_supervised" translatable="false">@*android:string/supervised_user_creation_label</string>
<!-- Manage users - For system user management [CHAR LIMIT=40] -->
<string name="manage_users">Manage users</string>
<!-- Toast shown when a notification does not support dragging to split [CHAR LIMIT=NONE] -->
<string name="drag_split_not_supported">This notification does not support dragging to Splitscreen.</string>
</resources>

View File

@@ -17,6 +17,11 @@
package com.android.systemui.statusbar.notification.row;
import static android.widget.Toast.LENGTH_SHORT;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.app.Notification;
import android.app.PendingIntent;
@@ -33,13 +38,15 @@ import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.view.DragEvent;
import android.view.HapticFeedbackConstants;
import android.view.SurfaceControl;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.VisibleForTesting;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.ShadeController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -55,12 +62,15 @@ public class ExpandableNotificationRowDragController {
private final Context mContext;
private final HeadsUpManager mHeadsUpManager;
private final ShadeController mShadeController;
@Inject
public ExpandableNotificationRowDragController(Context context,
HeadsUpManager headsUpManager) {
HeadsUpManager headsUpManager,
ShadeController shadeController) {
mContext = context;
mHeadsUpManager = headsUpManager;
mShadeController = shadeController;
init();
}
@@ -87,6 +97,16 @@ public class ExpandableNotificationRowDragController {
final PendingIntent contentIntent = notification.contentIntent != null
? notification.contentIntent
: notification.fullScreenIntent;
if (contentIntent == null) {
if (!enr.isPinned()) {
// We dismiss the shade for consistency, but also because toasts currently don't
// show above the shade
dismissShade();
}
Toast.makeText(mContext, R.string.drag_split_not_supported, LENGTH_SHORT)
.show();
return;
}
Bitmap iconBitmap = getBitmapFromDrawable(
getPkgIcon(enr.getEntry().getSbn().getPackageName()));
@@ -97,15 +117,30 @@ public class ExpandableNotificationRowDragController {
ClipDescription clipDescription = new ClipDescription("Drag And Drop",
new String[]{ClipDescription.MIMETYPE_APPLICATION_ACTIVITY});
Intent dragIntent = new Intent();
dragIntent.putExtra("android.intent.extra.PENDING_INTENT", contentIntent);
dragIntent.putExtra(ClipDescription.EXTRA_PENDING_INTENT, contentIntent);
dragIntent.putExtra(Intent.EXTRA_USER, android.os.Process.myUserHandle());
ClipData.Item item = new ClipData.Item(dragIntent);
ClipData dragData = new ClipData(clipDescription, item);
View.DragShadowBuilder myShadow = new View.DragShadowBuilder(snapshot);
view.setOnDragListener(getDraggedViewDragListener());
view.startDragAndDrop(dragData, myShadow, null, View.DRAG_FLAG_GLOBAL);
boolean result = view.startDragAndDrop(dragData, myShadow, null, View.DRAG_FLAG_GLOBAL
| View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION);
if (result) {
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
if (enr.isPinned()) {
mHeadsUpManager.releaseAllImmediately();
} else {
dismissShade();
}
}
}
private void dismissShade() {
// Speed up dismissing the shade since the drag needs to be handled by
// the shell layer underneath
mShadeController.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */,
false /* delayed */, 1.1f /* speedUpFactor */);
}
private Drawable getPkgIcon(String pkgName) {
Drawable pkgicon = null;
@@ -145,16 +180,6 @@ public class ExpandableNotificationRowDragController {
return (view, dragEvent) -> {
switch (dragEvent.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
if (view instanceof ExpandableNotificationRow) {
ExpandableNotificationRow enr = (ExpandableNotificationRow) view;
if (enr.isPinned()) {
mHeadsUpManager.releaseAllImmediately();
} else {
Dependency.get(ShadeController.class).animateCollapsePanels(
CommandQueue.FLAG_EXCLUDE_NONE, true /* force */);
}
}
return true;
case DragEvent.ACTION_DRAG_ENDED:
if (dragEvent.getResult()) {
@@ -162,10 +187,55 @@ public class ExpandableNotificationRowDragController {
ExpandableNotificationRow enr = (ExpandableNotificationRow) view;
enr.dragAndDropSuccess();
}
} else {
// Fade out the drag surface in place instead of animating back to the
// start position now that the shade is closed
fadeOutAndRemoveDragSurface(dragEvent);
}
// Clear the drag listener set above
view.setOnDragListener(null);
return true;
}
return false;
};
}
private void fadeOutAndRemoveDragSurface(DragEvent dragEvent) {
SurfaceControl dragSurface = dragEvent.getDragSurface();
SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
ValueAnimator returnAnimator = ValueAnimator.ofFloat(0f, 1f);
returnAnimator.setDuration(200);
returnAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
returnAnimator.addUpdateListener(animation -> {
float t = animation.getAnimatedFraction();
float alpha = 1f - t;
tx.setAlpha(dragSurface, alpha);
tx.apply();
});
returnAnimator.addListener(new AnimatorListenerAdapter() {
private boolean mCanceled = false;
@Override
public void onAnimationCancel(Animator animation) {
cleanUpSurface();
mCanceled = true;
}
@Override
public void onAnimationEnd(Animator animation) {
if (mCanceled) {
// Already handled above
return;
}
cleanUpSurface();
}
private void cleanUpSurface() {
tx.remove(dragSurface);
tx.apply();
tx.close();
}
});
returnAnimator.start();
}
}

View File

@@ -18,6 +18,8 @@ package com.android.systemui.statusbar.notification.row;
import static android.view.DragEvent.ACTION_DRAG_STARTED;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -36,7 +38,12 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
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.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -58,6 +65,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase {
private NotificationMenuRow mMenuRow = mock(NotificationMenuRow.class);
private NotificationMenuRowPlugin.MenuItem mMenuItem =
mock(NotificationMenuRowPlugin.MenuItem.class);
private ShadeController mShadeController = mock(ShadeController.class);
@Before
public void setUp() throws Exception {
@@ -69,11 +77,15 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase {
mContext,
mDependency,
TestableLooper.get(this));
mRow = mNotificationTestHelper.createRow();
mRow = spy(mNotificationTestHelper.createRow());
Notification notification = mRow.getEntry().getSbn().getNotification();
notification.contentIntent = mock(PendingIntent.class);
doReturn(true).when(mRow).startDragAndDrop(any(), any(), any(), anyInt());
mGroupRow = mNotificationTestHelper.createGroup(4);
when(mMenuRow.getLongpressMenuItem(any(Context.class))).thenReturn(mMenuItem);
mController = new ExpandableNotificationRowDragController(mContext, mHeadsUpManager);
mController = new ExpandableNotificationRowDragController(mContext, mHeadsUpManager,
mShadeController);
}
@Test
@@ -86,10 +98,6 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase {
mRow.doLongClickCallback(0, 0);
mRow.doDragCallback(0, 0);
verify(controller).startDragAndDrop(mRow);
// Simulate the drag start
mRow.dispatchDragEvent(DragEvent.obtain(ACTION_DRAG_STARTED, 0, 0, 0, 0, null, null, null,
null, null, false));
verify(mHeadsUpManager, times(1)).releaseAllImmediately();
}
@@ -98,14 +106,27 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase {
ExpandableNotificationRowDragController controller = createSpyController();
mRow.setDragController(controller);
mDependency.get(ShadeController.class).instantExpandNotificationsPanel();
mRow.doDragCallback(0, 0);
verify(controller).startDragAndDrop(mRow);
verify(mShadeController).animateCollapsePanels(eq(0), eq(true),
eq(false), anyFloat());
}
@Test
public void testDoStartDrag_noLaunchIntent() throws Exception {
ExpandableNotificationRowDragController controller = createSpyController();
mRow.setDragController(controller);
// Clear the intents
Notification notification = mRow.getEntry().getSbn().getNotification();
notification.contentIntent = null;
notification.fullScreenIntent = null;
mRow.doDragCallback(0, 0);
verify(controller).startDragAndDrop(mRow);
// Simulate the drag start
mRow.dispatchDragEvent(DragEvent.obtain(ACTION_DRAG_STARTED, 0, 0, 0, 0, null, null, null,
null, null, false));
verify(mDependency.get(ShadeController.class)).animateCollapsePanels(0, true);
// Verify that we never start the actual drag since there is no content
verify(mRow, never()).startDragAndDrop(any(), any(), any(), anyInt());
}
private ExpandableNotificationRowDragController createSpyController() {