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