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