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