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.isDark() 350 && !mParent.showingAmbientPulsing() 351 && (mCheckForDrag == null || !mHandler.hasCallbacks(mCheckForDrag))) { 352 // Only show the menu if we're not a heads up view and guts aren't exposed. 353 mCheckForDrag = new CheckForDrag(); 354 mHandler.postDelayed(mCheckForDrag, SHOW_MENU_DELAY); 355 } 356 } 357 358 @VisibleForTesting beginDrag()359 protected void beginDrag() { 360 mSnapping = false; 361 if (mFadeAnimator != null) { 362 mFadeAnimator.cancel(); 363 } 364 mHandler.removeCallbacks(mCheckForDrag); 365 mCheckForDrag = null; 366 mIsUserTouching = true; 367 } 368 369 @Override onTouchStart()370 public void onTouchStart() { 371 beginDrag(); 372 } 373 374 @Override onSnapOpen()375 public void onSnapOpen() { 376 mMenuSnapped = true; 377 mMenuSnappedOnLeft = isMenuOnLeft(); 378 if (mAlpha == 0f && mParent != null) { 379 fadeInMenu(mParent.getWidth()); 380 } 381 if (mMenuListener != null) { 382 mMenuListener.onMenuShown(getParent()); 383 } 384 } 385 386 @Override onSnapClosed()387 public void onSnapClosed() { 388 cancelDrag(); 389 mMenuSnapped = false; 390 mSnapping = true; 391 } 392 393 @Override onDismiss()394 public void onDismiss() { 395 cancelDrag(); 396 mMenuSnapped = false; 397 mDismissing = true; 398 } 399 400 @VisibleForTesting cancelDrag()401 protected void cancelDrag() { 402 if (mFadeAnimator != null) { 403 mFadeAnimator.cancel(); 404 } 405 mHandler.removeCallbacks(mCheckForDrag); 406 } 407 408 @VisibleForTesting getMinimumSwipeDistance()409 protected float getMinimumSwipeDistance() { 410 final float multiplier = getParent().canViewBeDismissed() 411 ? SWIPED_FAR_ENOUGH_MENU_FRACTION 412 : SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION; 413 return mHorizSpaceForIcon * multiplier; 414 } 415 416 @VisibleForTesting getMaximumSwipeDistance()417 protected float getMaximumSwipeDistance() { 418 return mHorizSpaceForIcon * SWIPED_BACK_ENOUGH_TO_COVER_FRACTION; 419 } 420 421 /** 422 * Returns whether the gesture is towards the menu location or not. 423 */ 424 @Override isTowardsMenu(float movement)425 public boolean isTowardsMenu(float movement) { 426 return isMenuVisible() 427 && ((isMenuOnLeft() && movement <= 0) 428 || (!isMenuOnLeft() && movement >= 0)); 429 } 430 431 @Override setAppName(String appName)432 public void setAppName(String appName) { 433 if (appName == null) { 434 return; 435 } 436 setAppName(appName, mLeftMenuItems); 437 setAppName(appName, mRightMenuItems); 438 } 439 setAppName(String appName, ArrayList<MenuItem> menuItems)440 private void setAppName(String appName, 441 ArrayList<MenuItem> menuItems) { 442 Resources res = mContext.getResources(); 443 final int count = menuItems.size(); 444 for (int i = 0; i < count; i++) { 445 MenuItem item = menuItems.get(i); 446 String description = String.format( 447 res.getString(R.string.notification_menu_accessibility), 448 appName, item.getContentDescription()); 449 View menuView = item.getMenuView(); 450 if (menuView != null) { 451 menuView.setContentDescription(description); 452 } 453 } 454 } 455 456 @Override onParentHeightUpdate()457 public void onParentHeightUpdate() { 458 if (mParent == null 459 || (mLeftMenuItems.isEmpty() && mRightMenuItems.isEmpty()) 460 || mMenuContainer == null) { 461 return; 462 } 463 int parentHeight = mParent.getActualHeight(); 464 float translationY; 465 if (parentHeight < mVertSpaceForIcons) { 466 translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2); 467 } else { 468 translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2; 469 } 470 mMenuContainer.setTranslationY(translationY); 471 } 472 473 @Override onParentTranslationUpdate(float translation)474 public void onParentTranslationUpdate(float translation) { 475 mTranslation = translation; 476 if (mAnimating || !mMenuFadedIn) { 477 // Don't adjust when animating, or if the menu hasn't been shown yet. 478 return; 479 } 480 final float fadeThreshold = mParent.getWidth() * 0.3f; 481 final float absTrans = Math.abs(translation); 482 float desiredAlpha = 0; 483 if (absTrans == 0) { 484 desiredAlpha = 0; 485 } else if (absTrans <= fadeThreshold) { 486 desiredAlpha = 1; 487 } else { 488 desiredAlpha = 1 - ((absTrans - fadeThreshold) / (mParent.getWidth() - fadeThreshold)); 489 } 490 setMenuAlpha(desiredAlpha); 491 } 492 493 @Override onClick(View v)494 public void onClick(View v) { 495 if (mMenuListener == null) { 496 // Nothing to do 497 return; 498 } 499 v.getLocationOnScreen(mIconLocation); 500 mParent.getLocationOnScreen(mParentLocation); 501 final int centerX = mHorizSpaceForIcon / 2; 502 final int centerY = v.getHeight() / 2; 503 final int x = mIconLocation[0] - mParentLocation[0] + centerX; 504 final int y = mIconLocation[1] - mParentLocation[1] + centerY; 505 if (mMenuItemsByView.containsKey(v)) { 506 mMenuListener.onMenuClicked(mParent, x, y, mMenuItemsByView.get(v)); 507 } 508 } 509 isMenuLocationChange()510 private boolean isMenuLocationChange() { 511 boolean onLeft = mTranslation > mIconPadding; 512 boolean onRight = mTranslation < -mIconPadding; 513 if ((isMenuOnLeft() && onRight) || (!isMenuOnLeft() && onLeft)) { 514 return true; 515 } 516 return false; 517 } 518 519 private void setMenuLocation() { 520 boolean showOnLeft = mTranslation > 0; 521 if ((mIconsPlaced && showOnLeft == isMenuOnLeft()) || isSnapping() || mMenuContainer == null 522 || !mMenuContainer.isAttachedToWindow()) { 523 // Do nothing 524 return; 525 } 526 boolean wasOnLeft = mOnLeft; 527 mOnLeft = showOnLeft; 528 if (wasOnLeft != showOnLeft) { 529 populateMenuViews(); 530 } 531 final int count = mMenuContainer.getChildCount(); 532 for (int i = 0; i < count; i++) { 533 final View v = mMenuContainer.getChildAt(i); 534 final float left = i * mHorizSpaceForIcon; 535 final float right = mParent.getWidth() - (mHorizSpaceForIcon * (i + 1)); 536 v.setX(showOnLeft ? left : right); 537 } 538 mIconsPlaced = true; 539 } 540 541 @VisibleForTesting setMenuAlpha(float alpha)542 protected void setMenuAlpha(float alpha) { 543 mAlpha = alpha; 544 if (mMenuContainer == null) { 545 return; 546 } 547 if (alpha == 0) { 548 mMenuFadedIn = false; // Can fade in again once it's gone. 549 mMenuContainer.setVisibility(View.INVISIBLE); 550 } else { 551 mMenuContainer.setVisibility(View.VISIBLE); 552 } 553 final int count = mMenuContainer.getChildCount(); 554 for (int i = 0; i < count; i++) { 555 mMenuContainer.getChildAt(i).setAlpha(mAlpha); 556 } 557 } 558 559 /** 560 * Returns the horizontal space in pixels required to display the menu. 561 */ 562 @VisibleForTesting getSpaceForMenu()563 protected int getSpaceForMenu() { 564 return mHorizSpaceForIcon * mMenuContainer.getChildCount(); 565 } 566 567 private final class CheckForDrag implements Runnable { 568 @Override run()569 public void run() { 570 final float absTransX = Math.abs(mTranslation); 571 final float bounceBackToMenuWidth = getSpaceForMenu(); 572 final float notiThreshold = mParent.getWidth() * 0.4f; 573 if ((!isMenuVisible() || isMenuLocationChange()) 574 && absTransX >= bounceBackToMenuWidth * 0.4 575 && absTransX < notiThreshold) { 576 fadeInMenu(notiThreshold); 577 } 578 } 579 } 580 fadeInMenu(final float notiThreshold)581 private void fadeInMenu(final float notiThreshold) { 582 if (mDismissing || mAnimating) { 583 return; 584 } 585 if (isMenuLocationChange()) { 586 setMenuAlpha(0f); 587 } 588 final float transX = mTranslation; 589 final boolean fromLeft = mTranslation > 0; 590 setMenuLocation(); 591 mFadeAnimator = ValueAnimator.ofFloat(mAlpha, 1); 592 mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 593 @Override 594 public void onAnimationUpdate(ValueAnimator animation) { 595 final float absTrans = Math.abs(transX); 596 597 boolean pastMenu = (fromLeft && transX <= notiThreshold) 598 || (!fromLeft && absTrans <= notiThreshold); 599 if (pastMenu && !mMenuFadedIn) { 600 setMenuAlpha((float) animation.getAnimatedValue()); 601 } 602 } 603 }); 604 mFadeAnimator.addListener(new AnimatorListenerAdapter() { 605 @Override 606 public void onAnimationStart(Animator animation) { 607 mAnimating = true; 608 } 609 610 @Override 611 public void onAnimationCancel(Animator animation) { 612 // TODO should animate back to 0f from current alpha 613 setMenuAlpha(0f); 614 } 615 616 @Override 617 public void onAnimationEnd(Animator animation) { 618 mAnimating = false; 619 mMenuFadedIn = mAlpha == 1; 620 } 621 }); 622 mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN); 623 mFadeAnimator.setDuration(ICON_ALPHA_ANIM_DURATION); 624 mFadeAnimator.start(); 625 } 626 627 @Override setMenuItems(ArrayList<MenuItem> items)628 public void setMenuItems(ArrayList<MenuItem> items) { 629 // Do nothing we use our own for now. 630 // TODO -- handle / allow custom menu items! 631 } 632 633 @Override shouldShowGutsOnSnapOpen()634 public boolean shouldShowGutsOnSnapOpen() { 635 return mIsUsingBidirectionalSwipe; 636 } 637 638 @Override menuItemToExposeOnSnap()639 public MenuItem menuItemToExposeOnSnap() { 640 return mIsUsingBidirectionalSwipe ? mInfoItem : null; 641 } 642 643 @Override getRevealAnimationOrigin()644 public Point getRevealAnimationOrigin() { 645 View v = mInfoItem.getMenuView(); 646 int menuX = v.getLeft() + v.getPaddingLeft() + (v.getWidth() / 2); 647 int menuY = v.getTop() + v.getPaddingTop() + (v.getHeight() / 2); 648 if (isMenuOnLeft()) { 649 return new Point(menuX, menuY); 650 } else { 651 menuX = mParent.getRight() - menuX; 652 return new Point(menuX, menuY); 653 } 654 } 655 createSnoozeItem(Context context)656 static MenuItem createSnoozeItem(Context context) { 657 Resources res = context.getResources(); 658 NotificationSnooze content = (NotificationSnooze) LayoutInflater.from(context) 659 .inflate(R.layout.notification_snooze, null, false); 660 String snoozeDescription = res.getString(R.string.notification_menu_snooze_description); 661 MenuItem snooze = new NotificationMenuItem(context, snoozeDescription, content, 662 R.drawable.ic_snooze); 663 return snooze; 664 } 665 createInfoItem(Context context)666 static NotificationMenuItem createInfoItem(Context context) { 667 Resources res = context.getResources(); 668 String infoDescription = res.getString(R.string.notification_menu_gear_description); 669 NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate( 670 R.layout.notification_info, null, false); 671 return new NotificationMenuItem(context, infoDescription, infoContent, 672 R.drawable.ic_settings); 673 } 674 createInfoItem(Context context, boolean isCurrentlySilent)675 static NotificationMenuItem createInfoItem(Context context, boolean isCurrentlySilent) { 676 Resources res = context.getResources(); 677 String infoDescription = res.getString(R.string.notification_menu_gear_description); 678 NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate( 679 R.layout.notification_info, null, false); 680 int iconResId = isCurrentlySilent 681 ? R.drawable.ic_notifications_silence 682 : R.drawable.ic_notifications_alert; 683 return new NotificationMenuItem(context, infoDescription, infoContent, iconResId); 684 } 685 createAppOpsItem(Context context)686 static MenuItem createAppOpsItem(Context context) { 687 AppOpsInfo appOpsContent = (AppOpsInfo) LayoutInflater.from(context).inflate( 688 R.layout.app_ops_info, null, false); 689 MenuItem info = new NotificationMenuItem(context, null, appOpsContent, 690 -1 /*don't show in slow swipe menu */); 691 return info; 692 } 693 addMenuView(MenuItem item, ViewGroup parent)694 private void addMenuView(MenuItem item, ViewGroup parent) { 695 View menuView = item.getMenuView(); 696 if (menuView != null) { 697 menuView.setAlpha(mAlpha); 698 parent.addView(menuView); 699 menuView.setOnClickListener(this); 700 FrameLayout.LayoutParams lp = (LayoutParams) menuView.getLayoutParams(); 701 lp.width = mHorizSpaceForIcon; 702 lp.height = mHorizSpaceForIcon; 703 menuView.setLayoutParams(lp); 704 } 705 mMenuItemsByView.put(menuView, item); 706 } 707 708 @VisibleForTesting 709 /** 710 * Determine the minimum offset below which the menu should snap back closed. 711 */ getSnapBackThreshold()712 protected float getSnapBackThreshold() { 713 return getSpaceForMenu() - getMaximumSwipeDistance(); 714 } 715 716 /** 717 * Determine the maximum offset above which the parent notification should be dismissed. 718 * @return 719 */ 720 @VisibleForTesting getDismissThreshold()721 protected float getDismissThreshold() { 722 return getParent().getWidth() * SWIPED_FAR_ENOUGH_SIZE_FRACTION; 723 } 724 725 @Override isWithinSnapMenuThreshold()726 public boolean isWithinSnapMenuThreshold() { 727 float translation = getTranslation(); 728 float snapBackThreshold = getSnapBackThreshold(); 729 float targetRight = getDismissThreshold(); 730 return isMenuOnLeft() 731 ? translation > snapBackThreshold && translation < targetRight 732 : translation < -snapBackThreshold && translation > -targetRight; 733 } 734 735 @Override isSwipedEnoughToShowMenu()736 public boolean isSwipedEnoughToShowMenu() { 737 final float minimumSwipeDistance = getMinimumSwipeDistance(); 738 final float translation = getTranslation(); 739 return isMenuVisible() && (isMenuOnLeft() ? 740 translation > minimumSwipeDistance 741 : translation < -minimumSwipeDistance); 742 } 743 744 @Override getMenuSnapTarget()745 public int getMenuSnapTarget() { 746 return isMenuOnLeft() ? getSpaceForMenu() : -getSpaceForMenu(); 747 } 748 749 @Override shouldSnapBack()750 public boolean shouldSnapBack() { 751 float translation = getTranslation(); 752 float targetLeft = getSnapBackThreshold(); 753 return isMenuOnLeft() ? translation < targetLeft : translation > -targetLeft; 754 } 755 756 @Override isSnappedAndOnSameSide()757 public boolean isSnappedAndOnSameSide() { 758 return isMenuSnapped() && isMenuVisible() 759 && isMenuSnappedOnLeft() == isMenuOnLeft(); 760 } 761 762 @Override canBeDismissed()763 public boolean canBeDismissed() { 764 return getParent().canViewBeDismissed(); 765 } 766 767 @Override setDismissRtl(boolean dismissRtl)768 public void setDismissRtl(boolean dismissRtl) { 769 mDismissRtl = dismissRtl; 770 if (mMenuContainer != null) { 771 createMenuViews(true, mIsForeground); 772 } 773 } 774 775 public static class NotificationMenuItem implements MenuItem { 776 View mMenuView; 777 GutsContent mGutsContent; 778 String mContentDescription; 779 780 /** 781 * Add a new 'guts' panel. If iconResId < 0 it will not appear in the slow swipe menu 782 * but can still be exposed via other affordances. 783 */ NotificationMenuItem(Context context, String contentDescription, GutsContent content, int iconResId)784 public NotificationMenuItem(Context context, String contentDescription, GutsContent content, 785 int iconResId) { 786 Resources res = context.getResources(); 787 int padding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding); 788 int tint = res.getColor(R.color.notification_gear_color); 789 if (iconResId >= 0) { 790 AlphaOptimizedImageView iv = new AlphaOptimizedImageView(context); 791 iv.setPadding(padding, padding, padding, padding); 792 Drawable icon = context.getResources().getDrawable(iconResId); 793 iv.setImageDrawable(icon); 794 iv.setColorFilter(tint); 795 iv.setAlpha(1f); 796 mMenuView = iv; 797 } 798 mContentDescription = contentDescription; 799 mGutsContent = content; 800 } 801 802 @Override 803 @Nullable getMenuView()804 public View getMenuView() { 805 return mMenuView; 806 } 807 808 @Override getGutsView()809 public View getGutsView() { 810 return mGutsContent.getContentView(); 811 } 812 813 @Override getContentDescription()814 public String getContentDescription() { 815 return mContentDescription; 816 } 817 } 818 } 819