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