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