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