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.systemui.statusbar.notification.row; 18 19 import static android.app.Flags.notificationsRedesignTemplates; 20 import static android.view.HapticFeedbackConstants.CLOCK_TICK; 21 22 import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION; 23 24 import android.animation.Animator; 25 import android.animation.AnimatorListenerAdapter; 26 import android.animation.ValueAnimator; 27 import android.annotation.Nullable; 28 import android.content.Context; 29 import android.content.res.Resources; 30 import android.graphics.Point; 31 import android.graphics.drawable.Drawable; 32 import android.os.Handler; 33 import android.os.Looper; 34 import android.provider.Settings; 35 import android.service.notification.StatusBarNotification; 36 import android.util.ArrayMap; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.widget.FrameLayout; 41 import android.widget.FrameLayout.LayoutParams; 42 import android.widget.ImageView; 43 44 import com.android.app.animation.Interpolators; 45 import com.android.internal.annotations.VisibleForTesting; 46 import com.android.systemui.Flags; 47 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; 48 import com.android.systemui.res.R; 49 import com.android.systemui.statusbar.AlphaOptimizedImageView; 50 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; 51 import com.android.systemui.statusbar.notification.row.NotificationGuts.GutsContent; 52 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; 53 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; 54 55 import java.util.ArrayList; 56 import java.util.List; 57 import java.util.Map; 58 59 public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnClickListener, 60 ExpandableNotificationRow.LayoutListener { 61 62 // Notification must be swiped at least this fraction of a single menu item to show menu 63 private static final float SWIPED_FAR_ENOUGH_MENU_FRACTION = 0.25f; 64 private static final float SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION = 0.15f; 65 66 // When the menu is displayed, the notification must be swiped within this fraction of a single 67 // menu item to snap back to menu (else it will cover the menu or it'll be dismissed) 68 private static final float SWIPED_BACK_ENOUGH_TO_COVER_FRACTION = 0.2f; 69 70 private static final int ICON_ALPHA_ANIM_DURATION = 200; 71 private static final long SHOW_MENU_DELAY = 60; 72 73 private ExpandableNotificationRow mParent; 74 75 private Context mContext; 76 private FrameLayout mMenuContainer; 77 private NotificationMenuItem mInfoItem; 78 private MenuItem mFeedbackItem; 79 private MenuItem mSnoozeItem; 80 private ArrayList<MenuItem> mLeftMenuItems; 81 private ArrayList<MenuItem> mRightMenuItems; 82 private final Map<View, MenuItem> mMenuItemsByView = new ArrayMap<>(); 83 private OnMenuEventListener mMenuListener; 84 85 private ValueAnimator mFadeAnimator; 86 private boolean mAnimating; 87 private boolean mMenuFadedIn; 88 89 private boolean mOnLeft; 90 private boolean mIconsPlaced; 91 92 private boolean mDismissing; 93 private boolean mSnapping; 94 private float mTranslation; 95 96 private int[] mIconLocation = new int[2]; 97 private int[] mParentLocation = new int[2]; 98 99 private int mHorizSpaceForIcon = -1; 100 private int mVertSpaceForIcons = -1; 101 private int mIconPadding = -1; 102 private int mSidePadding; 103 104 private float mAlpha = 0f; 105 106 private CheckForDrag mCheckForDrag; 107 private Handler mHandler; 108 109 private boolean mMenuSnapped; 110 private boolean mMenuSnappedOnLeft; 111 private boolean mShouldShowMenu; 112 113 private boolean mIsUserTouching; 114 115 private boolean mSnappingToDismiss; 116 117 private final PeopleNotificationIdentifier mPeopleNotificationIdentifier; 118 NotificationMenuRow(Context context, PeopleNotificationIdentifier peopleNotificationIdentifier)119 public NotificationMenuRow(Context context, 120 PeopleNotificationIdentifier peopleNotificationIdentifier) { 121 mContext = context; 122 mShouldShowMenu = context.getResources().getBoolean(R.bool.config_showNotificationGear); 123 mHandler = new Handler(Looper.getMainLooper()); 124 mLeftMenuItems = new ArrayList<>(); 125 mRightMenuItems = new ArrayList<>(); 126 mPeopleNotificationIdentifier = peopleNotificationIdentifier; 127 } 128 129 @Override getMenuItems(Context context)130 public ArrayList<MenuItem> getMenuItems(Context context) { 131 return mOnLeft ? mLeftMenuItems : mRightMenuItems; 132 } 133 134 @Override getLongpressMenuItem(Context context)135 public MenuItem getLongpressMenuItem(Context context) { 136 return mInfoItem; 137 } 138 139 @Override getFeedbackMenuItem(Context context)140 public MenuItem getFeedbackMenuItem(Context context) { 141 return mFeedbackItem; 142 } 143 144 @Override getSnoozeMenuItem(Context context)145 public MenuItem getSnoozeMenuItem(Context context) { 146 return mSnoozeItem; 147 } 148 149 @VisibleForTesting getParent()150 protected ExpandableNotificationRow getParent() { 151 return mParent; 152 } 153 154 @VisibleForTesting isMenuOnLeft()155 protected boolean isMenuOnLeft() { 156 return mOnLeft; 157 } 158 159 @VisibleForTesting isMenuSnappedOnLeft()160 protected boolean isMenuSnappedOnLeft() { 161 return mMenuSnappedOnLeft; 162 } 163 164 @VisibleForTesting isMenuSnapped()165 protected boolean isMenuSnapped() { 166 return mMenuSnapped; 167 } 168 169 @VisibleForTesting isDismissing()170 protected boolean isDismissing() { 171 return mDismissing; 172 } 173 174 @VisibleForTesting isSnapping()175 protected boolean isSnapping() { 176 return mSnapping; 177 } 178 179 @VisibleForTesting isSnappingToDismiss()180 protected boolean isSnappingToDismiss() { 181 return mSnappingToDismiss; 182 } 183 184 @Override setMenuClickListener(OnMenuEventListener listener)185 public void setMenuClickListener(OnMenuEventListener listener) { 186 mMenuListener = listener; 187 } 188 189 @Override createMenu(ViewGroup parent)190 public void createMenu(ViewGroup parent) { 191 mParent = (ExpandableNotificationRow) parent; 192 createMenuViews(true /* resetState */); 193 } 194 195 @Override isMenuVisible()196 public boolean isMenuVisible() { 197 return mAlpha > 0; 198 } 199 200 @VisibleForTesting isUserTouching()201 protected boolean isUserTouching() { 202 return mIsUserTouching; 203 } 204 205 @Override shouldShowMenu()206 public boolean shouldShowMenu() { 207 return mShouldShowMenu; 208 } 209 210 @Override getMenuView()211 public View getMenuView() { 212 return mMenuContainer; 213 } 214 215 @VisibleForTesting getTranslation()216 protected float getTranslation() { 217 return mTranslation; 218 } 219 220 @Override resetMenu()221 public void resetMenu() { 222 resetState(true); 223 } 224 225 @Override onTouchEnd()226 public void onTouchEnd() { 227 mIsUserTouching = false; 228 } 229 230 @Override onNotificationUpdated()231 public void onNotificationUpdated() { 232 if (mMenuContainer == null) { 233 // Menu hasn't been created yet, no need to do anything. 234 return; 235 } 236 createMenuViews(!isMenuVisible() /* resetState */); 237 } 238 239 @Override onConfigurationChanged()240 public void onConfigurationChanged() { 241 mParent.setLayoutListener(this); 242 } 243 244 @Override onLayout()245 public void onLayout() { 246 mIconsPlaced = false; // Force icons to be re-placed 247 setMenuLocation(); 248 mParent.setLayoutListener(null); 249 } 250 createMenuViews(boolean resetState)251 private void createMenuViews(boolean resetState) { 252 final Resources res = mContext.getResources(); 253 mHorizSpaceForIcon = res.getDimensionPixelSize(R.dimen.notification_menu_icon_size); 254 mVertSpaceForIcons = res.getDimensionPixelSize(R.dimen.notification_min_height); 255 mLeftMenuItems.clear(); 256 mRightMenuItems.clear(); 257 258 final boolean showSnooze = mParent.getShowSnooze(); 259 // Construct the menu items based on the notification 260 if (showSnooze) { 261 // Only show snooze for non-foreground notifications, and if the setting is on 262 mSnoozeItem = createSnoozeItem(mContext); 263 } 264 mFeedbackItem = createFeedbackItem(mContext); 265 int personNotifType = NotificationBundleUi.isEnabled() 266 ? mParent.getEntryAdapter().getPeopleNotificationType() 267 : mPeopleNotificationIdentifier.getPeopleNotificationType(mParent.getEntryLegacy()); 268 StatusBarNotification sbn = NotificationBundleUi.isEnabled() 269 ? mParent.getEntryAdapter().getSbn() 270 : mParent.getEntryLegacy().getSbn(); 271 if (personNotifType == PeopleNotificationIdentifier.TYPE_PERSON) { 272 mInfoItem = createPartialConversationItem(mContext); 273 } else if (personNotifType >= PeopleNotificationIdentifier.TYPE_FULL_PERSON) { 274 mInfoItem = createConversationItem(mContext); 275 } else if (android.app.Flags.uiRichOngoing() 276 && android.app.Flags.apiRichOngoing() 277 && Flags.permissionHelperUiRichOngoing() 278 && sbn.getNotification().isPromotedOngoing()) { 279 mInfoItem = createPromotedItem(mContext); 280 } else { 281 mInfoItem = createInfoItem(mContext); 282 } 283 284 if (showSnooze) { 285 mRightMenuItems.add(mSnoozeItem); 286 } 287 mRightMenuItems.add(mInfoItem); 288 mRightMenuItems.add(mFeedbackItem); 289 boolean isPromotedOngoing = NotificationBundleUi.isEnabled() 290 ? mParent.getEntryAdapter().isPromotedOngoing() 291 : mParent.getEntryLegacy().isPromotedOngoing(); 292 if (android.app.Flags.uiRichOngoing() && Flags.permissionHelperInlineUiRichOngoing() 293 && isPromotedOngoing) { 294 mRightMenuItems.add(createDemoteItem(mContext)); 295 } 296 297 298 mLeftMenuItems.addAll(mRightMenuItems); 299 300 populateMenuViews(); 301 if (resetState) { 302 resetState(false /* notify */); 303 } else { 304 mIconsPlaced = false; 305 setMenuLocation(); 306 if (!mIsUserTouching) { 307 onSnapOpen(); 308 } 309 } 310 } 311 populateMenuViews()312 private void populateMenuViews() { 313 if (mMenuContainer != null) { 314 mMenuContainer.removeAllViews(); 315 mMenuItemsByView.clear(); 316 } else { 317 mMenuContainer = new FrameLayout(mContext); 318 } 319 320 final int showDismissSetting = Settings.Global.getInt(mContext.getContentResolver(), 321 Settings.Global.SHOW_NEW_NOTIF_DISMISS, /* default = */ 1); 322 final boolean newFlowHideShelf = showDismissSetting == 1; 323 324 // Populate menu items if we are using the new permission helper (U+) or if we are using 325 // the very old dismiss setting (SC-). 326 // TODO: SHOW_NEW_NOTIF_DISMISS==0 case can likely be removed. 327 if (Flags.permissionHelperInlineUiRichOngoing() || !newFlowHideShelf) { 328 List<MenuItem> menuItems = mOnLeft ? mLeftMenuItems : mRightMenuItems; 329 for (int i = 0; i < menuItems.size(); i++) { 330 addMenuView(menuItems.get(i), mMenuContainer); 331 } 332 } 333 } 334 resetState(boolean notify)335 private void resetState(boolean notify) { 336 setMenuAlpha(0f); 337 mIconsPlaced = false; 338 mMenuFadedIn = false; 339 mAnimating = false; 340 mSnapping = false; 341 mDismissing = false; 342 mMenuSnapped = false; 343 setMenuLocation(); 344 if (mMenuListener != null && notify) { 345 mMenuListener.onMenuReset(mParent); 346 } 347 } 348 349 @Override onTouchMove(float delta)350 public void onTouchMove(float delta) { 351 mSnapping = false; 352 353 if (!isTowardsMenu(delta) && isMenuLocationChange()) { 354 // Don't consider it "snapped" if location has changed. 355 mMenuSnapped = false; 356 357 // Changed directions, make sure we check to fade in icon again. 358 if (!mHandler.hasCallbacks(mCheckForDrag)) { 359 // No check scheduled, set null to schedule a new one. 360 mCheckForDrag = null; 361 } else { 362 // Check scheduled, reset alpha and update location; check will fade it in 363 setMenuAlpha(0f); 364 setMenuLocation(); 365 } 366 } 367 if (mShouldShowMenu 368 && !NotificationStackScrollLayout.isPinnedHeadsUp(getParent()) 369 && !mParent.areGutsExposed() 370 && !mParent.showingPulsing() 371 && (mCheckForDrag == null || !mHandler.hasCallbacks(mCheckForDrag))) { 372 // Only show the menu if we're not a heads up view and guts aren't exposed. 373 mCheckForDrag = new CheckForDrag(); 374 mHandler.postDelayed(mCheckForDrag, SHOW_MENU_DELAY); 375 } 376 if (canBeDismissed()) { 377 final float dismissThreshold = getDismissThreshold(); 378 final boolean snappingToDismiss = delta < -dismissThreshold || delta > dismissThreshold; 379 if (mSnappingToDismiss != snappingToDismiss) { 380 if (!Flags.magneticNotificationSwipes()) { 381 getMenuView().performHapticFeedback(CLOCK_TICK); 382 } 383 } 384 mSnappingToDismiss = snappingToDismiss; 385 } 386 } 387 388 @VisibleForTesting beginDrag()389 protected void beginDrag() { 390 mSnapping = false; 391 if (mFadeAnimator != null) { 392 mFadeAnimator.cancel(); 393 } 394 mHandler.removeCallbacks(mCheckForDrag); 395 mCheckForDrag = null; 396 mIsUserTouching = true; 397 } 398 399 @Override onTouchStart()400 public void onTouchStart() { 401 beginDrag(); 402 mSnappingToDismiss = false; 403 } 404 405 @Override onSnapOpen()406 public void onSnapOpen() { 407 mMenuSnapped = true; 408 mMenuSnappedOnLeft = isMenuOnLeft(); 409 if (mAlpha == 0f && mParent != null) { 410 fadeInMenu(mParent.getWidth()); 411 } 412 if (mMenuListener != null) { 413 mMenuListener.onMenuShown(getParent()); 414 } 415 } 416 417 @Override onSnapClosed()418 public void onSnapClosed() { 419 cancelDrag(); 420 mMenuSnapped = false; 421 mSnapping = true; 422 } 423 424 @Override onDismiss()425 public void onDismiss() { 426 cancelDrag(); 427 mMenuSnapped = false; 428 mDismissing = true; 429 } 430 431 @VisibleForTesting cancelDrag()432 protected void cancelDrag() { 433 if (mFadeAnimator != null) { 434 mFadeAnimator.cancel(); 435 } 436 mHandler.removeCallbacks(mCheckForDrag); 437 } 438 439 @VisibleForTesting getMinimumSwipeDistance()440 protected float getMinimumSwipeDistance() { 441 final float multiplier = getParent().canViewBeDismissed() 442 ? SWIPED_FAR_ENOUGH_MENU_FRACTION 443 : SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION; 444 return mHorizSpaceForIcon * multiplier; 445 } 446 447 @VisibleForTesting getMaximumSwipeDistance()448 protected float getMaximumSwipeDistance() { 449 return mHorizSpaceForIcon * SWIPED_BACK_ENOUGH_TO_COVER_FRACTION; 450 } 451 452 /** 453 * Returns whether the gesture is towards the menu location or not. 454 */ 455 @Override isTowardsMenu(float movement)456 public boolean isTowardsMenu(float movement) { 457 return isMenuVisible() 458 && ((isMenuOnLeft() && movement <= 0) 459 || (!isMenuOnLeft() && movement >= 0)); 460 } 461 462 @Override setAppName(String appName)463 public void setAppName(String appName) { 464 if (appName == null) { 465 return; 466 } 467 setAppName(appName, mLeftMenuItems); 468 setAppName(appName, mRightMenuItems); 469 } 470 setAppName(String appName, ArrayList<MenuItem> menuItems)471 private void setAppName(String appName, 472 ArrayList<MenuItem> menuItems) { 473 Resources res = mContext.getResources(); 474 final int count = menuItems.size(); 475 for (int i = 0; i < count; i++) { 476 MenuItem item = menuItems.get(i); 477 String description = String.format( 478 res.getString(R.string.notification_menu_accessibility), 479 appName, item.getContentDescription()); 480 View menuView = item.getMenuView(); 481 if (menuView != null) { 482 menuView.setContentDescription(description); 483 } 484 } 485 } 486 487 @Override onParentHeightUpdate()488 public void onParentHeightUpdate() { 489 // If we are using only icon-based buttons, adjust layout for height changes. 490 // For permission helper full-layout buttons, do not adjust. 491 if (!Flags.permissionHelperInlineUiRichOngoing()) { 492 if (mParent == null 493 || (mLeftMenuItems.isEmpty() && mRightMenuItems.isEmpty()) 494 || mMenuContainer == null) { 495 return; 496 } 497 int parentHeight = mParent.getActualHeight(); 498 float translationY; 499 if (parentHeight < mVertSpaceForIcons) { 500 translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2); 501 } else { 502 translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2; 503 } 504 mMenuContainer.setTranslationY(translationY); 505 } 506 } 507 508 @Override onParentTranslationUpdate(float translation)509 public void onParentTranslationUpdate(float translation) { 510 mTranslation = translation; 511 if (mAnimating || !mMenuFadedIn) { 512 // Don't adjust when animating, or if the menu hasn't been shown yet. 513 return; 514 } 515 final float fadeThreshold = mParent.getWidth() * 0.3f; 516 final float absTrans = Math.abs(translation); 517 float desiredAlpha = 0; 518 if (absTrans == 0) { 519 desiredAlpha = 0; 520 } else if (absTrans <= fadeThreshold) { 521 desiredAlpha = 1; 522 } else { 523 desiredAlpha = 1 - ((absTrans - fadeThreshold) / (mParent.getWidth() - fadeThreshold)); 524 } 525 setMenuAlpha(desiredAlpha); 526 } 527 528 @Override onClick(View v)529 public void onClick(View v) { 530 if (mMenuListener == null) { 531 // Nothing to do 532 return; 533 } 534 v.getLocationOnScreen(mIconLocation); 535 mParent.getLocationOnScreen(mParentLocation); 536 final int centerX = mHorizSpaceForIcon / 2; 537 final int centerY = v.getHeight() / 2; 538 final int x = mIconLocation[0] - mParentLocation[0] + centerX; 539 final int y = mIconLocation[1] - mParentLocation[1] + centerY; 540 if (mMenuItemsByView.containsKey(v)) { 541 mMenuListener.onMenuClicked(mParent, x, y, mMenuItemsByView.get(v)); 542 } 543 } 544 isMenuLocationChange()545 private boolean isMenuLocationChange() { 546 boolean onLeft = mTranslation > mIconPadding; 547 boolean onRight = mTranslation < -mIconPadding; 548 if ((isMenuOnLeft() && onRight) || (!isMenuOnLeft() && onLeft)) { 549 return true; 550 } 551 return false; 552 } 553 554 private void setMenuLocation() { 555 boolean showOnLeft = mTranslation > 0; 556 if ((mIconsPlaced && showOnLeft == isMenuOnLeft()) || isSnapping() || mMenuContainer == null 557 || !mMenuContainer.isAttachedToWindow()) { 558 // Do nothing 559 return; 560 } 561 boolean wasOnLeft = mOnLeft; 562 mOnLeft = showOnLeft; 563 if (wasOnLeft != showOnLeft) { 564 populateMenuViews(); 565 } 566 final int count = mMenuContainer.getChildCount(); 567 for (int i = 0; i < count; i++) { 568 final View v = mMenuContainer.getChildAt(i); 569 final float left = i * mHorizSpaceForIcon; 570 final float right = mParent.getWidth() - (mHorizSpaceForIcon * (i + 1)); 571 v.setX(showOnLeft ? left : right); 572 } 573 mIconsPlaced = true; 574 } 575 576 @VisibleForTesting setMenuAlpha(float alpha)577 protected void setMenuAlpha(float alpha) { 578 mAlpha = alpha; 579 if (mMenuContainer == null) { 580 return; 581 } 582 if (alpha == 0) { 583 mMenuFadedIn = false; // Can fade in again once it's gone. 584 mMenuContainer.setVisibility(View.INVISIBLE); 585 } else { 586 mMenuContainer.setVisibility(View.VISIBLE); 587 } 588 final int count = mMenuContainer.getChildCount(); 589 for (int i = 0; i < count; i++) { 590 mMenuContainer.getChildAt(i).setAlpha(mAlpha); 591 } 592 } 593 594 /** 595 * Returns the horizontal space in pixels required to display the menu. 596 */ 597 @VisibleForTesting getSpaceForMenu()598 protected int getSpaceForMenu() { 599 return mHorizSpaceForIcon * mMenuContainer.getChildCount(); 600 } 601 602 private final class CheckForDrag implements Runnable { 603 @Override run()604 public void run() { 605 final float absTransX = Math.abs(mTranslation); 606 final float bounceBackToMenuWidth = getSpaceForMenu(); 607 final float notiThreshold = mParent.getWidth() * 0.4f; 608 if ((!isMenuVisible() || isMenuLocationChange()) 609 && absTransX >= bounceBackToMenuWidth * 0.4 610 && absTransX < notiThreshold) { 611 fadeInMenu(notiThreshold); 612 } 613 } 614 } 615 fadeInMenu(final float notiThreshold)616 private void fadeInMenu(final float notiThreshold) { 617 if (mDismissing || mAnimating) { 618 return; 619 } 620 if (isMenuLocationChange()) { 621 setMenuAlpha(0f); 622 } 623 final float transX = mTranslation; 624 final boolean fromLeft = mTranslation > 0; 625 setMenuLocation(); 626 mFadeAnimator = ValueAnimator.ofFloat(mAlpha, 1); 627 mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 628 @Override 629 public void onAnimationUpdate(ValueAnimator animation) { 630 final float absTrans = Math.abs(transX); 631 632 boolean pastMenu = (fromLeft && transX <= notiThreshold) 633 || (!fromLeft && absTrans <= notiThreshold); 634 if (pastMenu && !mMenuFadedIn) { 635 setMenuAlpha((float) animation.getAnimatedValue()); 636 } 637 } 638 }); 639 mFadeAnimator.addListener(new AnimatorListenerAdapter() { 640 @Override 641 public void onAnimationStart(Animator animation) { 642 mAnimating = true; 643 } 644 645 @Override 646 public void onAnimationCancel(Animator animation) { 647 // TODO should animate back to 0f from current alpha 648 setMenuAlpha(0f); 649 } 650 651 @Override 652 public void onAnimationEnd(Animator animation) { 653 mAnimating = false; 654 mMenuFadedIn = mAlpha == 1; 655 } 656 }); 657 mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN); 658 mFadeAnimator.setDuration(ICON_ALPHA_ANIM_DURATION); 659 mFadeAnimator.start(); 660 } 661 662 @Override setMenuItems(ArrayList<MenuItem> items)663 public void setMenuItems(ArrayList<MenuItem> items) { 664 // Do nothing we use our own for now. 665 // TODO -- handle / allow custom menu items! 666 } 667 668 @Override shouldShowGutsOnSnapOpen()669 public boolean shouldShowGutsOnSnapOpen() { 670 return false; 671 } 672 673 @Override menuItemToExposeOnSnap()674 public MenuItem menuItemToExposeOnSnap() { 675 return null; 676 } 677 678 @Override getRevealAnimationOrigin()679 public Point getRevealAnimationOrigin() { 680 View v = mInfoItem.getMenuView(); 681 int menuX = v.getLeft() + v.getPaddingLeft() + (v.getWidth() / 2); 682 int menuY = v.getTop() + v.getPaddingTop() + (v.getHeight() / 2); 683 if (isMenuOnLeft()) { 684 return new Point(menuX, menuY); 685 } else { 686 menuX = mParent.getRight() - menuX; 687 return new Point(menuX, menuY); 688 } 689 } 690 createSnoozeItem(Context context)691 static MenuItem createSnoozeItem(Context context) { 692 Resources res = context.getResources(); 693 NotificationSnooze content = (NotificationSnooze) LayoutInflater.from(context) 694 .inflate(R.layout.notification_snooze, null, false); 695 String snoozeDescription = res.getString(R.string.notification_menu_snooze_description); 696 MenuItem snooze = new NotificationMenuItem(context, snoozeDescription, content, 697 R.drawable.ic_snooze); 698 return snooze; 699 } 700 createDemoteItem(Context context)701 static MenuItem createDemoteItem(Context context) { 702 PromotedPermissionGutsContent demoteContent = 703 (PromotedPermissionGutsContent) LayoutInflater.from(context).inflate( 704 R.layout.promoted_permission_guts, null, false); 705 View demoteButton = LayoutInflater.from(context) 706 .inflate(R.layout.promoted_menu_item, null, false); 707 MenuItem info = new NotificationMenuItem(context, null, demoteContent, 708 demoteButton); 709 710 return info; 711 } 712 createConversationItem(Context context)713 static NotificationMenuItem createConversationItem(Context context) { 714 Resources res = context.getResources(); 715 String infoDescription = res.getString(R.string.notification_menu_gear_description); 716 NotificationConversationInfo infoContent = 717 (NotificationConversationInfo) LayoutInflater.from(context).inflate( 718 R.layout.notification_conversation_info, null, false); 719 return new NotificationMenuItem(context, infoDescription, infoContent, 720 NotificationMenuItem.OMIT_FROM_SWIPE_MENU); 721 } 722 createPromotedItem(Context context)723 static NotificationMenuItem createPromotedItem(Context context) { 724 Resources res = context.getResources(); 725 String infoDescription = res.getString(R.string.notification_menu_gear_description); 726 PromotedNotificationInfo infoContent = 727 (PromotedNotificationInfo) LayoutInflater.from(context).inflate( 728 R.layout.promoted_notification_info, null, false); 729 return new NotificationMenuItem(context, infoDescription, infoContent, 730 NotificationMenuItem.OMIT_FROM_SWIPE_MENU); 731 } 732 createPartialConversationItem(Context context)733 static NotificationMenuItem createPartialConversationItem(Context context) { 734 Resources res = context.getResources(); 735 String infoDescription = res.getString(R.string.notification_menu_gear_description); 736 PartialConversationInfo infoContent = 737 (PartialConversationInfo) LayoutInflater.from(context).inflate( 738 R.layout.partial_conversation_info, null, false); 739 return new NotificationMenuItem(context, infoDescription, infoContent, 740 NotificationMenuItem.OMIT_FROM_SWIPE_MENU); 741 } 742 createInfoItem(Context context)743 static NotificationMenuItem createInfoItem(Context context) { 744 Resources res = context.getResources(); 745 String infoDescription = res.getString(R.string.notification_menu_gear_description); 746 int layoutId = notificationsRedesignTemplates() 747 ? R.layout.notification_2025_info 748 : R.layout.notification_info; 749 NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate( 750 layoutId, null, false); 751 return new NotificationMenuItem(context, infoDescription, infoContent, 752 NotificationMenuItem.OMIT_FROM_SWIPE_MENU); 753 } 754 createFeedbackItem(Context context)755 static MenuItem createFeedbackItem(Context context) { 756 FeedbackInfo feedbackContent = (FeedbackInfo) LayoutInflater.from(context).inflate( 757 R.layout.feedback_info, null, false); 758 MenuItem info = new NotificationMenuItem(context, null, feedbackContent, 759 NotificationMenuItem.OMIT_FROM_SWIPE_MENU); 760 return info; 761 } 762 addMenuView(MenuItem item, ViewGroup parent)763 private void addMenuView(MenuItem item, ViewGroup parent) { 764 View menuView = item.getMenuView(); 765 if (menuView != null) { 766 menuView.setAlpha(mAlpha); 767 parent.addView(menuView); 768 menuView.setOnClickListener(this); 769 if (item instanceof ImageView) { 770 FrameLayout.LayoutParams lp = (LayoutParams) menuView.getLayoutParams(); 771 lp.width = mHorizSpaceForIcon; 772 lp.height = mHorizSpaceForIcon; 773 menuView.setLayoutParams(lp); 774 } 775 } 776 mMenuItemsByView.put(menuView, item); 777 } 778 779 @VisibleForTesting 780 /** 781 * Determine the minimum offset below which the menu should snap back closed. 782 */ getSnapBackThreshold()783 protected float getSnapBackThreshold() { 784 return getSpaceForMenu() - getMaximumSwipeDistance(); 785 } 786 787 /** 788 * Determine the maximum offset above which the parent notification should be dismissed. 789 * @return 790 */ 791 @VisibleForTesting getDismissThreshold()792 protected float getDismissThreshold() { 793 return getParent().getWidth() * SWIPED_FAR_ENOUGH_SIZE_FRACTION; 794 } 795 796 @Override isWithinSnapMenuThreshold()797 public boolean isWithinSnapMenuThreshold() { 798 if (getSpaceForMenu() == 0) { 799 // don't snap open if there are no items 800 return false; 801 } 802 float translation = getTranslation(); 803 float snapBackThreshold = getSnapBackThreshold(); 804 float targetRight = getDismissThreshold(); 805 return isMenuOnLeft() 806 ? translation > snapBackThreshold && translation < targetRight 807 : translation < -snapBackThreshold && translation > -targetRight; 808 } 809 810 @Override isSwipedEnoughToShowMenu()811 public boolean isSwipedEnoughToShowMenu() { 812 final float minimumSwipeDistance = getMinimumSwipeDistance(); 813 final float translation = getTranslation(); 814 return isMenuVisible() && (isMenuOnLeft() ? 815 translation > minimumSwipeDistance 816 : translation < -minimumSwipeDistance); 817 } 818 819 @Override getMenuSnapTarget()820 public int getMenuSnapTarget() { 821 return isMenuOnLeft() ? getSpaceForMenu() : -getSpaceForMenu(); 822 } 823 824 @Override shouldSnapBack()825 public boolean shouldSnapBack() { 826 float translation = getTranslation(); 827 float targetLeft = getSnapBackThreshold(); 828 return isMenuOnLeft() ? translation < targetLeft : translation > -targetLeft; 829 } 830 831 @Override isSnappedAndOnSameSide()832 public boolean isSnappedAndOnSameSide() { 833 return isMenuSnapped() && isMenuVisible() 834 && isMenuSnappedOnLeft() == isMenuOnLeft(); 835 } 836 837 @Override canBeDismissed()838 public boolean canBeDismissed() { 839 return getParent().canViewBeDismissed(); 840 } 841 842 public static class NotificationMenuItem implements MenuItem { 843 844 // Constant signaling that this MenuItem should not appear in slow swipe. 845 public static final int OMIT_FROM_SWIPE_MENU = -1; 846 847 View mMenuView; 848 GutsContent mGutsContent; 849 String mContentDescription; 850 851 /** 852 * Add a new 'guts' panel. If iconResId < 0 it will not appear in the slow swipe menu 853 * but can still be exposed via other affordances. 854 */ NotificationMenuItem(Context context, String contentDescription, GutsContent content, int iconResId)855 public NotificationMenuItem(Context context, String contentDescription, GutsContent content, 856 int iconResId) { 857 Resources res = context.getResources(); 858 int padding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding); 859 int tint = res.getColor(R.color.notification_gear_color); 860 if (iconResId >= 0) { 861 AlphaOptimizedImageView iv = new AlphaOptimizedImageView(context); 862 iv.setPadding(padding, padding, padding, padding); 863 Drawable icon = context.getResources().getDrawable(iconResId); 864 iv.setImageDrawable(icon); 865 iv.setColorFilter(tint); 866 iv.setAlpha(1f); 867 mMenuView = iv; 868 } 869 mContentDescription = contentDescription; 870 mGutsContent = content; 871 } 872 873 874 /** 875 * Add a new 'guts' panel with custom view. 876 */ NotificationMenuItem(Context context, String contentDescription, GutsContent content, View itemView)877 public NotificationMenuItem(Context context, String contentDescription, GutsContent content, 878 View itemView) { 879 mMenuView = itemView; 880 mContentDescription = contentDescription; 881 mGutsContent = content; 882 } 883 884 @Override 885 @Nullable getMenuView()886 public View getMenuView() { 887 return mMenuView; 888 } 889 890 @Override getGutsView()891 public View getGutsView() { 892 return mGutsContent.getContentView(); 893 } 894 895 @Override getContentDescription()896 public String getContentDescription() { 897 return mContentDescription; 898 } 899 } 900 } 901