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