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