diff options
6 files changed, 667 insertions, 208 deletions
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java index 680f14bcf399..0b1dab1c3bca 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java @@ -38,11 +38,12 @@ import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem public interface NotificationMenuRowPlugin extends Plugin { public static final String ACTION = "com.android.systemui.action.PLUGIN_NOTIFICATION_MENU_ROW"; - public static final int VERSION = 4; + public static final int VERSION = 5; @ProvidesInterface(version = OnMenuEventListener.VERSION) public interface OnMenuEventListener { public static final int VERSION = 1; + public void onMenuClicked(View row, int x, int y, MenuItem menu); public void onMenuReset(View row); @@ -53,6 +54,7 @@ public interface NotificationMenuRowPlugin extends Plugin { @ProvidesInterface(version = MenuItem.VERSION) public interface MenuItem { public static final int VERSION = 1; + public View getMenuView(); public View getGutsView(); @@ -84,34 +86,136 @@ public interface NotificationMenuRowPlugin extends Plugin { public void setMenuClickListener(OnMenuEventListener listener); - public void setSwipeActionHelper(NotificationSwipeActionHelper listener); - public void setAppName(String appName); public void createMenu(ViewGroup parent, StatusBarNotification sbn); + public void resetMenu(); + public View getMenuView(); + /** + * Get the target position that a notification row should be snapped open to in order to reveal + * the menu. This is generally determined by the number of icons in the notification menu and the + * size of each icon. This method accounts for whether the menu appears on the left or ride side + * of the parent notification row. + * + + * @return an int representing the x-offset in pixels that the notification should snap open to. + * Positive values imply that the notification should be offset to the right to reveal the menu, + * and negative alues imply that the notification should be offset to the right. + */ + public int getMenuSnapTarget(); + + /** + * Determines whether or not the menu should be shown in response to user input. + * @return true if the menu should be shown, false otherwise. + */ + public boolean shouldShowMenu(); + + /** + * Determines whether the menu is currently visible. + * @return true if the menu is visible, false otherwise. + */ public boolean isMenuVisible(); - public void resetMenu(); + /** + * Determines whether a given movement is towards or away from the current location of the menu. + * @param movement + * @return true if the movement is towards the menu, false otherwise. + */ + public boolean isTowardsMenu(float movement); - public void onTranslationUpdate(float translation); + /** + * Determines whether the menu should snap closed instead of dismissing the + * parent notification, as a function of its current state. + * + * @return true if the menu should snap closed, false otherwise. + */ + public boolean shouldSnapBack(); - public void onHeightUpdate(); + /** + * Determines whether the menu was previously snapped open to the same side that it is currently + * being shown on. + * @return true if the menu is snapped open to the same side on which it currently appears, + * false otherwise. + */ + public boolean isSnappedAndOnSameSide(); - public void onNotificationUpdated(StatusBarNotification sbn); + /** + * Determines whether the notification the menu is attached to is able to be dismissed. + * @return true if the menu's parent notification is dismissable, false otherwise. + */ + public boolean canBeDismissed(); - public boolean onTouchEvent(View view, MotionEvent ev, float velocity); + /** + * Determines whether the menu should remain open given its current state, or snap closed. + * @return true if the menu should remain open, false otherwise. + */ + public boolean isWithinSnapMenuThreshold(); + + /** + * Determines whether the menu has been swiped far enough to snap open. + * @return true if the menu has been swiped far enough to open, false otherwise. + */ + public boolean isSwipedEnoughToShowMenu(); public default boolean onInterceptTouchEvent(View view, MotionEvent ev) { return false; } - public default boolean useDefaultMenuItems() { + public default boolean shouldUseDefaultMenuItems() { return false; } - public default void onConfigurationChanged() { - } + /** + * Callback used to signal the menu that its parent's translation has changed. + * @param translation The new x-translation of the menu as a position (not an offset). + */ + public void onParentTranslationUpdate(float translation); + + /** + * Callback used to signal the menu that its parent's height has changed. + */ + public void onParentHeightUpdate(); + + /** + * Callback used to signal the menu that its parent notification has been updated. + * @param sbn + */ + public void onNotificationUpdated(StatusBarNotification sbn); + + /** + * Callback used to signal the menu that a user is moving the parent notification. + * @param delta The change in the parent notification's position. + */ + public void onTouchMove(float delta); + + /** + * Callback used to signal the menu that a user has begun touching its parent notification. + */ + public void onTouchStart(); + + /** + * Callback used to signal the menu that a user has finished touching its parent notification. + */ + public void onTouchEnd(); + + /** + * Callback used to signal the menu that it has been snapped closed. + */ + public void onSnapClosed(); + + /** + * Callback used to signal the menu that it has been snapped open. + */ + public void onSnapOpen(); + + /** + * Callback used to signal the menu that its parent notification has been dismissed. + */ + public void onDismiss(); + + public default void onConfigurationChanged() { } + } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationSwipeActionHelper.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationSwipeActionHelper.java index f6cf03562014..8db0d02548b0 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationSwipeActionHelper.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationSwipeActionHelper.java @@ -39,7 +39,7 @@ public interface NotificationSwipeActionHelper { /** * Call this to snap a notification to provided {@code targetLeft}. */ - public void snap(View animView, float velocity, float targetLeft); + public void snapOpen(View animView, int targetLeft, float velocity); /** * Call this to snooze a notification based on the provided {@link SnoozeOption}. 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 216ed68e4fbe..4aaf2f0b0b9f 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 @@ -1014,7 +1014,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView removeView(mMenuRow.getMenuView()); } mMenuRow = plugin; - if (mMenuRow.useDefaultMenuItems()) { + if (mMenuRow.shouldUseDefaultMenuItems()) { ArrayList<MenuItem> items = new ArrayList<>(); items.add(NotificationMenuRow.createInfoItem(mContext)); items.add(NotificationMenuRow.createSnoozeItem(mContext)); @@ -1764,7 +1764,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView getEntry().expandedIcon.setScrollX((int) -translationX); } if (mMenuRow.getMenuView() != null) { - mMenuRow.onTranslationUpdate(translationX); + mMenuRow.onParentTranslationUpdate(translationX); } } @@ -2269,7 +2269,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView notifyHeightChanged(true /* needsAnimation */); } if (mMenuRow.getMenuView() != null) { - mMenuRow.onHeightUpdate(); + mMenuRow.onParentHeightUpdate(); } updateContentShiftHeight(); if (mLayoutListener != null) { @@ -2520,7 +2520,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mGuts.setActualHeight(height); } if (mMenuRow.getMenuView() != null) { - mMenuRow.onHeightUpdate(); + mMenuRow.onParentHeightUpdate(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java index dec88d43175b..7e60c4b1f348 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java @@ -16,14 +16,13 @@ package com.android.systemui.statusbar.notification.row; -import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION; - import java.util.ArrayList; +import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION; + import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; -import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.statusbar.AlphaOptimizedImageView; import com.android.systemui.statusbar.notification.row.NotificationGuts.GutsContent; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; @@ -38,25 +37,21 @@ import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; -import android.util.Log; import android.service.notification.StatusBarNotification; import android.view.LayoutInflater; -import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.FrameLayout.LayoutParams; +import com.android.internal.annotations.VisibleForTesting; + public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnClickListener, ExpandableNotificationRow.LayoutListener { private static final boolean DEBUG = false; private static final String TAG = "swipe"; - private static final int ICON_ALPHA_ANIM_DURATION = 200; - private static final long SHOW_MENU_DELAY = 60; - private static final long SWIPE_MENU_TIMING = 200; - // Notification must be swiped at least this fraction of a single menu item to show menu private static final float SWIPED_FAR_ENOUGH_MENU_FRACTION = 0.25f; private static final float SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION = 0.15f; @@ -65,6 +60,9 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl // menu item to snap back to menu (else it will cover the menu or it'll be dismissed) private static final float SWIPED_BACK_ENOUGH_TO_COVER_FRACTION = 0.2f; + private static final int ICON_ALPHA_ANIM_DURATION = 200; + private static final long SHOW_MENU_DELAY = 60; + private ExpandableNotificationRow mParent; private Context mContext; @@ -89,22 +87,20 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl private int[] mIconLocation = new int[2]; private int[] mParentLocation = new int[2]; - private float mHorizSpaceForIcon = -1; + private int mHorizSpaceForIcon = -1; private int mVertSpaceForIcons = -1; private int mIconPadding = -1; private int mSidePadding; private float mAlpha = 0f; - private float mPrevX; private CheckForDrag mCheckForDrag; private Handler mHandler; - private boolean mMenuSnappedTo; + private boolean mMenuSnapped; private boolean mMenuSnappedOnLeft; private boolean mShouldShowMenu; - private NotificationSwipeActionHelper mSwipeHelper; private boolean mIsUserTouching; public NotificationMenuRow(Context context) { @@ -134,9 +130,34 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl return mSnoozeItem; } - @Override - public void setSwipeActionHelper(NotificationSwipeActionHelper helper) { - mSwipeHelper = helper; + @VisibleForTesting + protected ExpandableNotificationRow getParent() { + return mParent; + } + + @VisibleForTesting + protected boolean isMenuOnLeft() { + return mOnLeft; + } + + @VisibleForTesting + protected boolean isMenuSnappedOnLeft() { + return mMenuSnappedOnLeft; + } + + @VisibleForTesting + protected boolean isMenuSnapped() { + return mMenuSnapped; + } + + @VisibleForTesting + protected boolean isDismissing() { + return mDismissing; + } + + @VisibleForTesting + protected boolean isSnapping() { + return mSnapping; } @Override @@ -155,17 +176,37 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl return mAlpha > 0; } + @VisibleForTesting + protected boolean isUserTouching() { + return mIsUserTouching; + } + + @Override + public boolean shouldShowMenu() { + return mShouldShowMenu; + } + @Override public View getMenuView() { return mMenuContainer; } + @VisibleForTesting + protected float getTranslation() { + return mTranslation; + } + @Override public void resetMenu() { resetState(true); } @Override + public void onTouchEnd() { + mIsUserTouching = false; + } + + @Override public void onNotificationUpdated(StatusBarNotification sbn) { if (mMenuContainer == null) { // Menu hasn't been created yet, no need to do anything. @@ -222,9 +263,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl mIconsPlaced = false; setMenuLocation(); if (!mIsUserTouching) { - // If the # of items showing changed we need to update the snap position - showMenu(mParent, mOnLeft ? getSpaceForMenu() : -getSpaceForMenu(), - 0 /* velocity */); + onSnapOpen(); } } } @@ -236,7 +275,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl mAnimating = false; mSnapping = false; mDismissing = false; - mMenuSnappedTo = false; + mMenuSnapped = false; setMenuLocation(); if (mMenuListener != null && notify) { mMenuListener.onMenuReset(mParent); @@ -244,185 +283,102 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl } @Override - public boolean onTouchEvent(View view, MotionEvent ev, float velocity) { - final int action = ev.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_DOWN: - mSnapping = false; - if (mFadeAnimator != null) { - mFadeAnimator.cancel(); - } - mHandler.removeCallbacks(mCheckForDrag); - mCheckForDrag = null; - mPrevX = ev.getRawX(); - mIsUserTouching = true; - break; - - case MotionEvent.ACTION_MOVE: - mSnapping = false; - float diffX = ev.getRawX() - mPrevX; - mPrevX = ev.getRawX(); - if (!isTowardsMenu(diffX) && isMenuLocationChange()) { - // Don't consider it "snapped" if location has changed. - mMenuSnappedTo = false; - - // Changed directions, make sure we check to fade in icon again. - if (!mHandler.hasCallbacks(mCheckForDrag)) { - // No check scheduled, set null to schedule a new one. - mCheckForDrag = null; - } else { - // Check scheduled, reset alpha and update location; check will fade it in - setMenuAlpha(0f); - setMenuLocation(); - } - } - if (mShouldShowMenu - && !NotificationStackScrollLayout.isPinnedHeadsUp(view) - && !mParent.areGutsExposed() - && !mParent.isDark() - && (mCheckForDrag == null || !mHandler.hasCallbacks(mCheckForDrag))) { - // Only show the menu if we're not a heads up view and guts aren't exposed. - mCheckForDrag = new CheckForDrag(); - mHandler.postDelayed(mCheckForDrag, SHOW_MENU_DELAY); - } - break; + public void onTouchMove(float delta) { + mSnapping = false; - case MotionEvent.ACTION_UP: - mIsUserTouching = false; - return handleUpEvent(ev, view, velocity); - case MotionEvent.ACTION_CANCEL: - mIsUserTouching = false; - cancelDrag(); - return false; - } - return false; - } + if (!isTowardsMenu(delta) && isMenuLocationChange()) { + // Don't consider it "snapped" if location has changed. + mMenuSnapped = false; - private boolean handleUpEvent(MotionEvent ev, View animView, float velocity) { - // 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 (!mShouldShowMenu) { - if (mSwipeHelper.isDismissGesture(ev)) { - dismiss(animView, velocity); + // Changed directions, make sure we check to fade in icon again. + if (!mHandler.hasCallbacks(mCheckForDrag)) { + // No check scheduled, set null to schedule a new one. + mCheckForDrag = null; } else { - snapBack(animView, velocity); + // Check scheduled, reset alpha and update location; check will fade it in + setMenuAlpha(0f); + setMenuLocation(); } - return true; } + if (mShouldShowMenu + && !NotificationStackScrollLayout.isPinnedHeadsUp(getParent()) + && !mParent.areGutsExposed() + && !mParent.isDark() + && (mCheckForDrag == null || !mHandler.hasCallbacks(mCheckForDrag))) { + // Only show the menu if we're not a heads up view and guts aren't exposed. + mCheckForDrag = new CheckForDrag(); + mHandler.postDelayed(mCheckForDrag, SHOW_MENU_DELAY); + } + } - final boolean gestureTowardsMenu = isTowardsMenu(velocity); - final boolean gestureFastEnough = - mSwipeHelper.getMinDismissVelocity() <= Math.abs(velocity); - final boolean gestureFarEnough = - mSwipeHelper.swipedFarEnough(mTranslation, mParent.getWidth()); - final double timeForGesture = ev.getEventTime() - ev.getDownTime(); - final boolean showMenuForSlowOnGoing = !mParent.canViewBeDismissed() - && timeForGesture >= SWIPE_MENU_TIMING; - final float menuSnapTarget = mOnLeft ? getSpaceForMenu() : -getSpaceForMenu(); - - if (DEBUG) { - Log.d(TAG, "mTranslation= " + mTranslation - + " mAlpha= " + mAlpha - + " velocity= " + velocity - + " mMenuSnappedTo= " + mMenuSnappedTo - + " mMenuSnappedOnLeft= " + mMenuSnappedOnLeft - + " mOnLeft= " + mOnLeft - + " minDismissVel= " + mSwipeHelper.getMinDismissVelocity() - + " isDismissGesture= " + mSwipeHelper.isDismissGesture(ev) - + " gestureTowardsMenu= " + gestureTowardsMenu - + " gestureFastEnough= " + gestureFastEnough - + " gestureFarEnough= " + gestureFarEnough); - } - - if (mMenuSnappedTo && isMenuVisible() && mMenuSnappedOnLeft == mOnLeft) { - // Menu was snapped to previously and we're on the same side, figure out if - // we should stick to the menu, snap back into place, or dismiss - final float maximumSwipeDistance = mHorizSpaceForIcon - * SWIPED_BACK_ENOUGH_TO_COVER_FRACTION; - final float targetLeft = getSpaceForMenu() - maximumSwipeDistance; - final float targetRight = mParent.getWidth() * SWIPED_FAR_ENOUGH_SIZE_FRACTION; - boolean withinSnapMenuThreshold = mOnLeft - ? mTranslation > targetLeft && mTranslation < targetRight - : mTranslation < -targetLeft && mTranslation > -targetRight; - boolean shouldSnapTo = mOnLeft ? mTranslation < targetLeft : mTranslation > -targetLeft; - if (DEBUG) { - Log.d(TAG, " withinSnapMenuThreshold= " + withinSnapMenuThreshold - + " shouldSnapTo= " + shouldSnapTo - + " targetLeft= " + targetLeft - + " targetRight= " + targetRight); - } - if (withinSnapMenuThreshold && !mSwipeHelper.isDismissGesture(ev)) { - // Haven't moved enough to unsnap from the menu - showMenu(animView, menuSnapTarget, velocity); - } else if (mSwipeHelper.isDismissGesture(ev) && !shouldSnapTo) { - // Only dismiss if we're not moving towards the menu - dismiss(animView, velocity); - } else { - snapBack(animView, velocity); - } - } else if (!mSwipeHelper.isFalseGesture(ev) - && (swipedEnoughToShowMenu() && (!gestureFastEnough || showMenuForSlowOnGoing)) - || (gestureTowardsMenu && !mSwipeHelper.isDismissGesture(ev))) { - // Menu has not been snapped to previously and this is menu revealing gesture - showMenu(animView, menuSnapTarget, velocity); - } else if (mSwipeHelper.isDismissGesture(ev) && !gestureTowardsMenu) { - dismiss(animView, velocity); - } else { - snapBack(animView, velocity); + @VisibleForTesting + protected void beginDrag() { + mSnapping = false; + if (mFadeAnimator != null) { + mFadeAnimator.cancel(); } - return true; + mHandler.removeCallbacks(mCheckForDrag); + mCheckForDrag = null; + mIsUserTouching = true; + } + + @Override + public void onTouchStart() { + beginDrag(); } - private void showMenu(View animView, float targetLeft, float velocity) { - mMenuSnappedTo = true; - mMenuSnappedOnLeft = mOnLeft; - mMenuListener.onMenuShown(animView); - mSwipeHelper.snap(animView, targetLeft, velocity); + @Override + public void onSnapOpen() { + mMenuSnapped = true; + mMenuSnappedOnLeft = isMenuOnLeft(); + if (mMenuListener != null) { + mMenuListener.onMenuShown(getParent()); + } } - private void snapBack(View animView, float velocity) { + @Override + public void onSnapClosed() { cancelDrag(); - mMenuSnappedTo = false; + mMenuSnapped = false; mSnapping = true; - mSwipeHelper.snap(animView, 0 /* leftTarget */, velocity); } - private void dismiss(View animView, float velocity) { + @Override + public void onDismiss() { cancelDrag(); - mMenuSnappedTo = false; + mMenuSnapped = false; mDismissing = true; - mSwipeHelper.dismiss(animView, velocity); } - private void cancelDrag() { + @VisibleForTesting + protected void cancelDrag() { if (mFadeAnimator != null) { mFadeAnimator.cancel(); } mHandler.removeCallbacks(mCheckForDrag); } - /** - * @return whether the notification has been translated enough to show the menu and not enough - * to be dismissed. - */ - private boolean swipedEnoughToShowMenu() { - final float multiplier = mParent.canViewBeDismissed() + @VisibleForTesting + protected float getMinimumSwipeDistance() { + final float multiplier = getParent().canViewBeDismissed() ? SWIPED_FAR_ENOUGH_MENU_FRACTION : SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION; - final float minimumSwipeDistance = mHorizSpaceForIcon * multiplier; - return !mSwipeHelper.swipedFarEnough(0, 0) && isMenuVisible() - && (mOnLeft ? mTranslation > minimumSwipeDistance - : mTranslation < -minimumSwipeDistance); + return mHorizSpaceForIcon * multiplier; + } + + @VisibleForTesting + protected float getMaximumSwipeDistance() { + return mHorizSpaceForIcon * SWIPED_BACK_ENOUGH_TO_COVER_FRACTION; } /** * Returns whether the gesture is towards the menu location or not. */ - private boolean isTowardsMenu(float movement) { + @Override + public boolean isTowardsMenu(float movement) { return isMenuVisible() - && ((mOnLeft && movement <= 0) - || (!mOnLeft && movement >= 0)); + && ((isMenuOnLeft() && movement <= 0) + || (!isMenuOnLeft() && movement >= 0)); } @Override @@ -445,7 +401,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl } @Override - public void onHeightUpdate() { + public void onParentHeightUpdate() { if (mParent == null || mMenuItems.size() == 0 || mMenuContainer == null) { return; } @@ -460,7 +416,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl } @Override - public void onTranslationUpdate(float translation) { + public void onParentTranslationUpdate(float translation) { mTranslation = translation; if (mAnimating || !mMenuFadedIn) { // Don't adjust when animating, or if the menu hasn't been shown yet. @@ -492,13 +448,15 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl final int x = mIconLocation[0] - mParentLocation[0] + centerX; final int y = mIconLocation[1] - mParentLocation[1] + centerY; final int index = mMenuContainer.indexOfChild(v); - mMenuListener.onMenuClicked(mParent, x, y, mMenuItems.get(index)); + if (mMenuListener != null) { + mMenuListener.onMenuClicked(mParent, x, y, mMenuItems.get(index)); + } } private boolean isMenuLocationChange() { boolean onLeft = mTranslation > mIconPadding; boolean onRight = mTranslation < -mIconPadding; - if ((mOnLeft && onRight) || (!mOnLeft && onLeft)) { + if ((isMenuOnLeft() && onRight) || (!isMenuOnLeft() && onLeft)) { return true; } return false; @@ -506,7 +464,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl private void setMenuLocation() { boolean showOnLeft = mTranslation > 0; - if ((mIconsPlaced && showOnLeft == mOnLeft) || mSnapping || mMenuContainer == null + if ((mIconsPlaced && showOnLeft == isMenuOnLeft()) || isSnapping() || mMenuContainer == null || !mMenuContainer.isAttachedToWindow()) { // Do nothing return; @@ -522,7 +480,8 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl mIconsPlaced = true; } - private void setMenuAlpha(float alpha) { + @VisibleForTesting + protected void setMenuAlpha(float alpha) { mAlpha = alpha; if (mMenuContainer == null) { return; @@ -542,7 +501,8 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl /** * Returns the horizontal space in pixels required to display the menu. */ - private float getSpaceForMenu() { + @VisibleForTesting + protected int getSpaceForMenu() { return mHorizSpaceForIcon * mMenuContainer.getChildCount(); } @@ -646,12 +606,71 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl parent.addView(menuView); menuView.setOnClickListener(this); FrameLayout.LayoutParams lp = (LayoutParams) menuView.getLayoutParams(); - lp.width = (int) mHorizSpaceForIcon; - lp.height = (int) mHorizSpaceForIcon; + lp.width = mHorizSpaceForIcon; + lp.height = mHorizSpaceForIcon; menuView.setLayoutParams(lp); } } + @VisibleForTesting + /** + * Determine the minimum offset below which the menu should snap back closed. + */ + protected float getSnapBackThreshold() { + return getSpaceForMenu() - getMaximumSwipeDistance(); + } + + /** + * Determine the maximum offset above which the parent notification should be dismissed. + * @return + */ + @VisibleForTesting + protected float getDismissThreshold() { + return getParent().getWidth() * SWIPED_FAR_ENOUGH_SIZE_FRACTION; + } + + @Override + public boolean isWithinSnapMenuThreshold() { + float translation = getTranslation(); + float snapBackThreshold = getSnapBackThreshold(); + float targetRight = getDismissThreshold(); + return isMenuOnLeft() + ? translation > snapBackThreshold && translation < targetRight + : translation < -snapBackThreshold && translation > -targetRight; + } + + @Override + public boolean isSwipedEnoughToShowMenu() { + final float minimumSwipeDistance = getMinimumSwipeDistance(); + final float translation = getTranslation(); + return isMenuVisible() && (isMenuOnLeft() ? + translation > minimumSwipeDistance + : translation < -minimumSwipeDistance); + } + + @Override + public int getMenuSnapTarget() { + return isMenuOnLeft() ? getSpaceForMenu() : -getSpaceForMenu(); + } + + @Override + public boolean shouldSnapBack() { + float translation = getTranslation(); + float targetLeft = getSnapBackThreshold(); + return isMenuOnLeft() ? translation < targetLeft : translation > -targetLeft; + } + + @Override + public boolean isSnappedAndOnSameSide() { + return isMenuSnapped() && isMenuVisible() + && isMenuSnappedOnLeft() == isMenuOnLeft(); + } + + @Override + public boolean canBeDismissed() { + return getParent().canViewBeDismissed(); + } + public static class NotificationMenuItem implements MenuItem { View mMenuView; GutsContent mGutsContent; 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 da98565f9324..4b38ef56be77 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 @@ -26,7 +26,6 @@ import android.animation.PropertyValuesHolder; import android.animation.TimeAnimator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.annotation.FloatRange; import android.annotation.Nullable; import android.app.WallpaperManager; import android.content.Context; @@ -5316,13 +5315,15 @@ public class NotificationStackScrollLayout extends ViewGroup void flingTopOverscroll(float velocity, boolean open); } - @ShadeViewRefactor(RefactorComponent.INPUT) - private class NotificationSwipeHelper extends SwipeHelper + @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(); @@ -5338,7 +5339,7 @@ public class NotificationStackScrollLayout extends ViewGroup public void onDownUpdate(View currView, MotionEvent ev) { mTranslatingParentView = currView; if (mCurrMenuRow != null) { - mCurrMenuRow.onTouchEvent(currView, ev, 0 /* velocity */); + mCurrMenuRow.onTouchStart(); } mCurrMenuRow = null; mHandler.removeCallbacks(mFalsingCheck); @@ -5351,18 +5352,21 @@ public class NotificationStackScrollLayout extends ViewGroup if (row.getEntry().hasFinishedInitialization()) { mCurrMenuRow = row.createMenu(); - mCurrMenuRow.setSwipeActionHelper(NotificationSwipeHelper.this); mCurrMenuRow.setMenuClickListener(NotificationStackScrollLayout.this); - mCurrMenuRow.onTouchEvent(currView, ev, 0 /* velocity */); + 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.onTouchEvent(view, ev, 0 /* velocity */); + mCurrMenuRow.onTouchMove(delta); } } @@ -5370,12 +5374,92 @@ public class NotificationStackScrollLayout extends ViewGroup public boolean handleUpEvent(MotionEvent ev, View animView, float velocity, float translation) { if (mCurrMenuRow != null) { - return mCurrMenuRow.onTouchEvent(animView, ev, velocity); + 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); @@ -5404,10 +5488,6 @@ public class NotificationStackScrollLayout extends ViewGroup mStatusBar.setNotificationSnoozed(sbn, snoozeOption); } - public boolean isFalseGesture(MotionEvent ev) { - return super.isFalseGesture(ev); - } - private void handleMenuCoveredOrDismissed() { if (mMenuExposedView != null && mMenuExposedView == mTranslatingParentView) { mMenuExposedView = null; @@ -5441,13 +5521,12 @@ public class NotificationStackScrollLayout extends ViewGroup } @Override - public void snap(View animView, float targetLeft, float velocity) { + public void snapOpen(View animView, int targetLeft, float velocity) { snapChild(animView, targetLeft, velocity); } - @Override - public boolean swipedFarEnough(float translation, float viewSize) { - return swipedFarEnough(); + private void snapBack(View animView, float velocity) { + snapChild(animView, 0, velocity); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java index 06265e5127ef..18dd1fd09ac0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java @@ -15,10 +15,15 @@ package com.android.systemui.statusbar.notification.row; import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.doNothing; import android.app.Notification; import android.service.notification.StatusBarNotification; @@ -27,7 +32,6 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; import android.testing.ViewUtils; -import android.testing.ViewUtils; import android.view.ViewGroup; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; @@ -36,6 +40,7 @@ import com.android.systemui.utils.leaks.LeakCheckedTest; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; @RunWith(AndroidTestingRunner.class) @RunWithLooper(setAsMainLooper = true) @@ -72,6 +77,7 @@ public class NotificationMenuRowTest extends LeakCheckedTest { row.resetMenu(); } + @Test public void testNoAppOpsInSlowSwipe() { NotificationMenuRow row = new NotificationMenuRow(mContext); @@ -86,4 +92,255 @@ public class NotificationMenuRowTest extends LeakCheckedTest { // one for snooze and one for noti blocking assertEquals(2, container.getChildCount()); } + + @Test + public void testIsSnappedAndOnSameSide() { + NotificationMenuRow row = Mockito.spy(new NotificationMenuRow((mContext))); + + when(row.isMenuVisible()).thenReturn(true); + when(row.isMenuSnapped()).thenReturn(true); + when(row.isMenuOnLeft()).thenReturn(true); + when(row.isMenuSnappedOnLeft()).thenReturn(true); + + assertTrue("Showing on left and on left", row.isSnappedAndOnSameSide()); + + + when(row.isMenuOnLeft()).thenReturn(false); + when(row.isMenuSnappedOnLeft()).thenReturn(false); + assertTrue("Snapped to right and on right", row.isSnappedAndOnSameSide()); + + when(row.isMenuOnLeft()).thenReturn(true); + when(row.isMenuSnapped()).thenReturn(false); + assertFalse("Snapped to right and on left", row.isSnappedAndOnSameSide()); + + when(row.isMenuOnLeft()).thenReturn(true); + when(row.isMenuSnappedOnLeft()).thenReturn(true); + when(row.isMenuVisible()).thenReturn(false); + assertFalse("Snapped to left and on left, but menu not visible", + row.isSnappedAndOnSameSide()); + + when(row.isMenuVisible()).thenReturn(true); + when(row.isMenuSnapped()).thenReturn(false); + assertFalse("Snapped to left and on left, but not actually snapped to", + row.isSnappedAndOnSameSide()); + } + + @Test + public void testGetMenuSnapTarget() { + NotificationMenuRow row = Mockito.spy(new NotificationMenuRow((mContext))); + when(row.isMenuOnLeft()).thenReturn(true); + doReturn(30).when(row).getSpaceForMenu(); + + assertEquals("When on left, snap target is space for menu", + 30, row.getMenuSnapTarget()); + + when(row.isMenuOnLeft()).thenReturn(false); + assertEquals("When on right, snap target is negative space for menu", + -30, row.getMenuSnapTarget()); + } + + @Test + public void testIsSwipedEnoughToShowMenu() { + NotificationMenuRow row = Mockito.spy(new NotificationMenuRow((mContext))); + when(row.isMenuVisible()).thenReturn(true); + when(row.isMenuOnLeft()).thenReturn(true); + doReturn(40f).when(row).getMinimumSwipeDistance(); + + when(row.getTranslation()).thenReturn(30f); + assertFalse("on left, translation is less than min", row.isSwipedEnoughToShowMenu()); + + when(row.getTranslation()).thenReturn(50f); + assertTrue("on left, translation is greater than min", row.isSwipedEnoughToShowMenu()); + + when(row.isMenuOnLeft()).thenReturn(false); + when(row.getTranslation()).thenReturn(-30f); + assertFalse("on right, translation is greater than -min", row.isSwipedEnoughToShowMenu()); + + when(row.getTranslation()).thenReturn(-50f); + assertTrue("on right, translation is less than -min", row.isSwipedEnoughToShowMenu()); + + when(row.isMenuVisible()).thenReturn(false); + when(row.getTranslation()).thenReturn(30f); + assertFalse("on left, translation greater than min, but not visible", + row.isSwipedEnoughToShowMenu()); + } + + @Test + public void testIsWithinSnapMenuThreshold() { + NotificationMenuRow row = Mockito.spy(new NotificationMenuRow((mContext))); + doReturn(30f).when(row).getSnapBackThreshold(); + doReturn(50f).when(row).getDismissThreshold(); + + when(row.isMenuOnLeft()).thenReturn(true); + when(row.getTranslation()).thenReturn(40f); + assertTrue("When on left, translation is between min and max", + row.isWithinSnapMenuThreshold()); + + when(row.getTranslation()).thenReturn(20f); + assertFalse("When on left, translation is less than min", + row.isWithinSnapMenuThreshold()); + + when(row.getTranslation()).thenReturn(60f); + assertFalse("When on left, translation is greater than max", + row.isWithinSnapMenuThreshold()); + + when(row.isMenuOnLeft()).thenReturn(false); + when(row.getTranslation()).thenReturn(-40f); + assertTrue("When on right, translation is between -min and -max", + row.isWithinSnapMenuThreshold()); + + when(row.getTranslation()).thenReturn(-20f); + assertFalse("When on right, translation is greater than -min", + row.isWithinSnapMenuThreshold()); + + when(row.getTranslation()).thenReturn(-60f); + assertFalse("When on right, translation is less than -max", + row.isWithinSnapMenuThreshold()); + } + + @Test + public void testShouldSnapBack() { + NotificationMenuRow row = Mockito.spy(new NotificationMenuRow((mContext))); + doReturn(40f).when(row).getSnapBackThreshold(); + when(row.isMenuVisible()).thenReturn(false); + when(row.isMenuOnLeft()).thenReturn(true); + + when(row.getTranslation()).thenReturn(50f); + assertFalse("On left, translation greater than minimum target", row.shouldSnapBack()); + + when(row.getTranslation()).thenReturn(30f); + assertTrue("On left, translation less than minimum target", row.shouldSnapBack()); + + when(row.isMenuOnLeft()).thenReturn(false); + when(row.getTranslation()).thenReturn(-50f); + assertFalse("On right, translation less than minimum target", row.shouldSnapBack()); + + when(row.getTranslation()).thenReturn(-30f); + assertTrue("On right, translation greater than minimum target", row.shouldSnapBack()); + } + + @Test + public void testCanBeDismissed() { + NotificationMenuRow row = Mockito.spy(new NotificationMenuRow((mContext))); + ExpandableNotificationRow parent = mock(ExpandableNotificationRow.class); + + when(row.getParent()).thenReturn(parent); + when(parent.canViewBeDismissed()).thenReturn(true); + + assertTrue("Row can be dismissed if parent can be dismissed", row.canBeDismissed()); + + when(parent.canViewBeDismissed()).thenReturn(false); + assertFalse("Row cannot be dismissed if parent cannot be dismissed", + row.canBeDismissed()); + } + + @Test + public void testIsTowardsMenu() { + NotificationMenuRow row = Mockito.spy(new NotificationMenuRow((mContext))); + when(row.isMenuVisible()).thenReturn(true); + when(row.isMenuOnLeft()).thenReturn(true); + + assertTrue("menu on left, movement is negative", row.isTowardsMenu(-30f)); + assertFalse("menu on left, movement is positive", row.isTowardsMenu(30f)); + assertTrue("menu on left, movement is 0", row.isTowardsMenu(0f)); + + when(row.isMenuOnLeft()).thenReturn(false); + assertTrue("menu on right, movement is positive", row.isTowardsMenu(30f)); + assertFalse("menu on right, movement is negative", row.isTowardsMenu(-30f)); + assertTrue("menu on right, movement is 0", row.isTowardsMenu(0f)); + + when(row.isMenuVisible()).thenReturn(false); + assertFalse("menu on left, movement is negative, but menu not visible", + row.isTowardsMenu(-30f)); + } + + @Test + public void onSnapBack() { + NotificationMenuRow row = Mockito.spy(new NotificationMenuRow((mContext))); + NotificationMenuRowPlugin.OnMenuEventListener listener = mock(NotificationMenuRowPlugin + .OnMenuEventListener.class); + row.setMenuClickListener(listener); + ExpandableNotificationRow parent = mock(ExpandableNotificationRow.class); + when(row.getParent()).thenReturn(parent); + doNothing().when(row).cancelDrag(); + + row.onSnapOpen(); + + assertTrue("before onSnapClosed, row is snapped to", row.isMenuSnapped()); + assertFalse("before onSnapClosed, row is not snapping", row.isSnapping()); + + row.onSnapClosed(); + + assertFalse("after onSnapClosed, row is not snapped to", row.isMenuSnapped()); + assertTrue("after onSnapClosed, row is snapping", row.isSnapping()); + } + + @Test + public void testOnSnap() { + NotificationMenuRow row = Mockito.spy(new NotificationMenuRow((mContext))); + when(row.isMenuOnLeft()).thenReturn(true); + NotificationMenuRowPlugin.OnMenuEventListener listener = mock(NotificationMenuRowPlugin + .OnMenuEventListener.class); + row.setMenuClickListener(listener); + ExpandableNotificationRow parent = mock(ExpandableNotificationRow.class); + when(row.getParent()).thenReturn(parent); + + assertFalse("before onSnapOpen, row is not snapped to", row.isMenuSnapped()); + assertFalse("before onSnapOpen, row is not snapped on left", row.isMenuSnappedOnLeft()); + + row.onSnapOpen(); + + assertTrue("after onSnapOpen, row is snapped to", row.isMenuSnapped()); + assertTrue("after onSnapOpen, row is snapped on left", row.isMenuSnapped()); + verify(listener, times(1)).onMenuShown(parent); + } + + @Test + public void testOnDismiss() { + NotificationMenuRow row = Mockito.spy(new NotificationMenuRow((mContext))); + doNothing().when(row).cancelDrag(); + row.onSnapOpen(); + + assertFalse("before onDismiss, row is not dismissing", row.isDismissing()); + assertTrue("before onDismiss, row is showing", row.isMenuSnapped()); + + row.onDismiss(); + + verify(row, times(1)).cancelDrag(); + assertTrue("after onDismiss, row is dismissing", row.isDismissing()); + assertFalse("after onDismiss, row is not showing", row.isMenuSnapped()); + } + + @Test + public void testOnDown() { + NotificationMenuRow row = Mockito.spy(new NotificationMenuRow((mContext))); + doNothing().when(row).beginDrag(); + + row.onTouchStart(); + + verify(row, times(1)).beginDrag(); + } + + @Test + public void testOnUp() { + NotificationMenuRow row = Mockito.spy(new NotificationMenuRow((mContext))); + row.onTouchStart(); + + assertTrue("before onTouchEnd, isUserTouching is true", row.isUserTouching()); + + row.onTouchEnd(); + + assertFalse("after onTouchEnd, isUserTouching is false", row.isUserTouching()); + } + + @Test + public void testIsMenuVisible() { + NotificationMenuRow row = Mockito.spy(new NotificationMenuRow((mContext))); + row.setMenuAlpha(0); + + assertFalse("when alpha is 0, menu is not visible", row.isMenuVisible()); + + row.setMenuAlpha(0.5f); + assertTrue("when alpha is .5, menu is visible", row.isMenuVisible()); + } } |