OSDN Git Service

De-dupe shortcuts with the same id as the main notification.
[android-x86/packages-apps-Launcher3.git] / src / com / android / launcher3 / popup / PopupContainerWithArrow.java
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.launcher3.popup;
18
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.animation.TimeInterpolator;
24 import android.animation.ValueAnimator;
25 import android.annotation.SuppressLint;
26 import android.annotation.TargetApi;
27 import android.content.Context;
28 import android.content.res.Resources;
29 import android.graphics.Color;
30 import android.graphics.PointF;
31 import android.graphics.Rect;
32 import android.graphics.drawable.ShapeDrawable;
33 import android.os.Build;
34 import android.os.Handler;
35 import android.os.Looper;
36 import android.util.AttributeSet;
37 import android.view.Gravity;
38 import android.view.LayoutInflater;
39 import android.view.MotionEvent;
40 import android.view.View;
41 import android.view.ViewConfiguration;
42 import android.view.accessibility.AccessibilityEvent;
43 import android.view.animation.DecelerateInterpolator;
44 import android.widget.FrameLayout;
45
46 import com.android.launcher3.AbstractFloatingView;
47 import com.android.launcher3.BubbleTextView;
48 import com.android.launcher3.DragSource;
49 import com.android.launcher3.DropTarget;
50 import com.android.launcher3.ItemInfo;
51 import com.android.launcher3.Launcher;
52 import com.android.launcher3.LauncherAnimUtils;
53 import com.android.launcher3.LauncherModel;
54 import com.android.launcher3.LauncherSettings;
55 import com.android.launcher3.LogAccelerateInterpolator;
56 import com.android.launcher3.R;
57 import com.android.launcher3.Utilities;
58 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
59 import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate;
60 import com.android.launcher3.anim.PropertyListBuilder;
61 import com.android.launcher3.anim.PropertyResetListener;
62 import com.android.launcher3.badge.BadgeInfo;
63 import com.android.launcher3.dragndrop.DragController;
64 import com.android.launcher3.dragndrop.DragLayer;
65 import com.android.launcher3.dragndrop.DragOptions;
66 import com.android.launcher3.graphics.TriangleShape;
67 import com.android.launcher3.notification.NotificationItemView;
68 import com.android.launcher3.notification.NotificationKeyData;
69 import com.android.launcher3.shortcuts.DeepShortcutView;
70 import com.android.launcher3.shortcuts.ShortcutsItemView;
71 import com.android.launcher3.util.PackageUserKey;
72
73 import java.util.Collections;
74 import java.util.List;
75 import java.util.Map;
76
77 import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
78 import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
79 import static com.android.launcher3.userevent.nano.LauncherLogProto.Target;
80
81 /**
82  * A container for shortcuts to deep links within apps.
83  */
84 @TargetApi(Build.VERSION_CODES.N)
85 public class PopupContainerWithArrow extends AbstractFloatingView implements DragSource,
86         DragController.DragListener {
87
88     protected final Launcher mLauncher;
89     private final int mStartDragThreshold;
90     private LauncherAccessibilityDelegate mAccessibilityDelegate;
91     private final boolean mIsRtl;
92
93     public ShortcutsItemView mShortcutsItemView;
94     private NotificationItemView mNotificationItemView;
95
96     protected BubbleTextView mOriginalIcon;
97     private final Rect mTempRect = new Rect();
98     private PointF mInterceptTouchDown = new PointF();
99     private boolean mIsLeftAligned;
100     protected boolean mIsAboveIcon;
101     private View mArrow;
102
103     protected Animator mOpenCloseAnimator;
104     private boolean mDeferContainerRemoval;
105
106     public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) {
107         super(context, attrs, defStyleAttr);
108         mLauncher = Launcher.getLauncher(context);
109
110         mStartDragThreshold = getResources().getDimensionPixelSize(
111                 R.dimen.deep_shortcuts_start_drag_threshold);
112         // TODO: make sure the delegate works for all items, not just shortcuts.
113         mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher);
114         mIsRtl = Utilities.isRtl(getResources());
115     }
116
117     public PopupContainerWithArrow(Context context, AttributeSet attrs) {
118         this(context, attrs, 0);
119     }
120
121     public PopupContainerWithArrow(Context context) {
122         this(context, null, 0);
123     }
124
125     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
126         return mAccessibilityDelegate;
127     }
128
129     /**
130      * Shows the notifications and deep shortcuts associated with {@param icon}.
131      * @return the container if shown or null.
132      */
133     public static PopupContainerWithArrow showForIcon(BubbleTextView icon) {
134         Launcher launcher = Launcher.getLauncher(icon.getContext());
135         if (getOpen(launcher) != null) {
136             // There is already an items container open, so don't open this one.
137             icon.clearFocus();
138             return null;
139         }
140         ItemInfo itemInfo = (ItemInfo) icon.getTag();
141         List<String> shortcutIds = launcher.getPopupDataProvider().getShortcutIdsForItem(itemInfo);
142         List<NotificationKeyData> notificationKeys = launcher.getPopupDataProvider()
143                 .getNotificationKeysForItem(itemInfo);
144         if (shortcutIds.size() > 0 || notificationKeys.size() > 0) {
145             final PopupContainerWithArrow container =
146                     (PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
147                             R.layout.popup_container, launcher.getDragLayer(), false);
148             container.setVisibility(View.INVISIBLE);
149             launcher.getDragLayer().addView(container);
150             container.populateAndShow(icon, shortcutIds, notificationKeys);
151             return container;
152         }
153         return null;
154     }
155
156     public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds,
157             final List<NotificationKeyData> notificationKeys) {
158         final Resources resources = getResources();
159         final int arrowWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_width);
160         final int arrowHeight = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_height);
161         final int arrowHorizontalOffset = resources.getDimensionPixelSize(
162                 R.dimen.deep_shortcuts_arrow_horizontal_offset);
163         final int arrowVerticalOffset = resources.getDimensionPixelSize(
164                 R.dimen.deep_shortcuts_arrow_vertical_offset);
165
166         // Add dummy views first, and populate with real info when ready.
167         PopupPopulator.Item[] itemsToPopulate = PopupPopulator
168                 .getItemsToPopulate(shortcutIds, notificationKeys);
169         addDummyViews(originalIcon, itemsToPopulate, notificationKeys.size() > 1);
170
171         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
172         orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
173
174         boolean reverseOrder = mIsAboveIcon;
175         if (reverseOrder) {
176             removeAllViews();
177             mNotificationItemView = null;
178             mShortcutsItemView = null;
179             itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate);
180             addDummyViews(originalIcon, itemsToPopulate, notificationKeys.size() > 1);
181
182             measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
183             orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
184         }
185
186         ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag();
187         List<DeepShortcutView> shortcutViews = mShortcutsItemView == null
188                 ? Collections.EMPTY_LIST
189                 : mShortcutsItemView.getDeepShortcutViews(reverseOrder);
190         if (mNotificationItemView != null) {
191             BadgeInfo badgeInfo = mLauncher.getPopupDataProvider()
192                     .getBadgeInfoForItem(originalItemInfo);
193             updateNotificationHeader(badgeInfo);
194         }
195
196         // Add the arrow.
197         mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
198         mArrow.setPivotX(arrowWidth / 2);
199         mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight);
200
201         animateOpen();
202
203         mOriginalIcon = originalIcon;
204
205         mLauncher.getDragController().addDragListener(this);
206
207         // Load the shortcuts on a background thread and update the container as it animates.
208         final Looper workerLooper = LauncherModel.getWorkerLooper();
209         new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
210                 mLauncher, originalItemInfo, new Handler(Looper.getMainLooper()),
211                 this, shortcutIds, shortcutViews, notificationKeys, mNotificationItemView));
212     }
213
214     private void addDummyViews(BubbleTextView originalIcon,
215             PopupPopulator.Item[] itemTypesToPopulate, boolean notificationFooterHasIcons) {
216         final Resources res = getResources();
217         final int spacing = res.getDimensionPixelSize(R.dimen.popup_items_spacing);
218         final LayoutInflater inflater = mLauncher.getLayoutInflater();
219         int numItems = itemTypesToPopulate.length;
220         for (int i = 0; i < numItems; i++) {
221             PopupPopulator.Item itemTypeToPopulate = itemTypesToPopulate[i];
222             final View item = inflater.inflate(itemTypeToPopulate.layoutId, this, false);
223
224             if (itemTypeToPopulate == PopupPopulator.Item.NOTIFICATION) {
225                 mNotificationItemView = (NotificationItemView) item;
226                 int footerHeight = notificationFooterHasIcons ?
227                         res.getDimensionPixelSize(R.dimen.notification_footer_height) : 0;
228                 item.findViewById(R.id.footer).getLayoutParams().height = footerHeight;
229             }
230
231             boolean itemIsFollowedByDifferentType = i < numItems - 1
232                     && itemTypesToPopulate[i + 1] != itemTypeToPopulate;
233
234             item.setAccessibilityDelegate(mAccessibilityDelegate);
235             if (itemTypeToPopulate == PopupPopulator.Item.SHORTCUT) {
236                 if (mShortcutsItemView == null) {
237                     mShortcutsItemView = (ShortcutsItemView) inflater.inflate(
238                             R.layout.shortcuts_item, this, false);
239                     addView(mShortcutsItemView);
240                 }
241                 mShortcutsItemView.addDeepShortcutView((DeepShortcutView) item);
242                 if (itemIsFollowedByDifferentType) {
243                     ((LayoutParams) mShortcutsItemView.getLayoutParams()).bottomMargin = spacing;
244                 }
245             } else {
246                 addView(item);
247                 if (itemIsFollowedByDifferentType) {
248                     ((LayoutParams) item.getLayoutParams()).bottomMargin = spacing;
249                 }
250             }
251         }
252         // TODO: update this, since not all items are shortcuts
253         setContentDescription(getContext().getString(R.string.shortcuts_menu_description,
254                 numItems, originalIcon.getContentDescription().toString()));
255     }
256
257     protected PopupItemView getItemViewAt(int index) {
258         if (!mIsAboveIcon) {
259             // Opening down, so arrow is the first view.
260             index++;
261         }
262         return (PopupItemView) getChildAt(index);
263     }
264
265     protected int getItemCount() {
266         // All children except the arrow are items.
267         return getChildCount() - 1;
268     }
269
270     private void animateOpen() {
271         setVisibility(View.VISIBLE);
272         mIsOpen = true;
273
274         final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
275         final int itemCount = getItemCount();
276
277         final long duration = getResources().getInteger(
278                 R.integer.config_deepShortcutOpenDuration);
279         final long arrowScaleDuration = getResources().getInteger(
280                 R.integer.config_deepShortcutArrowOpenDuration);
281         final long arrowScaleDelay = duration - arrowScaleDuration;
282         final long stagger = getResources().getInteger(
283                 R.integer.config_deepShortcutOpenStagger);
284         final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
285
286         // Animate shortcuts
287         DecelerateInterpolator interpolator = new DecelerateInterpolator();
288         for (int i = 0; i < itemCount; i++) {
289             final PopupItemView popupItemView = getItemViewAt(i);
290             popupItemView.setVisibility(INVISIBLE);
291             popupItemView.setAlpha(0);
292
293             Animator anim = popupItemView.createOpenAnimation(mIsAboveIcon, mIsLeftAligned);
294             anim.addListener(new AnimatorListenerAdapter() {
295                 @Override
296                 public void onAnimationStart(Animator animation) {
297                     popupItemView.setVisibility(VISIBLE);
298                 }
299             });
300             anim.setDuration(duration);
301             int animationIndex = mIsAboveIcon ? itemCount - i - 1 : i;
302             anim.setStartDelay(stagger * animationIndex);
303             anim.setInterpolator(interpolator);
304             shortcutAnims.play(anim);
305
306             Animator fadeAnim = ObjectAnimator.ofFloat(popupItemView, View.ALPHA, 1);
307             fadeAnim.setInterpolator(fadeInterpolator);
308             // We want the shortcut to be fully opaque before the arrow starts animating.
309             fadeAnim.setDuration(arrowScaleDelay);
310             shortcutAnims.play(fadeAnim);
311         }
312         shortcutAnims.addListener(new AnimatorListenerAdapter() {
313             @Override
314             public void onAnimationEnd(Animator animation) {
315                 mOpenCloseAnimator = null;
316                 Utilities.sendCustomAccessibilityEvent(
317                         PopupContainerWithArrow.this,
318                         AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
319                         getContext().getString(R.string.action_deep_shortcut));
320             }
321         });
322
323         // Animate the arrow
324         mArrow.setScaleX(0);
325         mArrow.setScaleY(0);
326         Animator arrowScale = createArrowScaleAnim(1).setDuration(arrowScaleDuration);
327         arrowScale.setStartDelay(arrowScaleDelay);
328         shortcutAnims.play(arrowScale);
329
330         mOpenCloseAnimator = shortcutAnims;
331         shortcutAnims.start();
332     }
333
334     /**
335      * Orients this container above or below the given icon, aligning with the left or right.
336      *
337      * These are the preferred orientations, in order (RTL prefers right-aligned over left):
338      * - Above and left-aligned
339      * - Above and right-aligned
340      * - Below and left-aligned
341      * - Below and right-aligned
342      *
343      * So we always align left if there is enough horizontal space
344      * and align above if there is enough vertical space.
345      */
346     private void orientAboutIcon(BubbleTextView icon, int arrowHeight) {
347         int width = getMeasuredWidth();
348         int height = getMeasuredHeight() + arrowHeight;
349
350         DragLayer dragLayer = mLauncher.getDragLayer();
351         dragLayer.getDescendantRectRelativeToSelf(icon, mTempRect);
352         Rect insets = dragLayer.getInsets();
353
354         // Align left (right in RTL) if there is room.
355         int leftAlignedX = mTempRect.left + icon.getPaddingLeft();
356         int rightAlignedX = mTempRect.right - width - icon.getPaddingRight();
357         int x = leftAlignedX;
358         boolean canBeLeftAligned = leftAlignedX + width + insets.left
359                 < dragLayer.getRight() - insets.right;
360         boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left;
361         if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) {
362             x = rightAlignedX;
363         }
364         mIsLeftAligned = x == leftAlignedX;
365         if (mIsRtl) {
366             x -= dragLayer.getWidth() - width;
367         }
368
369         // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
370         int iconWidth = icon.getWidth() - icon.getTotalPaddingLeft() - icon.getTotalPaddingRight();
371         iconWidth *= icon.getScaleX();
372         Resources resources = getResources();
373         int xOffset;
374         if (isAlignedWithStart()) {
375             // Aligning with the shortcut icon.
376             int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
377             int shortcutPaddingStart = resources.getDimensionPixelSize(
378                     R.dimen.deep_shortcut_padding_start);
379             xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
380         } else {
381             // Aligning with the drag handle.
382             int shortcutDragHandleWidth = resources.getDimensionPixelSize(
383                     R.dimen.deep_shortcut_drag_handle_size);
384             int shortcutPaddingEnd = resources.getDimensionPixelSize(
385                     R.dimen.deep_shortcut_padding_end);
386             xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
387         }
388         x += mIsLeftAligned ? xOffset : -xOffset;
389
390         // Open above icon if there is room.
391         int iconHeight = icon.getIcon().getBounds().height();
392         int y = mTempRect.top + icon.getPaddingTop() - height;
393         mIsAboveIcon = y > dragLayer.getTop() + insets.top;
394         if (!mIsAboveIcon) {
395             y = mTempRect.top + icon.getPaddingTop() + iconHeight;
396         }
397
398         // Insets are added later, so subtract them now.
399         if (mIsRtl) {
400             x += insets.right;
401         } else {
402             x -= insets.left;
403         }
404         y -= insets.top;
405
406         if (y < dragLayer.getTop() || y + height > dragLayer.getBottom()) {
407             // The container is opening off the screen, so just center it in the drag layer instead.
408             ((FrameLayout.LayoutParams) getLayoutParams()).gravity = Gravity.CENTER_VERTICAL;
409             // Put the container next to the icon, preferring the right side in ltr (left in rtl).
410             int rightSide = leftAlignedX + iconWidth - insets.left;
411             int leftSide = rightAlignedX - iconWidth - insets.left;
412             if (!mIsRtl) {
413                 if (rightSide + width < dragLayer.getRight()) {
414                     x = rightSide;
415                     mIsLeftAligned = true;
416                 } else {
417                     x = leftSide;
418                     mIsLeftAligned = false;
419                 }
420             } else {
421                 if (leftSide > dragLayer.getLeft()) {
422                     x = leftSide;
423                     mIsLeftAligned = false;
424                 } else {
425                     x = rightSide;
426                     mIsLeftAligned = true;
427                 }
428             }
429             mIsAboveIcon = true;
430         }
431
432         if (x < dragLayer.getLeft() || x + width > dragLayer.getRight()) {
433             // If we are still off screen, center horizontally too.
434             ((FrameLayout.LayoutParams) getLayoutParams()).gravity |= Gravity.CENTER_HORIZONTAL;
435         }
436
437         int gravity = ((FrameLayout.LayoutParams) getLayoutParams()).gravity;
438         if (!Gravity.isHorizontal(gravity)) {
439             setX(x);
440         }
441         if (!Gravity.isVertical(gravity)) {
442             setY(y);
443         }
444     }
445
446     private boolean isAlignedWithStart() {
447         return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
448     }
449
450     /**
451      * Adds an arrow view pointing at the original icon.
452      * @param horizontalOffset the horizontal offset of the arrow, so that it
453      *                              points at the center of the original icon
454      */
455     private View addArrowView(int horizontalOffset, int verticalOffset, int width, int height) {
456         LayoutParams layoutParams = new LayoutParams(width, height);
457         if (mIsLeftAligned) {
458             layoutParams.gravity = Gravity.LEFT;
459             layoutParams.leftMargin = horizontalOffset;
460         } else {
461             layoutParams.gravity = Gravity.RIGHT;
462             layoutParams.rightMargin = horizontalOffset;
463         }
464         if (mIsAboveIcon) {
465             layoutParams.topMargin = verticalOffset;
466         } else {
467             layoutParams.bottomMargin = verticalOffset;
468         }
469
470         View arrowView = new View(getContext());
471         if (Gravity.isVertical(((FrameLayout.LayoutParams) getLayoutParams()).gravity)) {
472             // This is only true if there wasn't room for the container next to the icon,
473             // so we centered it instead. In that case we don't want to show the arrow.
474             arrowView.setVisibility(INVISIBLE);
475         } else {
476             ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
477                     width, height, !mIsAboveIcon));
478             arrowDrawable.getPaint().setColor(Color.WHITE);
479             arrowView.setBackground(arrowDrawable);
480             arrowView.setElevation(getElevation());
481         }
482         addView(arrowView, mIsAboveIcon ? getChildCount() : 0, layoutParams);
483         return arrowView;
484     }
485
486     @Override
487     public View getExtendedTouchView() {
488         return mOriginalIcon;
489     }
490
491     /**
492      * Determines when the deferred drag should be started.
493      *
494      * Current behavior:
495      * - Start the drag if the touch passes a certain distance from the original touch down.
496      */
497     public DragOptions.PreDragCondition createPreDragCondition() {
498         return new DragOptions.PreDragCondition() {
499             @Override
500             public boolean shouldStartDrag(double distanceDragged) {
501                 return distanceDragged > mStartDragThreshold;
502             }
503
504             @Override
505             public void onPreDragStart(DropTarget.DragObject dragObject) {
506                 mOriginalIcon.setVisibility(INVISIBLE);
507             }
508
509             @Override
510             public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) {
511                 if (!dragStarted) {
512                     mOriginalIcon.setVisibility(VISIBLE);
513                     mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon);
514                     if (!mIsAboveIcon) {
515                         mOriginalIcon.setTextVisibility(false);
516                     }
517                 }
518             }
519         };
520     }
521
522     @Override
523     public boolean onInterceptTouchEvent(MotionEvent ev) {
524         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
525             mInterceptTouchDown.set(ev.getX(), ev.getY());
526             return false;
527         }
528         // Stop sending touch events to deep shortcut views if user moved beyond touch slop.
529         return Math.hypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY())
530                 > ViewConfiguration.get(getContext()).getScaledTouchSlop();
531     }
532
533     /**
534      * We need to handle touch events to prevent them from falling through to the workspace below.
535      */
536     @SuppressLint("ClickableViewAccessibility")
537     @Override
538     public boolean onTouchEvent(MotionEvent ev) {
539         return true;
540     }
541
542     /**
543      * Updates the notification header to reflect the badge info. Since this can be called
544      * for any badge info (not necessarily the one associated with this app), we first
545      * check that the ItemInfo matches the one of this popup.
546      */
547     public void updateNotificationHeader(BadgeInfo badgeInfo, ItemInfo originalItemInfo) {
548         if (originalItemInfo != mOriginalIcon.getTag()) {
549             return;
550         }
551         updateNotificationHeader(badgeInfo);
552     }
553
554     private void updateNotificationHeader(BadgeInfo badgeInfo) {
555         if (mNotificationItemView != null && badgeInfo != null) {
556             mNotificationItemView.updateHeader(badgeInfo.getNotificationCount());
557         }
558     }
559
560     public void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) {
561         if (mNotificationItemView == null) {
562             return;
563         }
564         ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
565         BadgeInfo badgeInfo = updatedBadges.get(PackageUserKey.fromItemInfo(originalInfo));
566         if (badgeInfo == null || badgeInfo.getNotificationCount() == 0) {
567             AnimatorSet removeNotification = LauncherAnimUtils.createAnimatorSet();
568             final int duration = getResources().getInteger(
569                     R.integer.config_removeNotificationViewDuration);
570             final int spacing = getResources().getDimensionPixelSize(R.dimen.popup_items_spacing);
571             removeNotification.play(reduceNotificationViewHeight(
572                     mNotificationItemView.getHeight() + spacing, duration));
573             final View removeMarginView = mIsAboveIcon ? getItemViewAt(getItemCount() - 2)
574                     : mNotificationItemView;
575             if (removeMarginView != null) {
576                 ValueAnimator removeMargin = ValueAnimator.ofFloat(1, 0).setDuration(duration);
577                 removeMargin.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
578                     @Override
579                     public void onAnimationUpdate(ValueAnimator valueAnimator) {
580                         ((MarginLayoutParams) removeMarginView.getLayoutParams()).bottomMargin
581                                 = (int) (spacing * (float) valueAnimator.getAnimatedValue());
582                     }
583                 });
584                 removeNotification.play(removeMargin);
585             }
586             Animator fade = ObjectAnimator.ofFloat(mNotificationItemView, ALPHA, 0)
587                     .setDuration(duration);
588             fade.addListener(new AnimatorListenerAdapter() {
589                 @Override
590                 public void onAnimationEnd(Animator animation) {
591                     removeView(mNotificationItemView);
592                     mNotificationItemView = null;
593                     if (getItemCount() == 0) {
594                         close(false);
595                         return;
596                     }
597                 }
598             });
599             removeNotification.play(fade);
600             final long arrowScaleDuration = getResources().getInteger(
601                     R.integer.config_deepShortcutArrowOpenDuration);
602             Animator hideArrow = createArrowScaleAnim(0).setDuration(arrowScaleDuration);
603             hideArrow.setStartDelay(0);
604             Animator showArrow = createArrowScaleAnim(1).setDuration(arrowScaleDuration);
605             showArrow.setStartDelay((long) (duration - arrowScaleDuration * 1.5));
606             removeNotification.playSequentially(hideArrow, showArrow);
607             removeNotification.start();
608             return;
609         }
610         mNotificationItemView.trimNotifications(NotificationKeyData.extractKeysOnly(
611                 badgeInfo.getNotificationKeys()));
612     }
613
614     private ObjectAnimator createArrowScaleAnim(float scale) {
615         return LauncherAnimUtils.ofPropertyValuesHolder(
616                 mArrow, new PropertyListBuilder().scale(scale).build());
617     }
618
619     /**
620      * Animates the height of the notification item and the translationY of other items accordingly.
621      */
622     public Animator reduceNotificationViewHeight(int heightToRemove, int duration) {
623         final int translateYBy = mIsAboveIcon ? heightToRemove : -heightToRemove;
624         AnimatorSet animatorSet = LauncherAnimUtils.createAnimatorSet();
625         animatorSet.play(mNotificationItemView.animateHeightRemoval(heightToRemove));
626         PropertyResetListener<View, Float> resetTranslationYListener
627                 = new PropertyResetListener<>(TRANSLATION_Y, 0f);
628         for (int i = 0; i < getItemCount(); i++) {
629             final PopupItemView itemView = getItemViewAt(i);
630             if (!mIsAboveIcon && itemView == mNotificationItemView) {
631                 // The notification view is already in the right place when container is below icon.
632                 continue;
633             }
634             ValueAnimator translateItem = ObjectAnimator.ofFloat(itemView, TRANSLATION_Y,
635                     itemView.getTranslationY() + translateYBy).setDuration(duration);
636             translateItem.addListener(resetTranslationYListener);
637             animatorSet.play(translateItem);
638         }
639         if (mIsAboveIcon) {
640             // All the items, including the notification item, translated down, but the
641             // container itself did not. This means the items would jump back to their
642             // original translation unless we update the container's translationY here.
643             animatorSet.addListener(new AnimatorListenerAdapter() {
644                 @Override
645                 public void onAnimationEnd(Animator animation) {
646                     setTranslationY(getTranslationY() + translateYBy);
647                 }
648             });
649         }
650         return animatorSet;
651     }
652
653     @Override
654     public boolean supportsAppInfoDropTarget() {
655         return true;
656     }
657
658     @Override
659     public boolean supportsDeleteDropTarget() {
660         return false;
661     }
662
663     @Override
664     public float getIntrinsicIconScaleFactor() {
665         return 1f;
666     }
667
668     @Override
669     public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
670             boolean success) {
671         if (!success) {
672             d.dragView.remove();
673             mLauncher.showWorkspace(true);
674             mLauncher.getDropTargetBar().onDragEnd();
675         }
676     }
677
678     @Override
679     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
680         // Either the original icon or one of the shortcuts was dragged.
681         // Hide the container, but don't remove it yet because that interferes with touch events.
682         mDeferContainerRemoval = true;
683         animateClose();
684     }
685
686     @Override
687     public void onDragEnd() {
688         if (!mIsOpen) {
689             if (mOpenCloseAnimator != null) {
690                 // Close animation is running.
691                 mDeferContainerRemoval = false;
692             } else {
693                 // Close animation is not running.
694                 if (mDeferContainerRemoval) {
695                     closeComplete();
696                 }
697             }
698         }
699     }
700
701     @Override
702     public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
703         target.itemType = ItemType.DEEPSHORTCUT;
704         targetParent.containerType = ContainerType.DEEPSHORTCUTS;
705     }
706
707     @Override
708     protected void handleClose(boolean animate) {
709         if (animate) {
710             animateClose();
711         } else {
712             closeComplete();
713         }
714     }
715
716     protected void animateClose() {
717         if (!mIsOpen) {
718             return;
719         }
720         if (mOpenCloseAnimator != null) {
721             mOpenCloseAnimator.cancel();
722         }
723         mIsOpen = false;
724
725         final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
726         final int itemCount = getItemCount();
727         int numOpenShortcuts = 0;
728         for (int i = 0; i < itemCount; i++) {
729             if (getItemViewAt(i).isOpenOrOpening()) {
730                 numOpenShortcuts++;
731             }
732         }
733         final long duration = getResources().getInteger(
734                 R.integer.config_deepShortcutCloseDuration);
735         final long arrowScaleDuration = getResources().getInteger(
736                 R.integer.config_deepShortcutArrowOpenDuration);
737         final long stagger = getResources().getInteger(
738                 R.integer.config_deepShortcutCloseStagger);
739         final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
740
741         int firstOpenItemIndex = mIsAboveIcon ? itemCount - numOpenShortcuts : 0;
742         for (int i = firstOpenItemIndex; i < firstOpenItemIndex + numOpenShortcuts; i++) {
743             final PopupItemView view = getItemViewAt(i);
744             Animator anim;
745             anim = view.createCloseAnimation(mIsAboveIcon, mIsLeftAligned, duration);
746             int animationIndex = mIsAboveIcon ? i - firstOpenItemIndex
747                     : numOpenShortcuts - i - 1;
748             anim.setStartDelay(stagger * animationIndex);
749
750             Animator fadeAnim = ObjectAnimator.ofFloat(view, View.ALPHA, 0);
751             // Don't start fading until the arrow is gone.
752             fadeAnim.setStartDelay(stagger * animationIndex + arrowScaleDuration);
753             fadeAnim.setDuration(duration - arrowScaleDuration);
754             fadeAnim.setInterpolator(fadeInterpolator);
755             shortcutAnims.play(fadeAnim);
756             anim.addListener(new AnimatorListenerAdapter() {
757                 @Override
758                 public void onAnimationEnd(Animator animation) {
759                     view.setVisibility(INVISIBLE);
760                 }
761             });
762             shortcutAnims.play(anim);
763         }
764         Animator arrowAnim = createArrowScaleAnim(0).setDuration(arrowScaleDuration);
765         arrowAnim.setStartDelay(0);
766         shortcutAnims.play(arrowAnim);
767
768         shortcutAnims.addListener(new AnimatorListenerAdapter() {
769             @Override
770             public void onAnimationEnd(Animator animation) {
771                 mOpenCloseAnimator = null;
772                 if (mDeferContainerRemoval) {
773                     setVisibility(INVISIBLE);
774                 } else {
775                     closeComplete();
776                 }
777             }
778         });
779         mOpenCloseAnimator = shortcutAnims;
780         shortcutAnims.start();
781     }
782
783     /**
784      * Closes the folder without animation.
785      */
786     protected void closeComplete() {
787         if (mOpenCloseAnimator != null) {
788             mOpenCloseAnimator.cancel();
789             mOpenCloseAnimator = null;
790         }
791         mIsOpen = false;
792         mDeferContainerRemoval = false;
793         boolean isInHotseat = ((ItemInfo) mOriginalIcon.getTag()).container
794                 == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
795         mOriginalIcon.setTextVisibility(!isInHotseat);
796         mLauncher.getDragController().removeDragListener(this);
797         mLauncher.getDragLayer().removeView(this);
798     }
799
800     @Override
801     protected boolean isOfType(int type) {
802         return (type & TYPE_POPUP_CONTAINER_WITH_ARROW) != 0;
803     }
804
805     /**
806      * Returns a DeepShortcutsContainer which is already open or null
807      */
808     public static PopupContainerWithArrow getOpen(Launcher launcher) {
809         return getOpenView(launcher, TYPE_POPUP_CONTAINER_WITH_ARROW);
810     }
811
812     @Override
813     public int getLogContainerType() {
814         return ContainerType.DEEPSHORTCUTS;
815     }
816 }