diff options
4 files changed, 1224 insertions, 536 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java index a3b539588d9b..0215fda81485 100644 --- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java @@ -61,13 +61,14 @@ public class SwipeHelper implements Gefingerpoken { public static final float SWIPED_FAR_ENOUGH_SIZE_FRACTION = 0.6f; static final float MAX_SCROLL_SIZE_FRACTION = 0.3f; + protected final Handler mHandler; + private float mMinSwipeProgress = 0f; private float mMaxSwipeProgress = 1f; private final FlingAnimationUtils mFlingAnimationUtils; private float mPagingTouchSlop; private final Callback mCallback; - private final Handler mHandler; private final int mSwipeDirection; private final VelocityTracker mVelocityTracker; private final FalsingManager mFalsingManager; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 72c2c0bec31f..9978ec364cdb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -87,7 +87,6 @@ import com.android.systemui.classifier.FalsingManager; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem; -import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.DragDownHelper.DragDownCallback; @@ -109,6 +108,7 @@ import com.android.systemui.statusbar.notification.NotificationData; import com.android.systemui.statusbar.notification.row.NotificationGuts; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.NotificationShelf; +import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.row.NotificationSnooze; import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; import com.android.systemui.statusbar.StatusBarStateController; @@ -146,11 +146,9 @@ import java.util.function.BiConsumer; * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack. */ public class NotificationStackScrollLayout extends ViewGroup - implements Callback, ExpandHelper.Callback, ScrollAdapter, - OnHeightChangedListener, OnGroupChangeListener, - OnMenuEventListener, VisibilityLocationProvider, - NotificationListContainer, ConfigurationListener, DragDownCallback, AnimationStateHandler, - Dumpable { + implements ExpandHelper.Callback, ScrollAdapter, OnHeightChangedListener, + OnGroupChangeListener, VisibilityLocationProvider, NotificationListContainer, + ConfigurationListener, DragDownCallback, AnimationStateHandler, Dumpable { public static final float BACKGROUND_ALPHA_DIMMED = 0.7f; private static final String TAG = "StackScroller"; @@ -164,7 +162,7 @@ public class NotificationStackScrollLayout extends ViewGroup private static final int INVALID_POINTER = -1; private ExpandHelper mExpandHelper; - private NotificationSwipeHelper mSwipeHelper; + private final NotificationSwipeHelper mSwipeHelper; private boolean mSwipingInProgress; private int mCurrentStackHeight = Integer.MAX_VALUE; private final Paint mBackgroundPaint = new Paint(); @@ -291,10 +289,6 @@ public class NotificationStackScrollLayout extends ViewGroup */ private int mMaxScrollAfterExpand; private ExpandableNotificationRow.LongPressListener mLongPressListener; - - private NotificationMenuRowPlugin mCurrMenuRow; - private View mTranslatingParentView; - private View mMenuExposedView; boolean mCheckForLeavebehind; /** @@ -466,6 +460,9 @@ public class NotificationStackScrollLayout extends ViewGroup private Interpolator mDarkXInterpolator = Interpolators.FAST_OUT_SLOW_IN; private NotificationPanelView mNotificationPanel; + private final NotificationGutsManager + mNotificationGutsManager = Dependency.get(NotificationGutsManager.class); + @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) public NotificationStackScrollLayout(Context context) { this(context, null); @@ -495,7 +492,8 @@ public class NotificationStackScrollLayout extends ViewGroup minHeight, maxHeight); mExpandHelper.setEventSource(this); mExpandHelper.setScrollAdapter(this); - mSwipeHelper = new NotificationSwipeHelper(SwipeHelper.X, this, getContext()); + mSwipeHelper = new NotificationSwipeHelper(SwipeHelper.X, new SwipeHelperCallback(), + getContext(), new NotificationMenuListener()); mStackScrollAlgorithm = createStackScrollAlgorithm(context); initView(context); mFalsingManager = FalsingManager.getInstance(context); @@ -639,41 +637,6 @@ public class NotificationStackScrollLayout extends ViewGroup } @Override - @ShadeViewRefactor(RefactorComponent.INPUT) - public void onMenuClicked(View view, int x, int y, MenuItem item) { - if (mLongPressListener == null) { - return; - } - if (view instanceof ExpandableNotificationRow) { - ExpandableNotificationRow row = (ExpandableNotificationRow) view; - MetricsLogger.action(mContext, MetricsEvent.ACTION_TOUCH_GEAR, - row.getStatusBarNotification().getPackageName()); - } - mLongPressListener.onLongPress(view, x, y, item); - } - - @Override - @ShadeViewRefactor(RefactorComponent.INPUT) - public void onMenuReset(View row) { - if (mTranslatingParentView != null && row == mTranslatingParentView) { - mMenuExposedView = null; - mTranslatingParentView = null; - } - } - - @Override - @ShadeViewRefactor(RefactorComponent.INPUT) - public void onMenuShown(View row) { - mMenuExposedView = mTranslatingParentView; - if (row instanceof ExpandableNotificationRow) { - MetricsLogger.action(mContext, MetricsEvent.ACTION_REVEAL_GEAR, - ((ExpandableNotificationRow) row).getStatusBarNotification() - .getPackageName()); - } - mSwipeHelper.onMenuShown(row); - } - - @Override @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) public void onUiModeChanged() { mBgColor = mContext.getColor(R.color.notification_shade_background_color); @@ -1295,111 +1258,6 @@ public class NotificationStackScrollLayout extends ViewGroup mQsContainer = qsContainer; } - /** - * Handles cleanup after the given {@code view} has been fully swiped out (including - * re-invoking dismiss logic in case the notification has not made its way out yet). - */ - @Override - @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) - public void onChildDismissed(View view) { - ExpandableNotificationRow row = (ExpandableNotificationRow) view; - if (!row.isDismissed()) { - handleChildViewDismissed(view); - } - ViewGroup transientContainer = row.getTransientContainer(); - if (transientContainer != null) { - transientContainer.removeTransientView(view); - } - } - - /** - * Starts up notification dismiss and tells the notification, if any, to remove itself from - * layout. - * - * @param view view (e.g. notification) to dismiss from the layout - */ - - @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) - private void handleChildViewDismissed(View view) { - if (mDismissAllInProgress) { - return; - } - - boolean isBlockingHelperShown = false; - - setSwipingInProgress(false); - if (mDragAnimPendingChildren.contains(view)) { - // We start the swipe and finish it in the same frame; we don't want a drag animation. - mDragAnimPendingChildren.remove(view); - } - mAmbientState.onDragFinished(view); - updateContinuousShadowDrawing(); - - if (view instanceof ExpandableNotificationRow) { - ExpandableNotificationRow row = (ExpandableNotificationRow) view; - if (row.isHeadsUp()) { - mHeadsUpManager.addSwipedOutNotification(row.getStatusBarNotification().getKey()); - } - isBlockingHelperShown = - row.performDismissWithBlockingHelper(false /* fromAccessibility */); - } - - if (!isBlockingHelperShown) { - mSwipedOutViews.add(view); - } - mFalsingManager.onNotificationDismissed(); - if (mFalsingManager.shouldEnforceBouncer()) { - mStatusBar.executeRunnableDismissingKeyguard( - null, - null /* cancelAction */, - false /* dismissShade */, - true /* afterKeyguardGone */, - false /* deferred */); - } - } - - @Override - @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) - public void onChildSnappedBack(View animView, float targetLeft) { - mAmbientState.onDragFinished(animView); - updateContinuousShadowDrawing(); - if (!mDragAnimPendingChildren.contains(animView)) { - if (mAnimationsEnabled) { - mSnappedBackChildren.add(animView); - mNeedsAnimation = true; - } - requestChildrenUpdate(); - } else { - // We start the swipe and snap back in the same frame, we don't want any animation - mDragAnimPendingChildren.remove(animView); - } - if (mCurrMenuRow != null && targetLeft == 0) { - mCurrMenuRow.resetMenu(); - mCurrMenuRow = null; - } - } - - @Override - @ShadeViewRefactor(RefactorComponent.INPUT) - public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) { - // Returning true prevents alpha fading. - return !mFadeNotificationsOnDismiss; - } - - @Override - @ShadeViewRefactor(RefactorComponent.INPUT) - public void onBeginDrag(View v) { - mFalsingManager.onNotificatonStartDismissing(); - setSwipingInProgress(true); - mAmbientState.onBeginDrag(v); - updateContinuousShadowDrawing(); - if (mAnimationsEnabled && (mIsExpanded || !isPinnedHeadsUp(v))) { - mDragAnimPendingChildren.add(v); - mNeedsAnimation = true; - } - requestChildrenUpdate(); - } - @ShadeViewRefactor(RefactorComponent.ADAPTER) public static boolean isPinnedHeadsUp(View v) { if (v instanceof ExpandableNotificationRow) { @@ -1418,41 +1276,6 @@ public class NotificationStackScrollLayout extends ViewGroup return false; } - @Override - @ShadeViewRefactor(RefactorComponent.INPUT) - public void onDragCancelled(View v) { - mFalsingManager.onNotificatonStopDismissing(); - setSwipingInProgress(false); - } - - @Override - @ShadeViewRefactor(RefactorComponent.INPUT) - public float getFalsingThresholdFactor() { - return mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f; - } - - @Override - @ShadeViewRefactor(RefactorComponent.INPUT) - public View getChildAtPosition(MotionEvent ev) { - View child = getChildAtPosition(ev.getX(), ev.getY()); - if (child instanceof ExpandableNotificationRow) { - ExpandableNotificationRow row = (ExpandableNotificationRow) child; - ExpandableNotificationRow parent = row.getNotificationParent(); - if (parent != null && parent.areChildrenExpanded() - && (parent.areGutsExposed() - || mMenuExposedView == parent - || (parent.getNotificationChildren().size() == 1 - && parent.isClearable()))) { - // In this case the group is expanded and showing the menu for the - // group, further interaction should apply to the group, not any - // child notifications so we use the parent of the child. We also do the same - // if we only have a single child. - child = parent; - } - } - return child; - } - @ShadeViewRefactor(RefactorComponent.INPUT) public ExpandableView getClosestChildAtRawPosition(float touchX, float touchY) { getLocationOnScreen(mTempInt2); @@ -1696,18 +1519,11 @@ public class NotificationStackScrollLayout extends ViewGroup return mScrollingEnabled; } - @Override @ShadeViewRefactor(RefactorComponent.ADAPTER) - public boolean canChildBeDismissed(View v) { + private boolean canChildBeDismissed(View v) { return StackScrollAlgorithm.canChildBeDismissed(v); } - @Override - @ShadeViewRefactor(RefactorComponent.INPUT) - public boolean isAntiFalsingNeeded() { - return onKeyguard(); - } - @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) private boolean onKeyguard() { return mStatusBarState == StatusBarState.KEYGUARD; @@ -1787,8 +1603,8 @@ public class NotificationStackScrollLayout extends ViewGroup } // Check if we need to clear any snooze leavebehinds - NotificationGuts guts = mStatusBar.getGutsManager().getExposedGuts(); - if (guts != null && !isTouchInView(ev, guts) + NotificationGuts guts = mNotificationGutsManager.getExposedGuts(); + if (guts != null && !NotificationSwipeHelper.isTouchInView(ev, guts) && guts.getGutsContent() instanceof NotificationSnooze) { NotificationSnooze ns = (NotificationSnooze) guts.getGutsContent(); if ((ns.isExpanded() && isCancelOrUp) @@ -3013,11 +2829,11 @@ public class NotificationStackScrollLayout extends ViewGroup } // Check if we need to clear any snooze leavebehinds boolean isUp = ev.getActionMasked() == MotionEvent.ACTION_UP; - NotificationGuts guts = mStatusBar.getGutsManager().getExposedGuts(); - if (!isTouchInView(ev, guts) && isUp && !swipeWantsIt && !expandWantsIt - && !scrollWantsIt) { + NotificationGuts guts = mNotificationGutsManager.getExposedGuts(); + if (!NotificationSwipeHelper.isTouchInView(ev, guts) && isUp && !swipeWantsIt && + !expandWantsIt && !scrollWantsIt) { mCheckForLeavebehind = false; - mStatusBar.getGutsManager().closeAndSaveGuts(true /* removeLeavebehind */, + mNotificationGutsManager.closeAndSaveGuts(true /* removeLeavebehind */, false /* force */, false /* removeControls */, -1 /* x */, -1 /* y */, false /* resetMenu */); } @@ -3077,8 +2893,8 @@ public class NotificationStackScrollLayout extends ViewGroup @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) @Override public void cleanUpViewState(View child) { - if (child == mTranslatingParentView) { - mTranslatingParentView = null; + if (child == mSwipeHelper.getTranslatingParentView()) { + mSwipeHelper.clearTranslatingParentView(); } mCurrentStackScrollState.removeViewStateForView(child); } @@ -3986,7 +3802,7 @@ public class NotificationStackScrollLayout extends ViewGroup @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) public void checkSnoozeLeavebehind() { if (mCheckForLeavebehind) { - mStatusBar.getGutsManager().closeAndSaveGuts(true /* removeLeavebehind */, + mNotificationGutsManager.closeAndSaveGuts(true /* removeLeavebehind */, false /* force */, false /* removeControls */, -1 /* x */, -1 /* y */, false /* resetMenu */); mCheckForLeavebehind = false; @@ -4068,7 +3884,7 @@ public class NotificationStackScrollLayout extends ViewGroup } @ShadeViewRefactor(RefactorComponent.COORDINATOR) - private void setIsExpanded(boolean isExpanded) { + public void setIsExpanded(boolean isExpanded) { boolean changed = isExpanded != mIsExpanded; mIsExpanded = isExpanded; mStackScrollAlgorithm.setIsExpanded(isExpanded); @@ -5242,8 +5058,8 @@ public class NotificationStackScrollLayout extends ViewGroup setFooterView(footerView); } - @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) - private void inflateEmptyShadeView() { + @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) + private void inflateEmptyShadeView() { EmptyShadeView view = (EmptyShadeView) LayoutInflater.from(mContext).inflate( R.layout.status_bar_no_notifications, this, false); view.setText(R.string.empty_shade_text); @@ -5274,8 +5090,8 @@ public class NotificationStackScrollLayout extends ViewGroup mScrimController.setNotificationCount(getNotGoneChildCount()); } - @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) - public void setNotificationPanel(NotificationPanelView notificationPanelView) { + @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) + public void setNotificationPanel(NotificationPanelView notificationPanelView) { mNotificationPanel = notificationPanelView; } @@ -5293,306 +5109,29 @@ public class NotificationStackScrollLayout extends ViewGroup @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) public interface OnOverscrollTopChangedListener { - /** - * Notifies a listener that the overscroll has changed. - * - * @param amount the amount of overscroll, in pixels - * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an - * unrubberbanded motion to directly expand overscroll view (e.g - * expand - * QS) - */ - void onOverscrollTopChanged(float amount, boolean isRubberbanded); - - /** - * Notify a listener that the scroller wants to escape from the scrolling motion and - * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS) - * - * @param velocity The velocity that the Scroller had when over flinging - * @param open Should the fling open or close the overscroll view. - */ - void flingTopOverscroll(float velocity, boolean open); - } - - @ShadeViewRefactor(RefactorComponent.INPUT) - private class NotificationSwipeHelper extends SwipeHelper - implements NotificationSwipeActionHelper { - private static final long COVER_MENU_DELAY = 4000; - private Runnable mFalsingCheck; - private Handler mHandler; - - private static final long SWIPE_MENU_TIMING = 200; - - public NotificationSwipeHelper(int swipeDirection, Callback callback, Context context) { - super(swipeDirection, callback, context); - mHandler = new Handler(); - mFalsingCheck = new Runnable() { - @Override - public void run() { - resetExposedMenuView(true /* animate */, true /* force */); - } - }; - } - - @Override - public void onDownUpdate(View currView, MotionEvent ev) { - mTranslatingParentView = currView; - if (mCurrMenuRow != null) { - mCurrMenuRow.onTouchStart(); - } - mCurrMenuRow = null; - mHandler.removeCallbacks(mFalsingCheck); - - // Slide back any notifications that might be showing a menu - resetExposedMenuView(true /* animate */, false /* force */); - - if (currView instanceof ExpandableNotificationRow) { - ExpandableNotificationRow row = (ExpandableNotificationRow) currView; - - if (row.getEntry().hasFinishedInitialization()) { - mCurrMenuRow = row.createMenu(); - mCurrMenuRow.setMenuClickListener(NotificationStackScrollLayout.this); - mCurrMenuRow.onTouchStart(); - } - } - } - - private boolean swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow) { - return !swipedFarEnough() && menuRow.isSwipedEnoughToShowMenu(); - } - - @Override - public void onMoveUpdate(View view, MotionEvent ev, float translation, float delta) { - mHandler.removeCallbacks(mFalsingCheck); - if (mCurrMenuRow != null) { - mCurrMenuRow.onTouchMove(delta); - } - } - - @Override - public boolean handleUpEvent(MotionEvent ev, View animView, float velocity, - float translation) { - if (mCurrMenuRow != null) { - mCurrMenuRow.onTouchEnd(); - handleMenuRowSwipe(ev, animView, velocity, mCurrMenuRow); - return true; - } - return false; - } - - @Override - public boolean swipedFarEnough(float translation, float viewSize) { - return swipedFarEnough(); - } - - private void handleMenuRowSwipe(MotionEvent ev, View animView, float velocity, - NotificationMenuRowPlugin menuRow) { - if (!menuRow.shouldShowMenu()) { - // If the menu should not be shown, then there is no need to check if the a swipe - // should result in a snapping to the menu. As a result, just check if the swipe - // was enough to dismiss the notification. - if (isDismissGesture(ev)) { - dismiss(animView, velocity); - } else { - snapBack(animView, velocity); - menuRow.onSnapClosed(); - } - return; - } - - if (menuRow.isSnappedAndOnSameSide()) { - // Menu was snapped to previously and we're on the same side - handleSwipeFromSnap(ev, animView, velocity, menuRow); - } else { - // Menu has not been snapped, or was snapped previously but is now on - // the opposite side. - handleSwipeFromNonSnap(ev, animView, velocity, menuRow); - } - } - - private void handleSwipeFromNonSnap(MotionEvent ev, View animView, float velocity, - NotificationMenuRowPlugin menuRow) { - boolean isDismissGesture = isDismissGesture(ev); - final boolean gestureTowardsMenu = menuRow.isTowardsMenu(velocity); - final boolean gestureFastEnough = - mSwipeHelper.getMinDismissVelocity() <= Math.abs(velocity); - - final double timeForGesture = ev.getEventTime() - ev.getDownTime(); - final boolean showMenuForSlowOnGoing = !menuRow.canBeDismissed() - && timeForGesture >= SWIPE_MENU_TIMING; - - if (!isFalseGesture(ev) - && (swipedEnoughToShowMenu(menuRow) - && (!gestureFastEnough || showMenuForSlowOnGoing)) - || (gestureTowardsMenu && !isDismissGesture)) { - // Menu has not been snapped to previously and this is menu revealing gesture - snapOpen(animView, menuRow.getMenuSnapTarget(), velocity); - menuRow.onSnapOpen(); - } else if (isDismissGesture(ev) && !gestureTowardsMenu) { - dismiss(animView, velocity); - menuRow.onDismiss(); - } else { - snapBack(animView, velocity); - menuRow.onSnapClosed(); - } - } - - private void handleSwipeFromSnap(MotionEvent ev, View animView, float velocity, - NotificationMenuRowPlugin menuRow) { - boolean isDismissGesture = isDismissGesture(ev); - - final boolean withinSnapMenuThreshold = - menuRow.isWithinSnapMenuThreshold(); - - if (withinSnapMenuThreshold && !isDismissGesture) { - // Haven't moved enough to unsnap from the menu - menuRow.onSnapOpen(); - snapOpen(animView, menuRow.getMenuSnapTarget(), velocity); - } else if (isDismissGesture && !menuRow.shouldSnapBack()) { - // Only dismiss if we're not moving towards the menu - dismiss(animView, velocity); - menuRow.onDismiss(); - } else { - snapBack(animView, velocity); - menuRow.onSnapClosed(); - } - } - - @Override - public void dismissChild(final View view, float velocity, - boolean useAccelerateInterpolator) { - super.dismissChild(view, velocity, useAccelerateInterpolator); - if (mIsExpanded) { - // We don't want to quick-dismiss when it's a heads up as this might lead to closing - // of the panel early. - handleChildViewDismissed(view); - } - mStatusBar.getGutsManager().closeAndSaveGuts(true /* removeLeavebehind */, - false /* force */, false /* removeControls */, -1 /* x */, -1 /* y */, - false /* resetMenu */); - handleMenuCoveredOrDismissed(); - } - - @Override - public void snapChild(final View animView, final float targetLeft, float velocity) { - super.snapChild(animView, targetLeft, velocity); - onDragCancelled(animView); - if (targetLeft == 0) { - handleMenuCoveredOrDismissed(); - } - } - - @Override - public void snooze(StatusBarNotification sbn, SnoozeOption snoozeOption) { - mStatusBar.setNotificationSnoozed(sbn, snoozeOption); - } - - private void handleMenuCoveredOrDismissed() { - if (mMenuExposedView != null && mMenuExposedView == mTranslatingParentView) { - mMenuExposedView = null; - } - } - - @Override - public Animator getViewTranslationAnimator(View v, float target, - AnimatorUpdateListener listener) { - if (v instanceof ExpandableNotificationRow) { - return ((ExpandableNotificationRow) v).getTranslateViewAnimator(target, listener); - } else { - return super.getViewTranslationAnimator(v, target, listener); - } - } - - @Override - public void setTranslation(View v, float translate) { - ((ExpandableView) v).setTranslation(translate); - } - - @Override - public float getTranslation(View v) { - return ((ExpandableView) v).getTranslation(); - } - - @Override - public void dismiss(View animView, float velocity) { - dismissChild(animView, velocity, - !swipedFastEnough(0, 0) /* useAccelerateInterpolator */); - } - - @Override - public void snapOpen(View animView, int targetLeft, float velocity) { - snapChild(animView, targetLeft, velocity); - } - - private void snapBack(View animView, float velocity) { - snapChild(animView, 0, velocity); - } - - @Override - public boolean swipedFastEnough(float translation, float velocity) { - return swipedFastEnough(); - } - - @Override - public float getMinDismissVelocity() { - return getEscapeVelocity(); - } - - public void onMenuShown(View animView) { - onDragCancelled(animView); - - // If we're on the lockscreen we want to false this. - if (isAntiFalsingNeeded()) { - mHandler.removeCallbacks(mFalsingCheck); - mHandler.postDelayed(mFalsingCheck, COVER_MENU_DELAY); - } - } - - public void closeControlsIfOutsideTouch(MotionEvent ev) { - NotificationGuts guts = mStatusBar.getGutsManager().getExposedGuts(); - View view = null; - if (guts != null && !guts.getGutsContent().isLeavebehind()) { - // Only close visible guts if they're not a leavebehind. - view = guts; - } else if (mCurrMenuRow != null && mCurrMenuRow.isMenuVisible() - && mTranslatingParentView != null) { - // Checking menu - view = mTranslatingParentView; - } - if (view != null && !isTouchInView(ev, view)) { - // Touch was outside visible guts / menu notification, close what's visible - mStatusBar.getGutsManager().closeAndSaveGuts(false /* removeLeavebehind */, - false /* force */, true /* removeControls */, -1 /* x */, -1 /* y */, - false /* resetMenu */); - resetExposedMenuView(true /* animate */, true /* force */); - } - } + /** + * Notifies a listener that the overscroll has changed. + * + * @param amount the amount of overscroll, in pixels + * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an + * unrubberbanded motion to directly expand overscroll view (e.g + * expand + * QS) + */ + void onOverscrollTopChanged(float amount, boolean isRubberbanded); - public void resetExposedMenuView(boolean animate, boolean force) { - if (mMenuExposedView == null - || (!force && mMenuExposedView == mTranslatingParentView)) { - // If no menu is showing or it's showing for this view we do nothing. - return; - } - final View prevMenuExposedView = mMenuExposedView; - if (animate) { - Animator anim = getViewTranslationAnimator(prevMenuExposedView, - 0 /* leftTarget */, null /* updateListener */); - if (anim != null) { - anim.start(); - } - } else if (mMenuExposedView instanceof ExpandableNotificationRow) { - ExpandableNotificationRow row = (ExpandableNotificationRow) mMenuExposedView; - if (!row.isRemoved()) { - row.resetTranslation(); - } - } - mMenuExposedView = null; - } - } + /** + * Notify a listener that the scroller wants to escape from the scrolling motion and + * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS) + * + * @param velocity The velocity that the Scroller had when over flinging + * @param open Should the fling open or close the overscroll view. + */ + void flingTopOverscroll(float velocity, boolean open); + } - @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) - public boolean hasActiveNotifications() { + @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) + public boolean hasActiveNotifications() { return !mEntryManager.getNotificationData().getActiveNotifications().isEmpty(); } @@ -5656,8 +5195,8 @@ public class NotificationStackScrollLayout extends ViewGroup return mStatusBarState == StatusBarState.KEYGUARD; } - @ShadeViewRefactor(RefactorComponent.INPUT) - public void updateSpeedBumpIndex() { + @ShadeViewRefactor(RefactorComponent.INPUT) + public void updateSpeedBumpIndex() { int speedBumpIndex = 0; int currentIndex = 0; final int N = getChildCount(); @@ -5677,24 +5216,6 @@ public class NotificationStackScrollLayout extends ViewGroup updateSpeedBumpIndex(speedBumpIndex, noAmbient); } - @ShadeViewRefactor(RefactorComponent.INPUT) - private boolean isTouchInView(MotionEvent ev, View view) { - if (view == null) { - return false; - } - final int height = (view instanceof ExpandableView) - ? ((ExpandableView) view).getActualHeight() - : view.getHeight(); - final int rx = (int) ev.getRawX(); - final int ry = (int) ev.getRawY(); - view.getLocationOnScreen(mTempInt2); - final int x = mTempInt2[0]; - final int y = mTempInt2[1]; - Rect rect = new Rect(x, y, x + view.getWidth(), y + height); - boolean ret = rect.contains(rx, ry); - return ret; - } - @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) private void updateContinuousShadowDrawing() { boolean continuousShadowUpdate = mAnimationRunning @@ -5718,11 +5239,29 @@ public class NotificationStackScrollLayout extends ViewGroup @ShadeViewRefactor(RefactorComponent.INPUT) public void closeControlsIfOutsideTouch(MotionEvent ev) { - mSwipeHelper.closeControlsIfOutsideTouch(ev); + NotificationGuts guts = mNotificationGutsManager.getExposedGuts(); + NotificationMenuRowPlugin menuRow = mSwipeHelper.getCurrentMenuRow(); + View translatingParentView = mSwipeHelper.getTranslatingParentView(); + View view = null; + if (guts != null && !guts.getGutsContent().isLeavebehind()) { + // Only close visible guts if they're not a leavebehind. + view = guts; + } else if (menuRow != null && menuRow.isMenuVisible() + && translatingParentView != null) { + // Checking menu + view = translatingParentView; + } + if (view != null && !NotificationSwipeHelper.isTouchInView(ev, view)) { + // Touch was outside visible guts / menu notification, close what's visible + mNotificationGutsManager.closeAndSaveGuts(false /* removeLeavebehind */, + false /* force */, true /* removeControls */, -1 /* x */, -1 /* y */, + false /* resetMenu */); + resetExposedMenuView(true /* animate */, true /* force */); + } } - @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) - static class AnimationEvent { + @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) + static class AnimationEvent { static AnimationFilter[] FILTERS = new AnimationFilter[]{ @@ -6022,8 +5561,8 @@ public class NotificationStackScrollLayout extends ViewGroup } } - @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) - private final StateListener mStateListener = new StateListener() { + @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) + private final StateListener mStateListener = new StateListener() { @Override public void onStatePreChange(int oldState, int newState) { if (oldState == StatusBarState.SHADE_LOCKED && newState == StatusBarState.KEYGUARD) { @@ -6036,9 +5575,222 @@ public class NotificationStackScrollLayout extends ViewGroup setStatusBarState(newState); } - @Override - public void onStatePostChange() { + @Override + public void onStatePostChange() { NotificationStackScrollLayout.this.onStatePostChange(); } - }; + }; + + class NotificationMenuListener implements NotificationMenuRowPlugin.OnMenuEventListener { + @Override + @ShadeViewRefactor(RefactorComponent.INPUT) + public void onMenuClicked(View view, int x, int y, MenuItem item) { + if (mLongPressListener == null) { + return; + } + if (view instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) view; + MetricsLogger.action(mContext, MetricsEvent.ACTION_TOUCH_GEAR, + row.getStatusBarNotification().getPackageName()); + } + mLongPressListener.onLongPress(view, x, y, item); + } + + @Override + @ShadeViewRefactor(RefactorComponent.INPUT) + public void onMenuReset(View row) { + View translatingParentView = mSwipeHelper.getTranslatingParentView(); + if (translatingParentView != null && row == translatingParentView) { + mSwipeHelper.clearExposedMenuView(); + mSwipeHelper.clearTranslatingParentView(); + } + } + + @Override + @ShadeViewRefactor(RefactorComponent.INPUT) + public void onMenuShown(View row) { + if (row instanceof ExpandableNotificationRow) { + MetricsLogger.action(mContext, MetricsEvent.ACTION_REVEAL_GEAR, + ((ExpandableNotificationRow) row).getStatusBarNotification() + .getPackageName()); + } + mSwipeHelper.onMenuShown(row); + } + } + + class SwipeHelperCallback implements NotificationSwipeHelper.NotificationCallback { + @Override + public void onDismiss() { + mNotificationGutsManager.closeAndSaveGuts(true /* removeLeavebehind */, + false /* force */, false /* removeControls */, -1 /* x */, -1 /* y */, + false /* resetMenu */); + } + + @Override + public void onSnooze(StatusBarNotification sbn, + NotificationSwipeActionHelper.SnoozeOption snoozeOption) { + mStatusBar.setNotificationSnoozed(sbn, snoozeOption); + } + + @Override + public boolean isExpanded() { + return NotificationStackScrollLayout.this.isExpanded(); + } + + @Override + @ShadeViewRefactor(RefactorComponent.INPUT) + public void onDragCancelled(View v) { + mFalsingManager.onNotificatonStopDismissing(); + setSwipingInProgress(false); + } + + /** + * Handles cleanup after the given {@code view} has been fully swiped out (including + * re-invoking dismiss logic in case the notification has not made its way out yet). + */ + @Override + @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) + public void onChildDismissed(View view) { + ExpandableNotificationRow row = (ExpandableNotificationRow) view; + if (!row.isDismissed()) { + handleChildViewDismissed(view); + } + ViewGroup transientContainer = row.getTransientContainer(); + if (transientContainer != null) { + transientContainer.removeTransientView(view); + } + } + + /** + * Starts up notification dismiss and tells the notification, if any, to remove itself from + * layout. + * + * @param view view (e.g. notification) to dismiss from the layout + */ + + @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) + public void handleChildViewDismissed(View view) { + if (mDismissAllInProgress) { + return; + } + + boolean isBlockingHelperShown = false; + + setSwipingInProgress(false); + if (mDragAnimPendingChildren.contains(view)) { + // We start the swipe and finish it in the same frame; we don't want a drag + // animation. + mDragAnimPendingChildren.remove(view); + } + mAmbientState.onDragFinished(view); + updateContinuousShadowDrawing(); + + if (view instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) view; + if (row.isHeadsUp()) { + mHeadsUpManager.addSwipedOutNotification( + row.getStatusBarNotification().getKey()); + } + isBlockingHelperShown = + row.performDismissWithBlockingHelper(false /* fromAccessibility */); + } + + if (!isBlockingHelperShown) { + mSwipedOutViews.add(view); + } + mFalsingManager.onNotificationDismissed(); + if (mFalsingManager.shouldEnforceBouncer()) { + mStatusBar.executeRunnableDismissingKeyguard( + null, + null /* cancelAction */, + false /* dismissShade */, + true /* afterKeyguardGone */, + false /* deferred */); + } + } + + @Override + @ShadeViewRefactor(RefactorComponent.INPUT) + public boolean isAntiFalsingNeeded() { + return onKeyguard(); + } + + @Override + @ShadeViewRefactor(RefactorComponent.INPUT) + public View getChildAtPosition(MotionEvent ev) { + View child = NotificationStackScrollLayout.this.getChildAtPosition(ev.getX(), + ev.getY()); + if (child instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) child; + ExpandableNotificationRow parent = row.getNotificationParent(); + if (parent != null && parent.areChildrenExpanded() + && (parent.areGutsExposed() + || mSwipeHelper.getExposedMenuView() == parent + || (parent.getNotificationChildren().size() == 1 + && parent.isClearable()))) { + // In this case the group is expanded and showing the menu for the + // group, further interaction should apply to the group, not any + // child notifications so we use the parent of the child. We also do the same + // if we only have a single child. + child = parent; + } + } + return child; + } + + @Override + @ShadeViewRefactor(RefactorComponent.INPUT) + public void onBeginDrag(View v) { + mFalsingManager.onNotificatonStartDismissing(); + setSwipingInProgress(true); + mAmbientState.onBeginDrag(v); + updateContinuousShadowDrawing(); + if (mAnimationsEnabled && (mIsExpanded || !isPinnedHeadsUp(v))) { + mDragAnimPendingChildren.add(v); + mNeedsAnimation = true; + } + requestChildrenUpdate(); + } + + @Override + @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) + public void onChildSnappedBack(View animView, float targetLeft) { + mAmbientState.onDragFinished(animView); + updateContinuousShadowDrawing(); + if (!mDragAnimPendingChildren.contains(animView)) { + if (mAnimationsEnabled) { + mSnappedBackChildren.add(animView); + mNeedsAnimation = true; + } + requestChildrenUpdate(); + } else { + // We start the swipe and snap back in the same frame, we don't want any animation + mDragAnimPendingChildren.remove(animView); + } + NotificationMenuRowPlugin menuRow = mSwipeHelper.getCurrentMenuRow(); + if (menuRow != null && targetLeft == 0) { + menuRow.resetMenu(); + mSwipeHelper.clearCurrentMenuRow(); + } + } + + @Override + @ShadeViewRefactor(RefactorComponent.INPUT) + public boolean updateSwipeProgress(View animView, boolean dismissable, + float swipeProgress) { + // Returning true prevents alpha fading. + return !mFadeNotificationsOnDismiss; + } + + @Override + @ShadeViewRefactor(RefactorComponent.INPUT) + public float getFalsingThresholdFactor() { + return mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f; + } + + @Override + public boolean canChildBeDismissed(View v) { + return NotificationStackScrollLayout.this.canChildBeDismissed(v); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java new file mode 100644 index 000000000000..028957d233ff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2018 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 Licen + */ + + +package com.android.systemui.statusbar.notification.stack; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Rect; +import android.os.Handler; +import android.service.notification.StatusBarNotification; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.SwipeHelper; +import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; +import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; +import com.android.systemui.statusbar.notification.ShadeViewRefactor; +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.row.ExpandableView; + +@ShadeViewRefactor(ShadeViewRefactor.RefactorComponent.INPUT) +class NotificationSwipeHelper extends SwipeHelper + implements NotificationSwipeActionHelper { + @VisibleForTesting + protected static final long COVER_MENU_DELAY = 4000; + private static final String TAG = "NotificationSwipeHelper"; + private final Runnable mFalsingCheck; + private View mTranslatingParentView; + private View mMenuExposedView; + private final NotificationCallback mCallback; + private final NotificationMenuRowPlugin.OnMenuEventListener mMenuListener; + + private static final long SWIPE_MENU_TIMING = 200; + + private NotificationMenuRowPlugin mCurrMenuRow; + + public NotificationSwipeHelper(int swipeDirection, NotificationCallback callback, + Context context, NotificationMenuRowPlugin.OnMenuEventListener menuListener) { + super(swipeDirection, callback, context); + mMenuListener = menuListener; + mCallback = callback; + mFalsingCheck = new Runnable() { + @Override + public void run() { + resetExposedMenuView(true /* animate */, true /* force */); + } + }; + } + + public View getTranslatingParentView() { + return mTranslatingParentView; + } + + public void clearTranslatingParentView() { setTranslatingParentView(null); } + + @VisibleForTesting + protected void setTranslatingParentView(View view) { mTranslatingParentView = view; }; + + public void setExposedMenuView(View view) { + mMenuExposedView = view; + } + + public void clearExposedMenuView() { setExposedMenuView(null); } + + public void clearCurrentMenuRow() { setCurrentMenuRow(null); } + + public View getExposedMenuView() { + return mMenuExposedView; + } + + public void setCurrentMenuRow(NotificationMenuRowPlugin menuRow) { + mCurrMenuRow = menuRow; + } + + public NotificationMenuRowPlugin getCurrentMenuRow() { return mCurrMenuRow; } + + @VisibleForTesting + protected Handler getHandler() { return mHandler; } + + @VisibleForTesting + protected Runnable getFalsingCheck() { return mFalsingCheck; }; + + @Override + public void onDownUpdate(View currView, MotionEvent ev) { + mTranslatingParentView = currView; + NotificationMenuRowPlugin menuRow = getCurrentMenuRow(); + if (menuRow != null) { + menuRow.onTouchStart(); + } + clearCurrentMenuRow(); + getHandler().removeCallbacks(getFalsingCheck()); + + // Slide back any notifications that might be showing a menu + resetExposedMenuView(true /* animate */, false /* force */); + + if (currView instanceof ExpandableNotificationRow) { + initializeRow((ExpandableNotificationRow) currView); + } + } + + @VisibleForTesting + protected void initializeRow(ExpandableNotificationRow row) { + if (row.getEntry().hasFinishedInitialization()) { + mCurrMenuRow = row.createMenu(); + mCurrMenuRow.setMenuClickListener(mMenuListener); + mCurrMenuRow.onTouchStart(); + } + } + + private boolean swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow) { + return !swipedFarEnough() && menuRow.isSwipedEnoughToShowMenu(); + } + + @Override + public void onMoveUpdate(View view, MotionEvent ev, float translation, float delta) { + getHandler().removeCallbacks(getFalsingCheck()); + NotificationMenuRowPlugin menuRow = getCurrentMenuRow(); + if (menuRow != null) { + menuRow.onTouchMove(delta); + } + } + + @Override + public boolean handleUpEvent(MotionEvent ev, View animView, float velocity, + float translation) { + NotificationMenuRowPlugin menuRow = getCurrentMenuRow(); + if (menuRow != null) { + menuRow.onTouchEnd(); + handleMenuRowSwipe(ev, animView, velocity, menuRow); + return true; + } + return false; + } + + @VisibleForTesting + protected void handleMenuRowSwipe(MotionEvent ev, View animView, float velocity, + NotificationMenuRowPlugin menuRow) { + if (!menuRow.shouldShowMenu()) { + // If the menu should not be shown, then there is no need to check if the a swipe + // should result in a snapping to the menu. As a result, just check if the swipe + // was enough to dismiss the notification. + if (isDismissGesture(ev)) { + dismiss(animView, velocity); + } else { + snapClosed(animView, velocity); + menuRow.onSnapClosed(); + } + return; + } + + if (menuRow.isSnappedAndOnSameSide()) { + // Menu was snapped to previously and we're on the same side + handleSwipeFromSnap(ev, animView, velocity, menuRow); + } else { + // Menu has not been snapped, or was snapped previously but is now on + // the opposite side. + handleSwipeFromNonSnap(ev, animView, velocity, menuRow); + } + } + + private void handleSwipeFromNonSnap(MotionEvent ev, View animView, float velocity, + NotificationMenuRowPlugin menuRow) { + boolean isDismissGesture = isDismissGesture(ev); + final boolean gestureTowardsMenu = menuRow.isTowardsMenu(velocity); + final boolean gestureFastEnough = getEscapeVelocity() <= Math.abs(velocity); + + final double timeForGesture = ev.getEventTime() - ev.getDownTime(); + final boolean showMenuForSlowOnGoing = !menuRow.canBeDismissed() + && timeForGesture >= SWIPE_MENU_TIMING; + + if (!isFalseGesture(ev) + && (swipedEnoughToShowMenu(menuRow) + && (!gestureFastEnough || showMenuForSlowOnGoing)) + || (gestureTowardsMenu && !isDismissGesture)) { + // Menu has not been snapped to previously and this is menu revealing gesture + snapOpen(animView, menuRow.getMenuSnapTarget(), velocity); + menuRow.onSnapOpen(); + } else if (isDismissGesture(ev) && !gestureTowardsMenu) { + dismiss(animView, velocity); + menuRow.onDismiss(); + } else { + snapClosed(animView, velocity); + menuRow.onSnapClosed(); + } + } + + private void handleSwipeFromSnap(MotionEvent ev, View animView, float velocity, + NotificationMenuRowPlugin menuRow) { + boolean isDismissGesture = isDismissGesture(ev); + + final boolean withinSnapMenuThreshold = + menuRow.isWithinSnapMenuThreshold(); + + if (withinSnapMenuThreshold && !isDismissGesture) { + // Haven't moved enough to unsnap from the menu + menuRow.onSnapOpen(); + snapOpen(animView, menuRow.getMenuSnapTarget(), velocity); + } else if (isDismissGesture && !menuRow.shouldSnapBack()) { + // Only dismiss if we're not moving towards the menu + dismiss(animView, velocity); + menuRow.onDismiss(); + } else { + snapClosed(animView, velocity); + menuRow.onSnapClosed(); + } + } + + @Override + public void dismissChild(final View view, float velocity, + boolean useAccelerateInterpolator) { + superDismissChild(view, velocity, useAccelerateInterpolator); + if (mCallback.isExpanded()) { + // We don't want to quick-dismiss when it's a heads up as this might lead to closing + // of the panel early. + mCallback.handleChildViewDismissed(view); + } + mCallback.onDismiss(); + handleMenuCoveredOrDismissed(); + } + + @VisibleForTesting + protected void superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator) { + super.dismissChild(view, velocity, useAccelerateInterpolator); + } + + @VisibleForTesting + protected void superSnapChild(final View animView, final float targetLeft, float velocity) { + super.snapChild(animView, targetLeft, velocity); + } + + @Override + public void snapChild(final View animView, final float targetLeft, float velocity) { + superSnapChild(animView, targetLeft, velocity); + mCallback.onDragCancelled(animView); + if (targetLeft == 0) { + handleMenuCoveredOrDismissed(); + } + } + + @Override + public void snooze(StatusBarNotification sbn, SnoozeOption snoozeOption) { + mCallback.onSnooze(sbn, snoozeOption); + } + + @VisibleForTesting + protected void handleMenuCoveredOrDismissed() { + View exposedMenuView = getExposedMenuView(); + if (exposedMenuView != null && exposedMenuView == mTranslatingParentView) { + clearExposedMenuView(); + } + } + + @VisibleForTesting + protected Animator superGetViewTranslationAnimator(View v, float target, + ValueAnimator.AnimatorUpdateListener listener) { + return super.getViewTranslationAnimator(v, target, listener); + } + + @Override + public Animator getViewTranslationAnimator(View v, float target, + ValueAnimator.AnimatorUpdateListener listener) { + if (v instanceof ExpandableNotificationRow) { + return ((ExpandableNotificationRow) v).getTranslateViewAnimator(target, listener); + } else { + return superGetViewTranslationAnimator(v, target, listener); + } + } + + @Override + public void setTranslation(View v, float translate) { + if (v instanceof ExpandableNotificationRow) { + ((ExpandableNotificationRow) v).setTranslation(translate); + } else { + Log.wtf(TAG, "setTranslation should only be called on an ExpandableNotificationRow."); + } + } + + @Override + public float getTranslation(View v) { + if (v instanceof ExpandableNotificationRow) { + return ((ExpandableNotificationRow) v).getTranslation(); + } + else { + Log.wtf(TAG, "getTranslation should only be called on an ExpandableNotificationRow."); + return 0f; + } + } + + @Override + public boolean swipedFastEnough(float translation, float viewSize) { + return swipedFastEnough(); + } + + @Override + @VisibleForTesting + protected boolean swipedFastEnough() { + return super.swipedFastEnough(); + } + + @Override + public boolean swipedFarEnough(float translation, float viewSize) { + return swipedFarEnough(); + } + + @Override + @VisibleForTesting + protected boolean swipedFarEnough() { + return super.swipedFarEnough(); + } + + @Override + public void dismiss(View animView, float velocity) { + dismissChild(animView, velocity, + !swipedFastEnough() /* useAccelerateInterpolator */); + } + + @Override + public void snapOpen(View animView, int targetLeft, float velocity) { + snapChild(animView, targetLeft, velocity); + } + + @VisibleForTesting + protected void snapClosed(View animView, float velocity) { + snapChild(animView, 0, velocity); + } + + @Override + @VisibleForTesting + protected float getEscapeVelocity() { + return super.getEscapeVelocity(); + } + + @Override + public float getMinDismissVelocity() { + return getEscapeVelocity(); + } + + public void onMenuShown(View animView) { + setExposedMenuView(getTranslatingParentView()); + mCallback.onDragCancelled(animView); + Handler handler = getHandler(); + + // If we're on the lockscreen we want to false this. + if (mCallback.isAntiFalsingNeeded()) { + handler.removeCallbacks(getFalsingCheck()); + handler.postDelayed(getFalsingCheck(), COVER_MENU_DELAY); + } + } + + @VisibleForTesting + protected boolean shouldResetMenu(boolean force) { + if (mMenuExposedView == null + || (!force && mMenuExposedView == mTranslatingParentView)) { + // If no menu is showing or it's showing for this view we do nothing. + return false; + } + return true; + } + + public void resetExposedMenuView(boolean animate, boolean force) { + if (!shouldResetMenu(force)) { + return; + } + final View prevMenuExposedView = getExposedMenuView(); + if (animate) { + Animator anim = getViewTranslationAnimator(prevMenuExposedView, + 0 /* leftTarget */, null /* updateListener */); + if (anim != null) { + anim.start(); + } + } else if (prevMenuExposedView instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) prevMenuExposedView; + if (!row.isRemoved()) { + row.resetTranslation(); + } + } + clearExposedMenuView(); + } + + public static boolean isTouchInView(MotionEvent ev, View view) { + if (view == null) { + return false; + } + final int height = (view instanceof ExpandableView) + ? ((ExpandableView) view).getActualHeight() + : view.getHeight(); + final int rx = (int) ev.getRawX(); + final int ry = (int) ev.getRawY(); + int[] temp = new int[2]; + view.getLocationOnScreen(temp); + final int x = temp[0]; + final int y = temp[1]; + Rect rect = new Rect(x, y, x + view.getWidth(), y + height); + boolean ret = rect.contains(rx, ry); + return ret; + } + + public interface NotificationCallback extends SwipeHelper.Callback{ + boolean isExpanded(); + + void handleChildViewDismissed(View view); + + void onSnooze(StatusBarNotification sbn, SnoozeOption snoozeOption); + + void onDismiss(); + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java new file mode 100644 index 000000000000..b5f67c06b2d1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java @@ -0,0 +1,511 @@ +/* + * Copyright (C) 2018 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.stack; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockitoSession; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.animation.Animator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.graphics.Rect; +import android.os.Handler; +import android.os.IPowerManager; +import android.os.Looper; +import android.os.PowerManager; +import android.service.notification.StatusBarNotification; +import android.support.test.annotation.UiThreadTest; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.testing.TestableLooper.RunWithLooper; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.MotionEvent; + +import com.android.systemui.SwipeHelper; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; +import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption; +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.row.NotificationMenuRow; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; + +import java.util.ArrayList; + +/** + * Tests for {@link NotificationSwipeHelper}. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class NotificationSwipeHelperTest extends SysuiTestCase { + + private NotificationSwipeHelper mSwipeHelper; + private NotificationSwipeHelper.NotificationCallback mCallback; + private NotificationMenuRowPlugin.OnMenuEventListener mListener; + private View mView; + private MotionEvent mEvent; + private NotificationMenuRowPlugin mMenuRow; + private Handler mHandler; + private ExpandableNotificationRow mNotificationRow; + private Runnable mFalsingCheck; + + @Rule public MockitoRule mockito = MockitoJUnit.rule(); + + @Before + @UiThreadTest + public void setUp() throws Exception { + mCallback = mock(NotificationSwipeHelper.NotificationCallback.class); + mListener = mock(NotificationMenuRowPlugin.OnMenuEventListener.class); + mSwipeHelper = spy(new NotificationSwipeHelper(SwipeHelper.X, mCallback, mContext, mListener)); + mView = mock(View.class); + mEvent = mock(MotionEvent.class); + mMenuRow = mock(NotificationMenuRowPlugin.class); + mNotificationRow = mock(ExpandableNotificationRow.class); + mHandler = mock(Handler.class); + mFalsingCheck = mock(Runnable.class); + } + + @Test + public void testSetExposedMenuView() { + assertEquals("intialized with null exposed menu view", null, + mSwipeHelper.getExposedMenuView()); + mSwipeHelper.setExposedMenuView(mView); + assertEquals("swipe helper has correct exposedMenuView after setExposedMenuView to a view", + mView, mSwipeHelper.getExposedMenuView()); + mSwipeHelper.setExposedMenuView(null); + assertEquals("swipe helper has null exposedMenuView after setExposedMenuView to null", + null, mSwipeHelper.getExposedMenuView()); + } + + @Test + public void testClearExposedMenuView() { + doNothing().when(mSwipeHelper).setExposedMenuView(mView); + mSwipeHelper.clearExposedMenuView(); + verify(mSwipeHelper, times(1)).setExposedMenuView(null); + } + + @Test + public void testGetTranslatingParentView() { + assertEquals("intialized with null translating parent view", null, + mSwipeHelper.getTranslatingParentView()); + mSwipeHelper.setTranslatingParentView(mView); + assertEquals("has translating parent view after setTranslatingParentView with a view", + mView, mSwipeHelper.getTranslatingParentView()); + } + + @Test + public void testClearTranslatingParentView() { + doNothing().when(mSwipeHelper).setTranslatingParentView(null); + mSwipeHelper.clearTranslatingParentView(); + verify(mSwipeHelper, times(1)).setTranslatingParentView(null); + } + + @Test + public void testSetCurrentMenuRow() { + assertEquals("currentMenuRow initializes to null", null, + mSwipeHelper.getCurrentMenuRow()); + mSwipeHelper.setCurrentMenuRow(mMenuRow); + assertEquals("currentMenuRow set correctly after setCurrentMenuRow", mMenuRow, + mSwipeHelper.getCurrentMenuRow()); + mSwipeHelper.setCurrentMenuRow(null); + assertEquals("currentMenuRow set to null after setCurrentMenuRow to null", + null, mSwipeHelper.getCurrentMenuRow()); + } + + @Test + public void testClearCurrentMenuRow() { + doNothing().when(mSwipeHelper).setCurrentMenuRow(null); + mSwipeHelper.clearCurrentMenuRow(); + verify(mSwipeHelper, times(1)).setCurrentMenuRow(null); + } + + @Test + public void testOnDownUpdate_ExpandableNotificationRow() { + when(mSwipeHelper.getHandler()).thenReturn(mHandler); + when(mSwipeHelper.getFalsingCheck()).thenReturn(mFalsingCheck); + doNothing().when(mSwipeHelper).resetExposedMenuView(true, false); + doNothing().when(mSwipeHelper).clearCurrentMenuRow(); + doNothing().when(mSwipeHelper).initializeRow(any()); + + mSwipeHelper.onDownUpdate(mNotificationRow, mEvent); + + verify(mSwipeHelper, times(1)).clearCurrentMenuRow(); + verify(mHandler, times(1)).removeCallbacks(mFalsingCheck); + verify(mSwipeHelper, times(1)).resetExposedMenuView(true, false); + verify(mSwipeHelper, times(1)).initializeRow(mNotificationRow); + } + + @Test + public void testOnDownUpdate_notExpandableNotificationRow() { + when(mSwipeHelper.getHandler()).thenReturn(mHandler); + when(mSwipeHelper.getFalsingCheck()).thenReturn(mFalsingCheck); + doNothing().when(mSwipeHelper).resetExposedMenuView(true, false); + doNothing().when(mSwipeHelper).clearCurrentMenuRow(); + doNothing().when(mSwipeHelper).initializeRow(any()); + + mSwipeHelper.onDownUpdate(mView, mEvent); + + verify(mSwipeHelper, times(1)).clearCurrentMenuRow(); + verify(mHandler, times(1)).removeCallbacks(mFalsingCheck); + verify(mSwipeHelper, times(1)).resetExposedMenuView(true, false); + verify(mSwipeHelper, times(0)).initializeRow(any()); + } + + @Test + public void testOnMoveUpdate_menuRow() { + when(mSwipeHelper.getCurrentMenuRow()).thenReturn(mMenuRow); + when(mSwipeHelper.getHandler()).thenReturn(mHandler); + when(mSwipeHelper.getFalsingCheck()).thenReturn(mFalsingCheck); + + mSwipeHelper.onMoveUpdate(mView, mEvent, 0, 10); + + verify(mHandler, times(1)).removeCallbacks(mFalsingCheck); + verify(mMenuRow, times(1)).onTouchMove(10); + } + + @Test + public void testOnMoveUpdate_noMenuRow() { + when(mSwipeHelper.getHandler()).thenReturn(mHandler); + when(mSwipeHelper.getFalsingCheck()).thenReturn(mFalsingCheck); + + mSwipeHelper.onMoveUpdate(mView, mEvent, 0, 10); + + verify(mHandler, times(1)).removeCallbacks(mFalsingCheck); + } + + @Test + public void testHandleUpEvent_noMenuRow() { + assertFalse("Menu row does not exist", + mSwipeHelper.handleUpEvent(mEvent, mView, 0, 0)); + } + + @Test + public void testHandleUpEvent_menuRow() { + when(mSwipeHelper.getCurrentMenuRow()).thenReturn(mMenuRow); + doNothing().when(mSwipeHelper).handleMenuRowSwipe(mEvent, mView, 0, mMenuRow); + + assertTrue("Menu row exists", + mSwipeHelper.handleUpEvent(mEvent, mView, 0, 0)); + verify(mMenuRow, times(1)).onTouchEnd(); + verify(mSwipeHelper, times(1)).handleMenuRowSwipe(mEvent, mView, 0, mMenuRow); + } + + @Test + public void testDismissChild_notExpanded() { + when(mCallback.isExpanded()).thenReturn(false); + doNothing().when(mSwipeHelper).superDismissChild(mView, 0, false); + doNothing().when(mSwipeHelper).handleMenuCoveredOrDismissed(); + + mSwipeHelper.dismissChild(mView, 0, false); + + verify(mSwipeHelper, times(1)).superDismissChild(mView, 0, false); + verify(mCallback, times(0)).handleChildViewDismissed(mView); + verify(mCallback, times(1)).onDismiss(); + verify(mSwipeHelper, times(1)).handleMenuCoveredOrDismissed(); + } + + @Test + public void testSnapchild_targetIsZero() { + doNothing().when(mSwipeHelper).superSnapChild(mView, 0, 0); + mSwipeHelper.snapChild(mView, 0, 0); + + verify(mCallback, times(1)).onDragCancelled(mView); + verify(mSwipeHelper, times(1)).superSnapChild(mView, 0, 0); + verify(mSwipeHelper, times(1)).handleMenuCoveredOrDismissed(); + } + + + @Test + public void testSnapchild_targetNotZero() { + doNothing().when(mSwipeHelper).superSnapChild(mView, 10, 0); + mSwipeHelper.snapChild(mView, 10, 0); + + verify(mCallback, times(1)).onDragCancelled(mView); + verify(mSwipeHelper, times(1)).superSnapChild(mView, 10, 0); + verify(mSwipeHelper, times(0)).handleMenuCoveredOrDismissed(); + } + + @Test + public void testSnooze() { + StatusBarNotification sbn = mock(StatusBarNotification.class); + SnoozeOption snoozeOption = mock(SnoozeOption.class); + mSwipeHelper.snooze(sbn, snoozeOption); + verify(mCallback, times(1)).onSnooze(sbn, snoozeOption); + } + + @Test + public void testGetViewTranslationAnimator_notExpandableNotificationRow() { + Animator animator = mock(Animator.class); + AnimatorUpdateListener listener = mock(AnimatorUpdateListener.class); + doReturn(animator).when(mSwipeHelper).superGetViewTranslationAnimator(mView, 0, listener); + + assertEquals("returns the correct animator from super", animator, + mSwipeHelper.getViewTranslationAnimator(mView, 0, listener)); + + verify(mSwipeHelper, times(1)).superGetViewTranslationAnimator(mView, 0, listener); + } + + @Test + public void testGetViewTranslationAnimator_expandableNotificationRow() { + Animator animator = mock(Animator.class); + AnimatorUpdateListener listener = mock(AnimatorUpdateListener.class); + doReturn(animator).when(mNotificationRow).getTranslateViewAnimator(0, listener); + + assertEquals("returns the correct animator from super when view is an ENR", animator, + mSwipeHelper.getViewTranslationAnimator(mNotificationRow, 0, listener)); + + verify(mNotificationRow, times(1)).getTranslateViewAnimator(0, listener); + } + + @Test + public void testSetTranslation() { + mSwipeHelper.setTranslation(mNotificationRow, 0); + verify(mNotificationRow, times(1)).setTranslation(0); + } + + @Test + public void testGetTranslation() { + doReturn(30f).when(mNotificationRow).getTranslation(); + + assertEquals("Returns getTranslation for the ENR", + mSwipeHelper.getTranslation(mNotificationRow), 30f); + + verify(mNotificationRow, times(1)).getTranslation(); + } + + @Test + public void testDismiss() { + doNothing().when(mSwipeHelper).dismissChild(mView, 0, true); + doReturn(false).when(mSwipeHelper).swipedFastEnough(); + + mSwipeHelper.dismiss(mView, 0); + + verify(mSwipeHelper, times(1)).swipedFastEnough(); + verify(mSwipeHelper, times(1)).dismissChild(mView, 0, true); + } + + @Test + public void testSnapOpen() { + doNothing().when(mSwipeHelper).snapChild(mView, 30, 0); + + mSwipeHelper.snapOpen(mView, 30, 0); + + verify(mSwipeHelper, times(1)).snapChild(mView, 30, 0); + } + + @Test + public void testSnapClosed() { + doNothing().when(mSwipeHelper).snapChild(mView, 0, 0); + + mSwipeHelper.snapClosed(mView, 0); + + verify(mSwipeHelper, times(1)).snapChild(mView, 0, 0); + } + + @Test + public void testGetMinDismissVelocity() { + doReturn(30f).when(mSwipeHelper).getEscapeVelocity(); + + assertEquals("Returns getEscapeVelocity", 30f, mSwipeHelper.getMinDismissVelocity()); + } + + @Test + public void onMenuShown_noAntiFalsing() { + doNothing().when(mSwipeHelper).setExposedMenuView(mView); + doReturn(mView).when(mSwipeHelper).getTranslatingParentView(); + doReturn(mHandler).when(mSwipeHelper).getHandler(); + doReturn(false).when(mCallback).isAntiFalsingNeeded(); + doReturn(mFalsingCheck).when(mSwipeHelper).getFalsingCheck(); + + mSwipeHelper.onMenuShown(mView); + + verify(mSwipeHelper, times(1)).setExposedMenuView(mView); + verify(mCallback, times(1)).onDragCancelled(mView); + verify(mCallback, times(1)).isAntiFalsingNeeded(); + + verify(mHandler, times(0)).removeCallbacks(mFalsingCheck); + verify(mHandler, times(0)).postDelayed(mFalsingCheck, mSwipeHelper.COVER_MENU_DELAY); + } + + @Test + public void onMenuShown_antiFalsing() { + doNothing().when(mSwipeHelper).setExposedMenuView(mView); + doReturn(mView).when(mSwipeHelper).getTranslatingParentView(); + doReturn(mHandler).when(mSwipeHelper).getHandler(); + doReturn(true).when(mCallback).isAntiFalsingNeeded(); + doReturn(mFalsingCheck).when(mSwipeHelper).getFalsingCheck(); + + mSwipeHelper.onMenuShown(mView); + + verify(mSwipeHelper, times(1)).setExposedMenuView(mView); + verify(mCallback, times(1)).onDragCancelled(mView); + verify(mCallback, times(1)).isAntiFalsingNeeded(); + + verify(mHandler, times(1)).removeCallbacks(mFalsingCheck); + verify(mHandler, times(1)).postDelayed(mFalsingCheck, mSwipeHelper.COVER_MENU_DELAY); + } + + @Test + public void testResetExposedMenuView_noReset() { + doReturn(false).when(mSwipeHelper).shouldResetMenu(false); + doNothing().when(mSwipeHelper).clearExposedMenuView(); + + mSwipeHelper.resetExposedMenuView(false, false); + + verify(mSwipeHelper, times(1)).shouldResetMenu(false); + + // should not clear exposed menu row + verify(mSwipeHelper, times(0)).clearExposedMenuView(); + } + + @Test + public void testResetExposedMenuView_animate() { + Animator animator = mock(Animator.class); + + doReturn(true).when(mSwipeHelper).shouldResetMenu(false); + doReturn(mNotificationRow).when(mSwipeHelper).getExposedMenuView(); + doReturn(false).when(mNotificationRow).isRemoved(); + doReturn(animator).when(mSwipeHelper).getViewTranslationAnimator(mNotificationRow, 0, null); + doNothing().when(mSwipeHelper).clearExposedMenuView(); + + mSwipeHelper.resetExposedMenuView(true, false); + + verify(mSwipeHelper, times(1)).shouldResetMenu(false); + + // should retrieve and start animator + verify(mSwipeHelper, times(1)).getViewTranslationAnimator(mNotificationRow, 0, null); + verify(animator, times(1)).start(); + + // should not reset translation on row directly + verify(mNotificationRow, times(0)).resetTranslation(); + + // should clear exposed menu row + verify(mSwipeHelper, times(1)).clearExposedMenuView(); + } + + + @Test + public void testResetExposedMenuView_noAnimate() { + Animator animator = mock(Animator.class); + + doReturn(true).when(mSwipeHelper).shouldResetMenu(false); + doReturn(mNotificationRow).when(mSwipeHelper).getExposedMenuView(); + doReturn(false).when(mNotificationRow).isRemoved(); + doReturn(animator).when(mSwipeHelper).getViewTranslationAnimator(mNotificationRow, 0, null); + doNothing().when(mSwipeHelper).clearExposedMenuView(); + + mSwipeHelper.resetExposedMenuView(false, false); + + verify(mSwipeHelper, times(1)).shouldResetMenu(false); + + // should not retrieve and start animator + verify(mSwipeHelper, times(0)).getViewTranslationAnimator(mNotificationRow, 0, null); + verify(animator, times(0)).start(); + + // should reset translation on row directly + verify(mNotificationRow, times(1)).resetTranslation(); + + // should clear exposed menu row + verify(mSwipeHelper, times(1)).clearExposedMenuView(); + } + + @Test + public void testIsTouchInView() { + assertEquals("returns false when view is null", false, + NotificationSwipeHelper.isTouchInView(mEvent, null)); + + doReturn(5f).when(mEvent).getRawX(); + doReturn(10f).when(mEvent).getRawY(); + + doReturn(20).when(mView).getWidth(); + doReturn(20).when(mView).getHeight(); + + Answer answer = (Answer) invocation -> { + int[] arr = invocation.getArgument(0); + arr[0] = 0; + arr[1] = 0; + return null; + }; + doAnswer(answer).when(mView).getLocationOnScreen(any()); + + assertTrue("Touch is within the view", + mSwipeHelper.isTouchInView(mEvent, mView)); + + doReturn(50f).when(mEvent).getRawX(); + + assertFalse("Touch is not within the view", + mSwipeHelper.isTouchInView(mEvent, mView)); + } + + @Test + public void testIsTouchInView_expandable() { + assertEquals("returns false when view is null", false, + NotificationSwipeHelper.isTouchInView(mEvent, null)); + + doReturn(5f).when(mEvent).getRawX(); + doReturn(10f).when(mEvent).getRawY(); + + doReturn(20).when(mNotificationRow).getWidth(); + doReturn(20).when(mNotificationRow).getActualHeight(); + + Answer answer = (Answer) invocation -> { + int[] arr = invocation.getArgument(0); + arr[0] = 0; + arr[1] = 0; + return null; + }; + doAnswer(answer).when(mNotificationRow).getLocationOnScreen(any()); + + assertTrue("Touch is within the view", + mSwipeHelper.isTouchInView(mEvent, mNotificationRow)); + + doReturn(50f).when(mEvent).getRawX(); + + assertFalse("Touch is not within the view", + mSwipeHelper.isTouchInView(mEvent, mNotificationRow)); + } +} |