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