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