From 12af953a69311fdee6ff59592408bc4a2fe3c9af Mon Sep 17 00:00:00 2001 From: "e.min" Date: Tue, 4 May 2021 12:52:36 +0900 Subject: [PATCH] [SystemUI] Add notification to window feature. Start apps intent through notification drag and drop. When user click the notification, its app activity was launched as a new task, but through this notification to window feature, user can launch app activity to popup(or split) window. Test: When get a HeadsUp notification > Long press it > open GUTS > drag beyond threshold Test: > start drag and drop > launch app as popup or split. Test: Open quick panel > long press any notification > open GUTS > drag beyond threshold Test: > start drag and drop > launch app as popup or split. Design doc : https://docs.google.com/document/d/1nr1-Wj3DLvOMZGP8X6hCekk7pbB2furvHML-gYormdc/edit?ts=6090accb#heading=h.1t3h5sf Bug: b/187095469 Signed-off-by: e.min Change-Id: Id7cd114e1afb7f484a4fa3db615a82d92d01a645 Merged-In: Id7cd114e1afb7f484a4fa3db615a82d92d01a645 --- packages/SystemUI/res/values/config.xml | 3 + packages/SystemUI/res/values/dimens.xml | 2 + .../src/com/android/systemui/SwipeHelper.java | 102 ++++++++--- .../NotificationActivityStarter.java | 4 + .../notification/NotificationClicker.java | 10 + .../row/ActivatableNotificationView.java | 18 +- .../row/ExpandableNotificationRow.java | 51 ++++++ .../ExpandableNotificationRowController.java | 11 +- ...pandableNotificationRowDragController.java | 171 ++++++++++++++++++ ...tificationStackScrollLayoutController.java | 41 ++++- .../stack/StackStateAnimator.java | 4 +- .../NotificationPanelViewController.java | 5 +- .../StatusBarNotificationActivityStarter.java | 48 +++++ ...ableNotificationRowDragControllerTest.java | 114 ++++++++++++ ...NotificationEntryManagerInflationTest.java | 3 +- 15 files changed, 545 insertions(+), 42 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index d664ddabd3f45..7fb0d83f2252c 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -671,4 +671,7 @@ 1 - Override the setting to always bypass keyguard 2 - Override the setting to never bypass keyguard --> 0 + + + false diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 2ee0026d2e01c..0816645527fc6 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1591,4 +1591,6 @@ 4dp 28dp + + 70dp diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java index affad7a57d868..b126cddf8eec1 100644 --- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java @@ -25,6 +25,8 @@ import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.Notification; +import android.app.PendingIntent; import android.content.res.Resources; import android.graphics.RectF; import android.os.Handler; @@ -73,6 +75,9 @@ public class SwipeHelper implements Gefingerpoken { private final FlingAnimationUtils mFlingAnimationUtils; private float mPagingTouchSlop; private final float mSlopMultiplier; + private int mTouchSlop; + private float mTouchSlopMultiplier; + private final Callback mCallback; private final int mSwipeDirection; private final VelocityTracker mVelocityTracker; @@ -105,6 +110,10 @@ public class SwipeHelper implements Gefingerpoken { final int y = (int) mDownLocation[1] - mViewOffset[1]; mTouchedView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); ((ExpandableNotificationRow) mTouchedView).doLongClickCallback(x, y); + + if (isAvailableToDragAndDrop(mTouchedView)) { + mCallback.onLongPressSent(mTouchedView); + } } } } @@ -126,6 +135,8 @@ public class SwipeHelper implements Gefingerpoken { mVelocityTracker = VelocityTracker.obtain(); mPagingTouchSlop = viewConfiguration.getScaledPagingTouchSlop(); mSlopMultiplier = viewConfiguration.getScaledAmbiguousGestureMultiplier(); + mTouchSlop = viewConfiguration.getScaledTouchSlop(); + mTouchSlopMultiplier = viewConfiguration.getAmbiguousGestureMultiplier(); // Extra long-press! mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f); @@ -297,7 +308,9 @@ public class SwipeHelper implements Gefingerpoken { mIsSwiping = false; mSnappingChild = false; mLongPressSent = false; + mCallback.onLongPressSent(null); mVelocityTracker.clear(); + cancelLongPress(); mTouchedView = mCallback.getChildAtPosition(ev); if (mTouchedView != null) { @@ -349,6 +362,7 @@ public class SwipeHelper implements Gefingerpoken { mIsSwiping = false; mTouchedView = null; mLongPressSent = false; + mCallback.onLongPressSent(null); mMenuRowIntercepting = false; cancelLongPress(); if (captured) return true; @@ -593,11 +607,7 @@ public class SwipeHelper implements Gefingerpoken { @Override public boolean onTouchEvent(MotionEvent ev) { - if (mLongPressSent && !mMenuRowIntercepting) { - return true; - } - - if (!mIsSwiping && !mMenuRowIntercepting) { + if (!mIsSwiping && !mMenuRowIntercepting && !mLongPressSent) { if (mCallback.getChildAtPosition(ev) != null) { // We are dragging directly over a card, make sure that we also catch the gesture // even if nobody else wants the touch event. @@ -623,30 +633,40 @@ public class SwipeHelper implements Gefingerpoken { if (absDelta >= getFalsingThreshold()) { mTouchAboveFalsingThreshold = true; } - // don't let items that can't be dismissed be dragged more than - // maxScrollDistance - if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissedInDirection( - mTouchedView, - delta > 0)) { - float size = getSize(mTouchedView); - float maxScrollDistance = MAX_SCROLL_SIZE_FRACTION * size; - if (absDelta >= size) { - delta = delta > 0 ? maxScrollDistance : -maxScrollDistance; - } else { - int startPosition = mCallback.getConstrainSwipeStartPosition(); - if (absDelta > startPosition) { - int signedStartPosition = - (int) (startPosition * Math.signum(delta)); - delta = signedStartPosition - + maxScrollDistance * (float) Math.sin( - ((delta - signedStartPosition) / size) * (Math.PI / 2)); + + if (mLongPressSent) { + if (absDelta >= getTouchSlop(ev)) { + if (mTouchedView instanceof ExpandableNotificationRow) { + ((ExpandableNotificationRow) mTouchedView) + .doDragCallback(ev.getX(), ev.getY()); + } + } + } else { + // don't let items that can't be dismissed be dragged more than + // maxScrollDistance + if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissedInDirection( + mTouchedView, + delta > 0)) { + float size = getSize(mTouchedView); + float maxScrollDistance = MAX_SCROLL_SIZE_FRACTION * size; + if (absDelta >= size) { + delta = delta > 0 ? maxScrollDistance : -maxScrollDistance; + } else { + int startPosition = mCallback.getConstrainSwipeStartPosition(); + if (absDelta > startPosition) { + int signedStartPosition = + (int) (startPosition * Math.signum(delta)); + delta = signedStartPosition + + maxScrollDistance * (float) Math.sin( + ((delta - signedStartPosition) / size) * (Math.PI / 2)); + } } } - } - setTranslation(mTouchedView, mTranslation + delta); - updateSwipeProgressFromOffset(mTouchedView, mCanCurrViewBeDimissed); - onMoveUpdate(mTouchedView, ev, mTranslation + delta, delta); + setTranslation(mTouchedView, mTranslation + delta); + updateSwipeProgressFromOffset(mTouchedView, mCanCurrViewBeDimissed); + onMoveUpdate(mTouchedView, ev, mTranslation + delta, delta); + } } break; case MotionEvent.ACTION_UP: @@ -747,6 +767,29 @@ public class SwipeHelper implements Gefingerpoken { mIsSwiping = false; } + private float getTouchSlop(MotionEvent event) { + // Adjust the touch slop if another gesture may be being performed. + return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE + ? mTouchSlop * mTouchSlopMultiplier + : mTouchSlop; + } + + private boolean isAvailableToDragAndDrop(View v) { + if (v.getResources().getBoolean(R.bool.config_notificationToContents)) { + if (v instanceof ExpandableNotificationRow) { + ExpandableNotificationRow enr = (ExpandableNotificationRow) v; + boolean canBubble = enr.getEntry().canBubble(); + Notification notif = enr.getEntry().getSbn().getNotification(); + PendingIntent dragIntent = notif.contentIntent != null ? notif.contentIntent + : notif.fullScreenIntent; + if (dragIntent != null && dragIntent.isActivity() && !canBubble) { + return true; + } + } + } + return false; + } + public interface Callback { View getChildAtPosition(MotionEvent ev); @@ -770,6 +813,13 @@ public class SwipeHelper implements Gefingerpoken { void onDragCancelled(View v); + /** + * Called when the child is long pressed and available to start drag and drop. + * + * @param v the view that was long pressed. + */ + void onLongPressSent(View v); + /** * Called when the child is snapped to a position. * diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java index 2537b19513d27..129fa5a7cc179 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java @@ -20,6 +20,7 @@ import android.content.Intent; import android.service.notification.StatusBarNotification; import android.view.View; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; /** @@ -37,6 +38,9 @@ public interface NotificationActivityStarter { /** Called when the user clicks "Manage" or "History" in the Shade. */ void startHistoryIntent(View view, boolean showHistory); + /** Called when the user succeed to drop notification to proper target view. */ + void onDragSuccess(NotificationEntry entry); + default boolean isCollapsingToShowActivityOverLockscreen() { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java index 0fb1c54bb1500..da706215863e0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java @@ -43,6 +43,14 @@ public final class NotificationClicker implements View.OnClickListener { private final Optional mBubblesOptional; private final NotificationActivityStarter mNotificationActivityStarter; + private ExpandableNotificationRow.OnDragSuccessListener mOnDragSuccessListener = + new ExpandableNotificationRow.OnDragSuccessListener() { + @Override + public void onDragSuccess(NotificationEntry entry) { + mNotificationActivityStarter.onDragSuccess(entry); + } + }; + private NotificationClicker( NotificationClickerLogger logger, Optional statusBarOptional, @@ -111,8 +119,10 @@ public final class NotificationClicker implements View.OnClickListener { if (notification.contentIntent != null || notification.fullScreenIntent != null || row.getEntry().isBubble()) { row.setOnClickListener(this); + row.setOnDragSuccessListener(mOnDragSuccessListener); } else { row.setOnClickListener(null); + row.setOnDragSuccessListener(null); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index 7a71ee1d154e9..acb0e82c24f2e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; +import android.graphics.Point; import android.graphics.RectF; import android.util.AttributeSet; import android.util.MathUtils; @@ -139,6 +140,8 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView private boolean mIsHeadsUpAnimation; private int mHeadsUpAddStartLocation; private float mHeadsUpLocation; + /* In order to track headsup longpress coorindate. */ + protected Point mTargetPoint; private boolean mIsAppearing; private boolean mDismissed; private boolean mRefocusOnDismiss; @@ -568,8 +571,19 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView final int actualHeight = getActualHeight(); float bottom = actualHeight * interpolatedFraction; - setOutlineRect(0, mAppearAnimationTranslation, getWidth(), - bottom + mAppearAnimationTranslation); + if (mTargetPoint != null) { + int width = getWidth(); + float fraction = 1 - mAppearAnimationFraction; + + setOutlineRect(mTargetPoint.x * fraction, + mAnimationTranslationY + + (mAnimationTranslationY - mTargetPoint.y) * fraction, + width - (width - mTargetPoint.x) * fraction, + actualHeight - (actualHeight - mTargetPoint.y) * fraction); + } else { + setOutlineRect(0, mAppearAnimationTranslation, getWidth(), + bottom + mAppearAnimationTranslation); + } } private float getInterpolatedAppearAnimationFraction() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 73bb6cd9ba1ce..0d8e850946464 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -37,6 +37,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Path; +import android.graphics.Point; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.AnimationDrawable; import android.graphics.drawable.ColorDrawable; @@ -260,6 +261,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView // Use #setLongPressPosition to optionally assign positional data with the long press. private LongPressListener mLongPressListener; + private ExpandableNotificationRowDragController mDragController; + private boolean mGroupExpansionChanging; /** @@ -331,6 +334,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } }; private OnClickListener mOnClickListener; + private OnDragSuccessListener mOnDragSuccessListener; private boolean mHeadsupDisappearRunning; private View mChildAfterViewWhenDismissed; private View mGroupParentWhenDismissed; @@ -1083,6 +1087,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mLongPressListener = longPressListener; } + public void setDragController(ExpandableNotificationRowDragController dragController) { + mDragController = dragController; + } + @Override public void setOnClickListener(@Nullable OnClickListener l) { super.setOnClickListener(l); @@ -1329,6 +1337,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public void dismiss(boolean refocusOnDismiss) { super.dismiss(refocusOnDismiss); setLongPressListener(null); + setDragController(null); mGroupParentWhenDismissed = mNotificationParent; mChildAfterViewWhenDismissed = null; mEntry.getIcons().getStatusBarIcon().setDismissed(); @@ -1637,6 +1646,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } onHeightReset(); requestLayout(); + + setTargetPoint(null); } public void showFeedbackIcon(boolean show, Pair resIds) { @@ -1727,6 +1738,29 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mTranslateableViews.remove(mGutsStub); } + /** + * Called once when starting drag motion after opening notification guts, + * in case of notification that has {@link android.app.Notification#contentIntent} + * and it is to start an activity. + */ + public void doDragCallback(float x, float y) { + if (mDragController != null) { + setTargetPoint(new Point((int) x, (int) y)); + mDragController.startDragAndDrop(this); + } + } + + public void setOnDragSuccessListener(OnDragSuccessListener listener) { + mOnDragSuccessListener = listener; + } + + /** + * Called when a notification is dropped on proper target window. + */ + public void dragAndDropSuccess() { + mOnDragSuccessListener.onDragSuccess(getEntry()); + } + private void doLongClickCallback() { doLongClickCallback(getWidth() / 2, getHeight() / 2); } @@ -3254,6 +3288,16 @@ public class ExpandableNotificationRow extends ActivatableNotificationView boolean onLongPress(View v, int x, int y, MenuItem item); } + /** + * Called when notification drag and drop is finished successfully. + */ + public interface OnDragSuccessListener { + /** + * @param entry NotificationEntry that succeed to drop on proper target window. + */ + void onDragSuccess(NotificationEntry entry); + } + /** * Equivalent to View.OnClickListener with coordinates */ @@ -3321,4 +3365,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } } + + private void setTargetPoint(Point p) { + mTargetPoint = p; + } + public Point getTargetPoint() { + return mTargetPoint; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index c9fcdac8e45f9..0662a1eba8b6b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -25,6 +25,7 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; +import com.android.systemui.R; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; @@ -85,6 +86,8 @@ public class ExpandableNotificationRowController implements NodeController { private final PeopleNotificationIdentifier mPeopleNotificationIdentifier; private final Optional mBubblesManagerOptional; + private final ExpandableNotificationRowDragController mDragController; + @Inject public ExpandableNotificationRowController( ExpandableNotificationRow view, @@ -109,7 +112,8 @@ public class ExpandableNotificationRowController implements NodeController { FalsingManager falsingManager, FalsingCollector falsingCollector, PeopleNotificationIdentifier peopleNotificationIdentifier, - Optional bubblesManagerOptional) { + Optional bubblesManagerOptional, + ExpandableNotificationRowDragController dragController) { mView = view; mListContainer = listContainer; mActivatableNotificationViewController = activatableNotificationViewController; @@ -134,6 +138,7 @@ public class ExpandableNotificationRowController implements NodeController { mFalsingCollector = falsingCollector; mPeopleNotificationIdentifier = peopleNotificationIdentifier; mBubblesManagerOptional = bubblesManagerOptional; + mDragController = dragController; } /** @@ -164,6 +169,10 @@ public class ExpandableNotificationRowController implements NodeController { ); mView.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); if (mAllowLongPress) { + if (mView.getResources().getBoolean(R.bool.config_notificationToContents)) { + mView.setDragController(mDragController); + } + mView.setLongPressListener((v, x, y, item) -> { if (mView.isSummaryWithChildren()) { mView.expandNotification(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java new file mode 100644 index 0000000000000..06b739b33e777 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2021 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.systemui.statusbar.notification.row; + +import android.annotation.NonNull; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.service.notification.StatusBarNotification; +import android.util.Log; +import android.view.DragEvent; +import android.view.HapticFeedbackConstants; +import android.view.View; +import android.widget.ImageView; + +import androidx.annotation.VisibleForTesting; + +import com.android.systemui.Dependency; +import com.android.systemui.R; +import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.phone.ShadeController; +import com.android.systemui.statusbar.policy.HeadsUpManager; + +import javax.inject.Inject; + +/** + * Controller for Notification to window. + */ +public class ExpandableNotificationRowDragController { + private static final String TAG = ExpandableNotificationRowDragController.class.getSimpleName(); + private int mIconSize; + + private final Context mContext; + private final HeadsUpManager mHeadsUpManager; + + @Inject + public ExpandableNotificationRowDragController(Context context, + HeadsUpManager headsUpManager) { + mContext = context; + mHeadsUpManager = headsUpManager; + + init(); + } + + private void init() { + mIconSize = mContext.getResources().getDimensionPixelSize(R.dimen.drag_and_drop_icon_size); + } + + /** + * Called when drag event beyond the touchslop, + * and start drag and drop. + * + * @param view notification that was long pressed and started to drag and drop. + */ + @VisibleForTesting + public void startDragAndDrop(View view) { + ExpandableNotificationRow enr = null; + if (view instanceof ExpandableNotificationRow) { + enr = (ExpandableNotificationRow) view; + } + + StatusBarNotification sn = enr.getEntry().getSbn(); + Notification notification = sn.getNotification(); + final PendingIntent contentIntent = notification.contentIntent != null + ? notification.contentIntent + : notification.fullScreenIntent; + Bitmap iconBitmap = getBitmapFromDrawable( + getPkgIcon(enr.getEntry().getSbn().getPackageName())); + + final ImageView snapshot = new ImageView(mContext); + snapshot.setImageBitmap(iconBitmap); + snapshot.layout(0, 0, mIconSize, mIconSize); + + 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(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); + } + + + private Drawable getPkgIcon(String pkgName) { + Drawable pkgicon = null; + PackageManager pm = mContext.getPackageManager(); + ApplicationInfo info; + try { + info = pm.getApplicationInfo( + pkgName, + PackageManager.MATCH_UNINSTALLED_PACKAGES + | PackageManager.MATCH_DISABLED_COMPONENTS + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE + | PackageManager.MATCH_DIRECT_BOOT_AWARE); + if (info != null) { + pkgicon = pm.getApplicationIcon(info); + } else { + Log.d(TAG, " application info is null "); + pkgicon = pm.getDefaultActivityIcon(); + } + } catch (PackageManager.NameNotFoundException e) { + Log.d(TAG, "can not find package with : " + pkgName); + pkgicon = pm.getDefaultActivityIcon(); + } + + return pkgicon; + } + + private Bitmap getBitmapFromDrawable(@NonNull Drawable drawable) { + final Bitmap bmp = Bitmap.createBitmap(drawable.getIntrinsicWidth(), + drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bmp); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bmp; + } + + private View.OnDragListener getDraggedViewDragListener() { + 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()) { + if (view instanceof ExpandableNotificationRow) { + ExpandableNotificationRow enr = (ExpandableNotificationRow) view; + enr.dragAndDropSuccess(); + } + } + return true; + } + return false; + }; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 417418af477f8..8b55a118acea6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -183,6 +183,8 @@ public class NotificationStackScrollLayoutController { private int mBarState; private HeadsUpAppearanceController mHeadsUpAppearanceController; + private View mLongPressedView; + private final NotificationListContainerImpl mNotificationListContainer = new NotificationListContainerImpl(); @@ -491,6 +493,11 @@ public class NotificationStackScrollLayoutController { return child; } + @Override + public void onLongPressSent(View v) { + mLongPressedView = v; + } + @Override public void onBeginDrag(View v) { mFalsingCollector.onNotificationStartDismissing(); @@ -1425,6 +1432,10 @@ public class NotificationStackScrollLayoutController { return mDynamicPrivacyController.isInLockedDownShade(); } + public boolean isLongPressInProgress() { + return mLongPressedView != null; + } + /** * Set the dimmed state for all of the notification views. */ @@ -1679,17 +1690,23 @@ public class NotificationStackScrollLayoutController { mView.handleEmptySpaceClick(ev); NotificationGuts guts = mNotificationGutsManager.getExposedGuts(); + + boolean longPressWantsIt = false; + if (mLongPressedView != null) { + longPressWantsIt = mSwipeHelper.onInterceptTouchEvent(ev); + } boolean expandWantsIt = false; - if (!mSwipeHelper.isSwiping() + if (mLongPressedView == null && !mSwipeHelper.isSwiping() && !mView.getOnlyScrollingInThisMotion() && guts == null) { expandWantsIt = mView.getExpandHelper().onInterceptTouchEvent(ev); } boolean scrollWantsIt = false; - if (!mSwipeHelper.isSwiping() && !mView.isExpandingNotification()) { + if (mLongPressedView == null && !mSwipeHelper.isSwiping() + && !mView.isExpandingNotification()) { scrollWantsIt = mView.onInterceptTouchEventScroll(ev); } boolean swipeWantsIt = false; - if (!mView.isBeingDragged() + if (mLongPressedView == null && !mView.isBeingDragged() && !mView.isExpandingNotification() && !mView.getExpandedInThisMotion() && !mView.getOnlyScrollingInThisMotion() @@ -1717,7 +1734,7 @@ public class NotificationStackScrollLayoutController { InteractionJankMonitor.getInstance().begin(mView, CUJ_NOTIFICATION_SHADE_SCROLL_FLING); } - return swipeWantsIt || scrollWantsIt || expandWantsIt; + return swipeWantsIt || scrollWantsIt || expandWantsIt || longPressWantsIt; } @Override @@ -1726,11 +1743,15 @@ public class NotificationStackScrollLayoutController { boolean isCancelOrUp = ev.getActionMasked() == MotionEvent.ACTION_CANCEL || ev.getActionMasked() == MotionEvent.ACTION_UP; mView.handleEmptySpaceClick(ev); + boolean longPressWantsIt = false; + if (guts != null && mLongPressedView != null) { + longPressWantsIt = mSwipeHelper.onTouchEvent(ev); + } boolean expandWantsIt = false; boolean onlyScrollingInThisMotion = mView.getOnlyScrollingInThisMotion(); boolean expandingNotification = mView.isExpandingNotification(); - if (mView.getIsExpanded() && !mSwipeHelper.isSwiping() && !onlyScrollingInThisMotion - && guts == null) { + if (mLongPressedView == null && mView.getIsExpanded() + && !mSwipeHelper.isSwiping() && !onlyScrollingInThisMotion && guts == null) { ExpandHelper expandHelper = mView.getExpandHelper(); if (isCancelOrUp) { expandHelper.onlyObserveMovements(false); @@ -1744,12 +1765,12 @@ public class NotificationStackScrollLayoutController { } } boolean scrollerWantsIt = false; - if (mView.isExpanded() && !mSwipeHelper.isSwiping() && !expandingNotification - && !mView.getDisallowScrollingInThisMotion()) { + if (mLongPressedView == null && mView.isExpanded() && !mSwipeHelper.isSwiping() + && !expandingNotification && !mView.getDisallowScrollingInThisMotion()) { scrollerWantsIt = mView.onScrollTouch(ev); } boolean horizontalSwipeWantsIt = false; - if (!mView.isBeingDragged() + if (mLongPressedView == null && !mView.isBeingDragged() && !expandingNotification && !mView.getExpandedInThisMotion() && !onlyScrollingInThisMotion @@ -1775,7 +1796,7 @@ public class NotificationStackScrollLayoutController { mView.setCheckForLeaveBehind(true); } traceJankOnTouchEvent(ev.getActionMasked(), scrollerWantsIt); - return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt; + return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || longPressWantsIt; } private void traceJankOnTouchEvent(int action, boolean scrollerWantsIt) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java index ee12b4b2d728c..2702bf7d31da6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java @@ -433,6 +433,7 @@ public class StackStateAnimator { if (row.isDismissed()) { needsAnimation = false; } + NotificationEntry entry = row.getEntry(); StatusBarIconView icon = entry.getIcons().getStatusBarIcon(); final StatusBarIconView centeredIcon = entry.getIcons().getCenteredIcon(); @@ -442,7 +443,8 @@ public class StackStateAnimator { if (icon.getParent() != null) { icon.getLocationOnScreen(mTmpLocation); float iconPosition = mTmpLocation[0] - icon.getTranslationX() - + ViewState.getFinalTranslationX(icon) + icon.getWidth() * 0.25f; + + ViewState.getFinalTranslationX(icon) + + icon.getWidth() * 0.25f; mHostLayout.getLocationOnScreen(mTmpLocation); targetLocation = iconPosition - mTmpLocation[0]; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index 3b65ce06da995..93f56875da018 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -3928,7 +3928,9 @@ public class NotificationPanelViewController extends PanelViewController { if (mStatusBar.isBouncerShowing()) { return true; } - if (mBar.panelEnabled() && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) { + if (mBar.panelEnabled() + && !mNotificationStackScrollLayoutController.isLongPressInProgress() + && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) { mMetricsLogger.count(COUNTER_PANEL_OPEN, 1); mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1); return true; @@ -3980,6 +3982,7 @@ public class NotificationPanelViewController extends PanelViewController { return true; } if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp() + && !mNotificationStackScrollLayoutController.isLongPressInProgress() && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) { mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 8873fbf111ba2..dba3b2418790c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -71,6 +71,7 @@ import com.android.systemui.statusbar.notification.collection.render.GroupMember import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowDragController; import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback; import com.android.systemui.statusbar.policy.HeadsUpUtil; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -402,6 +403,53 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mIsCollapsingToShowActivityOverLockscreen = false; } + /** + * Called when a notification is dropped on proper target window. + * Intent that is included in this entry notification, + * will be sent by {@link ExpandableNotificationRowDragController} + * + * @param entry notification entry that is dropped. + */ + @Override + public void onDragSuccess(NotificationEntry entry) { + // this method is not responsible for intent sending. + // will focus follow operation only after drag-and-drop that notification. + NotificationVisibility.NotificationLocation location = + NotificationLogger.getNotificationLocation(entry); + final NotificationVisibility nv = NotificationVisibility.obtain(entry.getKey(), + entry.getRanking().getRank(), getVisibleNotificationsCount(), true, location); + + // retrieve the group summary to remove with this entry before we tell NMS the + // notification was clicked to avoid a race condition + final boolean shouldAutoCancel = shouldAutoCancel(entry.getSbn()); + final NotificationEntry summaryToRemove = shouldAutoCancel + ? mOnUserInteractionCallback.getGroupSummaryToDismiss(entry) : null; + + String notificationKey = entry.getKey(); + // inform NMS that the notification was clicked + mClickNotifier.onNotificationClick(notificationKey, nv); + + if (shouldAutoCancel || mRemoteInputManager.isNotificationKeptForRemoteInputHistory( + notificationKey)) { + // Immediately remove notification from visually showing. + // We have to post the removal to the UI thread for synchronization. + mMainThreadHandler.post(() -> { + final Runnable removeNotification = () -> + mOnUserInteractionCallback.onDismiss( + entry, REASON_CLICK, summaryToRemove); + if (mPresenter.isCollapsing()) { + // To avoid lags we're only performing the remove + // after the shade is collapsed + mShadeController.addPostCollapseAction(removeNotification); + } else { + removeNotification.run(); + } + }); + } + + mIsCollapsingToShowActivityOverLockscreen = false; + } + private void expandBubbleStackOnMainThread(NotificationEntry entry) { if (!mBubblesManagerOptional.isPresent()) { return; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java new file mode 100644 index 0000000000000..24a0ad3de196f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2021 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.systemui.statusbar.notification.row; + +import static android.view.DragEvent.ACTION_DRAG_STARTED; + +import android.content.Context; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.testing.TestableLooper.RunWithLooper; +import android.view.DragEvent; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; +import com.android.systemui.statusbar.phone.ShadeController; +import com.android.systemui.statusbar.policy.HeadsUpManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@RunWithLooper +public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase { + + private ExpandableNotificationRow mRow; + private ExpandableNotificationRow mGroupRow; + private ExpandableNotificationRowDragController mController; + private NotificationTestHelper mNotificationTestHelper; + + private NotificationGutsManager mGutsManager = mock(NotificationGutsManager.class); + private HeadsUpManager mHeadsUpManager = mock(HeadsUpManager.class); + private NotificationMenuRow mMenuRow = mock(NotificationMenuRow.class); + private NotificationMenuRowPlugin.MenuItem mMenuItem = + mock(NotificationMenuRowPlugin.MenuItem.class); + + @Before + public void setUp() throws Exception { + allowTestableLooperAsMainThread(); + + mDependency.injectMockDependency(ShadeController.class); + + mNotificationTestHelper = new NotificationTestHelper( + mContext, + mDependency, + TestableLooper.get(this)); + mRow = mNotificationTestHelper.createRow(); + mGroupRow = mNotificationTestHelper.createGroup(4); + when(mMenuRow.getLongpressMenuItem(any(Context.class))).thenReturn(mMenuItem); + + mController = new ExpandableNotificationRowDragController(mContext, mHeadsUpManager); + } + + @Test + public void testDoStartDragHeadsUpNotif_startDragAndDrop() throws Exception { + ExpandableNotificationRowDragController controller = createSpyController(); + mRow.setDragController(controller); + mRow.setHeadsUp(true); + mRow.setPinned(true); + + 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(); + } + + @Test + public void testDoStartDragNotif() throws Exception { + ExpandableNotificationRowDragController controller = createSpyController(); + mRow.setDragController(controller); + + mDependency.get(ShadeController.class).instantExpandNotificationsPanel(); + 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); + } + + private ExpandableNotificationRowDragController createSpyController() { + return spy(mController); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java index 0854b9377eba5..4562e4f5954d0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java @@ -265,7 +265,8 @@ public class NotificationEntryManagerInflationTest extends SysuiTestCase { new FalsingManagerFake(), new FalsingCollectorFake(), mPeopleNotificationIdentifier, - Optional.of(mock(BubblesManager.class)) + Optional.of(mock(BubblesManager.class)), + mock(ExpandableNotificationRowDragController.class) )); when(mNotificationRowComponentBuilder.activatableNotificationView(any()))