• 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.launcher3.popup;
18 
19 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_SHORTCUTS;
20 import static com.android.launcher3.Utilities.squaredHypot;
21 import static com.android.launcher3.Utilities.squaredTouchSlop;
22 import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS;
23 import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS_IF_NOTIFICATIONS;
24 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
25 
26 import android.animation.AnimatorSet;
27 import android.animation.LayoutTransition;
28 import android.annotation.TargetApi;
29 import android.content.Context;
30 import android.graphics.Point;
31 import android.graphics.PointF;
32 import android.graphics.Rect;
33 import android.os.Build;
34 import android.os.Handler;
35 import android.os.Looper;
36 import android.util.AttributeSet;
37 import android.view.MotionEvent;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.widget.ImageView;
41 
42 import com.android.launcher3.AbstractFloatingView;
43 import com.android.launcher3.BaseDraggingActivity;
44 import com.android.launcher3.BubbleTextView;
45 import com.android.launcher3.DragSource;
46 import com.android.launcher3.DropTarget;
47 import com.android.launcher3.DropTarget.DragObject;
48 import com.android.launcher3.Launcher;
49 import com.android.launcher3.LauncherState;
50 import com.android.launcher3.R;
51 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
52 import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate;
53 import com.android.launcher3.dot.DotInfo;
54 import com.android.launcher3.dragndrop.DragController;
55 import com.android.launcher3.dragndrop.DragOptions;
56 import com.android.launcher3.dragndrop.DragView;
57 import com.android.launcher3.dragndrop.DraggableView;
58 import com.android.launcher3.model.data.ItemInfo;
59 import com.android.launcher3.model.data.ItemInfoWithIcon;
60 import com.android.launcher3.model.data.WorkspaceItemInfo;
61 import com.android.launcher3.notification.NotificationInfo;
62 import com.android.launcher3.notification.NotificationItemView;
63 import com.android.launcher3.notification.NotificationKeyData;
64 import com.android.launcher3.popup.PopupDataProvider.PopupDataChangeListener;
65 import com.android.launcher3.shortcuts.DeepShortcutView;
66 import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
67 import com.android.launcher3.statemanager.StatefulActivity;
68 import com.android.launcher3.touch.ItemLongClickListener;
69 import com.android.launcher3.util.PackageUserKey;
70 import com.android.launcher3.util.ShortcutUtil;
71 import com.android.launcher3.views.BaseDragLayer;
72 
73 import java.util.ArrayList;
74 import java.util.Arrays;
75 import java.util.List;
76 import java.util.Map;
77 import java.util.Objects;
78 import java.util.function.Predicate;
79 import java.util.stream.Collectors;
80 
81 /**
82  * A container for shortcuts to deep links and notifications associated with an app.
83  *
84  * @param <T> The activity on with the popup shows
85  */
86 public class PopupContainerWithArrow<T extends StatefulActivity<LauncherState>>
87         extends ArrowPopup<T> implements DragSource, DragController.DragListener {
88 
89     private final List<DeepShortcutView> mShortcuts = new ArrayList<>();
90     private final PointF mInterceptTouchDown = new PointF();
91 
92     private final int mStartDragThreshold;
93 
94     private BubbleTextView mOriginalIcon;
95     private NotificationItemView mNotificationItemView;
96     private int mNumNotifications;
97     private ViewGroup mNotificationContainer;
98 
99     private ViewGroup mWidgetContainer;
100 
101     private ViewGroup mDeepShortcutContainer;
102 
103     private ViewGroup mSystemShortcutContainer;
104 
105     protected PopupItemDragHandler mPopupItemDragHandler;
106     protected LauncherAccessibilityDelegate mAccessibilityDelegate;
107 
PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr)108     public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) {
109         super(context, attrs, defStyleAttr);
110         mStartDragThreshold = getResources().getDimensionPixelSize(
111                 R.dimen.deep_shortcuts_start_drag_threshold);
112     }
113 
PopupContainerWithArrow(Context context, AttributeSet attrs)114     public PopupContainerWithArrow(Context context, AttributeSet attrs) {
115         this(context, attrs, 0);
116     }
117 
PopupContainerWithArrow(Context context)118     public PopupContainerWithArrow(Context context) {
119         this(context, null, 0);
120     }
121 
getAccessibilityDelegate()122     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
123         return mAccessibilityDelegate;
124     }
125 
126     @Override
onInterceptTouchEvent(MotionEvent ev)127     public boolean onInterceptTouchEvent(MotionEvent ev) {
128         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
129             mInterceptTouchDown.set(ev.getX(), ev.getY());
130         }
131         if (mNotificationItemView != null
132                 && mNotificationItemView.onInterceptTouchEvent(ev)) {
133             return true;
134         }
135         // Stop sending touch events to deep shortcut views if user moved beyond touch slop.
136         return squaredHypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY())
137                 > squaredTouchSlop(getContext());
138     }
139 
140     @Override
isOfType(int type)141     protected boolean isOfType(int type) {
142         return (type & TYPE_ACTION_POPUP) != 0;
143     }
144 
getItemClickListener()145     public OnClickListener getItemClickListener() {
146         return (view) -> {
147             mLauncher.getItemOnClickListener().onClick(view);
148             close(true);
149         };
150     }
151 
getItemDragHandler()152     public PopupItemDragHandler getItemDragHandler() {
153         return mPopupItemDragHandler;
154     }
155 
156     @Override
onControllerInterceptTouchEvent(MotionEvent ev)157     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
158         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
159             BaseDragLayer dl = getPopupContainer();
160             if (!dl.isEventOverView(this, ev)) {
161                 // TODO: add WW log if want to log if tap closed deep shortcut container.
162                 close(true);
163 
164                 // We let touches on the original icon go through so that users can launch
165                 // the app with one tap if they don't find a shortcut they want.
166                 return mOriginalIcon == null || !dl.isEventOverView(mOriginalIcon, ev);
167             }
168         }
169         return false;
170     }
171 
172     @Override
setChildColor(View view, int color, AnimatorSet animatorSetOut)173     protected void setChildColor(View view, int color, AnimatorSet animatorSetOut) {
174         super.setChildColor(view, color, animatorSetOut);
175         if (view.getId() == R.id.notification_container && mNotificationItemView != null) {
176             mNotificationItemView.updateBackgroundColor(color, animatorSetOut);
177         }
178     }
179 
180     /**
181      * Returns true if we can show the container.
182      */
canShow(View icon, ItemInfo item)183     public static boolean canShow(View icon, ItemInfo item) {
184         return icon instanceof BubbleTextView && ShortcutUtil.supportsShortcuts(item);
185     }
186 
187     /**
188      * Shows the notifications and deep shortcuts associated with {@param icon}.
189      * @return the container if shown or null.
190      */
showForIcon(BubbleTextView icon)191     public static PopupContainerWithArrow showForIcon(BubbleTextView icon) {
192         Launcher launcher = Launcher.getLauncher(icon.getContext());
193         if (getOpen(launcher) != null) {
194             // There is already an items container open, so don't open this one.
195             icon.clearFocus();
196             return null;
197         }
198         ItemInfo item = (ItemInfo) icon.getTag();
199         if (!canShow(icon, item)) {
200             return null;
201         }
202 
203         final PopupContainerWithArrow container =
204                 (PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
205                         R.layout.popup_container, launcher.getDragLayer(), false);
206         container.configureForLauncher(launcher);
207 
208         PopupDataProvider popupDataProvider = launcher.getPopupDataProvider();
209         container.populateAndShow(icon,
210                 popupDataProvider.getShortcutCountForItem(item),
211                 popupDataProvider.getNotificationKeysForItem(item),
212                 launcher.getSupportedShortcuts()
213                         .map(s -> s.getShortcut(launcher, item))
214                         .filter(Objects::nonNull)
215                         .collect(Collectors.toList()));
216         launcher.refreshAndBindWidgetsForPackageUser(PackageUserKey.fromItemInfo(item));
217         container.requestFocus();
218         return container;
219     }
220 
configureForLauncher(Launcher launcher)221     private void configureForLauncher(Launcher launcher) {
222         addOnAttachStateChangeListener(new LiveUpdateHandler(launcher));
223         mPopupItemDragHandler = new LauncherPopupItemDragHandler(launcher, this);
224         mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(launcher);
225         launcher.getDragController().addDragListener(this);
226         addPreDrawForColorExtraction(launcher);
227     }
228 
229     @Override
getChildrenForColorExtraction()230     protected List<View> getChildrenForColorExtraction() {
231         return Arrays.asList(mSystemShortcutContainer, mWidgetContainer, mDeepShortcutContainer,
232                 mNotificationContainer);
233     }
234 
235     @Override
onInflationComplete(boolean isReversed)236     protected void onInflationComplete(boolean isReversed) {
237         if (isReversed && mNotificationItemView != null) {
238             mNotificationItemView.inverseGutterMargin();
239         }
240     }
241 
242     @TargetApi(Build.VERSION_CODES.P)
populateAndShow(final BubbleTextView originalIcon, int shortcutCount, final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts)243     public void populateAndShow(final BubbleTextView originalIcon, int shortcutCount,
244             final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts) {
245         mNumNotifications = notificationKeys.size();
246         mOriginalIcon = originalIcon;
247 
248         boolean hasDeepShortcuts = shortcutCount > 0;
249         int containerWidth = (int) getResources().getDimension(R.dimen.bg_popup_item_width);
250 
251         // if there are deep shortcuts, we might want to increase the width of shortcuts to fit
252         // horizontally laid out system shortcuts.
253         if (hasDeepShortcuts) {
254             containerWidth = (int) Math.max(containerWidth,
255                     systemShortcuts.size() * getResources().getDimension(
256                             R.dimen.system_shortcut_header_icon_touch_size));
257         }
258         // Add views
259         if (mNumNotifications > 0) {
260             // Add notification entries
261             if (mNotificationContainer == null) {
262                 mNotificationContainer = findViewById(R.id.notification_container);
263                 mNotificationContainer.setVisibility(VISIBLE);
264             }
265             View.inflate(getContext(), R.layout.notification_content, mNotificationContainer);
266             mNotificationItemView = new NotificationItemView(this, mNotificationContainer);
267             updateNotificationHeader();
268         }
269         int viewsToFlip = getChildCount();
270         mSystemShortcutContainer = this;
271         if (mDeepShortcutContainer == null) {
272             mDeepShortcutContainer = findViewById(R.id.deep_shortcuts_container);
273         }
274         if (hasDeepShortcuts) {
275             mDeepShortcutContainer.setVisibility(View.VISIBLE);
276 
277             if (mNotificationItemView != null) {
278                 mNotificationItemView.addGutter();
279             }
280 
281             for (int i = shortcutCount; i > 0; i--) {
282                 DeepShortcutView v = inflateAndAdd(R.layout.deep_shortcut, mDeepShortcutContainer);
283                 v.getLayoutParams().width = containerWidth;
284                 mShortcuts.add(v);
285             }
286             updateHiddenShortcuts();
287 
288             if (!systemShortcuts.isEmpty()) {
289                 for (SystemShortcut shortcut : systemShortcuts) {
290                     if (shortcut instanceof SystemShortcut.Widgets) {
291                         if (mWidgetContainer == null) {
292                             mWidgetContainer = inflateAndAdd(R.layout.widget_shortcut_container,
293                                     this);
294                         }
295                         initializeSystemShortcut(R.layout.system_shortcut, mWidgetContainer,
296                                 shortcut);
297                     }
298                 }
299                 mSystemShortcutContainer = inflateAndAdd(R.layout.system_shortcut_icons, this);
300 
301                 for (SystemShortcut shortcut : systemShortcuts) {
302                     if (!(shortcut instanceof SystemShortcut.Widgets)) {
303                         initializeSystemShortcut(
304                                 R.layout.system_shortcut_icon_only, mSystemShortcutContainer,
305                                 shortcut);
306                     }
307                 }
308             }
309         } else {
310             mDeepShortcutContainer.setVisibility(View.GONE);
311             if (!systemShortcuts.isEmpty()) {
312                 if (mNotificationItemView != null) {
313                     mNotificationItemView.addGutter();
314                 }
315 
316                 for (SystemShortcut shortcut : systemShortcuts) {
317                     initializeSystemShortcut(R.layout.system_shortcut, this, shortcut);
318                 }
319             }
320         }
321 
322         reorderAndShow(viewsToFlip);
323 
324         ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag();
325         if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
326             setAccessibilityPaneTitle(getTitleForAccessibility());
327         }
328 
329         mOriginalIcon.setForceHideDot(true);
330 
331         // All views are added. Animate layout from now on.
332         setLayoutTransition(new LayoutTransition());
333 
334         // Load the shortcuts on a background thread and update the container as it animates.
335         MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
336                 mLauncher, originalItemInfo, new Handler(Looper.getMainLooper()),
337                 this, mShortcuts, notificationKeys));
338     }
339 
getTitleForAccessibility()340     private String getTitleForAccessibility() {
341         return getContext().getString(mNumNotifications == 0 ?
342                 R.string.action_deep_shortcut :
343                 R.string.shortcuts_menu_with_notifications_description);
344     }
345 
346     @Override
getTargetObjectLocation(Rect outPos)347     protected void getTargetObjectLocation(Rect outPos) {
348         getPopupContainer().getDescendantRectRelativeToSelf(mOriginalIcon, outPos);
349         outPos.top += mOriginalIcon.getPaddingTop();
350         outPos.left += mOriginalIcon.getPaddingLeft();
351         outPos.right -= mOriginalIcon.getPaddingRight();
352         outPos.bottom = outPos.top + (mOriginalIcon.getIcon() != null
353                 ? mOriginalIcon.getIcon().getBounds().height()
354                 : mOriginalIcon.getHeight());
355     }
356 
applyNotificationInfos(List<NotificationInfo> notificationInfos)357     public void applyNotificationInfos(List<NotificationInfo> notificationInfos) {
358         if (mNotificationItemView != null) {
359             mNotificationItemView.applyNotificationInfos(notificationInfos);
360         }
361     }
362 
updateHiddenShortcuts()363     private void updateHiddenShortcuts() {
364         int allowedCount = mNotificationItemView != null
365                 ? MAX_SHORTCUTS_IF_NOTIFICATIONS : MAX_SHORTCUTS;
366 
367         int total = mShortcuts.size();
368         for (int i = 0; i < total; i++) {
369             DeepShortcutView view = mShortcuts.get(i);
370             view.setVisibility(i >= allowedCount ? GONE : VISIBLE);
371         }
372     }
373 
initializeSystemShortcut(int resId, ViewGroup container, SystemShortcut info)374     private void initializeSystemShortcut(int resId, ViewGroup container, SystemShortcut info) {
375         View view = inflateAndAdd(
376                 resId, container, getInsertIndexForSystemShortcut(container, info));
377         if (view instanceof DeepShortcutView) {
378             // Expanded system shortcut, with both icon and text shown on white background.
379             final DeepShortcutView shortcutView = (DeepShortcutView) view;
380             info.setIconAndLabelFor(shortcutView.getIconView(), shortcutView.getBubbleText());
381         } else if (view instanceof ImageView) {
382             // Only the system shortcut icon shows on a gray background header.
383             info.setIconAndContentDescriptionFor((ImageView) view);
384             view.setTooltipText(view.getContentDescription());
385         }
386         view.setTag(info);
387         view.setOnClickListener(info);
388     }
389 
390     /**
391      * Returns an index for inserting a shortcut into a container.
392      */
getInsertIndexForSystemShortcut(ViewGroup container, SystemShortcut shortcut)393     private int getInsertIndexForSystemShortcut(ViewGroup container, SystemShortcut shortcut) {
394         final View separator = container.findViewById(R.id.separator);
395 
396         return separator != null && shortcut.isLeftGroup() ?
397                 container.indexOfChild(separator) :
398                 container.getChildCount();
399     }
400 
401     /**
402      * Determines when the deferred drag should be started.
403      *
404      * Current behavior:
405      * - Start the drag if the touch passes a certain distance from the original touch down.
406      */
createPreDragCondition()407     public DragOptions.PreDragCondition createPreDragCondition() {
408         return new DragOptions.PreDragCondition() {
409 
410             @Override
411             public boolean shouldStartDrag(double distanceDragged) {
412                 return distanceDragged > mStartDragThreshold;
413             }
414 
415             @Override
416             public void onPreDragStart(DropTarget.DragObject dragObject) {
417                 if (mIsAboveIcon) {
418                     // Hide only the icon, keep the text visible.
419                     mOriginalIcon.setIconVisible(false);
420                     mOriginalIcon.setVisibility(VISIBLE);
421                 } else {
422                     // Hide both the icon and text.
423                     mOriginalIcon.setVisibility(INVISIBLE);
424                 }
425             }
426 
427             @Override
428             public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) {
429                 mOriginalIcon.setIconVisible(true);
430                 if (dragStarted) {
431                     // Make sure we keep the original icon hidden while it is being dragged.
432                     mOriginalIcon.setVisibility(INVISIBLE);
433                 } else {
434                     // TODO: add WW logging if want to add logging for long press on popup
435                     //  container.
436                     //  mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon);
437                     if (!mIsAboveIcon) {
438                         // Show the icon but keep the text hidden.
439                         mOriginalIcon.setVisibility(VISIBLE);
440                         mOriginalIcon.setTextVisibility(false);
441                     }
442                 }
443             }
444         };
445     }
446 
447     private void updateNotificationHeader() {
448         ItemInfoWithIcon itemInfo = (ItemInfoWithIcon) mOriginalIcon.getTag();
449         DotInfo dotInfo = mLauncher.getDotInfoForItem(itemInfo);
450         if (mNotificationItemView != null && dotInfo != null) {
451             mNotificationItemView.updateHeader(dotInfo.getNotificationCount());
452         }
453     }
454 
455     @Override
456     public void onDropCompleted(View target, DragObject d, boolean success) {  }
457 
458     @Override
459     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
460         // Either the original icon or one of the shortcuts was dragged.
461         // Hide the container, but don't remove it yet because that interferes with touch events.
462         mDeferContainerRemoval = true;
463         animateClose();
464     }
465 
466     @Override
467     public void onDragEnd() {
468         if (!mIsOpen) {
469             if (mOpenCloseAnimator != null) {
470                 // Close animation is running.
471                 mDeferContainerRemoval = false;
472             } else {
473                 // Close animation is not running.
474                 if (mDeferContainerRemoval) {
475                     closeComplete();
476                 }
477             }
478         }
479     }
480 
481     @Override
482     protected void onCreateCloseAnimation(AnimatorSet anim) {
483         // Animate original icon's text back in.
484         anim.play(mOriginalIcon.createTextAlphaAnimator(true /* fadeIn */));
485         mOriginalIcon.setForceHideDot(false);
486     }
487 
488     @Override
489     protected void closeComplete() {
490         PopupContainerWithArrow openPopup = getOpen(mLauncher);
491         if (openPopup == null || openPopup.mOriginalIcon != mOriginalIcon) {
492             mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible());
493             mOriginalIcon.setForceHideDot(false);
494         }
495         super.closeComplete();
496     }
497 
498     /**
499      * Returns a PopupContainerWithArrow which is already open or null
500      */
501     public static PopupContainerWithArrow getOpen(BaseDraggingActivity launcher) {
502         return getOpenView(launcher, TYPE_ACTION_POPUP);
503     }
504 
505     /**
506      * Utility class to handle updates while the popup is visible (like widgets and
507      * notification changes)
508      */
509     private class LiveUpdateHandler implements
510             PopupDataChangeListener, View.OnAttachStateChangeListener {
511 
512         private final Launcher mLauncher;
513 
514         LiveUpdateHandler(Launcher launcher) {
515             mLauncher = launcher;
516         }
517 
518         @Override
519         public void onViewAttachedToWindow(View view) {
520             mLauncher.getPopupDataProvider().setChangeListener(this);
521         }
522 
523         @Override
524         public void onViewDetachedFromWindow(View view) {
525             mLauncher.getPopupDataProvider().setChangeListener(null);
526         }
527 
528         private View getWidgetsView(ViewGroup container) {
529             for (int i = container.getChildCount() - 1; i >= 0; --i) {
530                 View systemShortcutView = container.getChildAt(i);
531                 if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) {
532                     return systemShortcutView;
533                 }
534             }
535             return null;
536         }
537 
538         @Override
539         public void onWidgetsBound() {
540             ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
541             SystemShortcut widgetInfo = SystemShortcut.WIDGETS.getShortcut(mLauncher, itemInfo);
542             View widgetsView = getWidgetsView(PopupContainerWithArrow.this);
543             if (widgetsView == null && mWidgetContainer != null) {
544                 widgetsView = getWidgetsView(mWidgetContainer);
545             }
546 
547             if (widgetInfo != null && widgetsView == null) {
548                 // We didn't have any widgets cached but now there are some, so enable the shortcut.
549                 if (mSystemShortcutContainer != PopupContainerWithArrow.this) {
550                     if (mWidgetContainer == null) {
551                         mWidgetContainer = inflateAndAdd(R.layout.widget_shortcut_container,
552                                 PopupContainerWithArrow.this);
553                     }
554                     initializeSystemShortcut(R.layout.system_shortcut, mWidgetContainer,
555                             widgetInfo);
556                 } else {
557                     // If using the expanded system shortcut (as opposed to just the icon), we need
558                     // to reopen the container to ensure measurements etc. all work out. While this
559                     // could be quite janky, in practice the user would typically see a small
560                     // flicker as the animation restarts partway through, and this is a very rare
561                     // edge case anyway.
562                     close(false);
563                     PopupContainerWithArrow.showForIcon(mOriginalIcon);
564                 }
565             } else if (widgetInfo == null && widgetsView != null) {
566                 // No widgets exist, but we previously added the shortcut so remove it.
567                 if (mSystemShortcutContainer
568                         != PopupContainerWithArrow.this
569                         && mWidgetContainer != null) {
570                     mWidgetContainer.removeView(widgetsView);
571                 } else {
572                     close(false);
573                     PopupContainerWithArrow.showForIcon(mOriginalIcon);
574                 }
575             }
576         }
577 
578         /**
579          * Updates the notification header if the original icon's dot updated.
580          */
581         @Override
582         public void onNotificationDotsUpdated(Predicate<PackageUserKey> updatedDots) {
583             ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
584             PackageUserKey packageUser = PackageUserKey.fromItemInfo(itemInfo);
585             if (updatedDots.test(packageUser)) {
586                 updateNotificationHeader();
587             }
588         }
589 
590 
591         @Override
592         public void trimNotifications(Map<PackageUserKey, DotInfo> updatedDots) {
593             if (mNotificationItemView == null) {
594                 return;
595             }
596             ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
597             DotInfo dotInfo = updatedDots.get(PackageUserKey.fromItemInfo(originalInfo));
598             if (dotInfo == null || dotInfo.getNotificationKeys().size() == 0) {
599                 // No more notifications, remove the notification views and expand all shortcuts.
600                 mNotificationItemView.removeAllViews();
601                 mNotificationItemView = null;
602                 mNotificationContainer.setVisibility(GONE);
603                 updateHiddenShortcuts();
604                 assignMarginsAndBackgrounds(PopupContainerWithArrow.this);
605             } else {
606                 mNotificationItemView.trimNotifications(
607                         NotificationKeyData.extractKeysOnly(dotInfo.getNotificationKeys()));
608             }
609         }
610     }
611 
612     /**
613      * Dismisses the popup if it is no longer valid
614      */
615     public static void dismissInvalidPopup(BaseDraggingActivity activity) {
616         PopupContainerWithArrow popup = getOpen(activity);
617         if (popup != null && (!popup.mOriginalIcon.isAttachedToWindow()
618                 || !canShow(popup.mOriginalIcon, (ItemInfo) popup.mOriginalIcon.getTag()))) {
619             popup.animateClose();
620         }
621     }
622 
623     /**
624      * Handler to control drag-and-drop for popup items
625      */
626     public interface PopupItemDragHandler extends OnLongClickListener, OnTouchListener { }
627 
628     /**
629      * Drag and drop handler for popup items in Launcher activity
630      */
631     public static class LauncherPopupItemDragHandler implements PopupItemDragHandler {
632 
633         protected final Point mIconLastTouchPos = new Point();
634         private final Launcher mLauncher;
635         private final PopupContainerWithArrow mContainer;
636 
637         LauncherPopupItemDragHandler(Launcher launcher, PopupContainerWithArrow container) {
638             mLauncher = launcher;
639             mContainer = container;
640         }
641 
642         @Override
643         public boolean onTouch(View v, MotionEvent ev) {
644             // Touched a shortcut, update where it was touched so we can drag from there on
645             // long click.
646             switch (ev.getAction()) {
647                 case MotionEvent.ACTION_DOWN:
648                 case MotionEvent.ACTION_MOVE:
649                     mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
650                     break;
651             }
652             return false;
653         }
654 
655         @Override
656         public boolean onLongClick(View v) {
657             if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
658             // Return early if not the correct view
659             if (!(v.getParent() instanceof DeepShortcutView)) return false;
660 
661             // Long clicked on a shortcut.
662             DeepShortcutView sv = (DeepShortcutView) v.getParent();
663             sv.setWillDrawIcon(false);
664 
665             // Move the icon to align with the center-top of the touch point
666             Point iconShift = new Point();
667             iconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x;
668             iconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx;
669 
670             DraggableView draggableView = DraggableView.ofType(DraggableView.DRAGGABLE_ICON);
671             WorkspaceItemInfo itemInfo = sv.getFinalInfo();
672             itemInfo.container = CONTAINER_SHORTCUTS;
673             DragView dv = mLauncher.getWorkspace().beginDragShared(sv.getIconView(), draggableView,
674                     mContainer, itemInfo,
675                     new ShortcutDragPreviewProvider(sv.getIconView(), iconShift),
676                     new DragOptions());
677             dv.animateShift(-iconShift.x, -iconShift.y);
678 
679             // TODO: support dragging from within folder without having to close it
680             AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_FOLDER);
681             return false;
682         }
683     }
684 }
685