1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.systemui.statusbar.notification.row;
18 
19 import static android.view.HapticFeedbackConstants.CLOCK_TICK;
20 
21 import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION;
22 
23 import android.animation.Animator;
24 import android.animation.AnimatorListenerAdapter;
25 import android.animation.ValueAnimator;
26 import android.annotation.Nullable;
27 import android.content.Context;
28 import android.content.res.Resources;
29 import android.graphics.Point;
30 import android.graphics.drawable.Drawable;
31 import android.os.Handler;
32 import android.os.Looper;
33 import android.provider.Settings;
34 import android.service.notification.StatusBarNotification;
35 import android.util.ArrayMap;
36 import android.view.LayoutInflater;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.widget.FrameLayout;
40 import android.widget.FrameLayout.LayoutParams;
41 
42 import com.android.app.animation.Interpolators;
43 import com.android.internal.annotations.VisibleForTesting;
44 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
45 import com.android.systemui.res.R;
46 import com.android.systemui.statusbar.AlphaOptimizedImageView;
47 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
48 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
49 import com.android.systemui.statusbar.notification.row.NotificationGuts.GutsContent;
50 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 import java.util.Map;
55 
56 public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnClickListener,
57         ExpandableNotificationRow.LayoutListener {
58 
59     // Notification must be swiped at least this fraction of a single menu item to show menu
60     private static final float SWIPED_FAR_ENOUGH_MENU_FRACTION = 0.25f;
61     private static final float SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION = 0.15f;
62 
63     // When the menu is displayed, the notification must be swiped within this fraction of a single
64     // menu item to snap back to menu (else it will cover the menu or it'll be dismissed)
65     private static final float SWIPED_BACK_ENOUGH_TO_COVER_FRACTION = 0.2f;
66 
67     private static final int ICON_ALPHA_ANIM_DURATION = 200;
68     private static final long SHOW_MENU_DELAY = 60;
69 
70     private ExpandableNotificationRow mParent;
71 
72     private Context mContext;
73     private FrameLayout mMenuContainer;
74     private NotificationMenuItem mInfoItem;
75     private MenuItem mFeedbackItem;
76     private MenuItem mSnoozeItem;
77     private ArrayList<MenuItem> mLeftMenuItems;
78     private ArrayList<MenuItem> mRightMenuItems;
79     private final Map<View, MenuItem> mMenuItemsByView = new ArrayMap<>();
80     private OnMenuEventListener mMenuListener;
81 
82     private ValueAnimator mFadeAnimator;
83     private boolean mAnimating;
84     private boolean mMenuFadedIn;
85 
86     private boolean mOnLeft;
87     private boolean mIconsPlaced;
88 
89     private boolean mDismissing;
90     private boolean mSnapping;
91     private float mTranslation;
92 
93     private int[] mIconLocation = new int[2];
94     private int[] mParentLocation = new int[2];
95 
96     private int mHorizSpaceForIcon = -1;
97     private int mVertSpaceForIcons = -1;
98     private int mIconPadding = -1;
99     private int mSidePadding;
100 
101     private float mAlpha = 0f;
102 
103     private CheckForDrag mCheckForDrag;
104     private Handler mHandler;
105 
106     private boolean mMenuSnapped;
107     private boolean mMenuSnappedOnLeft;
108     private boolean mShouldShowMenu;
109 
110     private boolean mIsUserTouching;
111 
112     private boolean mSnappingToDismiss;
113 
114     private final PeopleNotificationIdentifier mPeopleNotificationIdentifier;
115 
NotificationMenuRow(Context context, PeopleNotificationIdentifier peopleNotificationIdentifier)116     public NotificationMenuRow(Context context,
117             PeopleNotificationIdentifier peopleNotificationIdentifier) {
118         mContext = context;
119         mShouldShowMenu = context.getResources().getBoolean(R.bool.config_showNotificationGear);
120         mHandler = new Handler(Looper.getMainLooper());
121         mLeftMenuItems = new ArrayList<>();
122         mRightMenuItems = new ArrayList<>();
123         mPeopleNotificationIdentifier = peopleNotificationIdentifier;
124     }
125 
126     @Override
getMenuItems(Context context)127     public ArrayList<MenuItem> getMenuItems(Context context) {
128         return mOnLeft ? mLeftMenuItems : mRightMenuItems;
129     }
130 
131     @Override
getLongpressMenuItem(Context context)132     public MenuItem getLongpressMenuItem(Context context) {
133         return mInfoItem;
134     }
135 
136     @Override
getFeedbackMenuItem(Context context)137     public MenuItem getFeedbackMenuItem(Context context) {
138         return mFeedbackItem;
139     }
140 
141     @Override
getSnoozeMenuItem(Context context)142     public MenuItem getSnoozeMenuItem(Context context) {
143         return mSnoozeItem;
144     }
145 
146     @VisibleForTesting
getParent()147     protected ExpandableNotificationRow getParent() {
148         return mParent;
149     }
150 
151     @VisibleForTesting
isMenuOnLeft()152     protected boolean isMenuOnLeft() {
153         return mOnLeft;
154     }
155 
156     @VisibleForTesting
isMenuSnappedOnLeft()157     protected boolean isMenuSnappedOnLeft() {
158         return mMenuSnappedOnLeft;
159     }
160 
161     @VisibleForTesting
isMenuSnapped()162     protected boolean isMenuSnapped() {
163         return mMenuSnapped;
164     }
165 
166     @VisibleForTesting
isDismissing()167     protected boolean isDismissing() {
168         return mDismissing;
169     }
170 
171     @VisibleForTesting
isSnapping()172     protected boolean isSnapping() {
173         return mSnapping;
174     }
175 
176     @VisibleForTesting
isSnappingToDismiss()177     protected boolean isSnappingToDismiss() {
178         return mSnappingToDismiss;
179     }
180 
181     @Override
setMenuClickListener(OnMenuEventListener listener)182     public void setMenuClickListener(OnMenuEventListener listener) {
183         mMenuListener = listener;
184     }
185 
186     @Override
createMenu(ViewGroup parent, StatusBarNotification sbn)187     public void createMenu(ViewGroup parent, StatusBarNotification sbn) {
188         mParent = (ExpandableNotificationRow) parent;
189         createMenuViews(true /* resetState */);
190     }
191 
192     @Override
isMenuVisible()193     public boolean isMenuVisible() {
194         return mAlpha > 0;
195     }
196 
197     @VisibleForTesting
isUserTouching()198     protected boolean isUserTouching() {
199         return mIsUserTouching;
200     }
201 
202     @Override
shouldShowMenu()203     public boolean shouldShowMenu() {
204         return mShouldShowMenu;
205     }
206 
207     @Override
getMenuView()208     public View getMenuView() {
209         return mMenuContainer;
210     }
211 
212     @VisibleForTesting
getTranslation()213     protected float getTranslation() {
214         return mTranslation;
215     }
216 
217     @Override
resetMenu()218     public void resetMenu() {
219         resetState(true);
220     }
221 
222     @Override
onTouchEnd()223     public void onTouchEnd() {
224         mIsUserTouching = false;
225     }
226 
227     @Override
onNotificationUpdated(StatusBarNotification sbn)228     public void onNotificationUpdated(StatusBarNotification sbn) {
229         if (mMenuContainer == null) {
230             // Menu hasn't been created yet, no need to do anything.
231             return;
232         }
233         createMenuViews(!isMenuVisible() /* resetState */);
234     }
235 
236     @Override
onConfigurationChanged()237     public void onConfigurationChanged() {
238         mParent.setLayoutListener(this);
239     }
240 
241     @Override
onLayout()242     public void onLayout() {
243         mIconsPlaced = false; // Force icons to be re-placed
244         setMenuLocation();
245         mParent.setLayoutListener(null);
246     }
247 
createMenuViews(boolean resetState)248     private void createMenuViews(boolean resetState) {
249         final Resources res = mContext.getResources();
250         mHorizSpaceForIcon = res.getDimensionPixelSize(R.dimen.notification_menu_icon_size);
251         mVertSpaceForIcons = res.getDimensionPixelSize(R.dimen.notification_min_height);
252         mLeftMenuItems.clear();
253         mRightMenuItems.clear();
254 
255         final boolean showSnooze = mParent.getShowSnooze();
256         // Construct the menu items based on the notification
257         if (showSnooze) {
258             // Only show snooze for non-foreground notifications, and if the setting is on
259             mSnoozeItem = createSnoozeItem(mContext);
260         }
261         mFeedbackItem = createFeedbackItem(mContext);
262         NotificationEntry entry = mParent.getEntry();
263         int personNotifType = mPeopleNotificationIdentifier.getPeopleNotificationType(entry);
264         if (personNotifType == PeopleNotificationIdentifier.TYPE_PERSON) {
265             mInfoItem = createPartialConversationItem(mContext);
266         } else if (personNotifType >= PeopleNotificationIdentifier.TYPE_FULL_PERSON) {
267             mInfoItem = createConversationItem(mContext);
268         } else {
269             mInfoItem = createInfoItem(mContext);
270         }
271 
272         if (showSnooze) {
273             mRightMenuItems.add(mSnoozeItem);
274         }
275         mRightMenuItems.add(mInfoItem);
276         mRightMenuItems.add(mFeedbackItem);
277         mLeftMenuItems.addAll(mRightMenuItems);
278 
279         populateMenuViews();
280         if (resetState) {
281             resetState(false /* notify */);
282         } else {
283             mIconsPlaced = false;
284             setMenuLocation();
285             if (!mIsUserTouching) {
286                 onSnapOpen();
287             }
288         }
289     }
290 
populateMenuViews()291     private void populateMenuViews() {
292         if (mMenuContainer != null) {
293             mMenuContainer.removeAllViews();
294             mMenuItemsByView.clear();
295         } else {
296             mMenuContainer = new FrameLayout(mContext);
297         }
298         final int showDismissSetting =  Settings.Global.getInt(mContext.getContentResolver(),
299                 Settings.Global.SHOW_NEW_NOTIF_DISMISS, /* default = */ 1);
300         final boolean newFlowHideShelf = showDismissSetting == 1;
301         if (newFlowHideShelf) {
302             return;
303         }
304         List<MenuItem> menuItems = mOnLeft ? mLeftMenuItems : mRightMenuItems;
305         for (int i = 0; i < menuItems.size(); i++) {
306             addMenuView(menuItems.get(i), mMenuContainer);
307         }
308     }
309 
resetState(boolean notify)310     private void resetState(boolean notify) {
311         setMenuAlpha(0f);
312         mIconsPlaced = false;
313         mMenuFadedIn = false;
314         mAnimating = false;
315         mSnapping = false;
316         mDismissing = false;
317         mMenuSnapped = false;
318         setMenuLocation();
319         if (mMenuListener != null && notify) {
320             mMenuListener.onMenuReset(mParent);
321         }
322     }
323 
324     @Override
onTouchMove(float delta)325     public void onTouchMove(float delta) {
326         mSnapping = false;
327 
328         if (!isTowardsMenu(delta) && isMenuLocationChange()) {
329             // Don't consider it "snapped" if location has changed.
330             mMenuSnapped = false;
331 
332             // Changed directions, make sure we check to fade in icon again.
333             if (!mHandler.hasCallbacks(mCheckForDrag)) {
334                 // No check scheduled, set null to schedule a new one.
335                 mCheckForDrag = null;
336             } else {
337                 // Check scheduled, reset alpha and update location; check will fade it in
338                 setMenuAlpha(0f);
339                 setMenuLocation();
340             }
341         }
342         if (mShouldShowMenu
343                 && !NotificationStackScrollLayout.isPinnedHeadsUp(getParent())
344                 && !mParent.areGutsExposed()
345                 && !mParent.showingPulsing()
346                 && (mCheckForDrag == null || !mHandler.hasCallbacks(mCheckForDrag))) {
347             // Only show the menu if we're not a heads up view and guts aren't exposed.
348             mCheckForDrag = new CheckForDrag();
349             mHandler.postDelayed(mCheckForDrag, SHOW_MENU_DELAY);
350         }
351         if (canBeDismissed()) {
352             final float dismissThreshold = getDismissThreshold();
353             final boolean snappingToDismiss = delta < -dismissThreshold || delta > dismissThreshold;
354             if (mSnappingToDismiss != snappingToDismiss) {
355                 getMenuView().performHapticFeedback(CLOCK_TICK);
356             }
357             mSnappingToDismiss = snappingToDismiss;
358         }
359     }
360 
361     @VisibleForTesting
beginDrag()362     protected void beginDrag() {
363         mSnapping = false;
364         if (mFadeAnimator != null) {
365             mFadeAnimator.cancel();
366         }
367         mHandler.removeCallbacks(mCheckForDrag);
368         mCheckForDrag = null;
369         mIsUserTouching = true;
370     }
371 
372     @Override
onTouchStart()373     public void onTouchStart() {
374         beginDrag();
375         mSnappingToDismiss = false;
376     }
377 
378     @Override
onSnapOpen()379     public void onSnapOpen() {
380         mMenuSnapped = true;
381         mMenuSnappedOnLeft = isMenuOnLeft();
382         if (mAlpha == 0f && mParent != null) {
383             fadeInMenu(mParent.getWidth());
384         }
385         if (mMenuListener != null) {
386             mMenuListener.onMenuShown(getParent());
387         }
388     }
389 
390     @Override
onSnapClosed()391     public void onSnapClosed() {
392         cancelDrag();
393         mMenuSnapped = false;
394         mSnapping = true;
395     }
396 
397     @Override
onDismiss()398     public void onDismiss() {
399         cancelDrag();
400         mMenuSnapped = false;
401         mDismissing = true;
402     }
403 
404     @VisibleForTesting
cancelDrag()405     protected void cancelDrag() {
406         if (mFadeAnimator != null) {
407             mFadeAnimator.cancel();
408         }
409         mHandler.removeCallbacks(mCheckForDrag);
410     }
411 
412     @VisibleForTesting
getMinimumSwipeDistance()413     protected float getMinimumSwipeDistance() {
414         final float multiplier = getParent().canViewBeDismissed()
415                 ? SWIPED_FAR_ENOUGH_MENU_FRACTION
416                 : SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION;
417         return mHorizSpaceForIcon * multiplier;
418     }
419 
420     @VisibleForTesting
getMaximumSwipeDistance()421     protected float getMaximumSwipeDistance() {
422         return mHorizSpaceForIcon * SWIPED_BACK_ENOUGH_TO_COVER_FRACTION;
423     }
424 
425     /**
426      * Returns whether the gesture is towards the menu location or not.
427      */
428     @Override
isTowardsMenu(float movement)429     public boolean isTowardsMenu(float movement) {
430         return isMenuVisible()
431                 && ((isMenuOnLeft() && movement <= 0)
432                         || (!isMenuOnLeft() && movement >= 0));
433     }
434 
435     @Override
setAppName(String appName)436     public void setAppName(String appName) {
437         if (appName == null) {
438             return;
439         }
440         setAppName(appName, mLeftMenuItems);
441         setAppName(appName, mRightMenuItems);
442     }
443 
setAppName(String appName, ArrayList<MenuItem> menuItems)444     private void setAppName(String appName,
445             ArrayList<MenuItem> menuItems) {
446         Resources res = mContext.getResources();
447         final int count = menuItems.size();
448         for (int i = 0; i < count; i++) {
449             MenuItem item = menuItems.get(i);
450             String description = String.format(
451                     res.getString(R.string.notification_menu_accessibility),
452                     appName, item.getContentDescription());
453             View menuView = item.getMenuView();
454             if (menuView != null) {
455                 menuView.setContentDescription(description);
456             }
457         }
458     }
459 
460     @Override
onParentHeightUpdate()461     public void onParentHeightUpdate() {
462         if (mParent == null
463                 || (mLeftMenuItems.isEmpty() && mRightMenuItems.isEmpty())
464                 || mMenuContainer == null) {
465             return;
466         }
467         int parentHeight = mParent.getActualHeight();
468         float translationY;
469         if (parentHeight < mVertSpaceForIcons) {
470             translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2);
471         } else {
472             translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2;
473         }
474         mMenuContainer.setTranslationY(translationY);
475     }
476 
477     @Override
onParentTranslationUpdate(float translation)478     public void onParentTranslationUpdate(float translation) {
479         mTranslation = translation;
480         if (mAnimating || !mMenuFadedIn) {
481             // Don't adjust when animating, or if the menu hasn't been shown yet.
482             return;
483         }
484         final float fadeThreshold = mParent.getWidth() * 0.3f;
485         final float absTrans = Math.abs(translation);
486         float desiredAlpha = 0;
487         if (absTrans == 0) {
488             desiredAlpha = 0;
489         } else if (absTrans <= fadeThreshold) {
490             desiredAlpha = 1;
491         } else {
492             desiredAlpha = 1 - ((absTrans - fadeThreshold) / (mParent.getWidth() - fadeThreshold));
493         }
494         setMenuAlpha(desiredAlpha);
495     }
496 
497     @Override
onClick(View v)498     public void onClick(View v) {
499         if (mMenuListener == null) {
500             // Nothing to do
501             return;
502         }
503         v.getLocationOnScreen(mIconLocation);
504         mParent.getLocationOnScreen(mParentLocation);
505         final int centerX = mHorizSpaceForIcon / 2;
506         final int centerY = v.getHeight() / 2;
507         final int x = mIconLocation[0] - mParentLocation[0] + centerX;
508         final int y = mIconLocation[1] - mParentLocation[1] + centerY;
509         if (mMenuItemsByView.containsKey(v)) {
510             mMenuListener.onMenuClicked(mParent, x, y, mMenuItemsByView.get(v));
511         }
512     }
513 
isMenuLocationChange()514     private boolean isMenuLocationChange() {
515         boolean onLeft = mTranslation > mIconPadding;
516         boolean onRight = mTranslation < -mIconPadding;
517         if ((isMenuOnLeft() && onRight) || (!isMenuOnLeft() && onLeft)) {
518             return true;
519         }
520         return false;
521     }
522 
523     private void setMenuLocation() {
524         boolean showOnLeft = mTranslation > 0;
525         if ((mIconsPlaced && showOnLeft == isMenuOnLeft()) || isSnapping() || mMenuContainer == null
526                 || !mMenuContainer.isAttachedToWindow()) {
527             // Do nothing
528             return;
529         }
530         boolean wasOnLeft = mOnLeft;
531         mOnLeft = showOnLeft;
532         if (wasOnLeft != showOnLeft) {
533             populateMenuViews();
534         }
535         final int count = mMenuContainer.getChildCount();
536         for (int i = 0; i < count; i++) {
537             final View v = mMenuContainer.getChildAt(i);
538             final float left = i * mHorizSpaceForIcon;
539             final float right = mParent.getWidth() - (mHorizSpaceForIcon * (i + 1));
540             v.setX(showOnLeft ? left : right);
541         }
542         mIconsPlaced = true;
543     }
544 
545     @VisibleForTesting
setMenuAlpha(float alpha)546     protected void setMenuAlpha(float alpha) {
547         mAlpha = alpha;
548         if (mMenuContainer == null) {
549             return;
550         }
551         if (alpha == 0) {
552             mMenuFadedIn = false; // Can fade in again once it's gone.
553             mMenuContainer.setVisibility(View.INVISIBLE);
554         } else {
555             mMenuContainer.setVisibility(View.VISIBLE);
556         }
557         final int count = mMenuContainer.getChildCount();
558         for (int i = 0; i < count; i++) {
559             mMenuContainer.getChildAt(i).setAlpha(mAlpha);
560         }
561     }
562 
563     /**
564      * Returns the horizontal space in pixels required to display the menu.
565      */
566     @VisibleForTesting
getSpaceForMenu()567     protected int getSpaceForMenu() {
568         return mHorizSpaceForIcon * mMenuContainer.getChildCount();
569     }
570 
571     private final class CheckForDrag implements Runnable {
572         @Override
run()573         public void run() {
574             final float absTransX = Math.abs(mTranslation);
575             final float bounceBackToMenuWidth = getSpaceForMenu();
576             final float notiThreshold = mParent.getWidth() * 0.4f;
577             if ((!isMenuVisible() || isMenuLocationChange())
578                     && absTransX >= bounceBackToMenuWidth * 0.4
579                     && absTransX < notiThreshold) {
580                 fadeInMenu(notiThreshold);
581             }
582         }
583     }
584 
fadeInMenu(final float notiThreshold)585     private void fadeInMenu(final float notiThreshold) {
586         if (mDismissing || mAnimating) {
587             return;
588         }
589         if (isMenuLocationChange()) {
590             setMenuAlpha(0f);
591         }
592         final float transX = mTranslation;
593         final boolean fromLeft = mTranslation > 0;
594         setMenuLocation();
595         mFadeAnimator = ValueAnimator.ofFloat(mAlpha, 1);
596         mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
597             @Override
598             public void onAnimationUpdate(ValueAnimator animation) {
599                 final float absTrans = Math.abs(transX);
600 
601                 boolean pastMenu = (fromLeft && transX <= notiThreshold)
602                         || (!fromLeft && absTrans <= notiThreshold);
603                 if (pastMenu && !mMenuFadedIn) {
604                     setMenuAlpha((float) animation.getAnimatedValue());
605                 }
606             }
607         });
608         mFadeAnimator.addListener(new AnimatorListenerAdapter() {
609             @Override
610             public void onAnimationStart(Animator animation) {
611                 mAnimating = true;
612             }
613 
614             @Override
615             public void onAnimationCancel(Animator animation) {
616                 // TODO should animate back to 0f from current alpha
617                 setMenuAlpha(0f);
618             }
619 
620             @Override
621             public void onAnimationEnd(Animator animation) {
622                 mAnimating = false;
623                 mMenuFadedIn = mAlpha == 1;
624             }
625         });
626         mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN);
627         mFadeAnimator.setDuration(ICON_ALPHA_ANIM_DURATION);
628         mFadeAnimator.start();
629     }
630 
631     @Override
setMenuItems(ArrayList<MenuItem> items)632     public void setMenuItems(ArrayList<MenuItem> items) {
633         // Do nothing we use our own for now.
634         // TODO -- handle / allow custom menu items!
635     }
636 
637     @Override
shouldShowGutsOnSnapOpen()638     public boolean shouldShowGutsOnSnapOpen() {
639         return false;
640     }
641 
642     @Override
menuItemToExposeOnSnap()643     public MenuItem menuItemToExposeOnSnap() {
644         return null;
645     }
646 
647     @Override
getRevealAnimationOrigin()648     public Point getRevealAnimationOrigin() {
649         View v = mInfoItem.getMenuView();
650         int menuX = v.getLeft() + v.getPaddingLeft() + (v.getWidth() / 2);
651         int menuY = v.getTop() + v.getPaddingTop() + (v.getHeight() / 2);
652         if (isMenuOnLeft()) {
653             return new Point(menuX, menuY);
654         } else {
655             menuX = mParent.getRight() - menuX;
656             return new Point(menuX, menuY);
657         }
658     }
659 
createSnoozeItem(Context context)660     static MenuItem createSnoozeItem(Context context) {
661         Resources res = context.getResources();
662         NotificationSnooze content = (NotificationSnooze) LayoutInflater.from(context)
663                 .inflate(R.layout.notification_snooze, null, false);
664         String snoozeDescription = res.getString(R.string.notification_menu_snooze_description);
665         MenuItem snooze = new NotificationMenuItem(context, snoozeDescription, content,
666                 R.drawable.ic_snooze);
667         return snooze;
668     }
669 
createConversationItem(Context context)670     static NotificationMenuItem createConversationItem(Context context) {
671         Resources res = context.getResources();
672         String infoDescription = res.getString(R.string.notification_menu_gear_description);
673         NotificationConversationInfo infoContent =
674                 (NotificationConversationInfo) LayoutInflater.from(context).inflate(
675                         R.layout.notification_conversation_info, null, false);
676         return new NotificationMenuItem(context, infoDescription, infoContent,
677                 R.drawable.ic_settings);
678     }
679 
createPartialConversationItem(Context context)680     static NotificationMenuItem createPartialConversationItem(Context context) {
681         Resources res = context.getResources();
682         String infoDescription = res.getString(R.string.notification_menu_gear_description);
683         PartialConversationInfo infoContent =
684                 (PartialConversationInfo) LayoutInflater.from(context).inflate(
685                         R.layout.partial_conversation_info, null, false);
686         return new NotificationMenuItem(context, infoDescription, infoContent,
687                 R.drawable.ic_settings);
688     }
689 
createInfoItem(Context context)690     static NotificationMenuItem createInfoItem(Context context) {
691         Resources res = context.getResources();
692         String infoDescription = res.getString(R.string.notification_menu_gear_description);
693         NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate(
694                 R.layout.notification_info, null, false);
695         return new NotificationMenuItem(context, infoDescription, infoContent,
696                 R.drawable.ic_settings);
697     }
698 
createFeedbackItem(Context context)699     static MenuItem createFeedbackItem(Context context) {
700         FeedbackInfo feedbackContent = (FeedbackInfo) LayoutInflater.from(context).inflate(
701                 R.layout.feedback_info, null, false);
702         MenuItem info = new NotificationMenuItem(context, null, feedbackContent,
703                 -1 /*don't show in slow swipe menu */);
704         return info;
705     }
706 
addMenuView(MenuItem item, ViewGroup parent)707     private void addMenuView(MenuItem item, ViewGroup parent) {
708         View menuView = item.getMenuView();
709         if (menuView != null) {
710             menuView.setAlpha(mAlpha);
711             parent.addView(menuView);
712             menuView.setOnClickListener(this);
713             FrameLayout.LayoutParams lp = (LayoutParams) menuView.getLayoutParams();
714             lp.width = mHorizSpaceForIcon;
715             lp.height = mHorizSpaceForIcon;
716             menuView.setLayoutParams(lp);
717         }
718         mMenuItemsByView.put(menuView, item);
719     }
720 
721     @VisibleForTesting
722     /**
723      * Determine the minimum offset below which the menu should snap back closed.
724      */
getSnapBackThreshold()725     protected float getSnapBackThreshold() {
726         return getSpaceForMenu() - getMaximumSwipeDistance();
727     }
728 
729     /**
730      * Determine the maximum offset above which the parent notification should be dismissed.
731      * @return
732      */
733     @VisibleForTesting
getDismissThreshold()734     protected float getDismissThreshold() {
735         return getParent().getWidth() * SWIPED_FAR_ENOUGH_SIZE_FRACTION;
736     }
737 
738     @Override
isWithinSnapMenuThreshold()739     public boolean isWithinSnapMenuThreshold() {
740         float translation = getTranslation();
741         float snapBackThreshold = getSnapBackThreshold();
742         float targetRight = getDismissThreshold();
743         return isMenuOnLeft()
744                 ? translation > snapBackThreshold && translation < targetRight
745                 : translation < -snapBackThreshold && translation > -targetRight;
746     }
747 
748     @Override
isSwipedEnoughToShowMenu()749     public boolean isSwipedEnoughToShowMenu() {
750         final float minimumSwipeDistance = getMinimumSwipeDistance();
751         final float translation = getTranslation();
752         return isMenuVisible() && (isMenuOnLeft() ?
753                 translation > minimumSwipeDistance
754                 : translation < -minimumSwipeDistance);
755     }
756 
757     @Override
getMenuSnapTarget()758     public int getMenuSnapTarget() {
759         return isMenuOnLeft() ? getSpaceForMenu() : -getSpaceForMenu();
760     }
761 
762     @Override
shouldSnapBack()763     public boolean shouldSnapBack() {
764         float translation = getTranslation();
765         float targetLeft = getSnapBackThreshold();
766         return isMenuOnLeft() ? translation < targetLeft : translation > -targetLeft;
767     }
768 
769     @Override
isSnappedAndOnSameSide()770     public boolean isSnappedAndOnSameSide() {
771         return isMenuSnapped() && isMenuVisible()
772                 && isMenuSnappedOnLeft() == isMenuOnLeft();
773     }
774 
775     @Override
canBeDismissed()776     public boolean canBeDismissed() {
777         return getParent().canViewBeDismissed();
778     }
779 
780     public static class NotificationMenuItem implements MenuItem {
781         View mMenuView;
782         GutsContent mGutsContent;
783         String mContentDescription;
784 
785         /**
786          * Add a new 'guts' panel. If iconResId < 0 it will not appear in the slow swipe menu
787          * but can still be exposed via other affordances.
788          */
NotificationMenuItem(Context context, String contentDescription, GutsContent content, int iconResId)789         public NotificationMenuItem(Context context, String contentDescription, GutsContent content,
790                 int iconResId) {
791             Resources res = context.getResources();
792             int padding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding);
793             int tint = res.getColor(R.color.notification_gear_color);
794             if (iconResId >= 0) {
795                 AlphaOptimizedImageView iv = new AlphaOptimizedImageView(context);
796                 iv.setPadding(padding, padding, padding, padding);
797                 Drawable icon = context.getResources().getDrawable(iconResId);
798                 iv.setImageDrawable(icon);
799                 iv.setColorFilter(tint);
800                 iv.setAlpha(1f);
801                 mMenuView = iv;
802             }
803             mContentDescription = contentDescription;
804             mGutsContent = content;
805         }
806 
807         @Override
808         @Nullable
getMenuView()809         public View getMenuView() {
810             return mMenuView;
811         }
812 
813         @Override
getGutsView()814         public View getGutsView() {
815             return mGutsContent.getContentView();
816         }
817 
818         @Override
getContentDescription()819         public String getContentDescription() {
820             return mContentDescription;
821         }
822     }
823 }
824