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