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; 18 19 import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION; 20 21 import java.util.ArrayList; 22 23 import com.android.systemui.Interpolators; 24 import com.android.systemui.R; 25 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; 26 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; 27 import com.android.systemui.statusbar.NotificationGuts.GutsContent; 28 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; 29 30 import android.animation.Animator; 31 import android.animation.AnimatorListenerAdapter; 32 import android.animation.ValueAnimator; 33 import android.annotation.Nullable; 34 import android.app.Notification; 35 import android.content.Context; 36 import android.content.res.Resources; 37 import android.graphics.drawable.Drawable; 38 import android.os.Handler; 39 import android.os.Looper; 40 import android.util.Log; 41 import android.service.notification.StatusBarNotification; 42 import android.view.LayoutInflater; 43 import android.view.MotionEvent; 44 import android.view.View; 45 import android.view.ViewGroup; 46 import android.widget.FrameLayout; 47 import android.widget.FrameLayout.LayoutParams; 48 49 public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnClickListener, 50 ExpandableNotificationRow.LayoutListener { 51 52 private static final boolean DEBUG = false; 53 private static final String TAG = "swipe"; 54 55 private static final int ICON_ALPHA_ANIM_DURATION = 200; 56 private static final long SHOW_MENU_DELAY = 60; 57 private static final long SWIPE_MENU_TIMING = 200; 58 59 // Notification must be swiped at least this fraction of a single menu item to show menu 60 private static final float SWIPED_FAR_ENOUGH_MENU_FRACTION = 0.25f; 61 private static final float SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION = 0.15f; 62 63 // When the menu is displayed, the notification must be swiped within this fraction of a single 64 // menu item to snap back to menu (else it will cover the menu or it'll be dismissed) 65 private static final float SWIPED_BACK_ENOUGH_TO_COVER_FRACTION = 0.2f; 66 67 private ExpandableNotificationRow mParent; 68 69 private Context mContext; 70 private FrameLayout mMenuContainer; 71 private MenuItem mInfoItem; 72 private MenuItem mAppOpsItem; 73 private MenuItem mSnoozeItem; 74 private ArrayList<MenuItem> mMenuItems; 75 private OnMenuEventListener mMenuListener; 76 77 private ValueAnimator mFadeAnimator; 78 private boolean mAnimating; 79 private boolean mMenuFadedIn; 80 81 private boolean mOnLeft; 82 private boolean mIconsPlaced; 83 84 private boolean mDismissing; 85 private boolean mSnapping; 86 private float mTranslation; 87 88 private int[] mIconLocation = new int[2]; 89 private int[] mParentLocation = new int[2]; 90 91 private float mHorizSpaceForIcon = -1; 92 private int mVertSpaceForIcons = -1; 93 private int mIconPadding = -1; 94 private int mSidePadding; 95 96 private float mAlpha = 0f; 97 private float mPrevX; 98 99 private CheckForDrag mCheckForDrag; 100 private Handler mHandler; 101 102 private boolean mMenuSnappedTo; 103 private boolean mMenuSnappedOnLeft; 104 private boolean mShouldShowMenu; 105 106 private NotificationSwipeActionHelper mSwipeHelper; 107 private boolean mIsUserTouching; 108 NotificationMenuRow(Context context)109 public NotificationMenuRow(Context context) { 110 mContext = context; 111 mShouldShowMenu = context.getResources().getBoolean(R.bool.config_showNotificationGear); 112 mHandler = new Handler(Looper.getMainLooper()); 113 mMenuItems = new ArrayList<>(); 114 } 115 116 @Override getMenuItems(Context context)117 public ArrayList<MenuItem> getMenuItems(Context context) { 118 return mMenuItems; 119 } 120 121 @Override getLongpressMenuItem(Context context)122 public MenuItem getLongpressMenuItem(Context context) { 123 return mInfoItem; 124 } 125 126 @Override getAppOpsMenuItem(Context context)127 public MenuItem getAppOpsMenuItem(Context context) { 128 return mAppOpsItem; 129 } 130 131 @Override getSnoozeMenuItem(Context context)132 public MenuItem getSnoozeMenuItem(Context context) { 133 return mSnoozeItem; 134 } 135 136 @Override setSwipeActionHelper(NotificationSwipeActionHelper helper)137 public void setSwipeActionHelper(NotificationSwipeActionHelper helper) { 138 mSwipeHelper = helper; 139 } 140 141 @Override setMenuClickListener(OnMenuEventListener listener)142 public void setMenuClickListener(OnMenuEventListener listener) { 143 mMenuListener = listener; 144 } 145 146 @Override createMenu(ViewGroup parent, StatusBarNotification sbn)147 public void createMenu(ViewGroup parent, StatusBarNotification sbn) { 148 mParent = (ExpandableNotificationRow) parent; 149 createMenuViews(true /* resetState */); 150 } 151 152 @Override isMenuVisible()153 public boolean isMenuVisible() { 154 return mAlpha > 0; 155 } 156 157 @Override getMenuView()158 public View getMenuView() { 159 return mMenuContainer; 160 } 161 162 @Override resetMenu()163 public void resetMenu() { 164 resetState(true); 165 } 166 167 @Override onNotificationUpdated(StatusBarNotification sbn)168 public void onNotificationUpdated(StatusBarNotification sbn) { 169 if (mMenuContainer == null) { 170 // Menu hasn't been created yet, no need to do anything. 171 return; 172 } 173 createMenuViews(!isMenuVisible() /* resetState */); 174 } 175 176 @Override onConfigurationChanged()177 public void onConfigurationChanged() { 178 mParent.setLayoutListener(this); 179 } 180 181 @Override onLayout()182 public void onLayout() { 183 mIconsPlaced = false; // Force icons to be re-placed 184 setMenuLocation(); 185 mParent.removeListener(); 186 } 187 createMenuViews(boolean resetState)188 private void createMenuViews(boolean resetState) { 189 final Resources res = mContext.getResources(); 190 mHorizSpaceForIcon = res.getDimensionPixelSize(R.dimen.notification_menu_icon_size); 191 mVertSpaceForIcons = res.getDimensionPixelSize(R.dimen.notification_min_height); 192 mMenuItems.clear(); 193 // Construct the menu items based on the notification 194 if (mParent != null && mParent.getStatusBarNotification() != null) { 195 int flags = mParent.getStatusBarNotification().getNotification().flags; 196 boolean isForeground = (flags & Notification.FLAG_FOREGROUND_SERVICE) != 0; 197 if (!isForeground) { 198 // Only show snooze for non-foreground notifications 199 mSnoozeItem = createSnoozeItem(mContext); 200 mMenuItems.add(mSnoozeItem); 201 } 202 } 203 mInfoItem = createInfoItem(mContext); 204 mMenuItems.add(mInfoItem); 205 206 mAppOpsItem = createAppOpsItem(mContext); 207 mMenuItems.add(mAppOpsItem); 208 209 // Construct the menu views 210 if (mMenuContainer != null) { 211 mMenuContainer.removeAllViews(); 212 } else { 213 mMenuContainer = new FrameLayout(mContext); 214 } 215 for (int i = 0; i < mMenuItems.size(); i++) { 216 addMenuView(mMenuItems.get(i), mMenuContainer); 217 } 218 if (resetState) { 219 resetState(false /* notify */); 220 } else { 221 mIconsPlaced = false; 222 setMenuLocation(); 223 if (!mIsUserTouching) { 224 // If the # of items showing changed we need to update the snap position 225 showMenu(mParent, mOnLeft ? getSpaceForMenu() : -getSpaceForMenu(), 226 0 /* velocity */); 227 } 228 } 229 } 230 resetState(boolean notify)231 private void resetState(boolean notify) { 232 setMenuAlpha(0f); 233 mIconsPlaced = false; 234 mMenuFadedIn = false; 235 mAnimating = false; 236 mSnapping = false; 237 mDismissing = false; 238 mMenuSnappedTo = false; 239 setMenuLocation(); 240 if (mMenuListener != null && notify) { 241 mMenuListener.onMenuReset(mParent); 242 } 243 } 244 245 @Override onTouchEvent(View view, MotionEvent ev, float velocity)246 public boolean onTouchEvent(View view, MotionEvent ev, float velocity) { 247 final int action = ev.getActionMasked(); 248 switch (action) { 249 case MotionEvent.ACTION_DOWN: 250 mSnapping = false; 251 if (mFadeAnimator != null) { 252 mFadeAnimator.cancel(); 253 } 254 mHandler.removeCallbacks(mCheckForDrag); 255 mCheckForDrag = null; 256 mPrevX = ev.getRawX(); 257 mIsUserTouching = true; 258 break; 259 260 case MotionEvent.ACTION_MOVE: 261 mSnapping = false; 262 float diffX = ev.getRawX() - mPrevX; 263 mPrevX = ev.getRawX(); 264 if (!isTowardsMenu(diffX) && isMenuLocationChange()) { 265 // Don't consider it "snapped" if location has changed. 266 mMenuSnappedTo = false; 267 268 // Changed directions, make sure we check to fade in icon again. 269 if (!mHandler.hasCallbacks(mCheckForDrag)) { 270 // No check scheduled, set null to schedule a new one. 271 mCheckForDrag = null; 272 } else { 273 // Check scheduled, reset alpha and update location; check will fade it in 274 setMenuAlpha(0f); 275 setMenuLocation(); 276 } 277 } 278 if (mShouldShowMenu 279 && !NotificationStackScrollLayout.isPinnedHeadsUp(view) 280 && !mParent.areGutsExposed() 281 && !mParent.isDark() 282 && (mCheckForDrag == null || !mHandler.hasCallbacks(mCheckForDrag))) { 283 // Only show the menu if we're not a heads up view and guts aren't exposed. 284 mCheckForDrag = new CheckForDrag(); 285 mHandler.postDelayed(mCheckForDrag, SHOW_MENU_DELAY); 286 } 287 break; 288 289 case MotionEvent.ACTION_UP: 290 mIsUserTouching = false; 291 return handleUpEvent(ev, view, velocity); 292 case MotionEvent.ACTION_CANCEL: 293 mIsUserTouching = false; 294 cancelDrag(); 295 return false; 296 } 297 return false; 298 } 299 handleUpEvent(MotionEvent ev, View animView, float velocity)300 private boolean handleUpEvent(MotionEvent ev, View animView, float velocity) { 301 // If the menu should not be shown, then there is no need to check if the a swipe 302 // should result in a snapping to the menu. As a result, just check if the swipe 303 // was enough to dismiss the notification. 304 if (!mShouldShowMenu) { 305 if (mSwipeHelper.isDismissGesture(ev)) { 306 dismiss(animView, velocity); 307 } else { 308 snapBack(animView, velocity); 309 } 310 return true; 311 } 312 313 final boolean gestureTowardsMenu = isTowardsMenu(velocity); 314 final boolean gestureFastEnough = 315 mSwipeHelper.getMinDismissVelocity() <= Math.abs(velocity); 316 final boolean gestureFarEnough = 317 mSwipeHelper.swipedFarEnough(mTranslation, mParent.getWidth()); 318 final double timeForGesture = ev.getEventTime() - ev.getDownTime(); 319 final boolean showMenuForSlowOnGoing = !mParent.canViewBeDismissed() 320 && timeForGesture >= SWIPE_MENU_TIMING; 321 final float menuSnapTarget = mOnLeft ? getSpaceForMenu() : -getSpaceForMenu(); 322 323 if (DEBUG) { 324 Log.d(TAG, "mTranslation= " + mTranslation 325 + " mAlpha= " + mAlpha 326 + " velocity= " + velocity 327 + " mMenuSnappedTo= " + mMenuSnappedTo 328 + " mMenuSnappedOnLeft= " + mMenuSnappedOnLeft 329 + " mOnLeft= " + mOnLeft 330 + " minDismissVel= " + mSwipeHelper.getMinDismissVelocity() 331 + " isDismissGesture= " + mSwipeHelper.isDismissGesture(ev) 332 + " gestureTowardsMenu= " + gestureTowardsMenu 333 + " gestureFastEnough= " + gestureFastEnough 334 + " gestureFarEnough= " + gestureFarEnough); 335 } 336 337 if (mMenuSnappedTo && isMenuVisible() && mMenuSnappedOnLeft == mOnLeft) { 338 // Menu was snapped to previously and we're on the same side, figure out if 339 // we should stick to the menu, snap back into place, or dismiss 340 final float maximumSwipeDistance = mHorizSpaceForIcon 341 * SWIPED_BACK_ENOUGH_TO_COVER_FRACTION; 342 final float targetLeft = getSpaceForMenu() - maximumSwipeDistance; 343 final float targetRight = mParent.getWidth() * SWIPED_FAR_ENOUGH_SIZE_FRACTION; 344 boolean withinSnapMenuThreshold = mOnLeft 345 ? mTranslation > targetLeft && mTranslation < targetRight 346 : mTranslation < -targetLeft && mTranslation > -targetRight; 347 boolean shouldSnapTo = mOnLeft ? mTranslation < targetLeft : mTranslation > -targetLeft; 348 if (DEBUG) { 349 Log.d(TAG, " withinSnapMenuThreshold= " + withinSnapMenuThreshold 350 + " shouldSnapTo= " + shouldSnapTo 351 + " targetLeft= " + targetLeft 352 + " targetRight= " + targetRight); 353 } 354 if (withinSnapMenuThreshold && !mSwipeHelper.isDismissGesture(ev)) { 355 // Haven't moved enough to unsnap from the menu 356 showMenu(animView, menuSnapTarget, velocity); 357 } else if (mSwipeHelper.isDismissGesture(ev) && !shouldSnapTo) { 358 // Only dismiss if we're not moving towards the menu 359 dismiss(animView, velocity); 360 } else { 361 snapBack(animView, velocity); 362 } 363 } else if (!mSwipeHelper.isFalseGesture(ev) 364 && (swipedEnoughToShowMenu() && (!gestureFastEnough || showMenuForSlowOnGoing)) 365 || (gestureTowardsMenu && !mSwipeHelper.isDismissGesture(ev))) { 366 // Menu has not been snapped to previously and this is menu revealing gesture 367 showMenu(animView, menuSnapTarget, velocity); 368 } else if (mSwipeHelper.isDismissGesture(ev) && !gestureTowardsMenu) { 369 dismiss(animView, velocity); 370 } else { 371 snapBack(animView, velocity); 372 } 373 return true; 374 } 375 376 private void showMenu(View animView, float targetLeft, float velocity) { 377 mMenuSnappedTo = true; 378 mMenuSnappedOnLeft = mOnLeft; 379 mMenuListener.onMenuShown(animView); 380 mSwipeHelper.snap(animView, targetLeft, velocity); 381 } 382 383 private void snapBack(View animView, float velocity) { 384 cancelDrag(); 385 mMenuSnappedTo = false; 386 mSnapping = true; 387 mSwipeHelper.snap(animView, 0 /* leftTarget */, velocity); 388 } 389 390 private void dismiss(View animView, float velocity) { 391 cancelDrag(); 392 mMenuSnappedTo = false; 393 mDismissing = true; 394 mSwipeHelper.dismiss(animView, velocity); 395 } 396 397 private void cancelDrag() { 398 if (mFadeAnimator != null) { 399 mFadeAnimator.cancel(); 400 } 401 mHandler.removeCallbacks(mCheckForDrag); 402 } 403 404 /** 405 * @return whether the notification has been translated enough to show the menu and not enough 406 * to be dismissed. 407 */ 408 private boolean swipedEnoughToShowMenu() { 409 final float multiplier = mParent.canViewBeDismissed() 410 ? SWIPED_FAR_ENOUGH_MENU_FRACTION 411 : SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION; 412 final float minimumSwipeDistance = mHorizSpaceForIcon * multiplier; 413 return !mSwipeHelper.swipedFarEnough(0, 0) && isMenuVisible() 414 && (mOnLeft ? mTranslation > minimumSwipeDistance 415 : mTranslation < -minimumSwipeDistance); 416 } 417 418 /** 419 * Returns whether the gesture is towards the menu location or not. 420 */ 421 private boolean isTowardsMenu(float movement) { 422 return isMenuVisible() 423 && ((mOnLeft && movement <= 0) 424 || (!mOnLeft && movement >= 0)); 425 } 426 427 @Override 428 public void setAppName(String appName) { 429 if (appName == null) { 430 return; 431 } 432 Resources res = mContext.getResources(); 433 final int count = mMenuItems.size(); 434 for (int i = 0; i < count; i++) { 435 MenuItem item = mMenuItems.get(i); 436 String description = String.format( 437 res.getString(R.string.notification_menu_accessibility), 438 appName, item.getContentDescription()); 439 View menuView = item.getMenuView(); 440 if (menuView != null) { 441 menuView.setContentDescription(description); 442 } 443 } 444 } 445 446 @Override 447 public void onHeightUpdate() { 448 if (mParent == null || mMenuItems.size() == 0 || mMenuContainer == null) { 449 return; 450 } 451 int parentHeight = mParent.getActualHeight(); 452 float translationY; 453 if (parentHeight < mVertSpaceForIcons) { 454 translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2); 455 } else { 456 translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2; 457 } 458 mMenuContainer.setTranslationY(translationY); 459 } 460 461 @Override 462 public void onTranslationUpdate(float translation) { 463 mTranslation = translation; 464 if (mAnimating || !mMenuFadedIn) { 465 // Don't adjust when animating, or if the menu hasn't been shown yet. 466 return; 467 } 468 final float fadeThreshold = mParent.getWidth() * 0.3f; 469 final float absTrans = Math.abs(translation); 470 float desiredAlpha = 0; 471 if (absTrans == 0) { 472 desiredAlpha = 0; 473 } else if (absTrans <= fadeThreshold) { 474 desiredAlpha = 1; 475 } else { 476 desiredAlpha = 1 - ((absTrans - fadeThreshold) / (mParent.getWidth() - fadeThreshold)); 477 } 478 setMenuAlpha(desiredAlpha); 479 } 480 481 @Override 482 public void onClick(View v) { 483 if (mMenuListener == null) { 484 // Nothing to do 485 return; 486 } 487 v.getLocationOnScreen(mIconLocation); 488 mParent.getLocationOnScreen(mParentLocation); 489 final int centerX = (int) (mHorizSpaceForIcon / 2); 490 final int centerY = v.getHeight() / 2; 491 final int x = mIconLocation[0] - mParentLocation[0] + centerX; 492 final int y = mIconLocation[1] - mParentLocation[1] + centerY; 493 final int index = mMenuContainer.indexOfChild(v); 494 mMenuListener.onMenuClicked(mParent, x, y, mMenuItems.get(index)); 495 } 496 497 private boolean isMenuLocationChange() { 498 boolean onLeft = mTranslation > mIconPadding; 499 boolean onRight = mTranslation < -mIconPadding; 500 if ((mOnLeft && onRight) || (!mOnLeft && onLeft)) { 501 return true; 502 } 503 return false; 504 } 505 506 private void setMenuLocation() { 507 boolean showOnLeft = mTranslation > 0; 508 if ((mIconsPlaced && showOnLeft == mOnLeft) || mSnapping || mMenuContainer == null 509 || !mMenuContainer.isAttachedToWindow()) { 510 // Do nothing 511 return; 512 } 513 final int count = mMenuContainer.getChildCount(); 514 for (int i = 0; i < count; i++) { 515 final View v = mMenuContainer.getChildAt(i); 516 final float left = i * mHorizSpaceForIcon; 517 final float right = mParent.getWidth() - (mHorizSpaceForIcon * (i + 1)); 518 v.setX(showOnLeft ? left : right); 519 } 520 mOnLeft = showOnLeft; 521 mIconsPlaced = true; 522 } 523 524 private void setMenuAlpha(float alpha) { 525 mAlpha = alpha; 526 if (mMenuContainer == null) { 527 return; 528 } 529 if (alpha == 0) { 530 mMenuFadedIn = false; // Can fade in again once it's gone. 531 mMenuContainer.setVisibility(View.INVISIBLE); 532 } else { 533 mMenuContainer.setVisibility(View.VISIBLE); 534 } 535 final int count = mMenuContainer.getChildCount(); 536 for (int i = 0; i < count; i++) { 537 mMenuContainer.getChildAt(i).setAlpha(mAlpha); 538 } 539 } 540 541 /** 542 * Returns the horizontal space in pixels required to display the menu. 543 */ 544 private float getSpaceForMenu() { 545 return mHorizSpaceForIcon * mMenuContainer.getChildCount(); 546 } 547 548 private final class CheckForDrag implements Runnable { 549 @Override 550 public void run() { 551 final float absTransX = Math.abs(mTranslation); 552 final float bounceBackToMenuWidth = getSpaceForMenu(); 553 final float notiThreshold = mParent.getWidth() * 0.4f; 554 if ((!isMenuVisible() || isMenuLocationChange()) 555 && absTransX >= bounceBackToMenuWidth * 0.4 556 && absTransX < notiThreshold) { 557 fadeInMenu(notiThreshold); 558 } 559 } 560 } 561 562 private void fadeInMenu(final float notiThreshold) { 563 if (mDismissing || mAnimating) { 564 return; 565 } 566 if (isMenuLocationChange()) { 567 setMenuAlpha(0f); 568 } 569 final float transX = mTranslation; 570 final boolean fromLeft = mTranslation > 0; 571 setMenuLocation(); 572 mFadeAnimator = ValueAnimator.ofFloat(mAlpha, 1); 573 mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 574 @Override 575 public void onAnimationUpdate(ValueAnimator animation) { 576 final float absTrans = Math.abs(transX); 577 578 boolean pastMenu = (fromLeft && transX <= notiThreshold) 579 || (!fromLeft && absTrans <= notiThreshold); 580 if (pastMenu && !mMenuFadedIn) { 581 setMenuAlpha((float) animation.getAnimatedValue()); 582 } 583 } 584 }); 585 mFadeAnimator.addListener(new AnimatorListenerAdapter() { 586 @Override 587 public void onAnimationStart(Animator animation) { 588 mAnimating = true; 589 } 590 591 @Override 592 public void onAnimationCancel(Animator animation) { 593 // TODO should animate back to 0f from current alpha 594 setMenuAlpha(0f); 595 } 596 597 @Override 598 public void onAnimationEnd(Animator animation) { 599 mAnimating = false; 600 mMenuFadedIn = mAlpha == 1; 601 } 602 }); 603 mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN); 604 mFadeAnimator.setDuration(ICON_ALPHA_ANIM_DURATION); 605 mFadeAnimator.start(); 606 } 607 608 @Override 609 public void setMenuItems(ArrayList<MenuItem> items) { 610 // Do nothing we use our own for now. 611 // TODO -- handle / allow custom menu items! 612 } 613 614 public static MenuItem createSnoozeItem(Context context) { 615 Resources res = context.getResources(); 616 NotificationSnooze content = (NotificationSnooze) LayoutInflater.from(context) 617 .inflate(R.layout.notification_snooze, null, false); 618 String snoozeDescription = res.getString(R.string.notification_menu_snooze_description); 619 MenuItem snooze = new NotificationMenuItem(context, snoozeDescription, content, 620 R.drawable.ic_snooze); 621 return snooze; 622 } 623 624 public static MenuItem createInfoItem(Context context) { 625 Resources res = context.getResources(); 626 String infoDescription = res.getString(R.string.notification_menu_gear_description); 627 NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate( 628 R.layout.notification_info, null, false); 629 MenuItem info = new NotificationMenuItem(context, infoDescription, infoContent, 630 R.drawable.ic_settings); 631 return info; 632 } 633 634 public static MenuItem createAppOpsItem(Context context) { 635 AppOpsInfo appOpsContent = (AppOpsInfo) LayoutInflater.from(context).inflate( 636 R.layout.app_ops_info, null, false); 637 MenuItem info = new NotificationMenuItem(context, null, appOpsContent, 638 -1 /*don't show in slow swipe menu */); 639 return info; 640 } 641 642 private void addMenuView(MenuItem item, ViewGroup parent) { 643 View menuView = item.getMenuView(); 644 if (menuView != null) { 645 parent.addView(menuView); 646 menuView.setOnClickListener(this); 647 FrameLayout.LayoutParams lp = (LayoutParams) menuView.getLayoutParams(); 648 lp.width = (int) mHorizSpaceForIcon; 649 lp.height = (int) mHorizSpaceForIcon; 650 menuView.setLayoutParams(lp); 651 } 652 } 653 654 public static class NotificationMenuItem implements MenuItem { 655 View mMenuView; 656 GutsContent mGutsContent; 657 String mContentDescription; 658 659 /** 660 * Add a new 'guts' panel. If iconResId < 0 it will not appear in the slow swipe menu 661 * but can still be exposed via other affordances. 662 */ 663 public NotificationMenuItem(Context context, String s, GutsContent content, int iconResId) { 664 Resources res = context.getResources(); 665 int padding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding); 666 int tint = res.getColor(R.color.notification_gear_color); 667 if (iconResId >= 0) { 668 AlphaOptimizedImageView iv = new AlphaOptimizedImageView(context); 669 iv.setPadding(padding, padding, padding, padding); 670 Drawable icon = context.getResources().getDrawable(iconResId); 671 iv.setImageDrawable(icon); 672 iv.setColorFilter(tint); 673 iv.setAlpha(1f); 674 mMenuView = iv; 675 } 676 mContentDescription = s; 677 mGutsContent = content; 678 } 679 680 @Override 681 @Nullable 682 public View getMenuView() { 683 return mMenuView; 684 } 685 686 @Override 687 public View getGutsView() { 688 return mGutsContent.getContentView(); 689 } 690 691 @Override 692 public String getContentDescription() { 693 return mContentDescription; 694 } 695 } 696 } 697