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