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.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE;
23 import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS;
24 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
25 
26 import android.animation.AnimatorSet;
27 import android.animation.LayoutTransition;
28 import android.content.Context;
29 import android.graphics.Point;
30 import android.graphics.PointF;
31 import android.graphics.Rect;
32 import android.os.Handler;
33 import android.os.Looper;
34 import android.util.AttributeSet;
35 import android.view.MotionEvent;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.widget.ImageView;
39 
40 import androidx.annotation.LayoutRes;
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.Flags;
49 import com.android.launcher3.Launcher;
50 import com.android.launcher3.R;
51 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
52 import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate;
53 import com.android.launcher3.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.shortcuts.DeepShortcutView;
61 import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
62 import com.android.launcher3.touch.ItemLongClickListener;
63 import com.android.launcher3.util.PackageUserKey;
64 import com.android.launcher3.util.ShortcutUtil;
65 import com.android.launcher3.views.ActivityContext;
66 import com.android.launcher3.views.BaseDragLayer;
67 
68 import java.util.ArrayList;
69 import java.util.List;
70 import java.util.Objects;
71 import java.util.Optional;
72 import java.util.stream.Collectors;
73 
74 /**
75  * A container for shortcuts to deep links associated with an app.
76  *
77  * @param <T> The activity on with the popup shows
78  */
79 public class PopupContainerWithArrow<T extends Context & ActivityContext>
80         extends ArrowPopup<T> implements DragSource, DragController.DragListener {
81 
82     private final List<DeepShortcutView> mDeepShortcuts = new ArrayList<>();
83     private final PointF mInterceptTouchDown = new PointF();
84 
85     private final int mStartDragThreshold;
86 
87     private static final int SHORTCUT_COLLAPSE_THRESHOLD = 6;
88 
89     private final float mShortcutHeight;
90 
91     private BubbleTextView mOriginalIcon;
92     private int mContainerWidth;
93 
94     private ViewGroup mWidgetContainer;
95     private ViewGroup mDeepShortcutContainer;
96     private ViewGroup mSystemShortcutContainer;
97 
98     protected PopupItemDragHandler mPopupItemDragHandler;
99     protected LauncherAccessibilityDelegate mAccessibilityDelegate;
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         mContainerWidth = getResources().getDimensionPixelSize(R.dimen.bg_popup_item_width);
106         mShortcutHeight = getResources().getDimension(R.dimen.system_shortcut_header_height);
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 
117     @Override
getAccessibilityInitialFocusView()118     protected View getAccessibilityInitialFocusView() {
119         if (mSystemShortcutContainer != null) {
120             return mSystemShortcutContainer.getChildAt(0);
121         }
122         return super.getAccessibilityInitialFocusView();
123     }
124 
getAccessibilityDelegate()125     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
126         return mAccessibilityDelegate;
127     }
128 
129     @Override
onInterceptTouchEvent(MotionEvent ev)130     public boolean onInterceptTouchEvent(MotionEvent ev) {
131         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
132             mInterceptTouchDown.set(ev.getX(), ev.getY());
133         }
134         // Stop sending touch events to deep shortcut views if user moved beyond touch slop.
135         return squaredHypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY())
136                 > squaredTouchSlop(getContext());
137     }
138 
139     @Override
isOfType(int type)140     protected boolean isOfType(int type) {
141         return (type & TYPE_ACTION_POPUP) != 0;
142     }
143 
getItemClickListener()144     public OnClickListener getItemClickListener() {
145         return (view) -> {
146             mActivityContext.getItemOnClickListener().onClick(view);
147         };
148     }
149 
setPopupItemDragHandler(PopupItemDragHandler popupItemDragHandler)150     public void setPopupItemDragHandler(PopupItemDragHandler popupItemDragHandler) {
151         mPopupItemDragHandler = popupItemDragHandler;
152     }
153 
getItemDragHandler()154     public PopupItemDragHandler getItemDragHandler() {
155         return mPopupItemDragHandler;
156     }
157 
158     @Override
onControllerInterceptTouchEvent(MotionEvent ev)159     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
160         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
161             BaseDragLayer dl = getPopupContainer();
162             if (!dl.isEventOverView(this, ev)) {
163                 // TODO: add WW log if want to log if tap closed deep shortcut container.
164                 close(true);
165 
166                 // We let touches on the original icon go through so that users can launch
167                 // the app with one tap if they don't find a shortcut they want.
168                 return mOriginalIcon == null || !dl.isEventOverView(mOriginalIcon, ev);
169             }
170         }
171         return false;
172     }
173 
174     /**
175      * Returns true if we can show the container.
176      *
177      * @deprecated Left here since some dependent projects are using this method
178      */
179     @Deprecated
canShow(View icon, ItemInfo item)180     public static boolean canShow(View icon, ItemInfo item) {
181         return icon instanceof BubbleTextView && ShortcutUtil.supportsShortcuts(item);
182     }
183 
184     /**
185      * Shows a popup with shortcuts associated with a Launcher icon
186      * @param icon the app icon to show the popup for
187      * @return the container if shown or null.
188      */
showForIcon(BubbleTextView icon)189     public static PopupContainerWithArrow<Launcher> showForIcon(BubbleTextView icon) {
190         Launcher launcher = Launcher.getLauncher(icon.getContext());
191         if (getOpen(launcher) != null) {
192             // There is already an items container open, so don't open this one.
193             icon.clearFocus();
194             return null;
195         }
196         ItemInfo item = (ItemInfo) icon.getTag();
197         if (!ShortcutUtil.supportsShortcuts(item)) {
198             return null;
199         }
200 
201         PopupContainerWithArrow<Launcher> container;
202         PopupDataProvider popupDataProvider = launcher.getPopupDataProvider();
203         int deepShortcutCount = popupDataProvider.getShortcutCountForItem(item);
204         List<SystemShortcut> systemShortcuts = launcher.getSupportedShortcuts()
205                 .map(s -> s.getShortcut(launcher, item, icon))
206                 .filter(Objects::nonNull)
207                 .collect(Collectors.toList());
208         container = (PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
209                 R.layout.popup_container, launcher.getDragLayer(), false);
210         container.configureForLauncher(launcher, item);
211         container.populateAndShowRows(icon, deepShortcutCount, systemShortcuts);
212         launcher.refreshAndBindWidgetsForPackageUser(PackageUserKey.fromItemInfo(item));
213         container.requestFocus();
214         return container;
215     }
216 
configureForLauncher(Launcher launcher, ItemInfo itemInfo)217     private void configureForLauncher(Launcher launcher, ItemInfo itemInfo) {
218         addOnAttachStateChangeListener(new LauncherPopupLiveUpdateHandler(
219                 launcher, (PopupContainerWithArrow<Launcher>) this));
220         if (!Flags.privateSpaceRestrictItemDrag()
221                 || !(itemInfo instanceof ItemInfoWithIcon itemInfoWithIcon)
222                 || (itemInfoWithIcon.runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0) {
223             mPopupItemDragHandler = new LauncherPopupItemDragHandler(launcher, this);
224         }
225         mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(launcher);
226         launcher.getDragController().addDragListener(this);
227     }
228 
229     /**
230      * Populate and show shortcuts for the Launcher U app shortcut design.
231      * Will inflate the container and shortcut View instances for the popup container.
232      * @param originalIcon App icon that the popup is shown for
233      * @param deepShortcutCount Number of DeepShortcutView instances to add to container
234      * @param systemShortcuts List of SystemShortcuts to add to container
235      */
populateAndShowRows(final BubbleTextView originalIcon, int deepShortcutCount, List<SystemShortcut> systemShortcuts)236     public void populateAndShowRows(final BubbleTextView originalIcon,
237             int deepShortcutCount, List<SystemShortcut> systemShortcuts) {
238 
239         mOriginalIcon = originalIcon;
240         mContainerWidth = getResources().getDimensionPixelSize(R.dimen.bg_popup_item_width);
241 
242         if (deepShortcutCount > 0) {
243             addAllShortcuts(deepShortcutCount, systemShortcuts);
244         } else if (!systemShortcuts.isEmpty()) {
245             addSystemShortcuts(systemShortcuts,
246                     R.layout.system_shortcut_rows_container,
247                     R.layout.system_shortcut);
248         }
249         show();
250         loadAppShortcuts((ItemInfo) originalIcon.getTag());
251     }
252 
253     /**
254      * Animates and loads shortcuts on background thread for this popup container
255      */
loadAppShortcuts(ItemInfo originalItemInfo)256     private void loadAppShortcuts(ItemInfo originalItemInfo) {
257         setAccessibilityPaneTitle(getTitleForAccessibility());
258         mOriginalIcon.setForceHideDot(true);
259         // All views are added. Animate layout from now on.
260         setLayoutTransition(new LayoutTransition());
261         // Load the shortcuts on a background thread and update the container as it animates.
262         MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
263                 mActivityContext, originalItemInfo, new Handler(Looper.getMainLooper()),
264                 this, mDeepShortcuts));
265     }
266 
267     /**
268      * Adds any Deep Shortcuts, System Shortcuts and the Widget Shortcut to their respective
269      * containers
270      * @param deepShortcutCount number of DeepShortcutView instances
271      * @param systemShortcuts List of SystemShortcuts
272      */
addAllShortcuts(int deepShortcutCount, List<SystemShortcut> systemShortcuts)273     private void addAllShortcuts(int deepShortcutCount,
274             List<SystemShortcut> systemShortcuts) {
275         if (deepShortcutCount + systemShortcuts.size() <= SHORTCUT_COLLAPSE_THRESHOLD) {
276             // add all system shortcuts including widgets shortcut to same container
277             addSystemShortcuts(systemShortcuts,
278                     R.layout.system_shortcut_rows_container,
279                     R.layout.system_shortcut);
280             float currentHeight = (mShortcutHeight * systemShortcuts.size())
281                     + mChildContainerMargin;
282             addDeepShortcuts(deepShortcutCount, currentHeight);
283             return;
284         }
285 
286         float currentHeight = mShortcutHeight + mChildContainerMargin;
287         List<SystemShortcut> nonWidgetSystemShortcuts =
288                 getNonWidgetSystemShortcuts(systemShortcuts);
289         // If total shortcuts over threshold, collapse system shortcuts to single row
290         addSystemShortcutsIconsOnly(nonWidgetSystemShortcuts);
291         // May need to recalculate row width
292         mContainerWidth = Math.max(mContainerWidth,
293                 nonWidgetSystemShortcuts.size() * getResources()
294                         .getDimensionPixelSize(R.dimen.system_shortcut_header_icon_touch_size));
295         // Add widget shortcut to separate container
296         Optional<SystemShortcut.Widgets> widgetShortcutOpt = getWidgetShortcut(systemShortcuts);
297         if (widgetShortcutOpt.isPresent()) {
298             mWidgetContainer = inflateAndAdd(R.layout.widget_shortcut_container_material_u,
299                     this);
300             initializeWidgetShortcut(mWidgetContainer, widgetShortcutOpt.get());
301             currentHeight += mShortcutHeight + mChildContainerMargin;
302         }
303         addDeepShortcuts(deepShortcutCount, currentHeight);
304     }
305 
306     /**
307      * Finds the first instance of the Widgets Shortcut from the SystemShortcut List
308      * @param systemShortcuts List of SystemShortcut instances to search
309      * @return Optional Widgets SystemShortcut
310      */
getWidgetShortcut( List<SystemShortcut> systemShortcuts)311     private static Optional<SystemShortcut.Widgets> getWidgetShortcut(
312             List<SystemShortcut> systemShortcuts) {
313         return systemShortcuts
314                 .stream()
315                 .filter(shortcut -> shortcut instanceof SystemShortcut.Widgets)
316                 .map(SystemShortcut.Widgets.class::cast)
317                 .findFirst();
318     }
319 
320     /**
321      * Returns list of [systemShortcuts] without the Widgets shortcut instance if found
322      * @param systemShortcuts list of SystemShortcuts to filter from
323      * @return systemShortcuts without the Widgets Shortcut
324      */
getNonWidgetSystemShortcuts( List<SystemShortcut> systemShortcuts)325     private static List<SystemShortcut> getNonWidgetSystemShortcuts(
326             List<SystemShortcut> systemShortcuts) {
327 
328         return systemShortcuts
329                 .stream()
330                 .filter(shortcut -> !(shortcut instanceof SystemShortcut.Widgets))
331                 .collect(Collectors.toList());
332     }
333 
334     /**
335      * Inflates the given systemShortcutContainerLayout as a container, and populates with
336      * the systemShortcuts as views using the systemShortcutLayout
337      * @param systemShortcuts List of SystemShortcut to inflate as Views
338      * @param systemShortcutContainerLayout Layout Resource for the Container of shortcut Views
339      * @param systemShortcutLayout Layout Resource for the individual shortcut Views
340      */
addSystemShortcuts(List<SystemShortcut> systemShortcuts, @LayoutRes int systemShortcutContainerLayout, @LayoutRes int systemShortcutLayout)341     private void addSystemShortcuts(List<SystemShortcut> systemShortcuts,
342             @LayoutRes int systemShortcutContainerLayout, @LayoutRes int systemShortcutLayout) {
343 
344         if (systemShortcuts.size() == 0) {
345             return;
346         }
347         mSystemShortcutContainer = inflateAndAdd(systemShortcutContainerLayout, this);
348         mWidgetContainer = mSystemShortcutContainer;
349         for (int i = 0; i < systemShortcuts.size(); i++) {
350             initializeSystemShortcut(
351                     systemShortcutLayout,
352                     mSystemShortcutContainer,
353                     systemShortcuts.get(i),
354                     i < systemShortcuts.size() - 1);
355         }
356     }
357 
addSystemShortcutsIconsOnly(List<SystemShortcut> systemShortcuts)358     private void addSystemShortcutsIconsOnly(List<SystemShortcut> systemShortcuts) {
359         if (systemShortcuts.size() == 0) {
360             return;
361         }
362 
363         mSystemShortcutContainer = inflateAndAdd(R.layout.system_shortcut_icons_container, this);
364 
365         for (int i = 0; i < systemShortcuts.size(); i++) {
366             @LayoutRes int shortcutIconLayout = R.layout.system_shortcut_icon_only;
367             boolean shouldAppendSpacer = true;
368 
369             if (i == 0) {
370                 shortcutIconLayout = R.layout.system_shortcut_icon_only_start;
371             } else if (i == systemShortcuts.size() - 1) {
372                 shortcutIconLayout = R.layout.system_shortcut_icon_only_end;
373                 shouldAppendSpacer = false;
374             }
375             initializeSystemShortcut(
376                     shortcutIconLayout,
377                     mSystemShortcutContainer,
378                     systemShortcuts.get(i),
379                     shouldAppendSpacer);
380         }
381     }
382 
383     /**
384      * Inflates and adds [deepShortcutCount] number of DeepShortcutView for the  to a new container
385      * @param deepShortcutCount number of DeepShortcutView instances to add
386      * @param currentHeight height of popup before adding deep shortcuts
387      */
addDeepShortcuts(int deepShortcutCount, float currentHeight)388     private void addDeepShortcuts(int deepShortcutCount, float currentHeight) {
389         mDeepShortcutContainer = inflateAndAdd(R.layout.deep_shortcut_container, this);
390         for (int i = deepShortcutCount; i > 0; i--) {
391             currentHeight += mShortcutHeight;
392             // when there is limited vertical screen space, limit total popup rows to fit
393             if (currentHeight >= mActivityContext.getDeviceProfile().availableHeightPx) break;
394             DeepShortcutView v = inflateAndAdd(R.layout.deep_shortcut,
395                     mDeepShortcutContainer);
396             v.getLayoutParams().width = mContainerWidth;
397             mDeepShortcuts.add(v);
398         }
399         updateHiddenShortcuts();
400     }
401 
getOriginalIcon()402     protected BubbleTextView getOriginalIcon() {
403         return mOriginalIcon;
404     }
405 
getSystemShortcutContainer()406     protected ViewGroup getSystemShortcutContainer() {
407         return mSystemShortcutContainer;
408     }
409 
getWidgetContainer()410     protected ViewGroup getWidgetContainer() {
411         return mWidgetContainer;
412     }
413 
setWidgetContainer(ViewGroup widgetContainer)414     protected void setWidgetContainer(ViewGroup widgetContainer) {
415         mWidgetContainer = widgetContainer;
416     }
417 
getTitleForAccessibility()418     private String getTitleForAccessibility() {
419         return getContext().getString(R.string.action_deep_shortcut);
420     }
421 
422     @Override
getTargetObjectLocation(Rect outPos)423     protected void getTargetObjectLocation(Rect outPos) {
424         getPopupContainer().getDescendantRectRelativeToSelf(mOriginalIcon, outPos);
425         outPos.top += mOriginalIcon.getPaddingTop();
426         outPos.left += mOriginalIcon.getPaddingLeft();
427         outPos.right -= mOriginalIcon.getPaddingRight();
428         outPos.bottom = outPos.top + (mOriginalIcon.getIcon() != null
429                 ? mOriginalIcon.getIcon().getBounds().height()
430                 : mOriginalIcon.getHeight());
431     }
432 
updateHiddenShortcuts()433     protected void updateHiddenShortcuts() {
434         int total = mDeepShortcuts.size();
435         for (int i = 0; i < total; i++) {
436             DeepShortcutView view = mDeepShortcuts.get(i);
437             view.setVisibility(i >= MAX_SHORTCUTS ? GONE : VISIBLE);
438         }
439     }
440 
initializeWidgetShortcut(ViewGroup container, SystemShortcut info)441     protected void initializeWidgetShortcut(ViewGroup container, SystemShortcut info) {
442         View view = initializeSystemShortcut(R.layout.system_shortcut, container, info, false);
443         view.getLayoutParams().width = mContainerWidth;
444     }
445 
446     /**
447      * Initializes and adds View for given SystemShortcut to a container.
448      * @param resId Resource id to use for SystemShortcut View.
449      * @param container ViewGroup to add the shortcut View to as a parent
450      * @param info The SystemShortcut instance to create a View for.
451      * @param shouldAppendSpacer If True, will add a spacer after the shortcut, when showing the
452      *                        SystemShortcut as an icon only. Used to space the shortcut icons
453      *                        evenly.
454      * @return The view inflated for the SystemShortcut
455      */
initializeSystemShortcut(int resId, ViewGroup container, SystemShortcut info, boolean shouldAppendSpacer)456     protected View initializeSystemShortcut(int resId, ViewGroup container, SystemShortcut info,
457             boolean shouldAppendSpacer) {
458         View view = inflateAndAdd(resId, container);
459         if (view instanceof DeepShortcutView) {
460             // System shortcut takes entire row with icon and text
461             final DeepShortcutView shortcutView = (DeepShortcutView) view;
462             info.setIconAndLabelFor(shortcutView.getIconView(), shortcutView.getBubbleText());
463         } else if (view instanceof ImageView) {
464             // System shortcut is just an icon
465             info.setIconAndContentDescriptionFor((ImageView) view);
466             if (shouldAppendSpacer) inflateAndAdd(R.layout.system_shortcut_spacer, container);
467             view.setTooltipText(view.getContentDescription());
468         }
469         view.setTag(info);
470         view.setOnClickListener(info);
471         return view;
472     }
473 
474     /**
475      * Determines when the deferred drag should be started.
476      *
477      * Current behavior:
478      * - Start the drag if the touch passes a certain distance from the original touch down.
479      */
createPreDragCondition(boolean updateIconUi)480     public DragOptions.PreDragCondition createPreDragCondition(boolean updateIconUi) {
481         return new DragOptions.PreDragCondition() {
482 
483             @Override
484             public boolean shouldStartDrag(double distanceDragged) {
485                 return distanceDragged > mStartDragThreshold;
486             }
487 
488             @Override
489             public void onPreDragStart(DropTarget.DragObject dragObject) {
490                 if (!updateIconUi) {
491                     return;
492                 }
493                 if (mIsAboveIcon) {
494                     // Hide only the icon, keep the text visible.
495                     mOriginalIcon.setIconVisible(false);
496                     mOriginalIcon.setVisibility(VISIBLE);
497                 } else {
498                     // Hide both the icon and text.
499                     mOriginalIcon.setVisibility(INVISIBLE);
500                 }
501             }
502 
503             @Override
504             public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) {
505                 if (!updateIconUi) {
506                     return;
507                 }
508                 mOriginalIcon.setIconVisible(true);
509                 if (dragStarted) {
510                     // Make sure we keep the original icon hidden while it is being dragged.
511                     mOriginalIcon.setVisibility(INVISIBLE);
512                 } else {
513                     // TODO: add WW logging if want to add logging for long press on popup
514                     //  container.
515                     //  mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon);
516                     if (!mIsAboveIcon) {
517                         // Show the icon but keep the text hidden.
518                         mOriginalIcon.setVisibility(VISIBLE);
519                         mOriginalIcon.setTextVisibility(false);
520                     }
521                 }
522             }
523         };
524     }
525 
526     @Override
527     public void onDropCompleted(View target, DragObject d, boolean success) {  }
528 
529     @Override
530     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
531         // Either the original icon or one of the shortcuts was dragged.
532         // Hide the container, but don't remove it yet because that interferes with touch events.
533         mDeferContainerRemoval = true;
534         animateClose();
535     }
536 
537     @Override
538     public void onDragEnd() {
539         if (!mIsOpen) {
540             if (mOpenCloseAnimator != null) {
541                 // Close animation is running.
542                 mDeferContainerRemoval = false;
543             } else {
544                 // Close animation is not running.
545                 if (mDeferContainerRemoval) {
546                     closeComplete();
547                 }
548             }
549         }
550     }
551 
552     @Override
553     protected void onCreateCloseAnimation(AnimatorSet anim) {
554         // Animate original icon's text back in.
555         anim.play(mOriginalIcon.createTextAlphaAnimator(true /* fadeIn */));
556         mOriginalIcon.setForceHideDot(false);
557     }
558 
559     @Override
560     protected void closeComplete() {
561         super.closeComplete();
562         if (mActivityContext.getDragController() != null) {
563             mActivityContext.getDragController().removeDragListener(this);
564         }
565         PopupContainerWithArrow openPopup = getOpen(mActivityContext);
566         if (openPopup == null || openPopup.mOriginalIcon != mOriginalIcon) {
567             mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible());
568             mOriginalIcon.setForceHideDot(false);
569         }
570     }
571 
572     /**
573      * Returns a PopupContainerWithArrow which is already open or null
574      */
575     public static <T extends Context & ActivityContext> PopupContainerWithArrow getOpen(T context) {
576         return getOpenView(context, TYPE_ACTION_POPUP);
577     }
578 
579     /**
580      * Dismisses the popup if it is no longer valid
581      */
582     public static void dismissInvalidPopup(BaseDraggingActivity activity) {
583         PopupContainerWithArrow popup = getOpen(activity);
584         if (popup != null && (!popup.mOriginalIcon.isAttachedToWindow()
585                 || !ShortcutUtil.supportsShortcuts((ItemInfo) popup.mOriginalIcon.getTag()))) {
586             popup.animateClose();
587         }
588     }
589 
590     /**
591      * Handler to control drag-and-drop for popup items
592      */
593     public interface PopupItemDragHandler extends OnLongClickListener, OnTouchListener { }
594 
595     /**
596      * Drag and drop handler for popup items in Launcher activity
597      */
598     public static class LauncherPopupItemDragHandler implements PopupItemDragHandler {
599 
600         protected final Point mIconLastTouchPos = new Point();
601         private final Launcher mLauncher;
602         private final PopupContainerWithArrow mContainer;
603 
604         LauncherPopupItemDragHandler(Launcher launcher, PopupContainerWithArrow container) {
605             mLauncher = launcher;
606             mContainer = container;
607         }
608 
609         @Override
610         public boolean onTouch(View v, MotionEvent ev) {
611             // Touched a shortcut, update where it was touched so we can drag from there on
612             // long click.
613             switch (ev.getAction()) {
614                 case MotionEvent.ACTION_DOWN:
615                 case MotionEvent.ACTION_MOVE:
616                     mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
617                     break;
618             }
619             return false;
620         }
621 
622         @Override
623         public boolean onLongClick(View v) {
624             if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
625             // Return early if not the correct view
626             if (!(v.getParent() instanceof DeepShortcutView)) return false;
627 
628             // Long clicked on a shortcut.
629             DeepShortcutView sv = (DeepShortcutView) v.getParent();
630             sv.setWillDrawIcon(false);
631 
632             // Move the icon to align with the center-top of the touch point
633             Point iconShift = new Point();
634             iconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x;
635             iconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx;
636 
637             DraggableView draggableView = DraggableView.ofType(DraggableView.DRAGGABLE_ICON);
638             WorkspaceItemInfo itemInfo = sv.getFinalInfo();
639             itemInfo.container = CONTAINER_SHORTCUTS;
640             DragView dv = mLauncher.getWorkspace().beginDragShared(sv.getIconView(), draggableView,
641                     mContainer, itemInfo,
642                     new ShortcutDragPreviewProvider(sv.getIconView(), iconShift),
643                     new DragOptions());
644             dv.animateShift(-iconShift.x, -iconShift.y);
645 
646             // TODO: support dragging from within folder without having to close it
647             AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_FOLDER);
648             return false;
649         }
650     }
651 }
652