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