Home | History | Annotate | Download | only in stack
      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.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.graphics.drawable.ColorDrawable;
     24 import android.service.notification.StatusBarNotification;
     25 import android.util.AttributeSet;
     26 import android.view.LayoutInflater;
     27 import android.view.NotificationHeaderView;
     28 import android.view.View;
     29 import android.view.ViewGroup;
     30 import android.widget.RemoteViews;
     31 import android.widget.TextView;
     32 
     33 import com.android.internal.annotations.VisibleForTesting;
     34 import com.android.systemui.R;
     35 import com.android.systemui.statusbar.CrossFadeHelper;
     36 import com.android.systemui.statusbar.ExpandableNotificationRow;
     37 import com.android.systemui.statusbar.NotificationHeaderUtil;
     38 import com.android.systemui.statusbar.notification.HybridGroupManager;
     39 import com.android.systemui.statusbar.notification.HybridNotificationView;
     40 import com.android.systemui.statusbar.notification.NotificationUtils;
     41 import com.android.systemui.statusbar.notification.NotificationViewWrapper;
     42 import com.android.systemui.statusbar.notification.VisualStabilityManager;
     43 
     44 import java.util.ArrayList;
     45 import java.util.List;
     46 
     47 /**
     48  * A container containing child notifications
     49  */
     50 public class NotificationChildrenContainer extends ViewGroup {
     51 
     52     private static final int NUMBER_OF_CHILDREN_WHEN_COLLAPSED = 2;
     53     private static final int NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED = 5;
     54     private static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8;
     55     private static final int NUMBER_OF_CHILDREN_WHEN_AMBIENT = 3;
     56     private static final AnimationProperties ALPHA_FADE_IN = new AnimationProperties() {
     57         private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
     58 
     59         @Override
     60         public AnimationFilter getAnimationFilter() {
     61             return mAnimationFilter;
     62         }
     63     }.setDuration(200);
     64 
     65     private final List<View> mDividers = new ArrayList<>();
     66     private final List<ExpandableNotificationRow> mChildren = new ArrayList<>();
     67     private final HybridGroupManager mHybridGroupManager;
     68     private int mChildPadding;
     69     private int mDividerHeight;
     70     private float mDividerAlpha;
     71     private int mNotificationHeaderMargin;
     72 
     73     private int mNotificatonTopPadding;
     74     private float mCollapsedBottompadding;
     75     private boolean mChildrenExpanded;
     76     private ExpandableNotificationRow mContainingNotification;
     77     private TextView mOverflowNumber;
     78     private ViewState mGroupOverFlowState;
     79     private int mRealHeight;
     80     private boolean mUserLocked;
     81     private int mActualHeight;
     82     private boolean mNeverAppliedGroupState;
     83     private int mHeaderHeight;
     84 
     85     /**
     86      * Whether or not individual notifications that are part of this container will have shadows.
     87      */
     88     private boolean mEnableShadowOnChildNotifications;
     89 
     90     private NotificationHeaderView mNotificationHeader;
     91     private NotificationViewWrapper mNotificationHeaderWrapper;
     92     private NotificationHeaderView mNotificationHeaderLowPriority;
     93     private NotificationViewWrapper mNotificationHeaderWrapperLowPriority;
     94     private ViewGroup mNotificationHeaderAmbient;
     95     private NotificationViewWrapper mNotificationHeaderWrapperAmbient;
     96     private NotificationHeaderUtil mHeaderUtil;
     97     private ViewState mHeaderViewState;
     98     private int mClipBottomAmount;
     99     private boolean mIsLowPriority;
    100     private OnClickListener mHeaderClickListener;
    101     private ViewGroup mCurrentHeader;
    102 
    103     private boolean mShowDividersWhenExpanded;
    104     private boolean mHideDividersDuringExpand;
    105 
    106     public NotificationChildrenContainer(Context context) {
    107         this(context, null);
    108     }
    109 
    110     public NotificationChildrenContainer(Context context, AttributeSet attrs) {
    111         this(context, attrs, 0);
    112     }
    113 
    114     public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr) {
    115         this(context, attrs, defStyleAttr, 0);
    116     }
    117 
    118     public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr,
    119             int defStyleRes) {
    120         super(context, attrs, defStyleAttr, defStyleRes);
    121         initDimens();
    122         mHybridGroupManager = new HybridGroupManager(getContext(), this);
    123     }
    124 
    125     private void initDimens() {
    126         Resources res = getResources();
    127         mChildPadding = res.getDimensionPixelSize(R.dimen.notification_children_padding);
    128         mDividerHeight = res.getDimensionPixelSize(
    129                 R.dimen.notification_children_container_divider_height);
    130         mDividerAlpha = res.getFloat(R.dimen.notification_divider_alpha);
    131         mHeaderHeight = res.getDimensionPixelSize(
    132                 R.dimen.notification_children_container_header_height);
    133         mNotificationHeaderMargin = res.getDimensionPixelSize(
    134                 R.dimen.notification_children_container_margin_top);
    135         mNotificatonTopPadding = res.getDimensionPixelSize(
    136                 R.dimen.notification_children_container_top_padding);
    137         mCollapsedBottompadding = res.getDimensionPixelSize(
    138                 com.android.internal.R.dimen.notification_content_margin_bottom);
    139         mEnableShadowOnChildNotifications =
    140                 res.getBoolean(R.bool.config_enableShadowOnChildNotifications);
    141         mShowDividersWhenExpanded =
    142                 res.getBoolean(R.bool.config_showDividersWhenGroupNotificationExpanded);
    143         mHideDividersDuringExpand =
    144                 res.getBoolean(R.bool.config_hideDividersDuringExpand);
    145     }
    146 
    147     @Override
    148     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    149         int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
    150         for (int i = 0; i < childCount; i++) {
    151             View child = mChildren.get(i);
    152             // We need to layout all children even the GONE ones, such that the heights are
    153             // calculated correctly as they are used to calculate how many we can fit on the screen
    154             child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
    155             mDividers.get(i).layout(0, 0, getWidth(), mDividerHeight);
    156         }
    157         if (mOverflowNumber != null) {
    158             boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
    159             int left = (isRtl ? 0 : getWidth() - mOverflowNumber.getMeasuredWidth());
    160             int right = left + mOverflowNumber.getMeasuredWidth();
    161             mOverflowNumber.layout(left, 0, right, mOverflowNumber.getMeasuredHeight());
    162         }
    163         if (mNotificationHeader != null) {
    164             mNotificationHeader.layout(0, 0, mNotificationHeader.getMeasuredWidth(),
    165                     mNotificationHeader.getMeasuredHeight());
    166         }
    167         if (mNotificationHeaderLowPriority != null) {
    168             mNotificationHeaderLowPriority.layout(0, 0,
    169                     mNotificationHeaderLowPriority.getMeasuredWidth(),
    170                     mNotificationHeaderLowPriority.getMeasuredHeight());
    171         }
    172         if (mNotificationHeaderAmbient != null) {
    173             mNotificationHeaderAmbient.layout(0, 0,
    174                     mNotificationHeaderAmbient.getMeasuredWidth(),
    175                     mNotificationHeaderAmbient.getMeasuredHeight());
    176         }
    177     }
    178 
    179     @Override
    180     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    181         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    182         boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
    183         boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
    184         int size = MeasureSpec.getSize(heightMeasureSpec);
    185         int newHeightSpec = heightMeasureSpec;
    186         if (hasFixedHeight || isHeightLimited) {
    187             newHeightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
    188         }
    189         int width = MeasureSpec.getSize(widthMeasureSpec);
    190         if (mOverflowNumber != null) {
    191             mOverflowNumber.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
    192                     newHeightSpec);
    193         }
    194         int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY);
    195         int height = mNotificationHeaderMargin + mNotificatonTopPadding;
    196         int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
    197         int collapsedChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
    198         int overflowIndex = childCount > collapsedChildren ? collapsedChildren - 1 : -1;
    199         for (int i = 0; i < childCount; i++) {
    200             ExpandableNotificationRow child = mChildren.get(i);
    201             // We need to measure all children even the GONE ones, such that the heights are
    202             // calculated correctly as they are used to calculate how many we can fit on the screen.
    203             boolean isOverflow = i == overflowIndex;
    204             child.setSingleLineWidthIndention(isOverflow && mOverflowNumber != null
    205                     ? mOverflowNumber.getMeasuredWidth()
    206                     : 0);
    207             child.measure(widthMeasureSpec, newHeightSpec);
    208             // layout the divider
    209             View divider = mDividers.get(i);
    210             divider.measure(widthMeasureSpec, dividerHeightSpec);
    211             if (child.getVisibility() != GONE) {
    212                 height += child.getMeasuredHeight() + mDividerHeight;
    213             }
    214         }
    215         mRealHeight = height;
    216         if (heightMode != MeasureSpec.UNSPECIFIED) {
    217             height = Math.min(height, size);
    218         }
    219 
    220         int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY);
    221         if (mNotificationHeader != null) {
    222             mNotificationHeader.measure(widthMeasureSpec, headerHeightSpec);
    223         }
    224         if (mNotificationHeaderLowPriority != null) {
    225             headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY);
    226             mNotificationHeaderLowPriority.measure(widthMeasureSpec, headerHeightSpec);
    227         }
    228         if (mNotificationHeaderAmbient != null) {
    229             headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY);
    230             mNotificationHeaderAmbient.measure(widthMeasureSpec, headerHeightSpec);
    231         }
    232 
    233         setMeasuredDimension(width, height);
    234     }
    235 
    236     @Override
    237     public boolean hasOverlappingRendering() {
    238         return false;
    239     }
    240 
    241     @Override
    242     public boolean pointInView(float localX, float localY, float slop) {
    243         return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
    244                 localY < (mRealHeight + slop);
    245     }
    246 
    247     /**
    248      * Add a child notification to this view.
    249      *
    250      * @param row the row to add
    251      * @param childIndex the index to add it at, if -1 it will be added at the end
    252      */
    253     public void addNotification(ExpandableNotificationRow row, int childIndex) {
    254         int newIndex = childIndex < 0 ? mChildren.size() : childIndex;
    255         mChildren.add(newIndex, row);
    256         addView(row);
    257         row.setUserLocked(mUserLocked);
    258 
    259         View divider = inflateDivider();
    260         addView(divider);
    261         mDividers.add(newIndex, divider);
    262 
    263         updateGroupOverflow();
    264         row.setContentTransformationAmount(0, false /* isLastChild */);
    265     }
    266 
    267     public void removeNotification(ExpandableNotificationRow row) {
    268         int childIndex = mChildren.indexOf(row);
    269         mChildren.remove(row);
    270         removeView(row);
    271 
    272         final View divider = mDividers.remove(childIndex);
    273         removeView(divider);
    274         getOverlay().add(divider);
    275         CrossFadeHelper.fadeOut(divider, new Runnable() {
    276             @Override
    277             public void run() {
    278                 getOverlay().remove(divider);
    279             }
    280         });
    281 
    282         row.setSystemChildExpanded(false);
    283         row.setUserLocked(false);
    284         updateGroupOverflow();
    285         if (!row.isRemoved()) {
    286             mHeaderUtil.restoreNotificationHeader(row);
    287         }
    288     }
    289 
    290     /**
    291      * @return The number of notification children in the container.
    292      */
    293     public int getNotificationChildCount() {
    294         return mChildren.size();
    295     }
    296 
    297     public void recreateNotificationHeader(OnClickListener listener) {
    298         mHeaderClickListener = listener;
    299         StatusBarNotification notification = mContainingNotification.getStatusBarNotification();
    300         final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(),
    301                 notification.getNotification());
    302         RemoteViews header = builder.makeNotificationHeader(false /* ambient */);
    303         if (mNotificationHeader == null) {
    304             mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this);
    305             final View expandButton = mNotificationHeader.findViewById(
    306                     com.android.internal.R.id.expand_button);
    307             expandButton.setVisibility(VISIBLE);
    308             mNotificationHeader.setOnClickListener(mHeaderClickListener);
    309             mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(),
    310                     mNotificationHeader, mContainingNotification);
    311             addView(mNotificationHeader, 0);
    312             invalidate();
    313         } else {
    314             header.reapply(getContext(), mNotificationHeader);
    315         }
    316         mNotificationHeaderWrapper.onContentUpdated(mContainingNotification);
    317         recreateLowPriorityHeader(builder);
    318         recreateAmbientHeader(builder);
    319         updateHeaderVisibility(false /* animate */);
    320         updateChildrenHeaderAppearance();
    321     }
    322 
    323     private void recreateAmbientHeader(Notification.Builder builder) {
    324         RemoteViews header;
    325         StatusBarNotification notification = mContainingNotification.getStatusBarNotification();
    326         if (builder == null) {
    327             builder = Notification.Builder.recoverBuilder(getContext(),
    328                     notification.getNotification());
    329         }
    330         header = builder.makeNotificationHeader(true /* ambient */);
    331         if (mNotificationHeaderAmbient == null) {
    332             mNotificationHeaderAmbient = (ViewGroup) header.apply(getContext(), this);
    333             mNotificationHeaderWrapperAmbient = NotificationViewWrapper.wrap(getContext(),
    334                     mNotificationHeaderAmbient, mContainingNotification);
    335             mNotificationHeaderWrapperAmbient.onContentUpdated(mContainingNotification);
    336             addView(mNotificationHeaderAmbient, 0);
    337             invalidate();
    338         } else {
    339             header.reapply(getContext(), mNotificationHeaderAmbient);
    340         }
    341         resetHeaderVisibilityIfNeeded(mNotificationHeaderAmbient, calculateDesiredHeader());
    342         mNotificationHeaderWrapperAmbient.onContentUpdated(mContainingNotification);
    343     }
    344 
    345     /**
    346      * Recreate the low-priority header.
    347      *
    348      * @param builder a builder to reuse. Otherwise the builder will be recovered.
    349      */
    350     private void recreateLowPriorityHeader(Notification.Builder builder) {
    351         RemoteViews header;
    352         StatusBarNotification notification = mContainingNotification.getStatusBarNotification();
    353         if (mIsLowPriority) {
    354             if (builder == null) {
    355                 builder = Notification.Builder.recoverBuilder(getContext(),
    356                         notification.getNotification());
    357             }
    358             header = builder.makeLowPriorityContentView(true /* useRegularSubtext */);
    359             if (mNotificationHeaderLowPriority == null) {
    360                 mNotificationHeaderLowPriority = (NotificationHeaderView) header.apply(getContext(),
    361                         this);
    362                 final View expandButton = mNotificationHeaderLowPriority.findViewById(
    363                         com.android.internal.R.id.expand_button);
    364                 expandButton.setVisibility(VISIBLE);
    365                 mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener);
    366                 mNotificationHeaderWrapperLowPriority = NotificationViewWrapper.wrap(getContext(),
    367                         mNotificationHeaderLowPriority, mContainingNotification);
    368                 addView(mNotificationHeaderLowPriority, 0);
    369                 invalidate();
    370             } else {
    371                 header.reapply(getContext(), mNotificationHeaderLowPriority);
    372             }
    373             mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification);
    374             resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, calculateDesiredHeader());
    375         } else {
    376             removeView(mNotificationHeaderLowPriority);
    377             mNotificationHeaderLowPriority = null;
    378             mNotificationHeaderWrapperLowPriority = null;
    379         }
    380     }
    381 
    382     public void updateChildrenHeaderAppearance() {
    383         mHeaderUtil.updateChildrenHeaderAppearance();
    384     }
    385 
    386     public void updateGroupOverflow() {
    387         int childCount = mChildren.size();
    388         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
    389         if (childCount > maxAllowedVisibleChildren) {
    390             mOverflowNumber = mHybridGroupManager.bindOverflowNumber(
    391                     mOverflowNumber, childCount - maxAllowedVisibleChildren);
    392             if (mGroupOverFlowState == null) {
    393                 mGroupOverFlowState = new ViewState();
    394                 mNeverAppliedGroupState = true;
    395             }
    396         } else if (mOverflowNumber != null) {
    397             removeView(mOverflowNumber);
    398             if (isShown()) {
    399                 final View removedOverflowNumber = mOverflowNumber;
    400                 addTransientView(removedOverflowNumber, getTransientViewCount());
    401                 CrossFadeHelper.fadeOut(removedOverflowNumber, new Runnable() {
    402                     @Override
    403                     public void run() {
    404                         removeTransientView(removedOverflowNumber);
    405                     }
    406                 });
    407             }
    408             mOverflowNumber = null;
    409             mGroupOverFlowState = null;
    410         }
    411     }
    412 
    413     @Override
    414     protected void onConfigurationChanged(Configuration newConfig) {
    415         super.onConfigurationChanged(newConfig);
    416         updateGroupOverflow();
    417     }
    418 
    419     private View inflateDivider() {
    420         return LayoutInflater.from(mContext).inflate(
    421                 R.layout.notification_children_divider, this, false);
    422     }
    423 
    424     public List<ExpandableNotificationRow> getNotificationChildren() {
    425         return mChildren;
    426     }
    427 
    428     /**
    429      * Apply the order given in the list to the children.
    430      *
    431      * @param childOrder the new list order
    432      * @param visualStabilityManager
    433      * @param callback
    434      * @return whether the list order has changed
    435      */
    436     public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder,
    437             VisualStabilityManager visualStabilityManager,
    438             VisualStabilityManager.Callback callback) {
    439         if (childOrder == null) {
    440             return false;
    441         }
    442         boolean result = false;
    443         for (int i = 0; i < mChildren.size() && i < childOrder.size(); i++) {
    444             ExpandableNotificationRow child = mChildren.get(i);
    445             ExpandableNotificationRow desiredChild = childOrder.get(i);
    446             if (child != desiredChild) {
    447                 if (visualStabilityManager.canReorderNotification(desiredChild)) {
    448                     mChildren.remove(desiredChild);
    449                     mChildren.add(i, desiredChild);
    450                     result = true;
    451                 } else {
    452                     visualStabilityManager.addReorderingAllowedCallback(callback);
    453                 }
    454             }
    455         }
    456         updateExpansionStates();
    457         return result;
    458     }
    459 
    460     private void updateExpansionStates() {
    461         if (mChildrenExpanded || mUserLocked) {
    462             // we don't modify it the group is expanded or if we are expanding it
    463             return;
    464         }
    465         int size = mChildren.size();
    466         for (int i = 0; i < size; i++) {
    467             ExpandableNotificationRow child = mChildren.get(i);
    468             child.setSystemChildExpanded(i == 0 && size == 1);
    469         }
    470     }
    471 
    472     /**
    473      *
    474      * @return the intrinsic size of this children container, i.e the natural fully expanded state
    475      */
    476     public int getIntrinsicHeight() {
    477         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
    478         return getIntrinsicHeight(maxAllowedVisibleChildren);
    479     }
    480 
    481     /**
    482      * @return the intrinsic height with a number of children given
    483      *         in @param maxAllowedVisibleChildren
    484      */
    485     private int getIntrinsicHeight(float maxAllowedVisibleChildren) {
    486         if (showingAsLowPriority()) {
    487             return mNotificationHeaderLowPriority.getHeight();
    488         }
    489         int intrinsicHeight = mNotificationHeaderMargin;
    490         int visibleChildren = 0;
    491         int childCount = mChildren.size();
    492         boolean firstChild = true;
    493         float expandFactor = 0;
    494         if (mUserLocked) {
    495             expandFactor = getGroupExpandFraction();
    496         }
    497         boolean childrenExpanded = mChildrenExpanded || mContainingNotification.isShowingAmbient();
    498         for (int i = 0; i < childCount; i++) {
    499             if (visibleChildren >= maxAllowedVisibleChildren) {
    500                 break;
    501             }
    502             if (!firstChild) {
    503                 if (mUserLocked) {
    504                     intrinsicHeight += NotificationUtils.interpolate(mChildPadding, mDividerHeight,
    505                             expandFactor);
    506                 } else {
    507                     intrinsicHeight += childrenExpanded ? mDividerHeight : mChildPadding;
    508                 }
    509             } else {
    510                 if (mUserLocked) {
    511                     intrinsicHeight += NotificationUtils.interpolate(
    512                             0,
    513                             mNotificatonTopPadding + mDividerHeight,
    514                             expandFactor);
    515                 } else {
    516                     intrinsicHeight += childrenExpanded
    517                             ? mNotificatonTopPadding + mDividerHeight
    518                             : 0;
    519                 }
    520                 firstChild = false;
    521             }
    522             ExpandableNotificationRow child = mChildren.get(i);
    523             intrinsicHeight += child.getIntrinsicHeight();
    524             visibleChildren++;
    525         }
    526         if (mUserLocked) {
    527             intrinsicHeight += NotificationUtils.interpolate(mCollapsedBottompadding, 0.0f,
    528                     expandFactor);
    529         } else if (!childrenExpanded) {
    530             intrinsicHeight += mCollapsedBottompadding;
    531         }
    532         return intrinsicHeight;
    533     }
    534 
    535     /**
    536      * Update the state of all its children based on a linear layout algorithm.
    537      *
    538      * @param resultState the state to update
    539      * @param parentState the state of the parent
    540      */
    541     public void getState(StackScrollState resultState, ExpandableViewState parentState) {
    542         int childCount = mChildren.size();
    543         int yPosition = mNotificationHeaderMargin;
    544         boolean firstChild = true;
    545         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
    546         int lastVisibleIndex = maxAllowedVisibleChildren - 1;
    547         int firstOverflowIndex = lastVisibleIndex + 1;
    548         float expandFactor = 0;
    549         boolean expandingToExpandedGroup = mUserLocked && !showingAsLowPriority();
    550         if (mUserLocked) {
    551             expandFactor = getGroupExpandFraction();
    552             firstOverflowIndex = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
    553         }
    554 
    555         boolean childrenExpandedAndNotAnimating = mChildrenExpanded
    556                 && !mContainingNotification.isGroupExpansionChanging();
    557         for (int i = 0; i < childCount; i++) {
    558             ExpandableNotificationRow child = mChildren.get(i);
    559             if (!firstChild) {
    560                 if (expandingToExpandedGroup) {
    561                     yPosition += NotificationUtils.interpolate(mChildPadding, mDividerHeight,
    562                             expandFactor);
    563                 } else {
    564                     yPosition += mChildrenExpanded ? mDividerHeight : mChildPadding;
    565                 }
    566             } else {
    567                 if (expandingToExpandedGroup) {
    568                     yPosition += NotificationUtils.interpolate(
    569                             0,
    570                             mNotificatonTopPadding + mDividerHeight,
    571                             expandFactor);
    572                 } else {
    573                     yPosition += mChildrenExpanded ? mNotificatonTopPadding + mDividerHeight : 0;
    574                 }
    575                 firstChild = false;
    576             }
    577 
    578             ExpandableViewState childState = resultState.getViewStateForView(child);
    579             int intrinsicHeight = child.getIntrinsicHeight();
    580             childState.height = intrinsicHeight;
    581             childState.yTranslation = yPosition;
    582             childState.hidden = false;
    583             // When the group is expanded, the children cast the shadows rather than the parent
    584             // so use the parent's elevation here.
    585             childState.zTranslation =
    586                     (childrenExpandedAndNotAnimating && mEnableShadowOnChildNotifications)
    587                     ? mContainingNotification.getTranslationZ()
    588                     : 0;
    589             childState.dimmed = parentState.dimmed;
    590             childState.dark = parentState.dark;
    591             childState.hideSensitive = parentState.hideSensitive;
    592             childState.belowSpeedBump = parentState.belowSpeedBump;
    593             childState.clipTopAmount = 0;
    594             childState.alpha = 0;
    595             if (i < firstOverflowIndex) {
    596                 childState.alpha = showingAsLowPriority() ? expandFactor : 1.0f;
    597             } else if (expandFactor == 1.0f && i <= lastVisibleIndex) {
    598                 childState.alpha = (mActualHeight - childState.yTranslation) / childState.height;
    599                 childState.alpha = Math.max(0.0f, Math.min(1.0f, childState.alpha));
    600             }
    601             childState.location = parentState.location;
    602             childState.inShelf = parentState.inShelf;
    603             yPosition += intrinsicHeight;
    604 
    605         }
    606         if (mOverflowNumber != null) {
    607             ExpandableNotificationRow overflowView = mChildren.get(Math.min(
    608                     getMaxAllowedVisibleChildren(true /* likeCollpased */), childCount) - 1);
    609             mGroupOverFlowState.copyFrom(resultState.getViewStateForView(overflowView));
    610 
    611             if (mContainingNotification.isShowingAmbient() || !mChildrenExpanded) {
    612                 HybridNotificationView alignView = null;
    613                 if (mContainingNotification.isShowingAmbient()) {
    614                     alignView = overflowView.getAmbientSingleLineView();
    615                 } else if (mUserLocked) {
    616                     alignView = overflowView.getSingleLineView();
    617                 }
    618                 if (alignView != null) {
    619                     View mirrorView = alignView.getTextView();
    620                     if (mirrorView.getVisibility() == GONE) {
    621                         mirrorView = alignView.getTitleView();
    622                     }
    623                     if (mirrorView.getVisibility() == GONE) {
    624                         mirrorView = alignView;
    625                     }
    626                     mGroupOverFlowState.yTranslation += NotificationUtils.getRelativeYOffset(
    627                             mirrorView, overflowView);
    628                     mGroupOverFlowState.alpha = mirrorView.getAlpha();
    629                 }
    630             } else {
    631                 mGroupOverFlowState.yTranslation += mNotificationHeaderMargin;
    632                 mGroupOverFlowState.alpha = 0.0f;
    633             }
    634         }
    635         if (mNotificationHeader != null) {
    636             if (mHeaderViewState == null) {
    637                 mHeaderViewState = new ViewState();
    638             }
    639             mHeaderViewState.initFrom(mNotificationHeader);
    640             mHeaderViewState.zTranslation = childrenExpandedAndNotAnimating
    641                     ? mContainingNotification.getTranslationZ()
    642                     : 0;
    643         }
    644     }
    645 
    646     /**
    647      * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its
    648      * height, children in the group after this are gone.
    649      *
    650      * @param child the child who's height to adjust.
    651      * @param parentHeight the height of the parent.
    652      * @param childState the state to update.
    653      * @param yPosition the yPosition of the view.
    654      * @return true if children after this one should be hidden.
    655      */
    656     private boolean updateChildStateForExpandedGroup(ExpandableNotificationRow child,
    657             int parentHeight, ExpandableViewState childState, int yPosition) {
    658         final int top = yPosition + child.getClipTopAmount();
    659         final int intrinsicHeight = child.getIntrinsicHeight();
    660         final int bottom = top + intrinsicHeight;
    661         int newHeight = intrinsicHeight;
    662         if (bottom >= parentHeight) {
    663             // Child is either clipped or gone
    664             newHeight = Math.max((parentHeight - top), 0);
    665         }
    666         childState.hidden = newHeight == 0;
    667         childState.height = newHeight;
    668         return childState.height != intrinsicHeight && !childState.hidden;
    669     }
    670 
    671     private int getMaxAllowedVisibleChildren() {
    672         return getMaxAllowedVisibleChildren(false /* likeCollapsed */);
    673     }
    674 
    675     private int getMaxAllowedVisibleChildren(boolean likeCollapsed) {
    676         if (mContainingNotification.isShowingAmbient()) {
    677             return NUMBER_OF_CHILDREN_WHEN_AMBIENT;
    678         }
    679         if (!likeCollapsed && (mChildrenExpanded || mContainingNotification.isUserLocked())) {
    680             return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED;
    681         }
    682         if (mIsLowPriority || !mContainingNotification.isOnKeyguard()
    683                 && (mContainingNotification.isExpanded() || mContainingNotification.isHeadsUp())) {
    684             return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED;
    685         }
    686         return NUMBER_OF_CHILDREN_WHEN_COLLAPSED;
    687     }
    688 
    689     public void applyState(StackScrollState state) {
    690         int childCount = mChildren.size();
    691         ViewState tmpState = new ViewState();
    692         float expandFraction = 0.0f;
    693         if (mUserLocked) {
    694             expandFraction = getGroupExpandFraction();
    695         }
    696         final boolean dividersVisible = mUserLocked && !showingAsLowPriority()
    697                 || (mChildrenExpanded && mShowDividersWhenExpanded)
    698                 || (mContainingNotification.isGroupExpansionChanging()
    699                 && !mHideDividersDuringExpand);
    700         for (int i = 0; i < childCount; i++) {
    701             ExpandableNotificationRow child = mChildren.get(i);
    702             ExpandableViewState viewState = state.getViewStateForView(child);
    703             viewState.applyToView(child);
    704 
    705             // layout the divider
    706             View divider = mDividers.get(i);
    707             tmpState.initFrom(divider);
    708             tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
    709             float alpha = mChildrenExpanded && viewState.alpha != 0 ? mDividerAlpha : 0;
    710             if (mUserLocked && !showingAsLowPriority() && viewState.alpha != 0) {
    711                 alpha = NotificationUtils.interpolate(0, 0.5f,
    712                         Math.min(viewState.alpha, expandFraction));
    713             }
    714             tmpState.hidden = !dividersVisible;
    715             tmpState.alpha = alpha;
    716             tmpState.applyToView(divider);
    717             // There is no fake shadow to be drawn on the children
    718             child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
    719         }
    720         if (mGroupOverFlowState != null) {
    721             mGroupOverFlowState.applyToView(mOverflowNumber);
    722             mNeverAppliedGroupState = false;
    723         }
    724         if (mHeaderViewState != null) {
    725             mHeaderViewState.applyToView(mNotificationHeader);
    726         }
    727         updateChildrenClipping();
    728     }
    729 
    730     private void updateChildrenClipping() {
    731         int childCount = mChildren.size();
    732         int layoutEnd = mContainingNotification.getActualHeight() - mClipBottomAmount;
    733         for (int i = 0; i < childCount; i++) {
    734             ExpandableNotificationRow child = mChildren.get(i);
    735             if (child.getVisibility() == GONE) {
    736                 continue;
    737             }
    738             float childTop = child.getTranslationY();
    739             float childBottom = childTop + child.getActualHeight();
    740             boolean visible = true;
    741             int clipBottomAmount = 0;
    742             if (childTop > layoutEnd) {
    743                 visible = false;
    744             } else if (childBottom > layoutEnd) {
    745                 clipBottomAmount = (int) (childBottom - layoutEnd);
    746             }
    747 
    748             boolean isVisible = child.getVisibility() == VISIBLE;
    749             if (visible != isVisible) {
    750                 child.setVisibility(visible ? VISIBLE : INVISIBLE);
    751             }
    752 
    753             child.setClipBottomAmount(clipBottomAmount);
    754         }
    755     }
    756 
    757     /**
    758      * This is called when the children expansion has changed and positions the children properly
    759      * for an appear animation.
    760      *
    761      * @param state the new state we animate to
    762      */
    763     public void prepareExpansionChanged(StackScrollState state) {
    764         // TODO: do something that makes sense, like placing the invisible views correctly
    765         return;
    766     }
    767 
    768     public void startAnimationToState(StackScrollState state, AnimationProperties properties) {
    769         int childCount = mChildren.size();
    770         ViewState tmpState = new ViewState();
    771         float expandFraction = getGroupExpandFraction();
    772         final boolean dividersVisible = mUserLocked && !showingAsLowPriority()
    773                 || (mChildrenExpanded && mShowDividersWhenExpanded)
    774                 || (mContainingNotification.isGroupExpansionChanging()
    775                 && !mHideDividersDuringExpand);
    776         for (int i = childCount - 1; i >= 0; i--) {
    777             ExpandableNotificationRow child = mChildren.get(i);
    778             ExpandableViewState viewState = state.getViewStateForView(child);
    779             viewState.animateTo(child, properties);
    780 
    781             // layout the divider
    782             View divider = mDividers.get(i);
    783             tmpState.initFrom(divider);
    784             tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
    785             float alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0;
    786             if (mUserLocked && !showingAsLowPriority() && viewState.alpha != 0) {
    787                 alpha = NotificationUtils.interpolate(0, 0.5f,
    788                         Math.min(viewState.alpha, expandFraction));
    789             }
    790             tmpState.hidden = !dividersVisible;
    791             tmpState.alpha = alpha;
    792             tmpState.animateTo(divider, properties);
    793             // There is no fake shadow to be drawn on the children
    794             child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
    795         }
    796         if (mOverflowNumber != null) {
    797             if (mNeverAppliedGroupState) {
    798                 float alpha = mGroupOverFlowState.alpha;
    799                 mGroupOverFlowState.alpha = 0;
    800                 mGroupOverFlowState.applyToView(mOverflowNumber);
    801                 mGroupOverFlowState.alpha = alpha;
    802                 mNeverAppliedGroupState = false;
    803             }
    804             mGroupOverFlowState.animateTo(mOverflowNumber, properties);
    805         }
    806         if (mNotificationHeader != null) {
    807             mHeaderViewState.applyToView(mNotificationHeader);
    808         }
    809         updateChildrenClipping();
    810     }
    811 
    812     public ExpandableNotificationRow getViewAtPosition(float y) {
    813         // find the view under the pointer, accounting for GONE views
    814         final int count = mChildren.size();
    815         for (int childIdx = 0; childIdx < count; childIdx++) {
    816             ExpandableNotificationRow slidingChild = mChildren.get(childIdx);
    817             float childTop = slidingChild.getTranslationY();
    818             float top = childTop + slidingChild.getClipTopAmount();
    819             float bottom = childTop + slidingChild.getActualHeight();
    820             if (y >= top && y <= bottom) {
    821                 return slidingChild;
    822             }
    823         }
    824         return null;
    825     }
    826 
    827     public void setChildrenExpanded(boolean childrenExpanded) {
    828         mChildrenExpanded = childrenExpanded;
    829         updateExpansionStates();
    830         if (mNotificationHeader != null) {
    831             mNotificationHeader.setExpanded(childrenExpanded);
    832         }
    833         final int count = mChildren.size();
    834         for (int childIdx = 0; childIdx < count; childIdx++) {
    835             ExpandableNotificationRow child = mChildren.get(childIdx);
    836             child.setChildrenExpanded(childrenExpanded, false);
    837         }
    838     }
    839 
    840     public void setContainingNotification(ExpandableNotificationRow parent) {
    841         mContainingNotification = parent;
    842         mHeaderUtil = new NotificationHeaderUtil(mContainingNotification);
    843     }
    844 
    845     public ExpandableNotificationRow getContainingNotification() {
    846         return mContainingNotification;
    847     }
    848 
    849     public NotificationHeaderView getHeaderView() {
    850         return mNotificationHeader;
    851     }
    852 
    853     public NotificationHeaderView getLowPriorityHeaderView() {
    854         return mNotificationHeaderLowPriority;
    855     }
    856 
    857     @VisibleForTesting
    858     public ViewGroup getCurrentHeaderView() {
    859         return mCurrentHeader;
    860     }
    861 
    862     public void notifyShowAmbientChanged() {
    863         updateHeaderVisibility(false);
    864     }
    865 
    866     private void updateHeaderVisibility(boolean animate) {
    867         ViewGroup desiredHeader;
    868         ViewGroup currentHeader = mCurrentHeader;
    869         desiredHeader = calculateDesiredHeader();
    870 
    871         if (currentHeader == desiredHeader) {
    872             return;
    873         }
    874         if (desiredHeader == mNotificationHeaderAmbient
    875                 || currentHeader == mNotificationHeaderAmbient) {
    876             animate = false;
    877         }
    878 
    879         if (animate) {
    880             if (desiredHeader != null && currentHeader != null) {
    881                 currentHeader.setVisibility(VISIBLE);
    882                 desiredHeader.setVisibility(VISIBLE);
    883                 NotificationViewWrapper visibleWrapper = getWrapperForView(desiredHeader);
    884                 NotificationViewWrapper hiddenWrapper = getWrapperForView(currentHeader);
    885                 visibleWrapper.transformFrom(hiddenWrapper);
    886                 hiddenWrapper.transformTo(visibleWrapper, () -> updateHeaderVisibility(false));
    887                 startChildAlphaAnimations(desiredHeader == mNotificationHeader);
    888             } else {
    889                 animate = false;
    890             }
    891         }
    892         if (!animate) {
    893             if (desiredHeader != null) {
    894                 getWrapperForView(desiredHeader).setVisible(true);
    895                 desiredHeader.setVisibility(VISIBLE);
    896             }
    897             if (currentHeader != null) {
    898                 // Wrapper can be null if we were a low priority notification
    899                 // and just destroyed it by calling setIsLowPriority(false)
    900                 NotificationViewWrapper wrapper = getWrapperForView(currentHeader);
    901                 if (wrapper != null) {
    902                     wrapper.setVisible(false);
    903                 }
    904                 currentHeader.setVisibility(INVISIBLE);
    905             }
    906         }
    907 
    908         resetHeaderVisibilityIfNeeded(mNotificationHeader, desiredHeader);
    909         resetHeaderVisibilityIfNeeded(mNotificationHeaderAmbient, desiredHeader);
    910         resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, desiredHeader);
    911 
    912         mCurrentHeader = desiredHeader;
    913     }
    914 
    915     private void resetHeaderVisibilityIfNeeded(View header, View desiredHeader) {
    916         if (header == null) {
    917             return;
    918         }
    919         if (header != mCurrentHeader && header != desiredHeader) {
    920             getWrapperForView(header).setVisible(false);
    921             header.setVisibility(INVISIBLE);
    922         }
    923         if (header == desiredHeader && header.getVisibility() != VISIBLE) {
    924             getWrapperForView(header).setVisible(true);
    925             header.setVisibility(VISIBLE);
    926         }
    927     }
    928 
    929     private ViewGroup calculateDesiredHeader() {
    930         ViewGroup desiredHeader;
    931         if (mContainingNotification.isShowingAmbient()) {
    932             desiredHeader = mNotificationHeaderAmbient;
    933         } else if (showingAsLowPriority()) {
    934             desiredHeader = mNotificationHeaderLowPriority;
    935         } else {
    936             desiredHeader = mNotificationHeader;
    937         }
    938         return desiredHeader;
    939     }
    940 
    941     private void startChildAlphaAnimations(boolean toVisible) {
    942         float target = toVisible ? 1.0f : 0.0f;
    943         float start = 1.0f - target;
    944         int childCount = mChildren.size();
    945         for (int i = 0; i < childCount; i++) {
    946             if (i >= NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED) {
    947                 break;
    948             }
    949             ExpandableNotificationRow child = mChildren.get(i);
    950             child.setAlpha(start);
    951             ViewState viewState = new ViewState();
    952             viewState.initFrom(child);
    953             viewState.alpha = target;
    954             ALPHA_FADE_IN.setDelay(i * 50);
    955             viewState.animateTo(child, ALPHA_FADE_IN);
    956         }
    957     }
    958 
    959 
    960     private void updateHeaderTransformation() {
    961         if (mUserLocked && showingAsLowPriority()) {
    962             float fraction = getGroupExpandFraction();
    963             mNotificationHeaderWrapper.transformFrom(mNotificationHeaderWrapperLowPriority,
    964                     fraction);
    965             mNotificationHeader.setVisibility(VISIBLE);
    966             mNotificationHeaderWrapperLowPriority.transformTo(mNotificationHeaderWrapper,
    967                     fraction);
    968         }
    969 
    970     }
    971 
    972     private NotificationViewWrapper getWrapperForView(View visibleHeader) {
    973         if (visibleHeader == mNotificationHeader) {
    974             return mNotificationHeaderWrapper;
    975         }
    976         if (visibleHeader == mNotificationHeaderAmbient) {
    977             return mNotificationHeaderWrapperAmbient;
    978         }
    979         return mNotificationHeaderWrapperLowPriority;
    980     }
    981 
    982     /**
    983      * Called when a groups expansion changes to adjust the background of the header view.
    984      *
    985      * @param expanded whether the group is expanded.
    986      */
    987     public void updateHeaderForExpansion(boolean expanded) {
    988         if (mNotificationHeader != null) {
    989             if (expanded) {
    990                 ColorDrawable cd = new ColorDrawable();
    991                 cd.setColor(mContainingNotification.calculateBgColor());
    992                 mNotificationHeader.setHeaderBackgroundDrawable(cd);
    993             } else {
    994                 mNotificationHeader.setHeaderBackgroundDrawable(null);
    995             }
    996         }
    997     }
    998 
    999     public int getMaxContentHeight() {
   1000         if (showingAsLowPriority()) {
   1001             return getMinHeight(NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED, true
   1002                     /* likeHighPriority */);
   1003         }
   1004         int maxContentHeight = mNotificationHeaderMargin + mNotificatonTopPadding;
   1005         int visibleChildren = 0;
   1006         int childCount = mChildren.size();
   1007         for (int i = 0; i < childCount; i++) {
   1008             if (visibleChildren >= NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED) {
   1009                 break;
   1010             }
   1011             ExpandableNotificationRow child = mChildren.get(i);
   1012             float childHeight = child.isExpanded(true /* allowOnKeyguard */)
   1013                     ? child.getMaxExpandHeight()
   1014                     : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
   1015             maxContentHeight += childHeight;
   1016             visibleChildren++;
   1017         }
   1018         if (visibleChildren > 0) {
   1019             maxContentHeight += visibleChildren * mDividerHeight;
   1020         }
   1021         return maxContentHeight;
   1022     }
   1023 
   1024     public void setActualHeight(int actualHeight) {
   1025         if (!mUserLocked) {
   1026             return;
   1027         }
   1028         mActualHeight = actualHeight;
   1029         float fraction = getGroupExpandFraction();
   1030         boolean showingLowPriority = showingAsLowPriority();
   1031         updateHeaderTransformation();
   1032         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
   1033         int childCount = mChildren.size();
   1034         for (int i = 0; i < childCount; i++) {
   1035             ExpandableNotificationRow child = mChildren.get(i);
   1036             float childHeight;
   1037             if (showingLowPriority) {
   1038                 childHeight = child.getShowingLayout().getMinHeight(false /* likeGroupExpanded */);
   1039             } else if (child.isExpanded(true /* allowOnKeyguard */)) {
   1040                 childHeight = child.getMaxExpandHeight();
   1041             } else {
   1042                 childHeight = child.getShowingLayout().getMinHeight(
   1043                         true /* likeGroupExpanded */);
   1044             }
   1045             if (i < maxAllowedVisibleChildren) {
   1046                 float singleLineHeight = child.getShowingLayout().getMinHeight(
   1047                         false /* likeGroupExpanded */);
   1048                 child.setActualHeight((int) NotificationUtils.interpolate(singleLineHeight,
   1049                         childHeight, fraction), false);
   1050             } else {
   1051                 child.setActualHeight((int) childHeight, false);
   1052             }
   1053         }
   1054     }
   1055 
   1056     public float getGroupExpandFraction() {
   1057         int visibleChildrenExpandedHeight = showingAsLowPriority() ? getMaxContentHeight()
   1058                 : getVisibleChildrenExpandHeight();
   1059         int minExpandHeight = getCollapsedHeight();
   1060         float factor = (mActualHeight - minExpandHeight)
   1061                 / (float) (visibleChildrenExpandedHeight - minExpandHeight);
   1062         return Math.max(0.0f, Math.min(1.0f, factor));
   1063     }
   1064 
   1065     private int getVisibleChildrenExpandHeight() {
   1066         int intrinsicHeight = mNotificationHeaderMargin + mNotificatonTopPadding + mDividerHeight;
   1067         int visibleChildren = 0;
   1068         int childCount = mChildren.size();
   1069         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
   1070         for (int i = 0; i < childCount; i++) {
   1071             if (visibleChildren >= maxAllowedVisibleChildren) {
   1072                 break;
   1073             }
   1074             ExpandableNotificationRow child = mChildren.get(i);
   1075             float childHeight = child.isExpanded(true /* allowOnKeyguard */)
   1076                     ? child.getMaxExpandHeight()
   1077                     : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
   1078             intrinsicHeight += childHeight;
   1079             visibleChildren++;
   1080         }
   1081         return intrinsicHeight;
   1082     }
   1083 
   1084     public int getMinHeight() {
   1085         return getMinHeight(mContainingNotification.isShowingAmbient()
   1086                 ? NUMBER_OF_CHILDREN_WHEN_AMBIENT
   1087                 : NUMBER_OF_CHILDREN_WHEN_COLLAPSED, false /* likeHighPriority */);
   1088     }
   1089 
   1090     public int getCollapsedHeight() {
   1091         return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */),
   1092                 false /* likeHighPriority */);
   1093     }
   1094 
   1095     /**
   1096      * Get the minimum Height for this group.
   1097      *
   1098      * @param maxAllowedVisibleChildren the number of children that should be visible
   1099      * @param likeHighPriority if the height should be calculated as if it were not low priority
   1100      */
   1101     private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority) {
   1102         if (!likeHighPriority && showingAsLowPriority()) {
   1103             return mNotificationHeaderLowPriority.getHeight();
   1104         }
   1105         int minExpandHeight = mNotificationHeaderMargin;
   1106         int visibleChildren = 0;
   1107         boolean firstChild = true;
   1108         int childCount = mChildren.size();
   1109         for (int i = 0; i < childCount; i++) {
   1110             if (visibleChildren >= maxAllowedVisibleChildren) {
   1111                 break;
   1112             }
   1113             if (!firstChild) {
   1114                 minExpandHeight += mChildPadding;
   1115             } else {
   1116                 firstChild = false;
   1117             }
   1118             ExpandableNotificationRow child = mChildren.get(i);
   1119             minExpandHeight += child.getSingleLineView().getHeight();
   1120             visibleChildren++;
   1121         }
   1122         minExpandHeight += mCollapsedBottompadding;
   1123         return minExpandHeight;
   1124     }
   1125 
   1126     public boolean showingAsLowPriority() {
   1127         return mIsLowPriority && !mContainingNotification.isExpanded();
   1128     }
   1129 
   1130     public void setDark(boolean dark, boolean fade, long delay) {
   1131         if (mOverflowNumber != null) {
   1132             mHybridGroupManager.setOverflowNumberDark(mOverflowNumber, dark, fade, delay);
   1133         }
   1134         mNotificationHeaderWrapper.setDark(dark, fade, delay);
   1135     }
   1136 
   1137     public void reInflateViews(OnClickListener listener, StatusBarNotification notification) {
   1138         if (mNotificationHeader != null) {
   1139             removeView(mNotificationHeader);
   1140             mNotificationHeader = null;
   1141         }
   1142         if (mNotificationHeaderLowPriority != null) {
   1143             removeView(mNotificationHeaderLowPriority);
   1144             mNotificationHeaderLowPriority = null;
   1145         }
   1146         if (mNotificationHeaderAmbient != null) {
   1147             removeView(mNotificationHeaderAmbient);
   1148             mNotificationHeaderAmbient = null;
   1149         }
   1150         recreateNotificationHeader(listener);
   1151         initDimens();
   1152         for (int i = 0; i < mDividers.size(); i++) {
   1153             View prevDivider = mDividers.get(i);
   1154             int index = indexOfChild(prevDivider);
   1155             removeView(prevDivider);
   1156             View divider = inflateDivider();
   1157             addView(divider, index);
   1158             mDividers.set(i, divider);
   1159         }
   1160         removeView(mOverflowNumber);
   1161         mOverflowNumber = null;
   1162         mGroupOverFlowState = null;
   1163         updateGroupOverflow();
   1164     }
   1165 
   1166     public void setUserLocked(boolean userLocked) {
   1167         mUserLocked = userLocked;
   1168         if (!mUserLocked) {
   1169             updateHeaderVisibility(false /* animate */);
   1170         }
   1171         int childCount = mChildren.size();
   1172         for (int i = 0; i < childCount; i++) {
   1173             ExpandableNotificationRow child = mChildren.get(i);
   1174             child.setUserLocked(userLocked && !showingAsLowPriority());
   1175         }
   1176     }
   1177 
   1178     public void onNotificationUpdated() {
   1179         mHybridGroupManager.setOverflowNumberColor(mOverflowNumber,
   1180                 mContainingNotification.getNotificationColor(),
   1181                 mContainingNotification.getNotificationColorAmbient());
   1182     }
   1183 
   1184     public int getPositionInLinearLayout(View childInGroup) {
   1185         int position = mNotificationHeaderMargin + mNotificatonTopPadding;
   1186 
   1187         for (int i = 0; i < mChildren.size(); i++) {
   1188             ExpandableNotificationRow child = mChildren.get(i);
   1189             boolean notGone = child.getVisibility() != View.GONE;
   1190             if (notGone) {
   1191                 position += mDividerHeight;
   1192             }
   1193             if (child == childInGroup) {
   1194                 return position;
   1195             }
   1196             if (notGone) {
   1197                 position += child.getIntrinsicHeight();
   1198             }
   1199         }
   1200         return 0;
   1201     }
   1202 
   1203     public void setIconsVisible(boolean iconsVisible) {
   1204         if (mNotificationHeaderWrapper != null) {
   1205             NotificationHeaderView header = mNotificationHeaderWrapper.getNotificationHeader();
   1206             if (header != null) {
   1207                 header.getIcon().setForceHidden(!iconsVisible);
   1208             }
   1209         }
   1210         if (mNotificationHeaderWrapperLowPriority != null) {
   1211             NotificationHeaderView header
   1212                     = mNotificationHeaderWrapperLowPriority.getNotificationHeader();
   1213             if (header != null) {
   1214                 header.getIcon().setForceHidden(!iconsVisible);
   1215             }
   1216         }
   1217     }
   1218 
   1219     public void setClipBottomAmount(int clipBottomAmount) {
   1220         mClipBottomAmount = clipBottomAmount;
   1221         updateChildrenClipping();
   1222     }
   1223 
   1224     public void setIsLowPriority(boolean isLowPriority) {
   1225         mIsLowPriority = isLowPriority;
   1226         if (mContainingNotification != null) { /* we're not yet set up yet otherwise */
   1227             recreateLowPriorityHeader(null /* existingBuilder */);
   1228             updateHeaderVisibility(false /* animate */);
   1229         }
   1230         if (mUserLocked) {
   1231             setUserLocked(mUserLocked);
   1232         }
   1233     }
   1234 
   1235     public NotificationHeaderView getVisibleHeader() {
   1236         NotificationHeaderView header = mNotificationHeader;
   1237         if (showingAsLowPriority()) {
   1238             header = mNotificationHeaderLowPriority;
   1239         }
   1240         return header;
   1241     }
   1242 
   1243     public void onExpansionChanged() {
   1244         if (mIsLowPriority) {
   1245             if (mUserLocked) {
   1246                 setUserLocked(mUserLocked);
   1247             }
   1248             updateHeaderVisibility(true /* animate */);
   1249         }
   1250     }
   1251 
   1252     public float getIncreasedPaddingAmount() {
   1253         if (showingAsLowPriority()) {
   1254             return 0.0f;
   1255         }
   1256         return getGroupExpandFraction();
   1257     }
   1258 
   1259     @VisibleForTesting
   1260     public boolean isUserLocked() {
   1261         return mUserLocked;
   1262     }
   1263 }
   1264