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