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