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