ExpandableNotificationRow.java revision 434180ce5155f0c405080698031f05e9eab3dd4c
1/* 2 * Copyright (C) 2013 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 17package com.android.systemui.statusbar; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.animation.ValueAnimator.AnimatorUpdateListener; 23import android.annotation.Nullable; 24import android.content.Context; 25import android.content.res.ColorStateList; 26import android.graphics.drawable.AnimatedVectorDrawable; 27import android.graphics.drawable.AnimationDrawable; 28import android.graphics.drawable.ColorDrawable; 29import android.graphics.drawable.Drawable; 30import android.os.Build; 31import android.os.Bundle; 32import android.service.notification.StatusBarNotification; 33import android.util.AttributeSet; 34import android.util.FloatProperty; 35import android.util.Property; 36import android.view.LayoutInflater; 37import android.view.MotionEvent; 38import android.view.NotificationHeaderView; 39import android.view.View; 40import android.view.ViewGroup; 41import android.view.ViewStub; 42import android.view.accessibility.AccessibilityEvent; 43import android.view.accessibility.AccessibilityNodeInfo; 44import android.widget.Chronometer; 45import android.widget.ImageView; 46 47import com.android.internal.logging.MetricsLogger; 48import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 49import com.android.internal.util.NotificationColorUtil; 50import com.android.internal.widget.CachingIconView; 51import com.android.systemui.Interpolators; 52import com.android.systemui.R; 53import com.android.systemui.classifier.FalsingManager; 54import com.android.systemui.plugins.statusbar.NotificationMenuRowProvider.MenuItem; 55import com.android.systemui.statusbar.notification.HybridNotificationView; 56import com.android.systemui.statusbar.notification.NotificationUtils; 57import com.android.systemui.statusbar.notification.VisualStabilityManager; 58import com.android.systemui.statusbar.phone.NotificationGroupManager; 59import com.android.systemui.statusbar.phone.StatusBar; 60import com.android.systemui.statusbar.policy.HeadsUpManager; 61import com.android.systemui.statusbar.stack.AnimationProperties; 62import com.android.systemui.statusbar.stack.ExpandableViewState; 63import com.android.systemui.statusbar.stack.NotificationChildrenContainer; 64import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; 65import com.android.systemui.statusbar.stack.StackScrollState; 66 67import java.util.ArrayList; 68import java.util.List; 69 70public class ExpandableNotificationRow extends ActivatableNotificationView { 71 72 private static final int DEFAULT_DIVIDER_ALPHA = 0x29; 73 private static final int COLORED_DIVIDER_ALPHA = 0x7B; 74 private int mIconTransformContentShift; 75 private int mIconTransformContentShiftNoIcon; 76 private int mNotificationMinHeightLegacy; 77 private int mMaxHeadsUpHeightLegacy; 78 private int mMaxHeadsUpHeight; 79 private int mNotificationMinHeight; 80 private int mNotificationMinHeightLarge; 81 private int mNotificationMaxHeight; 82 private int mNotificationAmbientHeight; 83 private int mIncreasedPaddingBetweenElements; 84 85 /** Does this row contain layouts that can adapt to row expansion */ 86 private boolean mExpandable; 87 /** Has the user actively changed the expansion state of this row */ 88 private boolean mHasUserChangedExpansion; 89 /** If {@link #mHasUserChangedExpansion}, has the user expanded this row */ 90 private boolean mUserExpanded; 91 92 /** 93 * Has this notification been expanded while it was pinned 94 */ 95 private boolean mExpandedWhenPinned; 96 /** Is the user touching this row */ 97 private boolean mUserLocked; 98 /** Are we showing the "public" version */ 99 private boolean mShowingPublic; 100 private boolean mSensitive; 101 private boolean mSensitiveHiddenInGeneral; 102 private boolean mShowingPublicInitialized; 103 private boolean mHideSensitiveForIntrinsicHeight; 104 105 /** 106 * Is this notification expanded by the system. The expansion state can be overridden by the 107 * user expansion. 108 */ 109 private boolean mIsSystemExpanded; 110 111 /** 112 * Whether the notification is on the keyguard and the expansion is disabled. 113 */ 114 private boolean mOnKeyguard; 115 116 private Animator mTranslateAnim; 117 private ArrayList<View> mTranslateableViews; 118 private NotificationContentView mPublicLayout; 119 private NotificationContentView mPrivateLayout; 120 private NotificationContentView[] mLayouts; 121 private int mMaxExpandHeight; 122 private int mHeadsUpHeight; 123 private int mNotificationColor; 124 private ExpansionLogger mLogger; 125 private String mLoggingKey; 126 private NotificationMenuRow mMenuRow; 127 private NotificationGuts mGuts; 128 private NotificationData.Entry mEntry; 129 private StatusBarNotification mStatusBarNotification; 130 private String mAppName; 131 private boolean mIsHeadsUp; 132 private boolean mLastChronometerRunning = true; 133 private ViewStub mChildrenContainerStub; 134 private NotificationGroupManager mGroupManager; 135 private boolean mChildrenExpanded; 136 private boolean mIsSummaryWithChildren; 137 private NotificationChildrenContainer mChildrenContainer; 138 private ViewStub mMenuRowStub; 139 private ViewStub mGutsStub; 140 private boolean mIsSystemChildExpanded; 141 private boolean mIsPinned; 142 private FalsingManager mFalsingManager; 143 private HeadsUpManager mHeadsUpManager; 144 145 private boolean mJustClicked; 146 private boolean mIconAnimationRunning; 147 private boolean mShowNoBackground; 148 private ExpandableNotificationRow mNotificationParent; 149 private OnExpandClickListener mOnExpandClickListener; 150 private boolean mGroupExpansionChanging; 151 152 private OnClickListener mExpandClickListener = new OnClickListener() { 153 @Override 154 public void onClick(View v) { 155 if (!mShowingPublic && mGroupManager.isSummaryOfGroup(mStatusBarNotification)) { 156 mGroupExpansionChanging = true; 157 final boolean wasExpanded = mGroupManager.isGroupExpanded(mStatusBarNotification); 158 boolean nowExpanded = mGroupManager.toggleGroupExpansion(mStatusBarNotification); 159 mOnExpandClickListener.onExpandClicked(mEntry, nowExpanded); 160 MetricsLogger.action(mContext, MetricsEvent.ACTION_NOTIFICATION_GROUP_EXPANDER, 161 nowExpanded); 162 logExpansionEvent(true /* userAction */, wasExpanded); 163 } else { 164 if (v.isAccessibilityFocused()) { 165 mPrivateLayout.setFocusOnVisibilityChange(); 166 } 167 boolean nowExpanded; 168 if (isPinned()) { 169 nowExpanded = !mExpandedWhenPinned; 170 mExpandedWhenPinned = nowExpanded; 171 } else { 172 nowExpanded = !isExpanded(); 173 setUserExpanded(nowExpanded); 174 } 175 notifyHeightChanged(true); 176 mOnExpandClickListener.onExpandClicked(mEntry, nowExpanded); 177 MetricsLogger.action(mContext, MetricsEvent.ACTION_NOTIFICATION_EXPANDER, 178 nowExpanded); 179 } 180 } 181 }; 182 private boolean mForceUnlocked; 183 private boolean mDismissed; 184 private boolean mKeepInParent; 185 private boolean mRemoved; 186 private static final Property<ExpandableNotificationRow, Float> TRANSLATE_CONTENT = 187 new FloatProperty<ExpandableNotificationRow>("translate") { 188 @Override 189 public void setValue(ExpandableNotificationRow object, float value) { 190 object.setTranslation(value); 191 } 192 193 @Override 194 public Float get(ExpandableNotificationRow object) { 195 return object.getTranslation(); 196 } 197 }; 198 private OnClickListener mOnClickListener; 199 private boolean mHeadsupDisappearRunning; 200 private View mChildAfterViewWhenDismissed; 201 private View mGroupParentWhenDismissed; 202 private boolean mRefocusOnDismiss; 203 private float mContentTransformationAmount; 204 private boolean mIconsVisible = true; 205 private boolean mAboveShelf; 206 private boolean mShowAmbient; 207 private boolean mIsLastChild; 208 private Runnable mOnDismissRunnable; 209 private boolean mIsLowPriority; 210 private boolean mIsColorized; 211 private boolean mUseIncreasedCollapsedHeight; 212 213 @Override 214 public boolean isGroupExpansionChanging() { 215 if (isChildInGroup()) { 216 return mNotificationParent.isGroupExpansionChanging(); 217 } 218 return mGroupExpansionChanging; 219 } 220 221 public void setGroupExpansionChanging(boolean changing) { 222 mGroupExpansionChanging = changing; 223 } 224 225 @Override 226 public void setActualHeightAnimating(boolean animating) { 227 if (mPrivateLayout != null) { 228 mPrivateLayout.setContentHeightAnimating(animating); 229 } 230 } 231 232 public NotificationContentView getPrivateLayout() { 233 return mPrivateLayout; 234 } 235 236 public NotificationContentView getPublicLayout() { 237 return mPublicLayout; 238 } 239 240 public void setIconAnimationRunning(boolean running) { 241 for (NotificationContentView l : mLayouts) { 242 setIconAnimationRunning(running, l); 243 } 244 if (mIsSummaryWithChildren) { 245 setIconAnimationRunningForChild(running, mChildrenContainer.getHeaderView()); 246 List<ExpandableNotificationRow> notificationChildren = 247 mChildrenContainer.getNotificationChildren(); 248 for (int i = 0; i < notificationChildren.size(); i++) { 249 ExpandableNotificationRow child = notificationChildren.get(i); 250 child.setIconAnimationRunning(running); 251 } 252 } 253 mIconAnimationRunning = running; 254 } 255 256 private void setIconAnimationRunning(boolean running, NotificationContentView layout) { 257 if (layout != null) { 258 View contractedChild = layout.getContractedChild(); 259 View expandedChild = layout.getExpandedChild(); 260 View headsUpChild = layout.getHeadsUpChild(); 261 setIconAnimationRunningForChild(running, contractedChild); 262 setIconAnimationRunningForChild(running, expandedChild); 263 setIconAnimationRunningForChild(running, headsUpChild); 264 } 265 } 266 267 private void setIconAnimationRunningForChild(boolean running, View child) { 268 if (child != null) { 269 ImageView icon = (ImageView) child.findViewById(com.android.internal.R.id.icon); 270 setIconRunning(icon, running); 271 ImageView rightIcon = (ImageView) child.findViewById( 272 com.android.internal.R.id.right_icon); 273 setIconRunning(rightIcon, running); 274 } 275 } 276 277 private void setIconRunning(ImageView imageView, boolean running) { 278 if (imageView != null) { 279 Drawable drawable = imageView.getDrawable(); 280 if (drawable instanceof AnimationDrawable) { 281 AnimationDrawable animationDrawable = (AnimationDrawable) drawable; 282 if (running) { 283 animationDrawable.start(); 284 } else { 285 animationDrawable.stop(); 286 } 287 } else if (drawable instanceof AnimatedVectorDrawable) { 288 AnimatedVectorDrawable animationDrawable = (AnimatedVectorDrawable) drawable; 289 if (running) { 290 animationDrawable.start(); 291 } else { 292 animationDrawable.stop(); 293 } 294 } 295 } 296 } 297 298 public void onNotificationUpdated(NotificationData.Entry entry) { 299 mEntry = entry; 300 mStatusBarNotification = entry.notification; 301 for (NotificationContentView l : mLayouts) { 302 l.onNotificationUpdated(entry); 303 } 304 mIsColorized = mStatusBarNotification.getNotification().isColorized(); 305 mShowingPublicInitialized = false; 306 updateNotificationColor(); 307 if (mIsSummaryWithChildren) { 308 mChildrenContainer.recreateNotificationHeader(mExpandClickListener, mEntry.notification); 309 mChildrenContainer.onNotificationUpdated(); 310 } 311 if (mIconAnimationRunning) { 312 setIconAnimationRunning(true); 313 } 314 if (mNotificationParent != null) { 315 mNotificationParent.updateChildrenHeaderAppearance(); 316 } 317 onChildrenCountChanged(); 318 // The public layouts expand button is always visible 319 mPublicLayout.updateExpandButtons(true); 320 updateLimits(); 321 updateIconVisibilities(); 322 updateShelfIconColor(); 323 } 324 325 private void updateShelfIconColor() { 326 StatusBarIconView expandedIcon = mEntry.expandedIcon; 327 boolean isPreL = Boolean.TRUE.equals(expandedIcon.getTag(R.id.icon_is_pre_L)); 328 boolean colorize = !isPreL || NotificationUtils.isGrayscale(expandedIcon, 329 NotificationColorUtil.getInstance(mContext)); 330 if (colorize) { 331 int color = mEntry.getContrastedColor(mContext, mIsLowPriority && !isExpanded()); 332 expandedIcon.setImageTintList(ColorStateList.valueOf(color)); 333 } 334 } 335 336 private void updateLimits() { 337 for (NotificationContentView l : mLayouts) { 338 updateLimitsForView(l); 339 } 340 } 341 342 private void updateLimitsForView(NotificationContentView layout) { 343 boolean customView = layout.getContractedChild().getId() 344 != com.android.internal.R.id.status_bar_latest_event_content; 345 boolean beforeN = mEntry.targetSdk < Build.VERSION_CODES.N; 346 int minHeight; 347 if (customView && beforeN && !mIsSummaryWithChildren) { 348 minHeight = mNotificationMinHeightLegacy; 349 } else if (mUseIncreasedCollapsedHeight && layout == mPrivateLayout) { 350 minHeight = mNotificationMinHeightLarge; 351 } else { 352 minHeight = mNotificationMinHeight; 353 } 354 boolean headsUpCustom = layout.getHeadsUpChild() != null && 355 layout.getHeadsUpChild().getId() 356 != com.android.internal.R.id.status_bar_latest_event_content; 357 int headsUpheight = headsUpCustom && beforeN ? mMaxHeadsUpHeightLegacy 358 : mMaxHeadsUpHeight; 359 layout.setHeights(minHeight, headsUpheight, mNotificationMaxHeight, 360 mNotificationAmbientHeight); 361 } 362 363 public StatusBarNotification getStatusBarNotification() { 364 return mStatusBarNotification; 365 } 366 367 public NotificationData.Entry getEntry() { 368 return mEntry; 369 } 370 371 public boolean isHeadsUp() { 372 return mIsHeadsUp; 373 } 374 375 public void setHeadsUp(boolean isHeadsUp) { 376 int intrinsicBefore = getIntrinsicHeight(); 377 mIsHeadsUp = isHeadsUp; 378 mPrivateLayout.setHeadsUp(isHeadsUp); 379 if (mIsSummaryWithChildren) { 380 // The overflow might change since we allow more lines as HUN. 381 mChildrenContainer.updateGroupOverflow(); 382 } 383 if (intrinsicBefore != getIntrinsicHeight()) { 384 notifyHeightChanged(false /* needsAnimation */); 385 } 386 if (isHeadsUp) { 387 setAboveShelf(true); 388 } 389 } 390 391 public void setGroupManager(NotificationGroupManager groupManager) { 392 mGroupManager = groupManager; 393 mPrivateLayout.setGroupManager(groupManager); 394 } 395 396 public void setRemoteInputController(RemoteInputController r) { 397 mPrivateLayout.setRemoteInputController(r); 398 } 399 400 public void setAppName(String appName) { 401 mAppName = appName; 402 if (mMenuRow != null) { 403 mMenuRow.setAppName(mAppName); 404 } 405 } 406 407 public void addChildNotification(ExpandableNotificationRow row) { 408 addChildNotification(row, -1); 409 } 410 411 /** 412 * Add a child notification to this view. 413 * 414 * @param row the row to add 415 * @param childIndex the index to add it at, if -1 it will be added at the end 416 */ 417 public void addChildNotification(ExpandableNotificationRow row, int childIndex) { 418 if (mChildrenContainer == null) { 419 mChildrenContainerStub.inflate(); 420 } 421 mChildrenContainer.addNotification(row, childIndex); 422 onChildrenCountChanged(); 423 row.setIsChildInGroup(true, this); 424 } 425 426 public void removeChildNotification(ExpandableNotificationRow row) { 427 if (mChildrenContainer != null) { 428 mChildrenContainer.removeNotification(row); 429 } 430 onChildrenCountChanged(); 431 row.setIsChildInGroup(false, null); 432 } 433 434 @Override 435 public boolean isChildInGroup() { 436 return mNotificationParent != null; 437 } 438 439 public ExpandableNotificationRow getNotificationParent() { 440 return mNotificationParent; 441 } 442 443 /** 444 * @param isChildInGroup Is this notification now in a group 445 * @param parent the new parent notification 446 */ 447 public void setIsChildInGroup(boolean isChildInGroup, ExpandableNotificationRow parent) {; 448 boolean childInGroup = StatusBar.ENABLE_CHILD_NOTIFICATIONS && isChildInGroup; 449 mNotificationParent = childInGroup ? parent : null; 450 mPrivateLayout.setIsChildInGroup(childInGroup); 451 resetBackgroundAlpha(); 452 updateBackgroundForGroupState(); 453 updateClickAndFocus(); 454 if (mNotificationParent != null) { 455 mNotificationParent.updateBackgroundForGroupState(); 456 } 457 updateIconVisibilities(); 458 } 459 460 @Override 461 public boolean onTouchEvent(MotionEvent event) { 462 if (event.getActionMasked() != MotionEvent.ACTION_DOWN 463 || !isChildInGroup() || isGroupExpanded()) { 464 return super.onTouchEvent(event); 465 } else { 466 return false; 467 } 468 } 469 470 @Override 471 protected boolean handleSlideBack() { 472 if (mMenuRow != null && mMenuRow.isVisible()) { 473 animateTranslateNotification(0 /* targetLeft */); 474 return true; 475 } 476 return false; 477 } 478 479 @Override 480 protected boolean shouldHideBackground() { 481 return super.shouldHideBackground() || mShowNoBackground; 482 } 483 484 @Override 485 public boolean isSummaryWithChildren() { 486 return mIsSummaryWithChildren; 487 } 488 489 @Override 490 public boolean areChildrenExpanded() { 491 return mChildrenExpanded; 492 } 493 494 public List<ExpandableNotificationRow> getNotificationChildren() { 495 return mChildrenContainer == null ? null : mChildrenContainer.getNotificationChildren(); 496 } 497 498 public int getNumberOfNotificationChildren() { 499 if (mChildrenContainer == null) { 500 return 0; 501 } 502 return mChildrenContainer.getNotificationChildren().size(); 503 } 504 505 /** 506 * Apply the order given in the list to the children. 507 * 508 * @param childOrder the new list order 509 * @param visualStabilityManager 510 * @param callback the callback to invoked in case it is not allowed 511 * @return whether the list order has changed 512 */ 513 public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder, 514 VisualStabilityManager visualStabilityManager, 515 VisualStabilityManager.Callback callback) { 516 return mChildrenContainer != null && mChildrenContainer.applyChildOrder(childOrder, 517 visualStabilityManager, callback); 518 } 519 520 public void getChildrenStates(StackScrollState resultState) { 521 if (mIsSummaryWithChildren) { 522 ExpandableViewState parentState = resultState.getViewStateForView(this); 523 mChildrenContainer.getState(resultState, parentState); 524 } 525 } 526 527 public void applyChildrenState(StackScrollState state) { 528 if (mIsSummaryWithChildren) { 529 mChildrenContainer.applyState(state); 530 } 531 } 532 533 public void prepareExpansionChanged(StackScrollState state) { 534 if (mIsSummaryWithChildren) { 535 mChildrenContainer.prepareExpansionChanged(state); 536 } 537 } 538 539 public void startChildAnimation(StackScrollState finalState, AnimationProperties properties) { 540 if (mIsSummaryWithChildren) { 541 mChildrenContainer.startAnimationToState(finalState, properties); 542 } 543 } 544 545 public ExpandableNotificationRow getViewAtPosition(float y) { 546 if (!mIsSummaryWithChildren || !mChildrenExpanded) { 547 return this; 548 } else { 549 ExpandableNotificationRow view = mChildrenContainer.getViewAtPosition(y); 550 return view == null ? this : view; 551 } 552 } 553 554 public NotificationGuts getGuts() { 555 return mGuts; 556 } 557 558 /** 559 * Set this notification to be pinned to the top if {@link #isHeadsUp()} is true. By doing this 560 * the notification will be rendered on top of the screen. 561 * 562 * @param pinned whether it is pinned 563 */ 564 public void setPinned(boolean pinned) { 565 int intrinsicHeight = getIntrinsicHeight(); 566 mIsPinned = pinned; 567 if (intrinsicHeight != getIntrinsicHeight()) { 568 notifyHeightChanged(false /* needsAnimation */); 569 } 570 if (pinned) { 571 setIconAnimationRunning(true); 572 mExpandedWhenPinned = false; 573 } else if (mExpandedWhenPinned) { 574 setUserExpanded(true); 575 } 576 setChronometerRunning(mLastChronometerRunning); 577 } 578 579 public boolean isPinned() { 580 return mIsPinned; 581 } 582 583 @Override 584 public int getPinnedHeadsUpHeight() { 585 return getPinnedHeadsUpHeight(true /* atLeastMinHeight */); 586 } 587 588 /** 589 * @param atLeastMinHeight should the value returned be at least the minimum height. 590 * Used to avoid cyclic calls 591 * @return the height of the heads up notification when pinned 592 */ 593 private int getPinnedHeadsUpHeight(boolean atLeastMinHeight) { 594 if (mIsSummaryWithChildren) { 595 return mChildrenContainer.getIntrinsicHeight(); 596 } 597 if(mExpandedWhenPinned) { 598 return Math.max(getMaxExpandHeight(), mHeadsUpHeight); 599 } else if (atLeastMinHeight) { 600 return Math.max(getCollapsedHeight(), mHeadsUpHeight); 601 } else { 602 return mHeadsUpHeight; 603 } 604 } 605 606 /** 607 * Mark whether this notification was just clicked, i.e. the user has just clicked this 608 * notification in this frame. 609 */ 610 public void setJustClicked(boolean justClicked) { 611 mJustClicked = justClicked; 612 } 613 614 /** 615 * @return true if this notification has been clicked in this frame, false otherwise 616 */ 617 public boolean wasJustClicked() { 618 return mJustClicked; 619 } 620 621 public void setChronometerRunning(boolean running) { 622 mLastChronometerRunning = running; 623 setChronometerRunning(running, mPrivateLayout); 624 setChronometerRunning(running, mPublicLayout); 625 if (mChildrenContainer != null) { 626 List<ExpandableNotificationRow> notificationChildren = 627 mChildrenContainer.getNotificationChildren(); 628 for (int i = 0; i < notificationChildren.size(); i++) { 629 ExpandableNotificationRow child = notificationChildren.get(i); 630 child.setChronometerRunning(running); 631 } 632 } 633 } 634 635 private void setChronometerRunning(boolean running, NotificationContentView layout) { 636 if (layout != null) { 637 running = running || isPinned(); 638 View contractedChild = layout.getContractedChild(); 639 View expandedChild = layout.getExpandedChild(); 640 View headsUpChild = layout.getHeadsUpChild(); 641 setChronometerRunningForChild(running, contractedChild); 642 setChronometerRunningForChild(running, expandedChild); 643 setChronometerRunningForChild(running, headsUpChild); 644 } 645 } 646 647 private void setChronometerRunningForChild(boolean running, View child) { 648 if (child != null) { 649 View chronometer = child.findViewById(com.android.internal.R.id.chronometer); 650 if (chronometer instanceof Chronometer) { 651 ((Chronometer) chronometer).setStarted(running); 652 } 653 } 654 } 655 656 public NotificationHeaderView getNotificationHeader() { 657 if (mIsSummaryWithChildren) { 658 return mChildrenContainer.getHeaderView(); 659 } 660 return mPrivateLayout.getNotificationHeader(); 661 } 662 663 private NotificationHeaderView getVisibleNotificationHeader() { 664 if (mIsSummaryWithChildren && !mShowingPublic) { 665 return mChildrenContainer.getHeaderView(); 666 } 667 return getShowingLayout().getVisibleNotificationHeader(); 668 } 669 670 public void setOnExpandClickListener(OnExpandClickListener onExpandClickListener) { 671 mOnExpandClickListener = onExpandClickListener; 672 } 673 674 @Override 675 public void setOnClickListener(@Nullable OnClickListener l) { 676 super.setOnClickListener(l); 677 mOnClickListener = l; 678 updateClickAndFocus(); 679 } 680 681 private void updateClickAndFocus() { 682 boolean normalChild = !isChildInGroup() || isGroupExpanded(); 683 boolean clickable = mOnClickListener != null && normalChild; 684 if (isFocusable() != normalChild) { 685 setFocusable(normalChild); 686 } 687 if (isClickable() != clickable) { 688 setClickable(clickable); 689 } 690 } 691 692 public void setHeadsUpManager(HeadsUpManager headsUpManager) { 693 mHeadsUpManager = headsUpManager; 694 } 695 696 public void setGutsView(MenuItem item) { 697 if (mGuts != null) { 698 item.gutsContent.setInteractionListener(mGuts); 699 mGuts.setGutsContent(item.gutsContent); 700 } 701 } 702 703 public void reInflateViews() { 704 initDimens(); 705 if (mIsSummaryWithChildren) { 706 if (mChildrenContainer != null) { 707 mChildrenContainer.reInflateViews(mExpandClickListener, mEntry.notification); 708 } 709 } 710 if (mGuts != null) { 711 View oldGuts = mGuts; 712 int index = indexOfChild(oldGuts); 713 removeView(oldGuts); 714 mGuts = (NotificationGuts) LayoutInflater.from(mContext).inflate( 715 R.layout.notification_guts, this, false); 716 mGuts.setVisibility(oldGuts.getVisibility()); 717 addView(mGuts, index); 718 } 719 if (mMenuRow != null) { 720 View oldMenu = mMenuRow; 721 int menuIndex = indexOfChild(oldMenu); 722 removeView(oldMenu); 723 mMenuRow = (NotificationMenuRow) LayoutInflater.from(mContext).inflate( 724 R.layout.notification_menu_row, this, false); 725 mMenuRow.setNotificationRowParent(ExpandableNotificationRow.this); 726 mMenuRow.setAppName(mAppName); 727 mMenuRow.setVisibility(oldMenu.getVisibility()); 728 addView(mMenuRow, menuIndex); 729 730 } 731 for (NotificationContentView l : mLayouts) { 732 l.reInflateViews(); 733 } 734 } 735 736 public void setContentBackground(int customBackgroundColor, boolean animate, 737 NotificationContentView notificationContentView) { 738 if (getShowingLayout() == notificationContentView) { 739 setTintColor(customBackgroundColor, animate); 740 } 741 } 742 743 public void closeRemoteInput() { 744 for (NotificationContentView l : mLayouts) { 745 l.closeRemoteInput(); 746 } 747 } 748 749 /** 750 * Set by how much the single line view should be indented. 751 */ 752 public void setSingleLineWidthIndention(int indention) { 753 mPrivateLayout.setSingleLineWidthIndention(indention); 754 } 755 756 public int getNotificationColor() { 757 return mNotificationColor; 758 } 759 760 private void updateNotificationColor() { 761 mNotificationColor = NotificationColorUtil.resolveContrastColor(mContext, 762 getStatusBarNotification().getNotification().color); 763 } 764 765 public HybridNotificationView getSingleLineView() { 766 return mPrivateLayout.getSingleLineView(); 767 } 768 769 public boolean isOnKeyguard() { 770 return mOnKeyguard; 771 } 772 773 public void removeAllChildren() { 774 List<ExpandableNotificationRow> notificationChildren 775 = mChildrenContainer.getNotificationChildren(); 776 ArrayList<ExpandableNotificationRow> clonedList = new ArrayList<>(notificationChildren); 777 for (int i = 0; i < clonedList.size(); i++) { 778 ExpandableNotificationRow row = clonedList.get(i); 779 if (row.keepInParent()) { 780 continue; 781 } 782 mChildrenContainer.removeNotification(row); 783 row.setIsChildInGroup(false, null); 784 } 785 onChildrenCountChanged(); 786 } 787 788 public void setForceUnlocked(boolean forceUnlocked) { 789 mForceUnlocked = forceUnlocked; 790 if (mIsSummaryWithChildren) { 791 List<ExpandableNotificationRow> notificationChildren = getNotificationChildren(); 792 for (ExpandableNotificationRow child : notificationChildren) { 793 child.setForceUnlocked(forceUnlocked); 794 } 795 } 796 } 797 798 public void setDismissed(boolean dismissed, boolean fromAccessibility) { 799 mDismissed = dismissed; 800 mGroupParentWhenDismissed = mNotificationParent; 801 mRefocusOnDismiss = fromAccessibility; 802 mChildAfterViewWhenDismissed = null; 803 if (isChildInGroup()) { 804 List<ExpandableNotificationRow> notificationChildren = 805 mNotificationParent.getNotificationChildren(); 806 int i = notificationChildren.indexOf(this); 807 if (i != -1 && i < notificationChildren.size() - 1) { 808 mChildAfterViewWhenDismissed = notificationChildren.get(i + 1); 809 } 810 } 811 } 812 813 public boolean isDismissed() { 814 return mDismissed; 815 } 816 817 public boolean keepInParent() { 818 return mKeepInParent; 819 } 820 821 public void setKeepInParent(boolean keepInParent) { 822 mKeepInParent = keepInParent; 823 } 824 825 public boolean isRemoved() { 826 return mRemoved; 827 } 828 829 public void setRemoved() { 830 mRemoved = true; 831 832 mPrivateLayout.setRemoved(); 833 } 834 835 public NotificationChildrenContainer getChildrenContainer() { 836 return mChildrenContainer; 837 } 838 839 public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) { 840 mHeadsupDisappearRunning = headsUpAnimatingAway; 841 mPrivateLayout.setHeadsUpAnimatingAway(headsUpAnimatingAway); 842 } 843 844 /** 845 * @return if the view was just heads upped and is now animating away. During such a time the 846 * layout needs to be kept consistent 847 */ 848 public boolean isHeadsUpAnimatingAway() { 849 return mHeadsupDisappearRunning; 850 } 851 852 public View getChildAfterViewWhenDismissed() { 853 return mChildAfterViewWhenDismissed; 854 } 855 856 public View getGroupParentWhenDismissed() { 857 return mGroupParentWhenDismissed; 858 } 859 860 public void performDismiss() { 861 if (mOnDismissRunnable != null) { 862 mOnDismissRunnable.run(); 863 } 864 } 865 866 public void setOnDismissRunnable(Runnable onDismissRunnable) { 867 mOnDismissRunnable = onDismissRunnable; 868 } 869 870 public View getNotificationIcon() { 871 NotificationHeaderView notificationHeader = getNotificationHeader(); 872 if (notificationHeader != null) { 873 return notificationHeader.getIcon(); 874 } 875 return null; 876 } 877 878 /** 879 * @return whether the notification is currently showing a view with an icon. 880 */ 881 public boolean isShowingIcon() { 882 if (areGutsExposed()) { 883 return false; 884 } 885 if (mIsSummaryWithChildren) { 886 return true; 887 } 888 NotificationContentView showingLayout = getShowingLayout(); 889 NotificationHeaderView notificationHeader = showingLayout.getVisibleNotificationHeader(); 890 return notificationHeader != null; 891 } 892 893 /** 894 * Set how much this notification is transformed into an icon. 895 * 896 * @param contentTransformationAmount A value from 0 to 1 indicating how much we are transformed 897 * to the content away 898 * @param isLastChild is this the last child in the list. If true, then the transformation is 899 * different since it's content fades out. 900 */ 901 public void setContentTransformationAmount(float contentTransformationAmount, 902 boolean isLastChild) { 903 boolean changeTransformation = isLastChild != mIsLastChild; 904 changeTransformation |= mContentTransformationAmount != contentTransformationAmount; 905 mIsLastChild = isLastChild; 906 mContentTransformationAmount = contentTransformationAmount; 907 if (changeTransformation) { 908 updateContentTransformation(); 909 } 910 } 911 912 /** 913 * Set the icons to be visible of this notification. 914 */ 915 public void setIconsVisible(boolean iconsVisible) { 916 if (iconsVisible != mIconsVisible) { 917 mIconsVisible = iconsVisible; 918 updateIconVisibilities(); 919 } 920 } 921 922 @Override 923 protected void onBelowSpeedBumpChanged() { 924 updateIconVisibilities(); 925 } 926 927 private void updateContentTransformation() { 928 float contentAlpha; 929 float translationY = -mContentTransformationAmount * mIconTransformContentShift; 930 if (mIsLastChild) { 931 contentAlpha = 1.0f - mContentTransformationAmount; 932 contentAlpha = Math.min(contentAlpha / 0.5f, 1.0f); 933 contentAlpha = Interpolators.ALPHA_OUT.getInterpolation(contentAlpha); 934 translationY *= 0.4f; 935 } else { 936 contentAlpha = 1.0f; 937 } 938 for (NotificationContentView l : mLayouts) { 939 l.setAlpha(contentAlpha); 940 l.setTranslationY(translationY); 941 } 942 if (mChildrenContainer != null) { 943 mChildrenContainer.setAlpha(contentAlpha); 944 mChildrenContainer.setTranslationY(translationY); 945 // TODO: handle children fade out better 946 } 947 } 948 949 private void updateIconVisibilities() { 950 boolean visible = isChildInGroup() 951 || (isBelowSpeedBump() && !NotificationShelf.SHOW_AMBIENT_ICONS) 952 || mIconsVisible; 953 for (NotificationContentView l : mLayouts) { 954 l.setIconsVisible(visible); 955 } 956 if (mChildrenContainer != null) { 957 mChildrenContainer.setIconsVisible(visible); 958 } 959 } 960 961 /** 962 * Get the relative top padding of a view relative to this view. This recursively walks up the 963 * hierarchy and does the corresponding measuring. 964 * 965 * @param view the view to the the padding for. The requested view has to be a child of this 966 * notification. 967 * @return the toppadding 968 */ 969 public int getRelativeTopPadding(View view) { 970 int topPadding = 0; 971 while (view.getParent() instanceof ViewGroup) { 972 topPadding += view.getTop(); 973 view = (View) view.getParent(); 974 if (view instanceof ExpandableNotificationRow) { 975 return topPadding; 976 } 977 } 978 return topPadding; 979 } 980 981 public float getContentTranslation() { 982 return mPrivateLayout.getTranslationY(); 983 } 984 985 public void setIsLowPriority(boolean isLowPriority) { 986 mIsLowPriority = isLowPriority; 987 mPrivateLayout.setIsLowPriority(isLowPriority); 988 if (mChildrenContainer != null) { 989 mChildrenContainer.setIsLowPriority(isLowPriority); 990 } 991 } 992 993 public void setUseIncreasedCollapsedHeight(boolean use) { 994 mUseIncreasedCollapsedHeight = use; 995 } 996 997 public interface ExpansionLogger { 998 public void logNotificationExpansion(String key, boolean userAction, boolean expanded); 999 } 1000 1001 public ExpandableNotificationRow(Context context, AttributeSet attrs) { 1002 super(context, attrs); 1003 mFalsingManager = FalsingManager.getInstance(context); 1004 initDimens(); 1005 } 1006 1007 private void initDimens() { 1008 mNotificationMinHeightLegacy = getFontScaledHeight(R.dimen.notification_min_height_legacy); 1009 mNotificationMinHeight = getFontScaledHeight(R.dimen.notification_min_height); 1010 mNotificationMinHeightLarge = getFontScaledHeight( 1011 R.dimen.notification_min_height_large); 1012 mNotificationMaxHeight = getFontScaledHeight(R.dimen.notification_max_height); 1013 mNotificationAmbientHeight = getFontScaledHeight(R.dimen.notification_ambient_height); 1014 mMaxHeadsUpHeightLegacy = getFontScaledHeight( 1015 R.dimen.notification_max_heads_up_height_legacy); 1016 mMaxHeadsUpHeight = getFontScaledHeight(R.dimen.notification_max_heads_up_height); 1017 mIncreasedPaddingBetweenElements = getResources() 1018 .getDimensionPixelSize(R.dimen.notification_divider_height_increased); 1019 mIconTransformContentShiftNoIcon = getResources().getDimensionPixelSize( 1020 R.dimen.notification_icon_transform_content_shift); 1021 } 1022 1023 /** 1024 * @param dimenId the dimen to look up 1025 * @return the font scaled dimen as if it were in sp but doesn't shrink sizes below dp 1026 */ 1027 private int getFontScaledHeight(int dimenId) { 1028 int dimensionPixelSize = getResources().getDimensionPixelSize(dimenId); 1029 float factor = Math.max(1.0f, getResources().getDisplayMetrics().scaledDensity / 1030 getResources().getDisplayMetrics().density); 1031 return (int) (dimensionPixelSize * factor); 1032 } 1033 1034 /** 1035 * Resets this view so it can be re-used for an updated notification. 1036 */ 1037 @Override 1038 public void reset() { 1039 super.reset(); 1040 final boolean wasExpanded = isExpanded(); 1041 mExpandable = false; 1042 mHasUserChangedExpansion = false; 1043 mUserLocked = false; 1044 mShowingPublic = false; 1045 mSensitive = false; 1046 mShowingPublicInitialized = false; 1047 mIsSystemExpanded = false; 1048 mOnKeyguard = false; 1049 mPublicLayout.reset(); 1050 mPrivateLayout.reset(); 1051 resetHeight(); 1052 resetTranslation(); 1053 logExpansionEvent(false, wasExpanded); 1054 } 1055 1056 public void resetHeight() { 1057 onHeightReset(); 1058 requestLayout(); 1059 } 1060 1061 @Override 1062 protected void onFinishInflate() { 1063 super.onFinishInflate(); 1064 mPublicLayout = (NotificationContentView) findViewById(R.id.expandedPublic); 1065 mPrivateLayout = (NotificationContentView) findViewById(R.id.expanded); 1066 mLayouts = new NotificationContentView[] {mPrivateLayout, mPublicLayout}; 1067 1068 for (NotificationContentView l : mLayouts) { 1069 l.setExpandClickListener(mExpandClickListener); 1070 l.setContainingNotification(this); 1071 } 1072 mMenuRowStub = (ViewStub) findViewById(R.id.menu_row_stub); 1073 mMenuRowStub.setOnInflateListener(new ViewStub.OnInflateListener() { 1074 @Override 1075 public void onInflate(ViewStub stub, View inflated) { 1076 mMenuRow = (NotificationMenuRow) inflated; 1077 mMenuRow.setNotificationRowParent(ExpandableNotificationRow.this); 1078 mMenuRow.setAppName(mAppName); 1079 } 1080 }); 1081 mGutsStub = (ViewStub) findViewById(R.id.notification_guts_stub); 1082 mGutsStub.setOnInflateListener(new ViewStub.OnInflateListener() { 1083 @Override 1084 public void onInflate(ViewStub stub, View inflated) { 1085 mGuts = (NotificationGuts) inflated; 1086 mGuts.setClipTopAmount(getClipTopAmount()); 1087 mGuts.setActualHeight(getActualHeight()); 1088 mGutsStub = null; 1089 } 1090 }); 1091 mChildrenContainerStub = (ViewStub) findViewById(R.id.child_container_stub); 1092 mChildrenContainerStub.setOnInflateListener(new ViewStub.OnInflateListener() { 1093 1094 @Override 1095 public void onInflate(ViewStub stub, View inflated) { 1096 mChildrenContainer = (NotificationChildrenContainer) inflated; 1097 mChildrenContainer.setIsLowPriority(mIsLowPriority); 1098 mChildrenContainer.setNotificationParent(ExpandableNotificationRow.this); 1099 mChildrenContainer.onNotificationUpdated(); 1100 mTranslateableViews.add(mChildrenContainer); 1101 } 1102 }); 1103 1104 // Add the views that we translate to reveal the gear 1105 mTranslateableViews = new ArrayList<View>(); 1106 for (int i = 0; i < getChildCount(); i++) { 1107 mTranslateableViews.add(getChildAt(i)); 1108 } 1109 // Remove views that don't translate 1110 mTranslateableViews.remove(mMenuRowStub); 1111 mTranslateableViews.remove(mChildrenContainerStub); 1112 mTranslateableViews.remove(mGutsStub); 1113 } 1114 1115 public void resetTranslation() { 1116 if (mTranslateAnim != null) { 1117 mTranslateAnim.cancel(); 1118 } 1119 if (mTranslateableViews != null) { 1120 for (int i = 0; i < mTranslateableViews.size(); i++) { 1121 mTranslateableViews.get(i).setTranslationX(0); 1122 } 1123 } 1124 invalidateOutline(); 1125 if (mMenuRow != null) { 1126 mMenuRow.resetState(true /* notify */); 1127 } 1128 } 1129 1130 public void animateTranslateNotification(final float leftTarget) { 1131 if (mTranslateAnim != null) { 1132 mTranslateAnim.cancel(); 1133 } 1134 mTranslateAnim = getTranslateViewAnimator(leftTarget, null /* updateListener */); 1135 if (mTranslateAnim != null) { 1136 mTranslateAnim.start(); 1137 } 1138 } 1139 1140 @Override 1141 public void setTranslation(float translationX) { 1142 if (areGutsExposed()) { 1143 // Don't translate if guts are showing. 1144 return; 1145 } 1146 // Translate the group of views 1147 for (int i = 0; i < mTranslateableViews.size(); i++) { 1148 if (mTranslateableViews.get(i) != null) { 1149 mTranslateableViews.get(i).setTranslationX(translationX); 1150 } 1151 } 1152 invalidateOutline(); 1153 if (mMenuRow != null) { 1154 mMenuRow.updateMenuAlpha(translationX, getMeasuredWidth()); 1155 } 1156 } 1157 1158 @Override 1159 public float getTranslation() { 1160 if (mTranslateableViews != null && mTranslateableViews.size() > 0) { 1161 // All of the views in the list should have same translation, just use first one. 1162 return mTranslateableViews.get(0).getTranslationX(); 1163 } 1164 return 0; 1165 } 1166 1167 public Animator getTranslateViewAnimator(final float leftTarget, 1168 AnimatorUpdateListener listener) { 1169 if (mTranslateAnim != null) { 1170 mTranslateAnim.cancel(); 1171 } 1172 if (areGutsExposed()) { 1173 // No translation if guts are exposed. 1174 return null; 1175 } 1176 final ObjectAnimator translateAnim = ObjectAnimator.ofFloat(this, TRANSLATE_CONTENT, 1177 leftTarget); 1178 if (listener != null) { 1179 translateAnim.addUpdateListener(listener); 1180 } 1181 translateAnim.addListener(new AnimatorListenerAdapter() { 1182 boolean cancelled = false; 1183 1184 @Override 1185 public void onAnimationCancel(Animator anim) { 1186 cancelled = true; 1187 } 1188 1189 @Override 1190 public void onAnimationEnd(Animator anim) { 1191 if (!cancelled && mMenuRow != null && leftTarget == 0) { 1192 mMenuRow.resetState(true /* notify */); 1193 mTranslateAnim = null; 1194 } 1195 } 1196 }); 1197 mTranslateAnim = translateAnim; 1198 return translateAnim; 1199 } 1200 1201 public float getSpaceForGear() { 1202 if (mMenuRow != null) { 1203 return mMenuRow.getSpaceForMenu(); 1204 } 1205 return 0; 1206 } 1207 1208 public NotificationMenuRow getSettingsRow() { 1209 if (mMenuRow == null) { 1210 mMenuRowStub.inflate(); 1211 } 1212 return mMenuRow; 1213 } 1214 1215 public void inflateGuts() { 1216 if (mGuts == null) { 1217 mGutsStub.inflate(); 1218 } 1219 } 1220 1221 private void updateChildrenVisibility() { 1222 mPrivateLayout.setVisibility(!mShowingPublic && !mIsSummaryWithChildren ? VISIBLE 1223 : INVISIBLE); 1224 if (mChildrenContainer != null) { 1225 mChildrenContainer.setVisibility(!mShowingPublic && mIsSummaryWithChildren ? VISIBLE 1226 : INVISIBLE); 1227 mChildrenContainer.updateHeaderVisibility(!mShowingPublic && mIsSummaryWithChildren 1228 ? VISIBLE 1229 : INVISIBLE); 1230 } 1231 // The limits might have changed if the view suddenly became a group or vice versa 1232 updateLimits(); 1233 } 1234 1235 @Override 1236 public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) { 1237 if (super.onRequestSendAccessibilityEventInternal(child, event)) { 1238 // Add a record for the entire layout since its content is somehow small. 1239 // The event comes from a leaf view that is interacted with. 1240 AccessibilityEvent record = AccessibilityEvent.obtain(); 1241 onInitializeAccessibilityEvent(record); 1242 dispatchPopulateAccessibilityEvent(record); 1243 event.appendRecord(record); 1244 return true; 1245 } 1246 return false; 1247 } 1248 1249 @Override 1250 public void setDark(boolean dark, boolean fade, long delay) { 1251 super.setDark(dark, fade, delay); 1252 final NotificationContentView showing = getShowingLayout(); 1253 if (showing != null) { 1254 showing.setDark(dark, fade, delay); 1255 } 1256 if (mIsSummaryWithChildren) { 1257 mChildrenContainer.setDark(dark, fade, delay); 1258 } 1259 } 1260 1261 public boolean isExpandable() { 1262 if (mIsSummaryWithChildren && !mShowingPublic) { 1263 return !mChildrenExpanded; 1264 } 1265 return mExpandable; 1266 } 1267 1268 public void setExpandable(boolean expandable) { 1269 mExpandable = expandable; 1270 mPrivateLayout.updateExpandButtons(isExpandable()); 1271 } 1272 1273 @Override 1274 public void setClipToActualHeight(boolean clipToActualHeight) { 1275 super.setClipToActualHeight(clipToActualHeight || isUserLocked()); 1276 getShowingLayout().setClipToActualHeight(clipToActualHeight || isUserLocked()); 1277 } 1278 1279 /** 1280 * @return whether the user has changed the expansion state 1281 */ 1282 public boolean hasUserChangedExpansion() { 1283 return mHasUserChangedExpansion; 1284 } 1285 1286 public boolean isUserExpanded() { 1287 return mUserExpanded; 1288 } 1289 1290 /** 1291 * Set this notification to be expanded by the user 1292 * 1293 * @param userExpanded whether the user wants this notification to be expanded 1294 */ 1295 public void setUserExpanded(boolean userExpanded) { 1296 setUserExpanded(userExpanded, false /* allowChildExpansion */); 1297 updateShelfIconColor(); 1298 } 1299 1300 /** 1301 * Set this notification to be expanded by the user 1302 * 1303 * @param userExpanded whether the user wants this notification to be expanded 1304 * @param allowChildExpansion whether a call to this method allows expanding children 1305 */ 1306 public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) { 1307 mFalsingManager.setNotificationExpanded(); 1308 if (mIsSummaryWithChildren && !mShowingPublic && allowChildExpansion) { 1309 final boolean wasExpanded = mGroupManager.isGroupExpanded(mStatusBarNotification); 1310 mGroupManager.setGroupExpanded(mStatusBarNotification, userExpanded); 1311 logExpansionEvent(true /* userAction */, wasExpanded); 1312 return; 1313 } 1314 if (userExpanded && !mExpandable) return; 1315 final boolean wasExpanded = isExpanded(); 1316 mHasUserChangedExpansion = true; 1317 mUserExpanded = userExpanded; 1318 logExpansionEvent(true, wasExpanded); 1319 } 1320 1321 public void resetUserExpansion() { 1322 mHasUserChangedExpansion = false; 1323 mUserExpanded = false; 1324 } 1325 1326 public boolean isUserLocked() { 1327 return mUserLocked && !mForceUnlocked; 1328 } 1329 1330 public void setUserLocked(boolean userLocked) { 1331 mUserLocked = userLocked; 1332 mPrivateLayout.setUserExpanding(userLocked); 1333 if (mIsSummaryWithChildren) { 1334 mChildrenContainer.setUserLocked(userLocked); 1335 if (userLocked || !isGroupExpanded()) { 1336 updateBackgroundForGroupState(); 1337 } 1338 } 1339 } 1340 1341 /** 1342 * @return has the system set this notification to be expanded 1343 */ 1344 public boolean isSystemExpanded() { 1345 return mIsSystemExpanded; 1346 } 1347 1348 /** 1349 * Set this notification to be expanded by the system. 1350 * 1351 * @param expand whether the system wants this notification to be expanded. 1352 */ 1353 public void setSystemExpanded(boolean expand) { 1354 if (expand != mIsSystemExpanded) { 1355 final boolean wasExpanded = isExpanded(); 1356 mIsSystemExpanded = expand; 1357 updateShelfIconColor(); 1358 notifyHeightChanged(false /* needsAnimation */); 1359 logExpansionEvent(false, wasExpanded); 1360 if (mIsSummaryWithChildren) { 1361 mChildrenContainer.updateGroupOverflow(); 1362 } 1363 } 1364 } 1365 1366 /** 1367 * @param onKeyguard whether to prevent notification expansion 1368 */ 1369 public void setOnKeyguard(boolean onKeyguard) { 1370 if (onKeyguard != mOnKeyguard) { 1371 final boolean wasExpanded = isExpanded(); 1372 mOnKeyguard = onKeyguard; 1373 logExpansionEvent(false, wasExpanded); 1374 if (wasExpanded != isExpanded()) { 1375 if (mIsSummaryWithChildren) { 1376 mChildrenContainer.updateGroupOverflow(); 1377 } 1378 notifyHeightChanged(false /* needsAnimation */); 1379 } 1380 } 1381 } 1382 1383 /** 1384 * @return Can the underlying notification be cleared? This can be different from whether the 1385 * notification can be dismissed in case notifications are sensitive on the lockscreen. 1386 * @see #canViewBeDismissed() 1387 */ 1388 public boolean isClearable() { 1389 if (mStatusBarNotification == null || !mStatusBarNotification.isClearable()) { 1390 return false; 1391 } 1392 if (mIsSummaryWithChildren) { 1393 List<ExpandableNotificationRow> notificationChildren = 1394 mChildrenContainer.getNotificationChildren(); 1395 for (int i = 0; i < notificationChildren.size(); i++) { 1396 ExpandableNotificationRow child = notificationChildren.get(i); 1397 if (!child.isClearable()) { 1398 return false; 1399 } 1400 } 1401 } 1402 return true; 1403 } 1404 1405 @Override 1406 public int getIntrinsicHeight() { 1407 if (isUserLocked()) { 1408 return getActualHeight(); 1409 } 1410 if (mGuts != null && mGuts.isExposed()) { 1411 return mGuts.getHeight(); 1412 } else if ((isChildInGroup() && !isGroupExpanded())) { 1413 return mPrivateLayout.getMinHeight(); 1414 } else if (mShowAmbient) { 1415 return getAmbientHeight(); 1416 } else if (mSensitive && mHideSensitiveForIntrinsicHeight) { 1417 return getMinHeight(); 1418 } else if (mIsSummaryWithChildren && !mOnKeyguard) { 1419 return mChildrenContainer.getIntrinsicHeight(); 1420 } else if (!mOnKeyguard && (mIsHeadsUp || mHeadsupDisappearRunning)) { 1421 if (isPinned() || mHeadsupDisappearRunning) { 1422 return getPinnedHeadsUpHeight(true /* atLeastMinHeight */); 1423 } else if (isExpanded()) { 1424 return Math.max(getMaxExpandHeight(), mHeadsUpHeight); 1425 } else { 1426 return Math.max(getCollapsedHeight(), mHeadsUpHeight); 1427 } 1428 } else if (isExpanded()) { 1429 return getMaxExpandHeight(); 1430 } else { 1431 return getCollapsedHeight(); 1432 } 1433 } 1434 1435 @Override 1436 public boolean isGroupExpanded() { 1437 return mGroupManager.isGroupExpanded(mStatusBarNotification); 1438 } 1439 1440 private void onChildrenCountChanged() { 1441 mIsSummaryWithChildren = StatusBar.ENABLE_CHILD_NOTIFICATIONS 1442 && mChildrenContainer != null && mChildrenContainer.getNotificationChildCount() > 0; 1443 if (mIsSummaryWithChildren && mChildrenContainer.getHeaderView() == null) { 1444 mChildrenContainer.recreateNotificationHeader(mExpandClickListener, 1445 mEntry.notification); 1446 } 1447 getShowingLayout().updateBackgroundColor(false /* animate */); 1448 mPrivateLayout.updateExpandButtons(isExpandable()); 1449 updateChildrenHeaderAppearance(); 1450 updateChildrenVisibility(); 1451 } 1452 1453 public void updateChildrenHeaderAppearance() { 1454 if (mIsSummaryWithChildren) { 1455 mChildrenContainer.updateChildrenHeaderAppearance(); 1456 } 1457 } 1458 1459 /** 1460 * Check whether the view state is currently expanded. This is given by the system in {@link 1461 * #setSystemExpanded(boolean)} and can be overridden by user expansion or 1462 * collapsing in {@link #setUserExpanded(boolean)}. Note that the visual appearance of this 1463 * view can differ from this state, if layout params are modified from outside. 1464 * 1465 * @return whether the view state is currently expanded. 1466 */ 1467 public boolean isExpanded() { 1468 return isExpanded(false /* allowOnKeyguard */); 1469 } 1470 1471 public boolean isExpanded(boolean allowOnKeyguard) { 1472 return (!mOnKeyguard || allowOnKeyguard) 1473 && (!hasUserChangedExpansion() && (isSystemExpanded() || isSystemChildExpanded()) 1474 || isUserExpanded()); 1475 } 1476 1477 private boolean isSystemChildExpanded() { 1478 return mIsSystemChildExpanded; 1479 } 1480 1481 public void setSystemChildExpanded(boolean expanded) { 1482 mIsSystemChildExpanded = expanded; 1483 } 1484 1485 @Override 1486 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 1487 super.onLayout(changed, left, top, right, bottom); 1488 updateMaxHeights(); 1489 if (mMenuRow != null) { 1490 mMenuRow.updateVerticalLocation(); 1491 } 1492 updateContentShiftHeight(); 1493 } 1494 1495 /** 1496 * Updates the content shift height such that the header is completely hidden when coming from 1497 * the top. 1498 */ 1499 private void updateContentShiftHeight() { 1500 NotificationHeaderView notificationHeader = getNotificationHeader(); 1501 if (notificationHeader != null) { 1502 CachingIconView icon = notificationHeader.getIcon(); 1503 mIconTransformContentShift = getRelativeTopPadding(icon) + icon.getHeight(); 1504 } else { 1505 mIconTransformContentShift = mIconTransformContentShiftNoIcon; 1506 } 1507 } 1508 1509 private void updateMaxHeights() { 1510 int intrinsicBefore = getIntrinsicHeight(); 1511 View expandedChild = mPrivateLayout.getExpandedChild(); 1512 if (expandedChild == null) { 1513 expandedChild = mPrivateLayout.getContractedChild(); 1514 } 1515 mMaxExpandHeight = expandedChild.getHeight(); 1516 View headsUpChild = mPrivateLayout.getHeadsUpChild(); 1517 if (headsUpChild == null) { 1518 headsUpChild = mPrivateLayout.getContractedChild(); 1519 } 1520 mHeadsUpHeight = headsUpChild.getHeight(); 1521 if (intrinsicBefore != getIntrinsicHeight()) { 1522 notifyHeightChanged(true /* needsAnimation */); 1523 } 1524 } 1525 1526 @Override 1527 public void notifyHeightChanged(boolean needsAnimation) { 1528 super.notifyHeightChanged(needsAnimation); 1529 getShowingLayout().requestSelectLayout(needsAnimation || isUserLocked()); 1530 } 1531 1532 public void setSensitive(boolean sensitive, boolean hideSensitive) { 1533 mSensitive = sensitive; 1534 mSensitiveHiddenInGeneral = hideSensitive; 1535 } 1536 1537 @Override 1538 public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) { 1539 mHideSensitiveForIntrinsicHeight = hideSensitive; 1540 if (mIsSummaryWithChildren) { 1541 List<ExpandableNotificationRow> notificationChildren = 1542 mChildrenContainer.getNotificationChildren(); 1543 for (int i = 0; i < notificationChildren.size(); i++) { 1544 ExpandableNotificationRow child = notificationChildren.get(i); 1545 child.setHideSensitiveForIntrinsicHeight(hideSensitive); 1546 } 1547 } 1548 } 1549 1550 @Override 1551 public void setHideSensitive(boolean hideSensitive, boolean animated, long delay, 1552 long duration) { 1553 boolean oldShowingPublic = mShowingPublic; 1554 mShowingPublic = mSensitive && hideSensitive; 1555 if (mShowingPublicInitialized && mShowingPublic == oldShowingPublic) { 1556 return; 1557 } 1558 1559 // bail out if no public version 1560 if (mPublicLayout.getChildCount() == 0) return; 1561 1562 if (!animated) { 1563 mPublicLayout.animate().cancel(); 1564 mPrivateLayout.animate().cancel(); 1565 if (mChildrenContainer != null) { 1566 mChildrenContainer.animate().cancel(); 1567 mChildrenContainer.setAlpha(1f); 1568 } 1569 mPublicLayout.setAlpha(1f); 1570 mPrivateLayout.setAlpha(1f); 1571 mPublicLayout.setVisibility(mShowingPublic ? View.VISIBLE : View.INVISIBLE); 1572 updateChildrenVisibility(); 1573 } else { 1574 animateShowingPublic(delay, duration); 1575 } 1576 NotificationContentView showingLayout = getShowingLayout(); 1577 showingLayout.updateBackgroundColor(animated); 1578 mPrivateLayout.updateExpandButtons(isExpandable()); 1579 showingLayout.setDark(isDark(), false /* animate */, 0 /* delay */); 1580 mShowingPublicInitialized = true; 1581 } 1582 1583 private void animateShowingPublic(long delay, long duration) { 1584 View[] privateViews = mIsSummaryWithChildren 1585 ? new View[] {mChildrenContainer} 1586 : new View[] {mPrivateLayout}; 1587 View[] publicViews = new View[] {mPublicLayout}; 1588 View[] hiddenChildren = mShowingPublic ? privateViews : publicViews; 1589 View[] shownChildren = mShowingPublic ? publicViews : privateViews; 1590 for (final View hiddenView : hiddenChildren) { 1591 hiddenView.setVisibility(View.VISIBLE); 1592 hiddenView.animate().cancel(); 1593 hiddenView.animate() 1594 .alpha(0f) 1595 .setStartDelay(delay) 1596 .setDuration(duration) 1597 .withEndAction(new Runnable() { 1598 @Override 1599 public void run() { 1600 hiddenView.setVisibility(View.INVISIBLE); 1601 } 1602 }); 1603 } 1604 for (View showView : shownChildren) { 1605 showView.setVisibility(View.VISIBLE); 1606 showView.setAlpha(0f); 1607 showView.animate().cancel(); 1608 showView.animate() 1609 .alpha(1f) 1610 .setStartDelay(delay) 1611 .setDuration(duration); 1612 } 1613 } 1614 1615 @Override 1616 public boolean mustStayOnScreen() { 1617 return mIsHeadsUp; 1618 } 1619 1620 /** 1621 * @return Whether this view is allowed to be dismissed. Only valid for visible notifications as 1622 * otherwise some state might not be updated. To request about the general clearability 1623 * see {@link #isClearable()}. 1624 */ 1625 public boolean canViewBeDismissed() { 1626 return isClearable() && (!mShowingPublic || !mSensitiveHiddenInGeneral); 1627 } 1628 1629 public void makeActionsVisibile() { 1630 setUserExpanded(true, true); 1631 if (isChildInGroup()) { 1632 mGroupManager.setGroupExpanded(mStatusBarNotification, true); 1633 } 1634 notifyHeightChanged(false /* needsAnimation */); 1635 } 1636 1637 public void setChildrenExpanded(boolean expanded, boolean animate) { 1638 mChildrenExpanded = expanded; 1639 if (mChildrenContainer != null) { 1640 mChildrenContainer.setChildrenExpanded(expanded); 1641 } 1642 updateBackgroundForGroupState(); 1643 updateClickAndFocus(); 1644 } 1645 1646 public static void applyTint(View v, int color) { 1647 int alpha; 1648 if (color != 0) { 1649 alpha = COLORED_DIVIDER_ALPHA; 1650 } else { 1651 color = 0xff000000; 1652 alpha = DEFAULT_DIVIDER_ALPHA; 1653 } 1654 if (v.getBackground() instanceof ColorDrawable) { 1655 ColorDrawable background = (ColorDrawable) v.getBackground(); 1656 background.mutate(); 1657 background.setColor(color); 1658 background.setAlpha(alpha); 1659 } 1660 } 1661 1662 public int getMaxExpandHeight() { 1663 return mMaxExpandHeight; 1664 } 1665 1666 public boolean areGutsExposed() { 1667 return (mGuts != null && mGuts.isExposed()); 1668 } 1669 1670 @Override 1671 public boolean isContentExpandable() { 1672 NotificationContentView showingLayout = getShowingLayout(); 1673 return showingLayout.isContentExpandable(); 1674 } 1675 1676 @Override 1677 protected View getContentView() { 1678 if (mIsSummaryWithChildren && !mShowingPublic) { 1679 return mChildrenContainer; 1680 } 1681 return getShowingLayout(); 1682 } 1683 1684 @Override 1685 protected void onAppearAnimationFinished(boolean wasAppearing) { 1686 super.onAppearAnimationFinished(wasAppearing); 1687 if (wasAppearing) { 1688 // During the animation the visible view might have changed, so let's make sure all 1689 // alphas are reset 1690 if (mChildrenContainer != null) { 1691 mChildrenContainer.setAlpha(1.0f); 1692 mChildrenContainer.setLayerType(LAYER_TYPE_NONE, null); 1693 } 1694 for (NotificationContentView l : mLayouts) { 1695 l.setAlpha(1.0f); 1696 l.setLayerType(LAYER_TYPE_NONE, null); 1697 } 1698 } 1699 } 1700 1701 @Override 1702 public int getExtraBottomPadding() { 1703 if (mIsSummaryWithChildren && isGroupExpanded()) { 1704 return mIncreasedPaddingBetweenElements; 1705 } 1706 return 0; 1707 } 1708 1709 @Override 1710 public void setActualHeight(int height, boolean notifyListeners) { 1711 super.setActualHeight(height, notifyListeners); 1712 if (mGuts != null && mGuts.isExposed()) { 1713 mGuts.setActualHeight(height); 1714 return; 1715 } 1716 int contentHeight = Math.max(getMinHeight(), height); 1717 for (NotificationContentView l : mLayouts) { 1718 l.setContentHeight(contentHeight); 1719 } 1720 if (mIsSummaryWithChildren) { 1721 mChildrenContainer.setActualHeight(height); 1722 } 1723 if (mGuts != null) { 1724 mGuts.setActualHeight(height); 1725 } 1726 } 1727 1728 @Override 1729 public int getMaxContentHeight() { 1730 if (mIsSummaryWithChildren && !mShowingPublic) { 1731 return mChildrenContainer.getMaxContentHeight(); 1732 } 1733 NotificationContentView showingLayout = getShowingLayout(); 1734 return showingLayout.getMaxHeight(); 1735 } 1736 1737 @Override 1738 public int getMinHeight() { 1739 if (!mOnKeyguard && mIsHeadsUp && mHeadsUpManager.isTrackingHeadsUp()) { 1740 return getPinnedHeadsUpHeight(false /* atLeastMinHeight */); 1741 } else if (mIsSummaryWithChildren && !isGroupExpanded() && !mShowingPublic) { 1742 return mChildrenContainer.getMinHeight(); 1743 } else if (!mOnKeyguard && mIsHeadsUp) { 1744 return mHeadsUpHeight; 1745 } 1746 NotificationContentView showingLayout = getShowingLayout(); 1747 return showingLayout.getMinHeight(); 1748 } 1749 1750 private int getAmbientHeight() { 1751 NotificationContentView showingLayout = getShowingLayout(); 1752 return showingLayout.getAmbientChild() != null 1753 ? showingLayout.getAmbientChild().getHeight() 1754 : getCollapsedHeight(); 1755 } 1756 1757 @Override 1758 public int getCollapsedHeight() { 1759 if (mIsSummaryWithChildren && !mShowingPublic) { 1760 return mChildrenContainer.getCollapsedHeight(); 1761 } 1762 return getMinHeight(); 1763 } 1764 1765 @Override 1766 public void setClipTopAmount(int clipTopAmount) { 1767 super.setClipTopAmount(clipTopAmount); 1768 for (NotificationContentView l : mLayouts) { 1769 l.setClipTopAmount(clipTopAmount); 1770 } 1771 if (mGuts != null) { 1772 mGuts.setClipTopAmount(clipTopAmount); 1773 } 1774 } 1775 1776 @Override 1777 public void setClipBottomAmount(int clipBottomAmount) { 1778 if (clipBottomAmount != mClipBottomAmount) { 1779 super.setClipBottomAmount(clipBottomAmount); 1780 for (NotificationContentView l : mLayouts) { 1781 l.setClipBottomAmount(clipBottomAmount); 1782 } 1783 if (mGuts != null) { 1784 mGuts.setClipBottomAmount(clipBottomAmount); 1785 } 1786 } 1787 if (mChildrenContainer != null) { 1788 // We have to update this even if it hasn't changed, since the children locations can 1789 // have changed 1790 mChildrenContainer.setClipBottomAmount(clipBottomAmount); 1791 } 1792 } 1793 1794 public boolean isMaxExpandHeightInitialized() { 1795 return mMaxExpandHeight != 0; 1796 } 1797 1798 public NotificationContentView getShowingLayout() { 1799 return mShowingPublic ? mPublicLayout : mPrivateLayout; 1800 } 1801 1802 public void setShowingLegacyBackground(boolean showing) { 1803 for (NotificationContentView l : mLayouts) { 1804 l.setShowingLegacyBackground(showing); 1805 } 1806 } 1807 1808 @Override 1809 protected void updateBackgroundTint() { 1810 super.updateBackgroundTint(); 1811 updateBackgroundForGroupState(); 1812 if (mIsSummaryWithChildren) { 1813 List<ExpandableNotificationRow> notificationChildren = 1814 mChildrenContainer.getNotificationChildren(); 1815 for (int i = 0; i < notificationChildren.size(); i++) { 1816 ExpandableNotificationRow child = notificationChildren.get(i); 1817 child.updateBackgroundForGroupState(); 1818 } 1819 } 1820 } 1821 1822 /** 1823 * Called when a group has finished animating from collapsed or expanded state. 1824 */ 1825 public void onFinishedExpansionChange() { 1826 mGroupExpansionChanging = false; 1827 updateBackgroundForGroupState(); 1828 } 1829 1830 /** 1831 * Updates the parent and children backgrounds in a group based on the expansion state. 1832 */ 1833 public void updateBackgroundForGroupState() { 1834 if (mIsSummaryWithChildren) { 1835 // Only when the group has finished expanding do we hide its background. 1836 mShowNoBackground = isGroupExpanded() && !isGroupExpansionChanging() && !isUserLocked(); 1837 mChildrenContainer.updateHeaderForExpansion(mShowNoBackground); 1838 List<ExpandableNotificationRow> children = mChildrenContainer.getNotificationChildren(); 1839 for (int i = 0; i < children.size(); i++) { 1840 children.get(i).updateBackgroundForGroupState(); 1841 } 1842 } else if (isChildInGroup()) { 1843 final int childColor = getShowingLayout().getBackgroundColorForExpansionState(); 1844 // Only show a background if the group is expanded OR if it is expanding / collapsing 1845 // and has a custom background color 1846 final boolean showBackground = isGroupExpanded() 1847 || ((mNotificationParent.isGroupExpansionChanging() 1848 || mNotificationParent.isUserLocked()) && childColor != 0); 1849 mShowNoBackground = !showBackground; 1850 } else { 1851 // Only children or parents ever need no background. 1852 mShowNoBackground = false; 1853 } 1854 updateOutline(); 1855 updateBackground(); 1856 } 1857 1858 public int getPositionOfChild(ExpandableNotificationRow childRow) { 1859 if (mIsSummaryWithChildren) { 1860 return mChildrenContainer.getPositionInLinearLayout(childRow); 1861 } 1862 return 0; 1863 } 1864 1865 public void setExpansionLogger(ExpansionLogger logger, String key) { 1866 mLogger = logger; 1867 mLoggingKey = key; 1868 } 1869 1870 public void onExpandedByGesture(boolean userExpanded) { 1871 int event = MetricsEvent.ACTION_NOTIFICATION_GESTURE_EXPANDER; 1872 if (mGroupManager.isSummaryOfGroup(getStatusBarNotification())) { 1873 event = MetricsEvent.ACTION_NOTIFICATION_GROUP_GESTURE_EXPANDER; 1874 } 1875 MetricsLogger.action(mContext, event, userExpanded); 1876 } 1877 1878 @Override 1879 public float getIncreasedPaddingAmount() { 1880 if (mIsSummaryWithChildren) { 1881 if (isGroupExpanded()) { 1882 return 1.0f; 1883 } else if (isUserLocked()) { 1884 return mChildrenContainer.getGroupExpandFraction(); 1885 } 1886 } else if (isColorized() && (!mIsLowPriority || isExpanded())) { 1887 return -1.0f; 1888 } 1889 return 0.0f; 1890 } 1891 1892 private boolean isColorized() { 1893 return mIsColorized && mBgTint != NO_COLOR; 1894 } 1895 1896 @Override 1897 protected boolean disallowSingleClick(MotionEvent event) { 1898 float x = event.getX(); 1899 float y = event.getY(); 1900 NotificationHeaderView header = getVisibleNotificationHeader(); 1901 if (header != null) { 1902 return header.isInTouchRect(x - getTranslation(), y); 1903 } 1904 return super.disallowSingleClick(event); 1905 } 1906 1907 private void logExpansionEvent(boolean userAction, boolean wasExpanded) { 1908 boolean nowExpanded = isExpanded(); 1909 if (mIsSummaryWithChildren) { 1910 nowExpanded = mGroupManager.isGroupExpanded(mStatusBarNotification); 1911 } 1912 if (wasExpanded != nowExpanded && mLogger != null) { 1913 mLogger.logNotificationExpansion(mLoggingKey, userAction, nowExpanded) ; 1914 } 1915 } 1916 1917 @Override 1918 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 1919 super.onInitializeAccessibilityNodeInfoInternal(info); 1920 if (canViewBeDismissed()) { 1921 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS); 1922 } 1923 } 1924 1925 @Override 1926 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 1927 if (super.performAccessibilityActionInternal(action, arguments)) { 1928 return true; 1929 } 1930 switch (action) { 1931 case AccessibilityNodeInfo.ACTION_DISMISS: 1932 NotificationStackScrollLayout.performDismiss(this, mGroupManager, 1933 true /* fromAccessibility */); 1934 return true; 1935 } 1936 return false; 1937 } 1938 1939 public boolean shouldRefocusOnDismiss() { 1940 return mRefocusOnDismiss || isAccessibilityFocused(); 1941 } 1942 1943 public interface OnExpandClickListener { 1944 void onExpandClicked(NotificationData.Entry clickedEntry, boolean nowExpanded); 1945 } 1946 1947 @Override 1948 public ExpandableViewState createNewViewState(StackScrollState stackScrollState) { 1949 return new NotificationViewState(stackScrollState); 1950 } 1951 1952 @Override 1953 public boolean isAboveShelf() { 1954 return mIsPinned || mHeadsupDisappearRunning || (mIsHeadsUp && mAboveShelf); 1955 } 1956 1957 public void setShowAmbient(boolean showAmbient) { 1958 if (showAmbient != mShowAmbient) { 1959 mShowAmbient = showAmbient; 1960 notifyHeightChanged(false /* needsAnimation */); 1961 } 1962 } 1963 1964 public void setAboveShelf(boolean aboveShelf) { 1965 mAboveShelf = aboveShelf; 1966 } 1967 1968 public class NotificationViewState extends ExpandableViewState { 1969 1970 private final StackScrollState mOverallState; 1971 1972 1973 private NotificationViewState(StackScrollState stackScrollState) { 1974 mOverallState = stackScrollState; 1975 } 1976 1977 @Override 1978 public void applyToView(View view) { 1979 super.applyToView(view); 1980 if (view instanceof ExpandableNotificationRow) { 1981 ExpandableNotificationRow row = (ExpandableNotificationRow) view; 1982 row.applyChildrenState(mOverallState); 1983 } 1984 } 1985 1986 @Override 1987 protected void onYTranslationAnimationFinished(View view) { 1988 super.onYTranslationAnimationFinished(view); 1989 if (mHeadsupDisappearRunning) { 1990 setHeadsUpAnimatingAway(false); 1991 } 1992 } 1993 1994 @Override 1995 public void animateTo(View child, AnimationProperties properties) { 1996 super.animateTo(child, properties); 1997 if (child instanceof ExpandableNotificationRow) { 1998 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 1999 row.startChildAnimation(mOverallState, properties); 2000 } 2001 } 2002 } 2003} 2004