1 /* 2 * Copyright (C) 2015 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.stack; 18 19 import android.app.Notification; 20 import android.content.Context; 21 import android.content.res.Configuration; 22 import android.content.res.Resources; 23 import android.content.res.TypedArray; 24 import android.graphics.Canvas; 25 import android.graphics.Path; 26 import android.graphics.Path.Direction; 27 import android.graphics.drawable.ColorDrawable; 28 import android.os.Trace; 29 import android.service.notification.StatusBarNotification; 30 import android.util.AttributeSet; 31 import android.util.Log; 32 import android.view.ContextThemeWrapper; 33 import android.view.LayoutInflater; 34 import android.view.NotificationHeaderView; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.widget.RemoteViews; 38 import android.widget.TextView; 39 40 import androidx.annotation.NonNull; 41 import androidx.annotation.Nullable; 42 43 import com.android.internal.annotations.VisibleForTesting; 44 import com.android.internal.widget.NotificationExpandButton; 45 import com.android.systemui.res.R; 46 import com.android.systemui.statusbar.CrossFadeHelper; 47 import com.android.systemui.statusbar.NotificationGroupingUtil; 48 import com.android.systemui.statusbar.notification.FeedbackIcon; 49 import com.android.systemui.statusbar.notification.NotificationFadeAware; 50 import com.android.systemui.statusbar.notification.NotificationUtils; 51 import com.android.systemui.statusbar.notification.Roundable; 52 import com.android.systemui.statusbar.notification.RoundableState; 53 import com.android.systemui.statusbar.notification.SourceType; 54 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 55 import com.android.systemui.statusbar.notification.row.ExpandableView; 56 import com.android.systemui.statusbar.notification.row.HybridGroupManager; 57 import com.android.systemui.statusbar.notification.row.HybridNotificationView; 58 import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; 59 import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; 60 import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper; 61 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; 62 63 import java.util.ArrayList; 64 import java.util.List; 65 66 /** 67 * A container containing child notifications 68 */ 69 public class NotificationChildrenContainer extends ViewGroup 70 implements NotificationFadeAware, Roundable { 71 72 private static final String TAG = "NotificationChildrenContainer"; 73 74 @VisibleForTesting 75 static final int NUMBER_OF_CHILDREN_WHEN_COLLAPSED = 2; 76 @VisibleForTesting 77 static final int NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED = 5; 78 public static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8; 79 private static final AnimationProperties ALPHA_FADE_IN = new AnimationProperties() { 80 private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha(); 81 82 @Override 83 public AnimationFilter getAnimationFilter() { 84 return mAnimationFilter; 85 } 86 }.setDuration(200); 87 private static final SourceType FROM_PARENT = SourceType.from("FromParent(NCC)"); 88 89 private final List<View> mDividers = new ArrayList<>(); 90 private final List<ExpandableNotificationRow> mAttachedChildren = new ArrayList<>(); 91 private final HybridGroupManager mHybridGroupManager; 92 private int mChildPadding; 93 private int mDividerHeight; 94 private float mDividerAlpha; 95 private int mNotificationHeaderMargin; 96 97 private int mNotificationTopPadding; 98 private float mCollapsedBottomPadding; 99 private boolean mChildrenExpanded; 100 private ExpandableNotificationRow mContainingNotification; 101 private TextView mOverflowNumber; 102 private ViewState mGroupOverFlowState; 103 private int mRealHeight; 104 private boolean mUserLocked; 105 private int mActualHeight; 106 private boolean mNeverAppliedGroupState; 107 private int mHeaderHeight; 108 109 /** 110 * Whether or not individual notifications that are part of this container will have shadows. 111 */ 112 private boolean mEnableShadowOnChildNotifications; 113 114 private NotificationHeaderView mGroupHeader; 115 private NotificationHeaderViewWrapper mGroupHeaderWrapper; 116 private NotificationHeaderView mMinimizedGroupHeader; 117 private NotificationHeaderViewWrapper mMinimizedGroupHeaderWrapper; 118 private NotificationGroupingUtil mGroupingUtil; 119 private ViewState mHeaderViewState; 120 private int mClipBottomAmount; 121 private boolean mIsMinimized; 122 private OnClickListener mHeaderClickListener; 123 private ViewGroup mCurrentHeader; 124 private boolean mIsConversation; 125 private Path mChildClipPath = null; 126 private final Path mHeaderPath = new Path(); 127 private boolean mShowGroupCountInExpander; 128 private boolean mShowDividersWhenExpanded; 129 private boolean mHideDividersDuringExpand; 130 private int mTranslationForHeader; 131 private int mCurrentHeaderTranslation = 0; 132 private float mHeaderVisibleAmount = 1.0f; 133 private int mUntruncatedChildCount; 134 private boolean mContainingNotificationIsFaded = false; 135 private RoundableState mRoundableState; 136 private int mMinSingleLineHeight; 137 138 private NotificationChildrenContainerLogger mLogger; 139 NotificationChildrenContainer(Context context)140 public NotificationChildrenContainer(Context context) { 141 this(context, null); 142 } 143 NotificationChildrenContainer(Context context, AttributeSet attrs)144 public NotificationChildrenContainer(Context context, AttributeSet attrs) { 145 this(context, attrs, 0); 146 } 147 NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr)148 public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr) { 149 this(context, attrs, defStyleAttr, 0); 150 } 151 NotificationChildrenContainer( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)152 public NotificationChildrenContainer( 153 Context context, 154 AttributeSet attrs, 155 int defStyleAttr, 156 int defStyleRes) { 157 super(context, attrs, defStyleAttr, defStyleRes); 158 mHybridGroupManager = new HybridGroupManager(getContext()); 159 mRoundableState = new RoundableState(this, this, 0f); 160 initDimens(); 161 setClipChildren(false); 162 } 163 initDimens()164 private void initDimens() { 165 Resources res = getResources(); 166 mChildPadding = res.getDimensionPixelOffset(R.dimen.notification_children_padding); 167 mDividerHeight = res.getDimensionPixelOffset( 168 R.dimen.notification_children_container_divider_height); 169 mDividerAlpha = res.getFloat(R.dimen.notification_divider_alpha); 170 mNotificationHeaderMargin = res.getDimensionPixelOffset( 171 R.dimen.notification_children_container_margin_top); 172 mNotificationTopPadding = res.getDimensionPixelOffset( 173 R.dimen.notification_children_container_top_padding); 174 mHeaderHeight = mNotificationHeaderMargin + mNotificationTopPadding; 175 mCollapsedBottomPadding = res.getDimensionPixelOffset( 176 R.dimen.notification_children_collapsed_bottom_padding); 177 mEnableShadowOnChildNotifications = 178 res.getBoolean(R.bool.config_enableShadowOnChildNotifications); 179 mShowGroupCountInExpander = 180 res.getBoolean(R.bool.config_showNotificationGroupCountInExpander); 181 mShowDividersWhenExpanded = 182 res.getBoolean(R.bool.config_showDividersWhenGroupNotificationExpanded); 183 mHideDividersDuringExpand = 184 res.getBoolean(R.bool.config_hideDividersDuringExpand); 185 mTranslationForHeader = res.getDimensionPixelOffset( 186 com.android.internal.R.dimen.notification_content_margin) 187 - mNotificationHeaderMargin; 188 mHybridGroupManager.initDimens(); 189 mMinSingleLineHeight = getResources().getDimensionPixelSize( 190 R.dimen.conversation_single_line_face_pile_size); 191 } 192 193 @NonNull 194 @Override getRoundableState()195 public RoundableState getRoundableState() { 196 return mRoundableState; 197 } 198 199 @Override getClipHeight()200 public int getClipHeight() { 201 return Math.max(mActualHeight - mClipBottomAmount, 0); 202 } 203 204 @Override onLayout(boolean changed, int l, int t, int r, int b)205 protected void onLayout(boolean changed, int l, int t, int r, int b) { 206 int childCount = 207 Math.min(mAttachedChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED); 208 for (int i = 0; i < childCount; i++) { 209 View child = mAttachedChildren.get(i); 210 // We need to layout all children even the GONE ones, such that the heights are 211 // calculated correctly as they are used to calculate how many we can fit on the screen 212 child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); 213 mDividers.get(i).layout(0, 0, getWidth(), mDividerHeight); 214 } 215 if (mOverflowNumber != null) { 216 boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; 217 int left = (isRtl ? 0 : getWidth() - mOverflowNumber.getMeasuredWidth()); 218 int right = left + mOverflowNumber.getMeasuredWidth(); 219 mOverflowNumber.layout(left, 0, right, mOverflowNumber.getMeasuredHeight()); 220 } 221 if (mGroupHeader != null) { 222 mGroupHeader.layout(0, 0, mGroupHeader.getMeasuredWidth(), 223 mGroupHeader.getMeasuredHeight()); 224 } 225 if (mMinimizedGroupHeader != null) { 226 mMinimizedGroupHeader.layout(0, 0, 227 mMinimizedGroupHeader.getMeasuredWidth(), 228 mMinimizedGroupHeader.getMeasuredHeight()); 229 } 230 } 231 232 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)233 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 234 Trace.beginSection("NotificationChildrenContainer#onMeasure"); 235 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 236 boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; 237 boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; 238 int size = MeasureSpec.getSize(heightMeasureSpec); 239 int newHeightSpec = heightMeasureSpec; 240 if (hasFixedHeight || isHeightLimited) { 241 newHeightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST); 242 } 243 int width = MeasureSpec.getSize(widthMeasureSpec); 244 if (mOverflowNumber != null) { 245 mOverflowNumber.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), 246 newHeightSpec); 247 } 248 int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY); 249 int height = mNotificationHeaderMargin + mNotificationTopPadding; 250 int childCount = 251 Math.min(mAttachedChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED); 252 int collapsedChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */); 253 int overflowIndex = childCount > collapsedChildren ? collapsedChildren - 1 : -1; 254 for (int i = 0; i < childCount; i++) { 255 ExpandableNotificationRow child = mAttachedChildren.get(i); 256 // We need to measure all children even the GONE ones, such that the heights are 257 // calculated correctly as they are used to calculate how many we can fit on the screen. 258 boolean isOverflow = i == overflowIndex; 259 child.setSingleLineWidthIndention(isOverflow && mOverflowNumber != null 260 ? mOverflowNumber.getMeasuredWidth() : 0); 261 child.measure(widthMeasureSpec, newHeightSpec); 262 // layout the divider 263 View divider = mDividers.get(i); 264 divider.measure(widthMeasureSpec, dividerHeightSpec); 265 if (child.getVisibility() != GONE) { 266 height += child.getMeasuredHeight() + mDividerHeight; 267 } 268 } 269 mRealHeight = height; 270 if (heightMode != MeasureSpec.UNSPECIFIED) { 271 height = Math.min(height, size); 272 } 273 274 int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY); 275 if (mGroupHeader != null) { 276 mGroupHeader.measure(widthMeasureSpec, headerHeightSpec); 277 } 278 if (mMinimizedGroupHeader != null) { 279 mMinimizedGroupHeader.measure(widthMeasureSpec, headerHeightSpec); 280 } 281 282 setMeasuredDimension(width, height); 283 Trace.endSection(); 284 } 285 286 @Override hasOverlappingRendering()287 public boolean hasOverlappingRendering() { 288 return false; 289 } 290 291 @Override pointInView(float localX, float localY, float slop)292 public boolean pointInView(float localX, float localY, float slop) { 293 return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) && 294 localY < (mRealHeight + slop); 295 } 296 297 /** 298 * Set the untruncated number of children in the group so that the view can update the UI 299 * appropriately. Note that this may differ from the number of views attached as truncated 300 * children will not have views. 301 */ setUntruncatedChildCount(int childCount)302 public void setUntruncatedChildCount(int childCount) { 303 mUntruncatedChildCount = childCount; 304 updateGroupOverflow(); 305 } 306 307 /** 308 * Set the notification time in the group so that the view can show the latest event in the UI 309 * appropriately. 310 */ setNotificationGroupWhen(long whenMillis)311 public void setNotificationGroupWhen(long whenMillis) { 312 if (mGroupHeaderWrapper != null) { 313 mGroupHeaderWrapper.setNotificationWhen(whenMillis); 314 } 315 if (mMinimizedGroupHeaderWrapper != null) { 316 mMinimizedGroupHeaderWrapper.setNotificationWhen(whenMillis); 317 } 318 } 319 320 /** 321 * Add a child notification to this view. 322 * 323 * @param row the row to add 324 * @param childIndex the index to add it at, if -1 it will be added at the end 325 */ addNotification(ExpandableNotificationRow row, int childIndex)326 public void addNotification(ExpandableNotificationRow row, int childIndex) { 327 ensureRemovedFromTransientContainer(row); 328 int newIndex = childIndex < 0 ? mAttachedChildren.size() : childIndex; 329 mAttachedChildren.add(newIndex, row); 330 addView(row); 331 row.setUserLocked(mUserLocked); 332 333 View divider = inflateDivider(); 334 addView(divider); 335 mDividers.add(newIndex, divider); 336 337 row.setContentTransformationAmount(0, false /* isLastChild */); 338 row.setNotificationFaded(mContainingNotificationIsFaded); 339 340 // It doesn't make sense to keep old animations around, lets cancel them! 341 ExpandableViewState viewState = row.getViewState(); 342 if (viewState != null) { 343 viewState.cancelAnimations(row); 344 row.cancelAppearDrawing(); 345 } 346 347 applyRoundnessAndInvalidate(); 348 } 349 350 private void ensureRemovedFromTransientContainer(View v) { 351 if (v.getParent() != null && v instanceof ExpandableView) { 352 // If the child is animating away, it will still have a parent, so detach it first 353 // TODO: We should really cancel the active animations here. This will 354 // happen automatically when the view's intro animation starts, but 355 // it's a fragile link. 356 ((ExpandableView) v).removeFromTransientContainerForAdditionTo(this); 357 } 358 } 359 360 public void removeNotification(ExpandableNotificationRow row) { 361 int childIndex = mAttachedChildren.indexOf(row); 362 mAttachedChildren.remove(row); 363 removeView(row); 364 365 final View divider = mDividers.remove(childIndex); 366 removeView(divider); 367 getOverlay().add(divider); 368 CrossFadeHelper.fadeOut(divider, new Runnable() { 369 @Override 370 public void run() { 371 getOverlay().remove(divider); 372 } 373 }); 374 375 row.setSystemChildExpanded(false); 376 row.setNotificationFaded(false); 377 row.setUserLocked(false); 378 if (!row.isRemoved()) { 379 mGroupingUtil.restoreChildNotification(row); 380 } 381 382 row.requestRoundnessReset(FROM_PARENT, /* animate = */ false); 383 applyRoundnessAndInvalidate(); 384 } 385 386 /** 387 * @return The number of notification children in the container. 388 */ 389 public int getNotificationChildCount() { 390 return mAttachedChildren.size(); 391 } 392 393 /** 394 * Re-create the Notification header view 395 * @param listener OnClickListener of the header view 396 * @param isConversation if the notification group is a conversation group 397 */ 398 public void recreateNotificationHeader( 399 OnClickListener listener, 400 boolean isConversation 401 ) { 402 // We don't want to inflate headers from the main thread when async inflation enabled 403 AsyncGroupHeaderViewInflation.assertInLegacyMode(); 404 // TODO(b/217799515): remove traces from this function in a follow-up change 405 Trace.beginSection("NotifChildCont#recreateHeader"); 406 mHeaderClickListener = listener; 407 mIsConversation = isConversation; 408 StatusBarNotification notification = mContainingNotification.getEntry().getSbn(); 409 final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(), 410 notification.getNotification()); 411 Trace.beginSection("recreateHeader#makeNotificationGroupHeader"); 412 RemoteViews header = builder.makeNotificationGroupHeader(); 413 Trace.endSection(); 414 if (mGroupHeader == null) { 415 Trace.beginSection("recreateHeader#apply"); 416 mGroupHeader = (NotificationHeaderView) header.apply(getContext(), this); 417 Trace.endSection(); 418 mGroupHeader.findViewById(com.android.internal.R.id.expand_button) 419 .setVisibility(VISIBLE); 420 mGroupHeader.setOnClickListener(mHeaderClickListener); 421 mGroupHeaderWrapper = 422 (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( 423 getContext(), 424 mGroupHeader, 425 mContainingNotification); 426 mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); 427 addView(mGroupHeader, 0); 428 invalidate(); 429 } else { 430 Trace.beginSection("recreateHeader#reapply"); 431 header.reapply(getContext(), mGroupHeader); 432 Trace.endSection(); 433 } 434 mGroupHeaderWrapper.setExpanded(mChildrenExpanded); 435 mGroupHeaderWrapper.onContentUpdated(mContainingNotification); 436 recreateLowPriorityHeader(builder, isConversation); 437 updateHeaderVisibility(false /* animate */); 438 updateChildrenAppearance(); 439 Trace.endSection(); 440 } 441 442 /** 443 * Update the expand state of the group header. 444 */ 445 public void updateGroupHeaderExpandState() { 446 if (mGroupHeaderWrapper != null) { 447 mGroupHeaderWrapper.setExpanded(mChildrenExpanded); 448 } 449 } 450 451 private void removeGroupHeader() { 452 if (mGroupHeader == null) { 453 return; 454 } 455 removeView(mGroupHeader); 456 mGroupHeader = null; 457 mGroupHeaderWrapper = null; 458 } 459 460 private void removeLowPriorityGroupHeader() { 461 if (mMinimizedGroupHeader == null) { 462 return; 463 } 464 removeView(mMinimizedGroupHeader); 465 mMinimizedGroupHeader = null; 466 mMinimizedGroupHeaderWrapper = null; 467 } 468 469 /** 470 * Set the group header view 471 * @param headerView view to set 472 * @param onClickListener OnClickListener of the header view 473 */ 474 public void setGroupHeader( 475 NotificationHeaderView headerView, 476 OnClickListener onClickListener 477 ) { 478 if (AsyncGroupHeaderViewInflation.isUnexpectedlyInLegacyMode()) return; 479 mHeaderClickListener = onClickListener; 480 481 removeGroupHeader(); 482 483 if (headerView == null) { 484 return; 485 } 486 487 mGroupHeader = headerView; 488 mGroupHeader.findViewById(com.android.internal.R.id.expand_button) 489 .setVisibility(VISIBLE); 490 mGroupHeader.setOnClickListener(mHeaderClickListener); 491 mGroupHeaderWrapper = 492 (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( 493 getContext(), 494 mGroupHeader, 495 mContainingNotification); 496 mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); 497 addView(mGroupHeader, 0); 498 invalidate(); 499 500 mGroupHeaderWrapper.setExpanded(mChildrenExpanded); 501 mGroupHeaderWrapper.onContentUpdated(mContainingNotification); 502 503 updateHeaderVisibility(false /* animate */); 504 updateChildrenAppearance(); 505 506 Trace.endSection(); 507 } 508 509 /** 510 * Set the low-priority group header view 511 * @param headerViewLowPriority header view to set 512 * @param onClickListener OnClickListener of the header view 513 */ 514 public void setLowPriorityGroupHeader( 515 NotificationHeaderView headerViewLowPriority, 516 OnClickListener onClickListener 517 ) { 518 if (AsyncGroupHeaderViewInflation.isUnexpectedlyInLegacyMode()) return; 519 removeLowPriorityGroupHeader(); 520 if (headerViewLowPriority == null) { 521 return; 522 } 523 524 mMinimizedGroupHeader = headerViewLowPriority; 525 mMinimizedGroupHeader.findViewById(com.android.internal.R.id.expand_button) 526 .setVisibility(VISIBLE); 527 mMinimizedGroupHeader.setOnClickListener(onClickListener); 528 mMinimizedGroupHeaderWrapper = 529 (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( 530 getContext(), 531 mMinimizedGroupHeader, 532 mContainingNotification); 533 mMinimizedGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); 534 addView(mMinimizedGroupHeader, 0); 535 invalidate(); 536 537 mMinimizedGroupHeaderWrapper.onContentUpdated(mContainingNotification); 538 updateHeaderVisibility(false /* animate */); 539 updateChildrenAppearance(); 540 } 541 542 /** 543 * Recreate the low-priority header. 544 * 545 * @param builder a builder to reuse. Otherwise the builder will be recovered. 546 */ 547 @VisibleForTesting 548 void recreateLowPriorityHeader(Notification.Builder builder, boolean isConversation) { 549 AsyncGroupHeaderViewInflation.assertInLegacyMode(); 550 RemoteViews header; 551 StatusBarNotification notification = mContainingNotification.getEntry().getSbn(); 552 if (mIsMinimized) { 553 if (builder == null) { 554 builder = Notification.Builder.recoverBuilder(getContext(), 555 notification.getNotification()); 556 } 557 header = builder.makeLowPriorityContentView(true /* useRegularSubtext */); 558 if (mMinimizedGroupHeader == null) { 559 mMinimizedGroupHeader = (NotificationHeaderView) header.apply(getContext(), 560 this); 561 mMinimizedGroupHeader.findViewById(com.android.internal.R.id.expand_button) 562 .setVisibility(VISIBLE); 563 mMinimizedGroupHeader.setOnClickListener(mHeaderClickListener); 564 mMinimizedGroupHeaderWrapper = 565 (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( 566 getContext(), 567 mMinimizedGroupHeader, 568 mContainingNotification); 569 mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); 570 addView(mMinimizedGroupHeader, 0); 571 invalidate(); 572 } else { 573 header.reapply(getContext(), mMinimizedGroupHeader); 574 } 575 mMinimizedGroupHeaderWrapper.onContentUpdated(mContainingNotification); 576 resetHeaderVisibilityIfNeeded(mMinimizedGroupHeader, calculateDesiredHeader()); 577 } else { 578 removeView(mMinimizedGroupHeader); 579 mMinimizedGroupHeader = null; 580 mMinimizedGroupHeaderWrapper = null; 581 } 582 } 583 584 /** 585 * Update the appearance of the children to reduce redundancies. 586 */ 587 public void updateChildrenAppearance() { 588 mGroupingUtil.updateChildrenAppearance(); 589 } 590 591 private void setExpandButtonNumber(NotificationViewWrapper wrapper) { 592 View expandButton = wrapper == null 593 ? null : wrapper.getExpandButton(); 594 if (expandButton instanceof NotificationExpandButton) { 595 ((NotificationExpandButton) expandButton).setNumber(mUntruncatedChildCount); 596 } 597 } 598 599 public void updateGroupOverflow() { 600 if (mShowGroupCountInExpander) { 601 setExpandButtonNumber(mGroupHeaderWrapper); 602 setExpandButtonNumber(mMinimizedGroupHeaderWrapper); 603 return; 604 } 605 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */); 606 if (mUntruncatedChildCount > maxAllowedVisibleChildren) { 607 int number = mUntruncatedChildCount - maxAllowedVisibleChildren; 608 mOverflowNumber = mHybridGroupManager.bindOverflowNumber(mOverflowNumber, number, this); 609 if (mGroupOverFlowState == null) { 610 mGroupOverFlowState = new ViewState(); 611 mNeverAppliedGroupState = true; 612 } 613 } else if (mOverflowNumber != null) { 614 removeView(mOverflowNumber); 615 if (isShown() && isAttachedToWindow()) { 616 final View removedOverflowNumber = mOverflowNumber; 617 addTransientView(removedOverflowNumber, getTransientViewCount()); 618 CrossFadeHelper.fadeOut(removedOverflowNumber, new Runnable() { 619 @Override 620 public void run() { 621 removeTransientView(removedOverflowNumber); 622 } 623 }); 624 } 625 mOverflowNumber = null; 626 mGroupOverFlowState = null; 627 } 628 } 629 630 @Override 631 protected void onConfigurationChanged(Configuration newConfig) { 632 super.onConfigurationChanged(newConfig); 633 updateGroupOverflow(); 634 } 635 636 private View inflateDivider() { 637 return LayoutInflater.from(mContext).inflate( 638 R.layout.notification_children_divider, this, false); 639 } 640 641 /** 642 * Get notification children that are attached currently. 643 */ 644 public List<ExpandableNotificationRow> getAttachedChildren() { 645 return mAttachedChildren; 646 } 647 648 /** 649 * Sets the alpha on the content, while leaving the background of the container itself as is. 650 * 651 * @param alpha alpha value to apply to the content 652 */ 653 public void setContentAlpha(float alpha) { 654 if (mGroupHeader != null) { 655 for (int i = 0; i < mGroupHeader.getChildCount(); i++) { 656 mGroupHeader.getChildAt(i).setAlpha(alpha); 657 } 658 } 659 for (ExpandableNotificationRow child : getAttachedChildren()) { 660 child.setContentAlpha(alpha); 661 } 662 } 663 664 /** 665 * To be called any time the rows have been updated 666 */ 667 public void updateExpansionStates() { 668 if (mChildrenExpanded || mUserLocked) { 669 // we don't modify it the group is expanded or if we are expanding it 670 return; 671 } 672 int size = mAttachedChildren.size(); 673 for (int i = 0; i < size; i++) { 674 ExpandableNotificationRow child = mAttachedChildren.get(i); 675 child.setSystemChildExpanded(i == 0 && size == 1); 676 } 677 } 678 679 /** 680 * @return the intrinsic size of this children container, i.e the natural fully expanded state 681 */ 682 public int getIntrinsicHeight() { 683 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(); 684 return getIntrinsicHeight(maxAllowedVisibleChildren); 685 } 686 687 /** 688 * @return the intrinsic height with a number of children given 689 * in @param maxAllowedVisibleChildren 690 */ 691 private int getIntrinsicHeight(float maxAllowedVisibleChildren) { 692 if (showingAsLowPriority()) { 693 if (AsyncGroupHeaderViewInflation.isEnabled()) { 694 return mHeaderHeight; 695 } else { 696 return mMinimizedGroupHeader.getHeight(); 697 } 698 } 699 int intrinsicHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation; 700 int visibleChildren = 0; 701 int childCount = mAttachedChildren.size(); 702 boolean firstChild = true; 703 float expandFactor = 0; 704 if (mUserLocked) { 705 expandFactor = getGroupExpandFraction(); 706 } 707 boolean childrenExpanded = mChildrenExpanded; 708 for (int i = 0; i < childCount; i++) { 709 if (visibleChildren >= maxAllowedVisibleChildren) { 710 break; 711 } 712 if (!firstChild) { 713 if (mUserLocked) { 714 intrinsicHeight += NotificationUtils.interpolate(mChildPadding, mDividerHeight, 715 expandFactor); 716 } else { 717 intrinsicHeight += childrenExpanded ? mDividerHeight : mChildPadding; 718 } 719 } else { 720 if (mUserLocked) { 721 intrinsicHeight += NotificationUtils.interpolate( 722 0, 723 mNotificationTopPadding + mDividerHeight, 724 expandFactor); 725 } else { 726 intrinsicHeight += childrenExpanded 727 ? mNotificationTopPadding + mDividerHeight 728 : 0; 729 } 730 firstChild = false; 731 } 732 ExpandableNotificationRow child = mAttachedChildren.get(i); 733 intrinsicHeight += child.getIntrinsicHeight(); 734 visibleChildren++; 735 } 736 if (mUserLocked) { 737 intrinsicHeight += NotificationUtils.interpolate(mCollapsedBottomPadding, 0.0f, 738 expandFactor); 739 } else if (!childrenExpanded) { 740 intrinsicHeight += mCollapsedBottomPadding; 741 } 742 return intrinsicHeight; 743 } 744 745 /** 746 * Update the state of all its children based on a linear layout algorithm. 747 * 748 * @param parentState the state of the parent 749 */ 750 public void updateState(ExpandableViewState parentState) { 751 int childCount = mAttachedChildren.size(); 752 int yPosition = mNotificationHeaderMargin + mCurrentHeaderTranslation; 753 boolean firstChild = true; 754 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(); 755 int lastVisibleIndex = maxAllowedVisibleChildren - 1; 756 int firstOverflowIndex = lastVisibleIndex + 1; 757 float expandFactor = 0; 758 boolean expandingToExpandedGroup = mUserLocked && !showingAsLowPriority(); 759 if (mUserLocked) { 760 expandFactor = getGroupExpandFraction(); 761 firstOverflowIndex = getMaxAllowedVisibleChildren(true /* likeCollapsed */); 762 } 763 764 boolean childrenExpandedAndNotAnimating = mChildrenExpanded 765 && !mContainingNotification.isGroupExpansionChanging(); 766 int launchTransitionCompensation = 0; 767 for (int i = 0; i < childCount; i++) { 768 ExpandableNotificationRow child = mAttachedChildren.get(i); 769 if (!firstChild) { 770 if (expandingToExpandedGroup) { 771 yPosition += NotificationUtils.interpolate(mChildPadding, mDividerHeight, 772 expandFactor); 773 } else { 774 yPosition += mChildrenExpanded ? mDividerHeight : mChildPadding; 775 } 776 } else { 777 if (expandingToExpandedGroup) { 778 yPosition += NotificationUtils.interpolate( 779 0, 780 mNotificationTopPadding + mDividerHeight, 781 expandFactor); 782 } else { 783 yPosition += mChildrenExpanded ? mNotificationTopPadding + mDividerHeight : 0; 784 } 785 firstChild = false; 786 } 787 788 ExpandableViewState childState = child.getViewState(); 789 int intrinsicHeight = child.getIntrinsicHeight(); 790 childState.height = intrinsicHeight; 791 childState.setYTranslation(yPosition + launchTransitionCompensation); 792 childState.hidden = false; 793 if (child.isExpandAnimationRunning() || mContainingNotification.hasExpandingChild()) { 794 // Not modifying translationZ during launch animation. The translationZ of the 795 // expanding child is handled inside ExpandableNotificationRow and the translationZ 796 // of the other children inside the group should remain unchanged. In particular, 797 // they should not take over the translationZ of the parent, since the parent has 798 // a positive translationZ set only for the expanding child to be drawn above other 799 // notifications. 800 childState.setZTranslation(child.getTranslationZ()); 801 } else if (childrenExpandedAndNotAnimating && mEnableShadowOnChildNotifications) { 802 // When the group is expanded, the children cast the shadows rather than the parent 803 // so use the parent's elevation here. 804 childState.setZTranslation(parentState.getZTranslation()); 805 } else { 806 childState.setZTranslation(0); 807 } 808 childState.hideSensitive = parentState.hideSensitive; 809 childState.belowSpeedBump = parentState.belowSpeedBump; 810 childState.clipTopAmount = 0; 811 childState.setAlpha(0); 812 if (i < firstOverflowIndex) { 813 childState.setAlpha(showingAsLowPriority() ? expandFactor : 1.0f); 814 } else if (expandFactor == 1.0f && i <= lastVisibleIndex) { 815 childState.setAlpha( 816 (mActualHeight - childState.getYTranslation()) / childState.height); 817 childState.setAlpha(Math.max(0.0f, Math.min(1.0f, childState.getAlpha()))); 818 } 819 childState.location = parentState.location; 820 childState.inShelf = parentState.inShelf; 821 yPosition += intrinsicHeight; 822 } 823 if (mOverflowNumber != null) { 824 ExpandableNotificationRow overflowView = mAttachedChildren.get(Math.min( 825 getMaxAllowedVisibleChildren(true /* likeCollapsed */), childCount) - 1); 826 mGroupOverFlowState.copyFrom(overflowView.getViewState()); 827 828 if (!mChildrenExpanded) { 829 HybridNotificationView alignView = overflowView.getSingleLineView(); 830 if (alignView != null) { 831 View mirrorView = alignView.getTextView(); 832 if (mirrorView.getVisibility() == GONE) { 833 mirrorView = alignView.getTitleView(); 834 } 835 if (mirrorView.getVisibility() == GONE) { 836 mirrorView = alignView; 837 } 838 mGroupOverFlowState.setAlpha(mirrorView.getAlpha()); 839 float yTranslation = mGroupOverFlowState.getYTranslation() 840 + NotificationUtils.getRelativeYOffset( 841 mirrorView, overflowView); 842 mGroupOverFlowState.setYTranslation(yTranslation); 843 } 844 } else { 845 mGroupOverFlowState.setYTranslation( 846 mGroupOverFlowState.getYTranslation() + mNotificationHeaderMargin); 847 mGroupOverFlowState.setAlpha(0.0f); 848 } 849 } 850 if (mGroupHeader != null) { 851 if (mHeaderViewState == null) { 852 mHeaderViewState = new ViewState(); 853 } 854 mHeaderViewState.initFrom(mGroupHeader); 855 856 if (mContainingNotification.hasExpandingChild()) { 857 // Not modifying translationZ during expand animation. 858 mHeaderViewState.setZTranslation(mGroupHeader.getTranslationZ()); 859 } else if (childrenExpandedAndNotAnimating) { 860 mHeaderViewState.setZTranslation(parentState.getZTranslation()); 861 } else { 862 mHeaderViewState.setZTranslation(0); 863 } 864 mHeaderViewState.setYTranslation(mCurrentHeaderTranslation); 865 mHeaderViewState.setAlpha(mHeaderVisibleAmount); 866 // The hiding is done automatically by the alpha, otherwise we'll pick it up again 867 // in the next frame with the initFrom call above and have an invisible header 868 mHeaderViewState.hidden = false; 869 } 870 } 871 872 /** 873 * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its 874 * height, children in the group after this are gone. 875 * 876 * @param child the child who's height to adjust. 877 * @param parentHeight the height of the parent. 878 * @param childState the state to update. 879 * @param yPosition the yPosition of the view. 880 * @return true if children after this one should be hidden. 881 */ 882 private boolean updateChildStateForExpandedGroup( 883 ExpandableNotificationRow child, 884 int parentHeight, 885 ExpandableViewState childState, 886 int yPosition) { 887 final int top = yPosition + child.getClipTopAmount(); 888 final int intrinsicHeight = child.getIntrinsicHeight(); 889 final int bottom = top + intrinsicHeight; 890 int newHeight = intrinsicHeight; 891 if (bottom >= parentHeight) { 892 // Child is either clipped or gone 893 newHeight = Math.max((parentHeight - top), 0); 894 } 895 childState.hidden = newHeight == 0; 896 childState.height = newHeight; 897 return childState.height != intrinsicHeight && !childState.hidden; 898 } 899 900 @VisibleForTesting 901 int getMaxAllowedVisibleChildren() { 902 return getMaxAllowedVisibleChildren(false /* likeCollapsed */); 903 } 904 905 @VisibleForTesting 906 int getMaxAllowedVisibleChildren(boolean likeCollapsed) { 907 if (!likeCollapsed && (mChildrenExpanded || mContainingNotification.isUserLocked()) 908 && !showingAsLowPriority()) { 909 return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED; 910 } 911 if (mIsMinimized 912 || (!mContainingNotification.isOnKeyguard() && mContainingNotification.isExpanded()) 913 || (mContainingNotification.isHeadsUpState() 914 && mContainingNotification.canShowHeadsUp())) { 915 return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED; 916 } 917 return NUMBER_OF_CHILDREN_WHEN_COLLAPSED; 918 } 919 920 /** 921 * Applies state to children. 922 */ 923 public void applyState() { 924 int childCount = mAttachedChildren.size(); 925 ViewState tmpState = new ViewState(); 926 float expandFraction = 0.0f; 927 if (mUserLocked) { 928 expandFraction = getGroupExpandFraction(); 929 } 930 final boolean isExpanding = !showingAsLowPriority() 931 && (mUserLocked || mContainingNotification.isGroupExpansionChanging()); 932 final boolean dividersVisible = (mChildrenExpanded && mShowDividersWhenExpanded) 933 || (isExpanding && !mHideDividersDuringExpand); 934 for (int i = 0; i < childCount; i++) { 935 ExpandableNotificationRow child = mAttachedChildren.get(i); 936 ExpandableViewState viewState = child.getViewState(); 937 viewState.applyToView(child); 938 939 // layout the divider 940 View divider = mDividers.get(i); 941 tmpState.initFrom(divider); 942 tmpState.setYTranslation(viewState.getYTranslation() - mDividerHeight); 943 float alpha = mChildrenExpanded && viewState.getAlpha() != 0 ? mDividerAlpha : 0; 944 if (mUserLocked && !showingAsLowPriority() && viewState.getAlpha() != 0) { 945 alpha = NotificationUtils.interpolate(0, mDividerAlpha, 946 Math.min(viewState.getAlpha(), expandFraction)); 947 } 948 tmpState.hidden = !dividersVisible; 949 tmpState.setAlpha(alpha); 950 tmpState.applyToView(divider); 951 // There is no fake shadow to be drawn on the children 952 child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0); 953 } 954 if (mGroupOverFlowState != null) { 955 mGroupOverFlowState.applyToView(mOverflowNumber); 956 mNeverAppliedGroupState = false; 957 } 958 if (mHeaderViewState != null) { 959 mHeaderViewState.applyToView(mGroupHeader); 960 } 961 updateChildrenClipping(); 962 } 963 964 private void updateChildrenClipping() { 965 if (mContainingNotification.hasExpandingChild()) { 966 return; 967 } 968 int childCount = mAttachedChildren.size(); 969 int layoutEnd = mContainingNotification.getActualHeight() - mClipBottomAmount; 970 for (int i = 0; i < childCount; i++) { 971 ExpandableNotificationRow child = mAttachedChildren.get(i); 972 if (child.getVisibility() == GONE) { 973 continue; 974 } 975 float childTop = child.getTranslationY(); 976 float childBottom = childTop + child.getActualHeight(); 977 boolean visible = true; 978 int clipBottomAmount = 0; 979 if (childTop > layoutEnd) { 980 visible = false; 981 } else if (childBottom > layoutEnd) { 982 clipBottomAmount = (int) (childBottom - layoutEnd); 983 } 984 985 boolean isVisible = child.getVisibility() == VISIBLE; 986 if (visible != isVisible) { 987 child.setVisibility(visible ? VISIBLE : INVISIBLE); 988 } 989 990 child.setClipBottomAmount(clipBottomAmount); 991 } 992 } 993 994 @Override 995 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 996 boolean isCanvasChanged = false; 997 998 Path clipPath = mChildClipPath; 999 if (clipPath != null) { 1000 final float translation; 1001 if (child instanceof ExpandableNotificationRow notificationRow) { 1002 translation = notificationRow.getTranslation(); 1003 } else { 1004 translation = child.getTranslationX(); 1005 } 1006 1007 isCanvasChanged = true; 1008 canvas.save(); 1009 if (translation != 0f) { 1010 clipPath.offset(translation, 0f); 1011 canvas.clipPath(clipPath); 1012 clipPath.offset(-translation, 0f); 1013 } else { 1014 canvas.clipPath(clipPath); 1015 } 1016 } 1017 1018 if (child instanceof NotificationHeaderView 1019 && mGroupHeaderWrapper.hasRoundedCorner()) { 1020 float[] radii = mGroupHeaderWrapper.getUpdatedRadii(); 1021 mHeaderPath.reset(); 1022 mHeaderPath.addRoundRect( 1023 child.getLeft(), 1024 child.getTop(), 1025 child.getRight(), 1026 child.getBottom(), 1027 radii, 1028 Direction.CW 1029 ); 1030 if (!isCanvasChanged) { 1031 isCanvasChanged = true; 1032 canvas.save(); 1033 } 1034 canvas.clipPath(mHeaderPath); 1035 } 1036 1037 if (isCanvasChanged) { 1038 boolean result = super.drawChild(canvas, child, drawingTime); 1039 canvas.restore(); 1040 return result; 1041 } else { 1042 // If there have been no changes to the canvas we can proceed as usual 1043 return super.drawChild(canvas, child, drawingTime); 1044 } 1045 } 1046 1047 1048 /** 1049 * This is called when the children expansion has changed and positions the children properly 1050 * for an appear animation. 1051 */ 1052 public void prepareExpansionChanged() { 1053 // TODO: do something that makes sense, like placing the invisible views correctly 1054 return; 1055 } 1056 1057 /** 1058 * Animate to a given state. 1059 */ 1060 public void startAnimationToState(AnimationProperties properties) { 1061 int childCount = mAttachedChildren.size(); 1062 ViewState tmpState = new ViewState(); 1063 float expandFraction = getGroupExpandFraction(); 1064 final boolean isExpanding = !showingAsLowPriority() 1065 && (mUserLocked || mContainingNotification.isGroupExpansionChanging()); 1066 final boolean dividersVisible = (mChildrenExpanded && mShowDividersWhenExpanded) 1067 || (isExpanding && !mHideDividersDuringExpand); 1068 for (int i = childCount - 1; i >= 0; i--) { 1069 ExpandableNotificationRow child = mAttachedChildren.get(i); 1070 ExpandableViewState viewState = child.getViewState(); 1071 viewState.animateTo(child, properties); 1072 1073 // layout the divider 1074 View divider = mDividers.get(i); 1075 tmpState.initFrom(divider); 1076 tmpState.setYTranslation(viewState.getYTranslation() - mDividerHeight); 1077 float alpha = mChildrenExpanded && viewState.getAlpha() != 0 ? mDividerAlpha : 0; 1078 if (mUserLocked && !showingAsLowPriority() && viewState.getAlpha() != 0) { 1079 alpha = NotificationUtils.interpolate(0, mDividerAlpha, 1080 Math.min(viewState.getAlpha(), expandFraction)); 1081 } 1082 tmpState.hidden = !dividersVisible; 1083 tmpState.setAlpha(alpha); 1084 tmpState.animateTo(divider, properties); 1085 // There is no fake shadow to be drawn on the children 1086 child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0); 1087 } 1088 if (mOverflowNumber != null) { 1089 if (mNeverAppliedGroupState) { 1090 float alpha = mGroupOverFlowState.getAlpha(); 1091 mGroupOverFlowState.setAlpha(0); 1092 mGroupOverFlowState.applyToView(mOverflowNumber); 1093 mGroupOverFlowState.setAlpha(alpha); 1094 mNeverAppliedGroupState = false; 1095 } 1096 mGroupOverFlowState.animateTo(mOverflowNumber, properties); 1097 } 1098 if (mGroupHeader != null) { 1099 mHeaderViewState.applyToView(mGroupHeader); 1100 } 1101 updateChildrenClipping(); 1102 } 1103 1104 public ExpandableNotificationRow getViewAtPosition(float y) { 1105 // find the view under the pointer, accounting for GONE views 1106 final int count = mAttachedChildren.size(); 1107 for (int childIdx = 0; childIdx < count; childIdx++) { 1108 ExpandableNotificationRow slidingChild = mAttachedChildren.get(childIdx); 1109 float childTop = slidingChild.getTranslationY(); 1110 float top = childTop + Math.max(0, slidingChild.getClipTopAmount()); 1111 float bottom = childTop + slidingChild.getActualHeight(); 1112 if (y >= top && y <= bottom) { 1113 return slidingChild; 1114 } 1115 } 1116 return null; 1117 } 1118 1119 public void setChildrenExpanded(boolean childrenExpanded) { 1120 mChildrenExpanded = childrenExpanded; 1121 updateExpansionStates(); 1122 if (mGroupHeaderWrapper != null) { 1123 mGroupHeaderWrapper.setExpanded(childrenExpanded); 1124 } 1125 final int count = mAttachedChildren.size(); 1126 for (int childIdx = 0; childIdx < count; childIdx++) { 1127 ExpandableNotificationRow child = mAttachedChildren.get(childIdx); 1128 child.setChildrenExpanded(childrenExpanded, false); 1129 } 1130 updateHeaderTouchability(); 1131 } 1132 1133 public void setContainingNotification(ExpandableNotificationRow parent) { 1134 mContainingNotification = parent; 1135 mGroupingUtil = new NotificationGroupingUtil(mContainingNotification); 1136 } 1137 1138 public ExpandableNotificationRow getContainingNotification() { 1139 return mContainingNotification; 1140 } 1141 1142 public NotificationViewWrapper getNotificationViewWrapper() { 1143 return mGroupHeaderWrapper; 1144 } 1145 1146 public NotificationViewWrapper getMinimizedGroupHeaderWrapper() { 1147 return mMinimizedGroupHeaderWrapper; 1148 } 1149 1150 @VisibleForTesting 1151 public ViewGroup getCurrentHeaderView() { 1152 return mCurrentHeader; 1153 } 1154 1155 public NotificationHeaderView getGroupHeader() { 1156 return mGroupHeader; 1157 } 1158 1159 public NotificationHeaderView getMinimizedNotificationHeader() { 1160 return mMinimizedGroupHeader; 1161 } 1162 1163 private void updateHeaderVisibility(boolean animate) { 1164 ViewGroup desiredHeader; 1165 ViewGroup currentHeader = mCurrentHeader; 1166 desiredHeader = calculateDesiredHeader(); 1167 1168 if (currentHeader == desiredHeader) { 1169 return; 1170 } 1171 1172 if (AsyncGroupHeaderViewInflation.isEnabled() && desiredHeader == null) { 1173 return; 1174 } 1175 1176 if (animate) { 1177 if (desiredHeader != null && currentHeader != null) { 1178 currentHeader.setVisibility(VISIBLE); 1179 desiredHeader.setVisibility(VISIBLE); 1180 NotificationViewWrapper visibleWrapper = getWrapperForView(desiredHeader); 1181 NotificationViewWrapper hiddenWrapper = getWrapperForView(currentHeader); 1182 visibleWrapper.transformFrom(hiddenWrapper); 1183 hiddenWrapper.transformTo(visibleWrapper, () -> updateHeaderVisibility(false)); 1184 startChildAlphaAnimations(desiredHeader == mGroupHeader); 1185 } else { 1186 animate = false; 1187 } 1188 } 1189 if (!animate) { 1190 if (desiredHeader != null) { 1191 getWrapperForView(desiredHeader).setVisible(true); 1192 desiredHeader.setVisibility(VISIBLE); 1193 } 1194 if (currentHeader != null) { 1195 // Wrapper can be null if we were a low priority notification 1196 // and just destroyed it by calling setIsLowPriority(false) 1197 NotificationViewWrapper wrapper = getWrapperForView(currentHeader); 1198 if (wrapper != null) { 1199 wrapper.setVisible(false); 1200 } 1201 currentHeader.setVisibility(INVISIBLE); 1202 } 1203 } 1204 1205 resetHeaderVisibilityIfNeeded(mGroupHeader, desiredHeader); 1206 resetHeaderVisibilityIfNeeded(mMinimizedGroupHeader, desiredHeader); 1207 1208 mCurrentHeader = desiredHeader; 1209 } 1210 1211 private void resetHeaderVisibilityIfNeeded(View header, View desiredHeader) { 1212 if (header == null) { 1213 return; 1214 } 1215 if (header != mCurrentHeader && header != desiredHeader) { 1216 getWrapperForView(header).setVisible(false); 1217 header.setVisibility(INVISIBLE); 1218 } 1219 if (header == desiredHeader && header.getVisibility() != VISIBLE) { 1220 getWrapperForView(header).setVisible(true); 1221 header.setVisibility(VISIBLE); 1222 } 1223 } 1224 1225 private ViewGroup calculateDesiredHeader() { 1226 ViewGroup desiredHeader; 1227 if (showingAsLowPriority()) { 1228 desiredHeader = mMinimizedGroupHeader; 1229 } else { 1230 desiredHeader = mGroupHeader; 1231 } 1232 return desiredHeader; 1233 } 1234 1235 private void startChildAlphaAnimations(boolean toVisible) { 1236 float target = toVisible ? 1.0f : 0.0f; 1237 float start = 1.0f - target; 1238 int childCount = mAttachedChildren.size(); 1239 for (int i = 0; i < childCount; i++) { 1240 if (i >= NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED) { 1241 break; 1242 } 1243 ExpandableNotificationRow child = mAttachedChildren.get(i); 1244 child.setAlpha(start); 1245 ViewState viewState = new ViewState(); 1246 viewState.initFrom(child); 1247 viewState.setAlpha(target); 1248 ALPHA_FADE_IN.setDelay(i * 50); 1249 viewState.animateTo(child, ALPHA_FADE_IN); 1250 } 1251 } 1252 1253 1254 private void updateHeaderTransformation() { 1255 if (mUserLocked && showingAsLowPriority()) { 1256 float fraction = getGroupExpandFraction(); 1257 mGroupHeaderWrapper.transformFrom(mMinimizedGroupHeaderWrapper, 1258 fraction); 1259 mGroupHeader.setVisibility(VISIBLE); 1260 mMinimizedGroupHeaderWrapper.transformTo(mGroupHeaderWrapper, 1261 fraction); 1262 } 1263 1264 } 1265 1266 private NotificationViewWrapper getWrapperForView(View visibleHeader) { 1267 if (visibleHeader == mGroupHeader) { 1268 return mGroupHeaderWrapper; 1269 } 1270 return mMinimizedGroupHeaderWrapper; 1271 } 1272 1273 /** 1274 * Called when a groups expansion changes to adjust the background of the header view. 1275 * 1276 * @param expanded whether the group is expanded. 1277 */ 1278 public void updateHeaderForExpansion(boolean expanded) { 1279 if (mGroupHeader != null) { 1280 if (expanded) { 1281 ColorDrawable cd = new ColorDrawable(); 1282 cd.setColor(mContainingNotification.calculateBgColor()); 1283 mGroupHeader.setHeaderBackgroundDrawable(cd); 1284 } else { 1285 mGroupHeader.setHeaderBackgroundDrawable(null); 1286 } 1287 } 1288 } 1289 1290 public int getMaxContentHeight() { 1291 if (showingAsLowPriority()) { 1292 return getMinHeight(NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED, true 1293 /* likeHighPriority */); 1294 } 1295 int maxContentHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation 1296 + mNotificationTopPadding; 1297 int visibleChildren = 0; 1298 int childCount = mAttachedChildren.size(); 1299 for (int i = 0; i < childCount; i++) { 1300 if (visibleChildren >= NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED) { 1301 break; 1302 } 1303 ExpandableNotificationRow child = mAttachedChildren.get(i); 1304 float childHeight = child.isExpanded(true /* allowOnKeyguard */) 1305 ? child.getMaxExpandHeight() 1306 : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */); 1307 maxContentHeight += childHeight; 1308 visibleChildren++; 1309 } 1310 if (visibleChildren > 0) { 1311 maxContentHeight += visibleChildren * mDividerHeight; 1312 } 1313 return maxContentHeight; 1314 } 1315 1316 public void setActualHeight(int actualHeight) { 1317 if (!mUserLocked) { 1318 return; 1319 } 1320 mActualHeight = actualHeight; 1321 float fraction = getGroupExpandFraction(); 1322 boolean showingLowPriority = showingAsLowPriority(); 1323 updateHeaderTransformation(); 1324 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */); 1325 int childCount = mAttachedChildren.size(); 1326 for (int i = 0; i < childCount; i++) { 1327 ExpandableNotificationRow child = mAttachedChildren.get(i); 1328 float childHeight; 1329 if (showingLowPriority) { 1330 childHeight = child.getShowingLayout().getMinHeight(false /* likeGroupExpanded */); 1331 } else if (child.isExpanded(true /* allowOnKeyguard */)) { 1332 childHeight = child.getMaxExpandHeight(); 1333 } else { 1334 childHeight = child.getShowingLayout().getMinHeight( 1335 true /* likeGroupExpanded */); 1336 } 1337 if (i < maxAllowedVisibleChildren) { 1338 float singleLineHeight = child.getShowingLayout().getMinHeight( 1339 false /* likeGroupExpanded */); 1340 child.setActualHeight((int) NotificationUtils.interpolate(singleLineHeight, 1341 childHeight, fraction), false); 1342 } else { 1343 child.setActualHeight((int) childHeight, false); 1344 } 1345 } 1346 } 1347 1348 public float getGroupExpandFraction() { 1349 int visibleChildrenExpandedHeight = showingAsLowPriority() ? getMaxContentHeight() 1350 : getVisibleChildrenExpandHeight(); 1351 int minExpandHeight = getCollapsedHeight(); 1352 float factor = (mActualHeight - minExpandHeight) 1353 / (float) (visibleChildrenExpandedHeight - minExpandHeight); 1354 return Math.max(0.0f, Math.min(1.0f, factor)); 1355 } 1356 1357 private int getVisibleChildrenExpandHeight() { 1358 int intrinsicHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation 1359 + mNotificationTopPadding + mDividerHeight; 1360 int visibleChildren = 0; 1361 int childCount = mAttachedChildren.size(); 1362 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */); 1363 for (int i = 0; i < childCount; i++) { 1364 if (visibleChildren >= maxAllowedVisibleChildren) { 1365 break; 1366 } 1367 ExpandableNotificationRow child = mAttachedChildren.get(i); 1368 float childHeight = child.isExpanded(true /* allowOnKeyguard */) 1369 ? child.getMaxExpandHeight() 1370 : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */); 1371 intrinsicHeight += childHeight; 1372 visibleChildren++; 1373 } 1374 return intrinsicHeight; 1375 } 1376 1377 public int getMinHeight() { 1378 return getMinHeight(NUMBER_OF_CHILDREN_WHEN_COLLAPSED, false /* likeHighPriority */); 1379 } 1380 1381 public int getCollapsedHeight() { 1382 return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */), 1383 false /* likeHighPriority */); 1384 } 1385 1386 public int getCollapsedHeightWithoutHeader() { 1387 return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */), 1388 false /* likeHighPriority */, 0); 1389 } 1390 1391 /** 1392 * Get the minimum Height for this group. 1393 * 1394 * @param maxAllowedVisibleChildren the number of children that should be visible 1395 * @param likeHighPriority if the height should be calculated as if it were not low 1396 * priority 1397 */ 1398 private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority) { 1399 return getMinHeight(maxAllowedVisibleChildren, likeHighPriority, mCurrentHeaderTranslation); 1400 } 1401 1402 /** 1403 * Get the minimum Height for this group. 1404 * 1405 * @param maxAllowedVisibleChildren the number of children that should be visible 1406 * @param likeHighPriority if the height should be calculated as if it were not low 1407 * priority 1408 * @param headerTranslation the translation amount of the header 1409 */ 1410 private int getMinHeight( 1411 int maxAllowedVisibleChildren, 1412 boolean likeHighPriority, 1413 int headerTranslation) { 1414 if (!likeHighPriority && showingAsLowPriority()) { 1415 if (AsyncGroupHeaderViewInflation.isEnabled()) { 1416 return mHeaderHeight; 1417 } 1418 if (mMinimizedGroupHeader == null) { 1419 Log.e(TAG, "getMinHeight: low priority header is null", new Exception()); 1420 return 0; 1421 } 1422 return mMinimizedGroupHeader.getHeight(); 1423 } 1424 int minExpandHeight = mNotificationHeaderMargin + headerTranslation; 1425 int visibleChildren = 0; 1426 boolean firstChild = true; 1427 int childCount = mAttachedChildren.size(); 1428 for (int i = 0; i < childCount; i++) { 1429 if (visibleChildren >= maxAllowedVisibleChildren) { 1430 break; 1431 } 1432 if (!firstChild) { 1433 minExpandHeight += mChildPadding; 1434 } else { 1435 firstChild = false; 1436 } 1437 ExpandableNotificationRow child = mAttachedChildren.get(i); 1438 View singleLineView = child.getSingleLineView(); 1439 if (singleLineView != null) { 1440 minExpandHeight += singleLineView.getHeight(); 1441 } else { 1442 if (AsyncHybridViewInflation.isEnabled()) { 1443 minExpandHeight += mMinSingleLineHeight; 1444 } else { 1445 Log.e(TAG, "getMinHeight: child " + child.getEntry().getKey() 1446 + " single line view is null", new Exception()); 1447 } 1448 } 1449 visibleChildren++; 1450 } 1451 minExpandHeight += mCollapsedBottomPadding; 1452 return minExpandHeight; 1453 } 1454 1455 public boolean showingAsLowPriority() { 1456 return mIsMinimized && !mContainingNotification.isExpanded(); 1457 } 1458 1459 public void reInflateViews(OnClickListener listener, StatusBarNotification notification) { 1460 if (!AsyncGroupHeaderViewInflation.isEnabled()) { 1461 // When Async header inflation is enabled, we do not reinflate headers because they are 1462 // inflated from the background thread 1463 if (mGroupHeader != null) { 1464 removeView(mGroupHeader); 1465 mGroupHeader = null; 1466 } 1467 if (mMinimizedGroupHeader != null) { 1468 removeView(mMinimizedGroupHeader); 1469 mMinimizedGroupHeader = null; 1470 } 1471 recreateNotificationHeader(listener, mIsConversation); 1472 } 1473 initDimens(); 1474 for (int i = 0; i < mDividers.size(); i++) { 1475 View prevDivider = mDividers.get(i); 1476 int index = indexOfChild(prevDivider); 1477 removeView(prevDivider); 1478 View divider = inflateDivider(); 1479 addView(divider, index); 1480 mDividers.set(i, divider); 1481 } 1482 removeView(mOverflowNumber); 1483 mOverflowNumber = null; 1484 mGroupOverFlowState = null; 1485 updateGroupOverflow(); 1486 } 1487 1488 public void setUserLocked(boolean userLocked) { 1489 mUserLocked = userLocked; 1490 if (!mUserLocked) { 1491 updateHeaderVisibility(false /* animate */); 1492 } 1493 int childCount = mAttachedChildren.size(); 1494 for (int i = 0; i < childCount; i++) { 1495 ExpandableNotificationRow child = mAttachedChildren.get(i); 1496 child.setUserLocked(userLocked && !showingAsLowPriority()); 1497 } 1498 updateHeaderTouchability(); 1499 } 1500 1501 private void updateHeaderTouchability() { 1502 if (mGroupHeader != null) { 1503 mGroupHeader.setAcceptAllTouches(mChildrenExpanded || mUserLocked); 1504 } 1505 } 1506 1507 public void onNotificationUpdated() { 1508 if (mShowGroupCountInExpander) { 1509 // The overflow number is not used, so its color is irrelevant; skip this 1510 return; 1511 } 1512 int color = mContainingNotification.getNotificationColor(); 1513 Resources.Theme theme = new ContextThemeWrapper(mContext, 1514 com.android.internal.R.style.Theme_DeviceDefault_DayNight).getTheme(); 1515 try (TypedArray ta = theme.obtainStyledAttributes( 1516 new int[]{com.android.internal.R.attr.materialColorPrimary})) { 1517 color = ta.getColor(0, color); 1518 } 1519 mHybridGroupManager.setOverflowNumberColor(mOverflowNumber, color); 1520 } 1521 1522 public int getPositionInLinearLayout(View childInGroup) { 1523 int position = mNotificationHeaderMargin + mCurrentHeaderTranslation 1524 + mNotificationTopPadding; 1525 1526 for (int i = 0; i < mAttachedChildren.size(); i++) { 1527 ExpandableNotificationRow child = mAttachedChildren.get(i); 1528 boolean notGone = child.getVisibility() != View.GONE; 1529 if (notGone) { 1530 position += mDividerHeight; 1531 } 1532 if (child == childInGroup) { 1533 return position; 1534 } 1535 if (notGone) { 1536 position += child.getIntrinsicHeight(); 1537 } 1538 } 1539 return 0; 1540 } 1541 1542 public void setClipBottomAmount(int clipBottomAmount) { 1543 mClipBottomAmount = clipBottomAmount; 1544 updateChildrenClipping(); 1545 } 1546 1547 /** 1548 * Set whether the children container is minimized. 1549 */ 1550 public void setIsMinimized(boolean isMinimized) { 1551 mIsMinimized = isMinimized; 1552 if (mContainingNotification != null) { /* we're not yet set up yet otherwise */ 1553 if (!AsyncGroupHeaderViewInflation.isEnabled()) { 1554 recreateLowPriorityHeader(null /* existingBuilder */, mIsConversation); 1555 } 1556 updateHeaderVisibility(false /* animate */); 1557 } 1558 if (mUserLocked) { 1559 setUserLocked(mUserLocked); 1560 } 1561 } 1562 1563 /** 1564 * @return the view wrapper for the currently showing priority. 1565 */ 1566 public NotificationViewWrapper getVisibleWrapper() { 1567 if (showingAsLowPriority()) { 1568 return mMinimizedGroupHeaderWrapper; 1569 } 1570 return mGroupHeaderWrapper; 1571 } 1572 1573 public void onExpansionChanged() { 1574 if (mIsMinimized) { 1575 if (mUserLocked) { 1576 setUserLocked(mUserLocked); 1577 } 1578 updateHeaderVisibility(true /* animate */); 1579 } 1580 } 1581 1582 @VisibleForTesting 1583 public boolean isUserLocked() { 1584 return mUserLocked; 1585 } 1586 1587 @Override 1588 public void applyRoundnessAndInvalidate() { 1589 boolean last = true; 1590 if (mGroupHeaderWrapper != null) { 1591 mGroupHeaderWrapper.requestTopRoundness( 1592 /* value = */ getTopRoundness(), 1593 /* sourceType = */ FROM_PARENT, 1594 /* animate = */ false 1595 ); 1596 } 1597 if (mMinimizedGroupHeaderWrapper != null) { 1598 mMinimizedGroupHeaderWrapper.requestTopRoundness( 1599 /* value = */ getTopRoundness(), 1600 /* sourceType = */ FROM_PARENT, 1601 /* animate = */ false 1602 ); 1603 } 1604 for (int i = mAttachedChildren.size() - 1; i >= 0; i--) { 1605 ExpandableNotificationRow child = mAttachedChildren.get(i); 1606 if (child.getVisibility() == View.GONE) { 1607 continue; 1608 } 1609 child.requestRoundness( 1610 /* top = */ 0f, 1611 /* bottom = */ last ? getBottomRoundness() : 0f, 1612 /* sourceType = */ FROM_PARENT, 1613 /* animate = */ false); 1614 last = false; 1615 } 1616 Roundable.super.applyRoundnessAndInvalidate(); 1617 } 1618 1619 public void setHeaderVisibleAmount(float headerVisibleAmount) { 1620 mHeaderVisibleAmount = headerVisibleAmount; 1621 mCurrentHeaderTranslation = (int) ((1.0f - headerVisibleAmount) * mTranslationForHeader); 1622 } 1623 1624 /** 1625 * Shows the given feedback icon, or hides the icon if null. 1626 */ 1627 public void setFeedbackIcon(@Nullable FeedbackIcon icon) { 1628 if (mGroupHeaderWrapper != null) { 1629 mGroupHeaderWrapper.setFeedbackIcon(icon); 1630 } 1631 if (mMinimizedGroupHeaderWrapper != null) { 1632 mMinimizedGroupHeaderWrapper.setFeedbackIcon(icon); 1633 } 1634 } 1635 1636 public void setRecentlyAudiblyAlerted(boolean audiblyAlertedRecently) { 1637 if (mGroupHeaderWrapper != null) { 1638 mGroupHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently); 1639 } 1640 if (mMinimizedGroupHeaderWrapper != null) { 1641 mMinimizedGroupHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently); 1642 } 1643 } 1644 1645 @Override 1646 public void setNotificationFaded(boolean faded) { 1647 mContainingNotificationIsFaded = faded; 1648 if (mGroupHeaderWrapper != null) { 1649 mGroupHeaderWrapper.setNotificationFaded(faded); 1650 } 1651 if (mMinimizedGroupHeaderWrapper != null) { 1652 mMinimizedGroupHeaderWrapper.setNotificationFaded(faded); 1653 } 1654 for (ExpandableNotificationRow child : mAttachedChildren) { 1655 child.setNotificationFaded(faded); 1656 } 1657 } 1658 1659 /** 1660 * Allow to define a path the clip the children in #drawChild() 1661 * 1662 * @param childClipPath path used to clip the children 1663 */ 1664 public void setChildClipPath(@Nullable Path childClipPath) { 1665 mChildClipPath = childClipPath; 1666 invalidate(); 1667 } 1668 1669 public NotificationHeaderViewWrapper getNotificationHeaderWrapper() { 1670 return mGroupHeaderWrapper; 1671 } 1672 1673 public void setLogger(NotificationChildrenContainerLogger logger) { 1674 mLogger = logger; 1675 } 1676 1677 @Override 1678 public void addTransientView(View view, int index) { 1679 if (mLogger != null && view instanceof ExpandableNotificationRow) { 1680 mLogger.addTransientRow( 1681 ((ExpandableNotificationRow) view).getEntry(), 1682 getContainingNotification().getEntry(), 1683 index 1684 ); 1685 } 1686 super.addTransientView(view, index); 1687 } 1688 1689 @Override 1690 public void removeTransientView(View view) { 1691 if (mLogger != null && view instanceof ExpandableNotificationRow) { 1692 mLogger.removeTransientRow( 1693 ((ExpandableNotificationRow) view).getEntry(), 1694 getContainingNotification().getEntry() 1695 ); 1696 } 1697 super.removeTransientView(view); 1698 } 1699 1700 public String debugString() { 1701 return TAG + " { " 1702 + "visibility: " + getVisibility() 1703 + ", alpha: " + getAlpha() 1704 + ", translationY: " + getTranslationY() 1705 + ", roundableState: " + getRoundableState().debugString() + "}"; 1706 } 1707 } 1708