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