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.showingPulsing()
350                 && (mCheckForDrag == null || !mHandler.hasCallbacks(mCheckForDrag))) {
351             // Only show the menu if we're not a heads up view and guts aren't exposed.
352             mCheckForDrag = new CheckForDrag();
353             mHandler.postDelayed(mCheckForDrag, SHOW_MENU_DELAY);
354         }
355     }
356 
357     @VisibleForTesting
beginDrag()358     protected void beginDrag() {
359         mSnapping = false;
360         if (mFadeAnimator != null) {
361             mFadeAnimator.cancel();
362         }
363         mHandler.removeCallbacks(mCheckForDrag);
364         mCheckForDrag = null;
365         mIsUserTouching = true;
366     }
367 
368     @Override
onTouchStart()369     public void onTouchStart() {
370        beginDrag();
371     }
372 
373     @Override
onSnapOpen()374     public void onSnapOpen() {
375         mMenuSnapped = true;
376         mMenuSnappedOnLeft = isMenuOnLeft();
377         if (mAlpha == 0f && mParent != null) {
378             fadeInMenu(mParent.getWidth());
379         }
380         if (mMenuListener != null) {
381             mMenuListener.onMenuShown(getParent());
382         }
383     }
384 
385     @Override
onSnapClosed()386     public void onSnapClosed() {
387         cancelDrag();
388         mMenuSnapped = false;
389         mSnapping = true;
390     }
391 
392     @Override
onDismiss()393     public void onDismiss() {
394         cancelDrag();
395         mMenuSnapped = false;
396         mDismissing = true;
397     }
398 
399     @VisibleForTesting
cancelDrag()400     protected void cancelDrag() {
401         if (mFadeAnimator != null) {
402             mFadeAnimator.cancel();
403         }
404         mHandler.removeCallbacks(mCheckForDrag);
405     }
406 
407     @VisibleForTesting
getMinimumSwipeDistance()408     protected float getMinimumSwipeDistance() {
409         final float multiplier = getParent().canViewBeDismissed()
410                 ? SWIPED_FAR_ENOUGH_MENU_FRACTION
411                 : SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION;
412         return mHorizSpaceForIcon * multiplier;
413     }
414 
415     @VisibleForTesting
getMaximumSwipeDistance()416     protected float getMaximumSwipeDistance() {
417         return mHorizSpaceForIcon * SWIPED_BACK_ENOUGH_TO_COVER_FRACTION;
418     }
419 
420     /**
421      * Returns whether the gesture is towards the menu location or not.
422      */
423     @Override
isTowardsMenu(float movement)424     public boolean isTowardsMenu(float movement) {
425         return isMenuVisible()
426                 && ((isMenuOnLeft() && movement <= 0)
427                         || (!isMenuOnLeft() && movement >= 0));
428     }
429 
430     @Override
setAppName(String appName)431     public void setAppName(String appName) {
432         if (appName == null) {
433             return;
434         }
435         setAppName(appName, mLeftMenuItems);
436         setAppName(appName, mRightMenuItems);
437     }
438 
setAppName(String appName, ArrayList<MenuItem> menuItems)439     private void setAppName(String appName,
440             ArrayList<MenuItem> menuItems) {
441         Resources res = mContext.getResources();
442         final int count = menuItems.size();
443         for (int i = 0; i < count; i++) {
444             MenuItem item = menuItems.get(i);
445             String description = String.format(
446                     res.getString(R.string.notification_menu_accessibility),
447                     appName, item.getContentDescription());
448             View menuView = item.getMenuView();
449             if (menuView != null) {
450                 menuView.setContentDescription(description);
451             }
452         }
453     }
454 
455     @Override
onParentHeightUpdate()456     public void onParentHeightUpdate() {
457         if (mParent == null
458                 || (mLeftMenuItems.isEmpty() && mRightMenuItems.isEmpty())
459                 || mMenuContainer == null) {
460             return;
461         }
462         int parentHeight = mParent.getActualHeight();
463         float translationY;
464         if (parentHeight < mVertSpaceForIcons) {
465             translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2);
466         } else {
467             translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2;
468         }
469         mMenuContainer.setTranslationY(translationY);
470     }
471 
472     @Override
onParentTranslationUpdate(float translation)473     public void onParentTranslationUpdate(float translation) {
474         mTranslation = translation;
475         if (mAnimating || !mMenuFadedIn) {
476             // Don't adjust when animating, or if the menu hasn't been shown yet.
477             return;
478         }
479         final float fadeThreshold = mParent.getWidth() * 0.3f;
480         final float absTrans = Math.abs(translation);
481         float desiredAlpha = 0;
482         if (absTrans == 0) {
483             desiredAlpha = 0;
484         } else if (absTrans <= fadeThreshold) {
485             desiredAlpha = 1;
486         } else {
487             desiredAlpha = 1 - ((absTrans - fadeThreshold) / (mParent.getWidth() - fadeThreshold));
488         }
489         setMenuAlpha(desiredAlpha);
490     }
491 
492     @Override
onClick(View v)493     public void onClick(View v) {
494         if (mMenuListener == null) {
495             // Nothing to do
496             return;
497         }
498         v.getLocationOnScreen(mIconLocation);
499         mParent.getLocationOnScreen(mParentLocation);
500         final int centerX = mHorizSpaceForIcon / 2;
501         final int centerY = v.getHeight() / 2;
502         final int x = mIconLocation[0] - mParentLocation[0] + centerX;
503         final int y = mIconLocation[1] - mParentLocation[1] + centerY;
504         if (mMenuItemsByView.containsKey(v)) {
505             mMenuListener.onMenuClicked(mParent, x, y, mMenuItemsByView.get(v));
506         }
507     }
508 
isMenuLocationChange()509     private boolean isMenuLocationChange() {
510         boolean onLeft = mTranslation > mIconPadding;
511         boolean onRight = mTranslation < -mIconPadding;
512         if ((isMenuOnLeft() && onRight) || (!isMenuOnLeft() && onLeft)) {
513             return true;
514         }
515         return false;
516     }
517 
518     private void setMenuLocation() {
519         boolean showOnLeft = mTranslation > 0;
520         if ((mIconsPlaced && showOnLeft == isMenuOnLeft()) || isSnapping() || mMenuContainer == null
521                 || !mMenuContainer.isAttachedToWindow()) {
522             // Do nothing
523             return;
524         }
525         boolean wasOnLeft = mOnLeft;
526         mOnLeft = showOnLeft;
527         if (wasOnLeft != showOnLeft) {
528             populateMenuViews();
529         }
530         final int count = mMenuContainer.getChildCount();
531         for (int i = 0; i < count; i++) {
532             final View v = mMenuContainer.getChildAt(i);
533             final float left = i * mHorizSpaceForIcon;
534             final float right = mParent.getWidth() - (mHorizSpaceForIcon * (i + 1));
535             v.setX(showOnLeft ? left : right);
536         }
537         mIconsPlaced = true;
538     }
539 
540     @VisibleForTesting
setMenuAlpha(float alpha)541     protected void setMenuAlpha(float alpha) {
542         mAlpha = alpha;
543         if (mMenuContainer == null) {
544             return;
545         }
546         if (alpha == 0) {
547             mMenuFadedIn = false; // Can fade in again once it's gone.
548             mMenuContainer.setVisibility(View.INVISIBLE);
549         } else {
550             mMenuContainer.setVisibility(View.VISIBLE);
551         }
552         final int count = mMenuContainer.getChildCount();
553         for (int i = 0; i < count; i++) {
554             mMenuContainer.getChildAt(i).setAlpha(mAlpha);
555         }
556     }
557 
558     /**
559      * Returns the horizontal space in pixels required to display the menu.
560      */
561     @VisibleForTesting
getSpaceForMenu()562     protected int getSpaceForMenu() {
563         return mHorizSpaceForIcon * mMenuContainer.getChildCount();
564     }
565 
566     private final class CheckForDrag implements Runnable {
567         @Override
run()568         public void run() {
569             final float absTransX = Math.abs(mTranslation);
570             final float bounceBackToMenuWidth = getSpaceForMenu();
571             final float notiThreshold = mParent.getWidth() * 0.4f;
572             if ((!isMenuVisible() || isMenuLocationChange())
573                     && absTransX >= bounceBackToMenuWidth * 0.4
574                     && absTransX < notiThreshold) {
575                 fadeInMenu(notiThreshold);
576             }
577         }
578     }
579 
fadeInMenu(final float notiThreshold)580     private void fadeInMenu(final float notiThreshold) {
581         if (mDismissing || mAnimating) {
582             return;
583         }
584         if (isMenuLocationChange()) {
585             setMenuAlpha(0f);
586         }
587         final float transX = mTranslation;
588         final boolean fromLeft = mTranslation > 0;
589         setMenuLocation();
590         mFadeAnimator = ValueAnimator.ofFloat(mAlpha, 1);
591         mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
592             @Override
593             public void onAnimationUpdate(ValueAnimator animation) {
594                 final float absTrans = Math.abs(transX);
595 
596                 boolean pastMenu = (fromLeft && transX <= notiThreshold)
597                         || (!fromLeft && absTrans <= notiThreshold);
598                 if (pastMenu && !mMenuFadedIn) {
599                     setMenuAlpha((float) animation.getAnimatedValue());
600                 }
601             }
602         });
603         mFadeAnimator.addListener(new AnimatorListenerAdapter() {
604             @Override
605             public void onAnimationStart(Animator animation) {
606                 mAnimating = true;
607             }
608 
609             @Override
610             public void onAnimationCancel(Animator animation) {
611                 // TODO should animate back to 0f from current alpha
612                 setMenuAlpha(0f);
613             }
614 
615             @Override
616             public void onAnimationEnd(Animator animation) {
617                 mAnimating = false;
618                 mMenuFadedIn = mAlpha == 1;
619             }
620         });
621         mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN);
622         mFadeAnimator.setDuration(ICON_ALPHA_ANIM_DURATION);
623         mFadeAnimator.start();
624     }
625 
626     @Override
setMenuItems(ArrayList<MenuItem> items)627     public void setMenuItems(ArrayList<MenuItem> items) {
628         // Do nothing we use our own for now.
629         // TODO -- handle / allow custom menu items!
630     }
631 
632     @Override
shouldShowGutsOnSnapOpen()633     public boolean shouldShowGutsOnSnapOpen() {
634         return mIsUsingBidirectionalSwipe;
635     }
636 
637     @Override
menuItemToExposeOnSnap()638     public MenuItem menuItemToExposeOnSnap() {
639         return mIsUsingBidirectionalSwipe ? mInfoItem : null;
640     }
641 
642     @Override
getRevealAnimationOrigin()643     public Point getRevealAnimationOrigin() {
644         View v = mInfoItem.getMenuView();
645         int menuX = v.getLeft() + v.getPaddingLeft() + (v.getWidth() / 2);
646         int menuY = v.getTop() + v.getPaddingTop() + (v.getHeight() / 2);
647         if (isMenuOnLeft()) {
648             return new Point(menuX, menuY);
649         } else {
650             menuX = mParent.getRight() - menuX;
651             return new Point(menuX, menuY);
652         }
653     }
654 
createSnoozeItem(Context context)655     static MenuItem createSnoozeItem(Context context) {
656         Resources res = context.getResources();
657         NotificationSnooze content = (NotificationSnooze) LayoutInflater.from(context)
658                 .inflate(R.layout.notification_snooze, null, false);
659         String snoozeDescription = res.getString(R.string.notification_menu_snooze_description);
660         MenuItem snooze = new NotificationMenuItem(context, snoozeDescription, content,
661                 R.drawable.ic_snooze);
662         return snooze;
663     }
664 
createInfoItem(Context context)665     static NotificationMenuItem createInfoItem(Context context) {
666         Resources res = context.getResources();
667         String infoDescription = res.getString(R.string.notification_menu_gear_description);
668         NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate(
669                 R.layout.notification_info, null, false);
670         return new NotificationMenuItem(context, infoDescription, infoContent,
671                 R.drawable.ic_settings);
672     }
673 
createInfoItem(Context context, boolean isCurrentlySilent)674     static NotificationMenuItem createInfoItem(Context context, boolean isCurrentlySilent) {
675         Resources res = context.getResources();
676         String infoDescription = res.getString(R.string.notification_menu_gear_description);
677         NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate(
678                 R.layout.notification_info, null, false);
679         int iconResId = isCurrentlySilent
680                 ? R.drawable.ic_notifications_silence
681                 : R.drawable.ic_notifications_alert;
682         return new NotificationMenuItem(context, infoDescription, infoContent, iconResId);
683     }
684 
createAppOpsItem(Context context)685     static MenuItem createAppOpsItem(Context context) {
686         AppOpsInfo appOpsContent = (AppOpsInfo) LayoutInflater.from(context).inflate(
687                 R.layout.app_ops_info, null, false);
688         MenuItem info = new NotificationMenuItem(context, null, appOpsContent,
689                 -1 /*don't show in slow swipe menu */);
690         return info;
691     }
692 
addMenuView(MenuItem item, ViewGroup parent)693     private void addMenuView(MenuItem item, ViewGroup parent) {
694         View menuView = item.getMenuView();
695         if (menuView != null) {
696             menuView.setAlpha(mAlpha);
697             parent.addView(menuView);
698             menuView.setOnClickListener(this);
699             FrameLayout.LayoutParams lp = (LayoutParams) menuView.getLayoutParams();
700             lp.width = mHorizSpaceForIcon;
701             lp.height = mHorizSpaceForIcon;
702             menuView.setLayoutParams(lp);
703         }
704         mMenuItemsByView.put(menuView, item);
705     }
706 
707     @VisibleForTesting
708     /**
709      * Determine the minimum offset below which the menu should snap back closed.
710      */
getSnapBackThreshold()711     protected float getSnapBackThreshold() {
712         return getSpaceForMenu() - getMaximumSwipeDistance();
713     }
714 
715     /**
716      * Determine the maximum offset above which the parent notification should be dismissed.
717      * @return
718      */
719     @VisibleForTesting
getDismissThreshold()720     protected float getDismissThreshold() {
721         return getParent().getWidth() * SWIPED_FAR_ENOUGH_SIZE_FRACTION;
722     }
723 
724     @Override
isWithinSnapMenuThreshold()725     public boolean isWithinSnapMenuThreshold() {
726         float translation = getTranslation();
727         float snapBackThreshold = getSnapBackThreshold();
728         float targetRight = getDismissThreshold();
729         return isMenuOnLeft()
730                 ? translation > snapBackThreshold && translation < targetRight
731                 : translation < -snapBackThreshold && translation > -targetRight;
732     }
733 
734     @Override
isSwipedEnoughToShowMenu()735     public boolean isSwipedEnoughToShowMenu() {
736         final float minimumSwipeDistance = getMinimumSwipeDistance();
737         final float translation = getTranslation();
738         return isMenuVisible() && (isMenuOnLeft() ?
739                 translation > minimumSwipeDistance
740                 : translation < -minimumSwipeDistance);
741     }
742 
743     @Override
getMenuSnapTarget()744     public int getMenuSnapTarget() {
745         return isMenuOnLeft() ? getSpaceForMenu() : -getSpaceForMenu();
746     }
747 
748     @Override
shouldSnapBack()749     public boolean shouldSnapBack() {
750         float translation = getTranslation();
751         float targetLeft = getSnapBackThreshold();
752         return isMenuOnLeft() ? translation < targetLeft : translation > -targetLeft;
753     }
754 
755     @Override
isSnappedAndOnSameSide()756     public boolean isSnappedAndOnSameSide() {
757         return isMenuSnapped() && isMenuVisible()
758                 && isMenuSnappedOnLeft() == isMenuOnLeft();
759     }
760 
761     @Override
canBeDismissed()762     public boolean canBeDismissed() {
763         return getParent().canViewBeDismissed();
764     }
765 
766     @Override
setDismissRtl(boolean dismissRtl)767     public void setDismissRtl(boolean dismissRtl) {
768         mDismissRtl = dismissRtl;
769         if (mMenuContainer != null) {
770             createMenuViews(true, mIsForeground);
771         }
772     }
773 
774     public static class NotificationMenuItem implements MenuItem {
775         View mMenuView;
776         GutsContent mGutsContent;
777         String mContentDescription;
778 
779         /**
780          * Add a new 'guts' panel. If iconResId < 0 it will not appear in the slow swipe menu
781          * but can still be exposed via other affordances.
782          */
NotificationMenuItem(Context context, String contentDescription, GutsContent content, int iconResId)783         public NotificationMenuItem(Context context, String contentDescription, GutsContent content,
784                 int iconResId) {
785             Resources res = context.getResources();
786             int padding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding);
787             int tint = res.getColor(R.color.notification_gear_color);
788             if (iconResId >= 0) {
789                 AlphaOptimizedImageView iv = new AlphaOptimizedImageView(context);
790                 iv.setPadding(padding, padding, padding, padding);
791                 Drawable icon = context.getResources().getDrawable(iconResId);
792                 iv.setImageDrawable(icon);
793                 iv.setColorFilter(tint);
794                 iv.setAlpha(1f);
795                 mMenuView = iv;
796             }
797             mContentDescription = contentDescription;
798             mGutsContent = content;
799         }
800 
801         @Override
802         @Nullable
getMenuView()803         public View getMenuView() {
804             return mMenuView;
805         }
806 
807         @Override
getGutsView()808         public View getGutsView() {
809             return mGutsContent.getContentView();
810         }
811 
812         @Override
getContentDescription()813         public String getContentDescription() {
814             return mContentDescription;
815         }
816     }
817 }
818