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