Enable surfacing of notification children

Yo Dawg, I herd you like notifications, so I put a
notification in your notification so you can be
interrupted while you are being interrupted.

Bug: 15869874
Bug: 15188947
Change-Id: I6c733d6f8e8a04f85036182f82d3e945c6feb5bc
diff --git a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
index aa7ce4e..bc7f745 100644
--- a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
@@ -146,14 +146,14 @@
         }
         public void setHeight(float h) {
             if (DEBUG_SCALE) Log.v(TAG, "SetHeight: setting to " + h);
-            mView.setActualHeight((int) h);
+            mView.setContentHeight((int) h);
             mCurrentHeight = h;
         }
         public float getHeight() {
-            return mView.getActualHeight();
+            return mView.getContentHeight();
         }
         public int getNaturalHeight(int maximum) {
-            return Math.min(maximum, mView.getMaxHeight());
+            return Math.min(maximum, mView.getMaxContentHeight());
         }
     }
 
@@ -386,7 +386,8 @@
     }
 
     private boolean isFullyExpanded(ExpandableView underFocus) {
-        return underFocus.getIntrinsicHeight() == underFocus.getMaxHeight();
+        return underFocus.areChildrenExpanded() || underFocus.getIntrinsicHeight()
+                - underFocus.getBottomDecorHeight() == underFocus.getMaxContentHeight();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
index e021cd4..0e5fd94 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
@@ -94,7 +94,7 @@
             = new PathInterpolator(0, 0, 0.5f, 1);
     private final int mTintedRippleColor;
     private final int mLowPriorityRippleColor;
-    private final int mNormalRippleColor;
+    protected final int mNormalRippleColor;
 
     private boolean mDimmed;
     private boolean mDark;
@@ -115,7 +115,7 @@
     private OnActivatedListener mOnActivatedListener;
 
     private final Interpolator mLinearOutSlowInInterpolator;
-    private final Interpolator mFastOutSlowInInterpolator;
+    protected final Interpolator mFastOutSlowInInterpolator;
     private final Interpolator mSlowOutFastInInterpolator;
     private final Interpolator mSlowOutLinearInInterpolator;
     private final Interpolator mLinearInterpolator;
@@ -678,7 +678,7 @@
         }
     }
 
-    private int getRippleColor() {
+    protected int getRippleColor() {
         if (mBgTint != 0) {
             return mTintedRippleColor;
         } else if (mShowingLegacyBackground) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedButton.java
new file mode 100644
index 0000000..87c12c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedButton.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Button;
+
+/**
+ * A Button which doesn't have overlapping drawing commands
+ */
+public class AlphaOptimizedButton extends Button {
+    public AlphaOptimizedButton(Context context) {
+        super(context);
+    }
+
+    public AlphaOptimizedButton(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public AlphaOptimizedButton(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public AlphaOptimizedButton(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index d92ae44..fab7409 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -122,6 +122,8 @@
 
     public static final boolean ENABLE_REMOTE_INPUT =
             Build.IS_DEBUGGABLE && SystemProperties.getBoolean("debug.enable_remote_input", false);
+    public static final boolean ENABLE_CHILD_NOTIFICATIONS = Build.IS_DEBUGGABLE
+                    && SystemProperties.getBoolean("debug.child_notifs", false);
 
     protected static final int MSG_SHOW_RECENT_APPS = 1019;
     protected static final int MSG_HIDE_RECENT_APPS = 1020;
@@ -437,10 +439,12 @@
                         boolean isUpdate = mNotificationData.get(sbn.getKey()) != null
                                 || isHeadsUp(sbn.getKey());
 
-                        // Ignore children of notifications that have a summary, since we're not
-                        // going to show them anyway. This is true also when the summary is canceled,
+                        // In case we don't allow child notifications, we ignore children of
+                        // notifications that have a summary, since we're not going to show them
+                        // anyway. This is true also when the summary is canceled,
                         // because children are automatically canceled by NoMan in that case.
-                        if (mGroupManager.isChildInGroupWithSummary(sbn)) {
+                        if (!ENABLE_CHILD_NOTIFICATIONS
+                            && mGroupManager.isChildInGroupWithSummary(sbn)) {
                             if (DEBUG) {
                                 Log.d(TAG, "Ignoring group child due to existing summary: " + sbn);
                             }
@@ -1352,6 +1356,7 @@
             row = (ExpandableNotificationRow) inflater.inflate(R.layout.status_bar_notification_row,
                     parent, false);
             row.setExpansionLogger(this, entry.notification.getKey());
+            row.setGroupManager(mGroupManager);
         }
 
         workAroundBadLayerDrawableOpacity(row);
@@ -1867,22 +1872,25 @@
                     entry.row.setSystemExpanded(top);
                 }
             }
+            boolean isInvisibleChild = !mGroupManager.isVisible(entry.notification);
             boolean showOnKeyguard = shouldShowOnKeyguard(entry.notification);
             if ((isLockscreenPublicMode() && !mShowLockscreenNotifications) ||
                     (onKeyguard && (visibleNotifications >= maxKeyguardNotifications
-                            || !showOnKeyguard))) {
+                            || !showOnKeyguard || isInvisibleChild))) {
                 entry.row.setVisibility(View.GONE);
-                if (onKeyguard && showOnKeyguard) {
+                if (onKeyguard && showOnKeyguard && !isInvisibleChild) {
                     mKeyguardIconOverflowContainer.getIconsView().addNotification(entry);
                 }
             } else {
                 boolean wasGone = entry.row.getVisibility() == View.GONE;
                 entry.row.setVisibility(View.VISIBLE);
-                if (wasGone) {
-                    // notify the scroller of a child addition
-                    mStackScroller.generateAddAnimation(entry.row, true /* fromMoreCard */);
+                if (!isInvisibleChild) {
+                    if (wasGone) {
+                        // notify the scroller of a child addition
+                        mStackScroller.generateAddAnimation(entry.row, true /* fromMoreCard */);
+                    }
+                    visibleNotifications++;
                 }
-                visibleNotifications++;
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java
index c9f0260..15a092c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/DragDownHelper.java
@@ -162,20 +162,20 @@
                 ? RUBBERBAND_FACTOR_EXPANDABLE
                 : RUBBERBAND_FACTOR_STATIC;
         float rubberband = heightDelta * rubberbandFactor;
-        if (expandable && (rubberband + child.getMinHeight()) > child.getMaxHeight()) {
-            float overshoot = (rubberband + child.getMinHeight()) - child.getMaxHeight();
+        if (expandable && (rubberband + child.getMinHeight()) > child.getMaxContentHeight()) {
+            float overshoot = (rubberband + child.getMinHeight()) - child.getMaxContentHeight();
             overshoot *= (1 - RUBBERBAND_FACTOR_STATIC);
             rubberband -= overshoot;
         }
-        child.setActualHeight((int) (child.getMinHeight() + rubberband));
+        child.setContentHeight((int) (child.getMinHeight() + rubberband));
     }
 
     private void cancelExpansion(final ExpandableView child) {
-        if (child.getActualHeight() == child.getMinHeight()) {
+        if (child.getContentHeight() == child.getMinHeight()) {
             return;
         }
-        ObjectAnimator anim = ObjectAnimator.ofInt(child, "actualHeight",
-                child.getActualHeight(), child.getMinHeight());
+        ObjectAnimator anim = ObjectAnimator.ofInt(child, "contentHeight",
+                child.getContentHeight(), child.getMinHeight());
         anim.setInterpolator(mInterpolator);
         anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
         anim.addListener(new AnimatorListenerAdapter() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
index 788db29..06a174e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -16,9 +16,13 @@
 
 package com.android.systemui.statusbar;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.service.notification.StatusBarNotification;
 import android.util.AttributeSet;
@@ -26,10 +30,24 @@
 import android.view.View;
 import android.view.ViewStub;
 import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.LinearInterpolator;
 import android.widget.ImageView;
+
 import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.phone.PhoneStatusBar;
+import com.android.systemui.statusbar.stack.NotificationChildrenContainer;
+import com.android.systemui.statusbar.stack.StackScrollState;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+import com.android.systemui.statusbar.stack.StackViewState;
+
+import java.util.List;
 
 public class ExpandableNotificationRow extends ActivatableNotificationView {
+
+    private static final int DEFAULT_DIVIDER_ALPHA = 0x29;
+    private static final int COLORED_DIVIDER_ALPHA = 0x7B;
+    private final LinearInterpolator mLinearInterpolator = new LinearInterpolator();
     private int mRowMinHeight;
 
     /** Does this row contain layouts that can adapt to row expansion */
@@ -69,7 +87,27 @@
 
     private StatusBarNotification mStatusBarNotification;
     private boolean mIsHeadsUp;
+    private View mExpandButton;
+    private View mExpandButtonDivider;
+    private ViewStub mExpandButtonStub;
+    private ViewStub mChildrenContainerStub;
+    private NotificationGroupManager mGroupManager;
+    private View mExpandButtonContainer;
+    private boolean mChildrenExpanded;
+    private NotificationChildrenContainer mChildrenContainer;
+    private ValueAnimator mChildExpandAnimator;
+    private float mChildrenExpandProgress;
+    private float mExpandButtonStart;
     private ViewStub mGutsStub;
+    private boolean mHasExpandAction;
+    private boolean mIsSystemChildExpanded;
+    private OnClickListener mExpandClickListener = new OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mGroupManager.setGroupExpanded(mStatusBarNotification,
+                    !mChildrenExpanded);
+        }
+    };
 
     public void setIconAnimationRunning(boolean running) {
         setIconAnimationRunning(running, mPublicLayout);
@@ -119,6 +157,7 @@
     public void setStatusBarNotification(StatusBarNotification statusBarNotification) {
         mStatusBarNotification = statusBarNotification;
         updateVetoButton();
+        updateExpandButton();
     }
 
     public StatusBarNotification getStatusBarNotification() {
@@ -129,10 +168,101 @@
         mIsHeadsUp = isHeadsUp;
     }
 
+    public void setGroupManager(NotificationGroupManager groupManager) {
+        mGroupManager = groupManager;
+    }
+
+    public void addChildNotification(ExpandableNotificationRow row) {
+        addChildNotification(row, -1);
+    }
+
+    /**
+     * Add a child notification to this view.
+     *
+     * @param row the row to add
+     * @param childIndex the index to add it at, if -1 it will be added at the end
+     */
+    public void addChildNotification(ExpandableNotificationRow row, int childIndex) {
+        if (mChildrenContainer == null) {
+            mChildrenContainerStub.inflate();
+        }
+        mChildrenContainer.addNotification(row, childIndex);
+    }
+
+    public void removeChildNotification(ExpandableNotificationRow row) {
+        if (mChildrenContainer != null) {
+            mChildrenContainer.removeNotification(row);
+        }
+    }
+
+    @Override
+    public boolean areChildrenExpanded() {
+        return mChildrenExpanded;
+    }
+
+    public List<ExpandableNotificationRow> getNotificationChildren() {
+        return mChildrenContainer == null ? null : mChildrenContainer.getNotificationChildren();
+    }
+
+    /**
+     * Apply the order given in the list to the children.
+     *
+     * @param childOrder the new list order
+     * @return whether the list order has changed
+     */
+    public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) {
+        return mChildrenContainer != null && mChildrenContainer.applyChildOrder(childOrder);
+    }
+
+    public void getChildrenStates(StackScrollState resultState) {
+        if (mChildrenExpanded) {
+            StackViewState parentState = resultState.getViewStateForView(this);
+            mChildrenContainer.getState(resultState, parentState);
+        }
+    }
+
+    public void applyChildrenState(StackScrollState state) {
+        if (mChildrenExpanded) {
+            mChildrenContainer.applyState(state);
+        }
+    }
+
+    public void prepareExpansionChanged(StackScrollState state) {
+        if (mChildrenExpanded) {
+            mChildrenContainer.prepareExpansionChanged(state);
+        }
+    }
+
+    public void startChildAnimation(StackScrollState finalState,
+            StackStateAnimator stateAnimator, boolean withDelays, long delay, long duration) {
+        if (mChildrenExpanded) {
+            mChildrenContainer.startAnimationToState(finalState, stateAnimator, withDelays, delay,
+                    duration);
+        }
+    }
+
+    public ExpandableNotificationRow getViewAtPosition(float y) {
+        if (!mChildrenExpanded) {
+            return this;
+        } else {
+            ExpandableNotificationRow view = mChildrenContainer.getViewAtPosition(y);
+            return view == null ? this : view;
+        }
+    }
+
     public NotificationGuts getGuts() {
         return mGuts;
     }
 
+    protected int calculateContentHeightFromActualHeight(int actualHeight) {
+        int realActualHeight = actualHeight;
+        if (hasBottomDecor()) {
+            realActualHeight -= getBottomDecorHeight();
+        }
+        realActualHeight = Math.max(getMinHeight(), realActualHeight);
+        return realActualHeight;
+    }
+
     public interface ExpansionLogger {
         public void logNotificationExpansion(String key, boolean userAction, boolean expanded);
     }
@@ -194,6 +324,27 @@
                 mGutsStub = null;
             }
         });
+        mExpandButtonStub = (ViewStub) findViewById(R.id.more_button_stub);
+        mExpandButtonStub.setOnInflateListener(new ViewStub.OnInflateListener() {
+
+            @Override
+            public void onInflate(ViewStub stub, View inflated) {
+                mExpandButtonContainer = inflated;
+                mExpandButton = inflated.findViewById(R.id.notification_expand_button);
+                mExpandButtonDivider = inflated.findViewById(R.id.notification_expand_divider);
+                mExpandButtonContainer.setOnClickListener(mExpandClickListener);
+            }
+        });
+        mChildrenContainerStub = (ViewStub) findViewById(R.id.child_container_stub);
+        mChildrenContainerStub.setOnInflateListener(new ViewStub.OnInflateListener() {
+
+            @Override
+            public void onInflate(ViewStub stub, View inflated) {
+                mChildrenContainer = (NotificationChildrenContainer) inflated;
+                mChildrenContainer.setCollapseClickListener(mExpandClickListener);
+                updateChildrenVisibility(false);
+            }
+        });
         mVetoButton = findViewById(R.id.veto);
     }
 
@@ -203,6 +354,54 @@
         }
     }
 
+    private void updateChildrenVisibility(boolean animated) {
+        if (mChildrenContainer == null) {
+            return;
+        }
+        if (mChildExpandAnimator != null) {
+            mChildExpandAnimator.cancel();
+        }
+        float targetProgress = mChildrenExpanded ? 1.0f : 0.0f;
+        if (animated) {
+            if (mChildrenExpanded) {
+                mChildrenContainer.setVisibility(VISIBLE);
+            }
+            mExpandButtonStart = mExpandButtonContainer.getTranslationY();
+            mChildExpandAnimator = ValueAnimator.ofFloat(mChildrenExpandProgress, targetProgress);
+            mChildExpandAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    setChildrenExpandProgress((float) animation.getAnimatedValue());
+                }
+            });
+            mChildExpandAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mChildExpandAnimator = null;
+                    if (!mChildrenExpanded) {
+                        mChildrenContainer.setVisibility(INVISIBLE);
+                    }
+                }
+            });
+            mChildExpandAnimator.setInterpolator(mLinearInterpolator);
+            mChildExpandAnimator.setDuration(
+                    StackStateAnimator.ANIMATION_DURATION_EXPAND_CLICKED);
+            mChildExpandAnimator.start();
+        } else {
+            setChildrenExpandProgress(targetProgress);
+            mChildrenContainer.setVisibility(mChildrenExpanded ? VISIBLE : INVISIBLE);
+        }
+    }
+
+    private void setChildrenExpandProgress(float progress) {
+        mChildrenExpandProgress = progress;
+        updateExpandButtonAppearance();
+        NotificationContentView showingLayout = getShowingLayout();
+        float alpha = 1.0f - mChildrenExpandProgress;
+        alpha = PhoneStatusBar.ALPHA_OUT.getInterpolation(alpha);
+        showingLayout.setAlpha(alpha);
+    }
+
     @Override
     public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) {
         if (super.onRequestSendAccessibilityEventInternal(child, event)) {
@@ -292,7 +491,7 @@
         if (expand != mIsSystemExpanded) {
             final boolean wasExpanded = isExpanded();
             mIsSystemExpanded = expand;
-            notifyHeightChanged();
+            notifyHeightChanged(false /* needsAnimation */);
             logExpansionEvent(false, wasExpanded);
         }
     }
@@ -306,7 +505,7 @@
             mExpansionDisabled = expansionDisabled;
             logExpansionEvent(false, wasExpanded);
             if (wasExpanded != isExpanded()) {
-                notifyHeightChanged();
+                notifyHeightChanged(false  /* needsAnimation */);
             }
         }
     }
@@ -324,9 +523,9 @@
     public void applyExpansionToLayout() {
         boolean expand = isExpanded();
         if (expand && mExpandable) {
-            setActualHeight(mMaxExpandHeight);
+            setContentHeight(mMaxExpandHeight);
         } else {
-            setActualHeight(mRowMinHeight);
+            setContentHeight(mRowMinHeight);
         }
     }
 
@@ -336,12 +535,26 @@
             return getActualHeight();
         }
         boolean inExpansionState = isExpanded();
-        if (!inExpansionState) {
-            // not expanded, so we return the collapsed size
-            return mRowMinHeight;
+        int maxContentHeight;
+        if ((!inExpansionState && !mChildrenExpanded) || mShowingPublicForIntrinsicHeight) {
+            maxContentHeight = mRowMinHeight;
+        } else if (mChildrenExpanded) {
+            maxContentHeight = mChildrenContainer.getIntrinsicHeight();
+        } else {
+            maxContentHeight = getMaxExpandHeight();
         }
+        return maxContentHeight + getBottomDecorHeight();
+    }
 
-        return mShowingPublicForIntrinsicHeight ? mRowMinHeight : getMaxExpandHeight();
+    @Override
+    protected boolean hasBottomDecor() {
+        return BaseStatusBar.ENABLE_CHILD_NOTIFICATIONS
+                && !mIsHeadsUp && mGroupManager.hasGroupChildren(mStatusBarNotification);
+    }
+
+    @Override
+    protected boolean canHaveBottomDecor() {
+        return BaseStatusBar.ENABLE_CHILD_NOTIFICATIONS && !mIsHeadsUp;
     }
 
     /**
@@ -354,7 +567,16 @@
      */
     private boolean isExpanded() {
         return !mExpansionDisabled
-                && (!hasUserChangedExpansion() && isSystemExpanded() || isUserExpanded());
+                && (!hasUserChangedExpansion() && (isSystemExpanded() || isSystemChildExpanded())
+                || isUserExpanded());
+    }
+
+    private boolean isSystemChildExpanded() {
+        return mIsSystemChildExpanded;
+    }
+
+    public void setSystemChildExpanded(boolean expanded) {
+        mIsSystemChildExpanded = expanded;
     }
 
     @Override
@@ -368,11 +590,20 @@
         mWasReset = false;
     }
 
+    @Override
+    protected boolean isChildInvisible(View child) {
+
+        // We don't want to layout the ChildrenContainer if this is a heads-up view, otherwise the
+        // view will get too high and the shadows will be off.
+        boolean isInvisibleChildContainer = child == mChildrenContainer && mIsHeadsUp;
+        return super.isChildInvisible(child) || isInvisibleChildContainer;
+    }
+
     private void updateMaxExpandHeight() {
         int intrinsicBefore = getIntrinsicHeight();
         mMaxExpandHeight = mPrivateLayout.getMaxHeight();
         if (intrinsicBefore != getIntrinsicHeight()) {
-            notifyHeightChanged();
+            notifyHeightChanged(false  /* needsAnimation */);
         }
     }
 
@@ -439,8 +670,127 @@
         mVetoButton.setVisibility(isClearable() && !mShowingPublic ? View.VISIBLE : View.GONE);
     }
 
+    public void setChildrenExpanded(boolean expanded, boolean animate) {
+        mChildrenExpanded = expanded;
+        updateChildrenVisibility(animate);
+    }
+
+    public void updateExpandButton() {
+        boolean hasExpand = hasBottomDecor();
+        if (hasExpand != mHasExpandAction) {
+            if (hasExpand) {
+                if (mExpandButtonContainer == null) {
+                    mExpandButtonStub.inflate();
+                }
+                mExpandButtonContainer.setVisibility(View.VISIBLE);
+                updateExpandButtonAppearance();
+                updateExpandButtonColor();
+            } else if (mExpandButtonContainer != null) {
+                mExpandButtonContainer.setVisibility(View.GONE);
+            }
+            notifyHeightChanged(true  /* needsAnimation */);
+        }
+        mHasExpandAction = hasExpand;
+    }
+
+    private void updateExpandButtonAppearance() {
+        if (mExpandButtonContainer == null) {
+            return;
+        }
+        float expandButtonAlpha = 0.0f;
+        float expandButtonTranslation = 0.0f;
+        float containerTranslation = 0.0f;
+        int minHeight = getMinHeight();
+        if (!mChildrenExpanded || mChildExpandAnimator != null) {
+            int expandActionHeight = getBottomDecorHeight();
+            int translationY = getActualHeight() - expandActionHeight;
+            if (translationY > minHeight) {
+                containerTranslation = translationY;
+                expandButtonAlpha = 1.0f;
+                expandButtonTranslation = 0.0f;
+            } else {
+                containerTranslation = minHeight;
+                float progress = expandActionHeight != 0
+                        ? (minHeight - translationY) / (float) expandActionHeight
+                        : 1.0f;
+                expandButtonTranslation = -progress * expandActionHeight * 0.7f;
+                float alphaProgress = Math.min(progress / 0.7f, 1.0f);
+                alphaProgress = PhoneStatusBar.ALPHA_OUT.getInterpolation(alphaProgress);
+                expandButtonAlpha = 1.0f - alphaProgress;
+            }
+        }
+        if (mChildExpandAnimator != null || mChildrenExpanded) {
+            expandButtonAlpha = (1.0f - mChildrenExpandProgress)
+                    * expandButtonAlpha;
+            expandButtonTranslation = (1.0f - mChildrenExpandProgress)
+                    * expandButtonTranslation;
+            float newTranslation = -getBottomDecorHeight();
+
+            // We don't want to take the actual height of the view as this is already
+            // interpolated by a custom interpolator leading to a confusing animation. We want
+            // to have a stable end value to interpolate in between
+            float collapsedHeight = !mChildrenExpanded
+                    ? Math.max(StackStateAnimator.getFinalActualHeight(this)
+                            - getBottomDecorHeight(), minHeight)
+                    : mExpandButtonStart;
+            float translationProgress = mFastOutSlowInInterpolator.getInterpolation(
+                    mChildrenExpandProgress);
+            containerTranslation = (1.0f - translationProgress) * collapsedHeight
+                    + translationProgress * newTranslation;
+        }
+        mExpandButton.setAlpha(expandButtonAlpha);
+        mExpandButtonDivider.setAlpha(expandButtonAlpha);
+        mExpandButton.setTranslationY(expandButtonTranslation);
+        mExpandButtonContainer.setTranslationY(containerTranslation);
+        NotificationContentView showingLayout = getShowingLayout();
+        float layoutTranslation =
+                mExpandButtonContainer.getTranslationY() - showingLayout.getContentHeight();
+        layoutTranslation = Math.min(layoutTranslation, 0);
+        if (!mChildrenExpanded && mChildExpandAnimator == null) {
+            // Needed for the DragDownHelper in order not to jump there, as the position
+            // can be negative for a short time.
+            layoutTranslation = 0;
+        }
+        showingLayout.setTranslationY(layoutTranslation);
+        if (mChildrenContainer != null) {
+            mChildrenContainer.setTranslationY(
+                    mExpandButtonContainer.getTranslationY() + getBottomDecorHeight());
+        }
+    }
+
+    private void updateExpandButtonColor() {
+        // TODO: This needs some more baking, currently only the divider is colored according to
+        // the tint, but legacy black doesn't work yet perfectly for the button etc.
+        int color = getRippleColor();
+        if (color == mNormalRippleColor) {
+            color = 0;
+        }
+        if (mExpandButtonDivider != null) {
+            applyTint(mExpandButtonDivider, color);
+        }
+        if (mChildrenContainer != null) {
+            mChildrenContainer.setTintColor(color);
+        }
+    }
+
+    public static void applyTint(View v, int color) {
+        int alpha;
+        if (color != 0) {
+            alpha = COLORED_DIVIDER_ALPHA;
+        } else {
+            color = 0xff000000;
+            alpha = DEFAULT_DIVIDER_ALPHA;
+        }
+        if (v.getBackground() instanceof ColorDrawable) {
+            ColorDrawable background = (ColorDrawable) v.getBackground();
+            background.mutate();
+            background.setColor(color);
+            background.setAlpha(alpha);
+        }
+    }
+
     public int getMaxExpandHeight() {
-        return mShowingPublicForIntrinsicHeight ? mRowMinHeight : mMaxExpandHeight;
+        return mMaxExpandHeight;
     }
 
     @Override
@@ -451,17 +801,19 @@
 
     @Override
     public void setActualHeight(int height, boolean notifyListeners) {
-        mPrivateLayout.setActualHeight(height);
-        mPublicLayout.setActualHeight(height);
+        super.setActualHeight(height, notifyListeners);
+        int contentHeight = calculateContentHeightFromActualHeight(height);
+        mPrivateLayout.setContentHeight(contentHeight);
+        mPublicLayout.setContentHeight(contentHeight);
         if (mGuts != null) {
             mGuts.setActualHeight(height);
         }
         invalidate();
-        super.setActualHeight(height, notifyListeners);
+        updateExpandButtonAppearance();
     }
 
     @Override
-    public int getMaxHeight() {
+    public int getMaxContentHeight() {
         NotificationContentView showingLayout = getShowingLayout();
         return showingLayout.getMaxHeight();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
index 1e5dcf7..7ae0d6d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
@@ -33,8 +33,8 @@
  */
 public abstract class ExpandableView extends FrameLayout {
 
-
-    private OnHeightChangedListener mOnHeightChangedListener;
+    private final int mBottomDecorHeight;
+    protected OnHeightChangedListener mOnHeightChangedListener;
     protected int mMaxViewHeight;
     private int mActualHeight;
     protected int mClipTopAmount;
@@ -48,6 +48,12 @@
         super(context, attrs);
         mMaxViewHeight = getResources().getDimensionPixelSize(
                 R.dimen.notification_max_height);
+        mBottomDecorHeight = resolveBottomDecorHeight();
+    }
+
+    protected int resolveBottomDecorHeight() {
+        return getResources().getDimensionPixelSize(
+                R.dimen.notification_bottom_decor_height);
     }
 
     @Override
@@ -65,6 +71,9 @@
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
             View child = getChildAt(i);
+            if (child.getVisibility() == GONE || isChildInvisible(child)) {
+                continue;
+            }
             int childHeightSpec = newHeightSpec;
             ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
             if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
@@ -93,6 +102,10 @@
         }
         mMatchParentViews.clear();
         int width = MeasureSpec.getSize(widthMeasureSpec);
+        if (canHaveBottomDecor()) {
+            // We always account for the expandAction as well.
+            ownHeight += mBottomDecorHeight;
+        }
         setMeasuredDimension(width, ownHeight);
     }
 
@@ -102,7 +115,7 @@
         if (!mActualHeightInitialized && mActualHeight == 0) {
             int initialHeight = getInitialHeight();
             if (initialHeight != 0) {
-                setActualHeight(initialHeight);
+                setContentHeight(initialHeight);
             }
         }
     }
@@ -145,12 +158,12 @@
         mActualHeight = actualHeight;
         updateClipping();
         if (notifyListeners) {
-            notifyHeightChanged();
+            notifyHeightChanged(false  /* needsAnimation */);
         }
     }
 
-    public void setActualHeight(int actualHeight) {
-        setActualHeight(actualHeight, true);
+    public void setContentHeight(int contentHeight) {
+        setActualHeight(contentHeight + getBottomDecorHeight(), true);
     }
 
     /**
@@ -163,14 +176,39 @@
     }
 
     /**
+     * This view may have a bottom decor which will be placed below the content. If it has one, this
+     * view will be layouted higher than just the content by {@link #mBottomDecorHeight}.
+     * @return the height of the decor if it currently has one
+     */
+    public int getBottomDecorHeight() {
+        return hasBottomDecor() ? mBottomDecorHeight : 0;
+    }
+
+    /**
+     * @return whether this view may have a bottom decor at all. This will force the view to layout
+     *         itself higher than just it's content
+     */
+    protected boolean canHaveBottomDecor() {
+        return false;
+    }
+
+    /**
+     * @return whether this view has a decor view below it's content. This will make the intrinsic
+     *         height from {@link #getIntrinsicHeight()} higher as well
+     */
+    protected boolean hasBottomDecor() {
+        return false;
+    }
+
+    /**
      * @return The maximum height of this notification.
      */
-    public int getMaxHeight() {
+    public int getMaxContentHeight() {
         return getHeight();
     }
 
     /**
-     * @return The minimum height of this notification.
+     * @return The minimum content height of this notification.
      */
     public int getMinHeight() {
         return getHeight();
@@ -249,9 +287,9 @@
         return false;
     }
 
-    public void notifyHeightChanged() {
+    public void notifyHeightChanged(boolean needsAnimation) {
         if (mOnHeightChangedListener != null) {
-            mOnHeightChangedListener.onHeightChanged(this);
+            mOnHeightChangedListener.onHeightChanged(this, needsAnimation);
         }
     }
 
@@ -302,6 +340,21 @@
         outRect.top += getTranslationY() + getClipTopAmount();
     }
 
+    public int getContentHeight() {
+        return mActualHeight - getBottomDecorHeight();
+    }
+
+    /**
+     * @return whether the given child can be ignored for layouting and measuring purposes
+     */
+    protected boolean isChildInvisible(View child) {
+        return false;
+    }
+
+    public boolean areChildrenExpanded() {
+        return false;
+    }
+
     private void updateClipping() {
         mClipRect.set(0, mClipTopOptimization, getWidth(), getActualHeight());
         setClipBounds(mClipRect);
@@ -330,8 +383,9 @@
         /**
          * @param view the view for which the height changed, or {@code null} if just the top
          *             padding or the padding between the elements changed
+         * @param needsAnimation whether the view height needs to be animated
          */
-        void onHeightChanged(ExpandableView view);
+        void onHeightChanged(ExpandableView view, boolean needsAnimation);
 
         /**
          * Called when the view is reset and therefore the height will change abruptly
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
index 70c270c..745e75d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
@@ -47,7 +47,7 @@
 
     private int mSmallHeight;
     private int mClipTopAmount;
-    private int mActualHeight;
+    private int mContentHeight;
 
     private final Interpolator mLinearInterpolator = new LinearInterpolator();
 
@@ -97,7 +97,7 @@
         mSmallHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height);
         mContractedVisible = true;
         if (resetActualHeight) {
-            mActualHeight = mSmallHeight;
+            mContentHeight = mSmallHeight;
         }
     }
 
@@ -154,12 +154,17 @@
         }
     }
 
-    public void setActualHeight(int actualHeight) {
-        mActualHeight = actualHeight;
+    public void setContentHeight(int contentHeight) {
+        contentHeight = Math.max(Math.min(contentHeight, getHeight()), getMinHeight());
+        mContentHeight = contentHeight;
         selectLayout(mAnimate /* animate */, false /* force */);
         updateClipping();
     }
 
+    public int getContentHeight() {
+        return mContentHeight;
+    }
+
     public int getMaxHeight() {
 
         // The maximum height is just the laid out height.
@@ -176,7 +181,7 @@
     }
 
     private void updateClipping() {
-        mClipBounds.set(0, mClipTopAmount, getWidth(), mActualHeight);
+        mClipBounds.set(0, mClipTopAmount, getWidth(), mContentHeight);
         setClipBounds(mClipBounds);
     }
 
@@ -235,7 +240,7 @@
     }
 
     private boolean showContractedChild() {
-        return mActualHeight <= mSmallHeight || mExpandedChild == null;
+        return mContentHeight <= mSmallHeight || mExpandedChild == null;
     }
 
     public void notifyContentUpdated() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
index 7e68c10..912f414 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
@@ -240,7 +240,8 @@
             return true;
         }
 
-        if (mGroupManager.isChildInGroupWithSummary(sbn)) {
+        if (!BaseStatusBar.ENABLE_CHILD_NOTIFICATIONS
+                && mGroupManager.isChildInGroupWithSummary(sbn)) {
             return true;
         }
         return false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java
index bfa3aa5..5fa7070 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java
@@ -62,4 +62,13 @@
     public NotificationOverflowIconsView getIconsView() {
         return mIconsView;
     }
+
+    protected int getContentHeightFromActualHeight(int actualHeight) {
+        int realActualHeight = actualHeight;
+        if (hasBottomDecor()) {
+            realActualHeight -= getBottomDecorHeight();
+        }
+        realActualHeight = Math.max(getMinHeight(), realActualHeight);
+        return realActualHeight;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
index 5214ab4..7072dcb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
@@ -100,6 +100,8 @@
                             }
                         }
                     });
+                } else {
+                    group.summary.row.updateExpandButton();
                 }
             }
         }
@@ -116,11 +118,15 @@
         }
         if (notif.isGroupSummary()) {
             group.summary = added;
+            group.expanded = added.row.areChildrenExpanded();
             if (!group.children.isEmpty()) {
                 mListener.onGroupCreatedFromChildren(group);
             }
         } else {
             group.children.add(added);
+            if (group.summary != null && group.children.size() == 1 && !group.expanded) {
+                group.summary.row.updateExpandButton();
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index 7513fc6..195da46 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -1680,7 +1680,7 @@
     }
 
     @Override
-    public void onHeightChanged(ExpandableView view) {
+    public void onHeightChanged(ExpandableView view, boolean needsAnimation) {
 
         // Block update if we are in quick settings and just the top padding changed
         // (i.e. view == null).
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index e5eb747..f3ec34a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -170,6 +170,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -524,6 +525,8 @@
             goToLockedShade(null);
         }
     };
+    private HashMap<ExpandableNotificationRow, List<ExpandableNotificationRow>> mTmpChildOrderMap
+            = new HashMap<>();
 
     @Override
     public void start() {
@@ -664,6 +667,8 @@
                 R.id.notification_stack_scroller);
         mStackScroller.setLongPressListener(getNotificationLongClicker());
         mStackScroller.setPhoneStatusBar(this);
+        mStackScroller.setGroupManager(mGroupManager);
+        mGroupManager.setOnGroupChangeListener(mStackScroller);
 
         mKeyguardIconOverflowContainer =
                 (NotificationOverflowContainer) LayoutInflater.from(mContext).inflate(
@@ -855,9 +860,20 @@
         final ArrayList<View> viewsToHide = new ArrayList<View>(numChildren);
         for (int i = 0; i < numChildren; i++) {
             final View child = mStackScroller.getChildAt(i);
-            if (mStackScroller.canChildBeDismissed(child)) {
-                if (child.getVisibility() == View.VISIBLE) {
-                    viewsToHide.add(child);
+            if (child instanceof ExpandableNotificationRow) {
+                if (mStackScroller.canChildBeDismissed(child)) {
+                    if (child.getVisibility() == View.VISIBLE) {
+                        viewsToHide.add(child);
+                    }
+                }
+                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                List<ExpandableNotificationRow> children = row.getNotificationChildren();
+                if (row.areChildrenExpanded() && children != null) {
+                    for (ExpandableNotificationRow childRow : children) {
+                        if (childRow.getVisibility() == View.VISIBLE) {
+                            viewsToHide.add(childRow);
+                        }
+                    }
                 }
             }
         }
@@ -1296,10 +1312,23 @@
                     ent.row.setShowingLegacyBackground(true);
                 }
             }
-            toShow.add(ent.row);
+            if (mGroupManager.isChildInGroupWithSummary(ent.row.getStatusBarNotification())) {
+                ExpandableNotificationRow summary = mGroupManager.getGroupSummary(
+                        ent.row.getStatusBarNotification());
+                List<ExpandableNotificationRow> orderedChildren =
+                        mTmpChildOrderMap.get(summary);
+                if (orderedChildren == null) {
+                    orderedChildren = new ArrayList<>();
+                    mTmpChildOrderMap.put(summary, orderedChildren);
+                }
+                orderedChildren.add(ent.row);
+            } else {
+                toShow.add(ent.row);
+            }
+
         }
 
-        ArrayList<View> toRemove = new ArrayList<View>();
+        ArrayList<View> toRemove = new ArrayList<>();
         for (int i=0; i< mStackScroller.getChildCount(); i++) {
             View child = mStackScroller.getChildAt(i);
             if (!toShow.contains(child) && child instanceof ExpandableNotificationRow) {
@@ -1328,17 +1357,22 @@
                 continue;
             }
 
-            if (child == toShow.get(j)) {
-                // Everything is well, advance both lists.
-                j++;
-                continue;
+            ExpandableNotificationRow targetChild = toShow.get(j);
+            if (child != targetChild) {
+                // Oops, wrong notification at this position. Put the right one
+                // here and advance both lists.
+                mStackScroller.changeViewPosition(targetChild, i);
             }
-
-            // Oops, wrong notification at this position. Put the right one
-            // here and advance both lists.
-            mStackScroller.changeViewPosition(toShow.get(j), i);
             j++;
+
         }
+
+        // lets handle the child notifications now
+        updateNotificationShadeForChildren();
+
+        // clear the map again for the next usage
+        mTmpChildOrderMap.clear();
+
         updateRowStates();
         updateSpeedbump();
         updateClearAll();
@@ -1353,6 +1387,52 @@
         mShadeUpdates.check();
     }
 
+    private void updateNotificationShadeForChildren() {
+        ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>();
+        boolean orderChanged = false;
+        for (int i = 0; i < mStackScroller.getChildCount(); i++) {
+            View view = mStackScroller.getChildAt(i);
+            if (!(view instanceof ExpandableNotificationRow)) {
+                // We don't care about non-notification views.
+                continue;
+            }
+
+            ExpandableNotificationRow parent = (ExpandableNotificationRow) view;
+            List<ExpandableNotificationRow> children = parent.getNotificationChildren();
+            List<ExpandableNotificationRow> orderedChildren = mTmpChildOrderMap.get(parent);
+
+            // lets first remove all undesired children
+            if (children != null) {
+                toRemove.clear();
+                for (ExpandableNotificationRow childRow : children) {
+                    if (orderedChildren == null || !orderedChildren.contains(childRow)) {
+                        toRemove.add(childRow);
+                    }
+                }
+                for (ExpandableNotificationRow remove : toRemove) {
+                    parent.removeChildNotification(remove);
+                    mStackScroller.notifyGroupChildRemoved(remove);
+                }
+            }
+
+            // We now add all the children which are not in there already
+            for (int childIndex = 0; orderedChildren != null && childIndex < orderedChildren.size();
+                    childIndex++) {
+                ExpandableNotificationRow childView = orderedChildren.get(childIndex);
+                if (children == null || !children.contains(childView)) {
+                    parent.addChildNotification(childView, childIndex);
+                    mStackScroller.notifyGroupChildAdded(childView);
+                }
+            }
+
+            // Finally after removing and adding has been beformed we can apply the order.
+            orderChanged |= parent.applyChildOrder(orderedChildren);
+        }
+        if (orderChanged) {
+            mStackScroller.generateChildOrderChangedEvent();
+        }
+    }
+
     private boolean packageHasVisibilityOverride(String key) {
         return mNotificationData.getVisibilityOverride(key)
                 != NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE;
@@ -1379,6 +1459,10 @@
         final int N = activeNotifications.size();
         for (int i = 0; i < N; i++) {
             Entry entry = activeNotifications.get(i);
+            boolean isChild = !isTopLevelChild(entry);
+            if (isChild) {
+                continue;
+            }
             if (entry.row.getVisibility() != View.GONE &&
                     mNotificationData.isAmbient(entry.key)) {
                 speedbumpIndex = currentIndex;
@@ -1389,6 +1473,10 @@
         mStackScroller.updateSpeedBumpIndex(speedbumpIndex);
     }
 
+    public static boolean isTopLevelChild(Entry entry) {
+        return entry.row.getParent() instanceof NotificationStackScrollLayout;
+    }
+
     @Override
     protected void updateNotifications() {
         mNotificationData.filterAndSort();
@@ -3074,7 +3162,7 @@
         mLeaveOpenOnKeyguardHide = false;
         if (mDraggedDownRow != null) {
             mDraggedDownRow.setUserLocked(false);
-            mDraggedDownRow.notifyHeightChanged();
+            mDraggedDownRow.notifyHeightChanged(false  /* needsAnimation */);
             mDraggedDownRow = null;
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index 798467f..c49f620 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -158,13 +158,16 @@
         final int N = activeNotifications.size();
         ArrayList<StatusBarIconView> toShow = new ArrayList<>(N);
 
-        // Filter out ambient notifications.
+        // Filter out ambient notifications and notification children.
         for (int i = 0; i < N; i++) {
             NotificationData.Entry ent = activeNotifications.get(i);
             if (notificationData.isAmbient(ent.key)
                     && !NotificationData.showNotificationEvenIfUnprovisioned(ent.notification)) {
                 continue;
             }
+            if (!PhoneStatusBar.isTopLevelChild(ent)) {
+                continue;
+            }
             toShow.add(ent.icon);
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java
index f8b8a9a..6c1cdcd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java
@@ -204,6 +204,9 @@
     }
 
     private void resetViewForHeadsup() {
+        if (mHeadsUp.row.areChildrenExpanded()) {
+            mHeadsUp.row.setChildrenExpanded(false /* expanded */, false /* animated */);
+        }
         mHeadsUp.row.setSystemExpanded(true);
         mHeadsUp.row.setSensitive(false);
         mHeadsUp.row.setHeadsUp(true);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
new file mode 100644
index 0000000..3c9e8cf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar.stack;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A container containing child notifications
+ */
+public class NotificationChildrenContainer extends ViewGroup {
+
+    private final int mChildPadding;
+    private final int mDividerHeight;
+    private final int mMaxNotificationHeight;
+    private final List<View> mDividers = new ArrayList<>();
+    private final List<ExpandableNotificationRow> mChildren = new ArrayList<>();
+    private final View mCollapseButton;
+    private final View mCollapseDivider;
+    private final int mCollapseButtonHeight;
+    private final int mNotificationAppearDistance;
+
+    public NotificationChildrenContainer(Context context) {
+        this(context, null);
+    }
+
+    public NotificationChildrenContainer(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        mChildPadding = getResources().getDimensionPixelSize(
+                R.dimen.notification_children_padding);
+        mDividerHeight = getResources().getDimensionPixelSize(
+                R.dimen.notification_children_divider_height);
+        mMaxNotificationHeight = getResources().getDimensionPixelSize(
+                R.dimen.notification_max_height);
+        mNotificationAppearDistance = getResources().getDimensionPixelSize(
+                R.dimen.notification_appear_distance);
+        LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class);
+        mCollapseButton = inflater.inflate(R.layout.notification_collapse_button, this,
+                false);
+        mCollapseButtonHeight = getResources().getDimensionPixelSize(
+                R.dimen.notification_bottom_decor_height);
+        addView(mCollapseButton);
+        mCollapseDivider = inflateDivider();
+        addView(mCollapseDivider);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        int childCount = mChildren.size();
+        boolean firstChild = true;
+        for (int i = 0; i < childCount; i++) {
+            View child = mChildren.get(i);
+            boolean viewGone = child.getVisibility() == View.GONE;
+            if (i != 0) {
+                View divider = mDividers.get(i - 1);
+                int dividerVisibility = divider.getVisibility();
+                int newVisibility = viewGone ? INVISIBLE : VISIBLE;
+                if (dividerVisibility != newVisibility) {
+                    divider.setVisibility(newVisibility);
+                }
+            }
+            if (viewGone) {
+                continue;
+            }
+            child.layout(0, 0, getWidth(), child.getMeasuredHeight());
+            if (!firstChild) {
+                mDividers.get(i - 1).layout(0, 0, getWidth(), mDividerHeight);
+            } else {
+                firstChild = false;
+            }
+        }
+        mCollapseButton.layout(0, 0, getWidth(), mCollapseButtonHeight);
+        mCollapseDivider.layout(0, mCollapseButtonHeight - mDividerHeight, getWidth(),
+                mCollapseButtonHeight);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int ownMaxHeight = mMaxNotificationHeight;
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
+        boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
+        if (hasFixedHeight || isHeightLimited) {
+            int size = MeasureSpec.getSize(heightMeasureSpec);
+            ownMaxHeight = Math.min(ownMaxHeight, size);
+        }
+        int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
+        int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY);
+        int collapseButtonHeightSpec = MeasureSpec.makeMeasureSpec(mCollapseButtonHeight,
+                MeasureSpec.EXACTLY);
+        mCollapseButton.measure(widthMeasureSpec, collapseButtonHeightSpec);
+        mCollapseDivider.measure(widthMeasureSpec, dividerHeightSpec);
+        int height = mCollapseButtonHeight;
+        int childCount = mChildren.size();
+        boolean firstChild = true;
+        for (int i = 0; i < childCount; i++) {
+            View child = mChildren.get(i);
+            if (child.getVisibility() == View.GONE) {
+                continue;
+            }
+            child.measure(widthMeasureSpec, newHeightSpec);
+            height += child.getMeasuredHeight();
+            if (!firstChild) {
+                // layout the divider
+                View divider = mDividers.get(i - 1);
+                divider.measure(widthMeasureSpec, dividerHeightSpec);
+                height += mChildPadding;
+            } else {
+                firstChild = false;
+            }
+        }
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+        height = hasFixedHeight ? ownMaxHeight
+                : isHeightLimited ? Math.min(ownMaxHeight, height)
+                : height;
+        setMeasuredDimension(width, height);
+    }
+
+    /**
+     * Add a child notification to this view.
+     *
+     * @param row the row to add
+     * @param childIndex the index to add it at, if -1 it will be added at the end
+     */
+    public void addNotification(ExpandableNotificationRow row, int childIndex) {
+        int newIndex = childIndex < 0 ? mChildren.size() : childIndex;
+        mChildren.add(newIndex, row);
+        addView(row);
+        if (mChildren.size() != 1) {
+            View divider = inflateDivider();
+            addView(divider);
+            mDividers.add(Math.max(newIndex - 1, 0), divider);
+        }
+        // TODO: adapt background corners
+        // TODO: fix overdraw
+    }
+
+    public void removeNotification(ExpandableNotificationRow row) {
+        int childIndex = mChildren.indexOf(row);
+        mChildren.remove(row);
+        removeView(row);
+        if (!mDividers.isEmpty()) {
+            View divider = mDividers.remove(Math.max(childIndex - 1, 0));
+            removeView(divider);
+        }
+        row.setSystemChildExpanded(false);
+        // TODO: adapt background corners
+    }
+
+    private View inflateDivider() {
+        return LayoutInflater.from(mContext).inflate(
+                R.layout.notification_children_divider, this, false);
+    }
+
+    public List<ExpandableNotificationRow> getNotificationChildren() {
+        return mChildren;
+    }
+
+    /**
+     * Apply the order given in the list to the children.
+     *
+     * @param childOrder the new list order
+     * @return whether the list order has changed
+     */
+    public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) {
+        if (childOrder == null) {
+            return false;
+        }
+        boolean result = false;
+        for (int i = 0; i < mChildren.size() && i < childOrder.size(); i++) {
+            ExpandableNotificationRow child = mChildren.get(i);
+            ExpandableNotificationRow desiredChild = childOrder.get(i);
+            if (child != desiredChild) {
+                mChildren.remove(desiredChild);
+                mChildren.add(i, desiredChild);
+                result = true;
+            }
+        }
+
+        // Let's make the first child expanded!
+        boolean first = true;
+        for (int i = 0; i < childOrder.size(); i++) {
+            ExpandableNotificationRow child = childOrder.get(i);
+            child.setSystemChildExpanded(first);
+            first = false;
+        }
+        return result;
+    }
+
+    public int getIntrinsicHeight() {
+        int childCount = mChildren.size();
+        int intrinsicHeight = 0;
+        int visibleChildren = 0;
+        for (int i = 0; i < childCount; i++) {
+            ExpandableNotificationRow child = mChildren.get(i);
+            if (child.getVisibility() == View.GONE) {
+                continue;
+            }
+            intrinsicHeight += child.getIntrinsicHeight();
+            visibleChildren++;
+        }
+        if (visibleChildren > 0) {
+            intrinsicHeight += (visibleChildren - 1) * mDividerHeight;
+        }
+        return intrinsicHeight;
+    }
+
+    /**
+     * Update the state of all its children based on a linear layout algorithm.
+     *
+     * @param resultState the state to update
+     * @param parentState the state of the parent
+     */
+    public void getState(StackScrollState resultState, StackViewState parentState) {
+        int childCount = mChildren.size();
+        int yPosition = mCollapseButtonHeight;
+        boolean firstChild = true;
+        for (int i = 0; i < childCount; i++) {
+            ExpandableNotificationRow child = mChildren.get(i);
+            if (child.getVisibility() == View.GONE) {
+                continue;
+            }
+            if (!firstChild) {
+                // There's a divider
+                yPosition += mChildPadding;
+            } else {
+                firstChild = false;
+            }
+            StackViewState childState = resultState.getViewStateForView(child);
+            int intrinsicHeight = child.getIntrinsicHeight();
+            childState.yTranslation = yPosition;
+            childState.zTranslation = 0;
+            childState.height = intrinsicHeight;
+            childState.dimmed = parentState.dimmed;
+            childState.dark = parentState.dark;
+            childState.hideSensitive = parentState.hideSensitive;
+            childState.belowSpeedBump = parentState.belowSpeedBump;
+            childState.scale =  parentState.scale;
+            childState.clipTopAmount = 0;
+            childState.topOverLap = 0;
+            childState.location = parentState.location;
+            yPosition += intrinsicHeight;
+        }
+    }
+
+    public void applyState(StackScrollState state) {
+        int childCount = mChildren.size();
+        boolean firstChild = true;
+        ViewState dividerState = new ViewState();
+        for (int i = 0; i < childCount; i++) {
+            ExpandableNotificationRow child = mChildren.get(i);
+            StackViewState viewState = state.getViewStateForView(child);
+            if (child.getVisibility() == View.GONE) {
+                continue;
+            }
+            if (!firstChild) {
+                // layout the divider
+                View divider = mDividers.get(i - 1);
+                dividerState.initFrom(divider);
+                dividerState.yTranslation = (int) (viewState.yTranslation
+                        - (mChildPadding + mDividerHeight) / 2.0f);
+                dividerState.alpha = 1;
+                state.applyViewState(divider, dividerState);
+            } else {
+                firstChild = false;
+            }
+            state.applyState(child, viewState);
+        }
+    }
+
+    public void setCollapseClickListener(OnClickListener collapseClickListener) {
+        mCollapseButton.setOnClickListener(collapseClickListener);
+    }
+
+    /**
+     * This is called when the children expansion has changed and positions the children properly
+     * for an appear animation.
+     *
+     * @param state the new state we animate to
+     */
+    public void prepareExpansionChanged(StackScrollState state) {
+        int childCount = mChildren.size();
+        boolean firstChild = true;
+        StackViewState sourceState = new StackViewState();
+        ViewState dividerState = new ViewState();
+        for (int i = 0; i < childCount; i++) {
+            ExpandableNotificationRow child = mChildren.get(i);
+            StackViewState viewState = state.getViewStateForView(child);
+            if (child.getVisibility() == View.GONE) {
+                continue;
+            }
+            if (!firstChild) {
+                // layout the divider
+                View divider = mDividers.get(i - 1);
+                dividerState.initFrom(divider);
+                dividerState.yTranslation = viewState.yTranslation
+                        - (mChildPadding + mDividerHeight) / 2.0f + mNotificationAppearDistance;
+                dividerState.alpha = 0;
+                state.applyViewState(divider, dividerState);
+            } else {
+                firstChild = false;
+            }
+            sourceState.copyFrom(viewState);
+            sourceState.alpha = 0;
+            sourceState.yTranslation += mNotificationAppearDistance;
+            state.applyState(child, sourceState);
+        }
+        mCollapseButton.setAlpha(0);
+        mCollapseDivider.setAlpha(0);
+        mCollapseDivider.setTranslationY(mNotificationAppearDistance / 4);
+    }
+
+    public void startAnimationToState(StackScrollState state, StackStateAnimator stateAnimator,
+            boolean withDelays, long baseDelay, long duration) {
+        int childCount = mChildren.size();
+        boolean firstChild = true;
+        ViewState dividerState = new ViewState();
+        int notGoneIndex = 0;
+        for (int i = 0; i < childCount; i++) {
+            ExpandableNotificationRow child = mChildren.get(i);
+            StackViewState viewState = state.getViewStateForView(child);
+            if (child.getVisibility() == View.GONE) {
+                continue;
+            }
+            int difference = Math.min(StackStateAnimator.DELAY_EFFECT_MAX_INDEX_DIFFERENCE_CHILDREN,
+                    notGoneIndex + 1);
+            long delay = withDelays
+                    ? difference * StackStateAnimator.ANIMATION_DELAY_PER_ELEMENT_EXPAND_CHILDREN
+                    : 0;
+            delay += baseDelay;
+            if (!firstChild) {
+                // layout the divider
+                View divider = mDividers.get(i - 1);
+                dividerState.initFrom(divider);
+                dividerState.yTranslation = viewState.yTranslation
+                        - (mChildPadding + mDividerHeight) / 2.0f;
+                dividerState.alpha = 1;
+                stateAnimator.startViewAnimations(divider, dividerState, delay, duration);
+            } else {
+                firstChild = false;
+            }
+            stateAnimator.startStackAnimations(child, viewState, state, -1, delay);
+            notGoneIndex++;
+        }
+        dividerState.initFrom(mCollapseButton);
+        dividerState.alpha = 1.0f;
+        stateAnimator.startViewAnimations(mCollapseButton, dividerState, baseDelay, duration);
+        dividerState.initFrom(mCollapseDivider);
+        dividerState.alpha = 1.0f;
+        dividerState.yTranslation = 0.0f;
+        stateAnimator.startViewAnimations(mCollapseDivider, dividerState, baseDelay, duration);
+    }
+
+    public ExpandableNotificationRow getViewAtPosition(float y) {
+        // find the view under the pointer, accounting for GONE views
+        final int count = mChildren.size();
+        for (int childIdx = 0; childIdx < count; childIdx++) {
+            ExpandableNotificationRow slidingChild = mChildren.get(childIdx);
+            float childTop = slidingChild.getTranslationY();
+            float top = childTop + slidingChild.getClipTopAmount();
+            float bottom = childTop + slidingChild.getActualHeight();
+            if (y >= top && y <= bottom) {
+                return slidingChild;
+            }
+        }
+        return null;
+    }
+
+    public void setTintColor(int color) {
+        ExpandableNotificationRow.applyTint(mCollapseDivider, color);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
index 4c7dbbb..2eafd57 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -41,9 +41,11 @@
 import com.android.systemui.statusbar.EmptyShadeView;
 import com.android.systemui.statusbar.ExpandableNotificationRow;
 import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.NotificationData;
 import com.android.systemui.statusbar.SpeedBumpView;
 import com.android.systemui.statusbar.StackScrollerDecorView;
 import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.phone.PhoneStatusBar;
 import com.android.systemui.statusbar.policy.ScrollAdapter;
 
@@ -55,7 +57,7 @@
  */
 public class NotificationStackScrollLayout extends ViewGroup
         implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter,
-        ExpandableView.OnHeightChangedListener {
+        ExpandableView.OnHeightChangedListener, NotificationGroupManager.OnGroupChangeListener {
 
     private static final String TAG = "NotificationStackScrollLayout";
     private static final boolean DEBUG = false;
@@ -118,6 +120,7 @@
      */
     private StackScrollState mCurrentStackScrollState = new StackScrollState(this);
     private AmbientState mAmbientState = new AmbientState();
+    private NotificationGroupManager mGroupManager;
     private ArrayList<View> mChildrenToAddAnimated = new ArrayList<View>();
     private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<View>();
     private ArrayList<View> mSnappedBackChildren = new ArrayList<View>();
@@ -180,6 +183,7 @@
     private boolean mDontReportNextOverScroll;
     private boolean mRequestViewResizeAnimationOnLayout;
     private boolean mNeedViewResizeAnimation;
+    private View mExpandedGroupView;
     private boolean mEverythingNeedsAnimation;
 
     /**
@@ -213,6 +217,7 @@
     };
     private PhoneStatusBar mPhoneStatusBar;
     private int[] mTempInt2 = new int[2];
+    private boolean mGenerateChildOrderChangedEvent;
     private boolean mRemoveAnimationEnabled;
 
     public NotificationStackScrollLayout(Context context) {
@@ -309,7 +314,7 @@
 
     private void notifyHeightChangeListener(ExpandableView view) {
         if (mOnHeightChangedListener != null) {
-            mOnHeightChangedListener.onHeightChanged(view);
+            mOnHeightChangedListener.onHeightChanged(view, false /* needsAnimation */);
         }
     }
 
@@ -329,6 +334,9 @@
         float centerX = getWidth() / 2.0f;
         for (int i = 0; i < getChildCount(); i++) {
             View child = getChildAt(i);
+            if (child.getVisibility() == GONE) {
+                continue;
+            }
             float width = child.getMeasuredWidth();
             float height = child.getMeasuredHeight();
             child.layout((int) (centerX - width / 2.0f),
@@ -339,16 +347,18 @@
         setMaxLayoutHeight(getHeight());
         updateContentHeight();
         clampScrollPosition();
-        requestAnimationOnViewResize();
+        if (mRequestViewResizeAnimationOnLayout) {
+            requestAnimationOnViewResize();
+            mRequestViewResizeAnimationOnLayout = false;
+        }
         requestChildrenUpdate();
     }
 
     private void requestAnimationOnViewResize() {
-        if (mRequestViewResizeAnimationOnLayout && mIsExpanded && mAnimationsEnabled) {
+        if (mIsExpanded && mAnimationsEnabled) {
             mNeedViewResizeAnimation = true;
             mNeedsAnimation = true;
         }
-        mRequestViewResizeAnimationOnLayout = false;
     }
 
     public void updateSpeedBumpIndex(int newIndex) {
@@ -645,6 +655,10 @@
             int right = getWidth();
 
             if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
+                if (slidingChild instanceof ExpandableNotificationRow) {
+                    ExpandableNotificationRow row = (ExpandableNotificationRow) slidingChild;
+                    return row.getViewAtPosition(touchY - childTop);
+                }
                 return slidingChild;
             }
         }
@@ -1543,6 +1557,14 @@
     @Override
     protected void onViewRemoved(View child) {
         super.onViewRemoved(child);
+        // we only call our internal methods if this is actually a removal and not just a
+        // notification which becomes a child notification
+        if (!isChildInGroup(child)) {
+            onViewRemovedInternal(child);
+        }
+    }
+
+    private void onViewRemovedInternal(View child) {
         mStackScrollAlgorithm.notifyChildrenChanged(this);
         if (mChangePositionInProgress) {
             // This is only a position change, don't do anything special
@@ -1568,6 +1590,12 @@
         ((ExpandableView) child).setClipTopOptimization(0);
     }
 
+    private boolean isChildInGroup(View child) {
+        return child instanceof ExpandableNotificationRow
+                && mGroupManager.isChildInGroupWithSummary(
+                        ((ExpandableNotificationRow) child).getStatusBarNotification());
+    }
+
     /**
      * Generate a remove animation for a child view.
      *
@@ -1575,7 +1603,7 @@
      * @return Whether an animation was generated.
      */
     private boolean generateRemoveAnimation(View child) {
-        if (mIsExpanded && mAnimationsEnabled) {
+        if (mIsExpanded && mAnimationsEnabled && !isChildInInvisibleGroup(child)) {
             if (!mChildrenToAddAnimated.contains(child)) {
                 // Generate Animations
                 mChildrenToRemoveAnimated.add(child);
@@ -1591,6 +1619,23 @@
     }
 
     /**
+     * @param child the child to query
+     * @return whether a view is not a top level child but a child notification and that group is
+     *         not expanded
+     */
+    private boolean isChildInInvisibleGroup(View child) {
+        if (child instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+            ExpandableNotificationRow groupSummary =
+                    mGroupManager.getGroupSummary(row.getStatusBarNotification());
+            if (groupSummary != null && groupSummary != row) {
+                return !groupSummary.areChildrenExpanded();
+            }
+        }
+        return false;
+    }
+
+    /**
      * Updates the scroll position when a child was removed
      *
      * @param removedChild the removed child
@@ -1638,6 +1683,10 @@
     @Override
     protected void onViewAdded(View child) {
         super.onViewAdded(child);
+        onViewAddedInternal(child);
+    }
+
+    private void onViewAddedInternal(View child) {
         mStackScrollAlgorithm.notifyChildrenChanged(this);
         ((ExpandableView) child).setOnHeightChangedListener(this);
         generateAddAnimation(child, false /* fromMoreCard */);
@@ -1650,6 +1699,14 @@
         }
     }
 
+    public void notifyGroupChildRemoved(View row) {
+        onViewRemovedInternal(row);
+    }
+
+    public void notifyGroupChildAdded(View row) {
+        onViewAddedInternal(row);
+    }
+
     public void setAnimationsEnabled(boolean animationsEnabled) {
         mAnimationsEnabled = animationsEnabled;
         updateNotificationAnimationStates();
@@ -1745,10 +1802,20 @@
         generateDarkEvent();
         generateGoToFullShadeEvent();
         generateViewResizeEvent();
+        generateGroupExpansionEvent();
         generateAnimateEverythingEvent();
         mNeedsAnimation = false;
     }
 
+    private void generateGroupExpansionEvent() {
+        // Generate a group expansion/collapsing event if there is such a group at all
+        if (mExpandedGroupView != null) {
+            mAnimationEvents.add(new AnimationEvent(mExpandedGroupView,
+                    AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED));
+            mExpandedGroupView = null;
+        }
+    }
+
     private void generateViewResizeEvent() {
         if (mNeedViewResizeAnimation) {
             mAnimationEvents.add(
@@ -1795,6 +1862,11 @@
                     AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION));
         }
         mChildrenChangingPositions.clear();
+        if (mGenerateChildOrderChangedEvent) {
+            mAnimationEvents.add(new AnimationEvent(null,
+                    AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION));
+            mGenerateChildOrderChangedEvent = false;
+        }
     }
 
     private void generateChildAdditionEvents() {
@@ -2063,11 +2135,14 @@
     }
 
     @Override
-    public void onHeightChanged(ExpandableView view) {
+    public void onHeightChanged(ExpandableView view, boolean needsAnimation) {
         updateContentHeight();
         updateScrollPositionOnExpandInBottom(view);
         clampScrollPosition();
         notifyHeightChangeListener(view);
+        if (needsAnimation) {
+            requestAnimationOnViewResize();
+        }
         requestChildrenUpdate();
     }
 
@@ -2410,6 +2485,10 @@
         this.mPhoneStatusBar = phoneStatusBar;
     }
 
+    public void setGroupManager(NotificationGroupManager groupManager) {
+        this.mGroupManager = groupManager;
+    }
+
     public void onGoToKeyguard() {
         requestAnimateEverything();
     }
@@ -2455,6 +2534,51 @@
         mRemoveAnimationEnabled = enabled;
     }
 
+    private void updateExpandButtons() {
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            if (child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                row.updateExpandButton();
+            }
+        }
+    }
+
+    @Override
+    public void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded) {
+        boolean animated = mAnimationsEnabled && mIsExpanded;
+        if (animated) {
+            mExpandedGroupView = changedRow;
+            mNeedsAnimation = true;
+        }
+        changedRow.setChildrenExpanded(expanded, animated);
+        onHeightChanged(changedRow, false /* needsAnimation */);
+    }
+
+    @Override
+    public void onGroupsProhibitedChanged() {
+        updateExpandButtons();
+    }
+
+    @Override
+    public void onGroupCreatedFromChildren(NotificationGroupManager.NotificationGroup group) {
+        for (NotificationData.Entry entry : group.children) {
+            ExpandableNotificationRow row = entry.row;
+            if (indexOfChild(row) != -1) {
+                removeView(row);
+                group.summary.row.addChildNotification(row);
+            }
+        }
+    }
+
+    public void generateChildOrderChangedEvent() {
+        if (mIsExpanded && mAnimationsEnabled) {
+            mGenerateChildOrderChangedEvent = true;
+            mNeedsAnimation = true;
+            requestChildrenUpdate();
+        }
+    }
+
     /**
      * A listener that is notified when some child locations might have changed.
      */
@@ -2591,6 +2715,14 @@
                         .animateY()
                         .animateZ(),
 
+                // ANIMATION_TYPE_GROUP_EXPANSION_CHANGED
+                new AnimationFilter()
+                        .animateAlpha()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateZ(),
+
                 // ANIMATION_TYPE_EVERYTHING
                 new AnimationFilter()
                         .animateAlpha()
@@ -2645,6 +2777,9 @@
                 // ANIMATION_TYPE_VIEW_RESIZE
                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
 
+                // ANIMATION_TYPE_GROUP_EXPANSION_CHANGED
+                StackStateAnimator.ANIMATION_DURATION_EXPAND_CLICKED,
+
                 // ANIMATION_TYPE_EVERYTHING
                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
         };
@@ -2662,7 +2797,8 @@
         static final int ANIMATION_TYPE_GO_TO_FULL_SHADE = 10;
         static final int ANIMATION_TYPE_HIDE_SENSITIVE = 11;
         static final int ANIMATION_TYPE_VIEW_RESIZE = 12;
-        static final int ANIMATION_TYPE_EVERYTHING = 13;
+        static final int ANIMATION_TYPE_GROUP_EXPANSION_CHANGED = 13;
+        static final int ANIMATION_TYPE_EVERYTHING = 14;
 
         static final int DARK_ANIMATION_ORIGIN_INDEX_ABOVE = -1;
         static final int DARK_ANIMATION_ORIGIN_INDEX_BELOW = -2;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
index 2c124d0..e7bf47b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
@@ -27,6 +27,7 @@
 import com.android.systemui.statusbar.ExpandableView;
 
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * The Algorithm of the {@link com.android.systemui.statusbar.stack
@@ -171,6 +172,19 @@
         updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState);
         updateClipping(resultState, algorithmState);
         updateSpeedBumpState(resultState, algorithmState, ambientState.getSpeedBumpIndex());
+        getNotificationChildrenStates(resultState, algorithmState);
+    }
+
+    private void getNotificationChildrenStates(StackScrollState resultState,
+            StackScrollAlgorithmState algorithmState) {
+        int childCount = algorithmState.visibleChildren.size();
+        for (int i = 0; i < childCount; i++) {
+            ExpandableView v = algorithmState.visibleChildren.get(i);
+            if (v instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+                row.getChildrenStates(resultState);
+            }
+        }
     }
 
     private void updateSpeedBumpState(StackScrollState resultState,
@@ -328,6 +342,23 @@
                 viewState.notGoneIndex = notGoneIndex;
                 state.visibleChildren.add(v);
                 notGoneIndex++;
+
+                // handle the notgoneIndex for the children as well
+                if (v instanceof ExpandableNotificationRow) {
+                    ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+                    List<ExpandableNotificationRow> children =
+                            row.getNotificationChildren();
+                    if (row.areChildrenExpanded() && children != null) {
+                        for (ExpandableNotificationRow childRow : children) {
+                            if (childRow.getVisibility() != View.GONE) {
+                                StackViewState childState
+                                        = resultState.getViewStateForView(childRow);
+                                childState.notGoneIndex = notGoneIndex;
+                                notGoneIndex++;
+                            }
+                        }
+                    }
+                }
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java
index ffd700d..feae590 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java
@@ -23,10 +23,12 @@
 import com.android.systemui.R;
 import com.android.systemui.statusbar.DismissView;
 import com.android.systemui.statusbar.EmptyShadeView;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
 import com.android.systemui.statusbar.ExpandableView;
 import com.android.systemui.statusbar.SpeedBumpView;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -57,6 +59,18 @@
         for (int i = 0; i < numChildren; i++) {
             ExpandableView child = (ExpandableView) mHostView.getChildAt(i);
             resetViewState(child);
+
+            // handling reset for child notifications
+            if (child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                List<ExpandableNotificationRow> children =
+                        row.getNotificationChildren();
+                if (row.areChildrenExpanded() && children != null) {
+                    for (ExpandableNotificationRow childRow : children) {
+                        resetViewState(childRow);
+                    }
+                }
+            }
         }
     }
 
@@ -154,6 +168,10 @@
         if (oldClipTopOptimization != state.topOverLap) {
             view.setClipTopOptimization(state.topOverLap);
         }
+        if (view instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+            row.applyChildrenState(this);
+        }
         return true;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
index 47f0114..b249fbf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
@@ -26,6 +26,7 @@
 import android.view.animation.Interpolator;
 
 import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
 import com.android.systemui.statusbar.ExpandableView;
 import com.android.systemui.statusbar.SpeedBumpView;
 
@@ -42,12 +43,15 @@
     public static final int ANIMATION_DURATION_STANDARD = 360;
     public static final int ANIMATION_DURATION_GO_TO_FULL_SHADE = 448;
     public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
+    public static final int ANIMATION_DURATION_EXPAND_CLICKED = 360;
     public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
     public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
+    public static final int ANIMATION_DELAY_PER_ELEMENT_EXPAND_CHILDREN = 54;
     public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
     public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48;
     public static final int ANIMATION_DELAY_PER_ELEMENT_DARK = 24;
-    private static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
+    public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
+    public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE_CHILDREN = 3;
 
     private static final int TAG_ANIMATOR_TRANSLATION_Y = R.id.translation_y_animator_tag;
     private static final int TAG_ANIMATOR_TRANSLATION_Z = R.id.translation_z_animator_tag;
@@ -85,6 +89,7 @@
 
     private ValueAnimator mTopOverScrollAnimator;
     private ValueAnimator mBottomOverScrollAnimator;
+    private ExpandableNotificationRow mChildExpandingView;
 
     public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
         mHostLayout = hostLayout;
@@ -127,6 +132,7 @@
         }
         mNewEvents.clear();
         mNewAddChildren.clear();
+        mChildExpandingView = null;
     }
 
     private int findLastNotAddedIndex(StackScrollState finalState) {
@@ -216,6 +222,10 @@
         if (child instanceof SpeedBumpView) {
             finalState.performSpeedBumpAnimation(i, (SpeedBumpView) child, viewState,
                     delay + duration);
+        } else if (child instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+            row.startChildAnimation(finalState, this, child == mChildExpandingView, delay,
+                    duration);
         }
     }
 
@@ -813,6 +823,11 @@
                 // A race condition can trigger the view to be added to the overlay even though
                 // it is swiped out. So let's remove it
                 mHostLayout.getOverlay().remove(changingView);
+            } else if (event.animationType == NotificationStackScrollLayout
+                    .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) event.changingView;
+                row.prepareExpansionChanged(finalState);
+                mChildExpandingView = row;
             }
             mNewEvents.add(event);
         }