Import Android SDK Platform PI [4335822]

/google/data/ro/projects/android/fetch_artifact \
    --bid 4335822 \
    --target sdk_phone_armv7-win_sdk \
    sdk-repo-linux-sources-4335822.zip

AndroidVersion.ApiLevel has been modified to appear as 28

Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
diff --git a/com/android/systemui/statusbar/Abortable.java b/com/android/systemui/statusbar/Abortable.java
new file mode 100644
index 0000000..d5ec4f6
--- /dev/null
+++ b/com/android/systemui/statusbar/Abortable.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2017 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;
+
+/**
+ * An interface that allows aborting existing operations.
+ */
+public interface Abortable {
+    void abort();
+}
diff --git a/com/android/systemui/statusbar/ActivatableNotificationView.java b/com/android/systemui/statusbar/ActivatableNotificationView.java
new file mode 100644
index 0000000..68fe9a8
--- /dev/null
+++ b/com/android/systemui/statusbar/ActivatableNotificationView.java
@@ -0,0 +1,1012 @@
+/*
+ * Copyright (C) 2014 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.TimeAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.statusbar.notification.FakeShadowView;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.phone.DoubleTapHelper;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+/**
+ * Base class for both {@link ExpandableNotificationRow} and {@link NotificationShelf}
+ * to implement dimming/activating on Keyguard for the double-tap gesture
+ */
+public abstract class ActivatableNotificationView extends ExpandableOutlineView {
+
+    private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220;
+    private static final int ACTIVATE_ANIMATION_LENGTH = 220;
+    private static final long DARK_ANIMATION_LENGTH = StackStateAnimator.ANIMATION_DURATION_WAKEUP;
+
+    /**
+     * The amount of width, which is kept in the end when performing a disappear animation (also
+     * the amount from which the horizontal appearing begins)
+     */
+    private static final float HORIZONTAL_COLLAPSED_REST_PARTIAL = 0.05f;
+
+    /**
+     * At which point from [0,1] does the horizontal collapse animation end (or start when
+     * expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated.
+     */
+    private static final float HORIZONTAL_ANIMATION_END = 0.2f;
+
+    /**
+     * At which point from [0,1] does the alpha animation end (or start when
+     * expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated.
+     */
+    private static final float ALPHA_ANIMATION_END = 0.0f;
+
+    /**
+     * At which point from [0,1] does the horizontal collapse animation start (or start when
+     * expanding)? 1.0 meaning that it starts immediately and 0.0 that it is animated at all.
+     */
+    private static final float HORIZONTAL_ANIMATION_START = 1.0f;
+
+    /**
+     * At which point from [0,1] does the vertical collapse animation start (or end when
+     * expanding) 1.0 meaning that it starts immediately and 0.0 that it is animated at all.
+     */
+    private static final float VERTICAL_ANIMATION_START = 1.0f;
+
+    /**
+     * Scale for the background to animate from when exiting dark mode.
+     */
+    private static final float DARK_EXIT_SCALE_START = 0.93f;
+
+    /**
+     * A sentinel value when no color should be used. Can be used with {@link #setTintColor(int)}
+     * or {@link #setOverrideTintColor(int, float)}.
+     */
+    protected static final int NO_COLOR = 0;
+
+    private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR
+            = new PathInterpolator(0.6f, 0, 0.5f, 1);
+    private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR
+            = new PathInterpolator(0, 0, 0.5f, 1);
+    private final int mTintedRippleColor;
+    private final int mLowPriorityRippleColor;
+    protected final int mNormalRippleColor;
+    private final AccessibilityManager mAccessibilityManager;
+    private final DoubleTapHelper mDoubleTapHelper;
+
+    private boolean mDimmed;
+    private boolean mDark;
+
+    protected int mBgTint = NO_COLOR;
+    private float mBgAlpha = 1f;
+
+    /**
+     * Flag to indicate that the notification has been touched once and the second touch will
+     * click it.
+     */
+    private boolean mActivated;
+
+    private OnActivatedListener mOnActivatedListener;
+
+    private final Interpolator mSlowOutFastInInterpolator;
+    private final Interpolator mSlowOutLinearInInterpolator;
+    private Interpolator mCurrentAppearInterpolator;
+    private Interpolator mCurrentAlphaInterpolator;
+
+    private NotificationBackgroundView mBackgroundNormal;
+    private NotificationBackgroundView mBackgroundDimmed;
+    private ObjectAnimator mBackgroundAnimator;
+    private RectF mAppearAnimationRect = new RectF();
+    private float mAnimationTranslationY;
+    private boolean mDrawingAppearAnimation;
+    private ValueAnimator mAppearAnimator;
+    private ValueAnimator mBackgroundColorAnimator;
+    private float mAppearAnimationFraction = -1.0f;
+    private float mAppearAnimationTranslation;
+    private final int mNormalColor;
+    private final int mLowPriorityColor;
+    private boolean mIsBelowSpeedBump;
+    private FalsingManager mFalsingManager;
+
+    private float mNormalBackgroundVisibilityAmount;
+    private ValueAnimator mFadeInFromDarkAnimator;
+    private float mDimmedBackgroundFadeInAmount = -1;
+    private ValueAnimator.AnimatorUpdateListener mBackgroundVisibilityUpdater
+            = new ValueAnimator.AnimatorUpdateListener() {
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            setNormalBackgroundVisibilityAmount(mBackgroundNormal.getAlpha());
+            mDimmedBackgroundFadeInAmount = mBackgroundDimmed.getAlpha();
+        }
+    };
+    private AnimatorListenerAdapter mFadeInEndListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            super.onAnimationEnd(animation);
+            mFadeInFromDarkAnimator = null;
+            mDimmedBackgroundFadeInAmount = -1;
+            updateBackground();
+        }
+    };
+    private ValueAnimator.AnimatorUpdateListener mUpdateOutlineListener
+            = new ValueAnimator.AnimatorUpdateListener() {
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            updateOutlineAlpha();
+        }
+    };
+    private float mShadowAlpha = 1.0f;
+    private FakeShadowView mFakeShadow;
+    private int mCurrentBackgroundTint;
+    private int mTargetTint;
+    private int mStartTint;
+    private int mOverrideTint;
+    private float mOverrideAmount;
+    private boolean mShadowHidden;
+    private boolean mWasActivatedOnDown;
+    /**
+     * Similar to mDimmed but is also true if it's not dimmable but should be
+     */
+    private boolean mNeedsDimming;
+    private int mDimmedAlpha;
+
+    public ActivatableNotificationView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f);
+        mSlowOutLinearInInterpolator = new PathInterpolator(0.8f, 0.0f, 1.0f, 1.0f);
+        setClipChildren(false);
+        setClipToPadding(false);
+        mNormalColor = context.getColor(R.color.notification_material_background_color);
+        mLowPriorityColor = context.getColor(
+                R.color.notification_material_background_low_priority_color);
+        mTintedRippleColor = context.getColor(
+                R.color.notification_ripple_tinted_color);
+        mLowPriorityRippleColor = context.getColor(
+                R.color.notification_ripple_color_low_priority);
+        mNormalRippleColor = context.getColor(
+                R.color.notification_ripple_untinted_color);
+        mFalsingManager = FalsingManager.getInstance(context);
+        mAccessibilityManager = AccessibilityManager.getInstance(mContext);
+
+        mDoubleTapHelper = new DoubleTapHelper(this, (active) -> {
+            if (active) {
+                makeActive();
+            } else {
+                makeInactive(true /* animate */);
+            }
+        }, this::performClick, this::handleSlideBack, mFalsingManager::onNotificationDoubleTap);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mBackgroundNormal = findViewById(R.id.backgroundNormal);
+        mFakeShadow = findViewById(R.id.fake_shadow);
+        mShadowHidden = mFakeShadow.getVisibility() != VISIBLE;
+        mBackgroundDimmed = findViewById(R.id.backgroundDimmed);
+        mDimmedAlpha = Color.alpha(mContext.getColor(
+                R.color.notification_material_background_dimmed_color));
+        initBackground();
+        updateBackground();
+        updateBackgroundTint();
+        updateOutlineAlpha();
+    }
+
+    /**
+     * Sets the custom backgrounds on {@link #mBackgroundNormal} and {@link #mBackgroundDimmed}.
+     * This method can also be used to reload the backgrounds on both of those views, which can
+     * be useful in a configuration change.
+     */
+    protected void initBackground() {
+        mBackgroundNormal.setCustomBackground(R.drawable.notification_material_bg);
+        mBackgroundDimmed.setCustomBackground(R.drawable.notification_material_bg_dim);
+    }
+
+    private final Runnable mTapTimeoutRunnable = new Runnable() {
+        @Override
+        public void run() {
+            makeInactive(true /* animate */);
+        }
+    };
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (mNeedsDimming && !mActivated && ev.getActionMasked() == MotionEvent.ACTION_DOWN
+                && disallowSingleClick(ev) && !isTouchExplorationEnabled()) {
+            return true;
+        }
+        return super.onInterceptTouchEvent(ev);
+    }
+
+    private boolean isTouchExplorationEnabled() {
+        return mAccessibilityManager.isTouchExplorationEnabled();
+    }
+
+    protected boolean disallowSingleClick(MotionEvent ev) {
+        return false;
+    }
+
+    protected boolean handleSlideBack() {
+        return false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        boolean result;
+        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+            mWasActivatedOnDown = mActivated;
+        }
+        if ((mNeedsDimming && !mActivated) && !isTouchExplorationEnabled() && isInteractive()) {
+            boolean wasActivated = mActivated;
+            result = handleTouchEventDimmed(event);
+            if (wasActivated && result && event.getAction() == MotionEvent.ACTION_UP) {
+                removeCallbacks(mTapTimeoutRunnable);
+            }
+        } else {
+            result = super.onTouchEvent(event);
+        }
+        return result;
+    }
+
+    /**
+     * @return whether this view is interactive and can be double tapped
+     */
+    protected boolean isInteractive() {
+        return true;
+    }
+
+    @Override
+    public void drawableHotspotChanged(float x, float y) {
+        if (!mDimmed){
+            mBackgroundNormal.drawableHotspotChanged(x, y);
+        }
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+        if (mDimmed) {
+            mBackgroundDimmed.setState(getDrawableState());
+        } else {
+            mBackgroundNormal.setState(getDrawableState());
+        }
+    }
+
+    private boolean handleTouchEventDimmed(MotionEvent event) {
+        if (mNeedsDimming && !mDimmed) {
+            // We're actually dimmed, but our content isn't dimmable, let's ensure we have a ripple
+            super.onTouchEvent(event);
+        }
+        return mDoubleTapHelper.onTouchEvent(event, getActualHeight());
+    }
+
+    @Override
+    public boolean performClick() {
+        if (mWasActivatedOnDown || !mNeedsDimming || isTouchExplorationEnabled()) {
+            return super.performClick();
+        }
+        return false;
+    }
+
+    private void makeActive() {
+        mFalsingManager.onNotificationActive();
+        startActivateAnimation(false /* reverse */);
+        mActivated = true;
+        if (mOnActivatedListener != null) {
+            mOnActivatedListener.onActivated(this);
+        }
+    }
+
+    private void startActivateAnimation(final boolean reverse) {
+        if (!isAttachedToWindow()) {
+            return;
+        }
+        if (!isDimmable()) {
+            return;
+        }
+        int widthHalf = mBackgroundNormal.getWidth()/2;
+        int heightHalf = mBackgroundNormal.getActualHeight()/2;
+        float radius = (float) Math.sqrt(widthHalf*widthHalf + heightHalf*heightHalf);
+        Animator animator;
+        if (reverse) {
+            animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal,
+                    widthHalf, heightHalf, radius, 0);
+        } else {
+            animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal,
+                    widthHalf, heightHalf, 0, radius);
+        }
+        mBackgroundNormal.setVisibility(View.VISIBLE);
+        Interpolator interpolator;
+        Interpolator alphaInterpolator;
+        if (!reverse) {
+            interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+            alphaInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+        } else {
+            interpolator = ACTIVATE_INVERSE_INTERPOLATOR;
+            alphaInterpolator = ACTIVATE_INVERSE_ALPHA_INTERPOLATOR;
+        }
+        animator.setInterpolator(interpolator);
+        animator.setDuration(ACTIVATE_ANIMATION_LENGTH);
+        if (reverse) {
+            mBackgroundNormal.setAlpha(1f);
+            animator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    updateBackground();
+                }
+            });
+            animator.start();
+        } else {
+            mBackgroundNormal.setAlpha(0.4f);
+            animator.start();
+        }
+        mBackgroundNormal.animate()
+                .alpha(reverse ? 0f : 1f)
+                .setInterpolator(alphaInterpolator)
+                .setUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                    @Override
+                    public void onAnimationUpdate(ValueAnimator animation) {
+                        float animatedFraction = animation.getAnimatedFraction();
+                        if (reverse) {
+                            animatedFraction = 1.0f - animatedFraction;
+                        }
+                        setNormalBackgroundVisibilityAmount(animatedFraction);
+                    }
+                })
+                .setDuration(ACTIVATE_ANIMATION_LENGTH);
+    }
+
+    /**
+     * Cancels the hotspot and makes the notification inactive.
+     */
+    public void makeInactive(boolean animate) {
+        if (mActivated) {
+            mActivated = false;
+            if (mDimmed) {
+                if (animate) {
+                    startActivateAnimation(true /* reverse */);
+                } else {
+                    updateBackground();
+                }
+            }
+        }
+        if (mOnActivatedListener != null) {
+            mOnActivatedListener.onActivationReset(this);
+        }
+        removeCallbacks(mTapTimeoutRunnable);
+    }
+
+    public void setDimmed(boolean dimmed, boolean fade) {
+        mNeedsDimming = dimmed;
+        dimmed &= isDimmable();
+        if (mDimmed != dimmed) {
+            mDimmed = dimmed;
+            resetBackgroundAlpha();
+            if (fade) {
+                fadeDimmedBackground();
+            } else {
+                updateBackground();
+            }
+        }
+    }
+
+    public boolean isDimmable() {
+        return true;
+    }
+
+    public void setDark(boolean dark, boolean fade, long delay) {
+        super.setDark(dark, fade, delay);
+        if (mDark == dark) {
+            return;
+        }
+        mDark = dark;
+        updateBackground();
+        updateBackgroundTint(false);
+        if (!dark && fade && !shouldHideBackground()) {
+            fadeInFromDark(delay);
+        }
+        updateOutlineAlpha();
+    }
+
+    private void updateOutlineAlpha() {
+        if (mDark) {
+            setOutlineAlpha(0f);
+            return;
+        }
+        float alpha = NotificationStackScrollLayout.BACKGROUND_ALPHA_DIMMED;
+        alpha = (alpha + (1.0f - alpha) * mNormalBackgroundVisibilityAmount);
+        alpha *= mShadowAlpha;
+        if (mFadeInFromDarkAnimator != null) {
+            alpha *= mFadeInFromDarkAnimator.getAnimatedFraction();
+        }
+        setOutlineAlpha(alpha);
+    }
+
+    public void setNormalBackgroundVisibilityAmount(float normalBackgroundVisibilityAmount) {
+        mNormalBackgroundVisibilityAmount = normalBackgroundVisibilityAmount;
+        updateOutlineAlpha();
+    }
+
+    @Override
+    public void setBelowSpeedBump(boolean below) {
+        super.setBelowSpeedBump(below);
+        if (below != mIsBelowSpeedBump) {
+            mIsBelowSpeedBump = below;
+            updateBackgroundTint();
+            onBelowSpeedBumpChanged();
+        }
+    }
+
+    protected void onBelowSpeedBumpChanged() {
+    }
+
+    /**
+     * @return whether we are below the speed bump
+     */
+    public boolean isBelowSpeedBump() {
+        return mIsBelowSpeedBump;
+    }
+
+    /**
+     * Sets the tint color of the background
+     */
+    public void setTintColor(int color) {
+        setTintColor(color, false);
+    }
+
+    /**
+     * Sets the tint color of the background
+     */
+    public void setTintColor(int color, boolean animated) {
+        if (color != mBgTint) {
+            mBgTint = color;
+            updateBackgroundTint(animated);
+        }
+    }
+
+    /**
+     * Set an override tint color that is used for the background.
+     *
+     * @param color the color that should be used to tint the background.
+     *              This can be {@link #NO_COLOR} if the tint should be normally computed.
+     * @param overrideAmount a value from 0 to 1 how much the override tint should be used. The
+     *                       background color will then be the interpolation between this and the
+     *                       regular background color, where 1 means the overrideTintColor is fully
+     *                       used and the background color not at all.
+     */
+    public void setOverrideTintColor(int color, float overrideAmount) {
+        if (mDark) {
+            color = NO_COLOR;
+            overrideAmount = 0;
+        }
+        mOverrideTint = color;
+        mOverrideAmount = overrideAmount;
+        int newColor = calculateBgColor();
+        setBackgroundTintColor(newColor);
+        if (!isDimmable() && mNeedsDimming) {
+           mBackgroundNormal.setDrawableAlpha((int) NotificationUtils.interpolate(255,
+                   mDimmedAlpha,
+                   overrideAmount));
+        } else {
+            mBackgroundNormal.setDrawableAlpha(255);
+        }
+    }
+
+    protected void updateBackgroundTint() {
+        updateBackgroundTint(false /* animated */);
+    }
+
+    private void updateBackgroundTint(boolean animated) {
+        if (mBackgroundColorAnimator != null) {
+            mBackgroundColorAnimator.cancel();
+        }
+        int rippleColor = getRippleColor();
+        mBackgroundDimmed.setRippleColor(rippleColor);
+        mBackgroundNormal.setRippleColor(rippleColor);
+        int color = calculateBgColor();
+        if (!animated) {
+            setBackgroundTintColor(color);
+        } else if (color != mCurrentBackgroundTint) {
+            mStartTint = mCurrentBackgroundTint;
+            mTargetTint = color;
+            mBackgroundColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+            mBackgroundColorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    int newColor = NotificationUtils.interpolateColors(mStartTint, mTargetTint,
+                            animation.getAnimatedFraction());
+                    setBackgroundTintColor(newColor);
+                }
+            });
+            mBackgroundColorAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+            mBackgroundColorAnimator.setInterpolator(Interpolators.LINEAR);
+            mBackgroundColorAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mBackgroundColorAnimator = null;
+                }
+            });
+            mBackgroundColorAnimator.start();
+        }
+    }
+
+    private void setBackgroundTintColor(int color) {
+        if (color != mCurrentBackgroundTint) {
+            mCurrentBackgroundTint = color;
+            if (color == mNormalColor) {
+                // We don't need to tint a normal notification
+                color = 0;
+            }
+            mBackgroundDimmed.setTint(color);
+            mBackgroundNormal.setTint(color);
+        }
+    }
+
+    /**
+     * Fades in the background when exiting dark mode.
+     */
+    private void fadeInFromDark(long delay) {
+        final View background = mDimmed ? mBackgroundDimmed : mBackgroundNormal;
+        background.setAlpha(0f);
+        mBackgroundVisibilityUpdater.onAnimationUpdate(null);
+        background.animate()
+                .alpha(1f)
+                .setDuration(DARK_ANIMATION_LENGTH)
+                .setStartDelay(delay)
+                .setInterpolator(Interpolators.ALPHA_IN)
+                .setListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationCancel(Animator animation) {
+                        // Jump state if we are cancelled
+                        background.setAlpha(1f);
+                    }
+                })
+                .setUpdateListener(mBackgroundVisibilityUpdater)
+                .start();
+        mFadeInFromDarkAnimator = TimeAnimator.ofFloat(0.0f, 1.0f);
+        mFadeInFromDarkAnimator.setDuration(DARK_ANIMATION_LENGTH);
+        mFadeInFromDarkAnimator.setStartDelay(delay);
+        mFadeInFromDarkAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+        mFadeInFromDarkAnimator.addListener(mFadeInEndListener);
+        mFadeInFromDarkAnimator.addUpdateListener(mUpdateOutlineListener);
+        mFadeInFromDarkAnimator.start();
+    }
+
+    /**
+     * Fades the background when the dimmed state changes.
+     */
+    private void fadeDimmedBackground() {
+        mBackgroundDimmed.animate().cancel();
+        mBackgroundNormal.animate().cancel();
+        if (mActivated) {
+            updateBackground();
+            return;
+        }
+        if (!shouldHideBackground()) {
+            if (mDimmed) {
+                mBackgroundDimmed.setVisibility(View.VISIBLE);
+            } else {
+                mBackgroundNormal.setVisibility(View.VISIBLE);
+            }
+        }
+        float startAlpha = mDimmed ? 1f : 0;
+        float endAlpha = mDimmed ? 0 : 1f;
+        int duration = BACKGROUND_ANIMATION_LENGTH_MS;
+        // Check whether there is already a background animation running.
+        if (mBackgroundAnimator != null) {
+            startAlpha = (Float) mBackgroundAnimator.getAnimatedValue();
+            duration = (int) mBackgroundAnimator.getCurrentPlayTime();
+            mBackgroundAnimator.removeAllListeners();
+            mBackgroundAnimator.cancel();
+            if (duration <= 0) {
+                updateBackground();
+                return;
+            }
+        }
+        mBackgroundNormal.setAlpha(startAlpha);
+        mBackgroundAnimator =
+                ObjectAnimator.ofFloat(mBackgroundNormal, View.ALPHA, startAlpha, endAlpha);
+        mBackgroundAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        mBackgroundAnimator.setDuration(duration);
+        mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                updateBackground();
+                mBackgroundAnimator = null;
+                if (mFadeInFromDarkAnimator == null) {
+                    mDimmedBackgroundFadeInAmount = -1;
+                }
+            }
+        });
+        mBackgroundAnimator.addUpdateListener(mBackgroundVisibilityUpdater);
+        mBackgroundAnimator.start();
+    }
+
+    protected void updateBackgroundAlpha(float transformationAmount) {
+        mBgAlpha =  isChildInGroup() && mDimmed ? transformationAmount : 1f;
+        if (mDimmedBackgroundFadeInAmount != -1) {
+            mBgAlpha *= mDimmedBackgroundFadeInAmount;
+        }
+        mBackgroundDimmed.setAlpha(mBgAlpha);
+    }
+
+    protected void resetBackgroundAlpha() {
+        updateBackgroundAlpha(0f /* transformationAmount */);
+    }
+
+    protected void updateBackground() {
+        cancelFadeAnimations();
+        if (shouldHideBackground()) {
+            mBackgroundDimmed.setVisibility(INVISIBLE);
+            mBackgroundNormal.setVisibility(mActivated ? VISIBLE : INVISIBLE);
+        } else if (mDimmed) {
+            // When groups are animating to the expanded state from the lockscreen, show the
+            // normal background instead of the dimmed background
+            final boolean dontShowDimmed = isGroupExpansionChanging() && isChildInGroup();
+            mBackgroundDimmed.setVisibility(dontShowDimmed ? View.INVISIBLE : View.VISIBLE);
+            mBackgroundNormal.setVisibility((mActivated || dontShowDimmed)
+                    ? View.VISIBLE
+                    : View.INVISIBLE);
+        } else {
+            mBackgroundDimmed.setVisibility(View.INVISIBLE);
+            mBackgroundNormal.setVisibility(View.VISIBLE);
+            mBackgroundNormal.setAlpha(1f);
+            removeCallbacks(mTapTimeoutRunnable);
+            // make in inactive to avoid it sticking around active
+            makeInactive(false /* animate */);
+        }
+        setNormalBackgroundVisibilityAmount(
+                mBackgroundNormal.getVisibility() == View.VISIBLE ? 1.0f : 0.0f);
+    }
+
+    protected boolean shouldHideBackground() {
+        return mDark;
+    }
+
+    private void cancelFadeAnimations() {
+        if (mBackgroundAnimator != null) {
+            mBackgroundAnimator.cancel();
+        }
+        mBackgroundDimmed.animate().cancel();
+        mBackgroundNormal.animate().cancel();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        setPivotX(getWidth() / 2);
+    }
+
+    @Override
+    public void setActualHeight(int actualHeight, boolean notifyListeners) {
+        super.setActualHeight(actualHeight, notifyListeners);
+        setPivotY(actualHeight / 2);
+        mBackgroundNormal.setActualHeight(actualHeight);
+        mBackgroundDimmed.setActualHeight(actualHeight);
+    }
+
+    @Override
+    public void setClipTopAmount(int clipTopAmount) {
+        super.setClipTopAmount(clipTopAmount);
+        mBackgroundNormal.setClipTopAmount(clipTopAmount);
+        mBackgroundDimmed.setClipTopAmount(clipTopAmount);
+    }
+
+    @Override
+    public void setClipBottomAmount(int clipBottomAmount) {
+        super.setClipBottomAmount(clipBottomAmount);
+        mBackgroundNormal.setClipBottomAmount(clipBottomAmount);
+        mBackgroundDimmed.setClipBottomAmount(clipBottomAmount);
+    }
+
+    @Override
+    public void performRemoveAnimation(long duration, float translationDirection,
+            Runnable onFinishedRunnable) {
+        enableAppearDrawing(true);
+        if (mDrawingAppearAnimation) {
+            startAppearAnimation(false /* isAppearing */, translationDirection,
+                    0, duration, onFinishedRunnable);
+        } else if (onFinishedRunnable != null) {
+            onFinishedRunnable.run();
+        }
+    }
+
+    @Override
+    public void performAddAnimation(long delay, long duration) {
+        enableAppearDrawing(true);
+        if (mDrawingAppearAnimation) {
+            startAppearAnimation(true /* isAppearing */, -1.0f, delay, duration, null);
+        }
+    }
+
+    private void startAppearAnimation(boolean isAppearing, float translationDirection, long delay,
+            long duration, final Runnable onFinishedRunnable) {
+        cancelAppearAnimation();
+        mAnimationTranslationY = translationDirection * getActualHeight();
+        if (mAppearAnimationFraction == -1.0f) {
+            // not initialized yet, we start anew
+            if (isAppearing) {
+                mAppearAnimationFraction = 0.0f;
+                mAppearAnimationTranslation = mAnimationTranslationY;
+            } else {
+                mAppearAnimationFraction = 1.0f;
+                mAppearAnimationTranslation = 0;
+            }
+        }
+
+        float targetValue;
+        if (isAppearing) {
+            mCurrentAppearInterpolator = mSlowOutFastInInterpolator;
+            mCurrentAlphaInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+            targetValue = 1.0f;
+        } else {
+            mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN;
+            mCurrentAlphaInterpolator = mSlowOutLinearInInterpolator;
+            targetValue = 0.0f;
+        }
+        mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction,
+                targetValue);
+        mAppearAnimator.setInterpolator(Interpolators.LINEAR);
+        mAppearAnimator.setDuration(
+                (long) (duration * Math.abs(mAppearAnimationFraction - targetValue)));
+        mAppearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                mAppearAnimationFraction = (float) animation.getAnimatedValue();
+                updateAppearAnimationAlpha();
+                updateAppearRect();
+                invalidate();
+            }
+        });
+        if (delay > 0) {
+            // we need to apply the initial state already to avoid drawn frames in the wrong state
+            updateAppearAnimationAlpha();
+            updateAppearRect();
+            mAppearAnimator.setStartDelay(delay);
+        }
+        mAppearAnimator.addListener(new AnimatorListenerAdapter() {
+            private boolean mWasCancelled;
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (onFinishedRunnable != null) {
+                    onFinishedRunnable.run();
+                }
+                if (!mWasCancelled) {
+                    enableAppearDrawing(false);
+                    onAppearAnimationFinished(isAppearing);
+                }
+            }
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mWasCancelled = false;
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mWasCancelled = true;
+            }
+        });
+        mAppearAnimator.start();
+    }
+
+    protected void onAppearAnimationFinished(boolean wasAppearing) {
+    }
+
+    private void cancelAppearAnimation() {
+        if (mAppearAnimator != null) {
+            mAppearAnimator.cancel();
+            mAppearAnimator = null;
+        }
+    }
+
+    public void cancelAppearDrawing() {
+        cancelAppearAnimation();
+        enableAppearDrawing(false);
+    }
+
+    private void updateAppearRect() {
+        float inverseFraction = (1.0f - mAppearAnimationFraction);
+        float translationFraction = mCurrentAppearInterpolator.getInterpolation(inverseFraction);
+        float translateYTotalAmount = translationFraction * mAnimationTranslationY;
+        mAppearAnimationTranslation = translateYTotalAmount;
+
+        // handle width animation
+        float widthFraction = (inverseFraction - (1.0f - HORIZONTAL_ANIMATION_START))
+                / (HORIZONTAL_ANIMATION_START - HORIZONTAL_ANIMATION_END);
+        widthFraction = Math.min(1.0f, Math.max(0.0f, widthFraction));
+        widthFraction = mCurrentAppearInterpolator.getInterpolation(widthFraction);
+        float left = (getWidth() * (0.5f - HORIZONTAL_COLLAPSED_REST_PARTIAL / 2.0f) *
+                widthFraction);
+        float right = getWidth() - left;
+
+        // handle top animation
+        float heightFraction = (inverseFraction - (1.0f - VERTICAL_ANIMATION_START)) /
+                VERTICAL_ANIMATION_START;
+        heightFraction = Math.max(0.0f, heightFraction);
+        heightFraction = mCurrentAppearInterpolator.getInterpolation(heightFraction);
+
+        float top;
+        float bottom;
+        final int actualHeight = getActualHeight();
+        if (mAnimationTranslationY > 0.0f) {
+            bottom = actualHeight - heightFraction * mAnimationTranslationY * 0.1f
+                    - translateYTotalAmount;
+            top = bottom * heightFraction;
+        } else {
+            top = heightFraction * (actualHeight + mAnimationTranslationY) * 0.1f -
+                    translateYTotalAmount;
+            bottom = actualHeight * (1 - heightFraction) + top * heightFraction;
+        }
+        mAppearAnimationRect.set(left, top, right, bottom);
+        setOutlineRect(left, top + mAppearAnimationTranslation, right,
+                bottom + mAppearAnimationTranslation);
+    }
+
+    private void updateAppearAnimationAlpha() {
+        float contentAlphaProgress = mAppearAnimationFraction;
+        contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END);
+        contentAlphaProgress = Math.min(1.0f, contentAlphaProgress);
+        contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress);
+        setContentAlpha(contentAlphaProgress);
+    }
+
+    private void setContentAlpha(float contentAlpha) {
+        View contentView = getContentView();
+        if (contentView.hasOverlappingRendering()) {
+            int layerType = contentAlpha == 0.0f || contentAlpha == 1.0f ? LAYER_TYPE_NONE
+                    : LAYER_TYPE_HARDWARE;
+            int currentLayerType = contentView.getLayerType();
+            if (currentLayerType != layerType) {
+                contentView.setLayerType(layerType, null);
+            }
+        }
+        contentView.setAlpha(contentAlpha);
+    }
+
+    protected abstract View getContentView();
+
+    public int calculateBgColor() {
+        return calculateBgColor(true /* withTint */, true /* withOverRide */);
+    }
+
+    /**
+     * @param withTint should a possible tint be factored in?
+     * @param withOverRide should the value be interpolated with {@link #mOverrideTint}
+     * @return the calculated background color
+     */
+    private int calculateBgColor(boolean withTint, boolean withOverRide) {
+        if (withTint && mDark) {
+            return getContext().getColor(R.color.notification_material_background_dark_color);
+        }
+        if (withOverRide && mOverrideTint != NO_COLOR) {
+            int defaultTint = calculateBgColor(withTint, false);
+            return NotificationUtils.interpolateColors(defaultTint, mOverrideTint, mOverrideAmount);
+        }
+        if (withTint && mBgTint != NO_COLOR) {
+            return mBgTint;
+        } else if (mIsBelowSpeedBump) {
+            return mLowPriorityColor;
+        } else {
+            return mNormalColor;
+        }
+    }
+
+    protected int getRippleColor() {
+        if (mBgTint != 0) {
+            return mTintedRippleColor;
+        } else if (mIsBelowSpeedBump) {
+            return mLowPriorityRippleColor;
+        } else {
+            return mNormalRippleColor;
+        }
+    }
+
+    /**
+     * When we draw the appear animation, we render the view in a bitmap and render this bitmap
+     * as a shader of a rect. This call creates the Bitmap and switches the drawing mode,
+     * such that the normal drawing of the views does not happen anymore.
+     *
+     * @param enable Should it be enabled.
+     */
+    private void enableAppearDrawing(boolean enable) {
+        if (enable != mDrawingAppearAnimation) {
+            mDrawingAppearAnimation = enable;
+            if (!enable) {
+                setContentAlpha(1.0f);
+                mAppearAnimationFraction = -1;
+                setOutlineRect(null);
+            }
+            invalidate();
+        }
+    }
+
+    public boolean isDrawingAppearAnimation() {
+        return mDrawingAppearAnimation;
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        if (mDrawingAppearAnimation) {
+            canvas.save();
+            canvas.translate(0, mAppearAnimationTranslation);
+        }
+        super.dispatchDraw(canvas);
+        if (mDrawingAppearAnimation) {
+            canvas.restore();
+        }
+    }
+
+    public void setOnActivatedListener(OnActivatedListener onActivatedListener) {
+        mOnActivatedListener = onActivatedListener;
+    }
+
+    public boolean hasSameBgColor(ActivatableNotificationView otherView) {
+        return calculateBgColor() == otherView.calculateBgColor();
+    }
+
+    @Override
+    public float getShadowAlpha() {
+        return mShadowAlpha;
+    }
+
+    @Override
+    public void setShadowAlpha(float shadowAlpha) {
+        if (shadowAlpha != mShadowAlpha) {
+            mShadowAlpha = shadowAlpha;
+            updateOutlineAlpha();
+        }
+    }
+
+    @Override
+    public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
+            int outlineTranslation) {
+        boolean hiddenBefore = mShadowHidden;
+        mShadowHidden = shadowIntensity == 0.0f;
+        if (!mShadowHidden || !hiddenBefore) {
+            mFakeShadow.setFakeShadowTranslationZ(shadowIntensity * (getTranslationZ()
+                            + FakeShadowView.SHADOW_SIBLING_TRESHOLD), outlineAlpha, shadowYEnd,
+                    outlineTranslation);
+        }
+    }
+
+    public int getBackgroundColorWithoutTint() {
+        return calculateBgColor(false /* withTint */, false /* withOverride */);
+    }
+
+    public interface OnActivatedListener {
+        void onActivated(ActivatableNotificationView view);
+        void onActivationReset(ActivatableNotificationView view);
+    }
+}
diff --git a/com/android/systemui/statusbar/AlphaImageView.java b/com/android/systemui/statusbar/AlphaImageView.java
new file mode 100644
index 0000000..06dc4e6
--- /dev/null
+++ b/com/android/systemui/statusbar/AlphaImageView.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2014 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.ImageView;
+
+/**
+ * An ImageView which does not have overlapping renderings commands and therefore does not need a
+ * layer when alpha is changed.
+ */
+public class AlphaImageView extends ImageView {
+    public AlphaImageView(Context context) {
+        super(context);
+    }
+
+    public AlphaImageView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public AlphaImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public AlphaImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+}
diff --git a/com/android/systemui/statusbar/AlphaOptimizedButton.java b/com/android/systemui/statusbar/AlphaOptimizedButton.java
new file mode 100644
index 0000000..87c12c2
--- /dev/null
+++ b/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/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java b/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java
new file mode 100644
index 0000000..359272e
--- /dev/null
+++ b/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2014 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.FrameLayout;
+
+/**
+ * A frame layout which does not have overlapping renderings commands and therefore does not need a
+ * layer when alpha is changed.
+ */
+public class AlphaOptimizedFrameLayout extends FrameLayout
+{
+    public AlphaOptimizedFrameLayout(Context context) {
+        super(context);
+    }
+
+    public AlphaOptimizedFrameLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public AlphaOptimizedFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public AlphaOptimizedFrameLayout(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+}
diff --git a/com/android/systemui/statusbar/AlphaOptimizedImageView.java b/com/android/systemui/statusbar/AlphaOptimizedImageView.java
new file mode 100644
index 0000000..ef03d5f
--- /dev/null
+++ b/com/android/systemui/statusbar/AlphaOptimizedImageView.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2014 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.ImageView;
+
+/**
+ * An ImageView which supports an attribute specifying whether it has overlapping rendering
+ * commands and therefore does not need a layer when alpha is changed.
+ */
+public class AlphaOptimizedImageView extends ImageView {
+
+    public AlphaOptimizedImageView(Context context) {
+        this(context, null /* attrs */);
+    }
+
+    public AlphaOptimizedImageView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0 /* defStyleAttr */);
+    }
+
+    public AlphaOptimizedImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
+    }
+
+    public AlphaOptimizedImageView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+}
diff --git a/com/android/systemui/statusbar/AlphaOptimizedView.java b/com/android/systemui/statusbar/AlphaOptimizedView.java
new file mode 100644
index 0000000..d2fe858
--- /dev/null
+++ b/com/android/systemui/statusbar/AlphaOptimizedView.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2014 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.view.View;
+
+/**
+ * A View which does not have overlapping renderings commands and therefore does not need a
+ * layer when alpha is changed.
+ */
+public class AlphaOptimizedView extends View
+{
+    public AlphaOptimizedView(Context context) {
+        super(context);
+    }
+
+    public AlphaOptimizedView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public AlphaOptimizedView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public AlphaOptimizedView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+}
diff --git a/com/android/systemui/statusbar/AnimatedImageView.java b/com/android/systemui/statusbar/AnimatedImageView.java
new file mode 100644
index 0000000..ba92c45
--- /dev/null
+++ b/com/android/systemui/statusbar/AnimatedImageView.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2008 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.content.res.TypedArray;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.systemui.R;
+
+@RemoteView
+public class AnimatedImageView extends ImageView {
+    private final boolean mHasOverlappingRendering;
+    AnimationDrawable mAnim;
+    boolean mAttached;
+    private boolean mAllowAnimation = true;
+
+    // Tracks the last image that was set, so that we don't refresh the image if it is exactly
+    // the same as the previous one. If this is a resid, we track that. If it's a drawable, we
+    // track the hashcode of the drawable.
+    int mDrawableId;
+
+    public AnimatedImageView(Context context) {
+        this(context, null);
+    }
+
+    public AnimatedImageView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
+                R.styleable.AnimatedImageView, 0, 0);
+
+        try {
+            // Default to true, which is what View.java defaults toA
+            mHasOverlappingRendering = a.getBoolean(
+                    R.styleable.AnimatedImageView_hasOverlappingRendering, true);
+        } finally {
+            a.recycle();
+        }
+    }
+
+    public void setAllowAnimation(boolean allowAnimation) {
+        if (mAllowAnimation != allowAnimation) {
+            mAllowAnimation = allowAnimation;
+            updateAnim();
+            if (!mAllowAnimation && mAnim != null) {
+                // Reset drawable such that we show the first frame whenever we're not animating.
+                mAnim.setVisible(getVisibility() == VISIBLE, true /* restart */);
+            }
+        }
+    }
+
+    private void updateAnim() {
+        Drawable drawable = getDrawable();
+        if (mAttached && mAnim != null) {
+            mAnim.stop();
+        }
+        if (drawable instanceof AnimationDrawable) {
+            mAnim = (AnimationDrawable) drawable;
+            if (isShown() && mAllowAnimation) {
+                mAnim.start();
+            }
+        } else {
+            mAnim = null;
+        }
+    }
+
+    @Override
+    public void setImageDrawable(Drawable drawable) {
+        if (drawable != null) {
+            if (mDrawableId == drawable.hashCode()) return;
+
+            mDrawableId = drawable.hashCode();
+        } else {
+            mDrawableId = 0;
+        }
+        super.setImageDrawable(drawable);
+        updateAnim();
+    }
+
+    @Override
+    @android.view.RemotableViewMethod
+    public void setImageResource(int resid) {
+        if (mDrawableId == resid) return;
+
+        mDrawableId = resid;
+        super.setImageResource(resid);
+        updateAnim();
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mAttached = true;
+        updateAnim();
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (mAnim != null) {
+            mAnim.stop();
+        }
+        mAttached = false;
+    }
+
+    @Override
+    protected void onVisibilityChanged(View changedView, int vis) {
+        super.onVisibilityChanged(changedView, vis);
+        if (mAnim != null) {
+            if (isShown() && mAllowAnimation) {
+                mAnim.start();
+            } else {
+                mAnim.stop();
+            }
+        }
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return mHasOverlappingRendering;
+    }
+}
+
diff --git a/com/android/systemui/statusbar/BackDropView.java b/com/android/systemui/statusbar/BackDropView.java
new file mode 100644
index 0000000..f1eb9fe
--- /dev/null
+++ b/com/android/systemui/statusbar/BackDropView.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2014 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.view.View;
+import android.widget.FrameLayout;
+
+/**
+ * A view who contains media artwork.
+ */
+public class BackDropView extends FrameLayout
+{
+    private Runnable mOnVisibilityChangedRunnable;
+
+    public BackDropView(Context context) {
+        super(context);
+    }
+
+    public BackDropView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public BackDropView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public BackDropView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    @Override
+    protected void onVisibilityChanged(View changedView, int visibility) {
+        super.onVisibilityChanged(changedView, visibility);
+        if (changedView == this && mOnVisibilityChangedRunnable != null) {
+            mOnVisibilityChangedRunnable.run();
+        }
+    }
+
+    public void setOnVisibilityChangedRunnable(Runnable runnable) {
+        mOnVisibilityChangedRunnable = runnable;
+    }
+
+}
diff --git a/com/android/systemui/statusbar/CommandQueue.java b/com/android/systemui/statusbar/CommandQueue.java
new file mode 100644
index 0000000..6349275
--- /dev/null
+++ b/com/android/systemui/statusbar/CommandQueue.java
@@ -0,0 +1,670 @@
+/*
+ * Copyright (C) 2010 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.ComponentName;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.support.annotation.VisibleForTesting;
+import android.util.Pair;
+
+import com.android.internal.os.SomeArgs;
+import com.android.internal.statusbar.IStatusBar;
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.SystemUI;
+
+import java.util.ArrayList;
+
+/**
+ * This class takes the functions from IStatusBar that come in on
+ * binder pool threads and posts messages to get them onto the main
+ * thread, and calls onto Callbacks.  It also takes care of
+ * coalescing these calls so they don't stack up.  For the calls
+ * are coalesced, note that they are all idempotent.
+ */
+public class CommandQueue extends IStatusBar.Stub {
+    private static final int INDEX_MASK = 0xffff;
+    private static final int MSG_SHIFT  = 16;
+    private static final int MSG_MASK   = 0xffff << MSG_SHIFT;
+
+    private static final int OP_SET_ICON    = 1;
+    private static final int OP_REMOVE_ICON = 2;
+
+    private static final int MSG_ICON                          = 1 << MSG_SHIFT;
+    private static final int MSG_DISABLE                       = 2 << MSG_SHIFT;
+    private static final int MSG_EXPAND_NOTIFICATIONS          = 3 << MSG_SHIFT;
+    private static final int MSG_COLLAPSE_PANELS               = 4 << MSG_SHIFT;
+    private static final int MSG_EXPAND_SETTINGS               = 5 << MSG_SHIFT;
+    private static final int MSG_SET_SYSTEMUI_VISIBILITY       = 6 << MSG_SHIFT;
+    private static final int MSG_TOP_APP_WINDOW_CHANGED        = 7 << MSG_SHIFT;
+    private static final int MSG_SHOW_IME_BUTTON               = 8 << MSG_SHIFT;
+    private static final int MSG_TOGGLE_RECENT_APPS            = 9 << MSG_SHIFT;
+    private static final int MSG_PRELOAD_RECENT_APPS           = 10 << MSG_SHIFT;
+    private static final int MSG_CANCEL_PRELOAD_RECENT_APPS    = 11 << MSG_SHIFT;
+    private static final int MSG_SET_WINDOW_STATE              = 12 << MSG_SHIFT;
+    private static final int MSG_SHOW_RECENT_APPS              = 13 << MSG_SHIFT;
+    private static final int MSG_HIDE_RECENT_APPS              = 14 << MSG_SHIFT;
+    private static final int MSG_SHOW_SCREEN_PIN_REQUEST       = 18 << MSG_SHIFT;
+    private static final int MSG_APP_TRANSITION_PENDING        = 19 << MSG_SHIFT;
+    private static final int MSG_APP_TRANSITION_CANCELLED      = 20 << MSG_SHIFT;
+    private static final int MSG_APP_TRANSITION_STARTING       = 21 << MSG_SHIFT;
+    private static final int MSG_ASSIST_DISCLOSURE             = 22 << MSG_SHIFT;
+    private static final int MSG_START_ASSIST                  = 23 << MSG_SHIFT;
+    private static final int MSG_CAMERA_LAUNCH_GESTURE         = 24 << MSG_SHIFT;
+    private static final int MSG_TOGGLE_KEYBOARD_SHORTCUTS     = 25 << MSG_SHIFT;
+    private static final int MSG_SHOW_PICTURE_IN_PICTURE_MENU  = 26 << MSG_SHIFT;
+    private static final int MSG_ADD_QS_TILE                   = 27 << MSG_SHIFT;
+    private static final int MSG_REMOVE_QS_TILE                = 28 << MSG_SHIFT;
+    private static final int MSG_CLICK_QS_TILE                 = 29 << MSG_SHIFT;
+    private static final int MSG_TOGGLE_APP_SPLIT_SCREEN       = 30 << MSG_SHIFT;
+    private static final int MSG_APP_TRANSITION_FINISHED       = 31 << MSG_SHIFT;
+    private static final int MSG_DISMISS_KEYBOARD_SHORTCUTS    = 32 << MSG_SHIFT;
+    private static final int MSG_HANDLE_SYSTEM_KEY             = 33 << MSG_SHIFT;
+    private static final int MSG_SHOW_GLOBAL_ACTIONS           = 34 << MSG_SHIFT;
+    private static final int MSG_TOGGLE_PANEL                  = 35 << MSG_SHIFT;
+    private static final int MSG_SHOW_SHUTDOWN_UI              = 36 << MSG_SHIFT;
+    private static final int MSG_SET_TOP_APP_HIDES_STATUS_BAR  = 37 << MSG_SHIFT;
+
+    public static final int FLAG_EXCLUDE_NONE = 0;
+    public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
+    public static final int FLAG_EXCLUDE_RECENTS_PANEL = 1 << 1;
+    public static final int FLAG_EXCLUDE_NOTIFICATION_PANEL = 1 << 2;
+    public static final int FLAG_EXCLUDE_INPUT_METHODS_PANEL = 1 << 3;
+    public static final int FLAG_EXCLUDE_COMPAT_MODE_PANEL = 1 << 4;
+
+    private static final String SHOW_IME_SWITCHER_KEY = "showImeSwitcherKey";
+
+    private final Object mLock = new Object();
+    private ArrayList<Callbacks> mCallbacks = new ArrayList<>();
+    private Handler mHandler = new H(Looper.getMainLooper());
+    private int mDisable1;
+    private int mDisable2;
+
+    /**
+     * These methods are called back on the main thread.
+     */
+    public interface Callbacks {
+        default void setIcon(String slot, StatusBarIcon icon) { }
+        default void removeIcon(String slot) { }
+        default void disable(int state1, int state2, boolean animate) { }
+        default void animateExpandNotificationsPanel() { }
+        default void animateCollapsePanels(int flags) { }
+        default void togglePanel() { }
+        default void animateExpandSettingsPanel(String obj) { }
+        default void setSystemUiVisibility(int vis, int fullscreenStackVis,
+                int dockedStackVis, int mask, Rect fullscreenStackBounds, Rect dockedStackBounds) {
+        }
+        default void topAppWindowChanged(boolean visible) { }
+        default void setImeWindowStatus(IBinder token, int vis, int backDisposition,
+                boolean showImeSwitcher) { }
+        default void showRecentApps(boolean triggeredFromAltTab, boolean fromHome) { }
+        default void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { }
+        default void toggleRecentApps() { }
+        default void toggleSplitScreen() { }
+        default void preloadRecentApps() { }
+        default void dismissKeyboardShortcutsMenu() { }
+        default void toggleKeyboardShortcutsMenu(int deviceId) { }
+        default void cancelPreloadRecentApps() { }
+        default void setWindowState(int window, int state) { }
+        default void showScreenPinningRequest(int taskId) { }
+        default void appTransitionPending(boolean forced) { }
+        default void appTransitionCancelled() { }
+        default void appTransitionStarting(long startTime, long duration, boolean forced) { }
+        default void appTransitionFinished() { }
+        default void showAssistDisclosure() { }
+        default void startAssist(Bundle args) { }
+        default void onCameraLaunchGestureDetected(int source) { }
+        default void showPictureInPictureMenu() { }
+        default void setTopAppHidesStatusBar(boolean topAppHidesStatusBar) { }
+
+        default void addQsTile(ComponentName tile) { }
+        default void remQsTile(ComponentName tile) { }
+        default void clickTile(ComponentName tile) { }
+
+        default void handleSystemKey(int arg1) { }
+        default void handleShowGlobalActionsMenu() { }
+        default void handleShowShutdownUi(boolean isReboot, String reason) { }
+    }
+
+    @VisibleForTesting
+    protected CommandQueue() {
+    }
+
+    public void addCallbacks(Callbacks callbacks) {
+        mCallbacks.add(callbacks);
+        callbacks.disable(mDisable1, mDisable2, false /* animate */);
+    }
+
+    public void removeCallbacks(Callbacks callbacks) {
+        mCallbacks.remove(callbacks);
+    }
+
+    public void setIcon(String slot, StatusBarIcon icon) {
+        synchronized (mLock) {
+            // don't coalesce these
+            mHandler.obtainMessage(MSG_ICON, OP_SET_ICON, 0,
+                    new Pair<String, StatusBarIcon>(slot, icon)).sendToTarget();
+        }
+    }
+
+    public void removeIcon(String slot) {
+        synchronized (mLock) {
+            // don't coalesce these
+            mHandler.obtainMessage(MSG_ICON, OP_REMOVE_ICON, 0, slot).sendToTarget();
+        }
+    }
+
+    public void disable(int state1, int state2, boolean animate) {
+        synchronized (mLock) {
+            mDisable1 = state1;
+            mDisable2 = state2;
+            mHandler.removeMessages(MSG_DISABLE);
+            Message msg = mHandler.obtainMessage(MSG_DISABLE, state1, state2, animate);
+            if (Looper.myLooper() == mHandler.getLooper()) {
+                // If its the right looper execute immediately so hides can be handled quickly.
+                mHandler.handleMessage(msg);
+                msg.recycle();
+            } else {
+                msg.sendToTarget();
+            }
+        }
+    }
+
+    public void disable(int state1, int state2) {
+        disable(state1, state2, true);
+    }
+
+    public void recomputeDisableFlags(boolean animate) {
+        disable(mDisable1, mDisable2, animate);
+    }
+
+    public void animateExpandNotificationsPanel() {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_EXPAND_NOTIFICATIONS);
+            mHandler.sendEmptyMessage(MSG_EXPAND_NOTIFICATIONS);
+        }
+    }
+
+    public void animateCollapsePanels() {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_COLLAPSE_PANELS);
+            mHandler.obtainMessage(MSG_COLLAPSE_PANELS, 0, 0).sendToTarget();
+        }
+    }
+
+    public void animateCollapsePanels(int flags) {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_COLLAPSE_PANELS);
+            mHandler.obtainMessage(MSG_COLLAPSE_PANELS, flags, 0).sendToTarget();
+        }
+    }
+
+    public void togglePanel() {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_TOGGLE_PANEL);
+            mHandler.obtainMessage(MSG_TOGGLE_PANEL, 0, 0).sendToTarget();
+        }
+    }
+
+    public void animateExpandSettingsPanel(String subPanel) {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_EXPAND_SETTINGS);
+            mHandler.obtainMessage(MSG_EXPAND_SETTINGS, subPanel).sendToTarget();
+        }
+    }
+
+    public void setSystemUiVisibility(int vis, int fullscreenStackVis, int dockedStackVis,
+            int mask, Rect fullscreenStackBounds, Rect dockedStackBounds) {
+        synchronized (mLock) {
+            // Don't coalesce these, since it might have one time flags set such as
+            // STATUS_BAR_UNHIDE which might get lost.
+            SomeArgs args = SomeArgs.obtain();
+            args.argi1 = vis;
+            args.argi2 = fullscreenStackVis;
+            args.argi3 = dockedStackVis;
+            args.argi4 = mask;
+            args.arg1 = fullscreenStackBounds;
+            args.arg2 = dockedStackBounds;
+            mHandler.obtainMessage(MSG_SET_SYSTEMUI_VISIBILITY, args).sendToTarget();
+        }
+    }
+
+    public void topAppWindowChanged(boolean menuVisible) {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_TOP_APP_WINDOW_CHANGED);
+            mHandler.obtainMessage(MSG_TOP_APP_WINDOW_CHANGED, menuVisible ? 1 : 0, 0,
+                    null).sendToTarget();
+        }
+    }
+
+    public void setImeWindowStatus(IBinder token, int vis, int backDisposition,
+            boolean showImeSwitcher) {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_SHOW_IME_BUTTON);
+            Message m = mHandler.obtainMessage(MSG_SHOW_IME_BUTTON, vis, backDisposition, token);
+            m.getData().putBoolean(SHOW_IME_SWITCHER_KEY, showImeSwitcher);
+            m.sendToTarget();
+        }
+    }
+
+    public void showRecentApps(boolean triggeredFromAltTab, boolean fromHome) {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_SHOW_RECENT_APPS);
+            mHandler.obtainMessage(MSG_SHOW_RECENT_APPS,
+                    triggeredFromAltTab ? 1 : 0, fromHome ? 1 : 0, null).sendToTarget();
+        }
+    }
+
+    public void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_HIDE_RECENT_APPS);
+            mHandler.obtainMessage(MSG_HIDE_RECENT_APPS,
+                    triggeredFromAltTab ? 1 : 0, triggeredFromHomeKey ? 1 : 0,
+                    null).sendToTarget();
+        }
+    }
+
+    public void toggleSplitScreen() {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_TOGGLE_APP_SPLIT_SCREEN);
+            mHandler.obtainMessage(MSG_TOGGLE_APP_SPLIT_SCREEN, 0, 0, null).sendToTarget();
+        }
+    }
+
+    public void toggleRecentApps() {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_TOGGLE_RECENT_APPS);
+            Message msg = mHandler.obtainMessage(MSG_TOGGLE_RECENT_APPS, 0, 0, null);
+            msg.setAsynchronous(true);
+            msg.sendToTarget();
+        }
+    }
+
+    public void preloadRecentApps() {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_PRELOAD_RECENT_APPS);
+            mHandler.obtainMessage(MSG_PRELOAD_RECENT_APPS, 0, 0, null).sendToTarget();
+        }
+    }
+
+    public void cancelPreloadRecentApps() {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_CANCEL_PRELOAD_RECENT_APPS);
+            mHandler.obtainMessage(MSG_CANCEL_PRELOAD_RECENT_APPS, 0, 0, null).sendToTarget();
+        }
+    }
+
+    @Override
+    public void dismissKeyboardShortcutsMenu() {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_DISMISS_KEYBOARD_SHORTCUTS);
+            mHandler.obtainMessage(MSG_DISMISS_KEYBOARD_SHORTCUTS).sendToTarget();
+        }
+    }
+
+    @Override
+    public void toggleKeyboardShortcutsMenu(int deviceId) {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_TOGGLE_KEYBOARD_SHORTCUTS);
+            mHandler.obtainMessage(MSG_TOGGLE_KEYBOARD_SHORTCUTS, deviceId, 0).sendToTarget();
+        }
+    }
+
+    @Override
+    public void showPictureInPictureMenu() {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_SHOW_PICTURE_IN_PICTURE_MENU);
+            mHandler.obtainMessage(MSG_SHOW_PICTURE_IN_PICTURE_MENU).sendToTarget();
+        }
+    }
+
+    public void setWindowState(int window, int state) {
+        synchronized (mLock) {
+            // don't coalesce these
+            mHandler.obtainMessage(MSG_SET_WINDOW_STATE, window, state, null).sendToTarget();
+        }
+    }
+
+    public void showScreenPinningRequest(int taskId) {
+        synchronized (mLock) {
+            mHandler.obtainMessage(MSG_SHOW_SCREEN_PIN_REQUEST, taskId, 0, null)
+                    .sendToTarget();
+        }
+    }
+
+    public void appTransitionPending() {
+        appTransitionPending(false /* forced */);
+    }
+
+    public void appTransitionPending(boolean forced) {
+        synchronized (mLock) {
+            mHandler.obtainMessage(MSG_APP_TRANSITION_PENDING, forced ? 1 : 0, 0).sendToTarget();
+        }
+    }
+
+    public void appTransitionCancelled() {
+        synchronized (mLock) {
+            mHandler.sendEmptyMessage(MSG_APP_TRANSITION_CANCELLED);
+        }
+    }
+
+    public void appTransitionStarting(long startTime, long duration) {
+        appTransitionStarting(startTime, duration, false /* forced */);
+    }
+
+    public void appTransitionStarting(long startTime, long duration, boolean forced) {
+        synchronized (mLock) {
+            mHandler.obtainMessage(MSG_APP_TRANSITION_STARTING, forced ? 1 : 0, 0,
+                    Pair.create(startTime, duration)).sendToTarget();
+        }
+    }
+
+    @Override
+    public void appTransitionFinished() {
+        synchronized (mLock) {
+            mHandler.sendEmptyMessage(MSG_APP_TRANSITION_FINISHED);
+        }
+    }
+
+    public void showAssistDisclosure() {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_ASSIST_DISCLOSURE);
+            mHandler.obtainMessage(MSG_ASSIST_DISCLOSURE).sendToTarget();
+        }
+    }
+
+    public void startAssist(Bundle args) {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_START_ASSIST);
+            mHandler.obtainMessage(MSG_START_ASSIST, args).sendToTarget();
+        }
+    }
+
+    @Override
+    public void onCameraLaunchGestureDetected(int source) {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_CAMERA_LAUNCH_GESTURE);
+            mHandler.obtainMessage(MSG_CAMERA_LAUNCH_GESTURE, source, 0).sendToTarget();
+        }
+    }
+
+    @Override
+    public void addQsTile(ComponentName tile) {
+        synchronized (mLock) {
+            mHandler.obtainMessage(MSG_ADD_QS_TILE, tile).sendToTarget();
+        }
+    }
+
+    @Override
+    public void remQsTile(ComponentName tile) {
+        synchronized (mLock) {
+            mHandler.obtainMessage(MSG_REMOVE_QS_TILE, tile).sendToTarget();
+        }
+    }
+
+    @Override
+    public void clickQsTile(ComponentName tile) {
+        synchronized (mLock) {
+            mHandler.obtainMessage(MSG_CLICK_QS_TILE, tile).sendToTarget();
+        }
+    }
+
+    @Override
+    public void handleSystemKey(int key) {
+        synchronized (mLock) {
+            mHandler.obtainMessage(MSG_HANDLE_SYSTEM_KEY, key, 0).sendToTarget();
+        }
+    }
+
+    @Override
+    public void showGlobalActionsMenu() {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_SHOW_GLOBAL_ACTIONS);
+            mHandler.obtainMessage(MSG_SHOW_GLOBAL_ACTIONS).sendToTarget();
+        }
+    }
+
+    @Override
+    public void setTopAppHidesStatusBar(boolean hidesStatusBar) {
+        mHandler.removeMessages(MSG_SET_TOP_APP_HIDES_STATUS_BAR);
+        mHandler.obtainMessage(MSG_SET_TOP_APP_HIDES_STATUS_BAR, hidesStatusBar ? 1 : 0, 0)
+                .sendToTarget();
+    }
+
+    @Override
+    public void showShutdownUi(boolean isReboot, String reason) {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_SHOW_SHUTDOWN_UI);
+            mHandler.obtainMessage(MSG_SHOW_SHUTDOWN_UI, isReboot ? 1 : 0, 0, reason)
+                    .sendToTarget();
+        }
+    }
+
+    private final class H extends Handler {
+        private H(Looper l) {
+            super(l);
+        }
+
+        public void handleMessage(Message msg) {
+            final int what = msg.what & MSG_MASK;
+            switch (what) {
+                case MSG_ICON: {
+                    switch (msg.arg1) {
+                        case OP_SET_ICON: {
+                            Pair<String, StatusBarIcon> p = (Pair<String, StatusBarIcon>) msg.obj;
+                            for (int i = 0; i < mCallbacks.size(); i++) {
+                                mCallbacks.get(i).setIcon(p.first, p.second);
+                            }
+                            break;
+                        }
+                        case OP_REMOVE_ICON:
+                            for (int i = 0; i < mCallbacks.size(); i++) {
+                                mCallbacks.get(i).removeIcon((String) msg.obj);
+                            }
+                            break;
+                    }
+                    break;
+                }
+                case MSG_DISABLE:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).disable(msg.arg1, msg.arg2, (Boolean) msg.obj);
+                    }
+                    break;
+                case MSG_EXPAND_NOTIFICATIONS:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).animateExpandNotificationsPanel();
+                    }
+                    break;
+                case MSG_COLLAPSE_PANELS:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).animateCollapsePanels(msg.arg1);
+                    }
+                    break;
+                case MSG_TOGGLE_PANEL:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).togglePanel();
+                    }
+                    break;
+                case MSG_EXPAND_SETTINGS:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).animateExpandSettingsPanel((String) msg.obj);
+                    }
+                    break;
+                case MSG_SET_SYSTEMUI_VISIBILITY:
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).setSystemUiVisibility(args.argi1, args.argi2, args.argi3,
+                                args.argi4, (Rect) args.arg1, (Rect) args.arg2);
+                    }
+                    args.recycle();
+                    break;
+                case MSG_TOP_APP_WINDOW_CHANGED:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).topAppWindowChanged(msg.arg1 != 0);
+                    }
+                    break;
+                case MSG_SHOW_IME_BUTTON:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).setImeWindowStatus((IBinder) msg.obj, msg.arg1, msg.arg2,
+                                msg.getData().getBoolean(SHOW_IME_SWITCHER_KEY, false));
+                    }
+                    break;
+                case MSG_SHOW_RECENT_APPS:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).showRecentApps(msg.arg1 != 0, msg.arg2 != 0);
+                    }
+                    break;
+                case MSG_HIDE_RECENT_APPS:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).hideRecentApps(msg.arg1 != 0, msg.arg2 != 0);
+                    }
+                    break;
+                case MSG_TOGGLE_RECENT_APPS:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).toggleRecentApps();
+                    }
+                    break;
+                case MSG_PRELOAD_RECENT_APPS:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).preloadRecentApps();
+                    }
+                    break;
+                case MSG_CANCEL_PRELOAD_RECENT_APPS:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).cancelPreloadRecentApps();
+                    }
+                    break;
+                case MSG_DISMISS_KEYBOARD_SHORTCUTS:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).dismissKeyboardShortcutsMenu();
+                    }
+                    break;
+                case MSG_TOGGLE_KEYBOARD_SHORTCUTS:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).toggleKeyboardShortcutsMenu(msg.arg1);
+                    }
+                    break;
+                case MSG_SET_WINDOW_STATE:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).setWindowState(msg.arg1, msg.arg2);
+                    }
+                    break;
+                case MSG_SHOW_SCREEN_PIN_REQUEST:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).showScreenPinningRequest(msg.arg1);
+                    }
+                    break;
+                case MSG_APP_TRANSITION_PENDING:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).appTransitionPending(msg.arg1 != 0);
+                    }
+                    break;
+                case MSG_APP_TRANSITION_CANCELLED:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).appTransitionCancelled();
+                    }
+                    break;
+                case MSG_APP_TRANSITION_STARTING:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        Pair<Long, Long> data = (Pair<Long, Long>) msg.obj;
+                        mCallbacks.get(i).appTransitionStarting(data.first, data.second,
+                                msg.arg1 != 0);
+                    }
+                    break;
+                case MSG_APP_TRANSITION_FINISHED:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).appTransitionFinished();
+                    }
+                    break;
+                case MSG_ASSIST_DISCLOSURE:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).showAssistDisclosure();
+                    }
+                    break;
+                case MSG_START_ASSIST:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).startAssist((Bundle) msg.obj);
+                    }
+                    break;
+                case MSG_CAMERA_LAUNCH_GESTURE:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).onCameraLaunchGestureDetected(msg.arg1);
+                    }
+                    break;
+                case MSG_SHOW_PICTURE_IN_PICTURE_MENU:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).showPictureInPictureMenu();
+                    }
+                    break;
+                case MSG_ADD_QS_TILE:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).addQsTile((ComponentName) msg.obj);
+                    }
+                    break;
+                case MSG_REMOVE_QS_TILE:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).remQsTile((ComponentName) msg.obj);
+                    }
+                    break;
+                case MSG_CLICK_QS_TILE:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).clickTile((ComponentName) msg.obj);
+                    }
+                    break;
+                case MSG_TOGGLE_APP_SPLIT_SCREEN:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).toggleSplitScreen();
+                    }
+                    break;
+                case MSG_HANDLE_SYSTEM_KEY:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).handleSystemKey(msg.arg1);
+                    }
+                    break;
+                case MSG_SHOW_GLOBAL_ACTIONS:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).handleShowGlobalActionsMenu();
+                    }
+                    break;
+                case MSG_SHOW_SHUTDOWN_UI:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).handleShowShutdownUi(msg.arg1 != 0, (String) msg.obj);
+                    }
+                    break;
+                case MSG_SET_TOP_APP_HIDES_STATUS_BAR:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).setTopAppHidesStatusBar(msg.arg1 != 0);
+                    }
+                    break;
+            }
+        }
+    }
+
+    // Need this class since CommandQueue already extends IStatusBar.Stub, so CommandQueueStart
+    // is needed so it can extend SystemUI.
+    public static class CommandQueueStart extends SystemUI {
+        @Override
+        public void start() {
+            putComponent(CommandQueue.class, new CommandQueue());
+        }
+    }
+}
+
diff --git a/com/android/systemui/statusbar/CrossFadeHelper.java b/com/android/systemui/statusbar/CrossFadeHelper.java
new file mode 100644
index 0000000..e8f0925
--- /dev/null
+++ b/com/android/systemui/statusbar/CrossFadeHelper.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2016 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.view.View;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+/**
+ * A helper to fade views in and out.
+ */
+public class CrossFadeHelper {
+    public static final long ANIMATION_DURATION_LENGTH = 210;
+
+    public static void fadeOut(final View view, final Runnable endRunnable) {
+        view.animate().cancel();
+        view.animate()
+                .alpha(0f)
+                .setDuration(ANIMATION_DURATION_LENGTH)
+                .setInterpolator(Interpolators.ALPHA_OUT)
+                .withEndAction(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (endRunnable != null) {
+                            endRunnable.run();
+                        }
+                        view.setVisibility(View.INVISIBLE);
+                    }
+                });
+        if (view.hasOverlappingRendering()) {
+            view.animate().withLayer();
+        }
+    }
+
+    public static void fadeOut(View view, float fadeOutAmount) {
+        fadeOut(view, fadeOutAmount, true /* remap */);
+    }
+
+    /**
+     * Fade out a view by a given progress amount
+     * @param view the view to fade out
+     * @param fadeOutAmount how much the view is faded out. 0 means not at all and 1 means fully
+     *                      faded out
+     * @param remap whether the fade amount should be remapped to the shorter duration
+     * {@link #ANIMATION_DURATION_LENGTH} from the normal fade duration
+     * {@link StackStateAnimator#ANIMATION_DURATION_STANDARD} in order to have a faster fading.
+     *
+     * @see #fadeIn(View, float, boolean)
+     */
+    public static void fadeOut(View view, float fadeOutAmount, boolean remap) {
+        view.animate().cancel();
+        if (fadeOutAmount == 1.0f) {
+            view.setVisibility(View.INVISIBLE);
+        } else if (view.getVisibility() == View.INVISIBLE) {
+            view.setVisibility(View.VISIBLE);
+        }
+        if (remap) {
+            fadeOutAmount = mapToFadeDuration(fadeOutAmount);
+        }
+        float alpha = Interpolators.ALPHA_OUT.getInterpolation(1.0f - fadeOutAmount);
+        view.setAlpha(alpha);
+        updateLayerType(view, alpha);
+    }
+
+    private static float mapToFadeDuration(float fadeOutAmount) {
+        // Assuming a linear interpolator, we can easily map it to our new duration
+        float endPoint = (float) ANIMATION_DURATION_LENGTH
+                / (float) StackStateAnimator.ANIMATION_DURATION_STANDARD;
+        return Math.min(fadeOutAmount / endPoint, 1.0f);
+    }
+
+    private static void updateLayerType(View view, float alpha) {
+        if (view.hasOverlappingRendering() && alpha > 0.0f && alpha < 1.0f) {
+            view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+        } else if (view.getLayerType() == View.LAYER_TYPE_HARDWARE) {
+            view.setLayerType(View.LAYER_TYPE_NONE, null);
+        }
+    }
+
+    public static void fadeIn(final View view) {
+        view.animate().cancel();
+        if (view.getVisibility() == View.INVISIBLE) {
+            view.setAlpha(0.0f);
+            view.setVisibility(View.VISIBLE);
+        }
+        view.animate()
+                .alpha(1f)
+                .setDuration(ANIMATION_DURATION_LENGTH)
+                .setInterpolator(Interpolators.ALPHA_IN)
+                .withEndAction(null);
+        if (view.hasOverlappingRendering()) {
+            view.animate().withLayer();
+        }
+    }
+
+    public static void fadeIn(View view, float fadeInAmount) {
+        fadeIn(view, fadeInAmount, true /* remap */);
+    }
+
+    /**
+     * Fade in a view by a given progress amount
+     * @param view the view to fade in
+     * @param fadeInAmount how much the view is faded in. 0 means not at all and 1 means fully
+     *                     faded in.
+     * @param remap whether the fade amount should be remapped to the shorter duration
+     * {@link #ANIMATION_DURATION_LENGTH} from the normal fade duration
+     * {@link StackStateAnimator#ANIMATION_DURATION_STANDARD} in order to have a faster fading.
+     *
+     * @see #fadeOut(View, float, boolean)
+     */
+    public static void fadeIn(View view, float fadeInAmount, boolean remap) {
+        view.animate().cancel();
+        if (view.getVisibility() == View.INVISIBLE) {
+            view.setVisibility(View.VISIBLE);
+        }
+        if (remap) {
+            fadeInAmount = mapToFadeDuration(fadeInAmount);
+        }
+        float alpha = Interpolators.ALPHA_IN.getInterpolation(fadeInAmount);
+        view.setAlpha(alpha);
+        updateLayerType(view, alpha);
+    }
+}
diff --git a/com/android/systemui/statusbar/DismissView.java b/com/android/systemui/statusbar/DismissView.java
new file mode 100644
index 0000000..d7c6443
--- /dev/null
+++ b/com/android/systemui/statusbar/DismissView.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2014 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.annotation.ColorInt;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.stack.ExpandableViewState;
+import com.android.systemui.statusbar.stack.StackScrollState;
+
+public class DismissView extends StackScrollerDecorView {
+    private final int mClearAllTopPadding;
+    private DismissViewButton mDismissButton;
+
+    public DismissView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mClearAllTopPadding = context.getResources().getDimensionPixelSize(
+                R.dimen.clear_all_padding_top);
+    }
+
+    @Override
+    protected View findContentView() {
+        return findViewById(R.id.dismiss_text);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mDismissButton = (DismissViewButton) findContentView();
+    }
+
+    public void setTextColor(@ColorInt int color) {
+        mDismissButton.setTextColor(color);
+    }
+
+    public void setOnButtonClickListener(OnClickListener listener) {
+        mContent.setOnClickListener(listener);
+    }
+
+    public boolean isOnEmptySpace(float touchX, float touchY) {
+        return touchX < mContent.getX()
+                || touchX > mContent.getX() + mContent.getWidth()
+                || touchY < mContent.getY()
+                || touchY > mContent.getY() + mContent.getHeight();
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mDismissButton.setText(R.string.clear_all_notifications_text);
+        mDismissButton.setContentDescription(
+                mContext.getString(R.string.accessibility_clear_all));
+    }
+
+    public boolean isButtonVisible() {
+        return mDismissButton.getAlpha() != 0.0f;
+    }
+
+    @Override
+    public ExpandableViewState createNewViewState(StackScrollState stackScrollState) {
+        return new DismissViewState();
+    }
+
+    public class DismissViewState extends ExpandableViewState {
+        @Override
+        public void applyToView(View view) {
+            super.applyToView(view);
+            if (view instanceof DismissView) {
+                DismissView dismissView = (DismissView) view;
+                boolean visible = this.clipTopAmount < mClearAllTopPadding;
+                dismissView.performVisibilityAnimation(visible && !dismissView.willBeGone());
+            }
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/DismissViewButton.java b/com/android/systemui/statusbar/DismissViewButton.java
new file mode 100644
index 0000000..b608d67
--- /dev/null
+++ b/com/android/systemui/statusbar/DismissViewButton.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2014 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.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+public class DismissViewButton extends AlphaOptimizedButton {
+
+    public DismissViewButton(Context context) {
+        this(context, null);
+    }
+
+    public DismissViewButton(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public DismissViewButton(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public DismissViewButton(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    /**
+     * This method returns the drawing rect for the view which is different from the regular
+     * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at
+     * position 0 and usually the translation is neglected. The standard implementation doesn't
+     * account for translation.
+     *
+     * @param outRect The (scrolled) drawing bounds of the view.
+     */
+    @Override
+    public void getDrawingRect(Rect outRect) {
+        super.getDrawingRect(outRect);
+        float translationX = ((ViewGroup) mParent).getTranslationX();
+        float translationY = ((ViewGroup) mParent).getTranslationY();
+        outRect.left += translationX;
+        outRect.right += translationX;
+        outRect.top += translationY;
+        outRect.bottom += translationY;
+    }
+}
diff --git a/com/android/systemui/statusbar/DragDownHelper.java b/com/android/systemui/statusbar/DragDownHelper.java
new file mode 100644
index 0000000..1bbf848
--- /dev/null
+++ b/com/android/systemui/statusbar/DragDownHelper.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2014 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import com.android.systemui.ExpandHelper;
+import com.android.systemui.Gefingerpoken;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.statusbar.phone.StatusBar;
+
+/**
+ * A utility class to enable the downward swipe on the lockscreen to go to the full shade and expand
+ * the notification where the drag started.
+ */
+public class DragDownHelper implements Gefingerpoken {
+
+    private static final float RUBBERBAND_FACTOR_EXPANDABLE = 0.5f;
+    private static final float RUBBERBAND_FACTOR_STATIC = 0.15f;
+
+    private static final int SPRING_BACK_ANIMATION_LENGTH_MS = 375;
+
+    private int mMinDragDistance;
+    private ExpandHelper.Callback mCallback;
+    private float mInitialTouchX;
+    private float mInitialTouchY;
+    private boolean mDraggingDown;
+    private float mTouchSlop;
+    private DragDownCallback mDragDownCallback;
+    private View mHost;
+    private final int[] mTemp2 = new int[2];
+    private boolean mDraggedFarEnough;
+    private ExpandableView mStartingChild;
+    private float mLastHeight;
+    private FalsingManager mFalsingManager;
+
+    public DragDownHelper(Context context, View host, ExpandHelper.Callback callback,
+            DragDownCallback dragDownCallback) {
+        mMinDragDistance = context.getResources().getDimensionPixelSize(
+                R.dimen.keyguard_drag_down_min_distance);
+        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+        mCallback = callback;
+        mDragDownCallback = dragDownCallback;
+        mHost = host;
+        mFalsingManager = FalsingManager.getInstance(context);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        final float x = event.getX();
+        final float y = event.getY();
+
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                mDraggedFarEnough = false;
+                mDraggingDown = false;
+                mStartingChild = null;
+                mInitialTouchY = y;
+                mInitialTouchX = x;
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                final float h = y - mInitialTouchY;
+                if (h > mTouchSlop && h > Math.abs(x - mInitialTouchX)) {
+                    mFalsingManager.onNotificatonStartDraggingDown();
+                    mDraggingDown = true;
+                    captureStartingChild(mInitialTouchX, mInitialTouchY);
+                    mInitialTouchY = y;
+                    mInitialTouchX = x;
+                    mDragDownCallback.onTouchSlopExceeded();
+                    return true;
+                }
+                break;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (!mDraggingDown) {
+            return false;
+        }
+        final float x = event.getX();
+        final float y = event.getY();
+
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_MOVE:
+                mLastHeight = y - mInitialTouchY;
+                captureStartingChild(mInitialTouchX, mInitialTouchY);
+                if (mStartingChild != null) {
+                    handleExpansion(mLastHeight, mStartingChild);
+                } else {
+                    mDragDownCallback.setEmptyDragAmount(mLastHeight);
+                }
+                if (mLastHeight > mMinDragDistance) {
+                    if (!mDraggedFarEnough) {
+                        mDraggedFarEnough = true;
+                        mDragDownCallback.onCrossedThreshold(true);
+                    }
+                } else {
+                    if (mDraggedFarEnough) {
+                        mDraggedFarEnough = false;
+                        mDragDownCallback.onCrossedThreshold(false);
+                    }
+                }
+                return true;
+            case MotionEvent.ACTION_UP:
+                if (!isFalseTouch() && mDragDownCallback.onDraggedDown(mStartingChild,
+                        (int) (y - mInitialTouchY))) {
+                    if (mStartingChild == null) {
+                        mDragDownCallback.setEmptyDragAmount(0f);
+                    } else {
+                        mCallback.setUserLockedChild(mStartingChild, false);
+                        mStartingChild = null;
+                    }
+                    mDraggingDown = false;
+                } else {
+                    stopDragging();
+                    return false;
+                }
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                stopDragging();
+                return false;
+        }
+        return false;
+    }
+
+    private boolean isFalseTouch() {
+        if (!mDragDownCallback.isFalsingCheckNeeded()) {
+            return false;
+        }
+        return mFalsingManager.isFalseTouch() || !mDraggedFarEnough;
+    }
+
+    private void captureStartingChild(float x, float y) {
+        if (mStartingChild == null) {
+            mStartingChild = findView(x, y);
+            if (mStartingChild != null) {
+                mCallback.setUserLockedChild(mStartingChild, true);
+            }
+        }
+    }
+
+    private void handleExpansion(float heightDelta, ExpandableView child) {
+        if (heightDelta < 0) {
+            heightDelta = 0;
+        }
+        boolean expandable = child.isContentExpandable();
+        float rubberbandFactor = expandable
+                ? RUBBERBAND_FACTOR_EXPANDABLE
+                : RUBBERBAND_FACTOR_STATIC;
+        float rubberband = heightDelta * rubberbandFactor;
+        if (expandable
+                && (rubberband + child.getCollapsedHeight()) > child.getMaxContentHeight()) {
+            float overshoot =
+                    (rubberband + child.getCollapsedHeight()) - child.getMaxContentHeight();
+            overshoot *= (1 - RUBBERBAND_FACTOR_STATIC);
+            rubberband -= overshoot;
+        }
+        child.setActualHeight((int) (child.getCollapsedHeight() + rubberband));
+    }
+
+    private void cancelExpansion(final ExpandableView child) {
+        if (child.getActualHeight() == child.getCollapsedHeight()) {
+            mCallback.setUserLockedChild(child, false);
+            return;
+        }
+        ObjectAnimator anim = ObjectAnimator.ofInt(child, "actualHeight",
+                child.getActualHeight(), child.getCollapsedHeight());
+        anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mCallback.setUserLockedChild(child, false);
+            }
+        });
+        anim.start();
+    }
+
+    private void cancelExpansion() {
+        ValueAnimator anim = ValueAnimator.ofFloat(mLastHeight, 0);
+        anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
+        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                mDragDownCallback.setEmptyDragAmount((Float) animation.getAnimatedValue());
+            }
+        });
+        anim.start();
+    }
+
+    private void stopDragging() {
+        mFalsingManager.onNotificatonStopDraggingDown();
+        if (mStartingChild != null) {
+            cancelExpansion(mStartingChild);
+            mStartingChild = null;
+        } else {
+            cancelExpansion();
+        }
+        mDraggingDown = false;
+        mDragDownCallback.onDragDownReset();
+    }
+
+    private ExpandableView findView(float x, float y) {
+        mHost.getLocationOnScreen(mTemp2);
+        x += mTemp2[0];
+        y += mTemp2[1];
+        return mCallback.getChildAtRawPosition(x, y);
+    }
+
+    public boolean isDraggingDown() {
+        return mDraggingDown;
+    }
+
+    public interface DragDownCallback {
+
+        /**
+         * @return true if the interaction is accepted, false if it should be cancelled
+         */
+        boolean onDraggedDown(View startingChild, int dragLengthY);
+        void onDragDownReset();
+
+        /**
+         * The user has dragged either above or below the threshold
+         * @param above whether he dragged above it
+         */
+        void onCrossedThreshold(boolean above);
+        void onTouchSlopExceeded();
+        void setEmptyDragAmount(float amount);
+        boolean isFalsingCheckNeeded();
+    }
+}
diff --git a/com/android/systemui/statusbar/EmptyShadeView.java b/com/android/systemui/statusbar/EmptyShadeView.java
new file mode 100644
index 0000000..58adde2
--- /dev/null
+++ b/com/android/systemui/statusbar/EmptyShadeView.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2014 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.annotation.ColorInt;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.stack.ExpandableViewState;
+import com.android.systemui.statusbar.stack.StackScrollState;
+
+public class EmptyShadeView extends StackScrollerDecorView {
+
+    private TextView mEmptyText;
+
+    public EmptyShadeView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mEmptyText.setText(R.string.empty_shade_text);
+    }
+
+    @Override
+    protected View findContentView() {
+        return findViewById(R.id.no_notifications);
+    }
+
+    public void setTextColor(@ColorInt int color) {
+        mEmptyText.setTextColor(color);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mEmptyText = (TextView) findContentView();
+    }
+
+    @Override
+    public ExpandableViewState createNewViewState(StackScrollState stackScrollState) {
+        return new EmptyShadeViewState();
+    }
+
+    public class EmptyShadeViewState extends ExpandableViewState {
+        @Override
+        public void applyToView(View view) {
+            super.applyToView(view);
+            if (view instanceof EmptyShadeView) {
+                EmptyShadeView emptyShadeView = (EmptyShadeView) view;
+                boolean visible = this.clipTopAmount <= mEmptyText.getPaddingTop() * 0.6f;
+                emptyShadeView.performVisibilityAnimation(
+                        visible && !emptyShadeView.willBeGone());
+            }
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/ExpandableNotificationRow.java b/com/android/systemui/statusbar/ExpandableNotificationRow.java
new file mode 100644
index 0000000..7067bc1
--- /dev/null
+++ b/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -0,0 +1,2334 @@
+/*
+ * Copyright (C) 2013 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 static com.android.systemui.statusbar.notification.NotificationInflater.InflationCallback;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Configuration;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.service.notification.StatusBarNotification;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.Property;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.NotificationHeaderView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.Chronometer;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.RemoteViews;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.util.NotificationColorUtil;
+import com.android.internal.widget.CachingIconView;
+import com.android.systemui.Dependency;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.PluginManager;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
+import com.android.systemui.statusbar.NotificationGuts.GutsContent;
+import com.android.systemui.statusbar.notification.AboveShelfChangedListener;
+import com.android.systemui.statusbar.notification.AboveShelfObserver;
+import com.android.systemui.statusbar.notification.HybridNotificationView;
+import com.android.systemui.statusbar.notification.NotificationInflater;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.stack.AnimationProperties;
+import com.android.systemui.statusbar.stack.ExpandableViewState;
+import com.android.systemui.statusbar.stack.NotificationChildrenContainer;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+import com.android.systemui.statusbar.stack.StackScrollState;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BooleanSupplier;
+
+public class ExpandableNotificationRow extends ActivatableNotificationView
+        implements PluginListener<NotificationMenuRowPlugin> {
+
+    private static final int DEFAULT_DIVIDER_ALPHA = 0x29;
+    private static final int COLORED_DIVIDER_ALPHA = 0x7B;
+    private static final int MENU_VIEW_INDEX = 0;
+
+    public interface LayoutListener {
+        public void onLayout();
+    }
+
+    private LayoutListener mLayoutListener;
+    private boolean mDark;
+    private boolean mLowPriorityStateUpdated;
+    private final NotificationInflater mNotificationInflater;
+    private int mIconTransformContentShift;
+    private int mIconTransformContentShiftNoIcon;
+    private int mNotificationMinHeightLegacy;
+    private int mMaxHeadsUpHeightLegacy;
+    private int mMaxHeadsUpHeight;
+    private int mMaxHeadsUpHeightIncreased;
+    private int mNotificationMinHeight;
+    private int mNotificationMinHeightLarge;
+    private int mNotificationMaxHeight;
+    private int mNotificationAmbientHeight;
+    private int mIncreasedPaddingBetweenElements;
+
+    /** Does this row contain layouts that can adapt to row expansion */
+    private boolean mExpandable;
+    /** Has the user actively changed the expansion state of this row */
+    private boolean mHasUserChangedExpansion;
+    /** If {@link #mHasUserChangedExpansion}, has the user expanded this row */
+    private boolean mUserExpanded;
+
+    /**
+     * Has this notification been expanded while it was pinned
+     */
+    private boolean mExpandedWhenPinned;
+    /** Is the user touching this row */
+    private boolean mUserLocked;
+    /** Are we showing the "public" version */
+    private boolean mShowingPublic;
+    private boolean mSensitive;
+    private boolean mSensitiveHiddenInGeneral;
+    private boolean mShowingPublicInitialized;
+    private boolean mHideSensitiveForIntrinsicHeight;
+
+    /**
+     * Is this notification expanded by the system. The expansion state can be overridden by the
+     * user expansion.
+     */
+    private boolean mIsSystemExpanded;
+
+    /**
+     * Whether the notification is on the keyguard and the expansion is disabled.
+     */
+    private boolean mOnKeyguard;
+
+    private Animator mTranslateAnim;
+    private ArrayList<View> mTranslateableViews;
+    private NotificationContentView mPublicLayout;
+    private NotificationContentView mPrivateLayout;
+    private NotificationContentView[] mLayouts;
+    private int mMaxExpandHeight;
+    private int mHeadsUpHeight;
+    private int mNotificationColor;
+    private ExpansionLogger mLogger;
+    private String mLoggingKey;
+    private NotificationGuts mGuts;
+    private NotificationData.Entry mEntry;
+    private StatusBarNotification mStatusBarNotification;
+    private String mAppName;
+    private boolean mIsHeadsUp;
+    private boolean mLastChronometerRunning = true;
+    private ViewStub mChildrenContainerStub;
+    private NotificationGroupManager mGroupManager;
+    private boolean mChildrenExpanded;
+    private boolean mIsSummaryWithChildren;
+    private NotificationChildrenContainer mChildrenContainer;
+    private NotificationMenuRowPlugin mMenuRow;
+    private ViewStub mGutsStub;
+    private boolean mIsSystemChildExpanded;
+    private boolean mIsPinned;
+    private FalsingManager mFalsingManager;
+    private AboveShelfChangedListener mAboveShelfChangedListener;
+    private HeadsUpManager mHeadsUpManager;
+
+    private boolean mJustClicked;
+    private boolean mIconAnimationRunning;
+    private boolean mShowNoBackground;
+    private ExpandableNotificationRow mNotificationParent;
+    private OnExpandClickListener mOnExpandClickListener;
+    private boolean mGroupExpansionChanging;
+
+    /**
+     * A supplier that returns true if keyguard is secure.
+     */
+    private BooleanSupplier mSecureStateProvider;
+
+    /**
+     * Whether or not a notification that is not part of a group of notifications can be manually
+     * expanded by the user.
+     */
+    private boolean mEnableNonGroupedNotificationExpand;
+
+    /**
+     * Whether or not to update the background of the header of the notification when its expanded.
+     * If {@code true}, the header background will disappear when expanded.
+     */
+    private boolean mShowGroupBackgroundWhenExpanded;
+
+    private OnClickListener mExpandClickListener = new OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            if (!mShowingPublic && (!mIsLowPriority || isExpanded())
+                    && mGroupManager.isSummaryOfGroup(mStatusBarNotification)) {
+                mGroupExpansionChanging = true;
+                final boolean wasExpanded = mGroupManager.isGroupExpanded(mStatusBarNotification);
+                boolean nowExpanded = mGroupManager.toggleGroupExpansion(mStatusBarNotification);
+                mOnExpandClickListener.onExpandClicked(mEntry, nowExpanded);
+                MetricsLogger.action(mContext, MetricsEvent.ACTION_NOTIFICATION_GROUP_EXPANDER,
+                        nowExpanded);
+                onExpansionChanged(true /* userAction */, wasExpanded);
+            } else if (mEnableNonGroupedNotificationExpand) {
+                if (v.isAccessibilityFocused()) {
+                    mPrivateLayout.setFocusOnVisibilityChange();
+                }
+                boolean nowExpanded;
+                if (isPinned()) {
+                    nowExpanded = !mExpandedWhenPinned;
+                    mExpandedWhenPinned = nowExpanded;
+                } else {
+                    nowExpanded = !isExpanded();
+                    setUserExpanded(nowExpanded);
+                }
+                notifyHeightChanged(true);
+                mOnExpandClickListener.onExpandClicked(mEntry, nowExpanded);
+                MetricsLogger.action(mContext, MetricsEvent.ACTION_NOTIFICATION_EXPANDER,
+                        nowExpanded);
+            }
+        }
+    };
+    private boolean mForceUnlocked;
+    private boolean mDismissed;
+    private boolean mKeepInParent;
+    private boolean mRemoved;
+    private static final Property<ExpandableNotificationRow, Float> TRANSLATE_CONTENT =
+            new FloatProperty<ExpandableNotificationRow>("translate") {
+                @Override
+                public void setValue(ExpandableNotificationRow object, float value) {
+                    object.setTranslation(value);
+                }
+
+                @Override
+                public Float get(ExpandableNotificationRow object) {
+                    return object.getTranslation();
+                }
+    };
+    private OnClickListener mOnClickListener;
+    private boolean mHeadsupDisappearRunning;
+    private View mChildAfterViewWhenDismissed;
+    private View mGroupParentWhenDismissed;
+    private boolean mRefocusOnDismiss;
+    private float mContentTransformationAmount;
+    private boolean mIconsVisible = true;
+    private boolean mAboveShelf;
+    private boolean mShowAmbient;
+    private boolean mIsLastChild;
+    private Runnable mOnDismissRunnable;
+    private boolean mIsLowPriority;
+    private boolean mIsColorized;
+    private boolean mUseIncreasedCollapsedHeight;
+    private boolean mUseIncreasedHeadsUpHeight;
+    private float mTranslationWhenRemoved;
+    private boolean mWasChildInGroupWhenRemoved;
+    private int mNotificationColorAmbient;
+
+    @Override
+    public boolean isGroupExpansionChanging() {
+        if (isChildInGroup()) {
+            return mNotificationParent.isGroupExpansionChanging();
+        }
+        return mGroupExpansionChanging;
+    }
+
+    public void setGroupExpansionChanging(boolean changing) {
+        mGroupExpansionChanging = changing;
+    }
+
+    @Override
+    public void setActualHeightAnimating(boolean animating) {
+        if (mPrivateLayout != null) {
+            mPrivateLayout.setContentHeightAnimating(animating);
+        }
+    }
+
+    public NotificationContentView getPrivateLayout() {
+        return mPrivateLayout;
+    }
+
+    public NotificationContentView getPublicLayout() {
+        return mPublicLayout;
+    }
+
+    public void setIconAnimationRunning(boolean running) {
+        for (NotificationContentView l : mLayouts) {
+            setIconAnimationRunning(running, l);
+        }
+        if (mIsSummaryWithChildren) {
+            setIconAnimationRunningForChild(running, mChildrenContainer.getHeaderView());
+            setIconAnimationRunningForChild(running, mChildrenContainer.getLowPriorityHeaderView());
+            List<ExpandableNotificationRow> notificationChildren =
+                    mChildrenContainer.getNotificationChildren();
+            for (int i = 0; i < notificationChildren.size(); i++) {
+                ExpandableNotificationRow child = notificationChildren.get(i);
+                child.setIconAnimationRunning(running);
+            }
+        }
+        mIconAnimationRunning = running;
+    }
+
+    private void setIconAnimationRunning(boolean running, NotificationContentView layout) {
+        if (layout != null) {
+            View contractedChild = layout.getContractedChild();
+            View expandedChild = layout.getExpandedChild();
+            View headsUpChild = layout.getHeadsUpChild();
+            setIconAnimationRunningForChild(running, contractedChild);
+            setIconAnimationRunningForChild(running, expandedChild);
+            setIconAnimationRunningForChild(running, headsUpChild);
+        }
+    }
+
+    private void setIconAnimationRunningForChild(boolean running, View child) {
+        if (child != null) {
+            ImageView icon = (ImageView) child.findViewById(com.android.internal.R.id.icon);
+            setIconRunning(icon, running);
+            ImageView rightIcon = (ImageView) child.findViewById(
+                    com.android.internal.R.id.right_icon);
+            setIconRunning(rightIcon, running);
+        }
+    }
+
+    private void setIconRunning(ImageView imageView, boolean running) {
+        if (imageView != null) {
+            Drawable drawable = imageView.getDrawable();
+            if (drawable instanceof AnimationDrawable) {
+                AnimationDrawable animationDrawable = (AnimationDrawable) drawable;
+                if (running) {
+                    animationDrawable.start();
+                } else {
+                    animationDrawable.stop();
+                }
+            } else if (drawable instanceof AnimatedVectorDrawable) {
+                AnimatedVectorDrawable animationDrawable = (AnimatedVectorDrawable) drawable;
+                if (running) {
+                    animationDrawable.start();
+                } else {
+                    animationDrawable.stop();
+                }
+            }
+        }
+    }
+
+    public void updateNotification(NotificationData.Entry entry) {
+        mEntry = entry;
+        mStatusBarNotification = entry.notification;
+        mNotificationInflater.inflateNotificationViews();
+    }
+
+    public void onNotificationUpdated() {
+        for (NotificationContentView l : mLayouts) {
+            l.onNotificationUpdated(mEntry);
+        }
+        mIsColorized = mStatusBarNotification.getNotification().isColorized();
+        mShowingPublicInitialized = false;
+        updateNotificationColor();
+        if (mMenuRow != null) {
+            mMenuRow.onNotificationUpdated(mStatusBarNotification);
+        }
+        if (mIsSummaryWithChildren) {
+            mChildrenContainer.recreateNotificationHeader(mExpandClickListener);
+            mChildrenContainer.onNotificationUpdated();
+        }
+        if (mIconAnimationRunning) {
+            setIconAnimationRunning(true);
+        }
+        if (mNotificationParent != null) {
+            mNotificationParent.updateChildrenHeaderAppearance();
+        }
+        onChildrenCountChanged();
+        // The public layouts expand button is always visible
+        mPublicLayout.updateExpandButtons(true);
+        updateLimits();
+        updateIconVisibilities();
+        updateShelfIconColor();
+    }
+
+    @VisibleForTesting
+    void updateShelfIconColor() {
+        StatusBarIconView expandedIcon = mEntry.expandedIcon;
+        boolean isPreL = Boolean.TRUE.equals(expandedIcon.getTag(R.id.icon_is_pre_L));
+        boolean colorize = !isPreL || NotificationUtils.isGrayscale(expandedIcon,
+                NotificationColorUtil.getInstance(mContext));
+        int color = StatusBarIconView.NO_COLOR;
+        if (colorize) {
+            NotificationHeaderView header = getVisibleNotificationHeader();
+            if (header != null) {
+                color = header.getOriginalIconColor();
+            } else {
+                color = mEntry.getContrastedColor(mContext, mIsLowPriority && !isExpanded(),
+                        getBackgroundColorWithoutTint());
+            }
+        }
+        expandedIcon.setStaticDrawableColor(color);
+    }
+
+    public void setAboveShelfChangedListener(AboveShelfChangedListener aboveShelfChangedListener) {
+        mAboveShelfChangedListener = aboveShelfChangedListener;
+    }
+
+    /**
+     * Sets a supplier that can determine whether the keyguard is secure or not.
+     * @param secureStateProvider A function that returns true if keyguard is secure.
+     */
+    public void setSecureStateProvider(BooleanSupplier secureStateProvider) {
+        mSecureStateProvider = secureStateProvider;
+    }
+
+    @Override
+    public boolean isDimmable() {
+        if (!getShowingLayout().isDimmable()) {
+            return false;
+        }
+        return super.isDimmable();
+    }
+
+    private void updateLimits() {
+        for (NotificationContentView l : mLayouts) {
+            updateLimitsForView(l);
+        }
+    }
+
+    private void updateLimitsForView(NotificationContentView layout) {
+        boolean customView = layout.getContractedChild().getId()
+                != com.android.internal.R.id.status_bar_latest_event_content;
+        boolean beforeN = mEntry.targetSdk < Build.VERSION_CODES.N;
+        int minHeight;
+        if (customView && beforeN && !mIsSummaryWithChildren) {
+            minHeight = mNotificationMinHeightLegacy;
+        } else if (mUseIncreasedCollapsedHeight && layout == mPrivateLayout) {
+            minHeight = mNotificationMinHeightLarge;
+        } else {
+            minHeight = mNotificationMinHeight;
+        }
+        boolean headsUpCustom = layout.getHeadsUpChild() != null &&
+                layout.getHeadsUpChild().getId()
+                        != com.android.internal.R.id.status_bar_latest_event_content;
+        int headsUpheight;
+        if (headsUpCustom && beforeN) {
+            headsUpheight = mMaxHeadsUpHeightLegacy;
+        } else if (mUseIncreasedHeadsUpHeight && layout == mPrivateLayout) {
+            headsUpheight = mMaxHeadsUpHeightIncreased;
+        } else {
+            headsUpheight = mMaxHeadsUpHeight;
+        }
+        layout.setHeights(minHeight, headsUpheight, mNotificationMaxHeight,
+                mNotificationAmbientHeight);
+    }
+
+    public StatusBarNotification getStatusBarNotification() {
+        return mStatusBarNotification;
+    }
+
+    public NotificationData.Entry getEntry() {
+        return mEntry;
+    }
+
+    public boolean isHeadsUp() {
+        return mIsHeadsUp;
+    }
+
+    public void setHeadsUp(boolean isHeadsUp) {
+        boolean wasAboveShelf = isAboveShelf();
+        int intrinsicBefore = getIntrinsicHeight();
+        mIsHeadsUp = isHeadsUp;
+        mPrivateLayout.setHeadsUp(isHeadsUp);
+        if (mIsSummaryWithChildren) {
+            // The overflow might change since we allow more lines as HUN.
+            mChildrenContainer.updateGroupOverflow();
+        }
+        if (intrinsicBefore != getIntrinsicHeight()) {
+            notifyHeightChanged(false  /* needsAnimation */);
+        }
+        if (isHeadsUp) {
+            setAboveShelf(true);
+        } else if (isAboveShelf() != wasAboveShelf) {
+            mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
+        }
+    }
+
+    public void setGroupManager(NotificationGroupManager groupManager) {
+        mGroupManager = groupManager;
+        mPrivateLayout.setGroupManager(groupManager);
+    }
+
+    public void setRemoteInputController(RemoteInputController r) {
+        mPrivateLayout.setRemoteInputController(r);
+    }
+
+    public void setAppName(String appName) {
+        mAppName = appName;
+        if (mMenuRow != null && mMenuRow.getMenuView() != null) {
+            mMenuRow.setAppName(mAppName);
+        }
+    }
+
+    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);
+        onChildrenCountChanged();
+        row.setIsChildInGroup(true, this);
+    }
+
+    public void removeChildNotification(ExpandableNotificationRow row) {
+        if (mChildrenContainer != null) {
+            mChildrenContainer.removeNotification(row);
+        }
+        onChildrenCountChanged();
+        row.setIsChildInGroup(false, null);
+    }
+
+    @Override
+    public boolean isChildInGroup() {
+        return mNotificationParent != null;
+    }
+
+    public ExpandableNotificationRow getNotificationParent() {
+        return mNotificationParent;
+    }
+
+    /**
+     * @param isChildInGroup Is this notification now in a group
+     * @param parent the new parent notification
+     */
+    public void setIsChildInGroup(boolean isChildInGroup, ExpandableNotificationRow parent) {;
+        boolean childInGroup = StatusBar.ENABLE_CHILD_NOTIFICATIONS && isChildInGroup;
+        mNotificationParent = childInGroup ? parent : null;
+        mPrivateLayout.setIsChildInGroup(childInGroup);
+        mNotificationInflater.setIsChildInGroup(childInGroup);
+        resetBackgroundAlpha();
+        updateBackgroundForGroupState();
+        updateClickAndFocus();
+        if (mNotificationParent != null) {
+            setOverrideTintColor(NO_COLOR, 0.0f);
+            mNotificationParent.updateBackgroundForGroupState();
+        }
+        updateIconVisibilities();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (event.getActionMasked() != MotionEvent.ACTION_DOWN
+                || !isChildInGroup() || isGroupExpanded()) {
+            return super.onTouchEvent(event);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    protected boolean handleSlideBack() {
+        if (mMenuRow != null && mMenuRow.isMenuVisible()) {
+            animateTranslateNotification(0 /* targetLeft */);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    protected boolean shouldHideBackground() {
+        return super.shouldHideBackground() || mShowNoBackground;
+    }
+
+    @Override
+    public boolean isSummaryWithChildren() {
+        return mIsSummaryWithChildren;
+    }
+
+    @Override
+    public boolean areChildrenExpanded() {
+        return mChildrenExpanded;
+    }
+
+    public List<ExpandableNotificationRow> getNotificationChildren() {
+        return mChildrenContainer == null ? null : mChildrenContainer.getNotificationChildren();
+    }
+
+    public int getNumberOfNotificationChildren() {
+        if (mChildrenContainer == null) {
+            return 0;
+        }
+        return mChildrenContainer.getNotificationChildren().size();
+    }
+
+    /**
+     * Apply the order given in the list to the children.
+     *
+     * @param childOrder the new list order
+     * @param visualStabilityManager
+     * @param callback the callback to invoked in case it is not allowed
+     * @return whether the list order has changed
+     */
+    public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder,
+            VisualStabilityManager visualStabilityManager,
+            VisualStabilityManager.Callback callback) {
+        return mChildrenContainer != null && mChildrenContainer.applyChildOrder(childOrder,
+                visualStabilityManager, callback);
+    }
+
+    public void getChildrenStates(StackScrollState resultState) {
+        if (mIsSummaryWithChildren) {
+            ExpandableViewState parentState = resultState.getViewStateForView(this);
+            mChildrenContainer.getState(resultState, parentState);
+        }
+    }
+
+    public void applyChildrenState(StackScrollState state) {
+        if (mIsSummaryWithChildren) {
+            mChildrenContainer.applyState(state);
+        }
+    }
+
+    public void prepareExpansionChanged(StackScrollState state) {
+        if (mIsSummaryWithChildren) {
+            mChildrenContainer.prepareExpansionChanged(state);
+        }
+    }
+
+    public void startChildAnimation(StackScrollState finalState, AnimationProperties properties) {
+        if (mIsSummaryWithChildren) {
+            mChildrenContainer.startAnimationToState(finalState, properties);
+        }
+    }
+
+    public ExpandableNotificationRow getViewAtPosition(float y) {
+        if (!mIsSummaryWithChildren || !mChildrenExpanded) {
+            return this;
+        } else {
+            ExpandableNotificationRow view = mChildrenContainer.getViewAtPosition(y);
+            return view == null ? this : view;
+        }
+    }
+
+    public NotificationGuts getGuts() {
+        return mGuts;
+    }
+
+    /**
+     * Set this notification to be pinned to the top if {@link #isHeadsUp()} is true. By doing this
+     * the notification will be rendered on top of the screen.
+     *
+     * @param pinned whether it is pinned
+     */
+    public void setPinned(boolean pinned) {
+        int intrinsicHeight = getIntrinsicHeight();
+        boolean wasAboveShelf = isAboveShelf();
+        mIsPinned = pinned;
+        if (intrinsicHeight != getIntrinsicHeight()) {
+            notifyHeightChanged(false /* needsAnimation */);
+        }
+        if (pinned) {
+            setIconAnimationRunning(true);
+            mExpandedWhenPinned = false;
+        } else if (mExpandedWhenPinned) {
+            setUserExpanded(true);
+        }
+        setChronometerRunning(mLastChronometerRunning);
+        if (isAboveShelf() != wasAboveShelf) {
+            mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
+        }
+    }
+
+    public boolean isPinned() {
+        return mIsPinned;
+    }
+
+    @Override
+    public int getPinnedHeadsUpHeight() {
+        return getPinnedHeadsUpHeight(true /* atLeastMinHeight */);
+    }
+
+    /**
+     * @param atLeastMinHeight should the value returned be at least the minimum height.
+     *                         Used to avoid cyclic calls
+     * @return the height of the heads up notification when pinned
+     */
+    private int getPinnedHeadsUpHeight(boolean atLeastMinHeight) {
+        if (mIsSummaryWithChildren) {
+            return mChildrenContainer.getIntrinsicHeight();
+        }
+        if(mExpandedWhenPinned) {
+            return Math.max(getMaxExpandHeight(), mHeadsUpHeight);
+        } else if (atLeastMinHeight) {
+            return Math.max(getCollapsedHeight(), mHeadsUpHeight);
+        } else {
+            return mHeadsUpHeight;
+        }
+    }
+
+    /**
+     * Mark whether this notification was just clicked, i.e. the user has just clicked this
+     * notification in this frame.
+     */
+    public void setJustClicked(boolean justClicked) {
+        mJustClicked = justClicked;
+    }
+
+    /**
+     * @return true if this notification has been clicked in this frame, false otherwise
+     */
+    public boolean wasJustClicked() {
+        return mJustClicked;
+    }
+
+    public void setChronometerRunning(boolean running) {
+        mLastChronometerRunning = running;
+        setChronometerRunning(running, mPrivateLayout);
+        setChronometerRunning(running, mPublicLayout);
+        if (mChildrenContainer != null) {
+            List<ExpandableNotificationRow> notificationChildren =
+                    mChildrenContainer.getNotificationChildren();
+            for (int i = 0; i < notificationChildren.size(); i++) {
+                ExpandableNotificationRow child = notificationChildren.get(i);
+                child.setChronometerRunning(running);
+            }
+        }
+    }
+
+    private void setChronometerRunning(boolean running, NotificationContentView layout) {
+        if (layout != null) {
+            running = running || isPinned();
+            View contractedChild = layout.getContractedChild();
+            View expandedChild = layout.getExpandedChild();
+            View headsUpChild = layout.getHeadsUpChild();
+            setChronometerRunningForChild(running, contractedChild);
+            setChronometerRunningForChild(running, expandedChild);
+            setChronometerRunningForChild(running, headsUpChild);
+        }
+    }
+
+    private void setChronometerRunningForChild(boolean running, View child) {
+        if (child != null) {
+            View chronometer = child.findViewById(com.android.internal.R.id.chronometer);
+            if (chronometer instanceof Chronometer) {
+                ((Chronometer) chronometer).setStarted(running);
+            }
+        }
+    }
+
+    public NotificationHeaderView getNotificationHeader() {
+        if (mIsSummaryWithChildren) {
+            return mChildrenContainer.getHeaderView();
+        }
+        return mPrivateLayout.getNotificationHeader();
+    }
+
+    /**
+     * @return the currently visible notification header. This can be different from
+     * {@link #getNotificationHeader()} in case it is a low-priority group.
+     */
+    public NotificationHeaderView getVisibleNotificationHeader() {
+        if (mIsSummaryWithChildren && !mShowingPublic) {
+            return mChildrenContainer.getVisibleHeader();
+        }
+        return getShowingLayout().getVisibleNotificationHeader();
+    }
+
+
+    /**
+     * @return the contracted notification header. This can be different from
+     * {@link #getNotificationHeader()} and also {@link #getVisibleNotificationHeader()} and only
+     * returns the contracted version.
+     */
+    public NotificationHeaderView getContractedNotificationHeader() {
+        if (mIsSummaryWithChildren) {
+            return mChildrenContainer.getHeaderView();
+        }
+        return mPrivateLayout.getContractedNotificationHeader();
+    }
+
+    public void setOnExpandClickListener(OnExpandClickListener onExpandClickListener) {
+        mOnExpandClickListener = onExpandClickListener;
+    }
+
+    @Override
+    public void setOnClickListener(@Nullable OnClickListener l) {
+        super.setOnClickListener(l);
+        mOnClickListener = l;
+        updateClickAndFocus();
+    }
+
+    private void updateClickAndFocus() {
+        boolean normalChild = !isChildInGroup() || isGroupExpanded();
+        boolean clickable = mOnClickListener != null && normalChild;
+        if (isFocusable() != normalChild) {
+            setFocusable(normalChild);
+        }
+        if (isClickable() != clickable) {
+            setClickable(clickable);
+        }
+    }
+
+    public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+        mHeadsUpManager = headsUpManager;
+    }
+
+    public void setGutsView(MenuItem item) {
+        if (mGuts != null && item.getGutsView() instanceof GutsContent) {
+            ((GutsContent) item.getGutsView()).setGutsParent(mGuts);
+            mGuts.setGutsContent((GutsContent) item.getGutsView());
+        }
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        Dependency.get(PluginManager.class).addPluginListener(this,
+                NotificationMenuRowPlugin.class, false /* Allow multiple */);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        Dependency.get(PluginManager.class).removePluginListener(this);
+    }
+
+    @Override
+    public void onPluginConnected(NotificationMenuRowPlugin plugin, Context pluginContext) {
+        boolean existed = mMenuRow.getMenuView() != null;
+        if (existed) {
+            removeView(mMenuRow.getMenuView());
+        }
+        mMenuRow = plugin;
+        if (mMenuRow.useDefaultMenuItems()) {
+            ArrayList<MenuItem> items = new ArrayList<>();
+            items.add(NotificationMenuRow.createInfoItem(mContext));
+            items.add(NotificationMenuRow.createSnoozeItem(mContext));
+            mMenuRow.setMenuItems(items);
+        }
+        if (existed) {
+            createMenu();
+        }
+    }
+
+    @Override
+    public void onPluginDisconnected(NotificationMenuRowPlugin plugin) {
+        boolean existed = mMenuRow.getMenuView() != null;
+        mMenuRow = new NotificationMenuRow(mContext); // Back to default
+        if (existed) {
+            createMenu();
+        }
+    }
+
+    public NotificationMenuRowPlugin createMenu() {
+        if (mMenuRow.getMenuView() == null) {
+            mMenuRow.createMenu(this, mStatusBarNotification);
+            mMenuRow.setAppName(mAppName);
+            FrameLayout.LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
+                    LayoutParams.MATCH_PARENT);
+            addView(mMenuRow.getMenuView(), MENU_VIEW_INDEX, lp);
+        }
+        return mMenuRow;
+    }
+
+    public NotificationMenuRowPlugin getProvider() {
+        return mMenuRow;
+    }
+
+    @Override
+    public void onDensityOrFontScaleChanged() {
+        super.onDensityOrFontScaleChanged();
+        initDimens();
+        initBackground();
+        // Let's update our childrencontainer. This is intentionally not guarded with
+        // mIsSummaryWithChildren since we might have had children but not anymore.
+        if (mChildrenContainer != null) {
+            mChildrenContainer.reInflateViews(mExpandClickListener, mEntry.notification);
+        }
+        if (mGuts != null) {
+            View oldGuts = mGuts;
+            int index = indexOfChild(oldGuts);
+            removeView(oldGuts);
+            mGuts = (NotificationGuts) LayoutInflater.from(mContext).inflate(
+                    R.layout.notification_guts, this, false);
+            mGuts.setVisibility(oldGuts.getVisibility());
+            addView(mGuts, index);
+        }
+        View oldMenu = mMenuRow.getMenuView();
+        if (oldMenu != null) {
+            int menuIndex = indexOfChild(oldMenu);
+            removeView(oldMenu);
+            mMenuRow.createMenu(ExpandableNotificationRow.this, mStatusBarNotification);
+            mMenuRow.setAppName(mAppName);
+            addView(mMenuRow.getMenuView(), menuIndex);
+        }
+        for (NotificationContentView l : mLayouts) {
+            l.reInflateViews();
+        }
+        mNotificationInflater.onDensityOrFontScaleChanged();
+        onNotificationUpdated();
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        if (mMenuRow.getMenuView() != null) {
+            mMenuRow.onConfigurationChanged();
+        }
+    }
+
+    public void setContentBackground(int customBackgroundColor, boolean animate,
+            NotificationContentView notificationContentView) {
+        if (getShowingLayout() == notificationContentView) {
+            setTintColor(customBackgroundColor, animate);
+        }
+    }
+
+    public void closeRemoteInput() {
+        for (NotificationContentView l : mLayouts) {
+            l.closeRemoteInput();
+        }
+    }
+
+    /**
+     * Set by how much the single line view should be indented.
+     */
+    public void setSingleLineWidthIndention(int indention) {
+        mPrivateLayout.setSingleLineWidthIndention(indention);
+    }
+
+    public int getNotificationColor() {
+        return mNotificationColor;
+    }
+
+    private void updateNotificationColor() {
+        mNotificationColor = NotificationColorUtil.resolveContrastColor(mContext,
+                getStatusBarNotification().getNotification().color,
+                getBackgroundColorWithoutTint());
+        mNotificationColorAmbient = NotificationColorUtil.resolveAmbientColor(mContext,
+                getStatusBarNotification().getNotification().color);
+    }
+
+    public HybridNotificationView getSingleLineView() {
+        return mPrivateLayout.getSingleLineView();
+    }
+
+    public HybridNotificationView getAmbientSingleLineView() {
+        return getShowingLayout().getAmbientSingleLineChild();
+    }
+
+    public boolean isOnKeyguard() {
+        return mOnKeyguard;
+    }
+
+    public void removeAllChildren() {
+        List<ExpandableNotificationRow> notificationChildren
+                = mChildrenContainer.getNotificationChildren();
+        ArrayList<ExpandableNotificationRow> clonedList = new ArrayList<>(notificationChildren);
+        for (int i = 0; i < clonedList.size(); i++) {
+            ExpandableNotificationRow row = clonedList.get(i);
+            if (row.keepInParent()) {
+                continue;
+            }
+            mChildrenContainer.removeNotification(row);
+            row.setIsChildInGroup(false, null);
+        }
+        onChildrenCountChanged();
+    }
+
+    public void setForceUnlocked(boolean forceUnlocked) {
+        mForceUnlocked = forceUnlocked;
+        if (mIsSummaryWithChildren) {
+            List<ExpandableNotificationRow> notificationChildren = getNotificationChildren();
+            for (ExpandableNotificationRow child : notificationChildren) {
+                child.setForceUnlocked(forceUnlocked);
+            }
+        }
+    }
+
+    public void setDismissed(boolean dismissed, boolean fromAccessibility) {
+        mDismissed = dismissed;
+        mGroupParentWhenDismissed = mNotificationParent;
+        mRefocusOnDismiss = fromAccessibility;
+        mChildAfterViewWhenDismissed = null;
+        if (isChildInGroup()) {
+            List<ExpandableNotificationRow> notificationChildren =
+                    mNotificationParent.getNotificationChildren();
+            int i = notificationChildren.indexOf(this);
+            if (i != -1 && i < notificationChildren.size() - 1) {
+                mChildAfterViewWhenDismissed = notificationChildren.get(i + 1);
+            }
+        }
+    }
+
+    public boolean isDismissed() {
+        return mDismissed;
+    }
+
+    public boolean keepInParent() {
+        return mKeepInParent;
+    }
+
+    public void setKeepInParent(boolean keepInParent) {
+        mKeepInParent = keepInParent;
+    }
+
+    public boolean isRemoved() {
+        return mRemoved;
+    }
+
+    public void setRemoved() {
+        mRemoved = true;
+        mTranslationWhenRemoved = getTranslationY();
+        mWasChildInGroupWhenRemoved = isChildInGroup();
+        if (isChildInGroup()) {
+            mTranslationWhenRemoved += getNotificationParent().getTranslationY();
+        }
+        mPrivateLayout.setRemoved();
+    }
+
+    public boolean wasChildInGroupWhenRemoved() {
+        return mWasChildInGroupWhenRemoved;
+    }
+
+    public float getTranslationWhenRemoved() {
+        return mTranslationWhenRemoved;
+    }
+
+    public NotificationChildrenContainer getChildrenContainer() {
+        return mChildrenContainer;
+    }
+
+    public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {
+        boolean wasAboveShelf = isAboveShelf();
+        mHeadsupDisappearRunning = headsUpAnimatingAway;
+        mPrivateLayout.setHeadsUpAnimatingAway(headsUpAnimatingAway);
+        if (isAboveShelf() != wasAboveShelf) {
+            mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
+        }
+    }
+
+    /**
+     * @return if the view was just heads upped and is now animating away. During such a time the
+     * layout needs to be kept consistent
+     */
+    public boolean isHeadsUpAnimatingAway() {
+        return mHeadsupDisappearRunning;
+    }
+
+    public View getChildAfterViewWhenDismissed() {
+        return mChildAfterViewWhenDismissed;
+    }
+
+    public View getGroupParentWhenDismissed() {
+        return mGroupParentWhenDismissed;
+    }
+
+    public void performDismiss() {
+        if (mOnDismissRunnable != null) {
+            mOnDismissRunnable.run();
+        }
+    }
+
+    public void setOnDismissRunnable(Runnable onDismissRunnable) {
+        mOnDismissRunnable = onDismissRunnable;
+    }
+
+    public View getNotificationIcon() {
+        NotificationHeaderView notificationHeader = getVisibleNotificationHeader();
+        if (notificationHeader != null) {
+            return notificationHeader.getIcon();
+        }
+        return null;
+    }
+
+    /**
+     * @return whether the notification is currently showing a view with an icon.
+     */
+    public boolean isShowingIcon() {
+        if (areGutsExposed()) {
+            return false;
+        }
+        return getVisibleNotificationHeader() != null;
+    }
+
+    /**
+     * Set how much this notification is transformed into an icon.
+     *
+     * @param contentTransformationAmount A value from 0 to 1 indicating how much we are transformed
+     *                                 to the content away
+     * @param isLastChild is this the last child in the list. If true, then the transformation is
+     *                    different since it's content fades out.
+     */
+    public void setContentTransformationAmount(float contentTransformationAmount,
+            boolean isLastChild) {
+        boolean changeTransformation = isLastChild != mIsLastChild;
+        changeTransformation |= mContentTransformationAmount != contentTransformationAmount;
+        mIsLastChild = isLastChild;
+        mContentTransformationAmount = contentTransformationAmount;
+        if (changeTransformation) {
+            updateContentTransformation();
+        }
+    }
+
+    /**
+     * Set the icons to be visible of this notification.
+     */
+    public void setIconsVisible(boolean iconsVisible) {
+        if (iconsVisible != mIconsVisible) {
+            mIconsVisible = iconsVisible;
+            updateIconVisibilities();
+        }
+    }
+
+    @Override
+    protected void onBelowSpeedBumpChanged() {
+        updateIconVisibilities();
+    }
+
+    private void updateContentTransformation() {
+        float contentAlpha;
+        float translationY = -mContentTransformationAmount * mIconTransformContentShift;
+        if (mIsLastChild) {
+            contentAlpha = 1.0f - mContentTransformationAmount;
+            contentAlpha = Math.min(contentAlpha / 0.5f, 1.0f);
+            contentAlpha = Interpolators.ALPHA_OUT.getInterpolation(contentAlpha);
+            translationY *= 0.4f;
+        } else {
+            contentAlpha = 1.0f;
+        }
+        for (NotificationContentView l : mLayouts) {
+            l.setAlpha(contentAlpha);
+            l.setTranslationY(translationY);
+        }
+        if (mChildrenContainer != null) {
+            mChildrenContainer.setAlpha(contentAlpha);
+            mChildrenContainer.setTranslationY(translationY);
+            // TODO: handle children fade out better
+        }
+    }
+
+    private void updateIconVisibilities() {
+        boolean visible = isChildInGroup()
+                || (isBelowSpeedBump() && !NotificationShelf.SHOW_AMBIENT_ICONS)
+                || mIconsVisible;
+        for (NotificationContentView l : mLayouts) {
+            l.setIconsVisible(visible);
+        }
+        if (mChildrenContainer != null) {
+            mChildrenContainer.setIconsVisible(visible);
+        }
+    }
+
+    /**
+     * Get the relative top padding of a view relative to this view. This recursively walks up the
+     * hierarchy and does the corresponding measuring.
+     *
+     * @param view the view to the the padding for. The requested view has to be a child of this
+     *             notification.
+     * @return the toppadding
+     */
+    public int getRelativeTopPadding(View view) {
+        int topPadding = 0;
+        while (view.getParent() instanceof ViewGroup) {
+            topPadding += view.getTop();
+            view = (View) view.getParent();
+            if (view instanceof ExpandableNotificationRow) {
+                return topPadding;
+            }
+        }
+        return topPadding;
+    }
+
+    public float getContentTranslation() {
+        return mPrivateLayout.getTranslationY();
+    }
+
+    public void setIsLowPriority(boolean isLowPriority) {
+        mIsLowPriority = isLowPriority;
+        mPrivateLayout.setIsLowPriority(isLowPriority);
+        mNotificationInflater.setIsLowPriority(mIsLowPriority);
+        if (mChildrenContainer != null) {
+            mChildrenContainer.setIsLowPriority(isLowPriority);
+        }
+    }
+
+
+    public void setLowPriorityStateUpdated(boolean lowPriorityStateUpdated) {
+        mLowPriorityStateUpdated = lowPriorityStateUpdated;
+    }
+
+    public boolean hasLowPriorityStateUpdated() {
+        return mLowPriorityStateUpdated;
+    }
+
+    public boolean isLowPriority() {
+        return mIsLowPriority;
+    }
+
+    public void setUseIncreasedCollapsedHeight(boolean use) {
+        mUseIncreasedCollapsedHeight = use;
+        mNotificationInflater.setUsesIncreasedHeight(use);
+    }
+
+    public void setUseIncreasedHeadsUpHeight(boolean use) {
+        mUseIncreasedHeadsUpHeight = use;
+        mNotificationInflater.setUsesIncreasedHeadsUpHeight(use);
+    }
+
+    public void setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler) {
+        mNotificationInflater.setRemoteViewClickHandler(remoteViewClickHandler);
+    }
+
+    public void setInflationCallback(InflationCallback callback) {
+        mNotificationInflater.setInflationCallback(callback);
+    }
+
+    public void setNeedsRedaction(boolean needsRedaction) {
+        mNotificationInflater.setRedactAmbient(needsRedaction);
+    }
+
+    @VisibleForTesting
+    public NotificationInflater getNotificationInflater() {
+        return mNotificationInflater;
+    }
+
+    public int getNotificationColorAmbient() {
+        return mNotificationColorAmbient;
+    }
+
+    public interface ExpansionLogger {
+        void logNotificationExpansion(String key, boolean userAction, boolean expanded);
+    }
+
+    public ExpandableNotificationRow(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mFalsingManager = FalsingManager.getInstance(context);
+        mNotificationInflater = new NotificationInflater(this);
+        mMenuRow = new NotificationMenuRow(mContext);
+        initDimens();
+    }
+
+    private void initDimens() {
+        mNotificationMinHeightLegacy = getFontScaledHeight(R.dimen.notification_min_height_legacy);
+        mNotificationMinHeight = getFontScaledHeight(R.dimen.notification_min_height);
+        mNotificationMinHeightLarge = getFontScaledHeight(
+                R.dimen.notification_min_height_increased);
+        mNotificationMaxHeight = getFontScaledHeight(R.dimen.notification_max_height);
+        mNotificationAmbientHeight = getFontScaledHeight(R.dimen.notification_ambient_height);
+        mMaxHeadsUpHeightLegacy = getFontScaledHeight(
+                R.dimen.notification_max_heads_up_height_legacy);
+        mMaxHeadsUpHeight = getFontScaledHeight(R.dimen.notification_max_heads_up_height);
+        mMaxHeadsUpHeightIncreased = getFontScaledHeight(
+                R.dimen.notification_max_heads_up_height_increased);
+
+        Resources res = getResources();
+        mIncreasedPaddingBetweenElements = res.getDimensionPixelSize(
+                R.dimen.notification_divider_height_increased);
+        mIconTransformContentShiftNoIcon = res.getDimensionPixelSize(
+                R.dimen.notification_icon_transform_content_shift);
+        mEnableNonGroupedNotificationExpand =
+                res.getBoolean(R.bool.config_enableNonGroupedNotificationExpand);
+        mShowGroupBackgroundWhenExpanded =
+                res.getBoolean(R.bool.config_showGroupNotificationBgWhenExpanded);
+    }
+
+    /**
+     * @param dimenId the dimen to look up
+     * @return the font scaled dimen as if it were in sp but doesn't shrink sizes below dp
+     */
+    private int getFontScaledHeight(int dimenId) {
+        int dimensionPixelSize = getResources().getDimensionPixelSize(dimenId);
+        float factor = Math.max(1.0f, getResources().getDisplayMetrics().scaledDensity /
+                getResources().getDisplayMetrics().density);
+        return (int) (dimensionPixelSize * factor);
+    }
+
+    /**
+     * Resets this view so it can be re-used for an updated notification.
+     */
+    public void reset() {
+        mShowingPublicInitialized = false;
+        onHeightReset();
+        requestLayout();
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mPublicLayout = (NotificationContentView) findViewById(R.id.expandedPublic);
+        mPrivateLayout = (NotificationContentView) findViewById(R.id.expanded);
+        mLayouts = new NotificationContentView[] {mPrivateLayout, mPublicLayout};
+
+        for (NotificationContentView l : mLayouts) {
+            l.setExpandClickListener(mExpandClickListener);
+            l.setContainingNotification(this);
+        }
+        mGutsStub = (ViewStub) findViewById(R.id.notification_guts_stub);
+        mGutsStub.setOnInflateListener(new ViewStub.OnInflateListener() {
+            @Override
+            public void onInflate(ViewStub stub, View inflated) {
+                mGuts = (NotificationGuts) inflated;
+                mGuts.setClipTopAmount(getClipTopAmount());
+                mGuts.setActualHeight(getActualHeight());
+                mGutsStub = null;
+            }
+        });
+        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.setIsLowPriority(mIsLowPriority);
+                mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this);
+                mChildrenContainer.onNotificationUpdated();
+
+                if (mShouldTranslateContents) {
+                    mTranslateableViews.add(mChildrenContainer);
+                }
+            }
+        });
+
+        if (mShouldTranslateContents) {
+            // Add the views that we translate to reveal the menu
+            mTranslateableViews = new ArrayList<>();
+            for (int i = 0; i < getChildCount(); i++) {
+                mTranslateableViews.add(getChildAt(i));
+            }
+            // Remove views that don't translate
+            mTranslateableViews.remove(mChildrenContainerStub);
+            mTranslateableViews.remove(mGutsStub);
+        }
+    }
+
+    public void resetTranslation() {
+        if (mTranslateAnim != null) {
+            mTranslateAnim.cancel();
+        }
+
+        if (!mShouldTranslateContents) {
+            setTranslationX(0);
+        } else if (mTranslateableViews != null) {
+            for (int i = 0; i < mTranslateableViews.size(); i++) {
+                mTranslateableViews.get(i).setTranslationX(0);
+            }
+            invalidateOutline();
+        }
+
+        mMenuRow.resetMenu();
+    }
+
+    public void animateTranslateNotification(final float leftTarget) {
+        if (mTranslateAnim != null) {
+            mTranslateAnim.cancel();
+        }
+        mTranslateAnim = getTranslateViewAnimator(leftTarget, null /* updateListener */);
+        if (mTranslateAnim != null) {
+            mTranslateAnim.start();
+        }
+    }
+
+    @Override
+    public void setTranslation(float translationX) {
+        if (areGutsExposed()) {
+            // Don't translate if guts are showing.
+            return;
+        }
+        if (!mShouldTranslateContents) {
+            setTranslationX(translationX);
+        } else if (mTranslateableViews != null) {
+            // Translate the group of views
+            for (int i = 0; i < mTranslateableViews.size(); i++) {
+                if (mTranslateableViews.get(i) != null) {
+                    mTranslateableViews.get(i).setTranslationX(translationX);
+                }
+            }
+            invalidateOutline();
+        }
+        if (mMenuRow.getMenuView() != null) {
+            mMenuRow.onTranslationUpdate(translationX);
+        }
+    }
+
+    @Override
+    public float getTranslation() {
+        if (!mShouldTranslateContents) {
+            return getTranslationX();
+        }
+
+        if (mTranslateableViews != null && mTranslateableViews.size() > 0) {
+            // All of the views in the list should have same translation, just use first one.
+            return mTranslateableViews.get(0).getTranslationX();
+        }
+
+        return 0;
+    }
+
+    public Animator getTranslateViewAnimator(final float leftTarget,
+            AnimatorUpdateListener listener) {
+        if (mTranslateAnim != null) {
+            mTranslateAnim.cancel();
+        }
+        if (areGutsExposed()) {
+            // No translation if guts are exposed.
+            return null;
+        }
+        final ObjectAnimator translateAnim = ObjectAnimator.ofFloat(this, TRANSLATE_CONTENT,
+                leftTarget);
+        if (listener != null) {
+            translateAnim.addUpdateListener(listener);
+        }
+        translateAnim.addListener(new AnimatorListenerAdapter() {
+            boolean cancelled = false;
+
+            @Override
+            public void onAnimationCancel(Animator anim) {
+                cancelled = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator anim) {
+                if (!cancelled && leftTarget == 0) {
+                    mMenuRow.resetMenu();
+                    mTranslateAnim = null;
+                }
+            }
+        });
+        mTranslateAnim = translateAnim;
+        return translateAnim;
+    }
+
+    public void inflateGuts() {
+        if (mGuts == null) {
+            mGutsStub.inflate();
+        }
+    }
+
+    private void updateChildrenVisibility() {
+        mPrivateLayout.setVisibility(!mShowingPublic && !mIsSummaryWithChildren ? VISIBLE
+                : INVISIBLE);
+        if (mChildrenContainer != null) {
+            mChildrenContainer.setVisibility(!mShowingPublic && mIsSummaryWithChildren ? VISIBLE
+                    : INVISIBLE);
+        }
+        // The limits might have changed if the view suddenly became a group or vice versa
+        updateLimits();
+    }
+
+    @Override
+    public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) {
+        if (super.onRequestSendAccessibilityEventInternal(child, event)) {
+            // Add a record for the entire layout since its content is somehow small.
+            // The event comes from a leaf view that is interacted with.
+            AccessibilityEvent record = AccessibilityEvent.obtain();
+            onInitializeAccessibilityEvent(record);
+            dispatchPopulateAccessibilityEvent(record);
+            event.appendRecord(record);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void setDark(boolean dark, boolean fade, long delay) {
+        super.setDark(dark, fade, delay);
+        mDark = dark;
+        if (!mIsHeadsUp) {
+            // Only fade the showing view of the pulsing notification.
+            fade = false;
+        }
+        final NotificationContentView showing = getShowingLayout();
+        if (showing != null) {
+            showing.setDark(dark, fade, delay);
+        }
+        if (mIsSummaryWithChildren) {
+            mChildrenContainer.setDark(dark, fade, delay);
+        }
+        updateShelfIconColor();
+    }
+
+    /**
+     * Tap sounds should not be played when we're unlocking.
+     * Doing so would cause audio collision and the system would feel unpolished.
+     */
+    @Override
+    public boolean isSoundEffectsEnabled() {
+        final boolean mute = mDark && mSecureStateProvider != null &&
+                !mSecureStateProvider.getAsBoolean();
+        return !mute && super.isSoundEffectsEnabled();
+    }
+
+    public boolean isExpandable() {
+        if (mIsSummaryWithChildren && !mShowingPublic) {
+            return !mChildrenExpanded;
+        }
+        return mEnableNonGroupedNotificationExpand && mExpandable;
+    }
+
+    public void setExpandable(boolean expandable) {
+        mExpandable = expandable;
+        mPrivateLayout.updateExpandButtons(isExpandable());
+    }
+
+    @Override
+    public void setClipToActualHeight(boolean clipToActualHeight) {
+        super.setClipToActualHeight(clipToActualHeight || isUserLocked());
+        getShowingLayout().setClipToActualHeight(clipToActualHeight || isUserLocked());
+    }
+
+    /**
+     * @return whether the user has changed the expansion state
+     */
+    public boolean hasUserChangedExpansion() {
+        return mHasUserChangedExpansion;
+    }
+
+    public boolean isUserExpanded() {
+        return mUserExpanded;
+    }
+
+    /**
+     * Set this notification to be expanded by the user
+     *
+     * @param userExpanded whether the user wants this notification to be expanded
+     */
+    public void setUserExpanded(boolean userExpanded) {
+        setUserExpanded(userExpanded, false /* allowChildExpansion */);
+    }
+
+    /**
+     * Set this notification to be expanded by the user
+     *
+     * @param userExpanded whether the user wants this notification to be expanded
+     * @param allowChildExpansion whether a call to this method allows expanding children
+     */
+    public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) {
+        mFalsingManager.setNotificationExpanded();
+        if (mIsSummaryWithChildren && !mShowingPublic && allowChildExpansion
+                && !mChildrenContainer.showingAsLowPriority()) {
+            final boolean wasExpanded = mGroupManager.isGroupExpanded(mStatusBarNotification);
+            mGroupManager.setGroupExpanded(mStatusBarNotification, userExpanded);
+            onExpansionChanged(true /* userAction */, wasExpanded);
+            return;
+        }
+        if (userExpanded && !mExpandable) return;
+        final boolean wasExpanded = isExpanded();
+        mHasUserChangedExpansion = true;
+        mUserExpanded = userExpanded;
+        onExpansionChanged(true /* userAction */, wasExpanded);
+        if (!wasExpanded && isExpanded()
+                && getActualHeight() != getIntrinsicHeight()) {
+            notifyHeightChanged(true /* needsAnimation */);
+        }
+    }
+
+    public void resetUserExpansion() {
+        boolean changed = mUserExpanded;
+        mHasUserChangedExpansion = false;
+        mUserExpanded = false;
+        if (changed && mIsSummaryWithChildren) {
+            mChildrenContainer.onExpansionChanged();
+        }
+        updateShelfIconColor();
+    }
+
+    public boolean isUserLocked() {
+        return mUserLocked && !mForceUnlocked;
+    }
+
+    public void setUserLocked(boolean userLocked) {
+        mUserLocked = userLocked;
+        mPrivateLayout.setUserExpanding(userLocked);
+        // This is intentionally not guarded with mIsSummaryWithChildren since we might have had
+        // children but not anymore.
+        if (mChildrenContainer != null) {
+            mChildrenContainer.setUserLocked(userLocked);
+            if (mIsSummaryWithChildren && (userLocked || !isGroupExpanded())) {
+                updateBackgroundForGroupState();
+            }
+        }
+    }
+
+    /**
+     * @return has the system set this notification to be expanded
+     */
+    public boolean isSystemExpanded() {
+        return mIsSystemExpanded;
+    }
+
+    /**
+     * Set this notification to be expanded by the system.
+     *
+     * @param expand whether the system wants this notification to be expanded.
+     */
+    public void setSystemExpanded(boolean expand) {
+        if (expand != mIsSystemExpanded) {
+            final boolean wasExpanded = isExpanded();
+            mIsSystemExpanded = expand;
+            notifyHeightChanged(false /* needsAnimation */);
+            onExpansionChanged(false /* userAction */, wasExpanded);
+            if (mIsSummaryWithChildren) {
+                mChildrenContainer.updateGroupOverflow();
+            }
+        }
+    }
+
+    /**
+     * @param onKeyguard whether to prevent notification expansion
+     */
+    public void setOnKeyguard(boolean onKeyguard) {
+        if (onKeyguard != mOnKeyguard) {
+            boolean wasAboveShelf = isAboveShelf();
+            final boolean wasExpanded = isExpanded();
+            mOnKeyguard = onKeyguard;
+            onExpansionChanged(false /* userAction */, wasExpanded);
+            if (wasExpanded != isExpanded()) {
+                if (mIsSummaryWithChildren) {
+                    mChildrenContainer.updateGroupOverflow();
+                }
+                notifyHeightChanged(false /* needsAnimation */);
+            }
+            if (isAboveShelf() != wasAboveShelf) {
+                mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
+            }
+        }
+    }
+
+    /**
+     * @return Can the underlying notification be cleared? This can be different from whether the
+     *         notification can be dismissed in case notifications are sensitive on the lockscreen.
+     * @see #canViewBeDismissed()
+     */
+    public boolean isClearable() {
+        if (mStatusBarNotification == null || !mStatusBarNotification.isClearable()) {
+            return false;
+        }
+        if (mIsSummaryWithChildren) {
+            List<ExpandableNotificationRow> notificationChildren =
+                    mChildrenContainer.getNotificationChildren();
+            for (int i = 0; i < notificationChildren.size(); i++) {
+                ExpandableNotificationRow child = notificationChildren.get(i);
+                if (!child.isClearable()) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public int getIntrinsicHeight() {
+        if (isUserLocked()) {
+            return getActualHeight();
+        }
+        if (mGuts != null && mGuts.isExposed()) {
+            return mGuts.getIntrinsicHeight();
+        } else if ((isChildInGroup() && !isGroupExpanded())) {
+            return mPrivateLayout.getMinHeight();
+        } else if (mSensitive && mHideSensitiveForIntrinsicHeight) {
+            return getMinHeight();
+        } else if (mIsSummaryWithChildren && (!mOnKeyguard || mShowAmbient)) {
+            return mChildrenContainer.getIntrinsicHeight();
+        } else if (isHeadsUpAllowed() && (mIsHeadsUp || mHeadsupDisappearRunning)) {
+            if (isPinned() || mHeadsupDisappearRunning) {
+                return getPinnedHeadsUpHeight(true /* atLeastMinHeight */);
+            } else if (isExpanded()) {
+                return Math.max(getMaxExpandHeight(), mHeadsUpHeight);
+            } else {
+                return Math.max(getCollapsedHeight(), mHeadsUpHeight);
+            }
+        } else if (isExpanded()) {
+            return getMaxExpandHeight();
+        } else {
+            return getCollapsedHeight();
+        }
+    }
+
+    private boolean isHeadsUpAllowed() {
+        return !mOnKeyguard && !mShowAmbient;
+    }
+
+    @Override
+    public boolean isGroupExpanded() {
+        return mGroupManager.isGroupExpanded(mStatusBarNotification);
+    }
+
+    private void onChildrenCountChanged() {
+        mIsSummaryWithChildren = StatusBar.ENABLE_CHILD_NOTIFICATIONS
+                && mChildrenContainer != null && mChildrenContainer.getNotificationChildCount() > 0;
+        if (mIsSummaryWithChildren && mChildrenContainer.getHeaderView() == null) {
+            mChildrenContainer.recreateNotificationHeader(mExpandClickListener
+            );
+        }
+        getShowingLayout().updateBackgroundColor(false /* animate */);
+        mPrivateLayout.updateExpandButtons(isExpandable());
+        updateChildrenHeaderAppearance();
+        updateChildrenVisibility();
+    }
+
+    public void updateChildrenHeaderAppearance() {
+        if (mIsSummaryWithChildren) {
+            mChildrenContainer.updateChildrenHeaderAppearance();
+        }
+    }
+
+    /**
+     * Check whether the view state is currently expanded. This is given by the system in {@link
+     * #setSystemExpanded(boolean)} and can be overridden by user expansion or
+     * collapsing in {@link #setUserExpanded(boolean)}. Note that the visual appearance of this
+     * view can differ from this state, if layout params are modified from outside.
+     *
+     * @return whether the view state is currently expanded.
+     */
+    public boolean isExpanded() {
+        return isExpanded(false /* allowOnKeyguard */);
+    }
+
+    public boolean isExpanded(boolean allowOnKeyguard) {
+        return (!mOnKeyguard || allowOnKeyguard)
+                && (!hasUserChangedExpansion() && (isSystemExpanded() || isSystemChildExpanded())
+                || isUserExpanded());
+    }
+
+    private boolean isSystemChildExpanded() {
+        return mIsSystemChildExpanded;
+    }
+
+    public void setSystemChildExpanded(boolean expanded) {
+        mIsSystemChildExpanded = expanded;
+    }
+
+    public void setLayoutListener(LayoutListener listener) {
+        mLayoutListener = listener;
+    }
+
+    public void removeListener() {
+        mLayoutListener = null;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        updateMaxHeights();
+        if (mMenuRow.getMenuView() != null) {
+            mMenuRow.onHeightUpdate();
+        }
+        updateContentShiftHeight();
+        if (mLayoutListener != null) {
+            mLayoutListener.onLayout();
+        }
+    }
+
+    /**
+     * Updates the content shift height such that the header is completely hidden when coming from
+     * the top.
+     */
+    private void updateContentShiftHeight() {
+        NotificationHeaderView notificationHeader = getVisibleNotificationHeader();
+        if (notificationHeader != null) {
+            CachingIconView icon = notificationHeader.getIcon();
+            mIconTransformContentShift = getRelativeTopPadding(icon) + icon.getHeight();
+        } else {
+            mIconTransformContentShift = mIconTransformContentShiftNoIcon;
+        }
+    }
+
+    private void updateMaxHeights() {
+        int intrinsicBefore = getIntrinsicHeight();
+        View expandedChild = mPrivateLayout.getExpandedChild();
+        if (expandedChild == null) {
+            expandedChild = mPrivateLayout.getContractedChild();
+        }
+        mMaxExpandHeight = expandedChild.getHeight();
+        View headsUpChild = mPrivateLayout.getHeadsUpChild();
+        if (headsUpChild == null) {
+            headsUpChild = mPrivateLayout.getContractedChild();
+        }
+        mHeadsUpHeight = headsUpChild.getHeight();
+        if (intrinsicBefore != getIntrinsicHeight()) {
+            notifyHeightChanged(true  /* needsAnimation */);
+        }
+    }
+
+    @Override
+    public void notifyHeightChanged(boolean needsAnimation) {
+        super.notifyHeightChanged(needsAnimation);
+        getShowingLayout().requestSelectLayout(needsAnimation || isUserLocked());
+    }
+
+    public void setSensitive(boolean sensitive, boolean hideSensitive) {
+        mSensitive = sensitive;
+        mSensitiveHiddenInGeneral = hideSensitive;
+    }
+
+    @Override
+    public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
+        mHideSensitiveForIntrinsicHeight = hideSensitive;
+        if (mIsSummaryWithChildren) {
+            List<ExpandableNotificationRow> notificationChildren =
+                    mChildrenContainer.getNotificationChildren();
+            for (int i = 0; i < notificationChildren.size(); i++) {
+                ExpandableNotificationRow child = notificationChildren.get(i);
+                child.setHideSensitiveForIntrinsicHeight(hideSensitive);
+            }
+        }
+    }
+
+    @Override
+    public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
+            long duration) {
+        boolean oldShowingPublic = mShowingPublic;
+        mShowingPublic = mSensitive && hideSensitive;
+        if (mShowingPublicInitialized && mShowingPublic == oldShowingPublic) {
+            return;
+        }
+
+        // bail out if no public version
+        if (mPublicLayout.getChildCount() == 0) return;
+
+        if (!animated) {
+            mPublicLayout.animate().cancel();
+            mPrivateLayout.animate().cancel();
+            if (mChildrenContainer != null) {
+                mChildrenContainer.animate().cancel();
+                mChildrenContainer.setAlpha(1f);
+            }
+            mPublicLayout.setAlpha(1f);
+            mPrivateLayout.setAlpha(1f);
+            mPublicLayout.setVisibility(mShowingPublic ? View.VISIBLE : View.INVISIBLE);
+            updateChildrenVisibility();
+        } else {
+            animateShowingPublic(delay, duration);
+        }
+        NotificationContentView showingLayout = getShowingLayout();
+        showingLayout.updateBackgroundColor(animated);
+        mPrivateLayout.updateExpandButtons(isExpandable());
+        updateShelfIconColor();
+        showingLayout.setDark(isDark(), false /* animate */, 0 /* delay */);
+        mShowingPublicInitialized = true;
+    }
+
+    private void animateShowingPublic(long delay, long duration) {
+        View[] privateViews = mIsSummaryWithChildren
+                ? new View[] {mChildrenContainer}
+                : new View[] {mPrivateLayout};
+        View[] publicViews = new View[] {mPublicLayout};
+        View[] hiddenChildren = mShowingPublic ? privateViews : publicViews;
+        View[] shownChildren = mShowingPublic ? publicViews : privateViews;
+        for (final View hiddenView : hiddenChildren) {
+            hiddenView.setVisibility(View.VISIBLE);
+            hiddenView.animate().cancel();
+            hiddenView.animate()
+                    .alpha(0f)
+                    .setStartDelay(delay)
+                    .setDuration(duration)
+                    .withEndAction(new Runnable() {
+                        @Override
+                        public void run() {
+                            hiddenView.setVisibility(View.INVISIBLE);
+                        }
+                    });
+        }
+        for (View showView : shownChildren) {
+            showView.setVisibility(View.VISIBLE);
+            showView.setAlpha(0f);
+            showView.animate().cancel();
+            showView.animate()
+                    .alpha(1f)
+                    .setStartDelay(delay)
+                    .setDuration(duration);
+        }
+    }
+
+    @Override
+    public boolean mustStayOnScreen() {
+        return mIsHeadsUp;
+    }
+
+    /**
+     * @return Whether this view is allowed to be dismissed. Only valid for visible notifications as
+     *         otherwise some state might not be updated. To request about the general clearability
+     *         see {@link #isClearable()}.
+     */
+    public boolean canViewBeDismissed() {
+        return isClearable() && (!mShowingPublic || !mSensitiveHiddenInGeneral);
+    }
+
+    public void makeActionsVisibile() {
+        setUserExpanded(true, true);
+        if (isChildInGroup()) {
+            mGroupManager.setGroupExpanded(mStatusBarNotification, true);
+        }
+        notifyHeightChanged(false /* needsAnimation */);
+    }
+
+    public void setChildrenExpanded(boolean expanded, boolean animate) {
+        mChildrenExpanded = expanded;
+        if (mChildrenContainer != null) {
+            mChildrenContainer.setChildrenExpanded(expanded);
+        }
+        updateBackgroundForGroupState();
+        updateClickAndFocus();
+    }
+
+    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 mMaxExpandHeight;
+    }
+
+    public boolean areGutsExposed() {
+        return (mGuts != null && mGuts.isExposed());
+    }
+
+    @Override
+    public boolean isContentExpandable() {
+        if (mIsSummaryWithChildren && !mShowingPublic) {
+            return true;
+        }
+        NotificationContentView showingLayout = getShowingLayout();
+        return showingLayout.isContentExpandable();
+    }
+
+    @Override
+    protected View getContentView() {
+        if (mIsSummaryWithChildren && !mShowingPublic) {
+            return mChildrenContainer;
+        }
+        return getShowingLayout();
+    }
+
+    @Override
+    protected void onAppearAnimationFinished(boolean wasAppearing) {
+        super.onAppearAnimationFinished(wasAppearing);
+        if (wasAppearing) {
+            // During the animation the visible view might have changed, so let's make sure all
+            // alphas are reset
+            if (mChildrenContainer != null) {
+                mChildrenContainer.setAlpha(1.0f);
+                mChildrenContainer.setLayerType(LAYER_TYPE_NONE, null);
+            }
+            for (NotificationContentView l : mLayouts) {
+                l.setAlpha(1.0f);
+                l.setLayerType(LAYER_TYPE_NONE, null);
+            }
+        }
+    }
+
+    @Override
+    public int getExtraBottomPadding() {
+        if (mIsSummaryWithChildren && isGroupExpanded()) {
+            return mIncreasedPaddingBetweenElements;
+        }
+        return 0;
+    }
+
+    @Override
+    public void setActualHeight(int height, boolean notifyListeners) {
+        boolean changed = height != getActualHeight();
+        super.setActualHeight(height, notifyListeners);
+        if (changed && isRemoved()) {
+            // TODO: remove this once we found the gfx bug for this.
+            // This is a hack since a removed view sometimes would just stay blank. it occured
+            // when sending yourself a message and then clicking on it.
+            ViewGroup parent = (ViewGroup) getParent();
+            if (parent != null) {
+                parent.invalidate();
+            }
+        }
+        if (mGuts != null && mGuts.isExposed()) {
+            mGuts.setActualHeight(height);
+            return;
+        }
+        int contentHeight = Math.max(getMinHeight(), height);
+        for (NotificationContentView l : mLayouts) {
+            l.setContentHeight(contentHeight);
+        }
+        if (mIsSummaryWithChildren) {
+            mChildrenContainer.setActualHeight(height);
+        }
+        if (mGuts != null) {
+            mGuts.setActualHeight(height);
+        }
+        if (mMenuRow.getMenuView() != null) {
+            mMenuRow.onHeightUpdate();
+        }
+    }
+
+    @Override
+    public int getMaxContentHeight() {
+        if (mIsSummaryWithChildren && !mShowingPublic) {
+            return mChildrenContainer.getMaxContentHeight();
+        }
+        NotificationContentView showingLayout = getShowingLayout();
+        return showingLayout.getMaxHeight();
+    }
+
+    @Override
+    public int getMinHeight() {
+        if (mGuts != null && mGuts.isExposed()) {
+            return mGuts.getIntrinsicHeight();
+        } else if (isHeadsUpAllowed() && mIsHeadsUp && mHeadsUpManager.isTrackingHeadsUp()) {
+                return getPinnedHeadsUpHeight(false /* atLeastMinHeight */);
+        } else if (mIsSummaryWithChildren && !isGroupExpanded() && !mShowingPublic) {
+            return mChildrenContainer.getMinHeight();
+        } else if (isHeadsUpAllowed() && mIsHeadsUp) {
+            return mHeadsUpHeight;
+        }
+        NotificationContentView showingLayout = getShowingLayout();
+        return showingLayout.getMinHeight();
+    }
+
+    @Override
+    public int getCollapsedHeight() {
+        if (mIsSummaryWithChildren && !mShowingPublic) {
+            return mChildrenContainer.getCollapsedHeight();
+        }
+        return getMinHeight();
+    }
+
+    @Override
+    public void setClipTopAmount(int clipTopAmount) {
+        super.setClipTopAmount(clipTopAmount);
+        for (NotificationContentView l : mLayouts) {
+            l.setClipTopAmount(clipTopAmount);
+        }
+        if (mGuts != null) {
+            mGuts.setClipTopAmount(clipTopAmount);
+        }
+    }
+
+    @Override
+    public void setClipBottomAmount(int clipBottomAmount) {
+        if (clipBottomAmount != mClipBottomAmount) {
+            super.setClipBottomAmount(clipBottomAmount);
+            for (NotificationContentView l : mLayouts) {
+                l.setClipBottomAmount(clipBottomAmount);
+            }
+            if (mGuts != null) {
+                mGuts.setClipBottomAmount(clipBottomAmount);
+            }
+        }
+        if (mChildrenContainer != null) {
+            // We have to update this even if it hasn't changed, since the children locations can
+            // have changed
+            mChildrenContainer.setClipBottomAmount(clipBottomAmount);
+        }
+    }
+
+    public boolean isMaxExpandHeightInitialized() {
+        return mMaxExpandHeight != 0;
+    }
+
+    public NotificationContentView getShowingLayout() {
+        return mShowingPublic ? mPublicLayout : mPrivateLayout;
+    }
+
+    public void setLegacy(boolean legacy) {
+        for (NotificationContentView l : mLayouts) {
+            l.setLegacy(legacy);
+        }
+    }
+
+    @Override
+    protected void updateBackgroundTint() {
+        super.updateBackgroundTint();
+        updateBackgroundForGroupState();
+        if (mIsSummaryWithChildren) {
+            List<ExpandableNotificationRow> notificationChildren =
+                    mChildrenContainer.getNotificationChildren();
+            for (int i = 0; i < notificationChildren.size(); i++) {
+                ExpandableNotificationRow child = notificationChildren.get(i);
+                child.updateBackgroundForGroupState();
+            }
+        }
+    }
+
+    /**
+     * Called when a group has finished animating from collapsed or expanded state.
+     */
+    public void onFinishedExpansionChange() {
+        mGroupExpansionChanging = false;
+        updateBackgroundForGroupState();
+    }
+
+    /**
+     * Updates the parent and children backgrounds in a group based on the expansion state.
+     */
+    public void updateBackgroundForGroupState() {
+        if (mIsSummaryWithChildren) {
+            // Only when the group has finished expanding do we hide its background.
+            mShowNoBackground = !mShowGroupBackgroundWhenExpanded && isGroupExpanded()
+                    && !isGroupExpansionChanging() && !isUserLocked();
+            mChildrenContainer.updateHeaderForExpansion(mShowNoBackground);
+            List<ExpandableNotificationRow> children = mChildrenContainer.getNotificationChildren();
+            for (int i = 0; i < children.size(); i++) {
+                children.get(i).updateBackgroundForGroupState();
+            }
+        } else if (isChildInGroup()) {
+            final int childColor = getShowingLayout().getBackgroundColorForExpansionState();
+            // Only show a background if the group is expanded OR if it is expanding / collapsing
+            // and has a custom background color.
+            final boolean showBackground = isGroupExpanded()
+                    || ((mNotificationParent.isGroupExpansionChanging()
+                    || mNotificationParent.isUserLocked()) && childColor != 0);
+            mShowNoBackground = !showBackground;
+        } else {
+            // Only children or parents ever need no background.
+            mShowNoBackground = false;
+        }
+        updateOutline();
+        updateBackground();
+    }
+
+    public int getPositionOfChild(ExpandableNotificationRow childRow) {
+        if (mIsSummaryWithChildren) {
+            return mChildrenContainer.getPositionInLinearLayout(childRow);
+        }
+        return 0;
+    }
+
+    public void setExpansionLogger(ExpansionLogger logger, String key) {
+        mLogger = logger;
+        mLoggingKey = key;
+    }
+
+    public void onExpandedByGesture(boolean userExpanded) {
+        int event = MetricsEvent.ACTION_NOTIFICATION_GESTURE_EXPANDER;
+        if (mGroupManager.isSummaryOfGroup(getStatusBarNotification())) {
+            event = MetricsEvent.ACTION_NOTIFICATION_GROUP_GESTURE_EXPANDER;
+        }
+        MetricsLogger.action(mContext, event, userExpanded);
+    }
+
+    @Override
+    public float getIncreasedPaddingAmount() {
+        if (mIsSummaryWithChildren) {
+            if (isGroupExpanded()) {
+                return 1.0f;
+            } else if (isUserLocked()) {
+                return mChildrenContainer.getIncreasedPaddingAmount();
+            }
+        } else if (isColorized() && (!mIsLowPriority || isExpanded())) {
+            return -1.0f;
+        }
+        return 0.0f;
+    }
+
+    private boolean isColorized() {
+        return mIsColorized && mBgTint != NO_COLOR;
+    }
+
+    @Override
+    protected boolean disallowSingleClick(MotionEvent event) {
+        float x = event.getX();
+        float y = event.getY();
+        NotificationHeaderView header = getVisibleNotificationHeader();
+        if (header != null && header.isInTouchRect(x - getTranslation(), y)) {
+            return true;
+        }
+        if ((!mIsSummaryWithChildren || mShowingPublic)
+                && getShowingLayout().disallowSingleClick(x, y)) {
+            return true;
+        }
+        return super.disallowSingleClick(event);
+    }
+
+    private void onExpansionChanged(boolean userAction, boolean wasExpanded) {
+        boolean nowExpanded = isExpanded();
+        if (mIsSummaryWithChildren && (!mIsLowPriority || wasExpanded)) {
+            nowExpanded = mGroupManager.isGroupExpanded(mStatusBarNotification);
+        }
+        if (nowExpanded != wasExpanded) {
+            updateShelfIconColor();
+            if (mLogger != null) {
+                mLogger.logNotificationExpansion(mLoggingKey, userAction, nowExpanded);
+            }
+            if (mIsSummaryWithChildren) {
+                mChildrenContainer.onExpansionChanged();
+            }
+        }
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        if (canViewBeDismissed()) {
+            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS);
+        }
+        boolean expandable = mShowingPublic;
+        boolean isExpanded = false;
+        if (!expandable) {
+            if (mIsSummaryWithChildren) {
+                expandable = true;
+                if (!mIsLowPriority || isExpanded()) {
+                    isExpanded = isGroupExpanded();
+                }
+            } else {
+                expandable = mPrivateLayout.isContentExpandable();
+                isExpanded = isExpanded();
+            }
+        }
+        if (expandable) {
+            if (isExpanded) {
+                info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE);
+            } else {
+                info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
+            }
+        }
+    }
+
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+        switch (action) {
+            case AccessibilityNodeInfo.ACTION_DISMISS:
+                NotificationStackScrollLayout.performDismiss(this, mGroupManager,
+                        true /* fromAccessibility */);
+                return true;
+            case AccessibilityNodeInfo.ACTION_COLLAPSE:
+            case AccessibilityNodeInfo.ACTION_EXPAND:
+                mExpandClickListener.onClick(this);
+                return true;
+        }
+        return false;
+    }
+
+    public boolean shouldRefocusOnDismiss() {
+        return mRefocusOnDismiss || isAccessibilityFocused();
+    }
+
+    public interface OnExpandClickListener {
+        void onExpandClicked(NotificationData.Entry clickedEntry, boolean nowExpanded);
+    }
+
+    @Override
+    public ExpandableViewState createNewViewState(StackScrollState stackScrollState) {
+        return new NotificationViewState(stackScrollState);
+    }
+
+    @Override
+    public boolean isAboveShelf() {
+        return !isOnKeyguard()
+                && (mIsPinned || mHeadsupDisappearRunning || (mIsHeadsUp && mAboveShelf));
+    }
+
+    public void setShowAmbient(boolean showAmbient) {
+        if (showAmbient != mShowAmbient) {
+            mShowAmbient = showAmbient;
+            if (mChildrenContainer != null) {
+                mChildrenContainer.notifyShowAmbientChanged();
+            }
+            notifyHeightChanged(false /* needsAnimation */);
+        }
+    }
+
+    public boolean isShowingAmbient() {
+        return mShowAmbient;
+    }
+
+    public void setAboveShelf(boolean aboveShelf) {
+        boolean wasAboveShelf = isAboveShelf();
+        mAboveShelf = aboveShelf;
+        if (isAboveShelf() != wasAboveShelf) {
+            mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
+        }
+    }
+
+    public static class NotificationViewState extends ExpandableViewState {
+
+        private final StackScrollState mOverallState;
+
+
+        private NotificationViewState(StackScrollState stackScrollState) {
+            mOverallState = stackScrollState;
+        }
+
+        @Override
+        public void applyToView(View view) {
+            super.applyToView(view);
+            if (view instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+                row.applyChildrenState(mOverallState);
+            }
+        }
+
+        @Override
+        protected void onYTranslationAnimationFinished(View view) {
+            super.onYTranslationAnimationFinished(view);
+            if (view instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+                if (row.isHeadsUpAnimatingAway()) {
+                    row.setHeadsUpAnimatingAway(false);
+                }
+            }
+        }
+
+        @Override
+        public void animateTo(View child, AnimationProperties properties) {
+            super.animateTo(child, properties);
+            if (child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                row.startChildAnimation(mOverallState, properties);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    protected void setChildrenContainer(NotificationChildrenContainer childrenContainer) {
+        mChildrenContainer = childrenContainer;
+    }
+}
diff --git a/com/android/systemui/statusbar/ExpandableOutlineView.java b/com/android/systemui/statusbar/ExpandableOutlineView.java
new file mode 100644
index 0000000..2556890
--- /dev/null
+++ b/com/android/systemui/statusbar/ExpandableOutlineView.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2014 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.content.res.Resources;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import com.android.systemui.R;
+
+/**
+ * Like {@link ExpandableView}, but setting an outline for the height and clipping.
+ */
+public abstract class ExpandableOutlineView extends ExpandableView {
+
+    private final Rect mOutlineRect = new Rect();
+    private boolean mCustomOutline;
+    private float mOutlineAlpha = -1f;
+    private float mOutlineRadius;
+
+    /**
+     * {@code true} if the children views of the {@link ExpandableOutlineView} are translated when
+     * it is moved. Otherwise, the translation is set on the {@code ExpandableOutlineView} itself.
+     */
+    protected boolean mShouldTranslateContents;
+
+    private final ViewOutlineProvider mProvider = new ViewOutlineProvider() {
+        @Override
+        public void getOutline(View view, Outline outline) {
+            int translation = mShouldTranslateContents ? (int) getTranslation() : 0;
+            if (!mCustomOutline) {
+                outline.setRoundRect(translation,
+                        mClipTopAmount,
+                        getWidth() + translation,
+                        Math.max(getActualHeight() - mClipBottomAmount, mClipTopAmount),
+                        mOutlineRadius);
+            } else {
+                outline.setRoundRect(mOutlineRect, mOutlineRadius);
+            }
+            outline.setAlpha(mOutlineAlpha);
+        }
+    };
+
+    public ExpandableOutlineView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setOutlineProvider(mProvider);
+        initDimens();
+    }
+
+    private void initDimens() {
+        Resources res = getResources();
+        mShouldTranslateContents =
+                res.getBoolean(R.bool.config_translateNotificationContentsOnSwipe);
+        mOutlineRadius = res.getDimension(R.dimen.notification_shadow_radius);
+        setClipToOutline(res.getBoolean(R.bool.config_clipNotificationsToOutline));
+    }
+
+    public void onDensityOrFontScaleChanged() {
+        initDimens();
+        invalidateOutline();
+    }
+
+    @Override
+    public void setActualHeight(int actualHeight, boolean notifyListeners) {
+        super.setActualHeight(actualHeight, notifyListeners);
+        invalidateOutline();
+    }
+
+    @Override
+    public void setClipTopAmount(int clipTopAmount) {
+        super.setClipTopAmount(clipTopAmount);
+        invalidateOutline();
+    }
+
+    @Override
+    public void setClipBottomAmount(int clipBottomAmount) {
+        super.setClipBottomAmount(clipBottomAmount);
+        invalidateOutline();
+    }
+
+    protected void setOutlineAlpha(float alpha) {
+        if (alpha != mOutlineAlpha) {
+            mOutlineAlpha = alpha;
+            invalidateOutline();
+        }
+    }
+
+    @Override
+    public float getOutlineAlpha() {
+        return mOutlineAlpha;
+    }
+
+    protected void setOutlineRect(RectF rect) {
+        if (rect != null) {
+            setOutlineRect(rect.left, rect.top, rect.right, rect.bottom);
+        } else {
+            mCustomOutline = false;
+            setClipToOutline(false);
+            invalidateOutline();
+        }
+    }
+
+    @Override
+    public int getOutlineTranslation() {
+        return mCustomOutline ? mOutlineRect.left : (int) getTranslation();
+    }
+
+    public void updateOutline() {
+        if (mCustomOutline) {
+            return;
+        }
+        boolean hasOutline = needsOutline();
+        setOutlineProvider(hasOutline ? mProvider : null);
+    }
+
+    /**
+     * @return Whether the view currently needs an outline. This is usually {@code false} in case
+     * it doesn't have a background.
+     */
+    protected boolean needsOutline() {
+        if (isChildInGroup()) {
+            return isGroupExpanded() && !isGroupExpansionChanging();
+        } else if (isSummaryWithChildren()) {
+            return !isGroupExpanded() || isGroupExpansionChanging();
+        }
+        return true;
+    }
+
+    public boolean isOutlineShowing() {
+        ViewOutlineProvider op = getOutlineProvider();
+        return op != null;
+    }
+
+    protected void setOutlineRect(float left, float top, float right, float bottom) {
+        mCustomOutline = true;
+        setClipToOutline(true);
+
+        mOutlineRect.set((int) left, (int) top, (int) right, (int) bottom);
+
+        // Outlines need to be at least 1 dp
+        mOutlineRect.bottom = (int) Math.max(top, mOutlineRect.bottom);
+        mOutlineRect.right = (int) Math.max(left, mOutlineRect.right);
+
+        invalidateOutline();
+    }
+
+}
diff --git a/com/android/systemui/statusbar/ExpandableView.java b/com/android/systemui/statusbar/ExpandableView.java
new file mode 100644
index 0000000..efe5e0c
--- /dev/null
+++ b/com/android/systemui/statusbar/ExpandableView.java
@@ -0,0 +1,530 @@
+/*
+ * Copyright (C) 2014 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.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.systemui.statusbar.stack.ExpandableViewState;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+import com.android.systemui.statusbar.stack.StackScrollState;
+
+import java.util.ArrayList;
+
+/**
+ * An abstract view for expandable views.
+ */
+public abstract class ExpandableView extends FrameLayout {
+
+    protected OnHeightChangedListener mOnHeightChangedListener;
+    private int mActualHeight;
+    protected int mClipTopAmount;
+    protected int mClipBottomAmount;
+    private boolean mDark;
+    private ArrayList<View> mMatchParentViews = new ArrayList<View>();
+    private static Rect mClipRect = new Rect();
+    private boolean mWillBeGone;
+    private int mMinClipTopAmount = 0;
+    private boolean mClipToActualHeight = true;
+    private boolean mChangingPosition = false;
+    private ViewGroup mTransientContainer;
+    private boolean mInShelf;
+    private boolean mTransformingInShelf;
+
+    public ExpandableView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int givenSize = MeasureSpec.getSize(heightMeasureSpec);
+        int ownMaxHeight = Integer.MAX_VALUE;
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        if (heightMode != MeasureSpec.UNSPECIFIED && givenSize != 0) {
+            ownMaxHeight = Math.min(givenSize, ownMaxHeight);
+        }
+        int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
+        int maxChildHeight = 0;
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            if (child.getVisibility() == GONE) {
+                continue;
+            }
+            int childHeightSpec = newHeightSpec;
+            ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
+            if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
+                if (layoutParams.height >= 0) {
+                    // An actual height is set
+                    childHeightSpec = layoutParams.height > ownMaxHeight
+                        ? MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.EXACTLY)
+                        : MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
+                }
+                child.measure(
+                        getChildMeasureSpec(widthMeasureSpec, 0 /* padding */, layoutParams.width),
+                        childHeightSpec);
+                int childHeight = child.getMeasuredHeight();
+                maxChildHeight = Math.max(maxChildHeight, childHeight);
+            } else {
+                mMatchParentViews.add(child);
+            }
+        }
+        int ownHeight = heightMode == MeasureSpec.EXACTLY
+                ? givenSize : Math.min(ownMaxHeight, maxChildHeight);
+        newHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY);
+        for (View child : mMatchParentViews) {
+            child.measure(getChildMeasureSpec(
+                    widthMeasureSpec, 0 /* padding */, child.getLayoutParams().width),
+                    newHeightSpec);
+        }
+        mMatchParentViews.clear();
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+        setMeasuredDimension(width, ownHeight);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        updateClipping();
+    }
+
+    @Override
+    public boolean pointInView(float localX, float localY, float slop) {
+        float top = mClipTopAmount;
+        float bottom = mActualHeight;
+        return localX >= -slop && localY >= top - slop && localX < ((mRight - mLeft) + slop) &&
+                localY < (bottom + slop);
+    }
+
+    /**
+     * Sets the actual height of this notification. This is different than the laid out
+     * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding.
+     *
+     * @param actualHeight The height of this notification.
+     * @param notifyListeners Whether the listener should be informed about the change.
+     */
+    public void setActualHeight(int actualHeight, boolean notifyListeners) {
+        mActualHeight = actualHeight;
+        updateClipping();
+        if (notifyListeners) {
+            notifyHeightChanged(false  /* needsAnimation */);
+        }
+    }
+
+    public void setActualHeight(int actualHeight) {
+        setActualHeight(actualHeight, true /* notifyListeners */);
+    }
+
+    /**
+     * See {@link #setActualHeight}.
+     *
+     * @return The current actual height of this notification.
+     */
+    public int getActualHeight() {
+        return mActualHeight;
+    }
+
+    /**
+     * @return The maximum height of this notification.
+     */
+    public int getMaxContentHeight() {
+        return getHeight();
+    }
+
+    /**
+     * @return The minimum content height of this notification.
+     */
+    public int getMinHeight() {
+        return getHeight();
+    }
+
+    /**
+     * @return The collapsed height of this view. Note that this might be different
+     * than {@link #getMinHeight()} because some elements like groups may have different sizes when
+     * they are system expanded.
+     */
+    public int getCollapsedHeight() {
+        return getHeight();
+    }
+
+    /**
+     * Sets the notification as dimmed. The default implementation does nothing.
+     *
+     * @param dimmed Whether the notification should be dimmed.
+     * @param fade Whether an animation should be played to change the state.
+     */
+    public void setDimmed(boolean dimmed, boolean fade) {
+    }
+
+    /**
+     * Sets the notification as dark. The default implementation does nothing.
+     *
+     * @param dark Whether the notification should be dark.
+     * @param fade Whether an animation should be played to change the state.
+     * @param delay If fading, the delay of the animation.
+     */
+    public void setDark(boolean dark, boolean fade, long delay) {
+        mDark = dark;
+    }
+
+    public boolean isDark() {
+        return mDark;
+    }
+
+    /**
+     * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about
+     * the upcoming state of hiding sensitive notifications. It gets called at the very beginning
+     * of a stack scroller update such that the updated intrinsic height (which is dependent on
+     * whether private or public layout is showing) gets taken into account into all layout
+     * calculations.
+     */
+    public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
+    }
+
+    /**
+     * Sets whether the notification should hide its private contents if it is sensitive.
+     */
+    public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
+            long duration) {
+    }
+
+    /**
+     * @return The desired notification height.
+     */
+    public int getIntrinsicHeight() {
+        return getHeight();
+    }
+
+    /**
+     * Sets the amount this view should be clipped from the top. This is used when an expanded
+     * notification is scrolling in the top or bottom stack.
+     *
+     * @param clipTopAmount The amount of pixels this view should be clipped from top.
+     */
+    public void setClipTopAmount(int clipTopAmount) {
+        mClipTopAmount = clipTopAmount;
+        updateClipping();
+    }
+
+    /**
+     * Set the amount the the notification is clipped on the bottom in addition to the regular
+     * clipping. This is mainly used to clip something in a non-animated way without changing the
+     * actual height of the notification and is purely visual.
+     *
+     * @param clipBottomAmount the amount to clip.
+     */
+    public void setClipBottomAmount(int clipBottomAmount) {
+        mClipBottomAmount = clipBottomAmount;
+        updateClipping();
+    }
+
+    public int getClipTopAmount() {
+        return mClipTopAmount;
+    }
+
+    public int getClipBottomAmount() {
+        return mClipBottomAmount;
+    }
+
+    public void setOnHeightChangedListener(OnHeightChangedListener listener) {
+        mOnHeightChangedListener = listener;
+    }
+
+    /**
+     * @return Whether we can expand this views content.
+     */
+    public boolean isContentExpandable() {
+        return false;
+    }
+
+    public void notifyHeightChanged(boolean needsAnimation) {
+        if (mOnHeightChangedListener != null) {
+            mOnHeightChangedListener.onHeightChanged(this, needsAnimation);
+        }
+    }
+
+    public boolean isTransparent() {
+        return false;
+    }
+
+    /**
+     * Perform a remove animation on this view.
+     *
+     * @param duration The duration of the remove animation.
+     * @param translationDirection The direction value from [-1 ... 1] indicating in which the
+     *                             animation should be performed. A value of -1 means that The
+     *                             remove animation should be performed upwards,
+     *                             such that the  child appears to be going away to the top. 1
+     *                             Should mean the opposite.
+     * @param onFinishedRunnable A runnable which should be run when the animation is finished.
+     */
+    public abstract void performRemoveAnimation(long duration, float translationDirection,
+            Runnable onFinishedRunnable);
+
+    public abstract void performAddAnimation(long delay, long duration);
+
+    /**
+     * Set the notification appearance to be below the speed bump.
+     * @param below true if it is below.
+     */
+    public void setBelowSpeedBump(boolean below) {
+    }
+
+    public int getPinnedHeadsUpHeight() {
+        return getIntrinsicHeight();
+    }
+
+
+    /**
+     * Sets the translation of the view.
+     */
+    public void setTranslation(float translation) {
+        setTranslationX(translation);
+    }
+
+    /**
+     * Gets the translation of the view.
+     */
+    public float getTranslation() {
+        return getTranslationX();
+    }
+
+    public void onHeightReset() {
+        if (mOnHeightChangedListener != null) {
+            mOnHeightChangedListener.onReset(this);
+        }
+    }
+
+    /**
+     * This method returns the drawing rect for the view which is different from the regular
+     * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at
+     * position 0 and usually the translation is neglected. Since we are manually clipping this
+     * view,we also need to subtract the clipTopAmount from the top. This is needed in order to
+     * ensure that accessibility and focusing work correctly.
+     *
+     * @param outRect The (scrolled) drawing bounds of the view.
+     */
+    @Override
+    public void getDrawingRect(Rect outRect) {
+        super.getDrawingRect(outRect);
+        outRect.left += getTranslationX();
+        outRect.right += getTranslationX();
+        outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight());
+        outRect.top += getTranslationY() + getClipTopAmount();
+    }
+
+    @Override
+    public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
+        super.getBoundsOnScreen(outRect, clipToParent);
+        if (getTop() + getTranslationY() < 0) {
+            // We got clipped to the parent here - make sure we undo that.
+            outRect.top += getTop() + getTranslationY();
+        }
+        outRect.bottom = outRect.top + getActualHeight();
+        outRect.top += getClipTopAmount();
+    }
+
+    public boolean isSummaryWithChildren() {
+        return false;
+    }
+
+    public boolean areChildrenExpanded() {
+        return false;
+    }
+
+    private void updateClipping() {
+        if (mClipToActualHeight) {
+            int top = getClipTopAmount();
+            mClipRect.set(0, top, getWidth(), Math.max(getActualHeight() + getExtraBottomPadding()
+                    - mClipBottomAmount, top));
+            setClipBounds(mClipRect);
+        } else {
+            setClipBounds(null);
+        }
+    }
+
+    public void setClipToActualHeight(boolean clipToActualHeight) {
+        mClipToActualHeight = clipToActualHeight;
+        updateClipping();
+    }
+
+    public boolean willBeGone() {
+        return mWillBeGone;
+    }
+
+    public void setWillBeGone(boolean willBeGone) {
+        mWillBeGone = willBeGone;
+    }
+
+    public int getMinClipTopAmount() {
+        return mMinClipTopAmount;
+    }
+
+    public void setMinClipTopAmount(int minClipTopAmount) {
+        mMinClipTopAmount = minClipTopAmount;
+    }
+
+    @Override
+    public void setLayerType(int layerType, Paint paint) {
+        if (hasOverlappingRendering()) {
+            super.setLayerType(layerType, paint);
+        }
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        // Otherwise it will be clipped
+        return super.hasOverlappingRendering() && getActualHeight() <= getHeight();
+    }
+
+    public float getShadowAlpha() {
+        return 0.0f;
+    }
+
+    public void setShadowAlpha(float shadowAlpha) {
+    }
+
+    /**
+     * @return an amount between -1 and 1 of increased padding that this child needs. 1 means it
+     * needs a full increased padding while -1 means it needs no padding at all. For 0.0f the normal
+     * padding is applied.
+     */
+    public float getIncreasedPaddingAmount() {
+        return 0.0f;
+    }
+
+    public boolean mustStayOnScreen() {
+        return false;
+    }
+
+    public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
+            int outlineTranslation) {
+    }
+
+    public float getOutlineAlpha() {
+        return 0.0f;
+    }
+
+    public int getOutlineTranslation() {
+        return 0;
+    }
+
+    public void setChangingPosition(boolean changingPosition) {
+        mChangingPosition = changingPosition;
+    }
+
+    public boolean isChangingPosition() {
+        return mChangingPosition;
+    }
+
+    public void setTransientContainer(ViewGroup transientContainer) {
+        mTransientContainer = transientContainer;
+    }
+
+    public ViewGroup getTransientContainer() {
+        return mTransientContainer;
+    }
+
+    /**
+     * @return padding used to alter how much of the view is clipped.
+     */
+    public int getExtraBottomPadding() {
+        return 0;
+    }
+
+    /**
+     * @return true if the group's expansion state is changing, false otherwise.
+     */
+    public boolean isGroupExpansionChanging() {
+        return false;
+    }
+
+    public boolean isGroupExpanded() {
+        return false;
+    }
+
+    public boolean isChildInGroup() {
+        return false;
+    }
+
+    public void setActualHeightAnimating(boolean animating) {}
+
+    public ExpandableViewState createNewViewState(StackScrollState stackScrollState) {
+        return new ExpandableViewState();
+    }
+
+    /**
+     * @return whether the current view doesn't add height to the overall content. This means that
+     * if it is added to a list of items, it's content will still have the same height.
+     * An example is the notification shelf, that is always placed on top of another view.
+     */
+    public boolean hasNoContentHeight() {
+        return false;
+    }
+
+    /**
+     * @param inShelf whether the view is currently fully in the notification shelf.
+     */
+    public void setInShelf(boolean inShelf) {
+        mInShelf = inShelf;
+    }
+
+    public boolean isInShelf() {
+        return mInShelf;
+    }
+
+    /**
+     * @param transformingInShelf whether the view is currently transforming into the shelf in an
+     *                            animated way
+     */
+    public void setTransformingInShelf(boolean transformingInShelf) {
+        mTransformingInShelf = transformingInShelf;
+    }
+
+    public boolean isTransformingIntoShelf() {
+        return mTransformingInShelf;
+    }
+
+    public boolean isAboveShelf() {
+        return false;
+    }
+
+    /**
+     * A listener notifying when {@link #getActualHeight} changes.
+     */
+    public interface OnHeightChangedListener {
+
+        /**
+         * @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, boolean needsAnimation);
+
+        /**
+         * Called when the view is reset and therefore the height will change abruptly
+         *
+         * @param view The view which was reset.
+         */
+        void onReset(ExpandableView view);
+    }
+}
diff --git a/com/android/systemui/statusbar/FlingAnimationUtils.java b/com/android/systemui/statusbar/FlingAnimationUtils.java
new file mode 100644
index 0000000..758fb7a
--- /dev/null
+++ b/com/android/systemui/statusbar/FlingAnimationUtils.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2014 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.animation.Animator;
+import android.content.Context;
+import android.view.ViewPropertyAnimator;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+
+/**
+ * Utility class to calculate general fling animation when the finger is released.
+ */
+public class FlingAnimationUtils {
+
+    private static final float LINEAR_OUT_SLOW_IN_X2 = 0.35f;
+    private static final float LINEAR_OUT_SLOW_IN_X2_MAX = 0.68f;
+    private static final float LINEAR_OUT_FASTER_IN_X2 = 0.5f;
+    private static final float LINEAR_OUT_FASTER_IN_Y2_MIN = 0.4f;
+    private static final float LINEAR_OUT_FASTER_IN_Y2_MAX = 0.5f;
+    private static final float MIN_VELOCITY_DP_PER_SECOND = 250;
+    private static final float HIGH_VELOCITY_DP_PER_SECOND = 3000;
+
+    private static final float LINEAR_OUT_SLOW_IN_START_GRADIENT = 0.75f;
+    private final float mSpeedUpFactor;
+    private final float mY2;
+
+    private float mMinVelocityPxPerSecond;
+    private float mMaxLengthSeconds;
+    private float mHighVelocityPxPerSecond;
+    private float mLinearOutSlowInX2;
+
+    private AnimatorProperties mAnimatorProperties = new AnimatorProperties();
+    private PathInterpolator mInterpolator;
+    private float mCachedStartGradient = -1;
+    private float mCachedVelocityFactor = -1;
+
+    public FlingAnimationUtils(Context ctx, float maxLengthSeconds) {
+        this(ctx, maxLengthSeconds, 0.0f);
+    }
+
+    /**
+     * @param maxLengthSeconds the longest duration an animation can become in seconds
+     * @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards
+     *                      the end of the animation. 0 means it's at the beginning and no
+     *                      acceleration will take place.
+     */
+    public FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor) {
+        this(ctx, maxLengthSeconds, speedUpFactor, -1.0f, 1.0f);
+    }
+
+    /**
+     * @param maxLengthSeconds the longest duration an animation can become in seconds
+     * @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards
+     *                      the end of the animation. 0 means it's at the beginning and no
+     *                      acceleration will take place.
+     * @param x2 the x value to take for the second point of the bezier spline. If a value below 0
+     *           is provided, the value is automatically calculated.
+     * @param y2 the y value to take for the second point of the bezier spline
+     */
+    public FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor, float x2,
+            float y2) {
+        mMaxLengthSeconds = maxLengthSeconds;
+        mSpeedUpFactor = speedUpFactor;
+        if (x2 < 0) {
+            mLinearOutSlowInX2 = NotificationUtils.interpolate(LINEAR_OUT_SLOW_IN_X2,
+                    LINEAR_OUT_SLOW_IN_X2_MAX,
+                    mSpeedUpFactor);
+        } else {
+            mLinearOutSlowInX2 = x2;
+        }
+        mY2 = y2;
+
+        mMinVelocityPxPerSecond
+                = MIN_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density;
+        mHighVelocityPxPerSecond
+                = HIGH_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density;
+    }
+
+    /**
+     * Applies the interpolator and length to the animator, such that the fling animation is
+     * consistent with the finger motion.
+     *
+     * @param animator the animator to apply
+     * @param currValue the current value
+     * @param endValue the end value of the animator
+     * @param velocity the current velocity of the motion
+     */
+    public void apply(Animator animator, float currValue, float endValue, float velocity) {
+        apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
+    }
+
+    /**
+     * Applies the interpolator and length to the animator, such that the fling animation is
+     * consistent with the finger motion.
+     *
+     * @param animator the animator to apply
+     * @param currValue the current value
+     * @param endValue the end value of the animator
+     * @param velocity the current velocity of the motion
+     */
+    public void apply(ViewPropertyAnimator animator, float currValue, float endValue,
+            float velocity) {
+        apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
+    }
+
+    /**
+     * Applies the interpolator and length to the animator, such that the fling animation is
+     * consistent with the finger motion.
+     *
+     * @param animator the animator to apply
+     * @param currValue the current value
+     * @param endValue the end value of the animator
+     * @param velocity the current velocity of the motion
+     * @param maxDistance the maximum distance for this interaction; the maximum animation length
+     *                    gets multiplied by the ratio between the actual distance and this value
+     */
+    public void apply(Animator animator, float currValue, float endValue, float velocity,
+            float maxDistance) {
+        AnimatorProperties properties = getProperties(currValue, endValue, velocity,
+                maxDistance);
+        animator.setDuration(properties.duration);
+        animator.setInterpolator(properties.interpolator);
+    }
+
+    /**
+     * Applies the interpolator and length to the animator, such that the fling animation is
+     * consistent with the finger motion.
+     *
+     * @param animator the animator to apply
+     * @param currValue the current value
+     * @param endValue the end value of the animator
+     * @param velocity the current velocity of the motion
+     * @param maxDistance the maximum distance for this interaction; the maximum animation length
+     *                    gets multiplied by the ratio between the actual distance and this value
+     */
+    public void apply(ViewPropertyAnimator animator, float currValue, float endValue,
+            float velocity, float maxDistance) {
+        AnimatorProperties properties = getProperties(currValue, endValue, velocity,
+                maxDistance);
+        animator.setDuration(properties.duration);
+        animator.setInterpolator(properties.interpolator);
+    }
+
+    private AnimatorProperties getProperties(float currValue,
+            float endValue, float velocity, float maxDistance) {
+        float maxLengthSeconds = (float) (mMaxLengthSeconds
+                * Math.sqrt(Math.abs(endValue - currValue) / maxDistance));
+        float diff = Math.abs(endValue - currValue);
+        float velAbs = Math.abs(velocity);
+        float velocityFactor = mSpeedUpFactor == 0.0f
+                ? 1.0f : Math.min(velAbs / HIGH_VELOCITY_DP_PER_SECOND, 1.0f);
+        float startGradient = NotificationUtils.interpolate(LINEAR_OUT_SLOW_IN_START_GRADIENT,
+                mY2 / mLinearOutSlowInX2, velocityFactor);
+        float durationSeconds = startGradient * diff / velAbs;
+        Interpolator slowInInterpolator = getInterpolator(startGradient, velocityFactor);
+        if (durationSeconds <= maxLengthSeconds) {
+            mAnimatorProperties.interpolator = slowInInterpolator;
+        } else if (velAbs >= mMinVelocityPxPerSecond) {
+
+            // Cross fade between fast-out-slow-in and linear interpolator with current velocity.
+            durationSeconds = maxLengthSeconds;
+            VelocityInterpolator velocityInterpolator
+                    = new VelocityInterpolator(durationSeconds, velAbs, diff);
+            InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator(
+                    velocityInterpolator, slowInInterpolator, Interpolators.LINEAR_OUT_SLOW_IN);
+            mAnimatorProperties.interpolator = superInterpolator;
+        } else {
+
+            // Just use a normal interpolator which doesn't take the velocity into account.
+            durationSeconds = maxLengthSeconds;
+            mAnimatorProperties.interpolator = Interpolators.FAST_OUT_SLOW_IN;
+        }
+        mAnimatorProperties.duration = (long) (durationSeconds * 1000);
+        return mAnimatorProperties;
+    }
+
+    private Interpolator getInterpolator(float startGradient, float velocityFactor) {
+        if (startGradient != mCachedStartGradient
+                || velocityFactor != mCachedVelocityFactor) {
+            float speedup = mSpeedUpFactor * (1.0f - velocityFactor);
+            mInterpolator = new PathInterpolator(speedup,
+                    speedup * startGradient,
+                    mLinearOutSlowInX2, mY2);
+            mCachedStartGradient = startGradient;
+            mCachedVelocityFactor = velocityFactor;
+        }
+        return mInterpolator;
+    }
+
+    /**
+     * Applies the interpolator and length to the animator, such that the fling animation is
+     * consistent with the finger motion for the case when the animation is making something
+     * disappear.
+     *
+     * @param animator the animator to apply
+     * @param currValue the current value
+     * @param endValue the end value of the animator
+     * @param velocity the current velocity of the motion
+     * @param maxDistance the maximum distance for this interaction; the maximum animation length
+     *                    gets multiplied by the ratio between the actual distance and this value
+     */
+    public void applyDismissing(Animator animator, float currValue, float endValue,
+            float velocity, float maxDistance) {
+        AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity,
+                maxDistance);
+        animator.setDuration(properties.duration);
+        animator.setInterpolator(properties.interpolator);
+    }
+
+    /**
+     * Applies the interpolator and length to the animator, such that the fling animation is
+     * consistent with the finger motion for the case when the animation is making something
+     * disappear.
+     *
+     * @param animator the animator to apply
+     * @param currValue the current value
+     * @param endValue the end value of the animator
+     * @param velocity the current velocity of the motion
+     * @param maxDistance the maximum distance for this interaction; the maximum animation length
+     *                    gets multiplied by the ratio between the actual distance and this value
+     */
+    public void applyDismissing(ViewPropertyAnimator animator, float currValue, float endValue,
+            float velocity, float maxDistance) {
+        AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity,
+                maxDistance);
+        animator.setDuration(properties.duration);
+        animator.setInterpolator(properties.interpolator);
+    }
+
+    private AnimatorProperties getDismissingProperties(float currValue, float endValue,
+            float velocity, float maxDistance) {
+        float maxLengthSeconds = (float) (mMaxLengthSeconds
+                * Math.pow(Math.abs(endValue - currValue) / maxDistance, 0.5f));
+        float diff = Math.abs(endValue - currValue);
+        float velAbs = Math.abs(velocity);
+        float y2 = calculateLinearOutFasterInY2(velAbs);
+
+        float startGradient = y2 / LINEAR_OUT_FASTER_IN_X2;
+        Interpolator mLinearOutFasterIn = new PathInterpolator(0, 0, LINEAR_OUT_FASTER_IN_X2, y2);
+        float durationSeconds = startGradient * diff / velAbs;
+        if (durationSeconds <= maxLengthSeconds) {
+            mAnimatorProperties.interpolator = mLinearOutFasterIn;
+        } else if (velAbs >= mMinVelocityPxPerSecond) {
+
+            // Cross fade between linear-out-faster-in and linear interpolator with current
+            // velocity.
+            durationSeconds = maxLengthSeconds;
+            VelocityInterpolator velocityInterpolator
+                    = new VelocityInterpolator(durationSeconds, velAbs, diff);
+            InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator(
+                    velocityInterpolator, mLinearOutFasterIn, Interpolators.LINEAR_OUT_SLOW_IN);
+            mAnimatorProperties.interpolator = superInterpolator;
+        } else {
+
+            // Just use a normal interpolator which doesn't take the velocity into account.
+            durationSeconds = maxLengthSeconds;
+            mAnimatorProperties.interpolator = Interpolators.FAST_OUT_LINEAR_IN;
+        }
+        mAnimatorProperties.duration = (long) (durationSeconds * 1000);
+        return mAnimatorProperties;
+    }
+
+    /**
+     * Calculates the y2 control point for a linear-out-faster-in path interpolator depending on the
+     * velocity. The faster the velocity, the more "linear" the interpolator gets.
+     *
+     * @param velocity the velocity of the gesture.
+     * @return the y2 control point for a cubic bezier path interpolator
+     */
+    private float calculateLinearOutFasterInY2(float velocity) {
+        float t = (velocity - mMinVelocityPxPerSecond)
+                / (mHighVelocityPxPerSecond - mMinVelocityPxPerSecond);
+        t = Math.max(0, Math.min(1, t));
+        return (1 - t) * LINEAR_OUT_FASTER_IN_Y2_MIN + t * LINEAR_OUT_FASTER_IN_Y2_MAX;
+    }
+
+    /**
+     * @return the minimum velocity a gesture needs to have to be considered a fling
+     */
+    public float getMinVelocityPxPerSecond() {
+        return mMinVelocityPxPerSecond;
+    }
+
+    /**
+     * An interpolator which interpolates two interpolators with an interpolator.
+     */
+    private static final class InterpolatorInterpolator implements Interpolator {
+
+        private Interpolator mInterpolator1;
+        private Interpolator mInterpolator2;
+        private Interpolator mCrossfader;
+
+        InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2,
+                Interpolator crossfader) {
+            mInterpolator1 = interpolator1;
+            mInterpolator2 = interpolator2;
+            mCrossfader = crossfader;
+        }
+
+        @Override
+        public float getInterpolation(float input) {
+            float t = mCrossfader.getInterpolation(input);
+            return (1 - t) * mInterpolator1.getInterpolation(input)
+                    + t * mInterpolator2.getInterpolation(input);
+        }
+    }
+
+    /**
+     * An interpolator which interpolates with a fixed velocity.
+     */
+    private static final class VelocityInterpolator implements Interpolator {
+
+        private float mDurationSeconds;
+        private float mVelocity;
+        private float mDiff;
+
+        private VelocityInterpolator(float durationSeconds, float velocity, float diff) {
+            mDurationSeconds = durationSeconds;
+            mVelocity = velocity;
+            mDiff = diff;
+        }
+
+        @Override
+        public float getInterpolation(float input) {
+            float time = input * mDurationSeconds;
+            return time * mVelocity / mDiff;
+        }
+    }
+
+    private static class AnimatorProperties {
+        Interpolator interpolator;
+        long duration;
+    }
+
+}
diff --git a/com/android/systemui/statusbar/GestureRecorder.java b/com/android/systemui/statusbar/GestureRecorder.java
new file mode 100644
index 0000000..f2adaf0
--- /dev/null
+++ b/com/android/systemui/statusbar/GestureRecorder.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2012 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.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import java.io.BufferedWriter;
+import java.io.FileDescriptor;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.LinkedList;
+
+/**
+ * Convenience class for capturing gestures for later analysis.
+ */
+public class GestureRecorder {
+    public static final boolean DEBUG = true; // for now
+    public static final String TAG = GestureRecorder.class.getSimpleName();
+
+    public class Gesture {
+        public abstract class Record {
+            long time;
+            public abstract String toJson();
+        }
+        public class MotionEventRecord extends Record {
+            public MotionEvent event;
+            public MotionEventRecord(long when, MotionEvent event) {
+                this.time = when;
+                this.event = MotionEvent.obtain(event);
+            }
+            String actionName(int action) {
+                switch (action) {
+                    case MotionEvent.ACTION_DOWN:
+                        return "down";
+                    case MotionEvent.ACTION_UP:
+                        return "up";
+                    case MotionEvent.ACTION_MOVE:
+                        return "move";
+                    case MotionEvent.ACTION_CANCEL:
+                        return "cancel";
+                    default:
+                        return String.valueOf(action);
+                }
+            }
+            public String toJson() {
+                return String.format(
+                        ("{\"type\":\"motion\", \"time\":%d, \"action\":\"%s\", "
+                            + "\"x\":%.2f, \"y\":%.2f, \"s\":%.2f, \"p\":%.2f}"),
+                        this.time,
+                        actionName(this.event.getAction()),
+                        this.event.getRawX(),
+                        this.event.getRawY(),
+                        this.event.getSize(),
+                        this.event.getPressure()
+                        );
+            }
+        }
+        public class TagRecord extends Record {
+            public String tag, info;
+            public TagRecord(long when, String tag, String info) {
+                this.time = when;
+                this.tag = tag;
+                this.info = info;
+            }
+            public String toJson() {
+                return String.format("{\"type\":\"tag\", \"time\":%d, \"tag\":\"%s\", \"info\":\"%s\"}",
+                        this.time,
+                        this.tag,
+                        this.info
+                        );
+            }
+        }
+        private LinkedList<Record> mRecords = new LinkedList<Record>();
+        private HashSet<String> mTags = new HashSet<String>();
+        long mDownTime = -1;
+        boolean mComplete = false;
+
+        public void add(MotionEvent ev) {
+            mRecords.add(new MotionEventRecord(ev.getEventTime(), ev));
+            if (mDownTime < 0) {
+                mDownTime = ev.getDownTime();
+            } else {
+                if (mDownTime != ev.getDownTime()) {
+                    Log.w(TAG, "Assertion failure in GestureRecorder: event downTime ("
+                            +ev.getDownTime()+") does not match gesture downTime ("+mDownTime+")");
+                }
+            }
+            switch (ev.getActionMasked()) {
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL:
+                    mComplete = true;
+            }
+        }
+        public void tag(long when, String tag, String info) {
+            mRecords.add(new TagRecord(when, tag, info));
+            mTags.add(tag);
+        }
+        public boolean isComplete() {
+            return mComplete;
+        }
+        public String toJson() {
+            StringBuilder sb = new StringBuilder();
+            boolean first = true;
+            sb.append("[");
+            for (Record r : mRecords) {
+                if (!first) sb.append(", ");
+                first = false;
+                sb.append(r.toJson());
+            }
+            sb.append("]");
+            return sb.toString();
+        }
+    }
+
+    // -=-=-=-=-=-=-=-=-=-=-=-
+
+    static final long SAVE_DELAY = 5000; // ms
+    static final int SAVE_MESSAGE = 6351;
+
+    private LinkedList<Gesture> mGestures;
+    private Gesture mCurrentGesture;
+    private int mLastSaveLen = -1;
+    private String mLogfile;
+
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.what == SAVE_MESSAGE) {
+                save();
+            }
+        }
+    };
+
+    public GestureRecorder(String filename) {
+        mLogfile = filename;
+        mGestures = new LinkedList<Gesture>();
+        mCurrentGesture = null;
+    }
+
+    public void add(MotionEvent ev) {
+        synchronized (mGestures) {
+            if (mCurrentGesture == null || mCurrentGesture.isComplete()) {
+                mCurrentGesture = new Gesture();
+                mGestures.add(mCurrentGesture);
+            }
+            mCurrentGesture.add(ev);
+        }
+        saveLater();
+    }
+
+    public void tag(long when, String tag, String info) {
+        synchronized (mGestures) {
+            if (mCurrentGesture == null) {
+                mCurrentGesture = new Gesture();
+                mGestures.add(mCurrentGesture);
+            }
+            mCurrentGesture.tag(when, tag, info);
+        }
+        saveLater();
+    }
+
+    public void tag(long when, String tag) {
+        tag(when, tag, null);
+    }
+
+    public void tag(String tag) {
+        tag(SystemClock.uptimeMillis(), tag, null);
+    }
+
+    public void tag(String tag, String info) {
+        tag(SystemClock.uptimeMillis(), tag, info);
+    }
+
+    /**
+     * Generates a JSON string capturing all completed gestures.
+     * Not threadsafe; call with a lock.
+     */
+    public String toJsonLocked() {
+        StringBuilder sb = new StringBuilder();
+        boolean first = true;
+        sb.append("[");
+        int count = 0;
+        for (Gesture g : mGestures) {
+            if (!g.isComplete()) continue;
+            if (!first) sb.append("," );
+            first = false;
+            sb.append(g.toJson());
+            count++;
+        }
+        mLastSaveLen = count;
+        sb.append("]");
+        return sb.toString();
+    }
+
+    public String toJson() {
+        String s;
+        synchronized (mGestures) {
+            s = toJsonLocked();
+        }
+        return s;
+    }
+
+    public void saveLater() {
+        mHandler.removeMessages(SAVE_MESSAGE);
+        mHandler.sendEmptyMessageDelayed(SAVE_MESSAGE, SAVE_DELAY);
+    }
+
+    public void save() {
+        synchronized (mGestures) {
+            try {
+                BufferedWriter w = new BufferedWriter(new FileWriter(mLogfile, /*append=*/ true));
+                w.append(toJsonLocked() + "\n");
+                w.close();
+                mGestures.clear();
+                // If we have a pending gesture, push it back
+                if (mCurrentGesture != null && !mCurrentGesture.isComplete()) {
+                    mGestures.add(mCurrentGesture);
+                }
+                if (DEBUG) {
+                    Log.v(TAG, String.format("Wrote %d complete gestures to %s", mLastSaveLen, mLogfile));
+                }
+            } catch (IOException e) {
+                Log.e(TAG, String.format("Couldn't write gestures to %s", mLogfile), e);
+                mLastSaveLen = -1;
+            }
+        }
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        save();
+        if (mLastSaveLen >= 0) {
+            pw.println(String.valueOf(mLastSaveLen) + " gestures written to " + mLogfile);
+        } else {
+            pw.println("error writing gestures");
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/InflationTask.java b/com/android/systemui/statusbar/InflationTask.java
new file mode 100644
index 0000000..22fd37c
--- /dev/null
+++ b/com/android/systemui/statusbar/InflationTask.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 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;
+
+/**
+ * An interface for running inflation tasks that allows aborting and superseding existing
+ * operations.
+ */
+public interface InflationTask {
+    void abort();
+
+    /**
+     * Supersedes an existing task. i.e another task was superceeded by this.
+     *
+     * @param task the task that was previously running
+     */
+    default void supersedeTask(InflationTask task) {}
+}
diff --git a/com/android/systemui/statusbar/KeyboardShortcutAppItemLayout.java b/com/android/systemui/statusbar/KeyboardShortcutAppItemLayout.java
new file mode 100644
index 0000000..5377dee
--- /dev/null
+++ b/com/android/systemui/statusbar/KeyboardShortcutAppItemLayout.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 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.view.View;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+
+/**
+ * Layout used for displaying keyboard shortcut items inside an alert dialog.
+ * The layout sets the maxWidth of shortcuts keyword textview to 70% of available space.
+ */
+public class KeyboardShortcutAppItemLayout extends RelativeLayout {
+
+    private static final double MAX_WIDTH_PERCENT_FOR_KEYWORDS = 0.70;
+
+    public KeyboardShortcutAppItemLayout(Context context) {
+        super(context);
+    }
+
+    public KeyboardShortcutAppItemLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
+            ImageView shortcutIcon = findViewById(R.id.keyboard_shortcuts_icon);
+            TextView shortcutKeyword = findViewById(R.id.keyboard_shortcuts_keyword);
+            int totalMeasuredWidth = MeasureSpec.getSize(widthMeasureSpec);
+            int totalPadding = getPaddingLeft() + getPaddingRight();
+            int availableWidth = totalMeasuredWidth - totalPadding;
+            if (shortcutIcon.getVisibility() == View.VISIBLE) {
+                availableWidth = availableWidth - shortcutIcon.getMeasuredWidth();
+            }
+            shortcutKeyword.setMaxWidth((int)
+                    Math.round(availableWidth * MAX_WIDTH_PERCENT_FOR_KEYWORDS));
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+}
diff --git a/com/android/systemui/statusbar/KeyboardShortcutKeysLayout.java b/com/android/systemui/statusbar/KeyboardShortcutKeysLayout.java
new file mode 100644
index 0000000..6746a67
--- /dev/null
+++ b/com/android/systemui/statusbar/KeyboardShortcutKeysLayout.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2016 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.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Layout used as a container for keyboard shortcut keys. It's children are wrapped and right
+ * aligned.
+ */
+public final class KeyboardShortcutKeysLayout extends ViewGroup {
+    private int mLineHeight;
+    private final Context mContext;
+
+    public KeyboardShortcutKeysLayout(Context context) {
+        super(context);
+        this.mContext = context;
+    }
+
+    public KeyboardShortcutKeysLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        this.mContext = context;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
+        int childCount = getChildCount();
+        int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
+        int lineHeight = 0;
+        int xPos = getPaddingLeft();
+        int yPos = getPaddingTop();
+
+        int childHeightMeasureSpec;
+        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
+            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
+        } else {
+            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+        }
+
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
+                child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
+                        childHeightMeasureSpec);
+                int childWidth = child.getMeasuredWidth();
+                lineHeight = Math.max(lineHeight,
+                        child.getMeasuredHeight() + layoutParams.mVerticalSpacing);
+
+                if (xPos + childWidth > width) {
+                    xPos = getPaddingLeft();
+                    yPos += lineHeight;
+                }
+                xPos += childWidth + layoutParams.mHorizontalSpacing;
+            }
+        }
+        this.mLineHeight = lineHeight;
+
+        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
+            height = yPos + lineHeight;
+        } else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
+            if (yPos + lineHeight < height) {
+                height = yPos + lineHeight;
+            }
+        }
+        setMeasuredDimension(width, height);
+    }
+
+    @Override
+    protected LayoutParams generateDefaultLayoutParams() {
+        int spacing = getHorizontalVerticalSpacing();
+        return new LayoutParams(spacing, spacing);
+    }
+
+    @Override
+    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams layoutParams) {
+        int spacing = getHorizontalVerticalSpacing();
+        return new LayoutParams(spacing, spacing, layoutParams);
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return (p instanceof LayoutParams);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        int childCount = getChildCount();
+        int fullRowWidth = r - l;
+        int xPos = isRTL()
+                ? fullRowWidth - getPaddingRight()
+                : getPaddingLeft();
+        int yPos = getPaddingTop();
+        int lastHorizontalSpacing = 0;
+        // The index of the child which starts the current row.
+        int rowStartIdx = 0;
+
+        // Go through all the children.
+        for (int i = 0; i < childCount; i++) {
+            View currentChild = getChildAt(i);
+            if (currentChild.getVisibility() != GONE) {
+                int currentChildWidth = currentChild.getMeasuredWidth();
+                LayoutParams lp = (LayoutParams) currentChild.getLayoutParams();
+
+                boolean childDoesNotFitOnRow = isRTL()
+                        ? xPos - getPaddingLeft() - currentChildWidth < 0
+                        : xPos + currentChildWidth > fullRowWidth;
+
+                if (childDoesNotFitOnRow) {
+                    // Layout all the children on this row but the current one.
+                    layoutChildrenOnRow(rowStartIdx, i, fullRowWidth, xPos, yPos,
+                            lastHorizontalSpacing);
+                    // Update the positions for starting on the new row.
+                    xPos = isRTL()
+                            ? fullRowWidth - getPaddingRight()
+                            : getPaddingLeft();
+                    yPos += mLineHeight;
+                    rowStartIdx = i;
+                }
+
+                xPos = isRTL()
+                        ? xPos - currentChildWidth - lp.mHorizontalSpacing
+                        : xPos + currentChildWidth + lp.mHorizontalSpacing;
+                lastHorizontalSpacing = lp.mHorizontalSpacing;
+            }
+        }
+
+        // Lay out the children on the last row.
+        if (rowStartIdx < childCount) {
+            layoutChildrenOnRow(rowStartIdx, childCount, fullRowWidth, xPos, yPos,
+                    lastHorizontalSpacing);
+        }
+    }
+
+    private int getHorizontalVerticalSpacing() {
+        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+        return (int) TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_DIP, 4, displayMetrics);
+    }
+
+    private void layoutChildrenOnRow(int startIndex, int endIndex, int fullRowWidth, int xPos,
+            int yPos, int lastHorizontalSpacing) {
+        if (!isRTL()) {
+            xPos = getPaddingLeft() + fullRowWidth - xPos + lastHorizontalSpacing;
+        }
+
+        for (int j = startIndex; j < endIndex; ++j) {
+            View currentChild = getChildAt(j);
+            int currentChildWidth = currentChild.getMeasuredWidth();
+            LayoutParams lp = (LayoutParams) currentChild.getLayoutParams();
+            if (isRTL() && j == startIndex) {
+                xPos = fullRowWidth - xPos - getPaddingRight() - currentChildWidth
+                        - lp.mHorizontalSpacing;
+            }
+
+            currentChild.layout(
+                    xPos,
+                    yPos,
+                    xPos + currentChildWidth,
+                    yPos + currentChild.getMeasuredHeight());
+
+            if (isRTL()) {
+                int nextChildWidth = j < endIndex - 1
+                        ? getChildAt(j + 1).getMeasuredWidth()
+                        : 0;
+                xPos -= nextChildWidth + lp.mHorizontalSpacing;
+            } else {
+                xPos += currentChildWidth + lp.mHorizontalSpacing;
+            }
+        }
+    }
+
+    private boolean isRTL() {
+        return mContext.getResources().getConfiguration().getLayoutDirection()
+                == View.LAYOUT_DIRECTION_RTL;
+    }
+
+    public static class LayoutParams extends ViewGroup.LayoutParams {
+        public final int mHorizontalSpacing;
+        public final int mVerticalSpacing;
+
+        public LayoutParams(int horizontalSpacing, int verticalSpacing,
+                ViewGroup.LayoutParams viewGroupLayout) {
+            super(viewGroupLayout);
+            this.mHorizontalSpacing = horizontalSpacing;
+            this.mVerticalSpacing = verticalSpacing;
+        }
+
+        public LayoutParams(int mHorizontalSpacing, int verticalSpacing) {
+            super(0, 0);
+            this.mHorizontalSpacing = mHorizontalSpacing;
+            this.mVerticalSpacing = verticalSpacing;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/KeyboardShortcuts.java b/com/android/systemui/statusbar/KeyboardShortcuts.java
new file mode 100644
index 0000000..d370a63
--- /dev/null
+++ b/com/android/systemui/statusbar/KeyboardShortcuts.java
@@ -0,0 +1,781 @@
+/*
+ * Copyright (C) 2016 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AlertDialog;
+import android.app.AppGlobals;
+import android.app.Dialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Icon;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.hardware.input.InputManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.ContextThemeWrapper;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
+import android.view.KeyboardShortcutInfo;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager.KeyboardShortcutsReceiver;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.internal.app.AssistUtils;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.settingslib.Utils;
+import com.android.systemui.R;
+import com.android.systemui.recents.misc.SystemServicesProxy;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import static android.content.Context.LAYOUT_INFLATER_SERVICE;
+import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
+import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
+
+/**
+ * Contains functionality for handling keyboard shortcuts.
+ */
+public final class KeyboardShortcuts {
+    private static final String TAG = KeyboardShortcuts.class.getSimpleName();
+    private static final Object sLock = new Object();
+    private static KeyboardShortcuts sInstance;
+
+    private final SparseArray<String> mSpecialCharacterNames = new SparseArray<>();
+    private final SparseArray<String> mModifierNames = new SparseArray<>();
+    private final SparseArray<Drawable> mSpecialCharacterDrawables = new SparseArray<>();
+    private final SparseArray<Drawable> mModifierDrawables = new SparseArray<>();
+    // Ordered list of modifiers that are supported. All values in this array must exist in
+    // mModifierNames.
+    private final int[] mModifierList = new int[] {
+            KeyEvent.META_META_ON, KeyEvent.META_CTRL_ON, KeyEvent.META_ALT_ON,
+            KeyEvent.META_SHIFT_ON, KeyEvent.META_SYM_ON, KeyEvent.META_FUNCTION_ON
+    };
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private final Context mContext;
+    private final IPackageManager mPackageManager;
+    private final OnClickListener mDialogCloseListener = new DialogInterface.OnClickListener() {
+        public void onClick(DialogInterface dialog, int id) {
+            dismissKeyboardShortcuts();
+        }
+    };
+    private final Comparator<KeyboardShortcutInfo> mApplicationItemsComparator =
+            new Comparator<KeyboardShortcutInfo>() {
+                @Override
+                public int compare(KeyboardShortcutInfo ksh1, KeyboardShortcutInfo ksh2) {
+                    boolean ksh1ShouldBeLast = ksh1.getLabel() == null
+                            || ksh1.getLabel().toString().isEmpty();
+                    boolean ksh2ShouldBeLast = ksh2.getLabel() == null
+                            || ksh2.getLabel().toString().isEmpty();
+                    if (ksh1ShouldBeLast && ksh2ShouldBeLast) {
+                        return 0;
+                    }
+                    if (ksh1ShouldBeLast) {
+                        return 1;
+                    }
+                    if (ksh2ShouldBeLast) {
+                        return -1;
+                    }
+                    return (ksh1.getLabel().toString()).compareToIgnoreCase(
+                            ksh2.getLabel().toString());
+                }
+            };
+
+    private Dialog mKeyboardShortcutsDialog;
+    private KeyCharacterMap mKeyCharacterMap;
+    private KeyCharacterMap mBackupKeyCharacterMap;
+
+    private KeyboardShortcuts(Context context) {
+        this.mContext = new ContextThemeWrapper(context, android.R.style.Theme_DeviceDefault_Light);
+        this.mPackageManager = AppGlobals.getPackageManager();
+        loadResources(context);
+    }
+
+    private static KeyboardShortcuts getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new KeyboardShortcuts(context);
+        }
+        return sInstance;
+    }
+
+    public static void show(Context context, int deviceId) {
+        MetricsLogger.visible(context,
+                MetricsProto.MetricsEvent.KEYBOARD_SHORTCUTS_HELPER);
+        synchronized (sLock) {
+            if (sInstance != null && !sInstance.mContext.equals(context)) {
+                dismiss();
+            }
+            getInstance(context).showKeyboardShortcuts(deviceId);
+        }
+    }
+
+    public static void toggle(Context context, int deviceId) {
+        synchronized (sLock) {
+            if (isShowing()) {
+                dismiss();
+            } else {
+                show(context, deviceId);
+            }
+        }
+    }
+
+    public static void dismiss() {
+        synchronized (sLock) {
+            if (sInstance != null) {
+                MetricsLogger.hidden(sInstance.mContext,
+                        MetricsProto.MetricsEvent.KEYBOARD_SHORTCUTS_HELPER);
+                sInstance.dismissKeyboardShortcuts();
+                sInstance = null;
+            }
+        }
+    }
+
+    private static boolean isShowing() {
+        return sInstance != null && sInstance.mKeyboardShortcutsDialog != null
+                && sInstance.mKeyboardShortcutsDialog.isShowing();
+    }
+
+    private void loadResources(Context context) {
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_HOME, context.getString(R.string.keyboard_key_home));
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_BACK, context.getString(R.string.keyboard_key_back));
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_DPAD_UP, context.getString(R.string.keyboard_key_dpad_up));
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_DPAD_DOWN, context.getString(R.string.keyboard_key_dpad_down));
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_DPAD_LEFT, context.getString(R.string.keyboard_key_dpad_left));
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_DPAD_RIGHT, context.getString(R.string.keyboard_key_dpad_right));
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_DPAD_CENTER, context.getString(R.string.keyboard_key_dpad_center));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_PERIOD, ".");
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_TAB, context.getString(R.string.keyboard_key_tab));
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_SPACE, context.getString(R.string.keyboard_key_space));
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_ENTER, context.getString(R.string.keyboard_key_enter));
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_DEL, context.getString(R.string.keyboard_key_backspace));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
+                context.getString(R.string.keyboard_key_media_play_pause));
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_MEDIA_STOP, context.getString(R.string.keyboard_key_media_stop));
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_MEDIA_NEXT, context.getString(R.string.keyboard_key_media_next));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_PREVIOUS,
+                context.getString(R.string.keyboard_key_media_previous));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_REWIND,
+                context.getString(R.string.keyboard_key_media_rewind));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD,
+                context.getString(R.string.keyboard_key_media_fast_forward));
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_PAGE_UP, context.getString(R.string.keyboard_key_page_up));
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_PAGE_DOWN, context.getString(R.string.keyboard_key_page_down));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_A,
+                context.getString(R.string.keyboard_key_button_template, "A"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_B,
+                context.getString(R.string.keyboard_key_button_template, "B"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_C,
+                context.getString(R.string.keyboard_key_button_template, "C"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_X,
+                context.getString(R.string.keyboard_key_button_template, "X"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_Y,
+                context.getString(R.string.keyboard_key_button_template, "Y"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_Z,
+                context.getString(R.string.keyboard_key_button_template, "Z"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_L1,
+                context.getString(R.string.keyboard_key_button_template, "L1"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_R1,
+                context.getString(R.string.keyboard_key_button_template, "R1"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_L2,
+                context.getString(R.string.keyboard_key_button_template, "L2"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_R2,
+                context.getString(R.string.keyboard_key_button_template, "R2"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_START,
+                context.getString(R.string.keyboard_key_button_template, "Start"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_SELECT,
+                context.getString(R.string.keyboard_key_button_template, "Select"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_MODE,
+                context.getString(R.string.keyboard_key_button_template, "Mode"));
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_FORWARD_DEL, context.getString(R.string.keyboard_key_forward_del));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_ESCAPE, "Esc");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_SYSRQ, "SysRq");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BREAK, "Break");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_SCROLL_LOCK, "Scroll Lock");
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_MOVE_HOME, context.getString(R.string.keyboard_key_move_home));
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_MOVE_END, context.getString(R.string.keyboard_key_move_end));
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_INSERT, context.getString(R.string.keyboard_key_insert));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F1, "F1");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F2, "F2");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F3, "F3");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F4, "F4");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F5, "F5");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F6, "F6");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F7, "F7");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F8, "F8");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F9, "F9");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F10, "F10");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F11, "F11");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F12, "F12");
+        mSpecialCharacterNames.put(
+                KeyEvent.KEYCODE_NUM_LOCK, context.getString(R.string.keyboard_key_num_lock));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_0,
+                context.getString(R.string.keyboard_key_numpad_template, "0"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_1,
+                context.getString(R.string.keyboard_key_numpad_template, "1"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_2,
+                context.getString(R.string.keyboard_key_numpad_template, "2"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_3,
+                context.getString(R.string.keyboard_key_numpad_template, "3"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_4,
+                context.getString(R.string.keyboard_key_numpad_template, "4"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_5,
+                context.getString(R.string.keyboard_key_numpad_template, "5"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_6,
+                context.getString(R.string.keyboard_key_numpad_template, "6"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_7,
+                context.getString(R.string.keyboard_key_numpad_template, "7"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_8,
+                context.getString(R.string.keyboard_key_numpad_template, "8"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_9,
+                context.getString(R.string.keyboard_key_numpad_template, "9"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_DIVIDE,
+                context.getString(R.string.keyboard_key_numpad_template, "/"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
+                context.getString(R.string.keyboard_key_numpad_template, "*"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
+                context.getString(R.string.keyboard_key_numpad_template, "-"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_ADD,
+                context.getString(R.string.keyboard_key_numpad_template, "+"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_DOT,
+                context.getString(R.string.keyboard_key_numpad_template, "."));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_COMMA,
+                context.getString(R.string.keyboard_key_numpad_template, ","));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_ENTER,
+                context.getString(R.string.keyboard_key_numpad_template,
+                        context.getString(R.string.keyboard_key_enter)));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_EQUALS,
+                context.getString(R.string.keyboard_key_numpad_template, "="));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_LEFT_PAREN,
+                context.getString(R.string.keyboard_key_numpad_template, "("));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_RIGHT_PAREN,
+                context.getString(R.string.keyboard_key_numpad_template, ")"));
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_ZENKAKU_HANKAKU, "半角/全角");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_EISU, "英数");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_MUHENKAN, "無変換");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_HENKAN, "変換");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_KATAKANA_HIRAGANA, "かな");
+
+        mModifierNames.put(KeyEvent.META_META_ON, "Meta");
+        mModifierNames.put(KeyEvent.META_CTRL_ON, "Ctrl");
+        mModifierNames.put(KeyEvent.META_ALT_ON, "Alt");
+        mModifierNames.put(KeyEvent.META_SHIFT_ON, "Shift");
+        mModifierNames.put(KeyEvent.META_SYM_ON, "Sym");
+        mModifierNames.put(KeyEvent.META_FUNCTION_ON, "Fn");
+
+        mSpecialCharacterDrawables.put(
+                KeyEvent.KEYCODE_DEL, context.getDrawable(R.drawable.ic_ksh_key_backspace));
+        mSpecialCharacterDrawables.put(
+                KeyEvent.KEYCODE_ENTER, context.getDrawable(R.drawable.ic_ksh_key_enter));
+        mSpecialCharacterDrawables.put(
+                KeyEvent.KEYCODE_DPAD_UP, context.getDrawable(R.drawable.ic_ksh_key_up));
+        mSpecialCharacterDrawables.put(
+                KeyEvent.KEYCODE_DPAD_RIGHT, context.getDrawable(R.drawable.ic_ksh_key_right));
+        mSpecialCharacterDrawables.put(
+                KeyEvent.KEYCODE_DPAD_DOWN, context.getDrawable(R.drawable.ic_ksh_key_down));
+        mSpecialCharacterDrawables.put(
+                KeyEvent.KEYCODE_DPAD_LEFT, context.getDrawable(R.drawable.ic_ksh_key_left));
+
+        mModifierDrawables.put(
+                KeyEvent.META_META_ON, context.getDrawable(R.drawable.ic_ksh_key_meta));
+    }
+
+    /**
+     * Retrieves a {@link KeyCharacterMap} and assigns it to mKeyCharacterMap. If the given id is an
+     * existing device, that device's map is used. Otherwise, it checks first all available devices
+     * and if there is a full keyboard it uses that map, otherwise falls back to the Virtual
+     * Keyboard with its default map.
+     */
+    private void retrieveKeyCharacterMap(int deviceId) {
+        final InputManager inputManager = InputManager.getInstance();
+        mBackupKeyCharacterMap = inputManager.getInputDevice(-1).getKeyCharacterMap();
+        if (deviceId != -1) {
+            final InputDevice inputDevice = inputManager.getInputDevice(deviceId);
+            if (inputDevice != null) {
+                mKeyCharacterMap = inputDevice.getKeyCharacterMap();
+                return;
+            }
+        }
+        final int[] deviceIds = inputManager.getInputDeviceIds();
+        for (int i = 0; i < deviceIds.length; ++i) {
+            final InputDevice inputDevice = inputManager.getInputDevice(deviceIds[i]);
+            // -1 is the Virtual Keyboard, with the default key map. Use that one only as last
+            // resort.
+            if (inputDevice.getId() != -1 && inputDevice.isFullKeyboard()) {
+                mKeyCharacterMap = inputDevice.getKeyCharacterMap();
+                return;
+            }
+        }
+        // Fall back to -1, the virtual keyboard.
+        mKeyCharacterMap = mBackupKeyCharacterMap;
+    }
+
+    private void showKeyboardShortcuts(int deviceId) {
+        retrieveKeyCharacterMap(deviceId);
+        SystemServicesProxy.getInstance(mContext).requestKeyboardShortcuts(mContext,
+                new KeyboardShortcutsReceiver() {
+                    @Override
+                    public void onKeyboardShortcutsReceived(
+                            final List<KeyboardShortcutGroup> result) {
+                        result.add(getSystemShortcuts());
+                        final KeyboardShortcutGroup appShortcuts = getDefaultApplicationShortcuts();
+                        if (appShortcuts != null) {
+                            result.add(appShortcuts);
+                        }
+                        showKeyboardShortcutsDialog(result);
+                    }
+                }, deviceId);
+    }
+
+    private void dismissKeyboardShortcuts() {
+        if (mKeyboardShortcutsDialog != null) {
+            mKeyboardShortcutsDialog.dismiss();
+            mKeyboardShortcutsDialog = null;
+        }
+    }
+
+    private KeyboardShortcutGroup getSystemShortcuts() {
+        final KeyboardShortcutGroup systemGroup = new KeyboardShortcutGroup(
+                mContext.getString(R.string.keyboard_shortcut_group_system), true);
+        systemGroup.addItem(new KeyboardShortcutInfo(
+                mContext.getString(R.string.keyboard_shortcut_group_system_home),
+                KeyEvent.KEYCODE_ENTER,
+                KeyEvent.META_META_ON));
+        systemGroup.addItem(new KeyboardShortcutInfo(
+                mContext.getString(R.string.keyboard_shortcut_group_system_back),
+                KeyEvent.KEYCODE_DEL,
+                KeyEvent.META_META_ON));
+        systemGroup.addItem(new KeyboardShortcutInfo(
+                mContext.getString(R.string.keyboard_shortcut_group_system_recents),
+                KeyEvent.KEYCODE_TAB,
+                KeyEvent.META_ALT_ON));
+        systemGroup.addItem(new KeyboardShortcutInfo(
+                mContext.getString(
+                        R.string.keyboard_shortcut_group_system_notifications),
+                KeyEvent.KEYCODE_N,
+                KeyEvent.META_META_ON));
+        systemGroup.addItem(new KeyboardShortcutInfo(
+                mContext.getString(
+                        R.string.keyboard_shortcut_group_system_shortcuts_helper),
+                KeyEvent.KEYCODE_SLASH,
+                KeyEvent.META_META_ON));
+        systemGroup.addItem(new KeyboardShortcutInfo(
+                mContext.getString(
+                        R.string.keyboard_shortcut_group_system_switch_input),
+                KeyEvent.KEYCODE_SPACE,
+                KeyEvent.META_META_ON));
+        return systemGroup;
+    }
+
+    private KeyboardShortcutGroup getDefaultApplicationShortcuts() {
+        final int userId = mContext.getUserId();
+        List<KeyboardShortcutInfo> keyboardShortcutInfoAppItems = new ArrayList<>();
+
+        // Assist.
+        final AssistUtils assistUtils = new AssistUtils(mContext);
+        final ComponentName assistComponent = assistUtils.getAssistComponentForUser(userId);
+        PackageInfo assistPackageInfo = null;
+        try {
+            assistPackageInfo = mPackageManager.getPackageInfo(
+                    assistComponent.getPackageName(), 0, userId);
+        } catch (RemoteException e) {
+            Log.e(TAG, "PackageManagerService is dead");
+        }
+
+        if (assistPackageInfo != null) {
+            final Icon assistIcon = Icon.createWithResource(
+                    assistPackageInfo.applicationInfo.packageName,
+                    assistPackageInfo.applicationInfo.icon);
+
+            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_assist),
+                    assistIcon,
+                    KeyEvent.KEYCODE_UNKNOWN,
+                    KeyEvent.META_META_ON));
+        }
+
+        // Browser.
+        final Icon browserIcon = getIconForIntentCategory(Intent.CATEGORY_APP_BROWSER, userId);
+        if (browserIcon != null) {
+            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
+                    browserIcon,
+                    KeyEvent.KEYCODE_B,
+                    KeyEvent.META_META_ON));
+        }
+
+
+        // Contacts.
+        final Icon contactsIcon = getIconForIntentCategory(Intent.CATEGORY_APP_CONTACTS, userId);
+        if (contactsIcon != null) {
+            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_contacts),
+                    contactsIcon,
+                    KeyEvent.KEYCODE_C,
+                    KeyEvent.META_META_ON));
+        }
+
+        // Email.
+        final Icon emailIcon = getIconForIntentCategory(Intent.CATEGORY_APP_EMAIL, userId);
+        if (emailIcon != null) {
+            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_email),
+                    emailIcon,
+                    KeyEvent.KEYCODE_E,
+                    KeyEvent.META_META_ON));
+        }
+
+        // Messaging.
+        final Icon messagingIcon = getIconForIntentCategory(Intent.CATEGORY_APP_MESSAGING, userId);
+        if (messagingIcon != null) {
+            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_sms),
+                    messagingIcon,
+                    KeyEvent.KEYCODE_S,
+                    KeyEvent.META_META_ON));
+        }
+
+        // Music.
+        final Icon musicIcon = getIconForIntentCategory(Intent.CATEGORY_APP_MUSIC, userId);
+        if (musicIcon != null) {
+            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_music),
+                    musicIcon,
+                    KeyEvent.KEYCODE_P,
+                    KeyEvent.META_META_ON));
+        }
+
+        // Calendar.
+        final Icon calendarIcon = getIconForIntentCategory(Intent.CATEGORY_APP_CALENDAR, userId);
+        if (calendarIcon != null) {
+            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
+                    calendarIcon,
+                    KeyEvent.KEYCODE_L,
+                    KeyEvent.META_META_ON));
+        }
+
+        final int itemsSize = keyboardShortcutInfoAppItems.size();
+        if (itemsSize == 0) {
+            return null;
+        }
+
+        // Sorts by label, case insensitive with nulls and/or empty labels last.
+        Collections.sort(keyboardShortcutInfoAppItems, mApplicationItemsComparator);
+        return new KeyboardShortcutGroup(
+                mContext.getString(R.string.keyboard_shortcut_group_applications),
+                keyboardShortcutInfoAppItems,
+                true);
+    }
+
+    private Icon getIconForIntentCategory(String intentCategory, int userId) {
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.addCategory(intentCategory);
+
+        final PackageInfo packageInfo = getPackageInfoForIntent(intent, userId);
+        if (packageInfo != null && packageInfo.applicationInfo.icon != 0) {
+            return Icon.createWithResource(
+                    packageInfo.applicationInfo.packageName,
+                    packageInfo.applicationInfo.icon);
+        }
+        return null;
+    }
+
+    private PackageInfo getPackageInfoForIntent(Intent intent, int userId) {
+        try {
+            ResolveInfo handler;
+            handler = mPackageManager.resolveIntent(
+                    intent, intent.resolveTypeIfNeeded(mContext.getContentResolver()), 0, userId);
+            if (handler == null || handler.activityInfo == null) {
+                return null;
+            }
+            return mPackageManager.getPackageInfo(handler.activityInfo.packageName, 0, userId);
+        } catch (RemoteException e) {
+            Log.e(TAG, "PackageManagerService is dead", e);
+            return null;
+        }
+    }
+
+    private void showKeyboardShortcutsDialog(
+            final List<KeyboardShortcutGroup> keyboardShortcutGroups) {
+        // Need to post on the main thread.
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                handleShowKeyboardShortcuts(keyboardShortcutGroups);
+            }
+        });
+    }
+
+    private void handleShowKeyboardShortcuts(List<KeyboardShortcutGroup> keyboardShortcutGroups) {
+        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext);
+        LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+                LAYOUT_INFLATER_SERVICE);
+        final View keyboardShortcutsView = inflater.inflate(
+                R.layout.keyboard_shortcuts_view, null);
+        populateKeyboardShortcuts((LinearLayout) keyboardShortcutsView.findViewById(
+                R.id.keyboard_shortcuts_container), keyboardShortcutGroups);
+        dialogBuilder.setView(keyboardShortcutsView);
+        dialogBuilder.setPositiveButton(R.string.quick_settings_done, mDialogCloseListener);
+        mKeyboardShortcutsDialog = dialogBuilder.create();
+        mKeyboardShortcutsDialog.setCanceledOnTouchOutside(true);
+        Window keyboardShortcutsWindow = mKeyboardShortcutsDialog.getWindow();
+        keyboardShortcutsWindow.setType(TYPE_SYSTEM_DIALOG);
+        synchronized (sLock) {
+            // showKeyboardShortcutsDialog only if it has not been dismissed already
+            if (sInstance != null) {
+                mKeyboardShortcutsDialog.show();
+            }
+        }
+    }
+
+    private void populateKeyboardShortcuts(LinearLayout keyboardShortcutsLayout,
+            List<KeyboardShortcutGroup> keyboardShortcutGroups) {
+        LayoutInflater inflater = LayoutInflater.from(mContext);
+        final int keyboardShortcutGroupsSize = keyboardShortcutGroups.size();
+        TextView shortcutsKeyView = (TextView) inflater.inflate(
+                R.layout.keyboard_shortcuts_key_view, null, false);
+        shortcutsKeyView.measure(
+                View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+        final int shortcutKeyTextItemMinWidth = shortcutsKeyView.getMeasuredHeight();
+        // Needed to be able to scale the image items to the same height as the text items.
+        final int shortcutKeyIconItemHeightWidth = shortcutsKeyView.getMeasuredHeight()
+                - shortcutsKeyView.getPaddingTop()
+                - shortcutsKeyView.getPaddingBottom();
+        for (int i = 0; i < keyboardShortcutGroupsSize; i++) {
+            KeyboardShortcutGroup group = keyboardShortcutGroups.get(i);
+            TextView categoryTitle = (TextView) inflater.inflate(
+                    R.layout.keyboard_shortcuts_category_title, keyboardShortcutsLayout, false);
+            categoryTitle.setText(group.getLabel());
+            categoryTitle.setTextColor(group.isSystemGroup()
+                    ? Utils.getColorAccent(mContext)
+                    : mContext.getColor(R.color.ksh_application_group_color));
+            keyboardShortcutsLayout.addView(categoryTitle);
+
+            LinearLayout shortcutContainer = (LinearLayout) inflater.inflate(
+                    R.layout.keyboard_shortcuts_container, keyboardShortcutsLayout, false);
+            final int itemsSize = group.getItems().size();
+            for (int j = 0; j < itemsSize; j++) {
+                KeyboardShortcutInfo info = group.getItems().get(j);
+                List<StringDrawableContainer> shortcutKeys = getHumanReadableShortcutKeys(info);
+                if (shortcutKeys == null) {
+                    // Ignore shortcuts we can't display keys for.
+                    Log.w(TAG, "Keyboard Shortcut contains unsupported keys, skipping.");
+                    continue;
+                }
+                View shortcutView = inflater.inflate(R.layout.keyboard_shortcut_app_item,
+                        shortcutContainer, false);
+
+                if (info.getIcon() != null) {
+                    ImageView shortcutIcon = (ImageView) shortcutView
+                            .findViewById(R.id.keyboard_shortcuts_icon);
+                    shortcutIcon.setImageIcon(info.getIcon());
+                    shortcutIcon.setVisibility(View.VISIBLE);
+                }
+
+                TextView shortcutKeyword = (TextView) shortcutView
+                        .findViewById(R.id.keyboard_shortcuts_keyword);
+                shortcutKeyword.setText(info.getLabel());
+                if (info.getIcon() != null) {
+                    RelativeLayout.LayoutParams lp =
+                            (RelativeLayout.LayoutParams) shortcutKeyword.getLayoutParams();
+                    lp.removeRule(RelativeLayout.ALIGN_PARENT_START);
+                    shortcutKeyword.setLayoutParams(lp);
+                }
+
+                ViewGroup shortcutItemsContainer = (ViewGroup) shortcutView
+                        .findViewById(R.id.keyboard_shortcuts_item_container);
+                final int shortcutKeysSize = shortcutKeys.size();
+                for (int k = 0; k < shortcutKeysSize; k++) {
+                    StringDrawableContainer shortcutRepresentation = shortcutKeys.get(k);
+                    if (shortcutRepresentation.mDrawable != null) {
+                        ImageView shortcutKeyIconView = (ImageView) inflater.inflate(
+                                R.layout.keyboard_shortcuts_key_icon_view, shortcutItemsContainer,
+                                false);
+                        Bitmap bitmap = Bitmap.createBitmap(shortcutKeyIconItemHeightWidth,
+                                shortcutKeyIconItemHeightWidth, Bitmap.Config.ARGB_8888);
+                        Canvas canvas = new Canvas(bitmap);
+                        shortcutRepresentation.mDrawable.setBounds(0, 0, canvas.getWidth(),
+                                canvas.getHeight());
+                        shortcutRepresentation.mDrawable.draw(canvas);
+                        shortcutKeyIconView.setImageBitmap(bitmap);
+                        shortcutKeyIconView.setImportantForAccessibility(
+                                IMPORTANT_FOR_ACCESSIBILITY_YES);
+                        shortcutKeyIconView.setAccessibilityDelegate(
+                                new ShortcutKeyAccessibilityDelegate(
+                                        shortcutRepresentation.mString));
+                        shortcutItemsContainer.addView(shortcutKeyIconView);
+                    } else if (shortcutRepresentation.mString != null) {
+                        TextView shortcutKeyTextView = (TextView) inflater.inflate(
+                                R.layout.keyboard_shortcuts_key_view, shortcutItemsContainer,
+                                false);
+                        shortcutKeyTextView.setMinimumWidth(shortcutKeyTextItemMinWidth);
+                        shortcutKeyTextView.setText(shortcutRepresentation.mString);
+                        shortcutKeyTextView.setAccessibilityDelegate(
+                                new ShortcutKeyAccessibilityDelegate(
+                                        shortcutRepresentation.mString));
+                        shortcutItemsContainer.addView(shortcutKeyTextView);
+                    }
+                }
+                shortcutContainer.addView(shortcutView);
+            }
+            keyboardShortcutsLayout.addView(shortcutContainer);
+            if (i < keyboardShortcutGroupsSize - 1) {
+                View separator = inflater.inflate(
+                        R.layout.keyboard_shortcuts_category_separator, keyboardShortcutsLayout,
+                        false);
+                keyboardShortcutsLayout.addView(separator);
+            }
+        }
+    }
+
+    private List<StringDrawableContainer> getHumanReadableShortcutKeys(KeyboardShortcutInfo info) {
+        List<StringDrawableContainer> shortcutKeys = getHumanReadableModifiers(info);
+        if (shortcutKeys == null) {
+            return null;
+        }
+        String shortcutKeyString = null;
+        Drawable shortcutKeyDrawable = null;
+        if (info.getBaseCharacter() > Character.MIN_VALUE) {
+            shortcutKeyString = String.valueOf(info.getBaseCharacter());
+        } else if (mSpecialCharacterDrawables.get(info.getKeycode()) != null) {
+            shortcutKeyDrawable = mSpecialCharacterDrawables.get(info.getKeycode());
+            shortcutKeyString = mSpecialCharacterNames.get(info.getKeycode());
+        } else if (mSpecialCharacterNames.get(info.getKeycode()) != null) {
+            shortcutKeyString = mSpecialCharacterNames.get(info.getKeycode());
+        } else {
+            // Special case for shortcuts with no base key or keycode.
+            if (info.getKeycode() == KeyEvent.KEYCODE_UNKNOWN) {
+                return shortcutKeys;
+            }
+            char displayLabel = mKeyCharacterMap.getDisplayLabel(info.getKeycode());
+            if (displayLabel != 0) {
+                shortcutKeyString = String.valueOf(displayLabel);
+            } else {
+                displayLabel = mBackupKeyCharacterMap.getDisplayLabel(info.getKeycode());
+                if (displayLabel != 0) {
+                    shortcutKeyString = String.valueOf(displayLabel);
+                } else {
+                    return null;
+                }
+            }
+        }
+
+        if (shortcutKeyString != null) {
+            shortcutKeys.add(new StringDrawableContainer(shortcutKeyString, shortcutKeyDrawable));
+        } else {
+            Log.w(TAG, "Keyboard Shortcut does not have a text representation, skipping.");
+        }
+
+        return shortcutKeys;
+    }
+
+    private List<StringDrawableContainer> getHumanReadableModifiers(KeyboardShortcutInfo info) {
+        final List<StringDrawableContainer> shortcutKeys = new ArrayList<>();
+        int modifiers = info.getModifiers();
+        if (modifiers == 0) {
+            return shortcutKeys;
+        }
+        for(int i = 0; i < mModifierList.length; ++i) {
+            final int supportedModifier = mModifierList[i];
+            if ((modifiers & supportedModifier) != 0) {
+                shortcutKeys.add(new StringDrawableContainer(
+                        mModifierNames.get(supportedModifier),
+                        mModifierDrawables.get(supportedModifier)));
+                modifiers &= ~supportedModifier;
+            }
+        }
+        if (modifiers != 0) {
+            // Remaining unsupported modifiers, don't show anything.
+            return null;
+        }
+        return shortcutKeys;
+    }
+
+    private final class ShortcutKeyAccessibilityDelegate extends AccessibilityDelegate {
+        private String mContentDescription;
+
+        ShortcutKeyAccessibilityDelegate(String contentDescription) {
+            mContentDescription = contentDescription;
+        }
+
+        @Override
+        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+            super.onInitializeAccessibilityNodeInfo(host, info);
+            if (mContentDescription != null) {
+                info.setContentDescription(mContentDescription.toLowerCase());
+            }
+        }
+    }
+
+    private static final class StringDrawableContainer {
+        @NonNull
+        public String mString;
+        @Nullable
+        public Drawable mDrawable;
+
+        StringDrawableContainer(String string, Drawable drawable) {
+            mString = string;
+            mDrawable = drawable;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java b/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java
new file mode 100644
index 0000000..8a5bece
--- /dev/null
+++ b/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016 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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Receiver for the Keyboard Shortcuts Helper.
+ */
+public class KeyboardShortcutsReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS.equals(intent.getAction())) {
+            KeyboardShortcuts.show(context, -1 /* deviceId unknown */);
+        } else if (Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS.equals(intent.getAction())) {
+            KeyboardShortcuts.dismiss();
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/KeyguardAffordanceView.java b/com/android/systemui/statusbar/KeyguardAffordanceView.java
new file mode 100644
index 0000000..e12b574
--- /dev/null
+++ b/com/android/systemui/statusbar/KeyguardAffordanceView.java
@@ -0,0 +1,564 @@
+/*
+ * Copyright (C) 2014 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ArgbEvaluator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.CanvasProperty;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.DisplayListCanvas;
+import android.view.RenderNodeAnimator;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.ImageView;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.KeyguardAffordanceHelper;
+
+/**
+ * An ImageView which does not have overlapping renderings commands and therefore does not need a
+ * layer when alpha is changed.
+ */
+public class KeyguardAffordanceView extends ImageView {
+
+    private static final long CIRCLE_APPEAR_DURATION = 80;
+    private static final long CIRCLE_DISAPPEAR_MAX_DURATION = 200;
+    private static final long NORMAL_ANIMATION_DURATION = 200;
+    public static final float MAX_ICON_SCALE_AMOUNT = 1.5f;
+    public static final float MIN_ICON_SCALE_AMOUNT = 0.8f;
+
+    private final int mMinBackgroundRadius;
+    private final Paint mCirclePaint;
+    private final int mDarkIconColor;
+    private final int mNormalColor;
+    private final ArgbEvaluator mColorInterpolator;
+    private final FlingAnimationUtils mFlingAnimationUtils;
+    private float mCircleRadius;
+    private int mCenterX;
+    private int mCenterY;
+    private ValueAnimator mCircleAnimator;
+    private ValueAnimator mAlphaAnimator;
+    private ValueAnimator mScaleAnimator;
+    private float mCircleStartValue;
+    private boolean mCircleWillBeHidden;
+    private int[] mTempPoint = new int[2];
+    private float mImageScale = 1f;
+    private int mCircleColor;
+    private boolean mIsLeft;
+    private View mPreviewView;
+    private float mCircleStartRadius;
+    private float mMaxCircleSize;
+    private Animator mPreviewClipper;
+    private float mRestingAlpha = KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT;
+    private boolean mSupportHardware;
+    private boolean mFinishing;
+    private boolean mLaunchingAffordance;
+    private boolean mShouldTint = true;
+
+    private CanvasProperty<Float> mHwCircleRadius;
+    private CanvasProperty<Float> mHwCenterX;
+    private CanvasProperty<Float> mHwCenterY;
+    private CanvasProperty<Paint> mHwCirclePaint;
+
+    private AnimatorListenerAdapter mClipEndListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mPreviewClipper = null;
+        }
+    };
+    private AnimatorListenerAdapter mCircleEndListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mCircleAnimator = null;
+        }
+    };
+    private AnimatorListenerAdapter mScaleEndListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mScaleAnimator = null;
+        }
+    };
+    private AnimatorListenerAdapter mAlphaEndListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mAlphaAnimator = null;
+        }
+    };
+
+    public KeyguardAffordanceView(Context context) {
+        this(context, null);
+    }
+
+    public KeyguardAffordanceView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        TypedArray a = context.obtainStyledAttributes(attrs, android.R.styleable.ImageView);
+
+        mCirclePaint = new Paint();
+        mCirclePaint.setAntiAlias(true);
+        mCircleColor = 0xffffffff;
+        mCirclePaint.setColor(mCircleColor);
+
+        mNormalColor = a.getColor(android.R.styleable.ImageView_tint, 0xffffffff);
+        mDarkIconColor = 0xff000000;
+        mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize(
+                R.dimen.keyguard_affordance_min_background_radius);
+        mColorInterpolator = new ArgbEvaluator();
+        mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.3f);
+
+        a.recycle();
+    }
+
+    public void setImageDrawable(@Nullable Drawable drawable, boolean tint) {
+        super.setImageDrawable(drawable);
+        mShouldTint = tint;
+        updateIconColor();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        mCenterX = getWidth() / 2;
+        mCenterY = getHeight() / 2;
+        mMaxCircleSize = getMaxCircleSize();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        mSupportHardware = canvas.isHardwareAccelerated();
+        drawBackgroundCircle(canvas);
+        canvas.save();
+        canvas.scale(mImageScale, mImageScale, getWidth() / 2, getHeight() / 2);
+        super.onDraw(canvas);
+        canvas.restore();
+    }
+
+    public void setPreviewView(View v) {
+        View oldPreviewView = mPreviewView;
+        mPreviewView = v;
+        if (mPreviewView != null) {
+            mPreviewView.setVisibility(mLaunchingAffordance
+                    ? oldPreviewView.getVisibility() : INVISIBLE);
+        }
+    }
+
+    private void updateIconColor() {
+        if (!mShouldTint) return;
+        Drawable drawable = getDrawable().mutate();
+        float alpha = mCircleRadius / mMinBackgroundRadius;
+        alpha = Math.min(1.0f, alpha);
+        int color = (int) mColorInterpolator.evaluate(alpha, mNormalColor, mDarkIconColor);
+        drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
+    }
+
+    private void drawBackgroundCircle(Canvas canvas) {
+        if (mCircleRadius > 0 || mFinishing) {
+            if (mFinishing && mSupportHardware && mHwCenterX != null) {
+                // Our hardware drawing proparties can be null if the finishing started but we have
+                // never drawn before. In that case we are not doing a render thread animation
+                // anyway, so we need to use the normal drawing.
+                DisplayListCanvas displayListCanvas = (DisplayListCanvas) canvas;
+                displayListCanvas.drawCircle(mHwCenterX, mHwCenterY, mHwCircleRadius,
+                        mHwCirclePaint);
+            } else {
+                updateCircleColor();
+                canvas.drawCircle(mCenterX, mCenterY, mCircleRadius, mCirclePaint);
+            }
+        }
+    }
+
+    private void updateCircleColor() {
+        float fraction = 0.5f + 0.5f * Math.max(0.0f, Math.min(1.0f,
+                (mCircleRadius - mMinBackgroundRadius) / (0.5f * mMinBackgroundRadius)));
+        if (mPreviewView != null && mPreviewView.getVisibility() == VISIBLE) {
+            float finishingFraction = 1 - Math.max(0, mCircleRadius - mCircleStartRadius)
+                    / (mMaxCircleSize - mCircleStartRadius);
+            fraction *= finishingFraction;
+        }
+        int color = Color.argb((int) (Color.alpha(mCircleColor) * fraction),
+                Color.red(mCircleColor),
+                Color.green(mCircleColor), Color.blue(mCircleColor));
+        mCirclePaint.setColor(color);
+    }
+
+    public void finishAnimation(float velocity, final Runnable mAnimationEndRunnable) {
+        cancelAnimator(mCircleAnimator);
+        cancelAnimator(mPreviewClipper);
+        mFinishing = true;
+        mCircleStartRadius = mCircleRadius;
+        final float maxCircleSize = getMaxCircleSize();
+        Animator animatorToRadius;
+        if (mSupportHardware) {
+            initHwProperties();
+            animatorToRadius = getRtAnimatorToRadius(maxCircleSize);
+            startRtAlphaFadeIn();
+        } else {
+            animatorToRadius = getAnimatorToRadius(maxCircleSize);
+        }
+        mFlingAnimationUtils.applyDismissing(animatorToRadius, mCircleRadius, maxCircleSize,
+                velocity, maxCircleSize);
+        animatorToRadius.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mAnimationEndRunnable.run();
+                mFinishing = false;
+                mCircleRadius = maxCircleSize;
+                invalidate();
+            }
+        });
+        animatorToRadius.start();
+        setImageAlpha(0, true);
+        if (mPreviewView != null) {
+            mPreviewView.setVisibility(View.VISIBLE);
+            mPreviewClipper = ViewAnimationUtils.createCircularReveal(
+                    mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius,
+                    maxCircleSize);
+            mFlingAnimationUtils.applyDismissing(mPreviewClipper, mCircleRadius, maxCircleSize,
+                    velocity, maxCircleSize);
+            mPreviewClipper.addListener(mClipEndListener);
+            mPreviewClipper.start();
+            if (mSupportHardware) {
+                startRtCircleFadeOut(animatorToRadius.getDuration());
+            }
+        }
+    }
+
+    /**
+     * Fades in the Circle on the RenderThread. It's used when finishing the circle when it had
+     * alpha 0 in the beginning.
+     */
+    private void startRtAlphaFadeIn() {
+        if (mCircleRadius == 0 && mPreviewView == null) {
+            Paint modifiedPaint = new Paint(mCirclePaint);
+            modifiedPaint.setColor(mCircleColor);
+            modifiedPaint.setAlpha(0);
+            mHwCirclePaint = CanvasProperty.createPaint(modifiedPaint);
+            RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint,
+                    RenderNodeAnimator.PAINT_ALPHA, 255);
+            animator.setTarget(this);
+            animator.setInterpolator(Interpolators.ALPHA_IN);
+            animator.setDuration(250);
+            animator.start();
+        }
+    }
+
+    public void instantFinishAnimation() {
+        cancelAnimator(mPreviewClipper);
+        if (mPreviewView != null) {
+            mPreviewView.setClipBounds(null);
+            mPreviewView.setVisibility(View.VISIBLE);
+        }
+        mCircleRadius = getMaxCircleSize();
+        setImageAlpha(0, false);
+        invalidate();
+    }
+
+    private void startRtCircleFadeOut(long duration) {
+        RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint,
+                RenderNodeAnimator.PAINT_ALPHA, 0);
+        animator.setDuration(duration);
+        animator.setInterpolator(Interpolators.ALPHA_OUT);
+        animator.setTarget(this);
+        animator.start();
+    }
+
+    private Animator getRtAnimatorToRadius(float circleRadius) {
+        RenderNodeAnimator animator = new RenderNodeAnimator(mHwCircleRadius, circleRadius);
+        animator.setTarget(this);
+        return animator;
+    }
+
+    private void initHwProperties() {
+        mHwCenterX = CanvasProperty.createFloat(mCenterX);
+        mHwCenterY = CanvasProperty.createFloat(mCenterY);
+        mHwCirclePaint = CanvasProperty.createPaint(mCirclePaint);
+        mHwCircleRadius = CanvasProperty.createFloat(mCircleRadius);
+    }
+
+    private float getMaxCircleSize() {
+        getLocationInWindow(mTempPoint);
+        float rootWidth = getRootView().getWidth();
+        float width = mTempPoint[0] + mCenterX;
+        width = Math.max(rootWidth - width, width);
+        float height = mTempPoint[1] + mCenterY;
+        return (float) Math.hypot(width, height);
+    }
+
+    public void setCircleRadius(float circleRadius) {
+        setCircleRadius(circleRadius, false, false);
+    }
+
+    public void setCircleRadius(float circleRadius, boolean slowAnimation) {
+        setCircleRadius(circleRadius, slowAnimation, false);
+    }
+
+    public void setCircleRadiusWithoutAnimation(float circleRadius) {
+        cancelAnimator(mCircleAnimator);
+        setCircleRadius(circleRadius, false ,true);
+    }
+
+    private void setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation) {
+
+        // Check if we need a new animation
+        boolean radiusHidden = (mCircleAnimator != null && mCircleWillBeHidden)
+                || (mCircleAnimator == null && mCircleRadius == 0.0f);
+        boolean nowHidden = circleRadius == 0.0f;
+        boolean radiusNeedsAnimation = (radiusHidden != nowHidden) && !noAnimation;
+        if (!radiusNeedsAnimation) {
+            if (mCircleAnimator == null) {
+                mCircleRadius = circleRadius;
+                updateIconColor();
+                invalidate();
+                if (nowHidden) {
+                    if (mPreviewView != null) {
+                        mPreviewView.setVisibility(View.INVISIBLE);
+                    }
+                }
+            } else if (!mCircleWillBeHidden) {
+
+                // We just update the end value
+                float diff = circleRadius - mMinBackgroundRadius;
+                PropertyValuesHolder[] values = mCircleAnimator.getValues();
+                values[0].setFloatValues(mCircleStartValue + diff, circleRadius);
+                mCircleAnimator.setCurrentPlayTime(mCircleAnimator.getCurrentPlayTime());
+            }
+        } else {
+            cancelAnimator(mCircleAnimator);
+            cancelAnimator(mPreviewClipper);
+            ValueAnimator animator = getAnimatorToRadius(circleRadius);
+            Interpolator interpolator = circleRadius == 0.0f
+                    ? Interpolators.FAST_OUT_LINEAR_IN
+                    : Interpolators.LINEAR_OUT_SLOW_IN;
+            animator.setInterpolator(interpolator);
+            long duration = 250;
+            if (!slowAnimation) {
+                float durationFactor = Math.abs(mCircleRadius - circleRadius)
+                        / (float) mMinBackgroundRadius;
+                duration = (long) (CIRCLE_APPEAR_DURATION * durationFactor);
+                duration = Math.min(duration, CIRCLE_DISAPPEAR_MAX_DURATION);
+            }
+            animator.setDuration(duration);
+            animator.start();
+            if (mPreviewView != null && mPreviewView.getVisibility() == View.VISIBLE) {
+                mPreviewView.setVisibility(View.VISIBLE);
+                mPreviewClipper = ViewAnimationUtils.createCircularReveal(
+                        mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius,
+                        circleRadius);
+                mPreviewClipper.setInterpolator(interpolator);
+                mPreviewClipper.setDuration(duration);
+                mPreviewClipper.addListener(mClipEndListener);
+                mPreviewClipper.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        mPreviewView.setVisibility(View.INVISIBLE);
+                    }
+                });
+                mPreviewClipper.start();
+            }
+        }
+    }
+
+    private ValueAnimator getAnimatorToRadius(float circleRadius) {
+        ValueAnimator animator = ValueAnimator.ofFloat(mCircleRadius, circleRadius);
+        mCircleAnimator = animator;
+        mCircleStartValue = mCircleRadius;
+        mCircleWillBeHidden = circleRadius == 0.0f;
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                mCircleRadius = (float) animation.getAnimatedValue();
+                updateIconColor();
+                invalidate();
+            }
+        });
+        animator.addListener(mCircleEndListener);
+        return animator;
+    }
+
+    private void cancelAnimator(Animator animator) {
+        if (animator != null) {
+            animator.cancel();
+        }
+    }
+
+    public void setImageScale(float imageScale, boolean animate) {
+        setImageScale(imageScale, animate, -1, null);
+    }
+
+    /**
+     * Sets the scale of the containing image
+     *
+     * @param imageScale The new Scale.
+     * @param animate Should an animation be performed
+     * @param duration If animate, whats the duration? When -1 we take the default duration
+     * @param interpolator If animate, whats the interpolator? When null we take the default
+     *                     interpolator.
+     */
+    public void setImageScale(float imageScale, boolean animate, long duration,
+            Interpolator interpolator) {
+        cancelAnimator(mScaleAnimator);
+        if (!animate) {
+            mImageScale = imageScale;
+            invalidate();
+        } else {
+            ValueAnimator animator = ValueAnimator.ofFloat(mImageScale, imageScale);
+            mScaleAnimator = animator;
+            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    mImageScale = (float) animation.getAnimatedValue();
+                    invalidate();
+                }
+            });
+            animator.addListener(mScaleEndListener);
+            if (interpolator == null) {
+                interpolator = imageScale == 0.0f
+                        ? Interpolators.FAST_OUT_LINEAR_IN
+                        : Interpolators.LINEAR_OUT_SLOW_IN;
+            }
+            animator.setInterpolator(interpolator);
+            if (duration == -1) {
+                float durationFactor = Math.abs(mImageScale - imageScale)
+                        / (1.0f - MIN_ICON_SCALE_AMOUNT);
+                durationFactor = Math.min(1.0f, durationFactor);
+                duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
+            }
+            animator.setDuration(duration);
+            animator.start();
+        }
+    }
+
+    public void setRestingAlpha(float alpha) {
+        mRestingAlpha = alpha;
+
+        // TODO: Handle the case an animation is playing.
+        setImageAlpha(alpha, false);
+    }
+
+    public float getRestingAlpha() {
+        return mRestingAlpha;
+    }
+
+    public void setImageAlpha(float alpha, boolean animate) {
+        setImageAlpha(alpha, animate, -1, null, null);
+    }
+
+    /**
+     * Sets the alpha of the containing image
+     *
+     * @param alpha The new alpha.
+     * @param animate Should an animation be performed
+     * @param duration If animate, whats the duration? When -1 we take the default duration
+     * @param interpolator If animate, whats the interpolator? When null we take the default
+     *                     interpolator.
+     */
+    public void setImageAlpha(float alpha, boolean animate, long duration,
+            Interpolator interpolator, Runnable runnable) {
+        cancelAnimator(mAlphaAnimator);
+        alpha = mLaunchingAffordance ? 0 : alpha;
+        int endAlpha = (int) (alpha * 255);
+        final Drawable background = getBackground();
+        if (!animate) {
+            if (background != null) background.mutate().setAlpha(endAlpha);
+            setImageAlpha(endAlpha);
+        } else {
+            int currentAlpha = getImageAlpha();
+            ValueAnimator animator = ValueAnimator.ofInt(currentAlpha, endAlpha);
+            mAlphaAnimator = animator;
+            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    int alpha = (int) animation.getAnimatedValue();
+                    if (background != null) background.mutate().setAlpha(alpha);
+                    setImageAlpha(alpha);
+                }
+            });
+            animator.addListener(mAlphaEndListener);
+            if (interpolator == null) {
+                interpolator = alpha == 0.0f
+                        ? Interpolators.FAST_OUT_LINEAR_IN
+                        : Interpolators.LINEAR_OUT_SLOW_IN;
+            }
+            animator.setInterpolator(interpolator);
+            if (duration == -1) {
+                float durationFactor = Math.abs(currentAlpha - endAlpha) / 255f;
+                durationFactor = Math.min(1.0f, durationFactor);
+                duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
+            }
+            animator.setDuration(duration);
+            if (runnable != null) {
+                animator.addListener(getEndListener(runnable));
+            }
+            animator.start();
+        }
+    }
+
+    private Animator.AnimatorListener getEndListener(final Runnable runnable) {
+        return new AnimatorListenerAdapter() {
+            boolean mCancelled;
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mCancelled = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (!mCancelled) {
+                    runnable.run();
+                }
+            }
+        };
+    }
+
+    public float getCircleRadius() {
+        return mCircleRadius;
+    }
+
+    @Override
+    public boolean performClick() {
+        if (isClickable()) {
+            return super.performClick();
+        } else {
+            return false;
+        }
+    }
+
+    public void setLaunchingAffordance(boolean launchingAffordance) {
+        mLaunchingAffordance = launchingAffordance;
+    }
+}
diff --git a/com/android/systemui/statusbar/KeyguardIndicationController.java b/com/android/systemui/statusbar/KeyguardIndicationController.java
new file mode 100644
index 0000000..569e58d
--- /dev/null
+++ b/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -0,0 +1,529 @@
+/*
+ * Copyright (C) 2014 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.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.BatteryManager;
+import android.os.BatteryStats;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.text.format.Formatter;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.IBatteryStats;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.settingslib.Utils;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.KeyguardIndicationTextView;
+import com.android.systemui.statusbar.phone.LockIcon;
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
+import com.android.systemui.statusbar.policy.UserInfoController;
+import com.android.systemui.util.wakelock.SettableWakeLock;
+import com.android.systemui.util.wakelock.WakeLock;
+
+/**
+ * Controls the indications and error messages shown on the Keyguard
+ */
+public class KeyguardIndicationController {
+
+    private static final String TAG = "KeyguardIndication";
+    private static final boolean DEBUG_CHARGING_SPEED = false;
+
+    private static final int MSG_HIDE_TRANSIENT = 1;
+    private static final int MSG_CLEAR_FP_MSG = 2;
+    private static final long TRANSIENT_FP_ERROR_TIMEOUT = 1300;
+
+    private final Context mContext;
+    private ViewGroup mIndicationArea;
+    private KeyguardIndicationTextView mTextView;
+    private KeyguardIndicationTextView mDisclosure;
+    private final UserManager mUserManager;
+    private final IBatteryStats mBatteryInfo;
+    private final SettableWakeLock mWakeLock;
+
+    private final int mSlowThreshold;
+    private final int mFastThreshold;
+    private LockIcon mLockIcon;
+    private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+
+    private String mRestingIndication;
+    private String mTransientIndication;
+    private int mTransientTextColor;
+    private int mInitialTextColor;
+    private boolean mVisible;
+
+    private boolean mPowerPluggedIn;
+    private boolean mPowerCharged;
+    private int mChargingSpeed;
+    private int mChargingWattage;
+    private String mMessageToShowOnScreenOn;
+
+    private KeyguardUpdateMonitorCallback mUpdateMonitorCallback;
+
+    private final DevicePolicyManager mDevicePolicyManager;
+    private boolean mDozing;
+
+    /**
+     * Creates a new KeyguardIndicationController and registers callbacks.
+     */
+    public KeyguardIndicationController(Context context, ViewGroup indicationArea,
+            LockIcon lockIcon) {
+        this(context, indicationArea, lockIcon,
+                WakeLock.createPartial(context, "Doze:KeyguardIndication"));
+
+        registerCallbacks(KeyguardUpdateMonitor.getInstance(context));
+    }
+
+    /**
+     * Creates a new KeyguardIndicationController for testing. Does *not* register callbacks.
+     */
+    @VisibleForTesting
+    KeyguardIndicationController(Context context, ViewGroup indicationArea, LockIcon lockIcon,
+                WakeLock wakeLock) {
+        mContext = context;
+        mIndicationArea = indicationArea;
+        mTextView = (KeyguardIndicationTextView) indicationArea.findViewById(
+                R.id.keyguard_indication_text);
+        mInitialTextColor = mTextView != null ? mTextView.getCurrentTextColor() : Color.WHITE;
+        mDisclosure = (KeyguardIndicationTextView) indicationArea.findViewById(
+                R.id.keyguard_indication_enterprise_disclosure);
+        mLockIcon = lockIcon;
+        mWakeLock = new SettableWakeLock(wakeLock);
+
+        Resources res = context.getResources();
+        mSlowThreshold = res.getInteger(R.integer.config_chargingSlowlyThreshold);
+        mFastThreshold = res.getInteger(R.integer.config_chargingFastThreshold);
+
+        mUserManager = context.getSystemService(UserManager.class);
+        mBatteryInfo = IBatteryStats.Stub.asInterface(
+                ServiceManager.getService(BatteryStats.SERVICE_NAME));
+
+        mDevicePolicyManager = (DevicePolicyManager) context.getSystemService(
+                Context.DEVICE_POLICY_SERVICE);
+
+        updateDisclosure();
+    }
+
+    private void registerCallbacks(KeyguardUpdateMonitor monitor) {
+        monitor.registerCallback(getKeyguardCallback());
+
+        mContext.registerReceiverAsUser(mTickReceiver, UserHandle.SYSTEM,
+                new IntentFilter(Intent.ACTION_TIME_TICK), null,
+                Dependency.get(Dependency.TIME_TICK_HANDLER));
+    }
+
+    /**
+     * Gets the {@link KeyguardUpdateMonitorCallback} instance associated with this
+     * {@link KeyguardIndicationController}.
+     *
+     * <p>Subclasses may override this method to extend or change the callback behavior by extending
+     * the {@link BaseKeyguardCallback}.
+     *
+     * @return A KeyguardUpdateMonitorCallback. Multiple calls to this method <b>must</b> return the
+     * same instance.
+     */
+    protected KeyguardUpdateMonitorCallback getKeyguardCallback() {
+        if (mUpdateMonitorCallback == null) {
+            mUpdateMonitorCallback = new BaseKeyguardCallback();
+        }
+        return mUpdateMonitorCallback;
+    }
+
+    private void updateDisclosure() {
+        if (mDevicePolicyManager == null) {
+            return;
+        }
+
+        if (!mDozing && mDevicePolicyManager.isDeviceManaged()) {
+            final CharSequence organizationName =
+                    mDevicePolicyManager.getDeviceOwnerOrganizationName();
+            if (organizationName != null) {
+                mDisclosure.switchIndication(mContext.getResources().getString(
+                        R.string.do_disclosure_with_name, organizationName));
+            } else {
+                mDisclosure.switchIndication(R.string.do_disclosure_generic);
+            }
+            mDisclosure.setVisibility(View.VISIBLE);
+        } else {
+            mDisclosure.setVisibility(View.GONE);
+        }
+    }
+
+    public void setVisible(boolean visible) {
+        mVisible = visible;
+        mIndicationArea.setVisibility(visible ? View.VISIBLE : View.GONE);
+        if (visible) {
+            // If this is called after an error message was already shown, we should not clear it.
+            // Otherwise the error message won't be shown
+            if  (!mHandler.hasMessages(MSG_HIDE_TRANSIENT)) {
+                hideTransientIndication();
+            }
+            updateIndication();
+        } else if (!visible) {
+            // If we unlock and return to keyguard quickly, previous error should not be shown
+            hideTransientIndication();
+        }
+    }
+
+    /**
+     * Sets the indication that is shown if nothing else is showing.
+     */
+    public void setRestingIndication(String restingIndication) {
+        mRestingIndication = restingIndication;
+        updateIndication();
+    }
+
+    /**
+     * Sets the active controller managing changes and callbacks to user information.
+     */
+    public void setUserInfoController(UserInfoController userInfoController) {
+    }
+
+    /**
+     * Returns the indication text indicating that trust has been granted.
+     *
+     * @return {@code null} or an empty string if a trust indication text should not be shown.
+     */
+    protected String getTrustGrantedIndication() {
+        return null;
+    }
+
+    /**
+     * Returns the indication text indicating that trust is currently being managed.
+     *
+     * @return {@code null} or an empty string if a trust managed text should not be shown.
+     */
+    protected String getTrustManagedIndication() {
+        return null;
+    }
+
+    /**
+     * Hides transient indication in {@param delayMs}.
+     */
+    public void hideTransientIndicationDelayed(long delayMs) {
+        mHandler.sendMessageDelayed(
+                mHandler.obtainMessage(MSG_HIDE_TRANSIENT), delayMs);
+    }
+
+    /**
+     * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}.
+     */
+    public void showTransientIndication(int transientIndication) {
+        showTransientIndication(mContext.getResources().getString(transientIndication));
+    }
+
+    /**
+     * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}.
+     */
+    public void showTransientIndication(String transientIndication) {
+        showTransientIndication(transientIndication, mInitialTextColor);
+    }
+
+    /**
+     * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}.
+     */
+    public void showTransientIndication(String transientIndication, int textColor) {
+        mTransientIndication = transientIndication;
+        mTransientTextColor = textColor;
+        mHandler.removeMessages(MSG_HIDE_TRANSIENT);
+        if (mDozing && !TextUtils.isEmpty(mTransientIndication)) {
+            // Make sure this doesn't get stuck and burns in. Acquire wakelock until its cleared.
+            mWakeLock.setAcquired(true);
+            hideTransientIndicationDelayed(BaseKeyguardCallback.HIDE_DELAY_MS);
+        }
+        updateIndication();
+    }
+
+    /**
+     * Hides transient indication.
+     */
+    public void hideTransientIndication() {
+        if (mTransientIndication != null) {
+            mTransientIndication = null;
+            mHandler.removeMessages(MSG_HIDE_TRANSIENT);
+            updateIndication();
+        }
+    }
+
+    protected final void updateIndication() {
+        if (TextUtils.isEmpty(mTransientIndication)) {
+            mWakeLock.setAcquired(false);
+        }
+
+        if (mVisible) {
+            // Walk down a precedence-ordered list of what indication
+            // should be shown based on user or device state
+            if (mDozing) {
+                // If we're dozing, never show a persistent indication.
+                if (!TextUtils.isEmpty(mTransientIndication)) {
+                    // When dozing we ignore any text color and use white instead, because
+                    // colors can be hard to read in low brightness.
+                    mTextView.setTextColor(Color.WHITE);
+                    mTextView.switchIndication(mTransientIndication);
+                } else {
+                    mTextView.switchIndication(null);
+                }
+                return;
+            }
+
+            KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+            int userId = KeyguardUpdateMonitor.getCurrentUser();
+            String trustGrantedIndication = getTrustGrantedIndication();
+            String trustManagedIndication = getTrustManagedIndication();
+            if (!mUserManager.isUserUnlocked(userId)) {
+                mTextView.switchIndication(com.android.internal.R.string.lockscreen_storage_locked);
+                mTextView.setTextColor(mInitialTextColor);
+            } else if (!TextUtils.isEmpty(mTransientIndication)) {
+                mTextView.switchIndication(mTransientIndication);
+                mTextView.setTextColor(mTransientTextColor);
+            } else if (!TextUtils.isEmpty(trustGrantedIndication)
+                    && updateMonitor.getUserHasTrust(userId)) {
+                mTextView.switchIndication(trustGrantedIndication);
+                mTextView.setTextColor(mInitialTextColor);
+            } else if (mPowerPluggedIn) {
+                String indication = computePowerIndication();
+                if (DEBUG_CHARGING_SPEED) {
+                    indication += ",  " + (mChargingWattage / 1000) + " mW";
+                }
+                mTextView.switchIndication(indication);
+                mTextView.setTextColor(mInitialTextColor);
+            } else if (!TextUtils.isEmpty(trustManagedIndication)
+                    && updateMonitor.getUserTrustIsManaged(userId)
+                    && !updateMonitor.getUserHasTrust(userId)) {
+                mTextView.switchIndication(trustManagedIndication);
+                mTextView.setTextColor(mInitialTextColor);
+            } else {
+                mTextView.switchIndication(mRestingIndication);
+                mTextView.setTextColor(mInitialTextColor);
+            }
+        }
+    }
+
+    private String computePowerIndication() {
+        if (mPowerCharged) {
+            return mContext.getResources().getString(R.string.keyguard_charged);
+        }
+
+        // Try fetching charging time from battery stats.
+        long chargingTimeRemaining = 0;
+        try {
+            chargingTimeRemaining = mBatteryInfo.computeChargeTimeRemaining();
+
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling IBatteryStats: ", e);
+        }
+        final boolean hasChargingTime = chargingTimeRemaining > 0;
+
+        int chargingId;
+        switch (mChargingSpeed) {
+            case KeyguardUpdateMonitor.BatteryStatus.CHARGING_FAST:
+                chargingId = hasChargingTime
+                        ? R.string.keyguard_indication_charging_time_fast
+                        : R.string.keyguard_plugged_in_charging_fast;
+                break;
+            case KeyguardUpdateMonitor.BatteryStatus.CHARGING_SLOWLY:
+                chargingId = hasChargingTime
+                        ? R.string.keyguard_indication_charging_time_slowly
+                        : R.string.keyguard_plugged_in_charging_slowly;
+                break;
+            default:
+                chargingId = hasChargingTime
+                        ? R.string.keyguard_indication_charging_time
+                        : R.string.keyguard_plugged_in;
+                break;
+        }
+
+        if (hasChargingTime) {
+            String chargingTimeFormatted = Formatter.formatShortElapsedTimeRoundingUpToMinutes(
+                    mContext, chargingTimeRemaining);
+            return mContext.getResources().getString(chargingId, chargingTimeFormatted);
+        } else {
+            return mContext.getResources().getString(chargingId);
+        }
+    }
+
+    public void setStatusBarKeyguardViewManager(
+            StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
+        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+    }
+
+    private final BroadcastReceiver mTickReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mHandler.post(() -> {
+                if (mVisible) {
+                    updateIndication();
+                }
+            });
+        }
+    };
+
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.what == MSG_HIDE_TRANSIENT) {
+                hideTransientIndication();
+            } else if (msg.what == MSG_CLEAR_FP_MSG) {
+                mLockIcon.setTransientFpError(false);
+            }
+        }
+    };
+
+    public void setDozing(boolean dozing) {
+        if (mDozing == dozing) {
+            return;
+        }
+        mDozing = dozing;
+        updateIndication();
+        updateDisclosure();
+    }
+
+    protected class BaseKeyguardCallback extends KeyguardUpdateMonitorCallback {
+        public static final int HIDE_DELAY_MS = 5000;
+        private int mLastSuccessiveErrorMessage = -1;
+
+        @Override
+        public void onRefreshBatteryInfo(KeyguardUpdateMonitor.BatteryStatus status) {
+            boolean isChargingOrFull = status.status == BatteryManager.BATTERY_STATUS_CHARGING
+                    || status.status == BatteryManager.BATTERY_STATUS_FULL;
+            boolean wasPluggedIn = mPowerPluggedIn;
+            mPowerPluggedIn = status.isPluggedIn() && isChargingOrFull;
+            mPowerCharged = status.isCharged();
+            mChargingWattage = status.maxChargingWattage;
+            mChargingSpeed = status.getChargingSpeed(mSlowThreshold, mFastThreshold);
+            updateIndication();
+            if (mDozing) {
+                if (!wasPluggedIn && mPowerPluggedIn) {
+                    showTransientIndication(computePowerIndication());
+                    hideTransientIndicationDelayed(HIDE_DELAY_MS);
+                } else if (wasPluggedIn && !mPowerPluggedIn) {
+                    hideTransientIndication();
+                }
+            }
+        }
+
+        @Override
+        public void onKeyguardVisibilityChanged(boolean showing) {
+            if (showing) {
+                updateDisclosure();
+            }
+        }
+
+        @Override
+        public void onFingerprintHelp(int msgId, String helpString) {
+            KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+            if (!updateMonitor.isUnlockingWithFingerprintAllowed()) {
+                return;
+            }
+            int errorColor = Utils.getColorError(mContext);
+            if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
+                mStatusBarKeyguardViewManager.showBouncerMessage(helpString, errorColor);
+            } else if (updateMonitor.isScreenOn()) {
+                mLockIcon.setTransientFpError(true);
+                showTransientIndication(helpString, errorColor);
+                hideTransientIndicationDelayed(TRANSIENT_FP_ERROR_TIMEOUT);
+                mHandler.removeMessages(MSG_CLEAR_FP_MSG);
+                mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CLEAR_FP_MSG),
+                        TRANSIENT_FP_ERROR_TIMEOUT);
+            }
+            // Help messages indicate that there was actually a try since the last error, so those
+            // are not two successive error messages anymore.
+            mLastSuccessiveErrorMessage = -1;
+        }
+
+        @Override
+        public void onFingerprintError(int msgId, String errString) {
+            KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+            if ((!updateMonitor.isUnlockingWithFingerprintAllowed()
+                    && msgId != FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT)
+                    || msgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
+                return;
+            }
+            int errorColor = Utils.getColorError(mContext);
+            if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
+                // When swiping up right after receiving a fingerprint error, the bouncer calls
+                // authenticate leading to the same message being shown again on the bouncer.
+                // We want to avoid this, as it may confuse the user when the message is too
+                // generic.
+                if (mLastSuccessiveErrorMessage != msgId) {
+                    mStatusBarKeyguardViewManager.showBouncerMessage(errString, errorColor);
+                }
+            } else if (updateMonitor.isScreenOn()) {
+                showTransientIndication(errString, errorColor);
+                // We want to keep this message around in case the screen was off
+                hideTransientIndicationDelayed(HIDE_DELAY_MS);
+            } else {
+                mMessageToShowOnScreenOn = errString;
+            }
+            mLastSuccessiveErrorMessage = msgId;
+        }
+
+        @Override
+        public void onScreenTurnedOn() {
+            if (mMessageToShowOnScreenOn != null) {
+                int errorColor = Utils.getColorError(mContext);
+                showTransientIndication(mMessageToShowOnScreenOn, errorColor);
+                // We want to keep this message around in case the screen was off
+                hideTransientIndicationDelayed(HIDE_DELAY_MS);
+                mMessageToShowOnScreenOn = null;
+            }
+        }
+
+        @Override
+        public void onFingerprintRunningStateChanged(boolean running) {
+            if (running) {
+                mMessageToShowOnScreenOn = null;
+            }
+        }
+
+        @Override
+        public void onFingerprintAuthenticated(int userId) {
+            super.onFingerprintAuthenticated(userId);
+            mLastSuccessiveErrorMessage = -1;
+        }
+
+        @Override
+        public void onFingerprintAuthFailed() {
+            super.onFingerprintAuthFailed();
+            mLastSuccessiveErrorMessage = -1;
+        }
+
+        @Override
+        public void onUserUnlocked() {
+            if (mVisible) {
+                updateIndication();
+            }
+        }
+    };
+}
diff --git a/com/android/systemui/statusbar/NotificationBackgroundView.java b/com/android/systemui/statusbar/NotificationBackgroundView.java
new file mode 100644
index 0000000..81a99bc
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationBackgroundView.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2014 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.content.res.ColorStateList;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.RippleDrawable;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * A view that can be used for both the dimmed and normal background of an notification.
+ */
+public class NotificationBackgroundView extends View {
+
+    private Drawable mBackground;
+    private int mClipTopAmount;
+    private int mActualHeight;
+    private int mClipBottomAmount;
+    private int mTintColor;
+
+    public NotificationBackgroundView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        draw(canvas, mBackground);
+    }
+
+    private void draw(Canvas canvas, Drawable drawable) {
+        int bottom = mActualHeight - mClipBottomAmount;
+        if (drawable != null && bottom > mClipTopAmount) {
+            drawable.setBounds(0, mClipTopAmount, getWidth(), bottom);
+            drawable.draw(canvas);
+        }
+    }
+
+    @Override
+    protected boolean verifyDrawable(Drawable who) {
+        return super.verifyDrawable(who) || who == mBackground;
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        drawableStateChanged(mBackground);
+    }
+
+    private void drawableStateChanged(Drawable d) {
+        if (d != null && d.isStateful()) {
+            d.setState(getDrawableState());
+        }
+    }
+
+    @Override
+    public void drawableHotspotChanged(float x, float y) {
+        if (mBackground != null) {
+            mBackground.setHotspot(x, y);
+        }
+    }
+
+    /**
+     * Sets a background drawable. As we need to change our bounds independently of layout, we need
+     * the notion of a background independently of the regular View background..
+     */
+    public void setCustomBackground(Drawable background) {
+        if (mBackground != null) {
+            mBackground.setCallback(null);
+            unscheduleDrawable(mBackground);
+        }
+        mBackground = background;
+        if (mBackground != null) {
+            mBackground.setCallback(this);
+            setTint(mTintColor);
+        }
+        if (mBackground instanceof RippleDrawable) {
+            ((RippleDrawable) mBackground).setForceSoftware(true);
+        }
+        invalidate();
+    }
+
+    public void setCustomBackground(int drawableResId) {
+        final Drawable d = mContext.getDrawable(drawableResId);
+        setCustomBackground(d);
+    }
+
+    public void setTint(int tintColor) {
+        if (tintColor != 0) {
+            mBackground.setColorFilter(tintColor, PorterDuff.Mode.SRC_ATOP);
+        } else {
+            mBackground.clearColorFilter();
+        }
+        mTintColor = tintColor;
+        invalidate();
+    }
+
+    public void setActualHeight(int actualHeight) {
+        mActualHeight = actualHeight;
+        invalidate();
+    }
+
+    public int getActualHeight() {
+        return mActualHeight;
+    }
+
+    public void setClipTopAmount(int clipTopAmount) {
+        mClipTopAmount = clipTopAmount;
+        invalidate();
+    }
+
+    public void setClipBottomAmount(int clipBottomAmount) {
+        mClipBottomAmount = clipBottomAmount;
+        invalidate();
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+
+        // Prevents this view from creating a layer when alpha is animating.
+        return false;
+    }
+
+    public void setState(int[] drawableState) {
+        mBackground.setState(drawableState);
+    }
+
+    public void setRippleColor(int color) {
+        if (mBackground instanceof RippleDrawable) {
+            RippleDrawable ripple = (RippleDrawable) mBackground;
+            ripple.setColor(ColorStateList.valueOf(color));
+        }
+    }
+
+    public void setDrawableAlpha(int drawableAlpha) {
+        mBackground.setAlpha(drawableAlpha);
+    }
+}
diff --git a/com/android/systemui/statusbar/NotificationContentView.java b/com/android/systemui/statusbar/NotificationContentView.java
new file mode 100644
index 0000000..9e059c8
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationContentView.java
@@ -0,0 +1,1462 @@
+/*
+ * Copyright (C) 2014 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.app.Notification;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Build;
+import android.service.notification.StatusBarNotification;
+import android.util.AttributeSet;
+import android.view.NotificationHeaderView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.NotificationColorUtil;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.notification.HybridNotificationView;
+import com.android.systemui.statusbar.notification.HybridGroupManager;
+import com.android.systemui.statusbar.notification.NotificationCustomViewWrapper;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.NotificationViewWrapper;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.policy.RemoteInputView;
+
+/**
+ * A frame layout containing the actual payload of the notification, including the contracted,
+ * expanded and heads up layout. This class is responsible for clipping the content and and
+ * switching between the expanded, contracted and the heads up view depending on its clipped size.
+ */
+public class NotificationContentView extends FrameLayout {
+
+    public static final int VISIBLE_TYPE_CONTRACTED = 0;
+    public static final int VISIBLE_TYPE_EXPANDED = 1;
+    public static final int VISIBLE_TYPE_HEADSUP = 2;
+    private static final int VISIBLE_TYPE_SINGLELINE = 3;
+    public static final int VISIBLE_TYPE_AMBIENT = 4;
+    private static final int VISIBLE_TYPE_AMBIENT_SINGLELINE = 5;
+    public static final int UNDEFINED = -1;
+
+    private final Rect mClipBounds = new Rect();
+    private final int mMinContractedHeight;
+    private final int mNotificationContentMarginEnd;
+
+    private View mContractedChild;
+    private View mExpandedChild;
+    private View mHeadsUpChild;
+    private HybridNotificationView mSingleLineView;
+    private View mAmbientChild;
+    private HybridNotificationView mAmbientSingleLineChild;
+
+    private RemoteInputView mExpandedRemoteInput;
+    private RemoteInputView mHeadsUpRemoteInput;
+
+    private NotificationViewWrapper mContractedWrapper;
+    private NotificationViewWrapper mExpandedWrapper;
+    private NotificationViewWrapper mHeadsUpWrapper;
+    private NotificationViewWrapper mAmbientWrapper;
+    private HybridGroupManager mHybridGroupManager;
+    private int mClipTopAmount;
+    private int mContentHeight;
+    private int mVisibleType = VISIBLE_TYPE_CONTRACTED;
+    private boolean mDark;
+    private boolean mAnimate;
+    private boolean mIsHeadsUp;
+    private boolean mLegacy;
+    private boolean mIsChildInGroup;
+    private int mSmallHeight;
+    private int mHeadsUpHeight;
+    private int mNotificationMaxHeight;
+    private int mNotificationAmbientHeight;
+    private StatusBarNotification mStatusBarNotification;
+    private NotificationGroupManager mGroupManager;
+    private RemoteInputController mRemoteInputController;
+    private Runnable mExpandedVisibleListener;
+
+    private final ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener
+            = new ViewTreeObserver.OnPreDrawListener() {
+        @Override
+        public boolean onPreDraw() {
+            // We need to post since we don't want the notification to animate on the very first
+            // frame
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    mAnimate = true;
+                }
+            });
+            getViewTreeObserver().removeOnPreDrawListener(this);
+            return true;
+        }
+    };
+
+    private OnClickListener mExpandClickListener;
+    private boolean mBeforeN;
+    private boolean mExpandable;
+    private boolean mClipToActualHeight = true;
+    private ExpandableNotificationRow mContainingNotification;
+    /** The visible type at the start of a touch driven transformation */
+    private int mTransformationStartVisibleType;
+    /** The visible type at the start of an animation driven transformation */
+    private int mAnimationStartVisibleType = UNDEFINED;
+    private boolean mUserExpanding;
+    private int mSingleLineWidthIndention;
+    private boolean mForceSelectNextLayout = true;
+    private PendingIntent mPreviousExpandedRemoteInputIntent;
+    private PendingIntent mPreviousHeadsUpRemoteInputIntent;
+    private RemoteInputView mCachedExpandedRemoteInput;
+    private RemoteInputView mCachedHeadsUpRemoteInput;
+
+    private int mContentHeightAtAnimationStart = UNDEFINED;
+    private boolean mFocusOnVisibilityChange;
+    private boolean mHeadsUpAnimatingAway;
+    private boolean mIconsVisible;
+    private int mClipBottomAmount;
+    private boolean mIsLowPriority;
+    private boolean mIsContentExpandable;
+
+
+    public NotificationContentView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mHybridGroupManager = new HybridGroupManager(getContext(), this);
+        mMinContractedHeight = getResources().getDimensionPixelSize(
+                R.dimen.min_notification_layout_height);
+        mNotificationContentMarginEnd = getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.notification_content_margin_end);
+    }
+
+    public void setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight,
+            int ambientHeight) {
+        mSmallHeight = smallHeight;
+        mHeadsUpHeight = headsUpMaxHeight;
+        mNotificationMaxHeight = maxHeight;
+        mNotificationAmbientHeight = ambientHeight;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
+        boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
+        int maxSize = Integer.MAX_VALUE;
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+        if (hasFixedHeight || isHeightLimited) {
+            maxSize = MeasureSpec.getSize(heightMeasureSpec);
+        }
+        int maxChildHeight = 0;
+        if (mExpandedChild != null) {
+            int size = Math.min(maxSize, mNotificationMaxHeight);
+            ViewGroup.LayoutParams layoutParams = mExpandedChild.getLayoutParams();
+            boolean useExactly = false;
+            if (layoutParams.height >= 0) {
+                // An actual height is set
+                size = Math.min(maxSize, layoutParams.height);
+                useExactly = true;
+            }
+            int spec = size == Integer.MAX_VALUE
+                    ? MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
+                    : MeasureSpec.makeMeasureSpec(size, useExactly
+                            ? MeasureSpec.EXACTLY
+                            : MeasureSpec.AT_MOST);
+            mExpandedChild.measure(widthMeasureSpec, spec);
+            maxChildHeight = Math.max(maxChildHeight, mExpandedChild.getMeasuredHeight());
+        }
+        if (mContractedChild != null) {
+            int heightSpec;
+            int size = Math.min(maxSize, mSmallHeight);
+            ViewGroup.LayoutParams layoutParams = mContractedChild.getLayoutParams();
+            boolean useExactly = false;
+            if (layoutParams.height >= 0) {
+                // An actual height is set
+                size = Math.min(size, layoutParams.height);
+                useExactly = true;
+            }
+            if (shouldContractedBeFixedSize() || useExactly) {
+                heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
+            } else {
+                heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
+            }
+            mContractedChild.measure(widthMeasureSpec, heightSpec);
+            int measuredHeight = mContractedChild.getMeasuredHeight();
+            if (measuredHeight < mMinContractedHeight) {
+                heightSpec = MeasureSpec.makeMeasureSpec(mMinContractedHeight, MeasureSpec.EXACTLY);
+                mContractedChild.measure(widthMeasureSpec, heightSpec);
+            }
+            maxChildHeight = Math.max(maxChildHeight, measuredHeight);
+            if (updateContractedHeaderWidth()) {
+                mContractedChild.measure(widthMeasureSpec, heightSpec);
+            }
+            if (mExpandedChild != null
+                    && mContractedChild.getMeasuredHeight() > mExpandedChild.getMeasuredHeight()) {
+                // the Expanded child is smaller then the collapsed. Let's remeasure it.
+                heightSpec = MeasureSpec.makeMeasureSpec(mContractedChild.getMeasuredHeight(),
+                        MeasureSpec.EXACTLY);
+                mExpandedChild.measure(widthMeasureSpec, heightSpec);
+            }
+        }
+        if (mHeadsUpChild != null) {
+            int size = Math.min(maxSize, mHeadsUpHeight);
+            ViewGroup.LayoutParams layoutParams = mHeadsUpChild.getLayoutParams();
+            boolean useExactly = false;
+            if (layoutParams.height >= 0) {
+                // An actual height is set
+                size = Math.min(size, layoutParams.height);
+                useExactly = true;
+            }
+            mHeadsUpChild.measure(widthMeasureSpec,
+                    MeasureSpec.makeMeasureSpec(size, useExactly ? MeasureSpec.EXACTLY
+                            : MeasureSpec.AT_MOST));
+            maxChildHeight = Math.max(maxChildHeight, mHeadsUpChild.getMeasuredHeight());
+        }
+        if (mSingleLineView != null) {
+            int singleLineWidthSpec = widthMeasureSpec;
+            if (mSingleLineWidthIndention != 0
+                    && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
+                singleLineWidthSpec = MeasureSpec.makeMeasureSpec(
+                        width - mSingleLineWidthIndention + mSingleLineView.getPaddingEnd(),
+                        MeasureSpec.EXACTLY);
+            }
+            mSingleLineView.measure(singleLineWidthSpec,
+                    MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.AT_MOST));
+            maxChildHeight = Math.max(maxChildHeight, mSingleLineView.getMeasuredHeight());
+        }
+        if (mAmbientChild != null) {
+            int size = Math.min(maxSize, mNotificationAmbientHeight);
+            ViewGroup.LayoutParams layoutParams = mAmbientChild.getLayoutParams();
+            boolean useExactly = false;
+            if (layoutParams.height >= 0) {
+                // An actual height is set
+                size = Math.min(size, layoutParams.height);
+                useExactly = true;
+            }
+            mAmbientChild.measure(widthMeasureSpec,
+                    MeasureSpec.makeMeasureSpec(size, useExactly ? MeasureSpec.EXACTLY
+                            : MeasureSpec.AT_MOST));
+            maxChildHeight = Math.max(maxChildHeight, mAmbientChild.getMeasuredHeight());
+        }
+        if (mAmbientSingleLineChild != null) {
+            int size = Math.min(maxSize, mNotificationAmbientHeight);
+            ViewGroup.LayoutParams layoutParams = mAmbientSingleLineChild.getLayoutParams();
+            boolean useExactly = false;
+            if (layoutParams.height >= 0) {
+                // An actual height is set
+                size = Math.min(size, layoutParams.height);
+                useExactly = true;
+            }
+            int ambientSingleLineWidthSpec = widthMeasureSpec;
+            if (mSingleLineWidthIndention != 0
+                    && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
+                ambientSingleLineWidthSpec = MeasureSpec.makeMeasureSpec(
+                        width - mSingleLineWidthIndention + mAmbientSingleLineChild.getPaddingEnd(),
+                        MeasureSpec.EXACTLY);
+            }
+            mAmbientSingleLineChild.measure(ambientSingleLineWidthSpec,
+                    MeasureSpec.makeMeasureSpec(size, useExactly ? MeasureSpec.EXACTLY
+                            : MeasureSpec.AT_MOST));
+            maxChildHeight = Math.max(maxChildHeight, mAmbientSingleLineChild.getMeasuredHeight());
+        }
+        int ownHeight = Math.min(maxChildHeight, maxSize);
+        setMeasuredDimension(width, ownHeight);
+    }
+
+    private boolean updateContractedHeaderWidth() {
+        // We need to update the expanded and the collapsed header to have exactly the same with to
+        // have the expand buttons laid out at the same location.
+        NotificationHeaderView contractedHeader = mContractedWrapper.getNotificationHeader();
+        if (contractedHeader != null) {
+            if (mExpandedChild != null
+                    && mExpandedWrapper.getNotificationHeader() != null) {
+                NotificationHeaderView expandedHeader = mExpandedWrapper.getNotificationHeader();
+                int expandedSize = expandedHeader.getMeasuredWidth()
+                        - expandedHeader.getPaddingEnd();
+                int collapsedSize = contractedHeader.getMeasuredWidth()
+                        - expandedHeader.getPaddingEnd();
+                if (expandedSize != collapsedSize) {
+                    int paddingEnd = contractedHeader.getMeasuredWidth() - expandedSize;
+                    contractedHeader.setPadding(
+                            contractedHeader.isLayoutRtl()
+                                    ? paddingEnd
+                                    : contractedHeader.getPaddingLeft(),
+                            contractedHeader.getPaddingTop(),
+                            contractedHeader.isLayoutRtl()
+                                    ? contractedHeader.getPaddingLeft()
+                                    : paddingEnd,
+                            contractedHeader.getPaddingBottom());
+                    contractedHeader.setShowWorkBadgeAtEnd(true);
+                    return true;
+                }
+            } else {
+                int paddingEnd = mNotificationContentMarginEnd;
+                if (contractedHeader.getPaddingEnd() != paddingEnd) {
+                    contractedHeader.setPadding(
+                            contractedHeader.isLayoutRtl()
+                                    ? paddingEnd
+                                    : contractedHeader.getPaddingLeft(),
+                            contractedHeader.getPaddingTop(),
+                            contractedHeader.isLayoutRtl()
+                                    ? contractedHeader.getPaddingLeft()
+                                    : paddingEnd,
+                            contractedHeader.getPaddingBottom());
+                    contractedHeader.setShowWorkBadgeAtEnd(false);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private boolean shouldContractedBeFixedSize() {
+        return mBeforeN && mContractedWrapper instanceof NotificationCustomViewWrapper;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        int previousHeight = 0;
+        if (mExpandedChild != null) {
+            previousHeight = mExpandedChild.getHeight();
+        }
+        super.onLayout(changed, left, top, right, bottom);
+        if (previousHeight != 0 && mExpandedChild.getHeight() != previousHeight) {
+            mContentHeightAtAnimationStart = previousHeight;
+        }
+        updateClipping();
+        invalidateOutline();
+        selectLayout(false /* animate */, mForceSelectNextLayout /* force */);
+        mForceSelectNextLayout = false;
+        updateExpandButtons(mExpandable);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        updateVisibility();
+    }
+
+    public View getContractedChild() {
+        return mContractedChild;
+    }
+
+    public View getExpandedChild() {
+        return mExpandedChild;
+    }
+
+    public View getHeadsUpChild() {
+        return mHeadsUpChild;
+    }
+
+    public View getAmbientChild() {
+        return mAmbientChild;
+    }
+
+    public HybridNotificationView getAmbientSingleLineChild() {
+        return mAmbientSingleLineChild;
+    }
+
+    public void setContractedChild(View child) {
+        if (mContractedChild != null) {
+            mContractedChild.animate().cancel();
+            removeView(mContractedChild);
+        }
+        addView(child);
+        mContractedChild = child;
+        mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child,
+                mContainingNotification);
+        mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */);
+    }
+
+    public void setExpandedChild(View child) {
+        if (mExpandedChild != null) {
+            mPreviousExpandedRemoteInputIntent = null;
+            if (mExpandedRemoteInput != null) {
+                mExpandedRemoteInput.onNotificationUpdateOrReset();
+                if (mExpandedRemoteInput.isActive()) {
+                    mPreviousExpandedRemoteInputIntent = mExpandedRemoteInput.getPendingIntent();
+                    mCachedExpandedRemoteInput = mExpandedRemoteInput;
+                    mExpandedRemoteInput.dispatchStartTemporaryDetach();
+                    ((ViewGroup)mExpandedRemoteInput.getParent()).removeView(mExpandedRemoteInput);
+                }
+            }
+            mExpandedChild.animate().cancel();
+            removeView(mExpandedChild);
+            mExpandedRemoteInput = null;
+        }
+        if (child == null) {
+            mExpandedChild = null;
+            mExpandedWrapper = null;
+            if (mVisibleType == VISIBLE_TYPE_EXPANDED) {
+                mVisibleType = VISIBLE_TYPE_CONTRACTED;
+            }
+            if (mTransformationStartVisibleType == VISIBLE_TYPE_EXPANDED) {
+                mTransformationStartVisibleType = UNDEFINED;
+            }
+            return;
+        }
+        addView(child);
+        mExpandedChild = child;
+        mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child,
+                mContainingNotification);
+    }
+
+    public void setHeadsUpChild(View child) {
+        if (mHeadsUpChild != null) {
+            mPreviousHeadsUpRemoteInputIntent = null;
+            if (mHeadsUpRemoteInput != null) {
+                mHeadsUpRemoteInput.onNotificationUpdateOrReset();
+                if (mHeadsUpRemoteInput.isActive()) {
+                    mPreviousHeadsUpRemoteInputIntent = mHeadsUpRemoteInput.getPendingIntent();
+                    mCachedHeadsUpRemoteInput = mHeadsUpRemoteInput;
+                    mHeadsUpRemoteInput.dispatchStartTemporaryDetach();
+                    ((ViewGroup)mHeadsUpRemoteInput.getParent()).removeView(mHeadsUpRemoteInput);
+                }
+            }
+            mHeadsUpChild.animate().cancel();
+            removeView(mHeadsUpChild);
+            mHeadsUpRemoteInput = null;
+        }
+        if (child == null) {
+            mHeadsUpChild = null;
+            mHeadsUpWrapper = null;
+            if (mVisibleType == VISIBLE_TYPE_HEADSUP) {
+                mVisibleType = VISIBLE_TYPE_CONTRACTED;
+            }
+            if (mTransformationStartVisibleType == VISIBLE_TYPE_HEADSUP) {
+                mTransformationStartVisibleType = UNDEFINED;
+            }
+            return;
+        }
+        addView(child);
+        mHeadsUpChild = child;
+        mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child,
+                mContainingNotification);
+    }
+
+    public void setAmbientChild(View child) {
+        if (mAmbientChild != null) {
+            mAmbientChild.animate().cancel();
+            removeView(mAmbientChild);
+        }
+        if (child == null) {
+            return;
+        }
+        addView(child);
+        mAmbientChild = child;
+        mAmbientWrapper = NotificationViewWrapper.wrap(getContext(), child,
+                mContainingNotification);
+    }
+
+    @Override
+    protected void onVisibilityChanged(View changedView, int visibility) {
+        super.onVisibilityChanged(changedView, visibility);
+        updateVisibility();
+    }
+
+    private void updateVisibility() {
+        setVisible(isShown());
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener);
+    }
+
+    private void setVisible(final boolean isVisible) {
+        if (isVisible) {
+            // This call can happen multiple times, but removing only removes a single one.
+            // We therefore need to remove the old one.
+            getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener);
+            // We only animate if we are drawn at least once, otherwise the view might animate when
+            // it's shown the first time
+            getViewTreeObserver().addOnPreDrawListener(mEnableAnimationPredrawListener);
+        } else {
+            getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener);
+            mAnimate = false;
+        }
+    }
+
+    private void focusExpandButtonIfNecessary() {
+        if (mFocusOnVisibilityChange) {
+            NotificationHeaderView header = getVisibleNotificationHeader();
+            if (header != null) {
+                ImageView expandButton = header.getExpandButton();
+                if (expandButton != null) {
+                    expandButton.requestAccessibilityFocus();
+                }
+            }
+            mFocusOnVisibilityChange = false;
+        }
+    }
+
+    public void setContentHeight(int contentHeight) {
+        mContentHeight = Math.max(Math.min(contentHeight, getHeight()), getMinHeight());
+        selectLayout(mAnimate /* animate */, false /* force */);
+
+        int minHeightHint = getMinContentHeightHint();
+
+        NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType);
+        if (wrapper != null) {
+            wrapper.setContentHeight(mContentHeight, minHeightHint);
+        }
+
+        wrapper = getVisibleWrapper(mTransformationStartVisibleType);
+        if (wrapper != null) {
+            wrapper.setContentHeight(mContentHeight, minHeightHint);
+        }
+
+        updateClipping();
+        invalidateOutline();
+    }
+
+    /**
+     * @return the minimum apparent height that the wrapper should allow for the purpose
+     *         of aligning elements at the bottom edge. If this is larger than the content
+     *         height, the notification is clipped instead of being further shrunk.
+     */
+    private int getMinContentHeightHint() {
+        if (mIsChildInGroup && isVisibleOrTransitioning(VISIBLE_TYPE_SINGLELINE)) {
+            return mContext.getResources().getDimensionPixelSize(
+                        com.android.internal.R.dimen.notification_action_list_height);
+        }
+
+        // Transition between heads-up & expanded, or pinned.
+        if (mHeadsUpChild != null && mExpandedChild != null) {
+            boolean transitioningBetweenHunAndExpanded =
+                    isTransitioningFromTo(VISIBLE_TYPE_HEADSUP, VISIBLE_TYPE_EXPANDED) ||
+                    isTransitioningFromTo(VISIBLE_TYPE_EXPANDED, VISIBLE_TYPE_HEADSUP);
+            boolean pinned = !isVisibleOrTransitioning(VISIBLE_TYPE_CONTRACTED)
+                    && (mIsHeadsUp || mHeadsUpAnimatingAway)
+                    && !mContainingNotification.isOnKeyguard();
+            if (transitioningBetweenHunAndExpanded || pinned) {
+                return Math.min(mHeadsUpChild.getHeight(), mExpandedChild.getHeight());
+            }
+        }
+
+        // Size change of the expanded version
+        if ((mVisibleType == VISIBLE_TYPE_EXPANDED) && mContentHeightAtAnimationStart >= 0
+                && mExpandedChild != null) {
+            return Math.min(mContentHeightAtAnimationStart, mExpandedChild.getHeight());
+        }
+
+        int hint;
+        if (mAmbientChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_AMBIENT)) {
+            hint = mAmbientChild.getHeight();
+        } else if (mAmbientSingleLineChild != null && isVisibleOrTransitioning(
+                VISIBLE_TYPE_AMBIENT_SINGLELINE)) {
+            hint = mAmbientSingleLineChild.getHeight();
+        } else if (mHeadsUpChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_HEADSUP)) {
+            hint = mHeadsUpChild.getHeight();
+        } else if (mExpandedChild != null) {
+            hint = mExpandedChild.getHeight();
+        } else {
+            hint = mContractedChild.getHeight() + mContext.getResources().getDimensionPixelSize(
+                    com.android.internal.R.dimen.notification_action_list_height);
+        }
+
+        if (mExpandedChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_EXPANDED)) {
+            hint = Math.min(hint, mExpandedChild.getHeight());
+        }
+        return hint;
+    }
+
+    private boolean isTransitioningFromTo(int from, int to) {
+        return (mTransformationStartVisibleType == from || mAnimationStartVisibleType == from)
+                && mVisibleType == to;
+    }
+
+    private boolean isVisibleOrTransitioning(int type) {
+        return mVisibleType == type || mTransformationStartVisibleType == type
+                || mAnimationStartVisibleType == type;
+    }
+
+    private void updateContentTransformation() {
+        int visibleType = calculateVisibleType();
+        if (visibleType != mVisibleType) {
+            // A new transformation starts
+            mTransformationStartVisibleType = mVisibleType;
+            final TransformableView shownView = getTransformableViewForVisibleType(visibleType);
+            final TransformableView hiddenView = getTransformableViewForVisibleType(
+                    mTransformationStartVisibleType);
+            shownView.transformFrom(hiddenView, 0.0f);
+            getViewForVisibleType(visibleType).setVisibility(View.VISIBLE);
+            hiddenView.transformTo(shownView, 0.0f);
+            mVisibleType = visibleType;
+            updateBackgroundColor(true /* animate */);
+        }
+        if (mForceSelectNextLayout) {
+            forceUpdateVisibilities();
+        }
+        if (mTransformationStartVisibleType != UNDEFINED
+                && mVisibleType != mTransformationStartVisibleType
+                && getViewForVisibleType(mTransformationStartVisibleType) != null) {
+            final TransformableView shownView = getTransformableViewForVisibleType(mVisibleType);
+            final TransformableView hiddenView = getTransformableViewForVisibleType(
+                    mTransformationStartVisibleType);
+            float transformationAmount = calculateTransformationAmount();
+            shownView.transformFrom(hiddenView, transformationAmount);
+            hiddenView.transformTo(shownView, transformationAmount);
+            updateBackgroundTransformation(transformationAmount);
+        } else {
+            updateViewVisibilities(visibleType);
+            updateBackgroundColor(false);
+        }
+    }
+
+    private void updateBackgroundTransformation(float transformationAmount) {
+        int endColor = getBackgroundColor(mVisibleType);
+        int startColor = getBackgroundColor(mTransformationStartVisibleType);
+        if (endColor != startColor) {
+            if (startColor == 0) {
+                startColor = mContainingNotification.getBackgroundColorWithoutTint();
+            }
+            if (endColor == 0) {
+                endColor = mContainingNotification.getBackgroundColorWithoutTint();
+            }
+            endColor = NotificationUtils.interpolateColors(startColor, endColor,
+                    transformationAmount);
+        }
+        mContainingNotification.updateBackgroundAlpha(transformationAmount);
+        mContainingNotification.setContentBackground(endColor, false, this);
+    }
+
+    private float calculateTransformationAmount() {
+        int startHeight = getViewForVisibleType(mTransformationStartVisibleType).getHeight();
+        int endHeight = getViewForVisibleType(mVisibleType).getHeight();
+        int progress = Math.abs(mContentHeight - startHeight);
+        int totalDistance = Math.abs(endHeight - startHeight);
+        float amount = (float) progress / (float) totalDistance;
+        return Math.min(1.0f, amount);
+    }
+
+    public int getContentHeight() {
+        return mContentHeight;
+    }
+
+    public int getMaxHeight() {
+        if (mContainingNotification.isShowingAmbient()) {
+            return getShowingAmbientView().getHeight();
+        } else if (mExpandedChild != null) {
+            return mExpandedChild.getHeight();
+        } else if (mIsHeadsUp && mHeadsUpChild != null && !mContainingNotification.isOnKeyguard()) {
+            return mHeadsUpChild.getHeight();
+        }
+        return mContractedChild.getHeight();
+    }
+
+    public int getMinHeight() {
+        return getMinHeight(false /* likeGroupExpanded */);
+    }
+
+    public int getMinHeight(boolean likeGroupExpanded) {
+        if (mContainingNotification.isShowingAmbient()) {
+            return getShowingAmbientView().getHeight();
+        } else if (likeGroupExpanded || !mIsChildInGroup || isGroupExpanded()) {
+            return mContractedChild.getHeight();
+        } else {
+            return mSingleLineView.getHeight();
+        }
+    }
+
+    public View getShowingAmbientView() {
+        View v = mIsChildInGroup ? mAmbientSingleLineChild : mAmbientChild;
+        if (v != null) {
+            return v;
+        } else {
+            return mContractedChild;
+        }
+    }
+
+    private boolean isGroupExpanded() {
+        return mGroupManager.isGroupExpanded(mStatusBarNotification);
+    }
+
+    public void setClipTopAmount(int clipTopAmount) {
+        mClipTopAmount = clipTopAmount;
+        updateClipping();
+    }
+
+
+    public void setClipBottomAmount(int clipBottomAmount) {
+        mClipBottomAmount = clipBottomAmount;
+        updateClipping();
+    }
+
+    @Override
+    public void setTranslationY(float translationY) {
+        super.setTranslationY(translationY);
+        updateClipping();
+    }
+
+    private void updateClipping() {
+        if (mClipToActualHeight) {
+            int top = (int) (mClipTopAmount - getTranslationY());
+            int bottom = (int) (mContentHeight - mClipBottomAmount - getTranslationY());
+            bottom = Math.max(top, bottom);
+            mClipBounds.set(0, top, getWidth(), bottom);
+            setClipBounds(mClipBounds);
+        } else {
+            setClipBounds(null);
+        }
+    }
+
+    public void setClipToActualHeight(boolean clipToActualHeight) {
+        mClipToActualHeight = clipToActualHeight;
+        updateClipping();
+    }
+
+    private void selectLayout(boolean animate, boolean force) {
+        if (mContractedChild == null) {
+            return;
+        }
+        if (mUserExpanding) {
+            updateContentTransformation();
+        } else {
+            int visibleType = calculateVisibleType();
+            boolean changedType = visibleType != mVisibleType;
+            if (changedType || force) {
+                View visibleView = getViewForVisibleType(visibleType);
+                if (visibleView != null) {
+                    visibleView.setVisibility(VISIBLE);
+                    transferRemoteInputFocus(visibleType);
+                }
+
+                if (animate && ((visibleType == VISIBLE_TYPE_EXPANDED && mExpandedChild != null)
+                        || (visibleType == VISIBLE_TYPE_HEADSUP && mHeadsUpChild != null)
+                        || (visibleType == VISIBLE_TYPE_SINGLELINE && mSingleLineView != null)
+                        || visibleType == VISIBLE_TYPE_CONTRACTED)) {
+                    animateToVisibleType(visibleType);
+                } else {
+                    updateViewVisibilities(visibleType);
+                }
+                mVisibleType = visibleType;
+                if (changedType) {
+                    focusExpandButtonIfNecessary();
+                }
+                NotificationViewWrapper visibleWrapper = getVisibleWrapper(visibleType);
+                if (visibleWrapper != null) {
+                    visibleWrapper.setContentHeight(mContentHeight, getMinContentHeightHint());
+                }
+                updateBackgroundColor(animate);
+            }
+        }
+    }
+
+    private void forceUpdateVisibilities() {
+        forceUpdateVisibility(VISIBLE_TYPE_CONTRACTED, mContractedChild, mContractedWrapper);
+        forceUpdateVisibility(VISIBLE_TYPE_EXPANDED, mExpandedChild, mExpandedWrapper);
+        forceUpdateVisibility(VISIBLE_TYPE_HEADSUP, mHeadsUpChild, mHeadsUpWrapper);
+        forceUpdateVisibility(VISIBLE_TYPE_SINGLELINE, mSingleLineView, mSingleLineView);
+        forceUpdateVisibility(VISIBLE_TYPE_AMBIENT, mAmbientChild, mAmbientWrapper);
+        forceUpdateVisibility(VISIBLE_TYPE_AMBIENT_SINGLELINE, mAmbientSingleLineChild,
+                mAmbientSingleLineChild);
+        fireExpandedVisibleListenerIfVisible();
+        // forceUpdateVisibilities cancels outstanding animations without updating the
+        // mAnimationStartVisibleType. Do so here instead.
+        mAnimationStartVisibleType = UNDEFINED;
+    }
+
+    private void fireExpandedVisibleListenerIfVisible() {
+        if (mExpandedVisibleListener != null && mExpandedChild != null && isShown()
+                && mExpandedChild.getVisibility() == VISIBLE) {
+            Runnable listener = mExpandedVisibleListener;
+            mExpandedVisibleListener = null;
+            listener.run();
+        }
+    }
+
+    private void forceUpdateVisibility(int type, View view, TransformableView wrapper) {
+        if (view == null) {
+            return;
+        }
+        boolean visible = mVisibleType == type
+                || mTransformationStartVisibleType == type;
+        if (!visible) {
+            view.setVisibility(INVISIBLE);
+        } else {
+            wrapper.setVisible(true);
+        }
+    }
+
+    public void updateBackgroundColor(boolean animate) {
+        int customBackgroundColor = getBackgroundColor(mVisibleType);
+        mContainingNotification.resetBackgroundAlpha();
+        mContainingNotification.setContentBackground(customBackgroundColor, animate, this);
+    }
+
+    public int getVisibleType() {
+        return mVisibleType;
+    }
+
+    public int getBackgroundColorForExpansionState() {
+        // When expanding or user locked we want the new type, when collapsing we want
+        // the original type
+        final int visibleType = (mContainingNotification.isGroupExpanded()
+                || mContainingNotification.isUserLocked())
+                        ? calculateVisibleType()
+                        : getVisibleType();
+        return getBackgroundColor(visibleType);
+    }
+
+    public int getBackgroundColor(int visibleType) {
+        NotificationViewWrapper currentVisibleWrapper = getVisibleWrapper(visibleType);
+        int customBackgroundColor = 0;
+        if (currentVisibleWrapper != null) {
+            customBackgroundColor = currentVisibleWrapper.getCustomBackgroundColor();
+        }
+        return customBackgroundColor;
+    }
+
+    private void updateViewVisibilities(int visibleType) {
+        updateViewVisibility(visibleType, VISIBLE_TYPE_CONTRACTED,
+                mContractedChild, mContractedWrapper);
+        updateViewVisibility(visibleType, VISIBLE_TYPE_EXPANDED,
+                mExpandedChild, mExpandedWrapper);
+        updateViewVisibility(visibleType, VISIBLE_TYPE_HEADSUP,
+                mHeadsUpChild, mHeadsUpWrapper);
+        updateViewVisibility(visibleType, VISIBLE_TYPE_SINGLELINE,
+                mSingleLineView, mSingleLineView);
+        updateViewVisibility(visibleType, VISIBLE_TYPE_AMBIENT,
+                mAmbientChild, mAmbientWrapper);
+        updateViewVisibility(visibleType, VISIBLE_TYPE_AMBIENT_SINGLELINE,
+                mAmbientSingleLineChild, mAmbientSingleLineChild);
+        fireExpandedVisibleListenerIfVisible();
+        // updateViewVisibilities cancels outstanding animations without updating the
+        // mAnimationStartVisibleType. Do so here instead.
+        mAnimationStartVisibleType = UNDEFINED;
+    }
+
+    private void updateViewVisibility(int visibleType, int type, View view,
+            TransformableView wrapper) {
+        if (view != null) {
+            wrapper.setVisible(visibleType == type);
+        }
+    }
+
+    private void animateToVisibleType(int visibleType) {
+        final TransformableView shownView = getTransformableViewForVisibleType(visibleType);
+        final TransformableView hiddenView = getTransformableViewForVisibleType(mVisibleType);
+        if (shownView == hiddenView || hiddenView == null) {
+            shownView.setVisible(true);
+            return;
+        }
+        mAnimationStartVisibleType = mVisibleType;
+        shownView.transformFrom(hiddenView);
+        getViewForVisibleType(visibleType).setVisibility(View.VISIBLE);
+        hiddenView.transformTo(shownView, new Runnable() {
+            @Override
+            public void run() {
+                if (hiddenView != getTransformableViewForVisibleType(mVisibleType)) {
+                    hiddenView.setVisible(false);
+                }
+                mAnimationStartVisibleType = UNDEFINED;
+            }
+        });
+        fireExpandedVisibleListenerIfVisible();
+    }
+
+    private void transferRemoteInputFocus(int visibleType) {
+        if (visibleType == VISIBLE_TYPE_HEADSUP
+                && mHeadsUpRemoteInput != null
+                && (mExpandedRemoteInput != null && mExpandedRemoteInput.isActive())) {
+            mHeadsUpRemoteInput.stealFocusFrom(mExpandedRemoteInput);
+        }
+        if (visibleType == VISIBLE_TYPE_EXPANDED
+                && mExpandedRemoteInput != null
+                && (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isActive())) {
+            mExpandedRemoteInput.stealFocusFrom(mHeadsUpRemoteInput);
+        }
+    }
+
+    /**
+     * @param visibleType one of the static enum types in this view
+     * @return the corresponding transformable view according to the given visible type
+     */
+    private TransformableView getTransformableViewForVisibleType(int visibleType) {
+        switch (visibleType) {
+            case VISIBLE_TYPE_EXPANDED:
+                return mExpandedWrapper;
+            case VISIBLE_TYPE_HEADSUP:
+                return mHeadsUpWrapper;
+            case VISIBLE_TYPE_SINGLELINE:
+                return mSingleLineView;
+            case VISIBLE_TYPE_AMBIENT:
+                return mAmbientWrapper;
+            case VISIBLE_TYPE_AMBIENT_SINGLELINE:
+                return mAmbientSingleLineChild;
+            default:
+                return mContractedWrapper;
+        }
+    }
+
+    /**
+     * @param visibleType one of the static enum types in this view
+     * @return the corresponding view according to the given visible type
+     */
+    private View getViewForVisibleType(int visibleType) {
+        switch (visibleType) {
+            case VISIBLE_TYPE_EXPANDED:
+                return mExpandedChild;
+            case VISIBLE_TYPE_HEADSUP:
+                return mHeadsUpChild;
+            case VISIBLE_TYPE_SINGLELINE:
+                return mSingleLineView;
+            case VISIBLE_TYPE_AMBIENT:
+                return mAmbientChild;
+            case VISIBLE_TYPE_AMBIENT_SINGLELINE:
+                return mAmbientSingleLineChild;
+            default:
+                return mContractedChild;
+        }
+    }
+
+    public NotificationViewWrapper getVisibleWrapper(int visibleType) {
+        switch (visibleType) {
+            case VISIBLE_TYPE_EXPANDED:
+                return mExpandedWrapper;
+            case VISIBLE_TYPE_HEADSUP:
+                return mHeadsUpWrapper;
+            case VISIBLE_TYPE_CONTRACTED:
+                return mContractedWrapper;
+            case VISIBLE_TYPE_AMBIENT:
+                return mAmbientWrapper;
+            default:
+                return null;
+        }
+    }
+
+    /**
+     * @return one of the static enum types in this view, calculated form the current state
+     */
+    public int calculateVisibleType() {
+        if (mContainingNotification.isShowingAmbient()) {
+            if (mIsChildInGroup && mAmbientSingleLineChild != null) {
+                return VISIBLE_TYPE_AMBIENT_SINGLELINE;
+            } else if (mAmbientChild != null) {
+                return VISIBLE_TYPE_AMBIENT;
+            } else {
+                return VISIBLE_TYPE_CONTRACTED;
+            }
+        }
+        if (mUserExpanding) {
+            int height = !mIsChildInGroup || isGroupExpanded()
+                    || mContainingNotification.isExpanded(true /* allowOnKeyguard */)
+                    ? mContainingNotification.getMaxContentHeight()
+                    : mContainingNotification.getShowingLayout().getMinHeight();
+            if (height == 0) {
+                height = mContentHeight;
+            }
+            int expandedVisualType = getVisualTypeForHeight(height);
+            int collapsedVisualType = mIsChildInGroup && !isGroupExpanded()
+                    ? VISIBLE_TYPE_SINGLELINE
+                    : getVisualTypeForHeight(mContainingNotification.getCollapsedHeight());
+            return mTransformationStartVisibleType == collapsedVisualType
+                    ? expandedVisualType
+                    : collapsedVisualType;
+        }
+        int intrinsicHeight = mContainingNotification.getIntrinsicHeight();
+        int viewHeight = mContentHeight;
+        if (intrinsicHeight != 0) {
+            // the intrinsicHeight might be 0 because it was just reset.
+            viewHeight = Math.min(mContentHeight, intrinsicHeight);
+        }
+        return getVisualTypeForHeight(viewHeight);
+    }
+
+    private int getVisualTypeForHeight(float viewHeight) {
+        boolean noExpandedChild = mExpandedChild == null;
+        if (!noExpandedChild && viewHeight == mExpandedChild.getHeight()) {
+            return VISIBLE_TYPE_EXPANDED;
+        }
+        if (!mUserExpanding && mIsChildInGroup && !isGroupExpanded()) {
+            return VISIBLE_TYPE_SINGLELINE;
+        }
+
+        if ((mIsHeadsUp || mHeadsUpAnimatingAway) && mHeadsUpChild != null
+                && !mContainingNotification.isOnKeyguard()) {
+            if (viewHeight <= mHeadsUpChild.getHeight() || noExpandedChild) {
+                return VISIBLE_TYPE_HEADSUP;
+            } else {
+                return VISIBLE_TYPE_EXPANDED;
+            }
+        } else {
+            if (noExpandedChild || (viewHeight <= mContractedChild.getHeight()
+                    && (!mIsChildInGroup || isGroupExpanded()
+                            || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) {
+                return VISIBLE_TYPE_CONTRACTED;
+            } else {
+                return VISIBLE_TYPE_EXPANDED;
+            }
+        }
+    }
+
+    public boolean isContentExpandable() {
+        return mIsContentExpandable;
+    }
+
+    public void setDark(boolean dark, boolean fade, long delay) {
+        if (mContractedChild == null) {
+            return;
+        }
+        mDark = dark;
+        if (mVisibleType == VISIBLE_TYPE_CONTRACTED || !dark) {
+            mContractedWrapper.setDark(dark, fade, delay);
+        }
+        if (mVisibleType == VISIBLE_TYPE_EXPANDED || (mExpandedChild != null && !dark)) {
+            mExpandedWrapper.setDark(dark, fade, delay);
+        }
+        if (mVisibleType == VISIBLE_TYPE_HEADSUP || (mHeadsUpChild != null && !dark)) {
+            mHeadsUpWrapper.setDark(dark, fade, delay);
+        }
+        if (mSingleLineView != null && (mVisibleType == VISIBLE_TYPE_SINGLELINE || !dark)) {
+            mSingleLineView.setDark(dark, fade, delay);
+        }
+        selectLayout(!dark && fade /* animate */, false /* force */);
+    }
+
+    public void setHeadsUp(boolean headsUp) {
+        mIsHeadsUp = headsUp;
+        selectLayout(false /* animate */, true /* force */);
+        updateExpandButtons(mExpandable);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+
+        // This is not really true, but good enough when fading from the contracted to the expanded
+        // layout, and saves us some layers.
+        return false;
+    }
+
+    public void setLegacy(boolean legacy) {
+        mLegacy = legacy;
+        updateLegacy();
+    }
+
+    private void updateLegacy() {
+        if (mContractedChild != null) {
+            mContractedWrapper.setLegacy(mLegacy);
+        }
+        if (mExpandedChild != null) {
+            mExpandedWrapper.setLegacy(mLegacy);
+        }
+        if (mHeadsUpChild != null) {
+            mHeadsUpWrapper.setLegacy(mLegacy);
+        }
+    }
+
+    public void setIsChildInGroup(boolean isChildInGroup) {
+        mIsChildInGroup = isChildInGroup;
+        if (mContractedChild != null) {
+            mContractedWrapper.setIsChildInGroup(mIsChildInGroup);
+        }
+        if (mExpandedChild != null) {
+            mExpandedWrapper.setIsChildInGroup(mIsChildInGroup);
+        }
+        if (mHeadsUpChild != null) {
+            mHeadsUpWrapper.setIsChildInGroup(mIsChildInGroup);
+        }
+        if (mAmbientChild != null) {
+            mAmbientWrapper.setIsChildInGroup(mIsChildInGroup);
+        }
+        updateAllSingleLineViews();
+    }
+
+    public void onNotificationUpdated(NotificationData.Entry entry) {
+        mStatusBarNotification = entry.notification;
+        mBeforeN = entry.targetSdk < Build.VERSION_CODES.N;
+        updateAllSingleLineViews();
+        if (mContractedChild != null) {
+            mContractedWrapper.onContentUpdated(entry.row);
+        }
+        if (mExpandedChild != null) {
+            mExpandedWrapper.onContentUpdated(entry.row);
+        }
+        if (mHeadsUpChild != null) {
+            mHeadsUpWrapper.onContentUpdated(entry.row);
+        }
+        if (mAmbientChild != null) {
+            mAmbientWrapper.onContentUpdated(entry.row);
+        }
+        applyRemoteInput(entry);
+        updateLegacy();
+        mForceSelectNextLayout = true;
+        setDark(mDark, false /* animate */, 0 /* delay */);
+        mPreviousExpandedRemoteInputIntent = null;
+        mPreviousHeadsUpRemoteInputIntent = null;
+    }
+
+    private void updateAllSingleLineViews() {
+        updateSingleLineView();
+        updateAmbientSingleLineView();
+    }
+    private void updateSingleLineView() {
+        if (mIsChildInGroup) {
+            mSingleLineView = mHybridGroupManager.bindFromNotification(
+                    mSingleLineView, mStatusBarNotification.getNotification());
+        } else if (mSingleLineView != null) {
+            removeView(mSingleLineView);
+            mSingleLineView = null;
+        }
+    }
+
+    private void updateAmbientSingleLineView() {
+        if (mIsChildInGroup) {
+            mAmbientSingleLineChild = mHybridGroupManager.bindAmbientFromNotification(
+                    mAmbientSingleLineChild, mStatusBarNotification.getNotification());
+        } else if (mAmbientSingleLineChild != null) {
+            removeView(mAmbientSingleLineChild);
+            mAmbientSingleLineChild = null;
+        }
+    }
+
+    private void applyRemoteInput(final NotificationData.Entry entry) {
+        if (mRemoteInputController == null) {
+            return;
+        }
+
+        boolean hasRemoteInput = false;
+
+        Notification.Action[] actions = entry.notification.getNotification().actions;
+        if (actions != null) {
+            for (Notification.Action a : actions) {
+                if (a.getRemoteInputs() != null) {
+                    for (RemoteInput ri : a.getRemoteInputs()) {
+                        if (ri.getAllowFreeFormInput()) {
+                            hasRemoteInput = true;
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        View bigContentView = mExpandedChild;
+        if (bigContentView != null) {
+            mExpandedRemoteInput = applyRemoteInput(bigContentView, entry, hasRemoteInput,
+                    mPreviousExpandedRemoteInputIntent, mCachedExpandedRemoteInput,
+                    mExpandedWrapper);
+        } else {
+            mExpandedRemoteInput = null;
+        }
+        if (mCachedExpandedRemoteInput != null
+                && mCachedExpandedRemoteInput != mExpandedRemoteInput) {
+            // We had a cached remote input but didn't reuse it. Clean up required.
+            mCachedExpandedRemoteInput.dispatchFinishTemporaryDetach();
+        }
+        mCachedExpandedRemoteInput = null;
+
+        View headsUpContentView = mHeadsUpChild;
+        if (headsUpContentView != null) {
+            mHeadsUpRemoteInput = applyRemoteInput(headsUpContentView, entry, hasRemoteInput,
+                    mPreviousHeadsUpRemoteInputIntent, mCachedHeadsUpRemoteInput, mHeadsUpWrapper);
+        } else {
+            mHeadsUpRemoteInput = null;
+        }
+        if (mCachedHeadsUpRemoteInput != null
+                && mCachedHeadsUpRemoteInput != mHeadsUpRemoteInput) {
+            // We had a cached remote input but didn't reuse it. Clean up required.
+            mCachedHeadsUpRemoteInput.dispatchFinishTemporaryDetach();
+        }
+        mCachedHeadsUpRemoteInput = null;
+    }
+
+    private RemoteInputView applyRemoteInput(View view, NotificationData.Entry entry,
+            boolean hasRemoteInput, PendingIntent existingPendingIntent,
+            RemoteInputView cachedView, NotificationViewWrapper wrapper) {
+        View actionContainerCandidate = view.findViewById(
+                com.android.internal.R.id.actions_container);
+        if (actionContainerCandidate instanceof FrameLayout) {
+            RemoteInputView existing = (RemoteInputView)
+                    view.findViewWithTag(RemoteInputView.VIEW_TAG);
+
+            if (existing != null) {
+                existing.onNotificationUpdateOrReset();
+            }
+
+            if (existing == null && hasRemoteInput) {
+                ViewGroup actionContainer = (FrameLayout) actionContainerCandidate;
+                if (cachedView == null) {
+                    RemoteInputView riv = RemoteInputView.inflate(
+                            mContext, actionContainer, entry, mRemoteInputController);
+
+                    riv.setVisibility(View.INVISIBLE);
+                    actionContainer.addView(riv, new LayoutParams(
+                            ViewGroup.LayoutParams.MATCH_PARENT,
+                            ViewGroup.LayoutParams.MATCH_PARENT)
+                    );
+                    existing = riv;
+                } else {
+                    actionContainer.addView(cachedView);
+                    cachedView.dispatchFinishTemporaryDetach();
+                    cachedView.requestFocus();
+                    existing = cachedView;
+                }
+            }
+            if (hasRemoteInput) {
+                int color = entry.notification.getNotification().color;
+                if (color == Notification.COLOR_DEFAULT) {
+                    color = mContext.getColor(R.color.default_remote_input_background);
+                }
+                existing.setBackgroundColor(NotificationColorUtil.ensureTextBackgroundColor(color,
+                        mContext.getColor(R.color.remote_input_text_enabled),
+                        mContext.getColor(R.color.remote_input_hint)));
+
+                existing.setWrapper(wrapper);
+
+                if (existingPendingIntent != null || existing.isActive()) {
+                    // The current action could be gone, or the pending intent no longer valid.
+                    // If we find a matching action in the new notification, focus, otherwise close.
+                    Notification.Action[] actions = entry.notification.getNotification().actions;
+                    if (existingPendingIntent != null) {
+                        existing.setPendingIntent(existingPendingIntent);
+                    }
+                    if (existing.updatePendingIntentFromActions(actions)) {
+                        if (!existing.isActive()) {
+                            existing.focus();
+                        }
+                    } else {
+                        if (existing.isActive()) {
+                            existing.close();
+                        }
+                    }
+                }
+            }
+            return existing;
+        }
+        return null;
+    }
+
+    public void closeRemoteInput() {
+        if (mHeadsUpRemoteInput != null) {
+            mHeadsUpRemoteInput.close();
+        }
+        if (mExpandedRemoteInput != null) {
+            mExpandedRemoteInput.close();
+        }
+    }
+
+    public void setGroupManager(NotificationGroupManager groupManager) {
+        mGroupManager = groupManager;
+    }
+
+    public void setRemoteInputController(RemoteInputController r) {
+        mRemoteInputController = r;
+    }
+
+    public void setExpandClickListener(OnClickListener expandClickListener) {
+        mExpandClickListener = expandClickListener;
+    }
+
+    public void updateExpandButtons(boolean expandable) {
+        mExpandable = expandable;
+        // if the expanded child has the same height as the collapsed one we hide it.
+        if (mExpandedChild != null && mExpandedChild.getHeight() != 0) {
+            if ((!mIsHeadsUp && !mHeadsUpAnimatingAway)
+                    || mHeadsUpChild == null || mContainingNotification.isOnKeyguard()) {
+                if (mExpandedChild.getHeight() <= mContractedChild.getHeight()) {
+                    expandable = false;
+                }
+            } else if (mExpandedChild.getHeight() <= mHeadsUpChild.getHeight()) {
+                expandable = false;
+            }
+        }
+        if (mExpandedChild != null) {
+            mExpandedWrapper.updateExpandability(expandable, mExpandClickListener);
+        }
+        if (mContractedChild != null) {
+            mContractedWrapper.updateExpandability(expandable, mExpandClickListener);
+        }
+        if (mHeadsUpChild != null) {
+            mHeadsUpWrapper.updateExpandability(expandable,  mExpandClickListener);
+        }
+        mIsContentExpandable = expandable;
+    }
+
+    public NotificationHeaderView getNotificationHeader() {
+        NotificationHeaderView header = null;
+        if (mContractedChild != null) {
+            header = mContractedWrapper.getNotificationHeader();
+        }
+        if (header == null && mExpandedChild != null) {
+            header = mExpandedWrapper.getNotificationHeader();
+        }
+        if (header == null && mHeadsUpChild != null) {
+            header = mHeadsUpWrapper.getNotificationHeader();
+        }
+        if (header == null && mAmbientChild != null) {
+            header = mAmbientWrapper.getNotificationHeader();
+        }
+        return header;
+    }
+
+
+    public NotificationHeaderView getContractedNotificationHeader() {
+        if (mContractedChild != null) {
+            return mContractedWrapper.getNotificationHeader();
+        }
+        return null;
+    }
+
+    public NotificationHeaderView getVisibleNotificationHeader() {
+        NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType);
+        return wrapper == null ? null : wrapper.getNotificationHeader();
+    }
+
+    public void setContainingNotification(ExpandableNotificationRow containingNotification) {
+        mContainingNotification = containingNotification;
+    }
+
+    public void requestSelectLayout(boolean needsAnimation) {
+        selectLayout(needsAnimation, false);
+    }
+
+    public void reInflateViews() {
+        if (mIsChildInGroup && mSingleLineView != null) {
+            removeView(mSingleLineView);
+            mSingleLineView = null;
+            updateAllSingleLineViews();
+        }
+    }
+
+    public void setUserExpanding(boolean userExpanding) {
+        mUserExpanding = userExpanding;
+        if (userExpanding) {
+            mTransformationStartVisibleType = mVisibleType;
+        } else {
+            mTransformationStartVisibleType = UNDEFINED;
+            mVisibleType = calculateVisibleType();
+            updateViewVisibilities(mVisibleType);
+            updateBackgroundColor(false);
+        }
+    }
+
+    /**
+     * Set by how much the single line view should be indented. Used when a overflow indicator is
+     * present and only during measuring
+     */
+    public void setSingleLineWidthIndention(int singleLineWidthIndention) {
+        if (singleLineWidthIndention != mSingleLineWidthIndention) {
+            mSingleLineWidthIndention = singleLineWidthIndention;
+            mContainingNotification.forceLayout();
+            forceLayout();
+        }
+    }
+
+    public HybridNotificationView getSingleLineView() {
+        return mSingleLineView;
+    }
+
+    public void setRemoved() {
+        if (mExpandedRemoteInput != null) {
+            mExpandedRemoteInput.setRemoved();
+        }
+        if (mHeadsUpRemoteInput != null) {
+            mHeadsUpRemoteInput.setRemoved();
+        }
+    }
+
+    public void setContentHeightAnimating(boolean animating) {
+        if (!animating) {
+            mContentHeightAtAnimationStart = UNDEFINED;
+        }
+    }
+
+    @VisibleForTesting
+    boolean isAnimatingVisibleType() {
+        return mAnimationStartVisibleType != UNDEFINED;
+    }
+
+    public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {
+        mHeadsUpAnimatingAway = headsUpAnimatingAway;
+        selectLayout(false /* animate */, true /* force */);
+    }
+
+    public void setFocusOnVisibilityChange() {
+        mFocusOnVisibilityChange = true;
+    }
+
+    public void setIconsVisible(boolean iconsVisible) {
+        mIconsVisible = iconsVisible;
+        updateIconVisibilities();
+    }
+
+    private void updateIconVisibilities() {
+        if (mContractedWrapper != null) {
+            NotificationHeaderView header = mContractedWrapper.getNotificationHeader();
+            if (header != null) {
+                header.getIcon().setForceHidden(!mIconsVisible);
+            }
+        }
+        if (mHeadsUpWrapper != null) {
+            NotificationHeaderView header = mHeadsUpWrapper.getNotificationHeader();
+            if (header != null) {
+                header.getIcon().setForceHidden(!mIconsVisible);
+            }
+        }
+        if (mExpandedWrapper != null) {
+            NotificationHeaderView header = mExpandedWrapper.getNotificationHeader();
+            if (header != null) {
+                header.getIcon().setForceHidden(!mIconsVisible);
+            }
+        }
+    }
+
+    @Override
+    public void onVisibilityAggregated(boolean isVisible) {
+        super.onVisibilityAggregated(isVisible);
+        if (isVisible) {
+            fireExpandedVisibleListenerIfVisible();
+        }
+    }
+
+    /**
+     * Sets a one-shot listener for when the expanded view becomes visible.
+     *
+     * This will fire the listener immediately if the expanded view is already visible.
+     */
+    public void setOnExpandedVisibleListener(Runnable r) {
+        mExpandedVisibleListener = r;
+        fireExpandedVisibleListenerIfVisible();
+    }
+
+    public void setIsLowPriority(boolean isLowPriority) {
+        mIsLowPriority = isLowPriority;
+    }
+
+    public boolean isDimmable() {
+        if (!mContractedWrapper.isDimmable()) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Should a single click be disallowed on this view when on the keyguard?
+     */
+    public boolean disallowSingleClick(float x, float y) {
+        NotificationViewWrapper visibleWrapper = getVisibleWrapper(getVisibleType());
+        if (visibleWrapper != null) {
+            return visibleWrapper.disallowSingleClick(x, y);
+        }
+        return false;
+    }
+}
diff --git a/com/android/systemui/statusbar/NotificationData.java b/com/android/systemui/statusbar/NotificationData.java
new file mode 100644
index 0000000..ddc7dd0
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationData.java
@@ -0,0 +1,604 @@
+/*
+ * Copyright (C) 2008 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.app.AppGlobals;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.NotificationListenerService.Ranking;
+import android.service.notification.NotificationListenerService.RankingMap;
+import android.service.notification.SnoozeCriterion;
+import android.service.notification.StatusBarNotification;
+import android.util.ArrayMap;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RemoteViews;
+import android.Manifest;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto;
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.internal.util.NotificationColorUtil;
+import com.android.systemui.Dependency;
+import com.android.systemui.ForegroundServiceController;
+import com.android.systemui.statusbar.notification.InflationException;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * The list of currently displaying notifications.
+ */
+public class NotificationData {
+
+    private final Environment mEnvironment;
+    private HeadsUpManager mHeadsUpManager;
+
+    public static final class Entry {
+        private static final long LAUNCH_COOLDOWN = 2000;
+        private static final long NOT_LAUNCHED_YET = -LAUNCH_COOLDOWN;
+        private static final int COLOR_INVALID = 1;
+        public String key;
+        public StatusBarNotification notification;
+        public NotificationChannel channel;
+        public StatusBarIconView icon;
+        public StatusBarIconView expandedIcon;
+        public ExpandableNotificationRow row; // the outer expanded view
+        private boolean interruption;
+        public boolean autoRedacted; // whether the redacted notification was generated by us
+        public int targetSdk;
+        private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
+        public RemoteViews cachedContentView;
+        public RemoteViews cachedBigContentView;
+        public RemoteViews cachedHeadsUpContentView;
+        public RemoteViews cachedPublicContentView;
+        public RemoteViews cachedAmbientContentView;
+        public CharSequence remoteInputText;
+        public List<SnoozeCriterion> snoozeCriteria;
+        private int mCachedContrastColor = COLOR_INVALID;
+        private int mCachedContrastColorIsFor = COLOR_INVALID;
+        private InflationTask mRunningTask = null;
+        private Throwable mDebugThrowable;
+
+        public Entry(StatusBarNotification n) {
+            this.key = n.getKey();
+            this.notification = n;
+        }
+
+        public void setInterruption() {
+            interruption = true;
+        }
+
+        public boolean hasInterrupted() {
+            return interruption;
+        }
+
+        /**
+         * Resets the notification entry to be re-used.
+         */
+        public void reset() {
+            if (row != null) {
+                row.reset();
+            }
+        }
+
+        public View getExpandedContentView() {
+            return row.getPrivateLayout().getExpandedChild();
+        }
+
+        public View getPublicContentView() {
+            return row.getPublicLayout().getContractedChild();
+        }
+
+        public void notifyFullScreenIntentLaunched() {
+            setInterruption();
+            lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime();
+        }
+
+        public boolean hasJustLaunchedFullScreenIntent() {
+            return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN;
+        }
+
+        /**
+         * Create the icons for a notification
+         * @param context the context to create the icons with
+         * @param sbn the notification
+         * @throws InflationException
+         */
+        public void createIcons(Context context, StatusBarNotification sbn)
+                throws InflationException {
+            Notification n = sbn.getNotification();
+            final Icon smallIcon = n.getSmallIcon();
+            if (smallIcon == null) {
+                throw new InflationException("No small icon in notification from "
+                        + sbn.getPackageName());
+            }
+
+            // Construct the icon.
+            icon = new StatusBarIconView(context,
+                    sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
+            icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
+
+            // Construct the expanded icon.
+            expandedIcon = new StatusBarIconView(context,
+                    sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
+            expandedIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
+            final StatusBarIcon ic = new StatusBarIcon(
+                    sbn.getUser(),
+                    sbn.getPackageName(),
+                    smallIcon,
+                    n.iconLevel,
+                    n.number,
+                    StatusBarIconView.contentDescForNotification(context, n));
+            if (!icon.set(ic) || !expandedIcon.set(ic)) {
+                icon = null;
+                expandedIcon = null;
+                throw new InflationException("Couldn't create icon: " + ic);
+            }
+            expandedIcon.setVisibility(View.INVISIBLE);
+            expandedIcon.setOnVisibilityChangedListener(
+                    newVisibility -> {
+                        if (row != null) {
+                            row.setIconsVisible(newVisibility != View.VISIBLE);
+                        }
+                    });
+        }
+
+        public void setIconTag(int key, Object tag) {
+            if (icon != null) {
+                icon.setTag(key, tag);
+                expandedIcon.setTag(key, tag);
+            }
+        }
+
+        /**
+         * Update the notification icons.
+         * @param context the context to create the icons with.
+         * @param n the notification to read the icon from.
+         * @throws InflationException
+         */
+        public void updateIcons(Context context, StatusBarNotification sbn)
+                throws InflationException {
+            if (icon != null) {
+                // Update the icon
+                Notification n = sbn.getNotification();
+                final StatusBarIcon ic = new StatusBarIcon(
+                        notification.getUser(),
+                        notification.getPackageName(),
+                        n.getSmallIcon(),
+                        n.iconLevel,
+                        n.number,
+                        StatusBarIconView.contentDescForNotification(context, n));
+                icon.setNotification(sbn);
+                expandedIcon.setNotification(sbn);
+                if (!icon.set(ic) || !expandedIcon.set(ic)) {
+                    throw new InflationException("Couldn't update icon: " + ic);
+                }
+            }
+        }
+
+        public int getContrastedColor(Context context, boolean isLowPriority,
+                int backgroundColor) {
+            int rawColor = isLowPriority ? Notification.COLOR_DEFAULT :
+                    notification.getNotification().color;
+            if (mCachedContrastColorIsFor == rawColor && mCachedContrastColor != COLOR_INVALID) {
+                return mCachedContrastColor;
+            }
+            final int contrasted = NotificationColorUtil.resolveContrastColor(context, rawColor,
+                    backgroundColor);
+            mCachedContrastColorIsFor = rawColor;
+            mCachedContrastColor = contrasted;
+            return mCachedContrastColor;
+        }
+
+        /**
+         * Abort all existing inflation tasks
+         */
+        public void abortTask() {
+            if (mRunningTask != null) {
+                mRunningTask.abort();
+                mRunningTask = null;
+            }
+        }
+
+        public void setInflationTask(InflationTask abortableTask) {
+            // abort any existing inflation
+            InflationTask existing = mRunningTask;
+            abortTask();
+            mRunningTask = abortableTask;
+            if (existing != null && mRunningTask != null) {
+                mRunningTask.supersedeTask(existing);
+            }
+        }
+
+        public void onInflationTaskFinished() {
+           mRunningTask = null;
+        }
+
+        @VisibleForTesting
+        public InflationTask getRunningTask() {
+            return mRunningTask;
+        }
+
+        /**
+         * Set a throwable that is used for debugging
+         *
+         * @param debugThrowable the throwable to save
+         */
+        public void setDebugThrowable(Throwable debugThrowable) {
+            mDebugThrowable = debugThrowable;
+        }
+
+        public Throwable getDebugThrowable() {
+            return mDebugThrowable;
+        }
+    }
+
+    private final ArrayMap<String, Entry> mEntries = new ArrayMap<>();
+    private final ArrayList<Entry> mSortedAndFiltered = new ArrayList<>();
+
+    private NotificationGroupManager mGroupManager;
+
+    private RankingMap mRankingMap;
+    private final Ranking mTmpRanking = new Ranking();
+
+    public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+        mHeadsUpManager = headsUpManager;
+    }
+
+    private final Comparator<Entry> mRankingComparator = new Comparator<Entry>() {
+        private final Ranking mRankingA = new Ranking();
+        private final Ranking mRankingB = new Ranking();
+
+        @Override
+        public int compare(Entry a, Entry b) {
+            final StatusBarNotification na = a.notification;
+            final StatusBarNotification nb = b.notification;
+            int aImportance = NotificationManager.IMPORTANCE_DEFAULT;
+            int bImportance = NotificationManager.IMPORTANCE_DEFAULT;
+            int aRank = 0;
+            int bRank = 0;
+
+            if (mRankingMap != null) {
+                // RankingMap as received from NoMan
+                mRankingMap.getRanking(a.key, mRankingA);
+                mRankingMap.getRanking(b.key, mRankingB);
+                aImportance = mRankingA.getImportance();
+                bImportance = mRankingB.getImportance();
+                aRank = mRankingA.getRank();
+                bRank = mRankingB.getRank();
+            }
+
+            String mediaNotification = mEnvironment.getCurrentMediaNotificationKey();
+
+            // IMPORTANCE_MIN media streams are allowed to drift to the bottom
+            final boolean aMedia = a.key.equals(mediaNotification)
+                    && aImportance > NotificationManager.IMPORTANCE_MIN;
+            final boolean bMedia = b.key.equals(mediaNotification)
+                    && bImportance > NotificationManager.IMPORTANCE_MIN;
+
+            boolean aSystemMax = aImportance >= NotificationManager.IMPORTANCE_HIGH &&
+                    isSystemNotification(na);
+            boolean bSystemMax = bImportance >= NotificationManager.IMPORTANCE_HIGH &&
+                    isSystemNotification(nb);
+
+            boolean isHeadsUp = a.row.isHeadsUp();
+            if (isHeadsUp != b.row.isHeadsUp()) {
+                return isHeadsUp ? -1 : 1;
+            } else if (isHeadsUp) {
+                // Provide consistent ranking with headsUpManager
+                return mHeadsUpManager.compare(a, b);
+            } else if (aMedia != bMedia) {
+                // Upsort current media notification.
+                return aMedia ? -1 : 1;
+            } else if (aSystemMax != bSystemMax) {
+                // Upsort PRIORITY_MAX system notifications
+                return aSystemMax ? -1 : 1;
+            } else if (aRank != bRank) {
+                return aRank - bRank;
+            } else {
+                return Long.compare(nb.getNotification().when, na.getNotification().when);
+            }
+        }
+    };
+
+    public NotificationData(Environment environment) {
+        mEnvironment = environment;
+        mGroupManager = environment.getGroupManager();
+    }
+
+    /**
+     * Returns the sorted list of active notifications (depending on {@link Environment}
+     *
+     * <p>
+     * This call doesn't update the list of active notifications. Call {@link #filterAndSort()}
+     * when the environment changes.
+     * <p>
+     * Don't hold on to or modify the returned list.
+     */
+    public ArrayList<Entry> getActiveNotifications() {
+        return mSortedAndFiltered;
+    }
+
+    public Entry get(String key) {
+        return mEntries.get(key);
+    }
+
+    public void add(Entry entry) {
+        synchronized (mEntries) {
+            mEntries.put(entry.notification.getKey(), entry);
+        }
+        mGroupManager.onEntryAdded(entry);
+
+        updateRankingAndSort(mRankingMap);
+    }
+
+    public Entry remove(String key, RankingMap ranking) {
+        Entry removed = null;
+        synchronized (mEntries) {
+            removed = mEntries.remove(key);
+        }
+        if (removed == null) return null;
+        mGroupManager.onEntryRemoved(removed);
+        updateRankingAndSort(ranking);
+        return removed;
+    }
+
+    public void updateRanking(RankingMap ranking) {
+        updateRankingAndSort(ranking);
+    }
+
+    public boolean isAmbient(String key) {
+        if (mRankingMap != null) {
+            mRankingMap.getRanking(key, mTmpRanking);
+            return mTmpRanking.isAmbient();
+        }
+        return false;
+    }
+
+    public int getVisibilityOverride(String key) {
+        if (mRankingMap != null) {
+            mRankingMap.getRanking(key, mTmpRanking);
+            return mTmpRanking.getVisibilityOverride();
+        }
+        return Ranking.VISIBILITY_NO_OVERRIDE;
+    }
+
+    public boolean shouldSuppressScreenOff(String key) {
+        if (mRankingMap != null) {
+            mRankingMap.getRanking(key, mTmpRanking);
+            return (mTmpRanking.getSuppressedVisualEffects()
+                    & NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_OFF) != 0;
+        }
+        return false;
+    }
+
+    public boolean shouldSuppressScreenOn(String key) {
+        if (mRankingMap != null) {
+            mRankingMap.getRanking(key, mTmpRanking);
+            return (mTmpRanking.getSuppressedVisualEffects()
+                    & NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_ON) != 0;
+        }
+        return false;
+    }
+
+    public int getImportance(String key) {
+        if (mRankingMap != null) {
+            mRankingMap.getRanking(key, mTmpRanking);
+            return mTmpRanking.getImportance();
+        }
+        return NotificationManager.IMPORTANCE_UNSPECIFIED;
+    }
+
+    public String getOverrideGroupKey(String key) {
+        if (mRankingMap != null) {
+            mRankingMap.getRanking(key, mTmpRanking);
+            return mTmpRanking.getOverrideGroupKey();
+        }
+         return null;
+    }
+
+    public List<SnoozeCriterion> getSnoozeCriteria(String key) {
+        if (mRankingMap != null) {
+            mRankingMap.getRanking(key, mTmpRanking);
+            return mTmpRanking.getSnoozeCriteria();
+        }
+        return null;
+    }
+
+    public NotificationChannel getChannel(String key) {
+        if (mRankingMap != null) {
+            mRankingMap.getRanking(key, mTmpRanking);
+            return mTmpRanking.getChannel();
+        }
+        return null;
+    }
+
+    private void updateRankingAndSort(RankingMap ranking) {
+        if (ranking != null) {
+            mRankingMap = ranking;
+            synchronized (mEntries) {
+                final int N = mEntries.size();
+                for (int i = 0; i < N; i++) {
+                    Entry entry = mEntries.valueAt(i);
+                    final StatusBarNotification oldSbn = entry.notification.cloneLight();
+                    final String overrideGroupKey = getOverrideGroupKey(entry.key);
+                    if (!Objects.equals(oldSbn.getOverrideGroupKey(), overrideGroupKey)) {
+                        entry.notification.setOverrideGroupKey(overrideGroupKey);
+                        mGroupManager.onEntryUpdated(entry, oldSbn);
+                    }
+                    entry.channel = getChannel(entry.key);
+                    entry.snoozeCriteria = getSnoozeCriteria(entry.key);
+                }
+            }
+        }
+        filterAndSort();
+    }
+
+    // TODO: This should not be public. Instead the Environment should notify this class when
+    // anything changed, and this class should call back the UI so it updates itself.
+    public void filterAndSort() {
+        mSortedAndFiltered.clear();
+
+        synchronized (mEntries) {
+            final int N = mEntries.size();
+            for (int i = 0; i < N; i++) {
+                Entry entry = mEntries.valueAt(i);
+                StatusBarNotification sbn = entry.notification;
+
+                if (shouldFilterOut(sbn)) {
+                    continue;
+                }
+
+                mSortedAndFiltered.add(entry);
+            }
+        }
+
+        Collections.sort(mSortedAndFiltered, mRankingComparator);
+    }
+
+    /**
+     * @param sbn
+     * @return true if this notification should NOT be shown right now
+     */
+    public boolean shouldFilterOut(StatusBarNotification sbn) {
+        if (!(mEnvironment.isDeviceProvisioned() ||
+                showNotificationEvenIfUnprovisioned(sbn))) {
+            return true;
+        }
+
+        if (!mEnvironment.isNotificationForCurrentProfiles(sbn)) {
+            return true;
+        }
+
+        if (mEnvironment.isSecurelyLocked(sbn.getUserId()) &&
+                (sbn.getNotification().visibility == Notification.VISIBILITY_SECRET
+                        || mEnvironment.shouldHideNotifications(sbn.getUserId())
+                        || mEnvironment.shouldHideNotifications(sbn.getKey()))) {
+            return true;
+        }
+
+        if (!StatusBar.ENABLE_CHILD_NOTIFICATIONS
+                && mGroupManager.isChildInGroupWithSummary(sbn)) {
+            return true;
+        }
+
+        final ForegroundServiceController fsc = Dependency.get(ForegroundServiceController.class);
+        if (fsc.isDungeonNotification(sbn) && !fsc.isDungeonNeededForUser(sbn.getUserId())) {
+            // this is a foreground-service disclosure for a user that does not need to show one
+            return true;
+        }
+
+        return false;
+    }
+
+    // Q: What kinds of notifications should show during setup?
+    // A: Almost none! Only things coming from packages with permission
+    // android.permission.NOTIFICATION_DURING_SETUP that also have special "kind" tags marking them
+    // as relevant for setup (see below).
+    public static boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) {
+        return showNotificationEvenIfUnprovisioned(AppGlobals.getPackageManager(), sbn);
+    }
+
+    @VisibleForTesting
+    static boolean showNotificationEvenIfUnprovisioned(IPackageManager packageManager,
+            StatusBarNotification sbn) {
+        return checkUidPermission(packageManager, Manifest.permission.NOTIFICATION_DURING_SETUP,
+                sbn.getUid()) == PackageManager.PERMISSION_GRANTED
+                && sbn.getNotification().extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP);
+    }
+
+    private static int checkUidPermission(IPackageManager packageManager, String permission,
+            int uid) {
+        try {
+            return packageManager.checkUidPermission(permission, uid);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    public void dump(PrintWriter pw, String indent) {
+        int N = mSortedAndFiltered.size();
+        pw.print(indent);
+        pw.println("active notifications: " + N);
+        int active;
+        for (active = 0; active < N; active++) {
+            NotificationData.Entry e = mSortedAndFiltered.get(active);
+            dumpEntry(pw, indent, active, e);
+        }
+        synchronized (mEntries) {
+            int M = mEntries.size();
+            pw.print(indent);
+            pw.println("inactive notifications: " + (M - active));
+            int inactiveCount = 0;
+            for (int i = 0; i < M; i++) {
+                Entry entry = mEntries.valueAt(i);
+                if (!mSortedAndFiltered.contains(entry)) {
+                    dumpEntry(pw, indent, inactiveCount, entry);
+                    inactiveCount++;
+                }
+            }
+        }
+    }
+
+    private void dumpEntry(PrintWriter pw, String indent, int i, Entry e) {
+        mRankingMap.getRanking(e.key, mTmpRanking);
+        pw.print(indent);
+        pw.println("  [" + i + "] key=" + e.key + " icon=" + e.icon);
+        StatusBarNotification n = e.notification;
+        pw.print(indent);
+        pw.println("      pkg=" + n.getPackageName() + " id=" + n.getId() + " importance=" +
+                mTmpRanking.getImportance());
+        pw.print(indent);
+        pw.println("      notification=" + n.getNotification());
+    }
+
+    private static boolean isSystemNotification(StatusBarNotification sbn) {
+        String sbnPackage = sbn.getPackageName();
+        return "android".equals(sbnPackage) || "com.android.systemui".equals(sbnPackage);
+    }
+
+    /**
+     * Provides access to keyguard state and user settings dependent data.
+     */
+    public interface Environment {
+        public boolean isSecurelyLocked(int userId);
+        public boolean shouldHideNotifications(int userid);
+        public boolean shouldHideNotifications(String key);
+        public boolean isDeviceProvisioned();
+        public boolean isNotificationForCurrentProfiles(StatusBarNotification sbn);
+        public String getCurrentMediaNotificationKey();
+        public NotificationGroupManager getGroupManager();
+    }
+}
diff --git a/com/android/systemui/statusbar/NotificationGuts.java b/com/android/systemui/statusbar/NotificationGuts.java
new file mode 100644
index 0000000..54d622b
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationGuts.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2014 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.app.INotificationManager;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.settingslib.Utils;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+import java.util.Set;
+
+/**
+ * The guts of a notification revealed when performing a long press.
+ */
+public class NotificationGuts extends FrameLayout {
+    private static final String TAG = "NotificationGuts";
+    private static final long CLOSE_GUTS_DELAY = 8000;
+
+    private Drawable mBackground;
+    private int mClipTopAmount;
+    private int mClipBottomAmount;
+    private int mActualHeight;
+    private boolean mExposed;
+
+    private Handler mHandler;
+    private Runnable mFalsingCheck;
+    private boolean mNeedsFalsingProtection;
+    private OnGutsClosedListener mClosedListener;
+    private OnHeightChangedListener mHeightListener;
+
+    private GutsContent mGutsContent;
+
+    public interface GutsContent {
+
+        public void setGutsParent(NotificationGuts listener);
+
+        /**
+         * @return the view to be shown in the notification guts.
+         */
+        public View getContentView();
+
+        /**
+         * @return the actual height of the content.
+         */
+        public int getActualHeight();
+
+        /**
+         * Called when the guts view have been told to close, typically after an outside
+         * interaction.
+         *
+         * @param save whether the state should be saved.
+         * @param force whether the guts view should be forced closed regardless of state.
+         * @return if closing the view has been handled.
+         */
+        public boolean handleCloseControls(boolean save, boolean force);
+
+        /**
+         * @return whether the notification associated with these guts is set to be removed.
+         */
+        public boolean willBeRemoved();
+
+        /**
+         * @return whether these guts are a leavebehind (e.g. {@link NotificationSnooze}).
+         */
+        public default boolean isLeavebehind() {
+            return false;
+        }
+    }
+
+    public interface OnGutsClosedListener {
+        public void onGutsClosed(NotificationGuts guts);
+    }
+
+    public interface OnHeightChangedListener {
+        public void onHeightChanged(NotificationGuts guts);
+    }
+
+    interface OnSettingsClickListener {
+        void onClick(View v, int appUid);
+    }
+
+    public NotificationGuts(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setWillNotDraw(false);
+        mHandler = new Handler();
+        mFalsingCheck = new Runnable() {
+            @Override
+            public void run() {
+                if (mNeedsFalsingProtection && mExposed) {
+                    closeControls(-1 /* x */, -1 /* y */, false /* save */, false /* force */);
+                }
+            }
+        };
+        final TypedArray ta = context.obtainStyledAttributes(attrs,
+                com.android.internal.R.styleable.Theme, 0, 0);
+        ta.recycle();
+    }
+
+    public NotificationGuts(Context context) {
+        this(context, null);
+    }
+
+    public void setGutsContent(GutsContent content) {
+        mGutsContent = content;
+        removeAllViews();
+        addView(mGutsContent.getContentView());
+    }
+
+    public GutsContent getGutsContent() {
+        return mGutsContent;
+    }
+
+    public void resetFalsingCheck() {
+        mHandler.removeCallbacks(mFalsingCheck);
+        if (mNeedsFalsingProtection && mExposed) {
+            mHandler.postDelayed(mFalsingCheck, CLOSE_GUTS_DELAY);
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        draw(canvas, mBackground);
+    }
+
+    private void draw(Canvas canvas, Drawable drawable) {
+        int top = mClipTopAmount;
+        int bottom = mActualHeight - mClipBottomAmount;
+        if (drawable != null && top < bottom) {
+            drawable.setBounds(0, top, getWidth(), bottom);
+            drawable.draw(canvas);
+        }
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mBackground = mContext.getDrawable(R.drawable.notification_guts_bg);
+        if (mBackground != null) {
+            mBackground.setCallback(this);
+        }
+    }
+
+    @Override
+    protected boolean verifyDrawable(Drawable who) {
+        return super.verifyDrawable(who) || who == mBackground;
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        drawableStateChanged(mBackground);
+    }
+
+    private void drawableStateChanged(Drawable d) {
+        if (d != null && d.isStateful()) {
+            d.setState(getDrawableState());
+        }
+    }
+
+    @Override
+    public void drawableHotspotChanged(float x, float y) {
+        if (mBackground != null) {
+            mBackground.setHotspot(x, y);
+        }
+    }
+
+    public void closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force) {
+        if (mGutsContent != null) {
+            if (mGutsContent.isLeavebehind() && leavebehinds) {
+                closeControls(x, y, true /* save */, force);
+            } else if (!mGutsContent.isLeavebehind() && controls) {
+                closeControls(x, y, true /* save */, force);
+            }
+        }
+    }
+
+    public void closeControls(int x, int y, boolean save, boolean force) {
+        if (getWindowToken() == null) {
+            if (mClosedListener != null) {
+                mClosedListener.onGutsClosed(this);
+            }
+            return;
+        }
+
+        if (mGutsContent == null || !mGutsContent.handleCloseControls(save, force)) {
+            animateClose(x, y);
+            setExposed(false, mNeedsFalsingProtection);
+            if (mClosedListener != null) {
+                mClosedListener.onGutsClosed(this);
+            }
+        }
+    }
+
+    private void animateClose(int x, int y) {
+        if (x == -1 || y == -1) {
+            x = (getLeft() + getRight()) / 2;
+            y = (getTop() + getHeight() / 2);
+        }
+        final double horz = Math.max(getWidth() - x, x);
+        final double vert = Math.max(getHeight() - y, y);
+        final float r = (float) Math.hypot(horz, vert);
+        final Animator a = ViewAnimationUtils.createCircularReveal(this,
+                x, y, r, 0);
+        a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+        a.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+        a.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
+                setVisibility(View.GONE);
+            }
+        });
+        a.start();
+    }
+
+    public void setActualHeight(int actualHeight) {
+        mActualHeight = actualHeight;
+        invalidate();
+    }
+
+    public int getActualHeight() {
+        return mActualHeight;
+    }
+
+    public int getIntrinsicHeight() {
+        return mGutsContent != null && mExposed ? mGutsContent.getActualHeight() : getHeight();
+    }
+
+    public void setClipTopAmount(int clipTopAmount) {
+        mClipTopAmount = clipTopAmount;
+        invalidate();
+    }
+
+    public void setClipBottomAmount(int clipBottomAmount) {
+        mClipBottomAmount = clipBottomAmount;
+        invalidate();
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        // Prevents this view from creating a layer when alpha is animating.
+        return false;
+    }
+
+    public void setClosedListener(OnGutsClosedListener listener) {
+        mClosedListener = listener;
+    }
+
+    public void setHeightChangedListener(OnHeightChangedListener listener) {
+        mHeightListener = listener;
+    }
+
+    protected void onHeightChanged() {
+        if (mHeightListener != null) {
+            mHeightListener.onHeightChanged(this);
+        }
+    }
+
+    public void setExposed(boolean exposed, boolean needsFalsingProtection) {
+        final boolean wasExposed = mExposed;
+        mExposed = exposed;
+        mNeedsFalsingProtection = needsFalsingProtection;
+        if (mExposed && mNeedsFalsingProtection) {
+            resetFalsingCheck();
+        } else {
+            mHandler.removeCallbacks(mFalsingCheck);
+        }
+        if (wasExposed != mExposed && mGutsContent != null) {
+            final View contentView = mGutsContent.getContentView();
+            contentView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+            if (mExposed) {
+                contentView.requestAccessibilityFocus();
+            }
+        }
+    }
+
+    public boolean willBeRemoved() {
+        return mGutsContent != null ? mGutsContent.willBeRemoved() : false;
+    }
+
+    public boolean isExposed() {
+        return mExposed;
+    }
+
+    public boolean isLeavebehind() {
+        return mGutsContent != null && mGutsContent.isLeavebehind();
+    }
+}
diff --git a/com/android/systemui/statusbar/NotificationHeaderUtil.java b/com/android/systemui/statusbar/NotificationHeaderUtil.java
new file mode 100644
index 0000000..4301817
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationHeaderUtil.java
@@ -0,0 +1,374 @@
+/*
+ * 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.app.Notification;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Icon;
+import android.text.TextUtils;
+import android.view.NotificationHeaderView;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * A Util to manage {@link android.view.NotificationHeaderView} objects and their redundancies.
+ */
+public class NotificationHeaderUtil {
+
+    private static final TextViewComparator sTextViewComparator = new TextViewComparator();
+    private static final VisibilityApplicator sVisibilityApplicator = new VisibilityApplicator();
+    private static  final DataExtractor sIconExtractor = new DataExtractor() {
+        @Override
+        public Object extractData(ExpandableNotificationRow row) {
+            return row.getStatusBarNotification().getNotification();
+        }
+    };
+    private static final IconComparator sIconVisibilityComparator = new IconComparator() {
+        public boolean compare(View parent, View child, Object parentData,
+                Object childData) {
+            return hasSameIcon(parentData, childData)
+                    && hasSameColor(parentData, childData);
+        }
+    };
+    private static final IconComparator sGreyComparator = new IconComparator() {
+        public boolean compare(View parent, View child, Object parentData,
+                Object childData) {
+            return !hasSameIcon(parentData, childData)
+                    || hasSameColor(parentData, childData);
+        }
+    };
+    private final static ResultApplicator mGreyApplicator = new ResultApplicator() {
+        @Override
+        public void apply(View view, boolean apply) {
+            NotificationHeaderView header = (NotificationHeaderView) view;
+            ImageView icon = (ImageView) view.findViewById(
+                    com.android.internal.R.id.icon);
+            ImageView expand = (ImageView) view.findViewById(
+                    com.android.internal.R.id.expand_button);
+            applyToChild(icon, apply, header.getOriginalIconColor());
+            applyToChild(expand, apply, header.getOriginalNotificationColor());
+        }
+
+        private void applyToChild(View view, boolean shouldApply, int originalColor) {
+            if (originalColor != NotificationHeaderView.NO_COLOR) {
+                ImageView imageView = (ImageView) view;
+                imageView.getDrawable().mutate();
+                if (shouldApply) {
+                    // lets gray it out
+                    int grey = view.getContext().getColor(
+                            com.android.internal.R.color.notification_icon_default_color);
+                    imageView.getDrawable().setColorFilter(grey, PorterDuff.Mode.SRC_ATOP);
+                } else {
+                    // lets reset it
+                    imageView.getDrawable().setColorFilter(originalColor,
+                            PorterDuff.Mode.SRC_ATOP);
+                }
+            }
+        }
+    };
+
+    private final ExpandableNotificationRow mRow;
+    private final ArrayList<HeaderProcessor> mComparators = new ArrayList<>();
+    private final HashSet<Integer> mDividers = new HashSet<>();
+
+    public NotificationHeaderUtil(ExpandableNotificationRow row) {
+        mRow = row;
+        // To hide the icons if they are the same and the color is the same
+        mComparators.add(new HeaderProcessor(mRow,
+                com.android.internal.R.id.icon,
+                sIconExtractor,
+                sIconVisibilityComparator,
+                sVisibilityApplicator));
+        // To grey them out the icons and expand button when the icons are not the same
+        mComparators.add(new HeaderProcessor(mRow,
+                com.android.internal.R.id.notification_header,
+                sIconExtractor,
+                sGreyComparator,
+                mGreyApplicator));
+        mComparators.add(new HeaderProcessor(mRow,
+                com.android.internal.R.id.profile_badge,
+                null /* Extractor */,
+                new ViewComparator() {
+                    @Override
+                    public boolean compare(View parent, View child, Object parentData,
+                            Object childData) {
+                        return parent.getVisibility() != View.GONE;
+                    }
+
+                    @Override
+                    public boolean isEmpty(View view) {
+                        if (view instanceof ImageView) {
+                            return ((ImageView) view).getDrawable() == null;
+                        }
+                        return false;
+                    }
+                },
+                sVisibilityApplicator));
+        mComparators.add(HeaderProcessor.forTextView(mRow,
+                com.android.internal.R.id.app_name_text));
+        mComparators.add(HeaderProcessor.forTextView(mRow,
+                com.android.internal.R.id.header_text));
+        mDividers.add(com.android.internal.R.id.header_text_divider);
+        mDividers.add(com.android.internal.R.id.time_divider);
+    }
+
+    public void updateChildrenHeaderAppearance() {
+        List<ExpandableNotificationRow> notificationChildren = mRow.getNotificationChildren();
+        if (notificationChildren == null) {
+            return;
+        }
+        // Initialize the comparators
+        for (int compI = 0; compI < mComparators.size(); compI++) {
+            mComparators.get(compI).init();
+        }
+
+        // Compare all notification headers
+        for (int i = 0; i < notificationChildren.size(); i++) {
+            ExpandableNotificationRow row = notificationChildren.get(i);
+            for (int compI = 0; compI < mComparators.size(); compI++) {
+                mComparators.get(compI).compareToHeader(row);
+            }
+        }
+
+        // Apply the comparison to the row
+        for (int i = 0; i < notificationChildren.size(); i++) {
+            ExpandableNotificationRow row = notificationChildren.get(i);
+            for (int compI = 0; compI < mComparators.size(); compI++) {
+                mComparators.get(compI).apply(row);
+            }
+            // We need to sanitize the dividers since they might be off-balance now
+            sanitizeHeaderViews(row);
+        }
+    }
+
+    private void sanitizeHeaderViews(ExpandableNotificationRow row) {
+        if (row.isSummaryWithChildren()) {
+            sanitizeHeader(row.getNotificationHeader());
+            return;
+        }
+        final NotificationContentView layout = row.getPrivateLayout();
+        sanitizeChild(layout.getContractedChild());
+        sanitizeChild(layout.getHeadsUpChild());
+        sanitizeChild(layout.getExpandedChild());
+    }
+
+    private void sanitizeChild(View child) {
+        if (child != null) {
+            NotificationHeaderView header = (NotificationHeaderView) child.findViewById(
+                    com.android.internal.R.id.notification_header);
+            sanitizeHeader(header);
+        }
+    }
+
+    private void sanitizeHeader(NotificationHeaderView rowHeader) {
+        if (rowHeader == null) {
+            return;
+        }
+        final int childCount = rowHeader.getChildCount();
+        View time = rowHeader.findViewById(com.android.internal.R.id.time);
+        boolean hasVisibleText = false;
+        for (int i = 1; i < childCount - 1 ; i++) {
+            View child = rowHeader.getChildAt(i);
+            if (child instanceof TextView
+                    && child.getVisibility() != View.GONE
+                    && !mDividers.contains(Integer.valueOf(child.getId()))
+                    && child != time) {
+                hasVisibleText = true;
+                break;
+            }
+        }
+        // in case no view is visible we make sure the time is visible
+        int timeVisibility = !hasVisibleText
+                || mRow.getStatusBarNotification().getNotification().showsTime()
+                ? View.VISIBLE : View.GONE;
+        time.setVisibility(timeVisibility);
+        View left = null;
+        View right;
+        for (int i = 1; i < childCount - 1 ; i++) {
+            View child = rowHeader.getChildAt(i);
+            if (mDividers.contains(Integer.valueOf(child.getId()))) {
+                boolean visible = false;
+                // Lets find the item to the right
+                for (i++; i < childCount - 1; i++) {
+                    right = rowHeader.getChildAt(i);
+                    if (mDividers.contains(Integer.valueOf(right.getId()))) {
+                        // A divider was found, this needs to be hidden
+                        i--;
+                        break;
+                    } else if (right.getVisibility() != View.GONE && right instanceof TextView) {
+                        visible = left != null;
+                        left = right;
+                        break;
+                    }
+                }
+                child.setVisibility(visible ? View.VISIBLE : View.GONE);
+            } else if (child.getVisibility() != View.GONE && child instanceof TextView) {
+                left = child;
+            }
+        }
+    }
+
+    public void restoreNotificationHeader(ExpandableNotificationRow row) {
+        for (int compI = 0; compI < mComparators.size(); compI++) {
+            mComparators.get(compI).apply(row, true /* reset */);
+        }
+        sanitizeHeaderViews(row);
+    }
+
+    private static class HeaderProcessor {
+        private final int mId;
+        private final DataExtractor mExtractor;
+        private final ResultApplicator mApplicator;
+        private final ExpandableNotificationRow mParentRow;
+        private boolean mApply;
+        private View mParentView;
+        private ViewComparator mComparator;
+        private Object mParentData;
+
+        public static HeaderProcessor forTextView(ExpandableNotificationRow row, int id) {
+            return new HeaderProcessor(row, id, null, sTextViewComparator, sVisibilityApplicator);
+        }
+
+        HeaderProcessor(ExpandableNotificationRow row, int id, DataExtractor extractor,
+                ViewComparator comparator,
+                ResultApplicator applicator) {
+            mId = id;
+            mExtractor = extractor;
+            mApplicator = applicator;
+            mComparator = comparator;
+            mParentRow = row;
+        }
+
+        public void init() {
+            mParentView = mParentRow.getNotificationHeader().findViewById(mId);
+            mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow);
+            mApply = !mComparator.isEmpty(mParentView);
+        }
+        public void compareToHeader(ExpandableNotificationRow row) {
+            if (!mApply) {
+                return;
+            }
+            NotificationHeaderView header = row.getContractedNotificationHeader();
+            if (header == null) {
+                // No header found. We still consider this to be the same to avoid weird flickering
+                // when for example showing an undo notification
+                return;
+            }
+            Object childData = mExtractor == null ? null : mExtractor.extractData(row);
+            mApply = mComparator.compare(mParentView, header.findViewById(mId),
+                    mParentData, childData);
+        }
+
+        public void apply(ExpandableNotificationRow row) {
+            apply(row, false /* reset */);
+        }
+
+        public void apply(ExpandableNotificationRow row, boolean reset) {
+            boolean apply = mApply && !reset;
+            if (row.isSummaryWithChildren()) {
+                applyToView(apply, row.getNotificationHeader());
+                return;
+            }
+            applyToView(apply, row.getPrivateLayout().getContractedChild());
+            applyToView(apply, row.getPrivateLayout().getHeadsUpChild());
+            applyToView(apply, row.getPrivateLayout().getExpandedChild());
+        }
+
+        private void applyToView(boolean apply, View parent) {
+            if (parent != null) {
+                View view = parent.findViewById(mId);
+                if (view != null && !mComparator.isEmpty(view)) {
+                    mApplicator.apply(view, apply);
+                }
+            }
+        }
+    }
+
+    private interface ViewComparator {
+        /**
+         * @param parent the parent view
+         * @param child the child view
+         * @param parentData optional data for the parent
+         * @param childData optional data for the child
+         * @return whether to views are the same
+         */
+        boolean compare(View parent, View child, Object parentData, Object childData);
+        boolean isEmpty(View view);
+    }
+
+    private interface DataExtractor {
+        Object extractData(ExpandableNotificationRow row);
+    }
+
+    private static class TextViewComparator implements ViewComparator {
+        @Override
+        public boolean compare(View parent, View child, Object parentData, Object childData) {
+            TextView parentView = (TextView) parent;
+            TextView childView = (TextView) child;
+            return parentView.getText().equals(childView.getText());
+        }
+
+        @Override
+        public boolean isEmpty(View view) {
+            return TextUtils.isEmpty(((TextView) view).getText());
+        }
+    }
+
+    private static abstract class IconComparator implements ViewComparator {
+        @Override
+        public boolean compare(View parent, View child, Object parentData, Object childData) {
+            return false;
+        }
+
+        protected boolean hasSameIcon(Object parentData, Object childData) {
+            Icon parentIcon = ((Notification) parentData).getSmallIcon();
+            Icon childIcon = ((Notification) childData).getSmallIcon();
+            return parentIcon.sameAs(childIcon);
+        }
+
+        /**
+         * @return whether two ImageViews have the same colorFilterSet or none at all
+         */
+        protected boolean hasSameColor(Object parentData, Object childData) {
+            int parentColor = ((Notification) parentData).color;
+            int childColor = ((Notification) childData).color;
+            return parentColor == childColor;
+        }
+
+        @Override
+        public boolean isEmpty(View view) {
+            return false;
+        }
+    }
+
+    private interface ResultApplicator {
+        void apply(View view, boolean apply);
+    }
+
+    private static class VisibilityApplicator implements ResultApplicator {
+
+        @Override
+        public void apply(View view, boolean apply) {
+            view.setVisibility(apply ? View.GONE : View.VISIBLE);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/NotificationInfo.java b/com/android/systemui/statusbar/NotificationInfo.java
new file mode 100644
index 0000000..3b23a0c
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationInfo.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2017 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 static android.app.NotificationManager.IMPORTANCE_NONE;
+
+import android.app.INotificationManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.RemoteException;
+import android.service.notification.StatusBarNotification;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.settingslib.Utils;
+import com.android.systemui.R;
+
+import java.lang.IllegalArgumentException;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The guts of a notification revealed when performing a long press.
+ */
+public class NotificationInfo extends LinearLayout implements NotificationGuts.GutsContent {
+    private static final String TAG = "InfoGuts";
+
+    private INotificationManager mINotificationManager;
+    private String mPkg;
+    private String mAppName;
+    private int mAppUid;
+    private List<NotificationChannel> mNotificationChannels;
+    private NotificationChannel mSingleNotificationChannel;
+    private boolean mIsSingleDefaultChannel;
+    private StatusBarNotification mSbn;
+    private int mStartingUserImportance;
+
+    private TextView mNumChannelsView;
+    private View mChannelDisabledView;
+    private TextView mSettingsLinkView;
+    private Switch mChannelEnabledSwitch;
+    private CheckSaveListener mCheckSaveListener;
+    private OnAppSettingsClickListener mAppSettingsClickListener;
+    private PackageManager mPm;
+
+    private NotificationGuts mGutsContainer;
+
+    public NotificationInfo(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    // Specify a CheckSaveListener to override when/if the user's changes are committed.
+    public interface CheckSaveListener {
+        // Invoked when importance has changed and the NotificationInfo wants to try to save it.
+        // Listener should run saveImportance unless the change should be canceled.
+        void checkSave(Runnable saveImportance);
+    }
+
+    public interface OnSettingsClickListener {
+        void onClick(View v, NotificationChannel channel, int appUid);
+    }
+
+    public interface OnAppSettingsClickListener {
+        void onClick(View v, Intent intent);
+    }
+
+    public void bindNotification(final PackageManager pm,
+            final INotificationManager iNotificationManager,
+            final String pkg,
+            final List<NotificationChannel> notificationChannels,
+            int startingUserImportance,
+            final StatusBarNotification sbn,
+            OnSettingsClickListener onSettingsClick,
+            OnAppSettingsClickListener onAppSettingsClick,
+            OnClickListener onDoneClick,
+            CheckSaveListener checkSaveListener,
+            final Set<String> nonBlockablePkgs)
+            throws RemoteException {
+        mINotificationManager = iNotificationManager;
+        mPkg = pkg;
+        mNotificationChannels = notificationChannels;
+        mCheckSaveListener = checkSaveListener;
+        mSbn = sbn;
+        mPm = pm;
+        mAppSettingsClickListener = onAppSettingsClick;
+        mStartingUserImportance = startingUserImportance;
+        mAppName = mPkg;
+        Drawable pkgicon = null;
+        CharSequence channelNameText = "";
+        ApplicationInfo info = null;
+        try {
+            info = pm.getApplicationInfo(mPkg,
+                    PackageManager.MATCH_UNINSTALLED_PACKAGES
+                            | PackageManager.MATCH_DISABLED_COMPONENTS
+                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+                            | PackageManager.MATCH_DIRECT_BOOT_AWARE);
+            if (info != null) {
+                mAppUid = sbn.getUid();
+                mAppName = String.valueOf(pm.getApplicationLabel(info));
+                pkgicon = pm.getApplicationIcon(info);
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            // app is gone, just show package name and generic icon
+            pkgicon = pm.getDefaultActivityIcon();
+        }
+        ((ImageView) findViewById(R.id.pkgicon)).setImageDrawable(pkgicon);
+
+        int numTotalChannels = iNotificationManager.getNumNotificationChannelsForPackage(
+                pkg, mAppUid, false /* includeDeleted */);
+        if (mNotificationChannels.isEmpty()) {
+            throw new IllegalArgumentException("bindNotification requires at least one channel");
+        } else  {
+            if (mNotificationChannels.size() == 1) {
+                mSingleNotificationChannel = mNotificationChannels.get(0);
+                // Special behavior for the Default channel if no other channels have been defined.
+                mIsSingleDefaultChannel =
+                        (mSingleNotificationChannel.getId()
+                                .equals(NotificationChannel.DEFAULT_CHANNEL_ID) &&
+                        numTotalChannels <= 1);
+            } else {
+                mSingleNotificationChannel = null;
+                mIsSingleDefaultChannel = false;
+            }
+        }
+
+        boolean nonBlockable = false;
+        try {
+            final PackageInfo pkgInfo = pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES);
+            if (Utils.isSystemPackage(getResources(), pm, pkgInfo)) {
+                final int numChannels = mNotificationChannels.size();
+                for (int i = 0; i < numChannels; i++) {
+                    final NotificationChannel notificationChannel = mNotificationChannels.get(i);
+                    // If any of the system channels is not blockable, the bundle is nonblockable
+                    if (!notificationChannel.isBlockableSystem()) {
+                        nonBlockable = true;
+                        break;
+                    }
+                }
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            // unlikely.
+        }
+        if (nonBlockablePkgs != null) {
+            nonBlockable |= nonBlockablePkgs.contains(pkg);
+        }
+
+        String channelsDescText;
+        mNumChannelsView = findViewById(R.id.num_channels_desc);
+        if (nonBlockable) {
+            channelsDescText = mContext.getString(R.string.notification_unblockable_desc);
+        } else if (mIsSingleDefaultChannel) {
+            channelsDescText = mContext.getString(R.string.notification_default_channel_desc);
+        } else {
+            switch (mNotificationChannels.size()) {
+                case 1:
+                    channelsDescText = String.format(mContext.getResources().getQuantityString(
+                            R.plurals.notification_num_channels_desc, numTotalChannels),
+                            numTotalChannels);
+                    break;
+                case 2:
+                    channelsDescText = mContext.getString(
+                            R.string.notification_channels_list_desc_2,
+                            mNotificationChannels.get(0).getName(),
+                            mNotificationChannels.get(1).getName());
+                    break;
+                default:
+                    final int numOthers = mNotificationChannels.size() - 2;
+                    channelsDescText = String.format(
+                            mContext.getResources().getQuantityString(
+                                    R.plurals.notification_channels_list_desc_2_and_others,
+                                    numOthers),
+                            mNotificationChannels.get(0).getName(),
+                            mNotificationChannels.get(1).getName(),
+                            numOthers);
+            }
+        }
+        mNumChannelsView.setText(channelsDescText);
+
+        if (mSingleNotificationChannel == null) {
+            // Multiple channels don't use a channel name for the title.
+            channelNameText = mContext.getString(R.string.notification_num_channels,
+                    mNotificationChannels.size());
+        } else if (mIsSingleDefaultChannel || nonBlockable) {
+            // If this is the default channel or the app is unblockable,
+            // don't use our channel-specific text.
+            channelNameText = mContext.getString(R.string.notification_header_default_channel);
+        } else {
+            channelNameText = mSingleNotificationChannel.getName();
+        }
+        ((TextView) findViewById(R.id.pkgname)).setText(mAppName);
+        ((TextView) findViewById(R.id.channel_name)).setText(channelNameText);
+
+        // Set group information if this channel has an associated group.
+        CharSequence groupName = null;
+        if (mSingleNotificationChannel != null && mSingleNotificationChannel.getGroup() != null) {
+            final NotificationChannelGroup notificationChannelGroup =
+                    iNotificationManager.getNotificationChannelGroupForPackage(
+                            mSingleNotificationChannel.getGroup(), pkg, mAppUid);
+            if (notificationChannelGroup != null) {
+                groupName = notificationChannelGroup.getName();
+            }
+        }
+        TextView groupNameView = ((TextView) findViewById(R.id.group_name));
+        TextView groupDividerView = ((TextView) findViewById(R.id.pkg_group_divider));
+        if (groupName != null) {
+            groupNameView.setText(groupName);
+            groupNameView.setVisibility(View.VISIBLE);
+            groupDividerView.setVisibility(View.VISIBLE);
+        } else {
+            groupNameView.setVisibility(View.GONE);
+            groupDividerView.setVisibility(View.GONE);
+        }
+
+        bindButtons(nonBlockable);
+
+        // Top-level importance group
+        mChannelDisabledView = findViewById(R.id.channel_disabled);
+        updateSecondaryText();
+
+        // Settings button.
+        final TextView settingsButton = (TextView) findViewById(R.id.more_settings);
+        if (mAppUid >= 0 && onSettingsClick != null) {
+            settingsButton.setVisibility(View.VISIBLE);
+            final int appUidF = mAppUid;
+            settingsButton.setOnClickListener(
+                    (View view) -> {
+                        onSettingsClick.onClick(view, mSingleNotificationChannel, appUidF);
+                    });
+            if (numTotalChannels <= 1 || nonBlockable) {
+                settingsButton.setText(R.string.notification_more_settings);
+            } else {
+                settingsButton.setText(R.string.notification_all_categories);
+            }
+        } else {
+            settingsButton.setVisibility(View.GONE);
+        }
+
+        // Done button.
+        final TextView doneButton = (TextView) findViewById(R.id.done);
+        doneButton.setText(R.string.notification_done);
+        doneButton.setOnClickListener(onDoneClick);
+
+        // Optional settings link
+        updateAppSettingsLink();
+    }
+
+    private boolean hasImportanceChanged() {
+        return mSingleNotificationChannel != null &&
+                mChannelEnabledSwitch != null &&
+                mStartingUserImportance != getSelectedImportance();
+    }
+
+    private void saveImportance() {
+        if (!hasImportanceChanged()) {
+            return;
+        }
+        final int selectedImportance = getSelectedImportance();
+        MetricsLogger.action(mContext, MetricsEvent.ACTION_SAVE_IMPORTANCE,
+                selectedImportance - mStartingUserImportance);
+        mSingleNotificationChannel.setImportance(selectedImportance);
+        mSingleNotificationChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
+        try {
+            mINotificationManager.updateNotificationChannelForPackage(
+                    mPkg, mAppUid, mSingleNotificationChannel);
+        } catch (RemoteException e) {
+            // :(
+        }
+    }
+
+    private int getSelectedImportance() {
+        if (!mChannelEnabledSwitch.isChecked()) {
+            return IMPORTANCE_NONE;
+        } else {
+            return mStartingUserImportance;
+        }
+    }
+
+    private void bindButtons(final boolean nonBlockable) {
+        // Enabled Switch
+        mChannelEnabledSwitch = (Switch) findViewById(R.id.channel_enabled_switch);
+        mChannelEnabledSwitch.setChecked(
+                mStartingUserImportance != IMPORTANCE_NONE);
+        final boolean visible = !nonBlockable && mSingleNotificationChannel != null;
+        mChannelEnabledSwitch.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+
+        // Callback when checked.
+        mChannelEnabledSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+            if (mGutsContainer != null) {
+                mGutsContainer.resetFalsingCheck();
+            }
+            updateSecondaryText();
+            updateAppSettingsLink();
+        });
+    }
+
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEvent(event);
+        if (mGutsContainer != null &&
+                event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+            if (mGutsContainer.isExposed()) {
+                event.getText().add(mContext.getString(
+                        R.string.notification_channel_controls_opened_accessibility, mAppName));
+            } else {
+                event.getText().add(mContext.getString(
+                        R.string.notification_channel_controls_closed_accessibility, mAppName));
+            }
+        }
+    }
+
+    private void updateSecondaryText() {
+        final boolean disabled = mSingleNotificationChannel != null &&
+                getSelectedImportance() == IMPORTANCE_NONE;
+        if (disabled) {
+            mChannelDisabledView.setVisibility(View.VISIBLE);
+            mNumChannelsView.setVisibility(View.GONE);
+        } else {
+            mChannelDisabledView.setVisibility(View.GONE);
+            mNumChannelsView.setVisibility(mIsSingleDefaultChannel ? View.INVISIBLE : View.VISIBLE);
+        }
+    }
+
+    private void updateAppSettingsLink() {
+        mSettingsLinkView = findViewById(R.id.app_settings);
+        Intent settingsIntent = getAppSettingsIntent(mPm, mPkg, mSingleNotificationChannel,
+                mSbn.getId(), mSbn.getTag());
+        if (settingsIntent != null && getSelectedImportance() != IMPORTANCE_NONE
+                && !TextUtils.isEmpty(mSbn.getNotification().getSettingsText())) {
+            mSettingsLinkView.setVisibility(View.VISIBLE);
+            mSettingsLinkView.setText(mContext.getString(R.string.notification_app_settings,
+                    mSbn.getNotification().getSettingsText()));
+            mSettingsLinkView.setOnClickListener((View view) -> {
+                mAppSettingsClickListener.onClick(view, settingsIntent);
+            });
+        } else {
+            mSettingsLinkView.setVisibility(View.GONE);
+        }
+    }
+
+    private Intent getAppSettingsIntent(PackageManager pm, String packageName,
+            NotificationChannel channel, int id, String tag) {
+        Intent intent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)
+                .setPackage(packageName);
+        final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(
+                intent,
+                PackageManager.MATCH_DEFAULT_ONLY
+        );
+        if (resolveInfos == null || resolveInfos.size() == 0 || resolveInfos.get(0) == null) {
+            return null;
+        }
+        final ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
+        intent.setClassName(activityInfo.packageName, activityInfo.name);
+        if (channel != null) {
+            intent.putExtra(Notification.EXTRA_CHANNEL_ID, channel.getId());
+        }
+        intent.putExtra(Notification.EXTRA_NOTIFICATION_ID, id);
+        intent.putExtra(Notification.EXTRA_NOTIFICATION_TAG, tag);
+        return intent;
+    }
+
+    @Override
+    public void setGutsParent(NotificationGuts guts) {
+        mGutsContainer = guts;
+    }
+
+    @Override
+    public boolean willBeRemoved() {
+        return mChannelEnabledSwitch != null && !mChannelEnabledSwitch.isChecked();
+    }
+
+    @Override
+    public View getContentView() {
+        return this;
+    }
+
+    @Override
+    public boolean handleCloseControls(boolean save, boolean force) {
+        if (save && hasImportanceChanged()) {
+            if (mCheckSaveListener != null) {
+                mCheckSaveListener.checkSave(() -> { saveImportance(); });
+            } else {
+                saveImportance();
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public int getActualHeight() {
+        return getHeight();
+    }
+}
diff --git a/com/android/systemui/statusbar/NotificationMenuRow.java b/com/android/systemui/statusbar/NotificationMenuRow.java
new file mode 100644
index 0000000..99b4b07
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationMenuRow.java
@@ -0,0 +1,664 @@
+/*
+ * Copyright (C) 2016 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 static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION;
+
+import java.util.ArrayList;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
+import com.android.systemui.statusbar.NotificationGuts.GutsContent;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.app.Notification;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.service.notification.StatusBarNotification;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+
+public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnClickListener,
+        ExpandableNotificationRow.LayoutListener {
+
+    private static final boolean DEBUG = false;
+    private static final String TAG = "swipe";
+
+    private static final int ICON_ALPHA_ANIM_DURATION = 200;
+    private static final long SHOW_MENU_DELAY = 60;
+    private static final long SWIPE_MENU_TIMING = 200;
+
+    // Notification must be swiped at least this fraction of a single menu item to show menu
+    private static final float SWIPED_FAR_ENOUGH_MENU_FRACTION = 0.25f;
+    private static final float SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION = 0.15f;
+
+    // When the menu is displayed, the notification must be swiped within this fraction of a single
+    // menu item to snap back to menu (else it will cover the menu or it'll be dismissed)
+    private static final float SWIPED_BACK_ENOUGH_TO_COVER_FRACTION = 0.2f;
+
+    private ExpandableNotificationRow mParent;
+
+    private Context mContext;
+    private FrameLayout mMenuContainer;
+    private MenuItem mInfoItem;
+    private ArrayList<MenuItem> mMenuItems;
+    private OnMenuEventListener mMenuListener;
+
+    private ValueAnimator mFadeAnimator;
+    private boolean mAnimating;
+    private boolean mMenuFadedIn;
+
+    private boolean mOnLeft;
+    private boolean mIconsPlaced;
+
+    private boolean mDismissing;
+    private boolean mSnapping;
+    private float mTranslation;
+
+    private int[] mIconLocation = new int[2];
+    private int[] mParentLocation = new int[2];
+
+    private float mHorizSpaceForIcon = -1;
+    private int mVertSpaceForIcons = -1;
+    private int mIconPadding = -1;
+
+    private float mAlpha = 0f;
+    private float mPrevX;
+
+    private CheckForDrag mCheckForDrag;
+    private Handler mHandler;
+
+    private boolean mMenuSnappedTo;
+    private boolean mMenuSnappedOnLeft;
+    private boolean mShouldShowMenu;
+
+    private NotificationSwipeActionHelper mSwipeHelper;
+    private boolean mIsUserTouching;
+
+    public NotificationMenuRow(Context context) {
+        mContext = context;
+        mShouldShowMenu = context.getResources().getBoolean(R.bool.config_showNotificationGear);
+        mHandler = new Handler(Looper.getMainLooper());
+        mMenuItems = new ArrayList<>();
+    }
+
+    @Override
+    public ArrayList<MenuItem> getMenuItems(Context context) {
+        return mMenuItems;
+    }
+
+    @Override
+    public MenuItem getLongpressMenuItem(Context context) {
+        return mInfoItem;
+    }
+
+    @Override
+    public void setSwipeActionHelper(NotificationSwipeActionHelper helper) {
+        mSwipeHelper = helper;
+    }
+
+    @Override
+    public void setMenuClickListener(OnMenuEventListener listener) {
+        mMenuListener = listener;
+    }
+
+    @Override
+    public void createMenu(ViewGroup parent, StatusBarNotification sbn) {
+        mParent = (ExpandableNotificationRow) parent;
+        createMenuViews(true /* resetState */);
+    }
+
+    @Override
+    public boolean isMenuVisible() {
+        return mAlpha > 0;
+    }
+
+    @Override
+    public View getMenuView() {
+        return mMenuContainer;
+    }
+
+    @Override
+    public void resetMenu() {
+        resetState(true);
+    }
+
+    @Override
+    public void onNotificationUpdated(StatusBarNotification sbn) {
+        if (mMenuContainer == null) {
+            // Menu hasn't been created yet, no need to do anything.
+            return;
+        }
+        createMenuViews(!isMenuVisible() /* resetState */);
+    }
+
+    @Override
+    public void onConfigurationChanged() {
+        mParent.setLayoutListener(this);
+    }
+
+    @Override
+    public void onLayout() {
+        mIconsPlaced = false; // Force icons to be re-placed
+        setMenuLocation();
+        mParent.removeListener();
+    }
+
+    private void createMenuViews(boolean resetState) {
+        final Resources res = mContext.getResources();
+        mHorizSpaceForIcon = res.getDimensionPixelSize(R.dimen.notification_menu_icon_size);
+        mVertSpaceForIcons = res.getDimensionPixelSize(R.dimen.notification_min_height);
+        mIconPadding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding);
+        mMenuItems.clear();
+        // Construct the menu items based on the notification
+        if (mParent != null && mParent.getStatusBarNotification() != null) {
+            int flags = mParent.getStatusBarNotification().getNotification().flags;
+            boolean isForeground = (flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
+            if (!isForeground) {
+                // Only show snooze for non-foreground notifications
+                mMenuItems.add(createSnoozeItem(mContext));
+            }
+        }
+        mInfoItem = createInfoItem(mContext);
+        mMenuItems.add(mInfoItem);
+
+        // Construct the menu views
+        if (mMenuContainer != null) {
+            mMenuContainer.removeAllViews();
+        } else {
+            mMenuContainer = new FrameLayout(mContext);
+        }
+        for (int i = 0; i < mMenuItems.size(); i++) {
+            addMenuView(mMenuItems.get(i), mMenuContainer);
+        }
+        if (resetState) {
+            resetState(false /* notify */);
+        } else {
+            mIconsPlaced = false;
+            setMenuLocation();
+            if (!mIsUserTouching) {
+                // If the # of items showing changed we need to update the snap position
+                showMenu(mParent, mOnLeft ? getSpaceForMenu() : -getSpaceForMenu(),
+                        0 /* velocity */);
+            }
+        }
+    }
+
+    private void resetState(boolean notify) {
+        setMenuAlpha(0f);
+        mIconsPlaced = false;
+        mMenuFadedIn = false;
+        mAnimating = false;
+        mSnapping = false;
+        mDismissing = false;
+        mMenuSnappedTo = false;
+        setMenuLocation();
+        if (mMenuListener != null && notify) {
+            mMenuListener.onMenuReset(mParent);
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(View view, MotionEvent ev, float velocity) {
+        final int action = ev.getActionMasked();
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                mSnapping = false;
+                if (mFadeAnimator != null) {
+                    mFadeAnimator.cancel();
+                }
+                mHandler.removeCallbacks(mCheckForDrag);
+                mCheckForDrag = null;
+                mPrevX = ev.getRawX();
+                mIsUserTouching = true;
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                mSnapping = false;
+                float diffX = ev.getRawX() - mPrevX;
+                mPrevX = ev.getRawX();
+                if (!isTowardsMenu(diffX) && isMenuLocationChange()) {
+                    // Don't consider it "snapped" if location has changed.
+                    mMenuSnappedTo = false;
+
+                    // Changed directions, make sure we check to fade in icon again.
+                    if (!mHandler.hasCallbacks(mCheckForDrag)) {
+                        // No check scheduled, set null to schedule a new one.
+                        mCheckForDrag = null;
+                    } else {
+                        // Check scheduled, reset alpha and update location; check will fade it in
+                        setMenuAlpha(0f);
+                        setMenuLocation();
+                    }
+                }
+                if (mShouldShowMenu
+                        && !NotificationStackScrollLayout.isPinnedHeadsUp(view)
+                        && !mParent.areGutsExposed()
+                        && !mParent.isDark()
+                        && (mCheckForDrag == null || !mHandler.hasCallbacks(mCheckForDrag))) {
+                    // Only show the menu if we're not a heads up view and guts aren't exposed.
+                    mCheckForDrag = new CheckForDrag();
+                    mHandler.postDelayed(mCheckForDrag, SHOW_MENU_DELAY);
+                }
+                break;
+
+            case MotionEvent.ACTION_UP:
+                mIsUserTouching = false;
+                return handleUpEvent(ev, view, velocity);
+            case MotionEvent.ACTION_CANCEL:
+                mIsUserTouching = false;
+                cancelDrag();
+                return false;
+        }
+        return false;
+    }
+
+    private boolean handleUpEvent(MotionEvent ev, View animView, float velocity) {
+        // If the menu should not be shown, then there is no need to check if the a swipe
+        // should result in a snapping to the menu. As a result, just check if the swipe
+        // was enough to dismiss the notification.
+        if (!mShouldShowMenu) {
+            if (mSwipeHelper.isDismissGesture(ev)) {
+                dismiss(animView, velocity);
+            } else {
+                snapBack(animView, velocity);
+            }
+            return true;
+        }
+
+        final boolean gestureTowardsMenu = isTowardsMenu(velocity);
+        final boolean gestureFastEnough =
+                mSwipeHelper.getMinDismissVelocity() <= Math.abs(velocity);
+        final boolean gestureFarEnough =
+                mSwipeHelper.swipedFarEnough(mTranslation, mParent.getWidth());
+        final double timeForGesture = ev.getEventTime() - ev.getDownTime();
+        final boolean showMenuForSlowOnGoing = !mParent.canViewBeDismissed()
+                && timeForGesture >= SWIPE_MENU_TIMING;
+        final float menuSnapTarget = mOnLeft ? getSpaceForMenu() : -getSpaceForMenu();
+
+        if (DEBUG) {
+            Log.d(TAG, "mTranslation= " + mTranslation
+                    + " mAlpha= " + mAlpha
+                    + " velocity= " + velocity
+                    + " mMenuSnappedTo= " + mMenuSnappedTo
+                    + " mMenuSnappedOnLeft= " + mMenuSnappedOnLeft
+                    + " mOnLeft= " + mOnLeft
+                    + " minDismissVel= " + mSwipeHelper.getMinDismissVelocity()
+                    + " isDismissGesture= " + mSwipeHelper.isDismissGesture(ev)
+                    + " gestureTowardsMenu= " + gestureTowardsMenu
+                    + " gestureFastEnough= " + gestureFastEnough
+                    + " gestureFarEnough= " + gestureFarEnough);
+        }
+
+        if (mMenuSnappedTo && isMenuVisible() && mMenuSnappedOnLeft == mOnLeft) {
+            // Menu was snapped to previously and we're on the same side, figure out if
+            // we should stick to the menu, snap back into place, or dismiss
+            final float maximumSwipeDistance = mHorizSpaceForIcon
+                    * SWIPED_BACK_ENOUGH_TO_COVER_FRACTION;
+            final float targetLeft = getSpaceForMenu() - maximumSwipeDistance;
+            final float targetRight = mParent.getWidth() * SWIPED_FAR_ENOUGH_SIZE_FRACTION;
+            boolean withinSnapMenuThreshold = mOnLeft
+                    ? mTranslation > targetLeft && mTranslation < targetRight
+                    : mTranslation < -targetLeft && mTranslation > -targetRight;
+            boolean shouldSnapTo = mOnLeft ? mTranslation < targetLeft : mTranslation > -targetLeft;
+            if (DEBUG) {
+                Log.d(TAG, "   withinSnapMenuThreshold= " + withinSnapMenuThreshold
+                        + "   shouldSnapTo= " + shouldSnapTo
+                        + "   targetLeft= " + targetLeft
+                        + "   targetRight= " + targetRight);
+            }
+            if (withinSnapMenuThreshold && !mSwipeHelper.isDismissGesture(ev)) {
+                // Haven't moved enough to unsnap from the menu
+                showMenu(animView, menuSnapTarget, velocity);
+            } else if (mSwipeHelper.isDismissGesture(ev) && !shouldSnapTo) {
+                // Only dismiss if we're not moving towards the menu
+                dismiss(animView, velocity);
+            } else {
+                snapBack(animView, velocity);
+            }
+        } else if (!mSwipeHelper.isFalseGesture(ev)
+                && (swipedEnoughToShowMenu() && (!gestureFastEnough || showMenuForSlowOnGoing))
+                || (gestureTowardsMenu && !mSwipeHelper.isDismissGesture(ev))) {
+            // Menu has not been snapped to previously and this is menu revealing gesture
+            showMenu(animView, menuSnapTarget, velocity);
+        } else if (mSwipeHelper.isDismissGesture(ev) && !gestureTowardsMenu) {
+            dismiss(animView, velocity);
+        } else {
+            snapBack(animView, velocity);
+        }
+        return true;
+    }
+
+    private void showMenu(View animView, float targetLeft, float velocity) {
+        mMenuSnappedTo = true;
+        mMenuSnappedOnLeft = mOnLeft;
+        mMenuListener.onMenuShown(animView);
+        mSwipeHelper.snap(animView, targetLeft, velocity);
+    }
+
+    private void snapBack(View animView, float velocity) {
+        cancelDrag();
+        mMenuSnappedTo = false;
+        mSnapping = true;
+        mSwipeHelper.snap(animView, 0 /* leftTarget */, velocity);
+    }
+
+    private void dismiss(View animView, float velocity) {
+        cancelDrag();
+        mMenuSnappedTo = false;
+        mDismissing = true;
+        mSwipeHelper.dismiss(animView, velocity);
+    }
+
+    private void cancelDrag() {
+        if (mFadeAnimator != null) {
+            mFadeAnimator.cancel();
+        }
+        mHandler.removeCallbacks(mCheckForDrag);
+    }
+
+    /**
+     * @return whether the notification has been translated enough to show the menu and not enough
+     *         to be dismissed.
+     */
+    private boolean swipedEnoughToShowMenu() {
+        final float multiplier = mParent.canViewBeDismissed()
+                ? SWIPED_FAR_ENOUGH_MENU_FRACTION
+                : SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION;
+        final float minimumSwipeDistance = mHorizSpaceForIcon * multiplier;
+        return !mSwipeHelper.swipedFarEnough(0, 0) && isMenuVisible()
+                && (mOnLeft ? mTranslation > minimumSwipeDistance
+                        : mTranslation < -minimumSwipeDistance);
+    }
+
+    /**
+     * Returns whether the gesture is towards the menu location or not.
+     */
+    private boolean isTowardsMenu(float movement) {
+        return isMenuVisible()
+                && ((mOnLeft && movement <= 0)
+                        || (!mOnLeft && movement >= 0));
+    }
+
+    @Override
+    public void setAppName(String appName) {
+        if (appName == null) {
+            return;
+        }
+        Resources res = mContext.getResources();
+        final int count = mMenuItems.size();
+        for (int i = 0; i < count; i++) {
+            MenuItem item = mMenuItems.get(i);
+            String description = String.format(
+                    res.getString(R.string.notification_menu_accessibility),
+                    appName, item.getContentDescription());
+            View menuView = item.getMenuView();
+            if (menuView != null) {
+                menuView.setContentDescription(description);
+            }
+        }
+    }
+
+    @Override
+    public void onHeightUpdate() {
+        if (mParent == null || mMenuItems.size() == 0 || mMenuContainer == null) {
+            return;
+        }
+        int parentHeight = mParent.getActualHeight();
+        float translationY;
+        if (parentHeight < mVertSpaceForIcons) {
+            translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2);
+        } else {
+            translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2;
+        }
+        mMenuContainer.setTranslationY(translationY);
+    }
+
+    @Override
+    public void onTranslationUpdate(float translation) {
+        mTranslation = translation;
+        if (mAnimating || !mMenuFadedIn) {
+            // Don't adjust when animating, or if the menu hasn't been shown yet.
+            return;
+        }
+        final float fadeThreshold = mParent.getWidth() * 0.3f;
+        final float absTrans = Math.abs(translation);
+        float desiredAlpha = 0;
+        if (absTrans == 0) {
+            desiredAlpha = 0;
+        } else if (absTrans <= fadeThreshold) {
+            desiredAlpha = 1;
+        } else {
+            desiredAlpha = 1 - ((absTrans - fadeThreshold) / (mParent.getWidth() - fadeThreshold));
+        }
+        setMenuAlpha(desiredAlpha);
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (mMenuListener == null) {
+            // Nothing to do
+            return;
+        }
+        v.getLocationOnScreen(mIconLocation);
+        mParent.getLocationOnScreen(mParentLocation);
+        final int centerX = (int) (mHorizSpaceForIcon / 2);
+        final int centerY = v.getHeight() / 2;
+        final int x = mIconLocation[0] - mParentLocation[0] + centerX;
+        final int y = mIconLocation[1] - mParentLocation[1] + centerY;
+        final int index = mMenuContainer.indexOfChild(v);
+        mMenuListener.onMenuClicked(mParent, x, y, mMenuItems.get(index));
+    }
+
+    private boolean isMenuLocationChange() {
+        boolean onLeft = mTranslation > mIconPadding;
+        boolean onRight = mTranslation < -mIconPadding;
+        if ((mOnLeft && onRight) || (!mOnLeft && onLeft)) {
+            return true;
+        }
+        return false;
+    }
+
+    private void setMenuLocation() {
+        boolean showOnLeft = mTranslation > 0;
+        if ((mIconsPlaced && showOnLeft == mOnLeft) || mSnapping || mMenuContainer == null
+                || !mMenuContainer.isAttachedToWindow()) {
+            // Do nothing
+            return;
+        }
+        final int count = mMenuContainer.getChildCount();
+        for (int i = 0; i < count; i++) {
+            final View v = mMenuContainer.getChildAt(i);
+            final float left = i * mHorizSpaceForIcon;
+            final float right = mParent.getWidth() - (mHorizSpaceForIcon * (i + 1));
+            v.setX(showOnLeft ? left : right);
+        }
+        mOnLeft = showOnLeft;
+        mIconsPlaced = true;
+    }
+
+    private void setMenuAlpha(float alpha) {
+        mAlpha = alpha;
+        if (mMenuContainer == null) {
+            return;
+        }
+        if (alpha == 0) {
+            mMenuFadedIn = false; // Can fade in again once it's gone.
+            mMenuContainer.setVisibility(View.INVISIBLE);
+        } else {
+            mMenuContainer.setVisibility(View.VISIBLE);
+        }
+        final int count = mMenuContainer.getChildCount();
+        for (int i = 0; i < count; i++) {
+            mMenuContainer.getChildAt(i).setAlpha(mAlpha);
+        }
+    }
+
+    /**
+     * Returns the horizontal space in pixels required to display the menu.
+     */
+    private float getSpaceForMenu() {
+        return mHorizSpaceForIcon * mMenuContainer.getChildCount();
+    }
+
+    private final class CheckForDrag implements Runnable {
+        @Override
+        public void run() {
+            final float absTransX = Math.abs(mTranslation);
+            final float bounceBackToMenuWidth = getSpaceForMenu();
+            final float notiThreshold = mParent.getWidth() * 0.4f;
+            if ((!isMenuVisible() || isMenuLocationChange())
+                    && absTransX >= bounceBackToMenuWidth * 0.4
+                    && absTransX < notiThreshold) {
+                fadeInMenu(notiThreshold);
+            }
+        }
+    }
+
+    private void fadeInMenu(final float notiThreshold) {
+        if (mDismissing || mAnimating) {
+            return;
+        }
+        if (isMenuLocationChange()) {
+            setMenuAlpha(0f);
+        }
+        final float transX = mTranslation;
+        final boolean fromLeft = mTranslation > 0;
+        setMenuLocation();
+        mFadeAnimator = ValueAnimator.ofFloat(mAlpha, 1);
+        mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                final float absTrans = Math.abs(transX);
+
+                boolean pastMenu = (fromLeft && transX <= notiThreshold)
+                        || (!fromLeft && absTrans <= notiThreshold);
+                if (pastMenu && !mMenuFadedIn) {
+                    setMenuAlpha((float) animation.getAnimatedValue());
+                }
+            }
+        });
+        mFadeAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mAnimating = true;
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                // TODO should animate back to 0f from current alpha
+                setMenuAlpha(0f);
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mAnimating = false;
+                mMenuFadedIn = mAlpha == 1;
+            }
+        });
+        mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN);
+        mFadeAnimator.setDuration(ICON_ALPHA_ANIM_DURATION);
+        mFadeAnimator.start();
+    }
+
+    @Override
+    public void setMenuItems(ArrayList<MenuItem> items) {
+        // Do nothing we use our own for now.
+        // TODO -- handle / allow custom menu items!
+    }
+
+    public static MenuItem createSnoozeItem(Context context) {
+        Resources res = context.getResources();
+        NotificationSnooze content = (NotificationSnooze) LayoutInflater.from(context)
+                .inflate(R.layout.notification_snooze, null, false);
+        String snoozeDescription = res.getString(R.string.notification_menu_snooze_description);
+        MenuItem snooze = new NotificationMenuItem(context, snoozeDescription, content,
+                R.drawable.ic_snooze);
+        return snooze;
+    }
+
+    public static MenuItem createInfoItem(Context context) {
+        Resources res = context.getResources();
+        String infoDescription = res.getString(R.string.notification_menu_gear_description);
+        NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate(
+                R.layout.notification_info, null, false);
+        MenuItem info = new NotificationMenuItem(context, infoDescription, infoContent,
+                R.drawable.ic_settings);
+        return info;
+    }
+
+    private void addMenuView(MenuItem item, ViewGroup parent) {
+        View menuView = item.getMenuView();
+        if (menuView != null) {
+            parent.addView(menuView);
+            menuView.setOnClickListener(this);
+            FrameLayout.LayoutParams lp = (LayoutParams) menuView.getLayoutParams();
+            lp.width = (int) mHorizSpaceForIcon;
+            lp.height = (int) mHorizSpaceForIcon;
+            menuView.setLayoutParams(lp);
+        }
+    }
+
+    public static class NotificationMenuItem implements MenuItem {
+        View mMenuView;
+        GutsContent mGutsContent;
+        String mContentDescription;
+
+        public NotificationMenuItem(Context context, String s, GutsContent content, int iconResId) {
+            Resources res = context.getResources();
+            int padding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding);
+            int tint = res.getColor(R.color.notification_gear_color);
+            AlphaOptimizedImageView iv = new AlphaOptimizedImageView(context);
+            iv.setPadding(padding, padding, padding, padding);
+            Drawable icon = context.getResources().getDrawable(iconResId);
+            iv.setImageDrawable(icon);
+            iv.setColorFilter(tint);
+            iv.setAlpha(1f);
+            mMenuView = iv;
+            mContentDescription = s;
+            mGutsContent = content;
+        }
+
+        @Override
+        public View getMenuView() {
+            return mMenuView;
+        }
+
+        @Override
+        public View getGutsView() {
+            return mGutsContent.getContentView();
+        }
+
+        @Override
+        public String getContentDescription() {
+            return mContentDescription;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/NotificationShelf.java b/com/android/systemui/statusbar/NotificationShelf.java
new file mode 100644
index 0000000..5557dde
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationShelf.java
@@ -0,0 +1,825 @@
+/*
+ * Copyright (C) 2016 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 static com.android.systemui.statusbar.phone.NotificationIconContainer.IconState.NO_VALUE;
+import static com.android.systemui.statusbar.phone.NotificationIconContainer.OVERFLOW_EARLY_AMOUNT;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.os.SystemProperties;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.ViewInvertHelper;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.phone.NotificationIconContainer;
+import com.android.systemui.statusbar.phone.NotificationPanelView;
+import com.android.systemui.statusbar.stack.AmbientState;
+import com.android.systemui.statusbar.stack.AnimationProperties;
+import com.android.systemui.statusbar.stack.ExpandableViewState;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+import com.android.systemui.statusbar.stack.StackScrollState;
+import com.android.systemui.statusbar.stack.ViewState;
+
+/**
+ * A notification shelf view that is placed inside the notification scroller. It manages the
+ * overflow icons that don't fit into the regular list anymore.
+ */
+public class NotificationShelf extends ActivatableNotificationView implements
+        View.OnLayoutChangeListener {
+
+    public static final boolean SHOW_AMBIENT_ICONS = true;
+    private static final boolean USE_ANIMATIONS_WHEN_OPENING =
+            SystemProperties.getBoolean("debug.icon_opening_animations", true);
+    private static final boolean ICON_ANMATIONS_WHILE_SCROLLING
+            = SystemProperties.getBoolean("debug.icon_scroll_animations", true);
+    private static final int TAG_CONTINUOUS_CLIPPING = R.id.continuous_clipping_tag;
+    private ViewInvertHelper mViewInvertHelper;
+    private boolean mDark;
+    private NotificationIconContainer mShelfIcons;
+    private ShelfState mShelfState;
+    private int[] mTmp = new int[2];
+    private boolean mHideBackground;
+    private int mIconAppearTopPadding;
+    private int mStatusBarHeight;
+    private int mStatusBarPaddingStart;
+    private AmbientState mAmbientState;
+    private NotificationStackScrollLayout mHostLayout;
+    private int mMaxLayoutHeight;
+    private int mPaddingBetweenElements;
+    private int mNotGoneIndex;
+    private boolean mHasItemsInStableShelf;
+    private NotificationIconContainer mCollapsedIcons;
+    private int mScrollFastThreshold;
+    private int mIconSize;
+    private int mStatusBarState;
+    private float mMaxShelfEnd;
+    private int mRelativeOffset;
+    private boolean mInteractive;
+    private float mOpenedAmount;
+    private boolean mNoAnimationsInThisFrame;
+    private boolean mAnimationsEnabled = true;
+    private boolean mShowNotificationShelf;
+    private boolean mVibrationOnAnimation;
+    private boolean mUserTouchingScreen;
+    private boolean mTouchActive;
+
+    public NotificationShelf(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mShelfIcons = findViewById(R.id.content);
+        mShelfIcons.setClipChildren(false);
+        mShelfIcons.setClipToPadding(false);
+
+        setClipToActualHeight(false);
+        setClipChildren(false);
+        setClipToPadding(false);
+        mShelfIcons.setShowAllIcons(false);
+        mVibrationOnAnimation = mContext.getResources().getBoolean(
+                R.bool.config_vibrateOnIconAnimation);
+        updateVibrationOnAnimation();
+        mViewInvertHelper = new ViewInvertHelper(mShelfIcons,
+                NotificationPanelView.DOZE_ANIMATION_DURATION);
+        mShelfState = new ShelfState();
+        initDimens();
+    }
+
+    private void updateVibrationOnAnimation() {
+        mShelfIcons.setVibrateOnAnimation(mVibrationOnAnimation && mTouchActive);
+    }
+
+    public void setTouchActive(boolean touchActive) {
+        mTouchActive = touchActive;
+        updateVibrationOnAnimation();
+    }
+
+    public void bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout) {
+        mAmbientState = ambientState;
+        mHostLayout = hostLayout;
+    }
+
+    private void initDimens() {
+        Resources res = getResources();
+        mIconAppearTopPadding = res.getDimensionPixelSize(R.dimen.notification_icon_appear_padding);
+        mStatusBarHeight = res.getDimensionPixelOffset(R.dimen.status_bar_height);
+        mStatusBarPaddingStart = res.getDimensionPixelOffset(R.dimen.status_bar_padding_start);
+        mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height);
+
+        ViewGroup.LayoutParams layoutParams = getLayoutParams();
+        layoutParams.height = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
+        setLayoutParams(layoutParams);
+
+        int padding = res.getDimensionPixelOffset(R.dimen.shelf_icon_container_padding);
+        mShelfIcons.setPadding(padding, 0, padding, 0);
+        mScrollFastThreshold = res.getDimensionPixelOffset(R.dimen.scroll_fast_threshold);
+        mShowNotificationShelf = res.getBoolean(R.bool.config_showNotificationShelf);
+        mIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size);
+
+        if (!mShowNotificationShelf) {
+            setVisibility(GONE);
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        initDimens();
+    }
+
+    @Override
+    public void setDark(boolean dark, boolean fade, long delay) {
+        super.setDark(dark, fade, delay);
+        if (mDark == dark) return;
+        mDark = dark;
+        mShelfIcons.setDark(dark, fade, delay);
+        updateInteractiveness();
+    }
+
+    @Override
+    protected View getContentView() {
+        return mShelfIcons;
+    }
+
+    public NotificationIconContainer getShelfIcons() {
+        return mShelfIcons;
+    }
+
+    @Override
+    public ExpandableViewState createNewViewState(StackScrollState stackScrollState) {
+        return mShelfState;
+    }
+
+    public void updateState(StackScrollState resultState,
+            AmbientState ambientState) {
+        View lastView = ambientState.getLastVisibleBackgroundChild();
+        if (mShowNotificationShelf && lastView != null) {
+            float maxShelfEnd = ambientState.getInnerHeight() + ambientState.getTopPadding()
+                    + ambientState.getStackTranslation();
+            ExpandableViewState lastViewState = resultState.getViewStateForView(lastView);
+            float viewEnd = lastViewState.yTranslation + lastViewState.height;
+            mShelfState.copyFrom(lastViewState);
+            mShelfState.height = getIntrinsicHeight();
+            mShelfState.yTranslation = Math.max(Math.min(viewEnd, maxShelfEnd) - mShelfState.height,
+                    getFullyClosedTranslation());
+            mShelfState.zTranslation = ambientState.getBaseZHeight();
+            float openedAmount = (mShelfState.yTranslation - getFullyClosedTranslation())
+                    / (getIntrinsicHeight() * 2);
+            openedAmount = Math.min(1.0f, openedAmount);
+            mShelfState.openedAmount = openedAmount;
+            mShelfState.clipTopAmount = 0;
+            mShelfState.alpha = mAmbientState.hasPulsingNotifications() ? 0 : 1;
+            mShelfState.belowSpeedBump = mAmbientState.getSpeedBumpIndex() == 0;
+            mShelfState.shadowAlpha = 1.0f;
+            mShelfState.hideSensitive = false;
+            mShelfState.xTranslation = getTranslationX();
+            if (mNotGoneIndex != -1) {
+                mShelfState.notGoneIndex = Math.min(mShelfState.notGoneIndex, mNotGoneIndex);
+            }
+            mShelfState.hasItemsInStableShelf = lastViewState.inShelf;
+            mShelfState.hidden = !mAmbientState.isShadeExpanded()
+                    || mAmbientState.isQsCustomizerShowing();
+            mShelfState.maxShelfEnd = maxShelfEnd;
+        } else {
+            mShelfState.hidden = true;
+            mShelfState.location = ExpandableViewState.LOCATION_GONE;
+            mShelfState.hasItemsInStableShelf = false;
+        }
+    }
+
+    /**
+     * Update the shelf appearance based on the other notifications around it. This transforms
+     * the icons from the notification area into the shelf.
+     */
+    public void updateAppearance() {
+        // If the shelf should not be shown, then there is no need to update anything.
+        if (!mShowNotificationShelf) {
+            return;
+        }
+
+        mShelfIcons.resetViewStates();
+        float shelfStart = getTranslationY();
+        float numViewsInShelf = 0.0f;
+        View lastChild = mAmbientState.getLastVisibleBackgroundChild();
+        mNotGoneIndex = -1;
+        float interpolationStart = mMaxLayoutHeight - getIntrinsicHeight() * 2;
+        float expandAmount = 0.0f;
+        if (shelfStart >= interpolationStart) {
+            expandAmount = (shelfStart - interpolationStart) / getIntrinsicHeight();
+            expandAmount = Math.min(1.0f, expandAmount);
+        }
+        //  find the first view that doesn't overlap with the shelf
+        int notificationIndex = 0;
+        int notGoneIndex = 0;
+        int colorOfViewBeforeLast = NO_COLOR;
+        boolean backgroundForceHidden = false;
+        if (mHideBackground && !mShelfState.hasItemsInStableShelf) {
+            backgroundForceHidden = true;
+        }
+        int colorTwoBefore = NO_COLOR;
+        int previousColor = NO_COLOR;
+        float transitionAmount = 0.0f;
+        float currentScrollVelocity = mAmbientState.getCurrentScrollVelocity();
+        boolean scrollingFast = currentScrollVelocity > mScrollFastThreshold
+                || (mAmbientState.isExpansionChanging()
+                        && Math.abs(mAmbientState.getExpandingVelocity()) > mScrollFastThreshold);
+        boolean scrolling = currentScrollVelocity > 0;
+        boolean expandingAnimated = mAmbientState.isExpansionChanging()
+                && !mAmbientState.isPanelTracking();
+        int baseZHeight = mAmbientState.getBaseZHeight();
+        while (notificationIndex < mHostLayout.getChildCount()) {
+            ExpandableView child = (ExpandableView) mHostLayout.getChildAt(notificationIndex);
+            notificationIndex++;
+            if (!(child instanceof ExpandableNotificationRow)
+                    || child.getVisibility() == GONE) {
+                continue;
+            }
+            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+            float notificationClipEnd;
+            boolean aboveShelf = ViewState.getFinalTranslationZ(row) > baseZHeight;
+            boolean isLastChild = child == lastChild;
+            float rowTranslationY = row.getTranslationY();
+            if ((isLastChild && !child.isInShelf()) || aboveShelf || backgroundForceHidden) {
+                notificationClipEnd = shelfStart + getIntrinsicHeight();
+            } else {
+                notificationClipEnd = shelfStart - mPaddingBetweenElements;
+                float height = notificationClipEnd - rowTranslationY;
+                if (!row.isBelowSpeedBump() && height <= getNotificationMergeSize()) {
+                    // We want the gap to close when we reached the minimum size and only shrink
+                    // before
+                    notificationClipEnd = Math.min(shelfStart,
+                            rowTranslationY + getNotificationMergeSize());
+                }
+            }
+            updateNotificationClipHeight(row, notificationClipEnd);
+            float inShelfAmount = updateIconAppearance(row, expandAmount, scrolling, scrollingFast,
+                    expandingAnimated, isLastChild);
+            numViewsInShelf += inShelfAmount;
+            int ownColorUntinted = row.getBackgroundColorWithoutTint();
+            if (rowTranslationY >= shelfStart && mNotGoneIndex == -1) {
+                mNotGoneIndex = notGoneIndex;
+                setTintColor(previousColor);
+                setOverrideTintColor(colorTwoBefore, transitionAmount);
+
+            } else if (mNotGoneIndex == -1) {
+                colorTwoBefore = previousColor;
+                transitionAmount = inShelfAmount;
+            }
+            if (isLastChild) {
+                if (colorOfViewBeforeLast == NO_COLOR) {
+                    colorOfViewBeforeLast = ownColorUntinted;
+                }
+                row.setOverrideTintColor(colorOfViewBeforeLast, inShelfAmount);
+            } else {
+                colorOfViewBeforeLast = ownColorUntinted;
+                row.setOverrideTintColor(NO_COLOR, 0 /* overrideAmount */);
+            }
+            if (notGoneIndex != 0 || !aboveShelf) {
+                row.setAboveShelf(false);
+            }
+            notGoneIndex++;
+            previousColor = ownColorUntinted;
+        }
+        mShelfIcons.setSpeedBumpIndex(mAmbientState.getSpeedBumpIndex());
+        mShelfIcons.calculateIconTranslations();
+        mShelfIcons.applyIconStates();
+        for (int i = 0; i < mHostLayout.getChildCount(); i++) {
+            View child = mHostLayout.getChildAt(i);
+            if (!(child instanceof ExpandableNotificationRow)
+                    || child.getVisibility() == GONE) {
+                continue;
+            }
+            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+            updateIconClipAmount(row);
+            updateContinuousClipping(row);
+        }
+        boolean hideBackground = numViewsInShelf < 1.0f;
+        setHideBackground(hideBackground || backgroundForceHidden);
+        if (mNotGoneIndex == -1) {
+            mNotGoneIndex = notGoneIndex;
+        }
+    }
+
+    private void updateIconClipAmount(ExpandableNotificationRow row) {
+        float maxTop = row.getTranslationY();
+        StatusBarIconView icon = row.getEntry().expandedIcon;
+        float shelfIconPosition = getTranslationY() + icon.getTop() + icon.getTranslationY();
+        if (shelfIconPosition < maxTop) {
+            int top = (int) (maxTop - shelfIconPosition);
+            Rect clipRect = new Rect(0, top, icon.getWidth(), Math.max(top, icon.getHeight()));
+            icon.setClipBounds(clipRect);
+        } else {
+            icon.setClipBounds(null);
+        }
+    }
+
+    private void updateContinuousClipping(final ExpandableNotificationRow row) {
+        StatusBarIconView icon = row.getEntry().expandedIcon;
+        boolean needsContinuousClipping = ViewState.isAnimatingY(icon);
+        boolean isContinuousClipping = icon.getTag(TAG_CONTINUOUS_CLIPPING) != null;
+        if (needsContinuousClipping && !isContinuousClipping) {
+            ViewTreeObserver.OnPreDrawListener predrawListener =
+                    new ViewTreeObserver.OnPreDrawListener() {
+                        @Override
+                        public boolean onPreDraw() {
+                            boolean animatingY = ViewState.isAnimatingY(icon);
+                            if (!animatingY || !icon.isAttachedToWindow()) {
+                                icon.getViewTreeObserver().removeOnPreDrawListener(this);
+                                icon.setTag(TAG_CONTINUOUS_CLIPPING, null);
+                                return true;
+                            }
+                            updateIconClipAmount(row);
+                            return true;
+                        }
+                    };
+            icon.getViewTreeObserver().addOnPreDrawListener(predrawListener);
+            icon.setTag(TAG_CONTINUOUS_CLIPPING, predrawListener);
+        }
+    }
+
+    private void updateNotificationClipHeight(ExpandableNotificationRow row,
+            float notificationClipEnd) {
+        float viewEnd = row.getTranslationY() + row.getActualHeight();
+        boolean isPinned = (row.isPinned() || row.isHeadsUpAnimatingAway())
+                && !mAmbientState.isDozingAndNotPulsing(row);
+        if (viewEnd > notificationClipEnd
+                && (mAmbientState.isShadeExpanded() || !isPinned)) {
+            int clipBottomAmount = (int) (viewEnd - notificationClipEnd);
+            if (isPinned) {
+                clipBottomAmount = Math.min(row.getIntrinsicHeight() - row.getCollapsedHeight(),
+                        clipBottomAmount);
+            }
+            row.setClipBottomAmount(clipBottomAmount);
+        } else {
+            row.setClipBottomAmount(0);
+        }
+    }
+
+    @Override
+    public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
+            int outlineTranslation) {
+        if (!mHasItemsInStableShelf) {
+            shadowIntensity = 0.0f;
+        }
+        super.setFakeShadowIntensity(shadowIntensity, outlineAlpha, shadowYEnd, outlineTranslation);
+    }
+
+    /**
+     * @return the icon amount how much this notification is in the shelf;
+     */
+    private float updateIconAppearance(ExpandableNotificationRow row, float expandAmount,
+            boolean scrolling, boolean scrollingFast, boolean expandingAnimated,
+            boolean isLastChild) {
+        StatusBarIconView icon = row.getEntry().expandedIcon;
+        NotificationIconContainer.IconState iconState = getIconState(icon);
+        if (iconState == null) {
+            return 0.0f;
+        }
+
+        // Let calculate how much the view is in the shelf
+        float viewStart = row.getTranslationY();
+        int fullHeight = row.getActualHeight() + mPaddingBetweenElements;
+        float iconTransformDistance = getIntrinsicHeight() * 1.5f;
+        iconTransformDistance *= NotificationUtils.interpolate(1.f, 1.5f, expandAmount);
+        iconTransformDistance = Math.min(iconTransformDistance, fullHeight);
+        if (isLastChild) {
+            fullHeight = Math.min(fullHeight, row.getMinHeight() - getIntrinsicHeight());
+            iconTransformDistance = Math.min(iconTransformDistance, row.getMinHeight()
+                    - getIntrinsicHeight());
+        }
+        float viewEnd = viewStart + fullHeight;
+        if (expandingAnimated && mAmbientState.getScrollY() == 0
+                && !mAmbientState.isOnKeyguard() && !iconState.isLastExpandIcon) {
+            // We are expanding animated. Because we switch to a linear interpolation in this case,
+            // the last icon may be stuck in between the shelf position and the notification
+            // position, which looks pretty bad. We therefore optimize this case by applying a
+            // shorter transition such that the icon is either fully in the notification or we clamp
+            // it into the shelf if it's close enough.
+            // We need to persist this, since after the expansion, the behavior should still be the
+            // same.
+            float position = mAmbientState.getIntrinsicPadding()
+                    + mHostLayout.getPositionInLinearLayout(row);
+            int maxShelfStart = mMaxLayoutHeight - getIntrinsicHeight();
+            if (position < maxShelfStart && position + row.getIntrinsicHeight() >= maxShelfStart
+                    && row.getTranslationY() < position) {
+                iconState.isLastExpandIcon = true;
+                iconState.customTransformHeight = NO_VALUE;
+                // Let's check if we're close enough to snap into the shelf
+                boolean forceInShelf = mMaxLayoutHeight - getIntrinsicHeight() - position
+                        < getIntrinsicHeight();
+                if (!forceInShelf) {
+                    // We are overlapping the shelf but not enough, so the icon needs to be
+                    // repositioned
+                    iconState.customTransformHeight = (int) (mMaxLayoutHeight
+                            - getIntrinsicHeight() - position);
+                }
+            }
+        }
+        float fullTransitionAmount;
+        float iconTransitionAmount;
+        float shelfStart = getTranslationY();
+        if (iconState.hasCustomTransformHeight()) {
+            fullHeight = iconState.customTransformHeight;
+            iconTransformDistance = iconState.customTransformHeight;
+        }
+        boolean fullyInOrOut = true;
+        if (viewEnd >= shelfStart && (!mAmbientState.isUnlockHintRunning() || row.isInShelf())
+                && (mAmbientState.isShadeExpanded()
+                        || (!row.isPinned() && !row.isHeadsUpAnimatingAway()))) {
+            if (viewStart < shelfStart) {
+                float fullAmount = (shelfStart - viewStart) / fullHeight;
+                fullAmount = Math.min(1.0f, fullAmount);
+                float interpolatedAmount =  Interpolators.ACCELERATE_DECELERATE.getInterpolation(
+                        fullAmount);
+                interpolatedAmount = NotificationUtils.interpolate(
+                        interpolatedAmount, fullAmount, expandAmount);
+                fullTransitionAmount = 1.0f - interpolatedAmount;
+
+                iconTransitionAmount = (shelfStart - viewStart) / iconTransformDistance;
+                iconTransitionAmount = Math.min(1.0f, iconTransitionAmount);
+                iconTransitionAmount = 1.0f - iconTransitionAmount;
+                fullyInOrOut = false;
+            } else {
+                fullTransitionAmount = 1.0f;
+                iconTransitionAmount = 1.0f;
+            }
+        } else {
+            fullTransitionAmount = 0.0f;
+            iconTransitionAmount = 0.0f;
+        }
+        if (fullyInOrOut && !expandingAnimated && iconState.isLastExpandIcon) {
+            iconState.isLastExpandIcon = false;
+            iconState.customTransformHeight = NO_VALUE;
+        }
+        updateIconPositioning(row, iconTransitionAmount, fullTransitionAmount,
+                iconTransformDistance, scrolling, scrollingFast, expandingAnimated, isLastChild);
+        return fullTransitionAmount;
+    }
+
+    private void updateIconPositioning(ExpandableNotificationRow row, float iconTransitionAmount,
+            float fullTransitionAmount, float iconTransformDistance, boolean scrolling,
+            boolean scrollingFast, boolean expandingAnimated, boolean isLastChild) {
+        StatusBarIconView icon = row.getEntry().expandedIcon;
+        NotificationIconContainer.IconState iconState = getIconState(icon);
+        if (iconState == null) {
+            return;
+        }
+        boolean forceInShelf = iconState.isLastExpandIcon && !iconState.hasCustomTransformHeight();
+        float clampedAmount = iconTransitionAmount > 0.5f ? 1.0f : 0.0f;
+        if (clampedAmount == fullTransitionAmount) {
+            iconState.noAnimations = (scrollingFast || expandingAnimated) && !forceInShelf;
+            iconState.useFullTransitionAmount = iconState.noAnimations
+                || (!ICON_ANMATIONS_WHILE_SCROLLING && fullTransitionAmount == 0.0f && scrolling);
+            iconState.useLinearTransitionAmount = !ICON_ANMATIONS_WHILE_SCROLLING
+                    && fullTransitionAmount == 0.0f && !mAmbientState.isExpansionChanging();
+            iconState.translateContent = mMaxLayoutHeight - getTranslationY()
+                    - getIntrinsicHeight() > 0;
+        }
+        if (!forceInShelf && (scrollingFast || (expandingAnimated
+                && iconState.useFullTransitionAmount && !ViewState.isAnimatingY(icon)))) {
+            iconState.cancelAnimations(icon);
+            iconState.useFullTransitionAmount = true;
+            iconState.noAnimations = true;
+        }
+        if (iconState.hasCustomTransformHeight()) {
+            iconState.useFullTransitionAmount = true;
+        }
+        if (iconState.isLastExpandIcon) {
+            iconState.translateContent = false;
+        }
+        float transitionAmount;
+        if (isLastChild || !USE_ANIMATIONS_WHEN_OPENING || iconState.useFullTransitionAmount
+                || iconState.useLinearTransitionAmount) {
+            transitionAmount = iconTransitionAmount;
+        } else {
+            // We take the clamped position instead
+            transitionAmount = clampedAmount;
+            iconState.needsCannedAnimation = iconState.clampedAppearAmount != clampedAmount
+                    && !mNoAnimationsInThisFrame;
+        }
+        iconState.iconAppearAmount = !USE_ANIMATIONS_WHEN_OPENING
+                    || iconState.useFullTransitionAmount
+                ? fullTransitionAmount
+                : transitionAmount;
+        iconState.clampedAppearAmount = clampedAmount;
+        float contentTransformationAmount = !mAmbientState.isAboveShelf(row)
+                    && (isLastChild || iconState.translateContent)
+                ? iconTransitionAmount
+                : 0.0f;
+        row.setContentTransformationAmount(contentTransformationAmount, isLastChild);
+        setIconTransformationAmount(row, transitionAmount, iconTransformDistance,
+                clampedAmount != transitionAmount, isLastChild);
+    }
+
+    private void setIconTransformationAmount(ExpandableNotificationRow row,
+            float transitionAmount, float iconTransformDistance, boolean usingLinearInterpolation,
+            boolean isLastChild) {
+        StatusBarIconView icon = row.getEntry().expandedIcon;
+        NotificationIconContainer.IconState iconState = getIconState(icon);
+
+        View rowIcon = row.getNotificationIcon();
+        float notificationIconPosition = row.getTranslationY() + row.getContentTranslation();
+        boolean stayingInShelf = row.isInShelf() && !row.isTransformingIntoShelf();
+        if (usingLinearInterpolation && !stayingInShelf) {
+            // If we interpolate from the notification position, this might lead to a slightly
+            // odd interpolation, since the notification position changes as well. Let's interpolate
+            // from a fixed distance. We can only do this if we don't animate and the icon is
+            // always in the interpolated positon.
+            notificationIconPosition = getTranslationY() - iconTransformDistance;
+        }
+        float notificationIconSize = 0.0f;
+        int iconTopPadding;
+        if (rowIcon != null) {
+            iconTopPadding = row.getRelativeTopPadding(rowIcon);
+            notificationIconSize = rowIcon.getHeight();
+        } else {
+            iconTopPadding = mIconAppearTopPadding;
+        }
+        notificationIconPosition += iconTopPadding;
+        float shelfIconPosition = getTranslationY() + icon.getTop();
+        shelfIconPosition += (icon.getHeight() - icon.getIconScale() * mIconSize) / 2.0f;
+        float iconYTranslation = NotificationUtils.interpolate(
+                notificationIconPosition - shelfIconPosition,
+                0,
+                transitionAmount);
+        float shelfIconSize = mIconSize * icon.getIconScale();
+        float alpha = 1.0f;
+        boolean noIcon = !row.isShowingIcon();
+        if (noIcon) {
+            // The view currently doesn't have an icon, lets transform it in!
+            alpha = transitionAmount;
+            notificationIconSize = shelfIconSize / 2.0f;
+        }
+        // The notification size is different from the size in the shelf / statusbar
+        float newSize = NotificationUtils.interpolate(notificationIconSize, shelfIconSize,
+                transitionAmount);
+        if (iconState != null) {
+            iconState.scaleX = newSize / shelfIconSize;
+            iconState.scaleY = iconState.scaleX;
+            iconState.hidden = transitionAmount == 0.0f && !iconState.isAnimating(icon);
+            boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf();
+            if (isAppearing) {
+                iconState.hidden = true;
+                iconState.iconAppearAmount = 0.0f;
+            }
+            iconState.alpha = alpha;
+            iconState.yTranslation = iconYTranslation;
+            if (stayingInShelf) {
+                iconState.iconAppearAmount = 1.0f;
+                iconState.alpha = 1.0f;
+                iconState.scaleX = 1.0f;
+                iconState.scaleY = 1.0f;
+                iconState.hidden = false;
+            }
+            if (mAmbientState.isAboveShelf(row) || (!row.isInShelf() && (isLastChild && row.areGutsExposed()
+                    || row.getTranslationZ() > mAmbientState.getBaseZHeight()))) {
+                iconState.hidden = true;
+            }
+            int backgroundColor = getBackgroundColorWithoutTint();
+            int shelfColor = icon.getContrastedStaticDrawableColor(backgroundColor);
+            if (!noIcon && shelfColor != StatusBarIconView.NO_COLOR) {
+                int iconColor = row.getVisibleNotificationHeader().getOriginalIconColor();
+                shelfColor = NotificationUtils.interpolateColors(iconColor, shelfColor,
+                        iconState.iconAppearAmount);
+            }
+            iconState.iconColor = shelfColor;
+        }
+    }
+
+    private NotificationIconContainer.IconState getIconState(StatusBarIconView icon) {
+        return mShelfIcons.getIconState(icon);
+    }
+
+    private float getFullyClosedTranslation() {
+        return - (getIntrinsicHeight() - mStatusBarHeight) / 2;
+    }
+
+    public int getNotificationMergeSize() {
+        return getIntrinsicHeight();
+    }
+
+    @Override
+    public boolean hasNoContentHeight() {
+        return true;
+    }
+
+    private void setHideBackground(boolean hideBackground) {
+        if (mHideBackground != hideBackground) {
+            mHideBackground = hideBackground;
+            updateBackground();
+            updateOutline();
+        }
+    }
+
+    public boolean hidesBackground() {
+        return mHideBackground;
+    }
+
+    @Override
+    protected boolean needsOutline() {
+        return !mHideBackground && super.needsOutline();
+    }
+
+    @Override
+    protected boolean shouldHideBackground() {
+        return super.shouldHideBackground() || mHideBackground;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        updateRelativeOffset();
+    }
+
+    private void updateRelativeOffset() {
+        mCollapsedIcons.getLocationOnScreen(mTmp);
+        mRelativeOffset = mTmp[0];
+        getLocationOnScreen(mTmp);
+        mRelativeOffset -= mTmp[0];
+    }
+
+    private void setOpenedAmount(float openedAmount) {
+        mNoAnimationsInThisFrame = openedAmount == 1.0f && mOpenedAmount == 0.0f;
+        mOpenedAmount = openedAmount;
+        if (!mAmbientState.isPanelFullWidth()) {
+            // We don't do a transformation at all, lets just assume we are fully opened
+            openedAmount = 1.0f;
+        }
+        int start = mRelativeOffset;
+        if (isLayoutRtl()) {
+            start = getWidth() - start - mCollapsedIcons.getWidth();
+        }
+        int width = (int) NotificationUtils.interpolate(start + mCollapsedIcons.getWidth(),
+                mShelfIcons.getWidth(),
+                openedAmount);
+        mShelfIcons.setActualLayoutWidth(width);
+        boolean hasOverflow = mCollapsedIcons.hasOverflow();
+        int collapsedPadding = mCollapsedIcons.getPaddingEnd();
+        if (!hasOverflow) {
+            // we have to ensure that adding the low priority notification won't lead to an
+            // overflow
+            collapsedPadding -= (1.0f + OVERFLOW_EARLY_AMOUNT) * mCollapsedIcons.getIconSize();
+        }
+        float padding = NotificationUtils.interpolate(collapsedPadding,
+                mShelfIcons.getPaddingEnd(),
+                openedAmount);
+        mShelfIcons.setActualPaddingEnd(padding);
+        float paddingStart = NotificationUtils.interpolate(start,
+                mShelfIcons.getPaddingStart(), openedAmount);
+        mShelfIcons.setActualPaddingStart(paddingStart);
+        mShelfIcons.setOpenedAmount(openedAmount);
+        mShelfIcons.setVisualOverflowAdaption(mCollapsedIcons.getVisualOverflowAdaption());
+    }
+
+    public void setMaxLayoutHeight(int maxLayoutHeight) {
+        mMaxLayoutHeight = maxLayoutHeight;
+    }
+
+    /**
+     * @return the index of the notification at which the shelf visually resides
+     */
+    public int getNotGoneIndex() {
+        return mNotGoneIndex;
+    }
+
+    private void setHasItemsInStableShelf(boolean hasItemsInStableShelf) {
+        if (mHasItemsInStableShelf != hasItemsInStableShelf) {
+            mHasItemsInStableShelf = hasItemsInStableShelf;
+            updateInteractiveness();
+        }
+    }
+
+    /**
+     * @return whether the shelf has any icons in it when a potential animation has finished, i.e
+     *         if the current state would be applied right now
+     */
+    public boolean hasItemsInStableShelf() {
+        return mHasItemsInStableShelf;
+    }
+
+    public void setCollapsedIcons(NotificationIconContainer collapsedIcons) {
+        mCollapsedIcons = collapsedIcons;
+        mCollapsedIcons.addOnLayoutChangeListener(this);
+    }
+
+    public void setStatusBarState(int statusBarState) {
+        if (mStatusBarState != statusBarState) {
+            mStatusBarState = statusBarState;
+            updateInteractiveness();
+        }
+    }
+
+    private void updateInteractiveness() {
+        mInteractive = mStatusBarState == StatusBarState.KEYGUARD && mHasItemsInStableShelf
+                && !mDark;
+        setClickable(mInteractive);
+        setFocusable(mInteractive);
+        setImportantForAccessibility(mInteractive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
+                : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+    }
+
+    @Override
+    protected boolean isInteractive() {
+        return mInteractive;
+    }
+
+    public void setMaxShelfEnd(float maxShelfEnd) {
+        mMaxShelfEnd = maxShelfEnd;
+    }
+
+    public void setAnimationsEnabled(boolean enabled) {
+        mAnimationsEnabled = enabled;
+        mCollapsedIcons.setAnimationsEnabled(enabled);
+        if (!enabled) {
+            // we need to wait with enabling the animations until the first frame has passed
+            mShelfIcons.setAnimationsEnabled(false);
+        }
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;  // Shelf only uses alpha for transitions where the difference can't be seen.
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        if (mInteractive) {
+            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
+            AccessibilityNodeInfo.AccessibilityAction unlock
+                    = new AccessibilityNodeInfo.AccessibilityAction(
+                    AccessibilityNodeInfo.ACTION_CLICK,
+                    getContext().getString(R.string.accessibility_overflow_action));
+            info.addAction(unlock);
+        }
+    }
+
+    @Override
+    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+            int oldTop, int oldRight, int oldBottom) {
+        updateRelativeOffset();
+    }
+
+    public void setDarkOffsetX(int offsetX) {
+        mShelfIcons.setDarkOffsetX(offsetX);
+    }
+
+    private class ShelfState extends ExpandableViewState {
+        private float openedAmount;
+        private boolean hasItemsInStableShelf;
+        private float maxShelfEnd;
+
+        @Override
+        public void applyToView(View view) {
+            if (!mShowNotificationShelf) {
+                return;
+            }
+
+            super.applyToView(view);
+            setMaxShelfEnd(maxShelfEnd);
+            setOpenedAmount(openedAmount);
+            updateAppearance();
+            setHasItemsInStableShelf(hasItemsInStableShelf);
+            mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
+        }
+
+        @Override
+        public void animateTo(View child, AnimationProperties properties) {
+            if (!mShowNotificationShelf) {
+                return;
+            }
+
+            super.animateTo(child, properties);
+            setMaxShelfEnd(maxShelfEnd);
+            setOpenedAmount(openedAmount);
+            updateAppearance();
+            setHasItemsInStableShelf(hasItemsInStableShelf);
+            mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/NotificationSnooze.java b/com/android/systemui/statusbar/NotificationSnooze.java
new file mode 100644
index 0000000..c45ca54
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationSnooze.java
@@ -0,0 +1,399 @@
+package com.android.systemui.statusbar;
+/*
+ * Copyright (C) 2017 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
+ */
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
+import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.service.notification.SnoozeCriterion;
+import android.service.notification.StatusBarNotification;
+import android.text.SpannableString;
+import android.text.style.StyleSpan;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+
+public class NotificationSnooze extends LinearLayout
+        implements NotificationGuts.GutsContent, View.OnClickListener {
+
+    /**
+     * If this changes more number increases, more assistant action resId's should be defined for
+     * accessibility purposes, see {@link #setSnoozeOptions(List)}
+     */
+    private static final int MAX_ASSISTANT_SUGGESTIONS = 1;
+    private NotificationGuts mGutsContainer;
+    private NotificationSwipeActionHelper mSnoozeListener;
+    private StatusBarNotification mSbn;
+
+    private TextView mSelectedOptionText;
+    private TextView mUndoButton;
+    private ImageView mExpandButton;
+    private View mDivider;
+    private ViewGroup mSnoozeOptionContainer;
+    private List<SnoozeOption> mSnoozeOptions;
+    private int mCollapsedHeight;
+    private SnoozeOption mDefaultOption;
+    private SnoozeOption mSelectedOption;
+    private boolean mSnoozing;
+    private boolean mExpanded;
+    private AnimatorSet mExpandAnimation;
+
+    public NotificationSnooze(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mCollapsedHeight = getResources().getDimensionPixelSize(R.dimen.snooze_snackbar_min_height);
+        findViewById(R.id.notification_snooze).setOnClickListener(this);
+        mSelectedOptionText = (TextView) findViewById(R.id.snooze_option_default);
+        mUndoButton = (TextView) findViewById(R.id.undo);
+        mUndoButton.setOnClickListener(this);
+        mExpandButton = (ImageView) findViewById(R.id.expand_button);
+        mDivider = findViewById(R.id.divider);
+        mDivider.setAlpha(0f);
+        mSnoozeOptionContainer = (ViewGroup) findViewById(R.id.snooze_options);
+        mSnoozeOptionContainer.setVisibility(View.INVISIBLE);
+        mSnoozeOptionContainer.setAlpha(0f);
+
+        // Create the different options based on list
+        mSnoozeOptions = getDefaultSnoozeOptions();
+        createOptionViews();
+
+        setSelected(mDefaultOption);
+    }
+
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEvent(event);
+        if (mGutsContainer != null && mGutsContainer.isExposed()) {
+            if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+                event.getText().add(mSelectedOptionText.getText());
+            }
+        }
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        info.addAction(new AccessibilityAction(R.id.action_snooze_undo,
+                getResources().getString(R.string.snooze_undo)));
+        int count = mSnoozeOptions.size();
+        for (int i = 0; i < count; i++) {
+            AccessibilityAction action = mSnoozeOptions.get(i).getAccessibilityAction();
+            if (action != null) {
+                info.addAction(action);
+            }
+        }
+    }
+
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+        if (action == R.id.action_snooze_undo) {
+            undoSnooze(mUndoButton);
+            return true;
+        }
+        for (int i = 0; i < mSnoozeOptions.size(); i++) {
+            SnoozeOption so = mSnoozeOptions.get(i);
+            if (so.getAccessibilityAction() != null
+                    && so.getAccessibilityAction().getId() == action) {
+                setSelected(so);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void setSnoozeOptions(final List<SnoozeCriterion> snoozeList) {
+        if (snoozeList == null) {
+            return;
+        }
+        mSnoozeOptions.clear();
+        mSnoozeOptions = getDefaultSnoozeOptions();
+        final int count = Math.min(MAX_ASSISTANT_SUGGESTIONS, snoozeList.size());
+        for (int i = 0; i < count; i++) {
+            SnoozeCriterion sc = snoozeList.get(i);
+            AccessibilityAction action = new AccessibilityAction(
+                    R.id.action_snooze_assistant_suggestion_1, sc.getExplanation());
+            mSnoozeOptions.add(new NotificationSnoozeOption(sc, 0, sc.getExplanation(),
+                    sc.getConfirmation(), action));
+        }
+        createOptionViews();
+    }
+
+    public boolean isExpanded() {
+        return mExpanded;
+    }
+
+    public void setSnoozeListener(NotificationSwipeActionHelper listener) {
+        mSnoozeListener = listener;
+    }
+
+    public void setStatusBarNotification(StatusBarNotification sbn) {
+        mSbn = sbn;
+    }
+
+    private ArrayList<SnoozeOption> getDefaultSnoozeOptions() {
+        ArrayList<SnoozeOption> options = new ArrayList<>();
+
+        options.add(createOption(15 /* minutes */, R.id.action_snooze_15_min));
+        options.add(createOption(30 /* minutes */, R.id.action_snooze_30_min));
+        mDefaultOption = createOption(60 /* minutes */, R.id.action_snooze_1_hour);
+        options.add(mDefaultOption);
+        options.add(createOption(60 * 2 /* minutes */, R.id.action_snooze_2_hours));
+        return options;
+    }
+
+    private SnoozeOption createOption(int minutes, int accessibilityActionId) {
+        Resources res = getResources();
+        boolean showInHours = minutes >= 60;
+        int pluralResId = showInHours
+                ? R.plurals.snoozeHourOptions
+                : R.plurals.snoozeMinuteOptions;
+        int count = showInHours ? (minutes / 60) : minutes;
+        String description = res.getQuantityString(pluralResId, count, count);
+        String resultText = String.format(res.getString(R.string.snoozed_for_time), description);
+        SpannableString string = new SpannableString(resultText);
+        string.setSpan(new StyleSpan(Typeface.BOLD),
+                resultText.length() - description.length(), resultText.length(), 0 /* flags */);
+        AccessibilityAction action = new AccessibilityAction(accessibilityActionId, description);
+        return new NotificationSnoozeOption(null, minutes, description, string,
+                action);
+    }
+
+    private void createOptionViews() {
+        mSnoozeOptionContainer.removeAllViews();
+        LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        for (int i = 0; i < mSnoozeOptions.size(); i++) {
+            SnoozeOption option = mSnoozeOptions.get(i);
+            TextView tv = (TextView) inflater.inflate(R.layout.notification_snooze_option,
+                    mSnoozeOptionContainer, false);
+            mSnoozeOptionContainer.addView(tv);
+            tv.setText(option.getDescription());
+            tv.setTag(option);
+            tv.setOnClickListener(this);
+        }
+    }
+
+    private void hideSelectedOption() {
+        final int childCount = mSnoozeOptionContainer.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = mSnoozeOptionContainer.getChildAt(i);
+            child.setVisibility(child.getTag() == mSelectedOption ? View.GONE : View.VISIBLE);
+        }
+    }
+
+    private void showSnoozeOptions(boolean show) {
+        int drawableId = show ? com.android.internal.R.drawable.ic_collapse_notification
+                : com.android.internal.R.drawable.ic_expand_notification;
+        mExpandButton.setImageResource(drawableId);
+        if (mExpanded != show) {
+            mExpanded = show;
+            animateSnoozeOptions(show);
+            if (mGutsContainer != null) {
+                mGutsContainer.onHeightChanged();
+            }
+        }
+    }
+
+    private void animateSnoozeOptions(boolean show) {
+        if (mExpandAnimation != null) {
+            mExpandAnimation.cancel();
+        }
+        ObjectAnimator dividerAnim = ObjectAnimator.ofFloat(mDivider, View.ALPHA,
+                mDivider.getAlpha(), show ? 1f : 0f);
+        ObjectAnimator optionAnim = ObjectAnimator.ofFloat(mSnoozeOptionContainer, View.ALPHA,
+                mSnoozeOptionContainer.getAlpha(), show ? 1f : 0f);
+        mSnoozeOptionContainer.setVisibility(View.VISIBLE);
+        mExpandAnimation = new AnimatorSet();
+        mExpandAnimation.playTogether(dividerAnim, optionAnim);
+        mExpandAnimation.setDuration(150);
+        mExpandAnimation.setInterpolator(show ? Interpolators.ALPHA_IN : Interpolators.ALPHA_OUT);
+        mExpandAnimation.addListener(new AnimatorListenerAdapter() {
+            boolean cancelled = false;
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                cancelled = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (!show && !cancelled) {
+                    mSnoozeOptionContainer.setVisibility(View.INVISIBLE);
+                    mSnoozeOptionContainer.setAlpha(0f);
+                }
+            }
+        });
+        mExpandAnimation.start();
+    }
+
+    private void setSelected(SnoozeOption option) {
+        mSelectedOption = option;
+        mSelectedOptionText.setText(option.getConfirmation());
+        showSnoozeOptions(false);
+        hideSelectedOption();
+        sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (mGutsContainer != null) {
+            mGutsContainer.resetFalsingCheck();
+        }
+        final int id = v.getId();
+        final SnoozeOption tag = (SnoozeOption) v.getTag();
+        if (tag != null) {
+            setSelected(tag);
+        } else if (id == R.id.notification_snooze) {
+            // Toggle snooze options
+            showSnoozeOptions(!mExpanded);
+        } else {
+            // Undo snooze was selected
+            undoSnooze(v);
+        }
+    }
+
+    private void undoSnooze(View v) {
+        mSelectedOption = null;
+        int[] parentLoc = new int[2];
+        int[] targetLoc = new int[2];
+        mGutsContainer.getLocationOnScreen(parentLoc);
+        v.getLocationOnScreen(targetLoc);
+        final int centerX = v.getWidth() / 2;
+        final int centerY = v.getHeight() / 2;
+        final int x = targetLoc[0] - parentLoc[0] + centerX;
+        final int y = targetLoc[1] - parentLoc[1] + centerY;
+        showSnoozeOptions(false);
+        mGutsContainer.closeControls(x, y, false /* save */, false /* force */);
+    }
+
+    @Override
+    public int getActualHeight() {
+        return mExpanded ? getHeight() : mCollapsedHeight;
+    }
+
+    @Override
+    public boolean willBeRemoved() {
+        return mSnoozing;
+    }
+
+    @Override
+    public View getContentView() {
+        // Reset the view before use
+        setSelected(mDefaultOption);
+        return this;
+    }
+
+    @Override
+    public void setGutsParent(NotificationGuts guts) {
+        mGutsContainer = guts;
+    }
+
+    @Override
+    public boolean handleCloseControls(boolean save, boolean force) {
+        if (mExpanded && !force) {
+            // Collapse expanded state on outside touch
+            showSnoozeOptions(false);
+            return true;
+        } else if (mSnoozeListener != null && mSelectedOption != null) {
+            // Snooze option selected so commit it
+            mSnoozing = true;
+            mSnoozeListener.snooze(mSbn, mSelectedOption);
+            return true;
+        } else {
+            // The view should actually be closed
+            setSelected(mSnoozeOptions.get(0));
+            return false; // Return false here so that guts handles closing the view
+        }
+    }
+
+    @Override
+    public boolean isLeavebehind() {
+        return true;
+    }
+
+    public class NotificationSnoozeOption implements SnoozeOption {
+        private SnoozeCriterion mCriterion;
+        private int mMinutesToSnoozeFor;
+        private CharSequence mDescription;
+        private CharSequence mConfirmation;
+        private AccessibilityAction mAction;
+
+        public NotificationSnoozeOption(SnoozeCriterion sc, int minToSnoozeFor,
+                CharSequence description,
+                CharSequence confirmation, AccessibilityAction action) {
+            mCriterion = sc;
+            mMinutesToSnoozeFor = minToSnoozeFor;
+            mDescription = description;
+            mConfirmation = confirmation;
+            mAction = action;
+        }
+
+        @Override
+        public SnoozeCriterion getSnoozeCriterion() {
+            return mCriterion;
+        }
+
+        @Override
+        public CharSequence getDescription() {
+            return mDescription;
+        }
+
+        @Override
+        public CharSequence getConfirmation() {
+            return mConfirmation;
+        }
+
+        @Override
+        public int getMinutesToSnoozeFor() {
+            return mMinutesToSnoozeFor;
+        }
+
+        @Override
+        public AccessibilityAction getAccessibilityAction() {
+            return mAction;
+        }
+
+    }
+}
diff --git a/com/android/systemui/statusbar/RemoteInputController.java b/com/android/systemui/statusbar/RemoteInputController.java
new file mode 100644
index 0000000..7f28c4c
--- /dev/null
+++ b/com/android/systemui/statusbar/RemoteInputController.java
@@ -0,0 +1,212 @@
+/*
+ * 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 com.android.internal.util.Preconditions;
+import com.android.systemui.Dependency;
+import com.android.systemui.statusbar.phone.StatusBarWindowManager;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.RemoteInputView;
+
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Pair;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * Keeps track of the currently active {@link RemoteInputView}s.
+ */
+public class RemoteInputController {
+
+    private final ArrayList<Pair<WeakReference<NotificationData.Entry>, Object>> mOpen
+            = new ArrayList<>();
+    private final ArrayMap<String, Object> mSpinning = new ArrayMap<>();
+    private final ArrayList<Callback> mCallbacks = new ArrayList<>(3);
+    private final HeadsUpManager mHeadsUpManager;
+
+    public RemoteInputController(HeadsUpManager headsUpManager) {
+        addCallback(Dependency.get(StatusBarWindowManager.class));
+        mHeadsUpManager = headsUpManager;
+    }
+
+    /**
+     * Adds a currently active remote input.
+     *
+     * @param entry the entry for which a remote input is now active.
+     * @param token a token identifying the view that is managing the remote input
+     */
+    public void addRemoteInput(NotificationData.Entry entry, Object token) {
+        Preconditions.checkNotNull(entry);
+        Preconditions.checkNotNull(token);
+
+        boolean found = pruneWeakThenRemoveAndContains(
+                entry /* contains */, null /* remove */, token /* removeToken */);
+        if (!found) {
+            mOpen.add(new Pair<>(new WeakReference<>(entry), token));
+        }
+
+        apply(entry);
+    }
+
+    /**
+     * Removes a currently active remote input.
+     *
+     * @param entry the entry for which a remote input should be removed.
+     * @param token a token identifying the view that is requesting the removal. If non-null,
+     *              the entry is only removed if the token matches the last added token for this
+     *              entry. If null, the entry is removed regardless.
+     */
+    public void removeRemoteInput(NotificationData.Entry entry, Object token) {
+        Preconditions.checkNotNull(entry);
+
+        pruneWeakThenRemoveAndContains(null /* contains */, entry /* remove */, token);
+
+        apply(entry);
+    }
+
+    /**
+     * Adds a currently spinning (i.e. sending) remote input.
+     *
+     * @param key the key of the entry that's spinning.
+     * @param token the token of the view managing the remote input.
+     */
+    public void addSpinning(String key, Object token) {
+        Preconditions.checkNotNull(key);
+        Preconditions.checkNotNull(token);
+
+        mSpinning.put(key, token);
+    }
+
+    /**
+     * Removes a currently spinning remote input.
+     *
+     * @param key the key of the entry for which a remote input should be removed.
+     * @param token a token identifying the view that is requesting the removal. If non-null,
+     *              the entry is only removed if the token matches the last added token for this
+     *              entry. If null, the entry is removed regardless.
+     */
+    public void removeSpinning(String key, Object token) {
+        Preconditions.checkNotNull(key);
+
+        if (token == null || mSpinning.get(key) == token) {
+            mSpinning.remove(key);
+        }
+    }
+
+    public boolean isSpinning(String key) {
+        return mSpinning.containsKey(key);
+    }
+
+    private void apply(NotificationData.Entry entry) {
+        mHeadsUpManager.setRemoteInputActive(entry, isRemoteInputActive(entry));
+        boolean remoteInputActive = isRemoteInputActive();
+        int N = mCallbacks.size();
+        for (int i = 0; i < N; i++) {
+            mCallbacks.get(i).onRemoteInputActive(remoteInputActive);
+        }
+    }
+
+    /**
+     * @return true if {@param entry} has an active RemoteInput
+     */
+    public boolean isRemoteInputActive(NotificationData.Entry entry) {
+        return pruneWeakThenRemoveAndContains(entry /* contains */, null /* remove */,
+                null /* removeToken */);
+    }
+
+    /**
+     * @return true if any entry has an active RemoteInput
+     */
+    public boolean isRemoteInputActive() {
+        pruneWeakThenRemoveAndContains(null /* contains */, null /* remove */,
+                null /* removeToken */);
+        return !mOpen.isEmpty();
+    }
+
+    /**
+     * Prunes dangling weak references, removes entries referring to {@param remove} and returns
+     * whether {@param contains} is part of the array in a single loop.
+     * @param remove if non-null, removes this entry from the active remote inputs
+     * @param removeToken if non-null, only removes an entry if this matches the token when the
+     *                    entry was added.
+     * @return true if {@param contains} is in the set of active remote inputs
+     */
+    private boolean pruneWeakThenRemoveAndContains(
+            NotificationData.Entry contains, NotificationData.Entry remove, Object removeToken) {
+        boolean found = false;
+        for (int i = mOpen.size() - 1; i >= 0; i--) {
+            NotificationData.Entry item = mOpen.get(i).first.get();
+            Object itemToken = mOpen.get(i).second;
+            boolean removeTokenMatches = (removeToken == null || itemToken == removeToken);
+
+            if (item == null || (item == remove && removeTokenMatches)) {
+                mOpen.remove(i);
+            } else if (item == contains) {
+                if (removeToken != null && removeToken != itemToken) {
+                    // We need to update the token. Remove here and let caller reinsert it.
+                    mOpen.remove(i);
+                } else {
+                    found = true;
+                }
+            }
+        }
+        return found;
+    }
+
+
+    public void addCallback(Callback callback) {
+        Preconditions.checkNotNull(callback);
+        mCallbacks.add(callback);
+    }
+
+    public void remoteInputSent(NotificationData.Entry entry) {
+        int N = mCallbacks.size();
+        for (int i = 0; i < N; i++) {
+            mCallbacks.get(i).onRemoteInputSent(entry);
+        }
+    }
+
+    public void closeRemoteInputs() {
+        if (mOpen.size() == 0) {
+            return;
+        }
+
+        // Make a copy because closing the remote inputs will modify mOpen.
+        ArrayList<NotificationData.Entry> list = new ArrayList<>(mOpen.size());
+        for (int i = mOpen.size() - 1; i >= 0; i--) {
+            NotificationData.Entry item = mOpen.get(i).first.get();
+            if (item != null && item.row != null) {
+                list.add(item);
+            }
+        }
+
+        for (int i = list.size() - 1; i >= 0; i--) {
+            NotificationData.Entry item = list.get(i);
+            if (item.row != null) {
+                item.row.closeRemoteInput();
+            }
+        }
+    }
+
+    public interface Callback {
+        default void onRemoteInputActive(boolean active) {}
+
+        default void onRemoteInputSent(NotificationData.Entry entry) {}
+    }
+}
diff --git a/com/android/systemui/statusbar/ScalingDrawableWrapper.java b/com/android/systemui/statusbar/ScalingDrawableWrapper.java
new file mode 100644
index 0000000..53cbf7f
--- /dev/null
+++ b/com/android/systemui/statusbar/ScalingDrawableWrapper.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 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.graphics.drawable.Drawable;
+import android.graphics.drawable.DrawableWrapper;
+
+/**
+ * An extension of {@link DrawableWrapper} that will take a given Drawable and scale it by
+ * the given factor.
+ */
+public class ScalingDrawableWrapper extends DrawableWrapper {
+    private float mScaleFactor;
+
+    public ScalingDrawableWrapper(Drawable drawable, float scaleFactor) {
+        super(drawable);
+        mScaleFactor = scaleFactor;
+    }
+
+    @Override
+    public int getIntrinsicWidth() {
+        return (int) (super.getIntrinsicWidth() * mScaleFactor);
+    }
+
+    @Override
+    public int getIntrinsicHeight() {
+        return (int) (super.getIntrinsicHeight() * mScaleFactor);
+    }
+}
diff --git a/com/android/systemui/statusbar/ScrimView.java b/com/android/systemui/statusbar/ScrimView.java
new file mode 100644
index 0000000..a53e348
--- /dev/null
+++ b/com/android/systemui/statusbar/ScrimView.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2014 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.v4.graphics.ColorUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Display;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.animation.Interpolator;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.colorextraction.ColorExtractor;
+import com.android.internal.colorextraction.drawable.GradientDrawable;
+import com.android.systemui.Dependency;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+
+/**
+ * A view which can draw a scrim
+ */
+public class ScrimView extends View implements ConfigurationController.ConfigurationListener {
+    private static final String TAG = "ScrimView";
+    private final ColorExtractor.GradientColors mColors;
+    private boolean mDrawAsSrc;
+    private float mViewAlpha = 1.0f;
+    private ValueAnimator mAlphaAnimator;
+    private Rect mExcludedRect = new Rect();
+    private boolean mHasExcludedArea;
+    private Drawable mDrawable;
+    private PorterDuffColorFilter mColorFilter;
+    private int mTintColor;
+    private ValueAnimator.AnimatorUpdateListener mAlphaUpdateListener = animation -> {
+        if (mDrawable == null) {
+            Log.w(TAG, "Trying to animate null drawable");
+            return;
+        }
+        mDrawable.setAlpha((int) (255 * (float) animation.getAnimatedValue()));
+    };
+    private AnimatorListenerAdapter mClearAnimatorListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mAlphaAnimator = null;
+        }
+    };
+    private Runnable mChangeRunnable;
+
+    public ScrimView(Context context) {
+        this(context, null);
+    }
+
+    public ScrimView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public ScrimView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public ScrimView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        mDrawable = new GradientDrawable(context);
+        mDrawable.setCallback(this);
+        mColors = new ColorExtractor.GradientColors();
+        updateScreenSize();
+        updateColorWithTint(false);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        // We need to know about configuration changes to update the gradient size
+        // since it's independent from view bounds.
+        ConfigurationController config = Dependency.get(ConfigurationController.class);
+        config.addCallback(this);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        ConfigurationController config = Dependency.get(ConfigurationController.class);
+        config.removeCallback(this);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (mDrawAsSrc || mDrawable.getAlpha() > 0) {
+            if (!mHasExcludedArea) {
+                mDrawable.draw(canvas);
+            } else {
+                if (mExcludedRect.top > 0) {
+                    canvas.save();
+                    canvas.clipRect(0, 0, getWidth(), mExcludedRect.top);
+                    mDrawable.draw(canvas);
+                    canvas.restore();
+                }
+                if (mExcludedRect.left > 0) {
+                    canvas.save();
+                    canvas.clipRect(0, mExcludedRect.top, mExcludedRect.left,
+                            mExcludedRect.bottom);
+                    mDrawable.draw(canvas);
+                    canvas.restore();
+                }
+                if (mExcludedRect.right < getWidth()) {
+                    canvas.save();
+                    canvas.clipRect(mExcludedRect.right, mExcludedRect.top, getWidth(),
+                            mExcludedRect.bottom);
+                    mDrawable.draw(canvas);
+                    canvas.restore();
+                }
+                if (mExcludedRect.bottom < getHeight()) {
+                    canvas.save();
+                    canvas.clipRect(0, mExcludedRect.bottom, getWidth(), getHeight());
+                    mDrawable.draw(canvas);
+                    canvas.restore();
+                }
+            }
+        }
+    }
+
+    public void setDrawable(Drawable drawable) {
+        mDrawable = drawable;
+        mDrawable.setCallback(this);
+        mDrawable.setBounds(getLeft(), getTop(), getRight(), getBottom());
+        mDrawable.setAlpha((int) (255 * mViewAlpha));
+        setDrawAsSrc(mDrawAsSrc);
+        updateScreenSize();
+        invalidate();
+    }
+
+    @Override
+    public void invalidateDrawable(@NonNull Drawable drawable) {
+        super.invalidateDrawable(drawable);
+        if (drawable == mDrawable) {
+            invalidate();
+        }
+    }
+
+    public void setDrawAsSrc(boolean asSrc) {
+        mDrawAsSrc = asSrc;
+        PorterDuff.Mode mode = asSrc ? PorterDuff.Mode.SRC : PorterDuff.Mode.SRC_OVER;
+        mDrawable.setXfermode(new PorterDuffXfermode(mode));
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        if (changed) {
+            mDrawable.setBounds(left, top, right, bottom);
+            invalidate();
+        }
+    }
+
+    public void setColors(@NonNull ColorExtractor.GradientColors colors) {
+        setColors(colors, false);
+    }
+
+    public void setColors(@NonNull ColorExtractor.GradientColors colors, boolean animated) {
+        if (colors == null) {
+            throw new IllegalArgumentException("Colors cannot be null");
+        }
+        if (mColors.equals(colors)) {
+            return;
+        }
+        mColors.set(colors);
+        updateColorWithTint(animated);
+    }
+
+    @VisibleForTesting
+    Drawable getDrawable() {
+        return mDrawable;
+    }
+
+    public ColorExtractor.GradientColors getColors() {
+        return mColors;
+    }
+
+    public void setTint(int color) {
+        setTint(color, false);
+    }
+
+    public void setTint(int color, boolean animated) {
+        if (mTintColor == color) {
+            return;
+        }
+        mTintColor = color;
+        updateColorWithTint(animated);
+    }
+
+    private void updateColorWithTint(boolean animated) {
+        if (mDrawable instanceof GradientDrawable) {
+            // Optimization to blend colors and avoid a color filter
+            GradientDrawable drawable = (GradientDrawable) mDrawable;
+            float tintAmount = Color.alpha(mTintColor) / 255f;
+            int mainTinted = ColorUtils.blendARGB(mColors.getMainColor(), mTintColor,
+                    tintAmount);
+            int secondaryTinted = ColorUtils.blendARGB(mColors.getSecondaryColor(), mTintColor,
+                    tintAmount);
+            drawable.setColors(mainTinted, secondaryTinted, animated);
+        } else {
+            if (mColorFilter == null) {
+                mColorFilter = new PorterDuffColorFilter(mTintColor, PorterDuff.Mode.SRC_OVER);
+            } else {
+                mColorFilter.setColor(mTintColor);
+            }
+            mDrawable.setColorFilter(Color.alpha(mTintColor) == 0 ? null : mColorFilter);
+            mDrawable.invalidateSelf();
+        }
+
+        if (mChangeRunnable != null) {
+            mChangeRunnable.run();
+        }
+    }
+
+    public int getTint() {
+        return mTintColor;
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    public void setViewAlpha(float alpha) {
+        if (alpha != mViewAlpha) {
+            mViewAlpha = alpha;
+
+            if (mAlphaAnimator != null) {
+                mAlphaAnimator.cancel();
+            }
+
+            mDrawable.setAlpha((int) (255 * alpha));
+            if (mChangeRunnable != null) {
+                mChangeRunnable.run();
+            }
+        }
+    }
+
+    public float getViewAlpha() {
+        return mViewAlpha;
+    }
+
+    public void animateViewAlpha(float alpha, long durationOut, Interpolator interpolator) {
+        if (mAlphaAnimator != null) {
+            mAlphaAnimator.cancel();
+        }
+        mAlphaAnimator = ValueAnimator.ofFloat(getViewAlpha(), alpha);
+        mAlphaAnimator.addUpdateListener(mAlphaUpdateListener);
+        mAlphaAnimator.addListener(mClearAnimatorListener);
+        mAlphaAnimator.setInterpolator(interpolator);
+        mAlphaAnimator.setDuration(durationOut);
+        mAlphaAnimator.start();
+    }
+
+    public void setExcludedArea(Rect area) {
+        if (area == null) {
+            mHasExcludedArea = false;
+            invalidate();
+            return;
+        }
+
+        int left = Math.max(area.left, 0);
+        int top = Math.max(area.top, 0);
+        int right = Math.min(area.right, getWidth());
+        int bottom = Math.min(area.bottom, getHeight());
+        mExcludedRect.set(left, top, right, bottom);
+        mHasExcludedArea = left < right && top < bottom;
+        invalidate();
+    }
+
+    public void setChangeRunnable(Runnable changeRunnable) {
+        mChangeRunnable = changeRunnable;
+    }
+
+    @Override
+    public void onConfigChanged(Configuration newConfig) {
+        updateScreenSize();
+    }
+
+    private void updateScreenSize() {
+        if (mDrawable instanceof GradientDrawable) {
+            WindowManager wm = mContext.getSystemService(WindowManager.class);
+            if (wm == null) {
+                Log.w(TAG, "Can't resize gradient drawable to fit the screen");
+                return;
+            }
+            Display display = wm.getDefaultDisplay();
+            if (display != null) {
+                Point size = new Point();
+                display.getRealSize(size);
+                ((GradientDrawable) mDrawable).setScreenSize(size.x, size.y);
+            }
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/SignalClusterView.java b/com/android/systemui/statusbar/SignalClusterView.java
new file mode 100644
index 0000000..759d2cf
--- /dev/null
+++ b/com/android/systemui/statusbar/SignalClusterView.java
@@ -0,0 +1,723 @@
+/*
+ * Copyright (C) 2011 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.annotation.DrawableRes;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.Animatable;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.telephony.SubscriptionInfo;
+import android.util.ArraySet;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.SignalDrawable;
+import com.android.systemui.statusbar.phone.StatusBarIconController;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.statusbar.policy.IconLogger;
+import com.android.systemui.statusbar.policy.NetworkController;
+import com.android.systemui.statusbar.policy.NetworkController.IconState;
+import com.android.systemui.statusbar.policy.NetworkControllerImpl;
+import com.android.systemui.statusbar.policy.SecurityController;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.tuner.TunerService.Tunable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+// Intimately tied to the design of res/layout/signal_cluster_view.xml
+public class SignalClusterView extends LinearLayout implements NetworkControllerImpl.SignalCallback,
+        SecurityController.SecurityControllerCallback, Tunable,
+        DarkReceiver {
+
+    static final String TAG = "SignalClusterView";
+    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final String SLOT_AIRPLANE = "airplane";
+    private static final String SLOT_MOBILE = "mobile";
+    private static final String SLOT_WIFI = "wifi";
+    private static final String SLOT_ETHERNET = "ethernet";
+    private static final String SLOT_VPN = "vpn";
+
+    private final NetworkController mNetworkController;
+    private final SecurityController mSecurityController;
+
+    private boolean mNoSimsVisible = false;
+    private boolean mVpnVisible = false;
+    private int mVpnIconId = 0;
+    private int mLastVpnIconId = -1;
+    private boolean mEthernetVisible = false;
+    private int mEthernetIconId = 0;
+    private int mLastEthernetIconId = -1;
+    private boolean mWifiVisible = false;
+    private int mWifiStrengthId = 0;
+    private int mLastWifiStrengthId = -1;
+    private boolean mWifiIn;
+    private boolean mWifiOut;
+    private int mLastWifiActivityId = -1;
+    private boolean mIsAirplaneMode = false;
+    private int mAirplaneIconId = 0;
+    private int mLastAirplaneIconId = -1;
+    private String mAirplaneContentDescription;
+    private String mWifiDescription;
+    private String mEthernetDescription;
+    private ArrayList<PhoneState> mPhoneStates = new ArrayList<PhoneState>();
+    private int mIconTint = Color.WHITE;
+    private float mDarkIntensity;
+    private final Rect mTintArea = new Rect();
+
+    ViewGroup mEthernetGroup, mWifiGroup;
+    View mNoSimsCombo;
+    ImageView mVpn, mEthernet, mWifi, mAirplane, mNoSims, mEthernetDark, mWifiDark, mNoSimsDark;
+    ImageView mWifiActivityIn;
+    ImageView mWifiActivityOut;
+    View mWifiAirplaneSpacer;
+    View mWifiSignalSpacer;
+    LinearLayout mMobileSignalGroup;
+
+    private final int mMobileSignalGroupEndPadding;
+    private final int mMobileDataIconStartPadding;
+    private final int mWideTypeIconStartPadding;
+    private final int mSecondaryTelephonyPadding;
+    private final int mEndPadding;
+    private final int mEndPaddingNothingVisible;
+    private final float mIconScaleFactor;
+
+    private boolean mBlockAirplane;
+    private boolean mBlockMobile;
+    private boolean mBlockWifi;
+    private boolean mBlockEthernet;
+    private boolean mActivityEnabled;
+    private boolean mForceBlockWifi;
+
+    private final IconLogger mIconLogger = Dependency.get(IconLogger.class);
+
+    public SignalClusterView(Context context) {
+        this(context, null);
+    }
+
+    public SignalClusterView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public SignalClusterView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        Resources res = getResources();
+        mMobileSignalGroupEndPadding =
+                res.getDimensionPixelSize(R.dimen.mobile_signal_group_end_padding);
+        mMobileDataIconStartPadding =
+                res.getDimensionPixelSize(R.dimen.mobile_data_icon_start_padding);
+        mWideTypeIconStartPadding = res.getDimensionPixelSize(R.dimen.wide_type_icon_start_padding);
+        mSecondaryTelephonyPadding = res.getDimensionPixelSize(R.dimen.secondary_telephony_padding);
+        mEndPadding = res.getDimensionPixelSize(R.dimen.signal_cluster_battery_padding);
+        mEndPaddingNothingVisible = res.getDimensionPixelSize(
+                R.dimen.no_signal_cluster_battery_padding);
+
+        TypedValue typedValue = new TypedValue();
+        res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
+        mIconScaleFactor = typedValue.getFloat();
+        mNetworkController = Dependency.get(NetworkController.class);
+        mSecurityController = Dependency.get(SecurityController.class);
+        updateActivityEnabled();
+    }
+
+    public void setForceBlockWifi() {
+        mForceBlockWifi = true;
+        mBlockWifi = true;
+        if (isAttachedToWindow()) {
+            // Re-register to get new callbacks.
+            mNetworkController.removeCallback(this);
+            mNetworkController.addCallback(this);
+        }
+    }
+
+    @Override
+    public void onTuningChanged(String key, String newValue) {
+        if (!StatusBarIconController.ICON_BLACKLIST.equals(key)) {
+            return;
+        }
+        ArraySet<String> blockList = StatusBarIconController.getIconBlacklist(newValue);
+        boolean blockAirplane = blockList.contains(SLOT_AIRPLANE);
+        boolean blockMobile = blockList.contains(SLOT_MOBILE);
+        boolean blockWifi = blockList.contains(SLOT_WIFI);
+        boolean blockEthernet = blockList.contains(SLOT_ETHERNET);
+
+        if (blockAirplane != mBlockAirplane || blockMobile != mBlockMobile
+                || blockEthernet != mBlockEthernet || blockWifi != mBlockWifi) {
+            mBlockAirplane = blockAirplane;
+            mBlockMobile = blockMobile;
+            mBlockEthernet = blockEthernet;
+            mBlockWifi = blockWifi || mForceBlockWifi;
+            // Re-register to get new callbacks.
+            mNetworkController.removeCallback(this);
+            mNetworkController.addCallback(this);
+        }
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mVpn            = findViewById(R.id.vpn);
+        mEthernetGroup  = findViewById(R.id.ethernet_combo);
+        mEthernet       = findViewById(R.id.ethernet);
+        mEthernetDark   = findViewById(R.id.ethernet_dark);
+        mWifiGroup      = findViewById(R.id.wifi_combo);
+        mWifi           = findViewById(R.id.wifi_signal);
+        mWifiDark       = findViewById(R.id.wifi_signal_dark);
+        mWifiActivityIn = findViewById(R.id.wifi_in);
+        mWifiActivityOut= findViewById(R.id.wifi_out);
+        mAirplane       = findViewById(R.id.airplane);
+        mNoSims         = findViewById(R.id.no_sims);
+        mNoSimsDark     = findViewById(R.id.no_sims_dark);
+        mNoSimsCombo    =             findViewById(R.id.no_sims_combo);
+        mWifiAirplaneSpacer =         findViewById(R.id.wifi_airplane_spacer);
+        mWifiSignalSpacer =           findViewById(R.id.wifi_signal_spacer);
+        mMobileSignalGroup =          findViewById(R.id.mobile_signal_group);
+
+        maybeScaleVpnAndNoSimsIcons();
+    }
+
+    /**
+     * Extracts the icon off of the VPN and no sims views and maybe scale them by
+     * {@link #mIconScaleFactor}. Note that the other icons are not scaled here because they are
+     * dynamic. As such, they need to be scaled each time the icon changes in {@link #apply()}.
+     */
+    private void maybeScaleVpnAndNoSimsIcons() {
+        if (mIconScaleFactor == 1.f) {
+            return;
+        }
+
+        mVpn.setImageDrawable(new ScalingDrawableWrapper(mVpn.getDrawable(), mIconScaleFactor));
+
+        mNoSims.setImageDrawable(
+                new ScalingDrawableWrapper(mNoSims.getDrawable(), mIconScaleFactor));
+        mNoSimsDark.setImageDrawable(
+                new ScalingDrawableWrapper(mNoSimsDark.getDrawable(), mIconScaleFactor));
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mVpnVisible = mSecurityController.isVpnEnabled();
+        mVpnIconId = currentVpnIconId(mSecurityController.isVpnBranded());
+
+        for (PhoneState state : mPhoneStates) {
+            if (state.mMobileGroup.getParent() == null) {
+                mMobileSignalGroup.addView(state.mMobileGroup);
+            }
+        }
+
+        int endPadding = mMobileSignalGroup.getChildCount() > 0 ? mMobileSignalGroupEndPadding : 0;
+        mMobileSignalGroup.setPaddingRelative(0, 0, endPadding, 0);
+
+        Dependency.get(TunerService.class).addTunable(this, StatusBarIconController.ICON_BLACKLIST);
+
+        apply();
+        applyIconTint();
+        mNetworkController.addCallback(this);
+        mSecurityController.addCallback(this);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        mMobileSignalGroup.removeAllViews();
+        Dependency.get(TunerService.class).removeTunable(this);
+        mSecurityController.removeCallback(this);
+        mNetworkController.removeCallback(this);
+
+        super.onDetachedFromWindow();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+
+        // Re-run all checks against the tint area for all icons
+        applyIconTint();
+    }
+
+    // From SecurityController.
+    @Override
+    public void onStateChanged() {
+        post(new Runnable() {
+            @Override
+            public void run() {
+                mVpnVisible = mSecurityController.isVpnEnabled();
+                mVpnIconId = currentVpnIconId(mSecurityController.isVpnBranded());
+                apply();
+            }
+        });
+    }
+
+    private void updateActivityEnabled() {
+        mActivityEnabled = mContext.getResources().getBoolean(R.bool.config_showActivity);
+    }
+
+    @Override
+    public void setWifiIndicators(boolean enabled, IconState statusIcon, IconState qsIcon,
+            boolean activityIn, boolean activityOut, String description, boolean isTransient) {
+        mWifiVisible = statusIcon.visible && !mBlockWifi;
+        mWifiStrengthId = statusIcon.icon;
+        mWifiDescription = statusIcon.contentDescription;
+        mWifiIn = activityIn && mActivityEnabled && mWifiVisible;
+        mWifiOut = activityOut && mActivityEnabled && mWifiVisible;
+
+        apply();
+    }
+
+    @Override
+    public void setMobileDataIndicators(IconState statusIcon, IconState qsIcon, int statusType,
+            int qsType, boolean activityIn, boolean activityOut, String typeContentDescription,
+            String description, boolean isWide, int subId, boolean roaming) {
+        PhoneState state = getState(subId);
+        if (state == null) {
+            return;
+        }
+        state.mMobileVisible = statusIcon.visible && !mBlockMobile;
+        state.mMobileStrengthId = statusIcon.icon;
+        state.mMobileTypeId = statusType;
+        state.mMobileDescription = statusIcon.contentDescription;
+        state.mMobileTypeDescription = typeContentDescription;
+        state.mIsMobileTypeIconWide = statusType != 0 && isWide;
+        state.mRoaming = roaming;
+        state.mActivityIn = activityIn && mActivityEnabled;
+        state.mActivityOut = activityOut && mActivityEnabled;
+
+        apply();
+    }
+
+    @Override
+    public void setEthernetIndicators(IconState state) {
+        mEthernetVisible = state.visible && !mBlockEthernet;
+        mEthernetIconId = state.icon;
+        mEthernetDescription = state.contentDescription;
+
+        apply();
+    }
+
+    @Override
+    public void setNoSims(boolean show) {
+        mNoSimsVisible = show && !mBlockMobile;
+        apply();
+    }
+
+    @Override
+    public void setSubs(List<SubscriptionInfo> subs) {
+        if (hasCorrectSubs(subs)) {
+            return;
+        }
+        mPhoneStates.clear();
+        if (mMobileSignalGroup != null) {
+            mMobileSignalGroup.removeAllViews();
+        }
+        final int n = subs.size();
+        for (int i = 0; i < n; i++) {
+            inflatePhoneState(subs.get(i).getSubscriptionId());
+        }
+        if (isAttachedToWindow()) {
+            applyIconTint();
+        }
+    }
+
+    private boolean hasCorrectSubs(List<SubscriptionInfo> subs) {
+        final int N = subs.size();
+        if (N != mPhoneStates.size()) {
+            return false;
+        }
+        for (int i = 0; i < N; i++) {
+            if (mPhoneStates.get(i).mSubId != subs.get(i).getSubscriptionId()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private PhoneState getState(int subId) {
+        for (PhoneState state : mPhoneStates) {
+            if (state.mSubId == subId) {
+                return state;
+            }
+        }
+        Log.e(TAG, "Unexpected subscription " + subId);
+        return null;
+    }
+
+    private PhoneState inflatePhoneState(int subId) {
+        PhoneState state = new PhoneState(subId, mContext);
+        if (mMobileSignalGroup != null) {
+            mMobileSignalGroup.addView(state.mMobileGroup);
+        }
+        mPhoneStates.add(state);
+        return state;
+    }
+
+    @Override
+    public void setIsAirplaneMode(IconState icon) {
+        mIsAirplaneMode = icon.visible && !mBlockAirplane;
+        mAirplaneIconId = icon.icon;
+        mAirplaneContentDescription = icon.contentDescription;
+
+        apply();
+    }
+
+    @Override
+    public void setMobileDataEnabled(boolean enabled) {
+        // Don't care.
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+        // Standard group layout onPopulateAccessibilityEvent() implementations
+        // ignore content description, so populate manually
+        if (mEthernetVisible && mEthernetGroup != null &&
+                mEthernetGroup.getContentDescription() != null)
+            event.getText().add(mEthernetGroup.getContentDescription());
+        if (mWifiVisible && mWifiGroup != null && mWifiGroup.getContentDescription() != null)
+            event.getText().add(mWifiGroup.getContentDescription());
+        for (PhoneState state : mPhoneStates) {
+            state.populateAccessibilityEvent(event);
+        }
+        return super.dispatchPopulateAccessibilityEventInternal(event);
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+
+        if (mEthernet != null) {
+            mEthernet.setImageDrawable(null);
+            mEthernetDark.setImageDrawable(null);
+            mLastEthernetIconId = -1;
+        }
+
+        if (mWifi != null) {
+            mWifi.setImageDrawable(null);
+            mWifiDark.setImageDrawable(null);
+            mLastWifiStrengthId = -1;
+        }
+
+        for (PhoneState state : mPhoneStates) {
+            if (state.mMobileType != null) {
+                state.mMobileType.setImageDrawable(null);
+                state.mLastMobileTypeId = -1;
+            }
+        }
+
+        if (mAirplane != null) {
+            mAirplane.setImageDrawable(null);
+            mLastAirplaneIconId = -1;
+        }
+
+        apply();
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    // Run after each indicator change.
+    private void apply() {
+        if (mWifiGroup == null) return;
+
+        if (mVpnVisible) {
+            if (mLastVpnIconId != mVpnIconId) {
+                setIconForView(mVpn, mVpnIconId);
+                mLastVpnIconId = mVpnIconId;
+            }
+            mIconLogger.onIconShown(SLOT_VPN);
+            mVpn.setVisibility(View.VISIBLE);
+        } else {
+            mIconLogger.onIconHidden(SLOT_VPN);
+            mVpn.setVisibility(View.GONE);
+        }
+        if (DEBUG) Log.d(TAG, String.format("vpn: %s", mVpnVisible ? "VISIBLE" : "GONE"));
+
+        if (mEthernetVisible) {
+            if (mLastEthernetIconId != mEthernetIconId) {
+                setIconForView(mEthernet, mEthernetIconId);
+                setIconForView(mEthernetDark, mEthernetIconId);
+                mLastEthernetIconId = mEthernetIconId;
+            }
+            mEthernetGroup.setContentDescription(mEthernetDescription);
+            mIconLogger.onIconShown(SLOT_ETHERNET);
+            mEthernetGroup.setVisibility(View.VISIBLE);
+        } else {
+            mIconLogger.onIconHidden(SLOT_ETHERNET);
+            mEthernetGroup.setVisibility(View.GONE);
+        }
+
+        if (DEBUG) Log.d(TAG,
+                String.format("ethernet: %s",
+                    (mEthernetVisible ? "VISIBLE" : "GONE")));
+
+        if (mWifiVisible) {
+            if (mWifiStrengthId != mLastWifiStrengthId) {
+                setIconForView(mWifi, mWifiStrengthId);
+                setIconForView(mWifiDark, mWifiStrengthId);
+                mLastWifiStrengthId = mWifiStrengthId;
+            }
+            mIconLogger.onIconShown(SLOT_WIFI);
+            mWifiGroup.setContentDescription(mWifiDescription);
+            mWifiGroup.setVisibility(View.VISIBLE);
+        } else {
+            mIconLogger.onIconHidden(SLOT_WIFI);
+            mWifiGroup.setVisibility(View.GONE);
+        }
+
+        if (DEBUG) Log.d(TAG,
+                String.format("wifi: %s sig=%d",
+                    (mWifiVisible ? "VISIBLE" : "GONE"),
+                    mWifiStrengthId));
+
+        mWifiActivityIn.setVisibility(mWifiIn ? View.VISIBLE : View.GONE);
+        mWifiActivityOut.setVisibility(mWifiOut ? View.VISIBLE : View.GONE);
+
+        boolean anyMobileVisible = false;
+        int firstMobileTypeId = 0;
+        for (PhoneState state : mPhoneStates) {
+            if (state.apply(anyMobileVisible)) {
+                if (!anyMobileVisible) {
+                    firstMobileTypeId = state.mMobileTypeId;
+                    anyMobileVisible = true;
+                }
+            }
+        }
+        if (anyMobileVisible) {
+            mIconLogger.onIconShown(SLOT_MOBILE);
+        } else {
+            mIconLogger.onIconHidden(SLOT_MOBILE);
+        }
+
+        if (mIsAirplaneMode) {
+            if (mLastAirplaneIconId != mAirplaneIconId) {
+                setIconForView(mAirplane, mAirplaneIconId);
+                mLastAirplaneIconId = mAirplaneIconId;
+            }
+            mAirplane.setContentDescription(mAirplaneContentDescription);
+            mIconLogger.onIconShown(SLOT_AIRPLANE);
+            mAirplane.setVisibility(View.VISIBLE);
+        } else {
+            mIconLogger.onIconHidden(SLOT_AIRPLANE);
+            mAirplane.setVisibility(View.GONE);
+        }
+
+        if (mIsAirplaneMode && mWifiVisible) {
+            mWifiAirplaneSpacer.setVisibility(View.VISIBLE);
+        } else {
+            mWifiAirplaneSpacer.setVisibility(View.GONE);
+        }
+
+        if (((anyMobileVisible && firstMobileTypeId != 0) || mNoSimsVisible) && mWifiVisible) {
+            mWifiSignalSpacer.setVisibility(View.VISIBLE);
+        } else {
+            mWifiSignalSpacer.setVisibility(View.GONE);
+        }
+
+        if (mNoSimsVisible) {
+            mIconLogger.onIconShown(SLOT_MOBILE);
+            mNoSimsCombo.setVisibility(View.VISIBLE);
+        } else {
+            mIconLogger.onIconHidden(SLOT_MOBILE);
+            mNoSimsCombo.setVisibility(View.GONE);
+        }
+
+        boolean anythingVisible = mNoSimsVisible || mWifiVisible || mIsAirplaneMode
+                || anyMobileVisible || mVpnVisible || mEthernetVisible;
+        setPaddingRelative(0, 0, anythingVisible ? mEndPadding : mEndPaddingNothingVisible, 0);
+    }
+
+    /**
+     * Sets the given drawable id on the view. This method will also scale the icon by
+     * {@link #mIconScaleFactor} if appropriate.
+     */
+    private void setIconForView(ImageView imageView, @DrawableRes int iconId) {
+        // Using the imageView's context to retrieve the Drawable so that theme is preserved.
+        Drawable icon = imageView.getContext().getDrawable(iconId);
+
+        if (mIconScaleFactor == 1.f) {
+            imageView.setImageDrawable(icon);
+        } else {
+            imageView.setImageDrawable(new ScalingDrawableWrapper(icon, mIconScaleFactor));
+        }
+    }
+
+
+    @Override
+    public void onDarkChanged(Rect tintArea, float darkIntensity, int tint) {
+        boolean changed = tint != mIconTint || darkIntensity != mDarkIntensity
+                || !mTintArea.equals(tintArea);
+        mIconTint = tint;
+        mDarkIntensity = darkIntensity;
+        mTintArea.set(tintArea);
+        if (changed && isAttachedToWindow()) {
+            applyIconTint();
+        }
+    }
+
+    private void applyIconTint() {
+        setTint(mVpn, DarkIconDispatcher.getTint(mTintArea, mVpn, mIconTint));
+        setTint(mAirplane, DarkIconDispatcher.getTint(mTintArea, mAirplane, mIconTint));
+        applyDarkIntensity(
+                DarkIconDispatcher.getDarkIntensity(mTintArea, mNoSims, mDarkIntensity),
+                mNoSims, mNoSimsDark);
+        applyDarkIntensity(
+                DarkIconDispatcher.getDarkIntensity(mTintArea, mWifi, mDarkIntensity),
+                mWifi, mWifiDark);
+        setTint(mWifiActivityIn,
+                DarkIconDispatcher.getTint(mTintArea, mWifiActivityIn, mIconTint));
+        setTint(mWifiActivityOut,
+                DarkIconDispatcher.getTint(mTintArea, mWifiActivityOut, mIconTint));
+        applyDarkIntensity(
+                DarkIconDispatcher.getDarkIntensity(mTintArea, mEthernet, mDarkIntensity),
+                mEthernet, mEthernetDark);
+        for (int i = 0; i < mPhoneStates.size(); i++) {
+            mPhoneStates.get(i).setIconTint(mIconTint, mDarkIntensity, mTintArea);
+        }
+    }
+
+    private void applyDarkIntensity(float darkIntensity, View lightIcon, View darkIcon) {
+        lightIcon.setAlpha(1 - darkIntensity);
+        darkIcon.setAlpha(darkIntensity);
+    }
+
+    private void setTint(ImageView v, int tint) {
+        v.setImageTintList(ColorStateList.valueOf(tint));
+    }
+
+    private int currentVpnIconId(boolean isBranded) {
+        return isBranded ? R.drawable.stat_sys_branded_vpn : R.drawable.stat_sys_vpn_ic;
+    }
+
+    private class PhoneState {
+        private final int mSubId;
+        private boolean mMobileVisible = false;
+        private int mMobileStrengthId = 0, mMobileTypeId = 0;
+        private int mLastMobileStrengthId = -1;
+        private int mLastMobileTypeId = -1;
+        private boolean mIsMobileTypeIconWide;
+        private String mMobileDescription, mMobileTypeDescription;
+
+        private ViewGroup mMobileGroup;
+        private ImageView mMobile, mMobileDark, mMobileType, mMobileRoaming;
+        public boolean mRoaming;
+        private ImageView mMobileActivityIn;
+        private ImageView mMobileActivityOut;
+        public boolean mActivityIn;
+        public boolean mActivityOut;
+
+        public PhoneState(int subId, Context context) {
+            ViewGroup root = (ViewGroup) LayoutInflater.from(context)
+                    .inflate(R.layout.mobile_signal_group, null);
+            setViews(root);
+            mSubId = subId;
+        }
+
+        public void setViews(ViewGroup root) {
+            mMobileGroup    = root;
+            mMobile         = root.findViewById(R.id.mobile_signal);
+            mMobileDark     = root.findViewById(R.id.mobile_signal_dark);
+            mMobileType     = root.findViewById(R.id.mobile_type);
+            mMobileRoaming  = root.findViewById(R.id.mobile_roaming);
+            mMobileActivityIn = root.findViewById(R.id.mobile_in);
+            mMobileActivityOut = root.findViewById(R.id.mobile_out);
+            // TODO: Remove the 2 instances because now the drawable can handle darkness.
+            mMobile.setImageDrawable(new SignalDrawable(mMobile.getContext()));
+            SignalDrawable drawable = new SignalDrawable(mMobileDark.getContext());
+            drawable.setDarkIntensity(1);
+            mMobileDark.setImageDrawable(drawable);
+        }
+
+        public boolean apply(boolean isSecondaryIcon) {
+            if (mMobileVisible && !mIsAirplaneMode) {
+                if (mLastMobileStrengthId != mMobileStrengthId) {
+                    mMobile.getDrawable().setLevel(mMobileStrengthId);
+                    mMobileDark.getDrawable().setLevel(mMobileStrengthId);
+                    mLastMobileStrengthId = mMobileStrengthId;
+                }
+
+                if (mLastMobileTypeId != mMobileTypeId) {
+                    mMobileType.setImageResource(mMobileTypeId);
+                    mLastMobileTypeId = mMobileTypeId;
+                }
+
+                mMobileGroup.setContentDescription(mMobileTypeDescription
+                        + " " + mMobileDescription);
+                mMobileGroup.setVisibility(View.VISIBLE);
+            } else {
+                mMobileGroup.setVisibility(View.GONE);
+            }
+
+            // When this isn't next to wifi, give it some extra padding between the signals.
+            mMobileGroup.setPaddingRelative(isSecondaryIcon ? mSecondaryTelephonyPadding : 0,
+                    0, 0, 0);
+            mMobile.setPaddingRelative(
+                    mIsMobileTypeIconWide ? mWideTypeIconStartPadding : mMobileDataIconStartPadding,
+                    0, 0, 0);
+            mMobileDark.setPaddingRelative(
+                    mIsMobileTypeIconWide ? mWideTypeIconStartPadding : mMobileDataIconStartPadding,
+                    0, 0, 0);
+
+            if (DEBUG) Log.d(TAG, String.format("mobile: %s sig=%d typ=%d",
+                        (mMobileVisible ? "VISIBLE" : "GONE"), mMobileStrengthId, mMobileTypeId));
+
+            mMobileType.setVisibility(mMobileTypeId != 0 ? View.VISIBLE : View.GONE);
+            mMobileRoaming.setVisibility(mRoaming ? View.VISIBLE : View.GONE);
+            mMobileActivityIn.setVisibility(mActivityIn ? View.VISIBLE : View.GONE);
+            mMobileActivityOut.setVisibility(mActivityOut ? View.VISIBLE : View.GONE);
+
+            return mMobileVisible;
+        }
+
+        public void populateAccessibilityEvent(AccessibilityEvent event) {
+            if (mMobileVisible && mMobileGroup != null
+                    && mMobileGroup.getContentDescription() != null) {
+                event.getText().add(mMobileGroup.getContentDescription());
+            }
+        }
+
+        public void setIconTint(int tint, float darkIntensity, Rect tintArea) {
+            applyDarkIntensity(
+                    DarkIconDispatcher.getDarkIntensity(tintArea, mMobile, darkIntensity),
+                    mMobile, mMobileDark);
+            setTint(mMobileType, DarkIconDispatcher.getTint(tintArea, mMobileType, tint));
+            setTint(mMobileRoaming, DarkIconDispatcher.getTint(tintArea, mMobileRoaming,
+                    tint));
+            setTint(mMobileActivityIn,
+                    DarkIconDispatcher.getTint(tintArea, mMobileActivityIn, tint));
+            setTint(mMobileActivityOut,
+                    DarkIconDispatcher.getTint(tintArea, mMobileActivityOut, tint));
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/StackScrollerDecorView.java b/com/android/systemui/statusbar/StackScrollerDecorView.java
new file mode 100644
index 0000000..0a7ee51
--- /dev/null
+++ b/com/android/systemui/statusbar/StackScrollerDecorView.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2014 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.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+
+/**
+ * A common base class for all views in the notification stack scroller which don't have a
+ * background.
+ */
+public abstract class StackScrollerDecorView extends ExpandableView {
+
+    protected View mContent;
+    private boolean mIsVisible;
+    private boolean mAnimating;
+
+    public StackScrollerDecorView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mContent = findContentView();
+        setInvisible();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        setOutlineProvider(null);
+    }
+
+    @Override
+    public boolean isTransparent() {
+        return true;
+    }
+
+    public void performVisibilityAnimation(boolean nowVisible) {
+        animateText(nowVisible, null /* onFinishedRunnable */);
+    }
+
+    public void performVisibilityAnimation(boolean nowVisible, Runnable onFinishedRunnable) {
+        animateText(nowVisible, onFinishedRunnable);
+    }
+
+    public boolean isVisible() {
+        return mIsVisible || mAnimating;
+    }
+
+    /**
+     * Animate the text to a new visibility.
+     *
+     * @param nowVisible should it now be visible
+     * @param onFinishedRunnable A runnable which should be run when the animation is
+     *        finished.
+     */
+    private void animateText(boolean nowVisible, final Runnable onFinishedRunnable) {
+        if (nowVisible != mIsVisible) {
+            // Animate text
+            float endValue = nowVisible ? 1.0f : 0.0f;
+            Interpolator interpolator;
+            if (nowVisible) {
+                interpolator = Interpolators.ALPHA_IN;
+            } else {
+                interpolator = Interpolators.ALPHA_OUT;
+            }
+            mAnimating = true;
+            mContent.animate()
+                    .alpha(endValue)
+                    .setInterpolator(interpolator)
+                    .setDuration(260)
+                    .withEndAction(new Runnable() {
+                        @Override
+                        public void run() {
+                            mAnimating = false;
+                            if (onFinishedRunnable != null) {
+                                onFinishedRunnable.run();
+                            }
+                        }
+                    });
+            mIsVisible = nowVisible;
+        } else {
+            if (onFinishedRunnable != null) {
+                onFinishedRunnable.run();
+            }
+        }
+    }
+
+    public void setInvisible() {
+        mContent.setAlpha(0.0f);
+        mIsVisible = false;
+    }
+
+    @Override
+    public void performRemoveAnimation(long duration, float translationDirection,
+            Runnable onFinishedRunnable) {
+        // TODO: Use duration
+        performVisibilityAnimation(false);
+    }
+
+    @Override
+    public void performAddAnimation(long delay, long duration) {
+        // TODO: use delay and duration
+        performVisibilityAnimation(true);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    public void cancelAnimation() {
+        mContent.animate().cancel();
+    }
+
+    protected abstract View findContentView();
+}
diff --git a/com/android/systemui/statusbar/StatusBarIconView.java b/com/android/systemui/statusbar/StatusBarIconView.java
new file mode 100644
index 0000000..2cff79d
--- /dev/null
+++ b/com/android/systemui/statusbar/StatusBarIconView.java
@@ -0,0 +1,810 @@
+/*
+ * Copyright (C) 2008 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.app.Notification;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Parcelable;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.support.v4.graphics.ColorUtils;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.Log;
+import android.util.Property;
+import android.util.TypedValue;
+import android.view.ViewDebug;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.Interpolator;
+
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.internal.util.NotificationColorUtil;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.notification.NotificationIconDozeHelper;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+
+import java.text.NumberFormat;
+import java.util.Arrays;
+
+public class StatusBarIconView extends AnimatedImageView {
+    public static final int NO_COLOR = 0;
+
+    /**
+     * Multiply alpha values with (1+DARK_ALPHA_BOOST) when dozing. The chosen value boosts
+     * everything above 30% to 50%, making it appear on 1bit color depths.
+     */
+    private static final float DARK_ALPHA_BOOST = 0.67f;
+    private final int ANIMATION_DURATION_FAST = 100;
+
+    public static final int STATE_ICON = 0;
+    public static final int STATE_DOT = 1;
+    public static final int STATE_HIDDEN = 2;
+
+    private static final String TAG = "StatusBarIconView";
+    private static final Property<StatusBarIconView, Float> ICON_APPEAR_AMOUNT
+            = new FloatProperty<StatusBarIconView>("iconAppearAmount") {
+
+        @Override
+        public void setValue(StatusBarIconView object, float value) {
+            object.setIconAppearAmount(value);
+        }
+
+        @Override
+        public Float get(StatusBarIconView object) {
+            return object.getIconAppearAmount();
+        }
+    };
+    private static final Property<StatusBarIconView, Float> DOT_APPEAR_AMOUNT
+            = new FloatProperty<StatusBarIconView>("dot_appear_amount") {
+
+        @Override
+        public void setValue(StatusBarIconView object, float value) {
+            object.setDotAppearAmount(value);
+        }
+
+        @Override
+        public Float get(StatusBarIconView object) {
+            return object.getDotAppearAmount();
+        }
+    };
+
+    private boolean mAlwaysScaleIcon;
+    private int mStatusBarIconDrawingSizeDark = 1;
+    private int mStatusBarIconDrawingSize = 1;
+    private int mStatusBarIconSize = 1;
+    private StatusBarIcon mIcon;
+    @ViewDebug.ExportedProperty private String mSlot;
+    private Drawable mNumberBackground;
+    private Paint mNumberPain;
+    private int mNumberX;
+    private int mNumberY;
+    private String mNumberText;
+    private StatusBarNotification mNotification;
+    private final boolean mBlocked;
+    private int mDensity;
+    private float mIconScale = 1.0f;
+    private final Paint mDotPaint = new Paint();
+    private float mDotRadius;
+    private int mStaticDotRadius;
+    private int mVisibleState = STATE_ICON;
+    private float mIconAppearAmount = 1.0f;
+    private ObjectAnimator mIconAppearAnimator;
+    private ObjectAnimator mDotAnimator;
+    private float mDotAppearAmount;
+    private OnVisibilityChangedListener mOnVisibilityChangedListener;
+    private int mDrawableColor;
+    private int mIconColor;
+    private int mDecorColor;
+    private float mDarkAmount;
+    private ValueAnimator mColorAnimator;
+    private int mCurrentSetColor = NO_COLOR;
+    private int mAnimationStartColor = NO_COLOR;
+    private final ValueAnimator.AnimatorUpdateListener mColorUpdater
+            = animation -> {
+        int newColor = NotificationUtils.interpolateColors(mAnimationStartColor, mIconColor,
+                animation.getAnimatedFraction());
+        setColorInternal(newColor);
+    };
+    private final NotificationIconDozeHelper mDozer;
+    private int mContrastedDrawableColor;
+    private int mCachedContrastBackgroundColor = NO_COLOR;
+    private float[] mMatrix;
+    private ColorMatrixColorFilter mMatrixColorFilter;
+    private boolean mIsInShelf;
+
+    public StatusBarIconView(Context context, String slot, StatusBarNotification sbn) {
+        this(context, slot, sbn, false);
+    }
+
+    public StatusBarIconView(Context context, String slot, StatusBarNotification sbn,
+            boolean blocked) {
+        super(context);
+        mDozer = new NotificationIconDozeHelper(context);
+        mBlocked = blocked;
+        mSlot = slot;
+        mNumberPain = new Paint();
+        mNumberPain.setTextAlign(Paint.Align.CENTER);
+        mNumberPain.setColor(context.getColor(R.drawable.notification_number_text_color));
+        mNumberPain.setAntiAlias(true);
+        setNotification(sbn);
+        maybeUpdateIconScaleDimens();
+        setScaleType(ScaleType.CENTER);
+        mDensity = context.getResources().getDisplayMetrics().densityDpi;
+        if (mNotification != null) {
+            setDecorColor(getContext().getColor(
+                    com.android.internal.R.color.notification_icon_default_color));
+        }
+        reloadDimens();
+    }
+
+    private void maybeUpdateIconScaleDimens() {
+        // We do not resize and scale system icons (on the right), only notification icons (on the
+        // left).
+        if (mNotification != null || mAlwaysScaleIcon) {
+            updateIconScaleDimens();
+        }
+    }
+
+    private void updateIconScaleDimens() {
+        Resources res = mContext.getResources();
+        mStatusBarIconSize = res.getDimensionPixelSize(R.dimen.status_bar_icon_size);
+        mStatusBarIconDrawingSizeDark =
+                res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size_dark);
+        mStatusBarIconDrawingSize =
+                res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size);
+        updateIconScale();
+    }
+
+    private void updateIconScale() {
+        final float imageBounds = NotificationUtils.interpolate(
+                mStatusBarIconDrawingSize,
+                mStatusBarIconDrawingSizeDark,
+                mDarkAmount);
+        final int outerBounds = mStatusBarIconSize;
+        mIconScale = (float)imageBounds / (float)outerBounds;
+    }
+
+    public float getIconScaleFullyDark() {
+        return (float) mStatusBarIconDrawingSizeDark / mStatusBarIconDrawingSize;
+    }
+
+    public float getIconScale() {
+        return mIconScale;
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        int density = newConfig.densityDpi;
+        if (density != mDensity) {
+            mDensity = density;
+            maybeUpdateIconScaleDimens();
+            updateDrawable();
+            reloadDimens();
+        }
+    }
+
+    private void reloadDimens() {
+        boolean applyRadius = mDotRadius == mStaticDotRadius;
+        mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
+        if (applyRadius) {
+            mDotRadius = mStaticDotRadius;
+        }
+    }
+
+    public void setNotification(StatusBarNotification notification) {
+        mNotification = notification;
+        if (notification != null) {
+            setContentDescription(notification.getNotification());
+        }
+    }
+
+    public StatusBarIconView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mDozer = new NotificationIconDozeHelper(context);
+        mBlocked = false;
+        mAlwaysScaleIcon = true;
+        updateIconScaleDimens();
+        mDensity = context.getResources().getDisplayMetrics().densityDpi;
+    }
+
+    private static boolean streq(String a, String b) {
+        if (a == b) {
+            return true;
+        }
+        if (a == null && b != null) {
+            return false;
+        }
+        if (a != null && b == null) {
+            return false;
+        }
+        return a.equals(b);
+    }
+
+    public boolean equalIcons(Icon a, Icon b) {
+        if (a == b) return true;
+        if (a.getType() != b.getType()) return false;
+        switch (a.getType()) {
+            case Icon.TYPE_RESOURCE:
+                return a.getResPackage().equals(b.getResPackage()) && a.getResId() == b.getResId();
+            case Icon.TYPE_URI:
+                return a.getUriString().equals(b.getUriString());
+            default:
+                return false;
+        }
+    }
+    /**
+     * Returns whether the set succeeded.
+     */
+    public boolean set(StatusBarIcon icon) {
+        final boolean iconEquals = mIcon != null && equalIcons(mIcon.icon, icon.icon);
+        final boolean levelEquals = iconEquals
+                && mIcon.iconLevel == icon.iconLevel;
+        final boolean visibilityEquals = mIcon != null
+                && mIcon.visible == icon.visible;
+        final boolean numberEquals = mIcon != null
+                && mIcon.number == icon.number;
+        mIcon = icon.clone();
+        setContentDescription(icon.contentDescription);
+        if (!iconEquals) {
+            if (!updateDrawable(false /* no clear */)) return false;
+            // we have to clear the grayscale tag since it may have changed
+            setTag(R.id.icon_is_grayscale, null);
+        }
+        if (!levelEquals) {
+            setImageLevel(icon.iconLevel);
+        }
+
+        if (!numberEquals) {
+            if (icon.number > 0 && getContext().getResources().getBoolean(
+                        R.bool.config_statusBarShowNumber)) {
+                if (mNumberBackground == null) {
+                    mNumberBackground = getContext().getResources().getDrawable(
+                            R.drawable.ic_notification_overlay);
+                }
+                placeNumber();
+            } else {
+                mNumberBackground = null;
+                mNumberText = null;
+            }
+            invalidate();
+        }
+        if (!visibilityEquals) {
+            setVisibility(icon.visible && !mBlocked ? VISIBLE : GONE);
+        }
+        return true;
+    }
+
+    public void updateDrawable() {
+        updateDrawable(true /* with clear */);
+    }
+
+    private boolean updateDrawable(boolean withClear) {
+        if (mIcon == null) {
+            return false;
+        }
+        Drawable drawable;
+        try {
+            drawable = getIcon(mIcon);
+        } catch (OutOfMemoryError e) {
+            Log.w(TAG, "OOM while inflating " + mIcon.icon + " for slot " + mSlot);
+            return false;
+        }
+
+        if (drawable == null) {
+            Log.w(TAG, "No icon for slot " + mSlot + "; " + mIcon.icon);
+            return false;
+        }
+        if (withClear) {
+            setImageDrawable(null);
+        }
+        setImageDrawable(drawable);
+        return true;
+    }
+
+    public Icon getSourceIcon() {
+        return mIcon.icon;
+    }
+
+    private Drawable getIcon(StatusBarIcon icon) {
+        return getIcon(getContext(), icon);
+    }
+
+    /**
+     * Returns the right icon to use for this item
+     *
+     * @param context Context to use to get resources
+     * @return Drawable for this item, or null if the package or item could not
+     *         be found
+     */
+    public static Drawable getIcon(Context context, StatusBarIcon statusBarIcon) {
+        int userId = statusBarIcon.user.getIdentifier();
+        if (userId == UserHandle.USER_ALL) {
+            userId = UserHandle.USER_SYSTEM;
+        }
+
+        Drawable icon = statusBarIcon.icon.loadDrawableAsUser(context, userId);
+
+        TypedValue typedValue = new TypedValue();
+        context.getResources().getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
+        float scaleFactor = typedValue.getFloat();
+
+        // No need to scale the icon, so return it as is.
+        if (scaleFactor == 1.f) {
+            return icon;
+        }
+
+        return new ScalingDrawableWrapper(icon, scaleFactor);
+    }
+
+    public StatusBarIcon getStatusBarIcon() {
+        return mIcon;
+    }
+
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEvent(event);
+        if (mNotification != null) {
+            event.setParcelableData(mNotification.getNotification());
+        }
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        if (mNumberBackground != null) {
+            placeNumber();
+        }
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+        updateDrawable();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (mIconAppearAmount > 0.0f) {
+            canvas.save();
+            canvas.scale(mIconScale * mIconAppearAmount, mIconScale * mIconAppearAmount,
+                    getWidth() / 2, getHeight() / 2);
+            super.onDraw(canvas);
+            canvas.restore();
+        }
+
+        if (mNumberBackground != null) {
+            mNumberBackground.draw(canvas);
+            canvas.drawText(mNumberText, mNumberX, mNumberY, mNumberPain);
+        }
+        if (mDotAppearAmount != 0.0f) {
+            float radius;
+            float alpha;
+            if (mDotAppearAmount <= 1.0f) {
+                radius = mDotRadius * mDotAppearAmount;
+                alpha = 1.0f;
+            } else {
+                float fadeOutAmount = mDotAppearAmount - 1.0f;
+                alpha = 1.0f - fadeOutAmount;
+                radius = NotificationUtils.interpolate(mDotRadius, getWidth() / 4, fadeOutAmount);
+            }
+            mDotPaint.setAlpha((int) (alpha * 255));
+            canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, mDotPaint);
+        }
+    }
+
+    @Override
+    protected void debug(int depth) {
+        super.debug(depth);
+        Log.d("View", debugIndent(depth) + "slot=" + mSlot);
+        Log.d("View", debugIndent(depth) + "icon=" + mIcon);
+    }
+
+    void placeNumber() {
+        final String str;
+        final int tooBig = getContext().getResources().getInteger(
+                android.R.integer.status_bar_notification_info_maxnum);
+        if (mIcon.number > tooBig) {
+            str = getContext().getResources().getString(
+                        android.R.string.status_bar_notification_info_overflow);
+        } else {
+            NumberFormat f = NumberFormat.getIntegerInstance();
+            str = f.format(mIcon.number);
+        }
+        mNumberText = str;
+
+        final int w = getWidth();
+        final int h = getHeight();
+        final Rect r = new Rect();
+        mNumberPain.getTextBounds(str, 0, str.length(), r);
+        final int tw = r.right - r.left;
+        final int th = r.bottom - r.top;
+        mNumberBackground.getPadding(r);
+        int dw = r.left + tw + r.right;
+        if (dw < mNumberBackground.getMinimumWidth()) {
+            dw = mNumberBackground.getMinimumWidth();
+        }
+        mNumberX = w-r.right-((dw-r.right-r.left)/2);
+        int dh = r.top + th + r.bottom;
+        if (dh < mNumberBackground.getMinimumWidth()) {
+            dh = mNumberBackground.getMinimumWidth();
+        }
+        mNumberY = h-r.bottom-((dh-r.top-th-r.bottom)/2);
+        mNumberBackground.setBounds(w-dw, h-dh, w, h);
+    }
+
+    private void setContentDescription(Notification notification) {
+        if (notification != null) {
+            String d = contentDescForNotification(mContext, notification);
+            if (!TextUtils.isEmpty(d)) {
+                setContentDescription(d);
+            }
+        }
+    }
+
+    public String toString() {
+        return "StatusBarIconView(slot=" + mSlot + " icon=" + mIcon
+            + " notification=" + mNotification + ")";
+    }
+
+    public StatusBarNotification getNotification() {
+        return mNotification;
+    }
+
+    public String getSlot() {
+        return mSlot;
+    }
+
+
+    public static String contentDescForNotification(Context c, Notification n) {
+        String appName = "";
+        try {
+            Notification.Builder builder = Notification.Builder.recoverBuilder(c, n);
+            appName = builder.loadHeaderAppName();
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Unable to recover builder", e);
+            // Trying to get the app name from the app info instead.
+            Parcelable appInfo = n.extras.getParcelable(
+                    Notification.EXTRA_BUILDER_APPLICATION_INFO);
+            if (appInfo instanceof ApplicationInfo) {
+                appName = String.valueOf(((ApplicationInfo) appInfo).loadLabel(
+                        c.getPackageManager()));
+            }
+        }
+
+        CharSequence title = n.extras.getCharSequence(Notification.EXTRA_TITLE);
+        CharSequence text = n.extras.getCharSequence(Notification.EXTRA_TEXT);
+        CharSequence ticker = n.tickerText;
+
+        // Some apps just put the app name into the title
+        CharSequence titleOrText = TextUtils.equals(title, appName) ? text : title;
+
+        CharSequence desc = !TextUtils.isEmpty(titleOrText) ? titleOrText
+                : !TextUtils.isEmpty(ticker) ? ticker : "";
+
+        return c.getString(R.string.accessibility_desc_notification_icon, appName, desc);
+    }
+
+    /**
+     * Set the color that is used to draw decoration like the overflow dot. This will not be applied
+     * to the drawable.
+     */
+    public void setDecorColor(int iconTint) {
+        mDecorColor = iconTint;
+        updateDecorColor();
+    }
+
+    private void updateDecorColor() {
+        int color = NotificationUtils.interpolateColors(mDecorColor, Color.WHITE, mDarkAmount);
+        if (mDotPaint.getColor() != color) {
+            mDotPaint.setColor(color);
+
+            if (mDotAppearAmount != 0) {
+                invalidate();
+            }
+        }
+    }
+
+    /**
+     * Set the static color that should be used for the drawable of this icon if it's not
+     * transitioning this also immediately sets the color.
+     */
+    public void setStaticDrawableColor(int color) {
+        mDrawableColor = color;
+        setColorInternal(color);
+        updateContrastedStaticColor();
+        mIconColor = color;
+        mDozer.setColor(color);
+    }
+
+    private void setColorInternal(int color) {
+        mCurrentSetColor = color;
+        updateIconColor();
+    }
+
+    private void updateIconColor() {
+        if (mCurrentSetColor != NO_COLOR) {
+            if (mMatrixColorFilter == null) {
+                mMatrix = new float[4 * 5];
+                mMatrixColorFilter = new ColorMatrixColorFilter(mMatrix);
+            }
+            int color = NotificationUtils.interpolateColors(
+                    mCurrentSetColor, Color.WHITE, mDarkAmount);
+            updateTintMatrix(mMatrix, color, DARK_ALPHA_BOOST * mDarkAmount);
+            mMatrixColorFilter.setColorMatrixArray(mMatrix);
+            setColorFilter(mMatrixColorFilter);
+            invalidate();  // setColorFilter only invalidates if the filter instance changed.
+        } else {
+            mDozer.updateGrayscale(this, mDarkAmount);
+        }
+    }
+
+    /**
+     * Updates {@param array} such that it represents a matrix that changes RGB to {@param color}
+     * and multiplies the alpha channel with the color's alpha+{@param alphaBoost}.
+     */
+    private static void updateTintMatrix(float[] array, int color, float alphaBoost) {
+        Arrays.fill(array, 0);
+        array[4] = Color.red(color);
+        array[9] = Color.green(color);
+        array[14] = Color.blue(color);
+        array[18] = Color.alpha(color) / 255f + alphaBoost;
+    }
+
+    public void setIconColor(int iconColor, boolean animate) {
+        if (mIconColor != iconColor) {
+            mIconColor = iconColor;
+            if (mColorAnimator != null) {
+                mColorAnimator.cancel();
+            }
+            if (mCurrentSetColor == iconColor) {
+                return;
+            }
+            if (animate && mCurrentSetColor != NO_COLOR) {
+                mAnimationStartColor = mCurrentSetColor;
+                mColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+                mColorAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+                mColorAnimator.setDuration(ANIMATION_DURATION_FAST);
+                mColorAnimator.addUpdateListener(mColorUpdater);
+                mColorAnimator.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        mColorAnimator = null;
+                        mAnimationStartColor = NO_COLOR;
+                    }
+                });
+                mColorAnimator.start();
+            } else {
+                setColorInternal(iconColor);
+            }
+        }
+    }
+
+    public int getStaticDrawableColor() {
+        return mDrawableColor;
+    }
+
+    /**
+     * A drawable color that passes GAR on a specific background.
+     * This value is cached.
+     *
+     * @param backgroundColor Background to test against.
+     * @return GAR safe version of {@link StatusBarIconView#getStaticDrawableColor()}.
+     */
+    int getContrastedStaticDrawableColor(int backgroundColor) {
+        if (mCachedContrastBackgroundColor != backgroundColor) {
+            mCachedContrastBackgroundColor = backgroundColor;
+            updateContrastedStaticColor();
+        }
+        return mContrastedDrawableColor;
+    }
+
+    private void updateContrastedStaticColor() {
+        if (Color.alpha(mCachedContrastBackgroundColor) != 255) {
+            mContrastedDrawableColor = mDrawableColor;
+            return;
+        }
+        // We'll modify the color if it doesn't pass GAR
+        int contrastedColor = mDrawableColor;
+        if (!NotificationColorUtil.satisfiesTextContrast(mCachedContrastBackgroundColor,
+                contrastedColor)) {
+            float[] hsl = new float[3];
+            ColorUtils.colorToHSL(mDrawableColor, hsl);
+            // This is basically a light grey, pushing the color will only distort it.
+            // Best thing to do in here is to fallback to the default color.
+            if (hsl[1] < 0.2f) {
+                contrastedColor = Notification.COLOR_DEFAULT;
+            }
+            contrastedColor = NotificationColorUtil.resolveContrastColor(mContext,
+                    contrastedColor, mCachedContrastBackgroundColor);
+        }
+        mContrastedDrawableColor = contrastedColor;
+    }
+
+    public void setVisibleState(int state) {
+        setVisibleState(state, true /* animate */, null /* endRunnable */);
+    }
+
+    public void setVisibleState(int state, boolean animate) {
+        setVisibleState(state, animate, null);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    public void setVisibleState(int visibleState, boolean animate, Runnable endRunnable) {
+        boolean runnableAdded = false;
+        if (visibleState != mVisibleState) {
+            mVisibleState = visibleState;
+            if (mIconAppearAnimator != null) {
+                mIconAppearAnimator.cancel();
+            }
+            if (mDotAnimator != null) {
+                mDotAnimator.cancel();
+            }
+            if (animate) {
+                float targetAmount = 0.0f;
+                Interpolator interpolator = Interpolators.FAST_OUT_LINEAR_IN;
+                if (visibleState == STATE_ICON) {
+                    targetAmount = 1.0f;
+                    interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+                }
+                float currentAmount = getIconAppearAmount();
+                if (targetAmount != currentAmount) {
+                    mIconAppearAnimator = ObjectAnimator.ofFloat(this, ICON_APPEAR_AMOUNT,
+                            currentAmount, targetAmount);
+                    mIconAppearAnimator.setInterpolator(interpolator);
+                    mIconAppearAnimator.setDuration(ANIMATION_DURATION_FAST);
+                    mIconAppearAnimator.addListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            mIconAppearAnimator = null;
+                            runRunnable(endRunnable);
+                        }
+                    });
+                    mIconAppearAnimator.start();
+                    runnableAdded = true;
+                }
+
+                targetAmount = visibleState == STATE_ICON ? 2.0f : 0.0f;
+                interpolator = Interpolators.FAST_OUT_LINEAR_IN;
+                if (visibleState == STATE_DOT) {
+                    targetAmount = 1.0f;
+                    interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+                }
+                currentAmount = getDotAppearAmount();
+                if (targetAmount != currentAmount) {
+                    mDotAnimator = ObjectAnimator.ofFloat(this, DOT_APPEAR_AMOUNT,
+                            currentAmount, targetAmount);
+                    mDotAnimator.setInterpolator(interpolator);
+                    mDotAnimator.setDuration(ANIMATION_DURATION_FAST);
+                    final boolean runRunnable = !runnableAdded;
+                    mDotAnimator.addListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            mDotAnimator = null;
+                            if (runRunnable) {
+                                runRunnable(endRunnable);
+                            }
+                        }
+                    });
+                    mDotAnimator.start();
+                    runnableAdded = true;
+                }
+            } else {
+                setIconAppearAmount(visibleState == STATE_ICON ? 1.0f : 0.0f);
+                setDotAppearAmount(visibleState == STATE_DOT ? 1.0f
+                        : visibleState == STATE_ICON ? 2.0f
+                        : 0.0f);
+            }
+        }
+        if (!runnableAdded) {
+            runRunnable(endRunnable);
+        }
+    }
+
+    private void runRunnable(Runnable runnable) {
+        if (runnable != null) {
+            runnable.run();
+        }
+    }
+
+    public void setIconAppearAmount(float iconAppearAmount) {
+        if (mIconAppearAmount != iconAppearAmount) {
+            mIconAppearAmount = iconAppearAmount;
+            invalidate();
+        }
+    }
+
+    public float getIconAppearAmount() {
+        return mIconAppearAmount;
+    }
+
+    public int getVisibleState() {
+        return mVisibleState;
+    }
+
+    public void setDotAppearAmount(float dotAppearAmount) {
+        if (mDotAppearAmount != dotAppearAmount) {
+            mDotAppearAmount = dotAppearAmount;
+            invalidate();
+        }
+    }
+
+    @Override
+    public void setVisibility(int visibility) {
+        super.setVisibility(visibility);
+        if (mOnVisibilityChangedListener != null) {
+            mOnVisibilityChangedListener.onVisibilityChanged(visibility);
+        }
+    }
+
+    public float getDotAppearAmount() {
+        return mDotAppearAmount;
+    }
+
+    public void setOnVisibilityChangedListener(OnVisibilityChangedListener listener) {
+        mOnVisibilityChangedListener = listener;
+    }
+
+    public void setDark(boolean dark, boolean fade, long delay) {
+        mDozer.setIntensityDark(f -> {
+            mDarkAmount = f;
+            updateIconScale();
+            updateDecorColor();
+            updateIconColor();
+            updateAllowAnimation();
+        }, dark, fade, delay);
+    }
+
+    private void updateAllowAnimation() {
+        if (mDarkAmount == 0 || mDarkAmount == 1) {
+            setAllowAnimation(mDarkAmount == 0);
+        }
+    }
+
+    public void setIsInShelf(boolean isInShelf) {
+        mIsInShelf = isInShelf;
+    }
+
+    public boolean isInShelf() {
+        return mIsInShelf;
+    }
+
+    public interface OnVisibilityChangedListener {
+        void onVisibilityChanged(int newVisibility);
+    }
+}
diff --git a/com/android/systemui/statusbar/StatusBarState.java b/com/android/systemui/statusbar/StatusBarState.java
new file mode 100644
index 0000000..c0148c0
--- /dev/null
+++ b/com/android/systemui/statusbar/StatusBarState.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2014 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;
+
+/**
+ * Class to encapsulate all possible status bar states regarding Keyguard.
+ */
+public class StatusBarState {
+
+    /**
+     * The status bar is in the "normal" shade mode.
+     */
+    public static final int SHADE = 0;
+
+    /**
+     * Status bar is currently the Keyguard.
+     */
+    public static final int KEYGUARD = 1;
+
+    /**
+     * Status bar is in the special mode, where it is fully interactive but still locked. So
+     * dismissing the shade will still show the bouncer.
+     */
+    public static final int SHADE_LOCKED = 2;
+
+    /**
+     * Status bar is locked and shows the full screen user switcher.
+     */
+    public static final int FULLSCREEN_USER_SWITCHER = 3;
+
+
+    public static String toShortString(int x) {
+        switch (x) {
+            case SHADE:
+                return "SHD";
+            case SHADE_LOCKED:
+                return "SHD_LCK";
+            case KEYGUARD:
+                return "KGRD";
+            case FULLSCREEN_USER_SWITCHER:
+                return "FS_USRSW";
+            default:
+                return "bad_value_" + x;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/TransformableView.java b/com/android/systemui/statusbar/TransformableView.java
new file mode 100644
index 0000000..063252f
--- /dev/null
+++ b/com/android/systemui/statusbar/TransformableView.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016 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 com.android.systemui.statusbar.notification.TransformState;
+
+/**
+ * A view that can be transformed to and from.
+ */
+public interface TransformableView {
+    int TRANSFORMING_VIEW_ICON = 0;
+    int TRANSFORMING_VIEW_TITLE = 1;
+    int TRANSFORMING_VIEW_TEXT = 2;
+    int TRANSFORMING_VIEW_IMAGE = 3;
+    int TRANSFORMING_VIEW_PROGRESS = 4;
+    int TRANSFORMING_VIEW_ACTIONS = 5;
+
+    /**
+     * Get the current state of a view in a transform animation
+     *
+     * @param fadingView which view we are interested in
+     * @return the current transform state of this viewtype
+     */
+    TransformState getCurrentState(int fadingView);
+
+    /**
+     * Transform to the given view
+     *
+     * @param notification the view to transform to
+     */
+    void transformTo(TransformableView notification, Runnable endRunnable);
+
+    /**
+     * Transform to the given view by a specified amount.
+     *
+     * @param notification the view to transform to
+     * @param transformationAmount how much transformation should be done
+     */
+    void transformTo(TransformableView notification, float transformationAmount);
+
+    /**
+     * Transform to this view from the given view
+     *
+     * @param notification the view to transform from
+     */
+    void transformFrom(TransformableView notification);
+
+    /**
+     * Transform to this view from the given view by a specified amount.
+     *
+     * @param notification the view to transform from
+     * @param transformationAmount how much transformation should be done
+     */
+    void transformFrom(TransformableView notification, float transformationAmount);
+
+    /**
+     * Set this view to be fully visible or gone
+     *
+     * @param visible
+     */
+    void setVisible(boolean visible);
+}
diff --git a/com/android/systemui/statusbar/UserUtil.java b/com/android/systemui/statusbar/UserUtil.java
new file mode 100644
index 0000000..f9afc7c
--- /dev/null
+++ b/com/android/systemui/statusbar/UserUtil.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 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 com.android.systemui.statusbar.phone.SystemUIDialog;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+import android.content.Context;
+import android.content.DialogInterface;
+
+import com.android.systemui.R;
+
+public class UserUtil {
+    public static void deleteUserWithPrompt(Context context, int userId,
+                                            UserSwitcherController userSwitcherController) {
+        new RemoveUserDialog(context, userId, userSwitcherController).show();
+    }
+
+    private final static class RemoveUserDialog extends SystemUIDialog implements
+            DialogInterface.OnClickListener {
+
+        private final int mUserId;
+        private final UserSwitcherController mUserSwitcherController;
+
+        public RemoveUserDialog(Context context, int userId,
+                                UserSwitcherController userSwitcherController) {
+            super(context);
+            setTitle(R.string.user_remove_user_title);
+            setMessage(context.getString(R.string.user_remove_user_message));
+            setButton(DialogInterface.BUTTON_NEGATIVE,
+                    context.getString(android.R.string.cancel), this);
+            setButton(DialogInterface.BUTTON_POSITIVE,
+                    context.getString(R.string.user_remove_user_remove), this);
+            setCanceledOnTouchOutside(false);
+            mUserId = userId;
+            mUserSwitcherController = userSwitcherController;
+        }
+
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            if (which == BUTTON_NEGATIVE) {
+                cancel();
+            } else {
+                dismiss();
+                mUserSwitcherController.removeUserId(mUserId);
+            }
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/ViewTransformationHelper.java b/com/android/systemui/statusbar/ViewTransformationHelper.java
new file mode 100644
index 0000000..5353005
--- /dev/null
+++ b/com/android/systemui/statusbar/ViewTransformationHelper.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2016 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.notification.TransformState;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+import java.util.Stack;
+
+/**
+ * A view that can be transformed to and from.
+ */
+public class ViewTransformationHelper implements TransformableView {
+
+    private static final int TAG_CONTAINS_TRANSFORMED_VIEW = R.id.contains_transformed_view;
+
+    private ArrayMap<Integer, View> mTransformedViews = new ArrayMap<>();
+    private ArrayMap<Integer, CustomTransformation> mCustomTransformations = new ArrayMap<>();
+    private ValueAnimator mViewTransformationAnimation;
+
+    public void addTransformedView(int key, View transformedView) {
+        mTransformedViews.put(key, transformedView);
+    }
+
+    public void reset() {
+        mTransformedViews.clear();
+    }
+
+    public void setCustomTransformation(CustomTransformation transformation, int viewType) {
+        mCustomTransformations.put(viewType, transformation);
+    }
+
+    @Override
+    public TransformState getCurrentState(int fadingView) {
+        View view = mTransformedViews.get(fadingView);
+        if (view != null && view.getVisibility() != View.GONE) {
+            return TransformState.createFrom(view);
+        }
+        return null;
+    }
+
+    @Override
+    public void transformTo(final TransformableView notification, final Runnable endRunnable) {
+        if (mViewTransformationAnimation != null) {
+            mViewTransformationAnimation.cancel();
+        }
+        mViewTransformationAnimation = ValueAnimator.ofFloat(0.0f, 1.0f);
+        mViewTransformationAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                transformTo(notification, animation.getAnimatedFraction());
+            }
+        });
+        mViewTransformationAnimation.setInterpolator(Interpolators.LINEAR);
+        mViewTransformationAnimation.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+        mViewTransformationAnimation.addListener(new AnimatorListenerAdapter() {
+            public boolean mCancelled;
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (!mCancelled) {
+                    if (endRunnable != null) {
+                        endRunnable.run();
+                    }
+                    setVisible(false);
+                } else {
+                    abortTransformations();
+                }
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mCancelled = true;
+            }
+        });
+        mViewTransformationAnimation.start();
+    }
+
+    @Override
+    public void transformTo(TransformableView notification, float transformationAmount) {
+        for (Integer viewType : mTransformedViews.keySet()) {
+            TransformState ownState = getCurrentState(viewType);
+            if (ownState != null) {
+                CustomTransformation customTransformation = mCustomTransformations.get(viewType);
+                if (customTransformation != null && customTransformation.transformTo(
+                        ownState, notification, transformationAmount)) {
+                    ownState.recycle();
+                    continue;
+                }
+                TransformState otherState = notification.getCurrentState(viewType);
+                if (otherState != null) {
+                    ownState.transformViewTo(otherState, transformationAmount);
+                    otherState.recycle();
+                } else {
+                    ownState.disappear(transformationAmount, notification);
+                }
+                ownState.recycle();
+            }
+        }
+    }
+
+    @Override
+    public void transformFrom(final TransformableView notification) {
+        if (mViewTransformationAnimation != null) {
+            mViewTransformationAnimation.cancel();
+        }
+        mViewTransformationAnimation = ValueAnimator.ofFloat(0.0f, 1.0f);
+        mViewTransformationAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                transformFrom(notification, animation.getAnimatedFraction());
+            }
+        });
+        mViewTransformationAnimation.addListener(new AnimatorListenerAdapter() {
+            public boolean mCancelled;
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (!mCancelled) {
+                    setVisible(true);
+                } else {
+                    abortTransformations();
+                }
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mCancelled = true;
+            }
+        });
+        mViewTransformationAnimation.setInterpolator(Interpolators.LINEAR);
+        mViewTransformationAnimation.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+        mViewTransformationAnimation.start();
+    }
+
+    @Override
+    public void transformFrom(TransformableView notification, float transformationAmount) {
+        for (Integer viewType : mTransformedViews.keySet()) {
+            TransformState ownState = getCurrentState(viewType);
+            if (ownState != null) {
+                CustomTransformation customTransformation = mCustomTransformations.get(viewType);
+                if (customTransformation != null && customTransformation.transformFrom(
+                        ownState, notification, transformationAmount)) {
+                    ownState.recycle();
+                    continue;
+                }
+                TransformState otherState = notification.getCurrentState(viewType);
+                if (otherState != null) {
+                    ownState.transformViewFrom(otherState, transformationAmount);
+                    otherState.recycle();
+                } else {
+                    ownState.appear(transformationAmount, notification);
+                }
+                ownState.recycle();
+            }
+        }
+    }
+
+    @Override
+    public void setVisible(boolean visible) {
+        if (mViewTransformationAnimation != null) {
+            mViewTransformationAnimation.cancel();
+        }
+        for (Integer viewType : mTransformedViews.keySet()) {
+            TransformState ownState = getCurrentState(viewType);
+            if (ownState != null) {
+                ownState.setVisible(visible, false /* force */);
+                ownState.recycle();
+            }
+        }
+    }
+
+    private void abortTransformations() {
+        for (Integer viewType : mTransformedViews.keySet()) {
+            TransformState ownState = getCurrentState(viewType);
+            if (ownState != null) {
+                ownState.abortTransformation();
+                ownState.recycle();
+            }
+        }
+    }
+
+    /**
+     * Add the remaining transformation views such that all views are being transformed correctly
+     * @param viewRoot the root below which all elements need to be transformed
+     */
+    public void addRemainingTransformTypes(View viewRoot) {
+        // lets now tag the right views
+        int numValues = mTransformedViews.size();
+        for (int i = 0; i < numValues; i++) {
+            View view = mTransformedViews.valueAt(i);
+            while (view != viewRoot.getParent()) {
+                view.setTag(TAG_CONTAINS_TRANSFORMED_VIEW, true);
+                view = (View) view.getParent();
+            }
+        }
+        Stack<View> stack = new Stack<>();
+        // Add the right views now
+        stack.push(viewRoot);
+        while (!stack.isEmpty()) {
+            View child = stack.pop();
+            Boolean containsView = (Boolean) child.getTag(TAG_CONTAINS_TRANSFORMED_VIEW);
+            if (containsView == null) {
+                // This one is unhandled, let's add it to our list.
+                int id = child.getId();
+                if (id != View.NO_ID) {
+                    // We only fade views with an id
+                    addTransformedView(id, child);
+                    continue;
+                }
+            }
+            child.setTag(TAG_CONTAINS_TRANSFORMED_VIEW, null);
+            if (child instanceof ViewGroup && !mTransformedViews.containsValue(child)){
+                ViewGroup group = (ViewGroup) child;
+                for (int i = 0; i < group.getChildCount(); i++) {
+                    stack.push(group.getChildAt(i));
+                }
+            }
+        }
+    }
+
+    public void resetTransformedView(View view) {
+        TransformState state = TransformState.createFrom(view);
+        state.setVisible(true /* visible */, true /* force */);
+        state.recycle();
+    }
+
+    /**
+     * @return a set of all views are being transformed.
+     */
+    public ArraySet<View> getAllTransformingViews() {
+        return new ArraySet<>(mTransformedViews.values());
+    }
+
+    public static abstract class CustomTransformation {
+        /**
+         * Transform a state to the given view
+         * @param ownState the state to transform
+         * @param notification the view to transform to
+         * @param transformationAmount how much transformation should be done
+         * @return whether a custom transformation is performed
+         */
+        public abstract boolean transformTo(TransformState ownState,
+                TransformableView notification,
+                float transformationAmount);
+
+        /**
+         * Transform to this state from the given view
+         * @param ownState the state to transform to
+         * @param notification the view to transform from
+         * @param transformationAmount how much transformation should be done
+         * @return whether a custom transformation is performed
+         */
+        public abstract boolean transformFrom(TransformState ownState,
+                TransformableView notification,
+                float transformationAmount);
+
+        /**
+         * Perform a custom initialisation before transforming.
+         *
+         * @param ownState our own state
+         * @param otherState the other state
+         * @return whether a custom initialization is done
+         */
+        public boolean initTransformation(TransformState ownState,
+                TransformState otherState) {
+            return false;
+        }
+
+        public boolean customTransformTarget(TransformState ownState,
+                TransformState otherState) {
+            return false;
+        }
+
+        /**
+         * Get a custom interpolator for this animation
+         * @param interpolationType the type of the interpolation, i.e TranslationX / TranslationY
+         * @param isFrom true if this transformation from the other view
+         */
+        public Interpolator getCustomInterpolator(int interpolationType, boolean isFrom) {
+            return null;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/car/CarBatteryController.java b/com/android/systemui/statusbar/car/CarBatteryController.java
new file mode 100644
index 0000000..fc39648
--- /dev/null
+++ b/com/android/systemui/statusbar/car/CarBatteryController.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2016 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.car;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadsetClient;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProfile.ServiceListener;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.systemui.statusbar.policy.BatteryController;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * A {@link BatteryController} that is specific to the Auto use-case. For Auto, the battery icon
+ * displays the battery status of a device that is connected via bluetooth and not the system's
+ * battery.
+ */
+public class CarBatteryController extends BroadcastReceiver implements BatteryController {
+    private static final String TAG = "CarBatteryController";
+
+    // According to the Bluetooth HFP 1.5 specification, battery levels are indicated by a
+    // value from 1-5, where these values represent the following:
+    // 0%% - 0, 1-25%% - 1, 26-50%% - 2, 51-75%% - 3, 76-99%% - 4, 100%% - 5
+    // As a result, set the level as the average within that range.
+    private static final int BATTERY_LEVEL_EMPTY = 0;
+    private static final int BATTERY_LEVEL_1 = 12;
+    private static final int BATTERY_LEVEL_2 = 28;
+    private static final int BATTERY_LEVEL_3 = 63;
+    private static final int BATTERY_LEVEL_4 = 87;
+    private static final int BATTERY_LEVEL_FULL = 100;
+
+    private static final int INVALID_BATTERY_LEVEL = -1;
+
+    private final Context mContext;
+
+    private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
+    private BluetoothHeadsetClient mBluetoothHeadsetClient;
+
+    private final ArrayList<BatteryStateChangeCallback> mChangeCallbacks = new ArrayList<>();
+
+    private int mLevel;
+
+    /**
+     * An interface indicating the container of a View that will display what the information
+     * in the {@link CarBatteryController}.
+     */
+    public interface BatteryViewHandler {
+        void hideBatteryView();
+        void showBatteryView();
+    }
+
+    private BatteryViewHandler mBatteryViewHandler;
+
+    public CarBatteryController(Context context) {
+        mContext = context;
+
+        if (mAdapter == null) {
+           return;
+        }
+
+        mAdapter.getProfileProxy(context.getApplicationContext(), mHfpServiceListener,
+                BluetoothProfile.HEADSET_CLIENT);
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("CarBatteryController state:");
+        pw.print("    mLevel=");
+        pw.println(mLevel);
+    }
+
+    @Override
+    public void setPowerSaveMode(boolean powerSave) {
+        // No-op. No power save mode for the car.
+    }
+
+    @Override
+    public void addCallback(BatteryController.BatteryStateChangeCallback cb) {
+        mChangeCallbacks.add(cb);
+
+        // There is no way to know if the phone is plugged in or charging via bluetooth, so pass
+        // false for these values.
+        cb.onBatteryLevelChanged(mLevel, false /* pluggedIn */, false /* charging */);
+        cb.onPowerSaveChanged(false /* isPowerSave */);
+    }
+
+    @Override
+    public void removeCallback(BatteryController.BatteryStateChangeCallback cb) {
+        mChangeCallbacks.remove(cb);
+    }
+
+    public void addBatteryViewHandler(BatteryViewHandler batteryViewHandler) {
+        mBatteryViewHandler = batteryViewHandler;
+    }
+
+    public void startListening() {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
+        filter.addAction(BluetoothHeadsetClient.ACTION_AG_EVENT);
+        mContext.registerReceiver(this, filter);
+    }
+
+    public void stopListening() {
+        mContext.unregisterReceiver(this);
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "onReceive(). action: " + action);
+        }
+
+        if (BluetoothHeadsetClient.ACTION_AG_EVENT.equals(action)) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Received ACTION_AG_EVENT");
+            }
+
+            int batteryLevel = intent.getIntExtra(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL,
+                    INVALID_BATTERY_LEVEL);
+
+            updateBatteryLevel(batteryLevel);
+
+            if (batteryLevel != INVALID_BATTERY_LEVEL && mBatteryViewHandler != null) {
+                mBatteryViewHandler.showBatteryView();
+            }
+        } else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
+            int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
+                Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED event: "
+                        + oldState + " -> " + newState);
+
+            }
+            BluetoothDevice device =
+                    (BluetoothDevice)intent.getExtra(BluetoothDevice.EXTRA_DEVICE);
+            updateBatteryIcon(device, newState);
+        }
+    }
+
+    /**
+     * Converts the battery level to a percentage that can be displayed on-screen and notifies
+     * any {@link BatteryStateChangeCallback}s of this.
+     */
+    private void updateBatteryLevel(int batteryLevel) {
+        if (batteryLevel == INVALID_BATTERY_LEVEL) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Battery level invalid. Ignoring.");
+            }
+            return;
+        }
+
+        // The battery level is a value between 0-5. Let the default battery level be 0.
+        switch (batteryLevel) {
+            case 5:
+                mLevel = BATTERY_LEVEL_FULL;
+                break;
+            case 4:
+                mLevel = BATTERY_LEVEL_4;
+                break;
+            case 3:
+                mLevel = BATTERY_LEVEL_3;
+                break;
+            case 2:
+                mLevel = BATTERY_LEVEL_2;
+                break;
+            case 1:
+                mLevel = BATTERY_LEVEL_1;
+                break;
+            case 0:
+            default:
+                mLevel = BATTERY_LEVEL_EMPTY;
+        }
+
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "Battery level: " + batteryLevel + "; setting mLevel as: " + mLevel);
+        }
+
+        notifyBatteryLevelChanged();
+    }
+
+    /**
+     * Updates the display of the battery icon depending on the given connection state from the
+     * given {@link BluetoothDevice}.
+     */
+    private void updateBatteryIcon(BluetoothDevice device, int newState) {
+        if (newState == BluetoothProfile.STATE_CONNECTED) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Device connected");
+            }
+
+            if (mBatteryViewHandler != null) {
+                mBatteryViewHandler.showBatteryView();
+            }
+
+            if (mBluetoothHeadsetClient == null || device == null) {
+                return;
+            }
+
+            // Check if battery information is available and immediately update.
+            Bundle featuresBundle = mBluetoothHeadsetClient.getCurrentAgEvents(device);
+            if (featuresBundle == null) {
+                return;
+            }
+
+            int batteryLevel = featuresBundle.getInt(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL,
+                    INVALID_BATTERY_LEVEL);
+            updateBatteryLevel(batteryLevel);
+        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Device disconnected");
+            }
+
+            if (mBatteryViewHandler != null) {
+                mBatteryViewHandler.hideBatteryView();
+            }
+        }
+    }
+
+    @Override
+    public void dispatchDemoCommand(String command, Bundle args) {
+        // TODO: Car demo mode.
+    }
+
+    @Override
+    public boolean isPowerSave() {
+        // Power save is not valid for the car, so always return false.
+        return false;
+    }
+
+    private void notifyBatteryLevelChanged() {
+        for (int i = 0, size = mChangeCallbacks.size(); i < size; i++) {
+            mChangeCallbacks.get(i)
+                    .onBatteryLevelChanged(mLevel, false /* pluggedIn */, false /* charging */);
+        }
+    }
+
+    private final ServiceListener mHfpServiceListener = new ServiceListener() {
+        @Override
+        public void onServiceConnected(int profile, BluetoothProfile proxy) {
+            if (profile == BluetoothProfile.HEADSET_CLIENT) {
+                mBluetoothHeadsetClient = (BluetoothHeadsetClient) proxy;
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(int profile) {
+            if (profile == BluetoothProfile.HEADSET_CLIENT) {
+                mBluetoothHeadsetClient = null;
+            }
+        }
+    };
+
+}
diff --git a/com/android/systemui/statusbar/car/CarNavigationBarController.java b/com/android/systemui/statusbar/car/CarNavigationBarController.java
new file mode 100644
index 0000000..7e08d56
--- /dev/null
+++ b/com/android/systemui/statusbar/car/CarNavigationBarController.java
@@ -0,0 +1,396 @@
+/*
+ * 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.car;
+
+import android.app.ActivityManager.StackId;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.support.v4.util.SimpleArrayMap;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+import android.view.View;
+import android.widget.LinearLayout;
+import com.android.systemui.R;
+
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A controller to populate data for CarNavigationBarView and handle user interactions.
+ *
+ * <p>Each button inside the navigation bar is defined by data in arrays_car.xml. OEMs can
+ * customize the navigation buttons by updating arrays_car.xml appropriately in an overlay.
+ */
+class CarNavigationBarController {
+    private static final String TAG = "CarNavBarController";
+
+    private static final String EXTRA_FACET_CATEGORIES = "categories";
+    private static final String EXTRA_FACET_PACKAGES = "packages";
+    private static final String EXTRA_FACET_ID = "filter_id";
+    private static final String EXTRA_FACET_LAUNCH_PICKER = "launch_picker";
+
+    /**
+     * Each facet of the navigation bar maps to a set of package names or categories defined in
+     * arrays_car.xml. Package names for a given facet are delimited by ";".
+     */
+    private static final String FACET_FILTER_DELIMITER = ";";
+
+    private final Context mContext;
+    private final CarNavigationBarView mNavBar;
+    private final CarStatusBar mStatusBar;
+
+    /**
+     * Set of categories each facet will filter on.
+     */
+    private final List<String[]> mFacetCategories = new ArrayList<>();
+
+    /**
+     * Set of package names each facet will filter on.
+     */
+    private final List<String[]> mFacetPackages = new ArrayList<>();
+
+    private final SimpleArrayMap<String, Integer> mFacetCategoryMap = new SimpleArrayMap<>();
+    private final SimpleArrayMap<String, Integer> mFacetPackageMap = new SimpleArrayMap<>();
+
+    private final List<CarNavigationButton> mNavButtons = new ArrayList<>();
+
+    private final SparseBooleanArray mFacetHasMultipleAppsCache = new SparseBooleanArray();
+
+    private int mCurrentFacetIndex;
+    private Intent mPersistentTaskIntent;
+
+    public CarNavigationBarController(Context context, CarNavigationBarView navBar,
+            CarStatusBar activityStarter) {
+        mContext = context;
+        mNavBar = navBar;
+        mStatusBar = activityStarter;
+        bind();
+
+        if (context.getResources().getBoolean(R.bool.config_enablePersistentDockedActivity)) {
+            setupPersistentDockedTask();
+        }
+    }
+
+    private void setupPersistentDockedTask() {
+        try {
+            mPersistentTaskIntent = Intent.parseUri(
+                    mContext.getString(R.string.config_persistentDockedActivityIntentUri),
+                    Intent.URI_INTENT_SCHEME);
+        } catch (URISyntaxException e) {
+            Log.e(TAG, "Malformed persistent task intent.");
+        }
+    }
+
+    public void taskChanged(String packageName, int stackId) {
+        // If the package name belongs to a filter, then highlight appropriate button in
+        // the navigation bar.
+        if (mFacetPackageMap.containsKey(packageName)) {
+            setCurrentFacet(mFacetPackageMap.get(packageName));
+        }
+
+        // Check if the package matches any of the categories for the facets
+        String category = getPackageCategory(packageName);
+        if (category != null) {
+            setCurrentFacet(mFacetCategoryMap.get(category));
+        }
+
+        // Set up the persistent docked task if needed.
+        if (mPersistentTaskIntent != null && !mStatusBar.hasDockedTask()
+                && stackId != StackId.HOME_STACK_ID) {
+            mStatusBar.startActivityOnStack(mPersistentTaskIntent, StackId.DOCKED_STACK_ID);
+        }
+    }
+
+    public void onPackageChange(String packageName) {
+        if (mFacetPackageMap.containsKey(packageName)) {
+            int index = mFacetPackageMap.get(packageName);
+            mFacetHasMultipleAppsCache.put(index, facetHasMultiplePackages(index));
+            // No need to check categories because we've already refreshed the cache.
+            return;
+        }
+
+        String category = getPackageCategory(packageName);
+        if (mFacetCategoryMap.containsKey(category)) {
+            int index = mFacetCategoryMap.get(category);
+            mFacetHasMultipleAppsCache.put(index, facetHasMultiplePackages(index));
+        }
+    }
+
+    /**
+     * Iterates through the items in arrays_car.xml and sets up the facet bar buttons to
+     * perform the task in that configuration file when clicked or long-pressed.
+     */
+    private void bind() {
+        Resources res = mContext.getResources();
+
+        TypedArray icons = res.obtainTypedArray(R.array.car_facet_icons);
+        TypedArray intents = res.obtainTypedArray(R.array.car_facet_intent_uris);
+        TypedArray longPressIntents = res.obtainTypedArray(R.array.car_facet_longpress_intent_uris);
+        TypedArray facetPackageNames = res.obtainTypedArray(R.array.car_facet_package_filters);
+        TypedArray facetCategories = res.obtainTypedArray(R.array.car_facet_category_filters);
+
+        try {
+            if (icons.length() != intents.length()
+                    || icons.length() != longPressIntents.length()
+                    || icons.length() != facetPackageNames.length()
+                    || icons.length() != facetCategories.length()) {
+                throw new RuntimeException("car_facet array lengths do not match");
+            }
+
+            for (int i = 0, size = icons.length(); i < size; i++) {
+                Drawable icon = icons.getDrawable(i);
+                CarNavigationButton button = createNavButton(icon);
+                initClickListeners(button, i, intents.getString(i), longPressIntents.getString(i));
+
+                mNavButtons.add(button);
+                mNavBar.addButton(button, createNavButton(icon) /* lightsOutButton */);
+
+                initFacetFilterMaps(i, facetPackageNames.getString(i).split(FACET_FILTER_DELIMITER),
+                        facetCategories.getString(i).split(FACET_FILTER_DELIMITER));
+                mFacetHasMultipleAppsCache.put(i, facetHasMultiplePackages(i));
+            }
+        } finally {
+            // Clean up all the TypedArrays.
+            icons.recycle();
+            intents.recycle();
+            longPressIntents.recycle();
+            facetPackageNames.recycle();
+            facetCategories.recycle();
+        }
+    }
+
+    /**
+     * Recreates each of the buttons on a density or font scale change. This manual process is
+     * necessary since this class is not part of an activity that automatically gets recreated.
+     */
+    public void onDensityOrFontScaleChanged() {
+        TypedArray icons = mContext.getResources().obtainTypedArray(R.array.car_facet_icons);
+
+        try {
+            int length = icons.length();
+            if (length != mNavButtons.size()) {
+                // This should not happen since the mNavButtons list is created from the length
+                // of the icons array in bind().
+                throw new RuntimeException("car_facet array lengths do not match number of "
+                        + "created buttons.");
+            }
+
+            for (int i = 0; i < length; i++) {
+                Drawable icon = icons.getDrawable(i);
+
+                // Setting a new icon will trigger a requestLayout() call if necessary.
+                mNavButtons.get(i).setResources(icon);
+            }
+        } finally {
+            icons.recycle();
+        }
+    }
+
+    private void initFacetFilterMaps(int id, String[] packageNames, String[] categories) {
+        mFacetCategories.add(categories);
+        for (String category : categories) {
+            mFacetCategoryMap.put(category, id);
+        }
+
+        mFacetPackages.add(packageNames);
+        for (String packageName : packageNames) {
+            mFacetPackageMap.put(packageName, id);
+        }
+    }
+
+    private String getPackageCategory(String packageName) {
+        PackageManager pm = mContext.getPackageManager();
+        int size = mFacetCategories.size();
+        // For each facet, check if the given package name matches one of its categories
+        for (int i = 0; i < size; i++) {
+            String[] categories = mFacetCategories.get(i);
+            for (int j = 0; j < categories.length; j++) {
+                String category = categories[j];
+                Intent intent = new Intent();
+                intent.setPackage(packageName);
+                intent.setAction(Intent.ACTION_MAIN);
+                intent.addCategory(category);
+                List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
+                if (list.size() > 0) {
+                    // Cache this package name into facetPackageMap, so we won't have to query
+                    // all categories next time this package name shows up.
+                    mFacetPackageMap.put(packageName, mFacetCategoryMap.get(category));
+                    return category;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Helper method to check if a given facet has multiple packages associated with it. This can
+     * be resource defined package names or package names filtered by facet category.
+     *
+     * @return {@code true} if the facet at the given index has more than one package.
+     */
+    private boolean facetHasMultiplePackages(int index) {
+        PackageManager pm = mContext.getPackageManager();
+
+        // Check if the packages defined for the filter actually exists on the device
+        String[] packages = mFacetPackages.get(index);
+        if (packages.length > 1) {
+            int count = 0;
+            for (int i = 0; i < packages.length; i++) {
+                count += pm.getLaunchIntentForPackage(packages[i]) != null ? 1 : 0;
+                if (count > 1) {
+                    return true;
+                }
+            }
+        }
+
+        // If there weren't multiple packages defined for the facet, check the categories
+        // and see if they resolve to multiple package names
+        String categories[] = mFacetCategories.get(index);
+
+        int count = 0;
+        for (int i = 0; i < categories.length; i++) {
+            String category = categories[i];
+            Intent intent = new Intent();
+            intent.setAction(Intent.ACTION_MAIN);
+            intent.addCategory(category);
+            count += pm.queryIntentActivities(intent, 0).size();
+            if (count > 1) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Sets the facet at the given index to be the facet that is currently active. The button will
+     * be highlighted appropriately.
+     */
+    private void setCurrentFacet(int index) {
+        if (index == mCurrentFacetIndex) {
+            return;
+        }
+
+        if (mNavButtons.get(mCurrentFacetIndex) != null) {
+            mNavButtons.get(mCurrentFacetIndex)
+                    .setSelected(false /* selected */, false /* showMoreIcon */);
+        }
+
+        if (mNavButtons.get(index) != null) {
+            mNavButtons.get(index).setSelected(true /* selected */,
+                    mFacetHasMultipleAppsCache.get(index)  /* showMoreIcon */);
+        }
+
+        mCurrentFacetIndex = index;
+    }
+
+    /**
+     * Creates the View that is used for the buttons along the navigation bar.
+     *
+     * @param icon The icon to be used for the button.
+     */
+    private CarNavigationButton createNavButton(Drawable icon) {
+        CarNavigationButton button = (CarNavigationButton) View.inflate(mContext,
+                R.layout.car_navigation_button, null);
+        button.setResources(icon);
+        LinearLayout.LayoutParams lp =
+                new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1);
+        button.setLayoutParams(lp);
+
+        return button;
+    }
+
+    /**
+     * Initializes the click and long click listeners that correspond to the given command string.
+     * The click listeners are attached to the given button.
+     */
+    private void initClickListeners(View button, int index, String clickString,
+            String longPressString) {
+        // Each button at least have an action when pressed.
+        if (TextUtils.isEmpty(clickString)) {
+            throw new RuntimeException("Facet at index " + index + " does not have click action.");
+        }
+
+        try {
+            Intent intent = Intent.parseUri(clickString, Intent.URI_INTENT_SCHEME);
+            button.setOnClickListener(v -> onFacetClicked(intent, index));
+        } catch (URISyntaxException e) {
+            throw new RuntimeException("Malformed intent uri", e);
+        }
+
+        if (TextUtils.isEmpty(longPressString)) {
+            button.setLongClickable(false);
+            return;
+        }
+
+        try {
+            Intent intent = Intent.parseUri(longPressString, Intent.URI_INTENT_SCHEME);
+            button.setOnLongClickListener(v -> {
+                onFacetLongClicked(intent, index);
+                return true;
+            });
+        } catch (URISyntaxException e) {
+            throw new RuntimeException("Malformed long-press intent uri", e);
+        }
+    }
+
+    /**
+     * Handles a click on a facet. A click will trigger the given Intent.
+     *
+     * @param index The index of the facet that was clicked.
+     */
+    private void onFacetClicked(Intent intent, int index) {
+        String packageName = intent.getPackage();
+
+        if (packageName == null) {
+            return;
+        }
+
+        intent.putExtra(EXTRA_FACET_CATEGORIES, mFacetCategories.get(index));
+        intent.putExtra(EXTRA_FACET_PACKAGES, mFacetPackages.get(index));
+        // The facet is identified by the index in which it was added to the nav bar.
+        // This value can be used to determine which facet was selected
+        intent.putExtra(EXTRA_FACET_ID, Integer.toString(index));
+
+        // If the current facet is clicked, we want to launch the picker by default
+        // rather than the "preferred/last run" app.
+        intent.putExtra(EXTRA_FACET_LAUNCH_PICKER, index == mCurrentFacetIndex);
+
+        int stackId = StackId.FULLSCREEN_WORKSPACE_STACK_ID;
+        if (intent.getCategories().contains(Intent.CATEGORY_HOME)) {
+            stackId = StackId.HOME_STACK_ID;
+        }
+
+        setCurrentFacet(index);
+        mStatusBar.startActivityOnStack(intent, stackId);
+    }
+
+    /**
+     * Handles a long-press on a facet. The long-press will trigger the given Intent.
+     *
+     * @param index The index of the facet that was clicked.
+     */
+    private void onFacetLongClicked(Intent intent, int index) {
+        setCurrentFacet(index);
+        mStatusBar.startActivityOnStack(intent, StackId.FULLSCREEN_WORKSPACE_STACK_ID);
+    }
+}
diff --git a/com/android/systemui/statusbar/car/CarNavigationBarView.java b/com/android/systemui/statusbar/car/CarNavigationBarView.java
new file mode 100644
index 0000000..6cbbd6c
--- /dev/null
+++ b/com/android/systemui/statusbar/car/CarNavigationBarView.java
@@ -0,0 +1,75 @@
+/*
+ * 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.car;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.NavigationBarView;
+
+/**
+ * A custom navigation bar for the automotive use case.
+ * <p>
+ * The navigation bar in the automotive use case is more like a list of shortcuts, rendered
+ * in a linear layout.
+ */
+class CarNavigationBarView extends NavigationBarView {
+    private LinearLayout mNavButtons;
+    private LinearLayout mLightsOutButtons;
+
+    public CarNavigationBarView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public void onFinishInflate() {
+        mNavButtons = findViewById(R.id.nav_buttons);
+        mLightsOutButtons = findViewById(R.id.lights_out);
+    }
+
+    public void addButton(CarNavigationButton button, CarNavigationButton lightsOutButton){
+        mNavButtons.addView(button);
+        mLightsOutButtons.addView(lightsOutButton);
+    }
+
+    @Override
+    public void setDisabledFlags(int disabledFlags, boolean force) {
+        // TODO: Populate.
+    }
+
+    @Override
+    public void reorient() {
+        // We expect the car head unit to always have a fixed rotation so we ignore this. The super
+        // class implentation expects mRotatedViews to be populated, so if you call into it, there
+        // is a possibility of a NullPointerException.
+    }
+
+    @Override
+    public View getCurrentView() {
+        return this;
+    }
+
+    @Override
+    public void setNavigationIconHints(int hints, boolean force) {
+        // We do not need to set the navigation icon hints for a vehicle
+        // Calling setNavigationIconHints in the base class will result in a NPE as the car
+        // navigation bar does not have a back button.
+    }
+}
diff --git a/com/android/systemui/statusbar/car/CarNavigationButton.java b/com/android/systemui/statusbar/car/CarNavigationButton.java
new file mode 100644
index 0000000..2de358f
--- /dev/null
+++ b/com/android/systemui/statusbar/car/CarNavigationButton.java
@@ -0,0 +1,72 @@
+/*
+ * 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.car;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+
+import com.android.keyguard.AlphaOptimizedImageButton;
+import com.android.systemui.R;
+
+/**
+ * A wrapper view for a car navigation facet, which includes a button icon and a drop down icon.
+ */
+public class CarNavigationButton extends RelativeLayout {
+    private static final float SELECTED_ALPHA = 1;
+    private static final float UNSELECTED_ALPHA = 0.7f;
+
+    private AlphaOptimizedImageButton mIcon;
+    private AlphaOptimizedImageButton mMoreIcon;
+
+    public CarNavigationButton(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public void onFinishInflate() {
+        super.onFinishInflate();
+        mIcon = findViewById(R.id.car_nav_button_icon);
+        mIcon.setScaleType(ImageView.ScaleType.CENTER);
+        mIcon.setClickable(false);
+        mIcon.setBackgroundColor(android.R.color.transparent);
+        mIcon.setAlpha(UNSELECTED_ALPHA);
+
+        mMoreIcon = findViewById(R.id.car_nav_button_more_icon);
+        mMoreIcon.setClickable(false);
+        mMoreIcon.setBackgroundColor(android.R.color.transparent);
+        mMoreIcon.setVisibility(INVISIBLE);
+        mMoreIcon.setImageDrawable(getContext().getDrawable(R.drawable.car_ic_arrow));
+        mMoreIcon.setAlpha(UNSELECTED_ALPHA);
+    }
+
+    public void setResources(Drawable icon) {
+        mIcon.setImageDrawable(icon);
+    }
+
+    public void setSelected(boolean selected, boolean showMoreIcon) {
+        if (selected) {
+            mMoreIcon.setVisibility(showMoreIcon ? VISIBLE : INVISIBLE);
+            mMoreIcon.setAlpha(SELECTED_ALPHA);
+            mIcon.setAlpha(SELECTED_ALPHA);
+        } else {
+            mMoreIcon.setVisibility(INVISIBLE);
+            mIcon.setAlpha(UNSELECTED_ALPHA);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/car/CarStatusBar.java b/com/android/systemui/statusbar/car/CarStatusBar.java
new file mode 100644
index 0000000..680f693
--- /dev/null
+++ b/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -0,0 +1,429 @@
+/*
+ * 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.car;
+
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewStub;
+import android.view.WindowManager;
+import android.widget.LinearLayout;
+
+import com.android.systemui.BatteryMeterView;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.SwipeHelper;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.recents.Recents;
+import com.android.systemui.recents.misc.SystemServicesProxy;
+import com.android.systemui.recents.misc.SystemServicesProxy.TaskStackListener;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
+import com.android.systemui.statusbar.phone.NavigationBarView;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.classifier.FalsingLog;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.Prefs;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Map;
+/**
+ * A status bar (and navigation bar) tailored for the automotive use case.
+ */
+public class CarStatusBar extends StatusBar implements
+        CarBatteryController.BatteryViewHandler {
+    private static final String TAG = "CarStatusBar";
+
+    private TaskStackListenerImpl mTaskStackListener;
+
+    private CarNavigationBarController mController;
+    private FullscreenUserSwitcher mFullscreenUserSwitcher;
+
+    private CarBatteryController mCarBatteryController;
+    private BatteryMeterView mBatteryMeterView;
+    private Drawable mNotificationPanelBackground;
+
+    private ConnectedDeviceSignalController mConnectedDeviceSignalController;
+    private ViewGroup mNavigationBarWindow;
+    private CarNavigationBarView mNavigationBarView;
+
+    private final Object mQueueLock = new Object();
+    @Override
+    public void start() {
+        super.start();
+        mTaskStackListener = new TaskStackListenerImpl();
+        SystemServicesProxy.getInstance(mContext).registerTaskStackListener(mTaskStackListener);
+        registerPackageChangeReceivers();
+
+        mStackScroller.setScrollingEnabled(true);
+
+        createBatteryController();
+        mCarBatteryController.startListening();
+    }
+
+    @Override
+    public void destroy() {
+        mCarBatteryController.stopListening();
+        mConnectedDeviceSignalController.stopListening();
+
+        if (mNavigationBarWindow != null) {
+            mWindowManager.removeViewImmediate(mNavigationBarWindow);
+            mNavigationBarView = null;
+        }
+
+        super.destroy();
+    }
+
+    @Override
+    protected void makeStatusBarView() {
+        super.makeStatusBarView();
+
+        mNotificationPanelBackground = getDefaultWallpaper();
+        mScrimController.setScrimBehindDrawable(mNotificationPanelBackground);
+
+        FragmentHostManager manager = FragmentHostManager.get(mStatusBarWindow);
+        manager.addTagListener(CollapsedStatusBarFragment.TAG, (tag, fragment) -> {
+            mBatteryMeterView = fragment.getView().findViewById(R.id.battery);
+
+            // By default, the BatteryMeterView should not be visible. It will be toggled
+            // when a device has connected by bluetooth.
+            mBatteryMeterView.setVisibility(View.GONE);
+
+            ViewStub stub = fragment.getView().findViewById(R.id.connected_device_signals_stub);
+            View signalsView = stub.inflate();
+
+            // When a ViewStub if inflated, it does not respect the margins on the
+            // inflated view.
+            // As a result, manually add the ending margin.
+            ((LinearLayout.LayoutParams) signalsView.getLayoutParams()).setMarginEnd(
+                    mContext.getResources().getDimensionPixelOffset(
+                            R.dimen.status_bar_connected_device_signal_margin_end));
+
+            if (mConnectedDeviceSignalController != null) {
+                mConnectedDeviceSignalController.stopListening();
+            }
+            mConnectedDeviceSignalController = new ConnectedDeviceSignalController(mContext,
+                    signalsView);
+            mConnectedDeviceSignalController.startListening();
+
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "makeStatusBarView(). mBatteryMeterView: " + mBatteryMeterView);
+            }
+        });
+    }
+
+    private BatteryController createBatteryController() {
+        mCarBatteryController = new CarBatteryController(mContext);
+        mCarBatteryController.addBatteryViewHandler(this);
+        return mCarBatteryController;
+    }
+
+    @Override
+    protected void createNavigationBar() {
+        if (mNavigationBarView != null) {
+            return;
+        }
+
+        // SystemUI requires that the navigation bar view have a parent. Since the regular
+        // StatusBar inflates navigation_bar_window as this parent view, use the same view for the
+        // CarNavigationBarView.
+        mNavigationBarWindow = (ViewGroup) View.inflate(mContext,
+                R.layout.navigation_bar_window, null);
+        if (mNavigationBarWindow == null) {
+            Log.e(TAG, "CarStatusBar failed inflate for R.layout.navigation_bar_window");
+        }
+
+
+        View.inflate(mContext, R.layout.car_navigation_bar, mNavigationBarWindow);
+        mNavigationBarView = (CarNavigationBarView) mNavigationBarWindow.getChildAt(0);
+        if (mNavigationBarView == null) {
+            Log.e(TAG, "CarStatusBar failed inflate for R.layout.car_navigation_bar");
+        }
+
+
+        mController = new CarNavigationBarController(mContext, mNavigationBarView,
+                this /* ActivityStarter*/);
+        mNavigationBarView.getBarTransitions().setAlwaysOpaque(true);
+        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
+                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
+                WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
+                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                        | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
+                PixelFormat.TRANSLUCENT);
+        lp.setTitle("CarNavigationBar");
+        lp.windowAnimations = 0;
+
+        mWindowManager.addView(mNavigationBarWindow, lp);
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        //When executing dump() funciton simultaneously, we need to serialize them
+        //to get mStackScroller's position correctly.
+        synchronized (mQueueLock) {
+            pw.println("  mStackScroller: " + viewInfo(mStackScroller));
+            pw.println("  mStackScroller: " + viewInfo(mStackScroller)
+                    + " scroll " + mStackScroller.getScrollX()
+                    + "," + mStackScroller.getScrollY());
+        }
+
+        pw.print("  mTaskStackListener="); pw.println(mTaskStackListener);
+        pw.print("  mController=");
+        pw.println(mController);
+        pw.print("  mFullscreenUserSwitcher="); pw.println(mFullscreenUserSwitcher);
+        pw.print("  mCarBatteryController=");
+        pw.println(mCarBatteryController);
+        pw.print("  mBatteryMeterView=");
+        pw.println(mBatteryMeterView);
+        pw.print("  mConnectedDeviceSignalController=");
+        pw.println(mConnectedDeviceSignalController);
+        pw.print("  mNavigationBarView=");
+        pw.println(mNavigationBarView);
+
+        if (KeyguardUpdateMonitor.getInstance(mContext) != null) {
+            KeyguardUpdateMonitor.getInstance(mContext).dump(fd, pw, args);
+        }
+
+        FalsingManager.getInstance(mContext).dump(pw);
+        FalsingLog.dump(pw);
+
+        pw.println("SharedPreferences:");
+        for (Map.Entry<String, ?> entry : Prefs.getAll(mContext).entrySet()) {
+            pw.print("  "); pw.print(entry.getKey()); pw.print("="); pw.println(entry.getValue());
+        }
+    }
+
+    @Override
+    public NavigationBarView getNavigationBarView() {
+        return mNavigationBarView;
+    }
+
+    @Override
+    public View getNavigationBarWindow() {
+        return mNavigationBarWindow;
+    }
+
+    @Override
+    protected View.OnTouchListener getStatusBarWindowTouchListener() {
+        // Usually, a touch on the background window will dismiss the notification shade. However,
+        // for the car use-case, the shade should remain unless the user switches to a different
+        // facet (e.g. phone).
+        return null;
+    }
+
+    /**
+     * Returns the {@link com.android.systemui.SwipeHelper.LongPressListener} that will be
+     * triggered when a notification card is long-pressed.
+     */
+    @Override
+    protected SwipeHelper.LongPressListener getNotificationLongClicker() {
+        // For the automative use case, we do not want to the user to be able to interact with
+        // a notification other than a regular click. As a result, just return null for the
+        // long click listener.
+        return null;
+    }
+
+    @Override
+    public void showBatteryView() {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "showBatteryView(). mBatteryMeterView: " + mBatteryMeterView);
+        }
+
+        if (mBatteryMeterView != null) {
+            mBatteryMeterView.setVisibility(View.VISIBLE);
+        }
+    }
+
+    @Override
+    public void hideBatteryView() {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "hideBatteryView(). mBatteryMeterView: " + mBatteryMeterView);
+        }
+
+        if (mBatteryMeterView != null) {
+            mBatteryMeterView.setVisibility(View.GONE);
+        }
+    }
+
+    private BroadcastReceiver mPackageChangeReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.getData() == null || mController == null) {
+                return;
+            }
+            String packageName = intent.getData().getSchemeSpecificPart();
+            mController.onPackageChange(packageName);
+        }
+    };
+
+    private void registerPackageChangeReceivers() {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        filter.addDataScheme("package");
+        mContext.registerReceiver(mPackageChangeReceiver, filter);
+    }
+
+    public boolean hasDockedTask() {
+        return Recents.getSystemServices().hasDockedTask();
+    }
+
+    /**
+     * An implementation of TaskStackListener, that listens for changes in the system task
+     * stack and notifies the navigation bar.
+     */
+    private class TaskStackListenerImpl extends TaskStackListener {
+        @Override
+        public void onTaskStackChanged() {
+            SystemServicesProxy ssp = Recents.getSystemServices();
+            ActivityManager.RunningTaskInfo runningTaskInfo = ssp.getRunningTask();
+            if (runningTaskInfo != null && runningTaskInfo.baseActivity != null) {
+                mController.taskChanged(runningTaskInfo.baseActivity.getPackageName(),
+                        runningTaskInfo.stackId);
+            }
+        }
+    }
+
+    @Override
+    protected void createUserSwitcher() {
+        UserSwitcherController userSwitcherController =
+                Dependency.get(UserSwitcherController.class);
+        if (userSwitcherController.useFullscreenUserSwitcher()) {
+            mFullscreenUserSwitcher = new FullscreenUserSwitcher(this,
+                    userSwitcherController,
+                    mStatusBarWindow.findViewById(R.id.fullscreen_user_switcher_stub));
+        } else {
+            super.createUserSwitcher();
+        }
+    }
+
+    @Override
+    public void userSwitched(int newUserId) {
+        super.userSwitched(newUserId);
+        if (mFullscreenUserSwitcher != null) {
+            mFullscreenUserSwitcher.onUserSwitched(newUserId);
+        }
+    }
+
+    @Override
+    public void updateKeyguardState(boolean goingToFullShade, boolean fromShadeLocked) {
+        super.updateKeyguardState(goingToFullShade, fromShadeLocked);
+        if (mFullscreenUserSwitcher != null) {
+            if (mState == StatusBarState.FULLSCREEN_USER_SWITCHER) {
+                mFullscreenUserSwitcher.show();
+            } else {
+                mFullscreenUserSwitcher.hide();
+            }
+        }
+    }
+
+    @Override
+    public void updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation) {
+        // Do nothing, we don't want to display media art in the lock screen for a car.
+    }
+
+    private int startActivityWithOptions(Intent intent, Bundle options) {
+        int result = ActivityManager.START_CANCELED;
+        try {
+            result = ActivityManager.getService().startActivityAsUser(null /* caller */,
+                    mContext.getBasePackageName(),
+                    intent,
+                    intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+                    null /* resultTo*/,
+                    null /* resultWho*/,
+                    0 /* requestCode*/,
+                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP,
+                    null /* profilerInfo*/,
+                    options,
+                    UserHandle.CURRENT.getIdentifier());
+        } catch (RemoteException e) {
+            Log.w(TAG, "Unable to start activity", e);
+        }
+
+        return result;
+    }
+
+    public int startActivityOnStack(Intent intent, int stackId) {
+        ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchStackId(stackId);
+        return startActivityWithOptions(intent, options.toBundle());
+    }
+
+    @Override
+    protected boolean shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn) {
+        // Because space is usually constrained in the auto use-case, there should not be a
+        // pinned notification when the shade has been expanded. Ensure this by not pinning any
+        // notification if the shade is already opened.
+        if (mPanelExpanded) {
+            return false;
+        }
+
+        return super.shouldPeek(entry, sbn);
+    }
+
+    @Override
+    public void animateExpandNotificationsPanel() {
+        // Because space is usually constrained in the auto use-case, there should not be a
+        // pinned notification when the shade has been expanded. Ensure this by removing all heads-
+        // up notifications.
+        mHeadsUpManager.removeAllHeadsUpEntries();
+        super.animateExpandNotificationsPanel();
+    }
+
+    /**
+     * Ensures that relevant child views are appropriately recreated when the device's density
+     * changes.
+     */
+    @Override
+    public void onDensityOrFontScaleChanged() {
+        super.onDensityOrFontScaleChanged();
+        mController.onDensityOrFontScaleChanged();
+
+        // Need to update the background on density changed in case the change was due to night
+        // mode.
+        mNotificationPanelBackground = getDefaultWallpaper();
+        mScrimController.setScrimBehindDrawable(mNotificationPanelBackground);
+    }
+
+    /**
+     * Returns the {@link Drawable} that represents the wallpaper that the user has currently set.
+     */
+    private Drawable getDefaultWallpaper() {
+        return mContext.getDrawable(com.android.internal.R.drawable.default_wallpaper);
+    }
+}
diff --git a/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java b/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java
new file mode 100644
index 0000000..677fa81
--- /dev/null
+++ b/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java
@@ -0,0 +1,255 @@
+package com.android.systemui.statusbar.car;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadsetClient;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProfile.ServiceListener;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.telephony.SignalStrength;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.ImageView;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ScalingDrawableWrapper;
+import com.android.systemui.statusbar.phone.SignalDrawable;
+import com.android.systemui.statusbar.policy.BluetoothController;
+
+import static com.android.systemui.statusbar.phone.StatusBar.DEBUG;
+
+/**
+ * Controller that monitors signal strength for a device that is connected via bluetooth.
+ */
+public class ConnectedDeviceSignalController extends BroadcastReceiver implements
+        BluetoothController.Callback {
+    private final static String TAG = "DeviceSignalCtlr";
+
+    /**
+     * The value that indicates if a network is unavailable. This value is according ot the
+     * Bluetooth HFP 1.5 spec, which indicates this value is one of two: 0 or 1. These stand
+     * for network unavailable and available respectively.
+     */
+    private static final int NETWORK_UNAVAILABLE = 0;
+    private static final int NETWORK_UNAVAILABLE_ICON_ID = R.drawable.stat_sys_signal_null;
+
+    /**
+     * All possible signal strength icons. According to the Bluetooth HFP 1.5 specification,
+     * signal strength is indicated by a value from 1-5, where these values represent the following:
+     *
+     * <p>0%% - 0, 1-25%% - 1, 26-50%% - 2, 51-75%% - 3, 76-99%% - 4, 100%% - 5
+     *
+     * <p>As a result, these are treated as an index into this array for the corresponding icon.
+     * Note that the icon is the same for 0 and 1.
+     */
+    private static final int[] SIGNAL_STRENGTH_ICONS = {
+            0,
+            0,
+            1,
+            2,
+            3,
+            4,
+    };
+
+    private static final int INVALID_SIGNAL = -1;
+
+    private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
+    private final Context mContext;
+    private final BluetoothController mController;
+
+    private final View mSignalsView;
+    private final ImageView mNetworkSignalView;
+
+    private final float mIconScaleFactor;
+    private final SignalDrawable mSignalDrawable;
+
+    private BluetoothHeadsetClient mBluetoothHeadsetClient;
+
+    public ConnectedDeviceSignalController(Context context, View signalsView) {
+        mContext = context;
+        mController = Dependency.get(BluetoothController.class);
+
+        mSignalsView = signalsView;
+        mNetworkSignalView = (ImageView)
+                mSignalsView.findViewById(R.id.connected_device_network_signal);
+
+        TypedValue typedValue = new TypedValue();
+        context.getResources().getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
+        mIconScaleFactor = typedValue.getFloat();
+        mSignalDrawable = new SignalDrawable(mNetworkSignalView.getContext());
+        mNetworkSignalView.setImageDrawable(
+                new ScalingDrawableWrapper(mSignalDrawable, mIconScaleFactor));
+
+        if (mAdapter == null) {
+          return;
+        }
+
+        mAdapter.getProfileProxy(context.getApplicationContext(), mHfpServiceListener,
+                BluetoothProfile.HEADSET_CLIENT);
+    }
+
+    public void startListening() {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
+        filter.addAction(BluetoothHeadsetClient.ACTION_AG_EVENT);
+        mContext.registerReceiver(this, filter);
+
+        mController.addCallback(this);
+    }
+
+    public void stopListening() {
+        mContext.unregisterReceiver(this);
+        mController.removeCallback(this);
+    }
+
+    @Override
+    public void onBluetoothDevicesChanged() {
+        // Nothing to do here because this Controller is not displaying a list of possible
+        // bluetooth devices.
+    }
+
+    @Override
+    public void onBluetoothStateChange(boolean enabled) {
+        if (DEBUG) {
+            Log.d(TAG, "onBluetoothStateChange(). enabled: " + enabled);
+        }
+
+        // Only need to handle the case if bluetooth has been disabled, in which case the
+        // signal indicators are hidden. If bluetooth has been enabled, then this class should
+        // receive updates to the connection state via onReceive().
+        if (!enabled) {
+            mNetworkSignalView.setVisibility(View.GONE);
+            mSignalsView.setVisibility(View.GONE);
+        }
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+
+        if (DEBUG) {
+            Log.d(TAG, "onReceive(). action: " + action);
+        }
+
+        if (BluetoothHeadsetClient.ACTION_AG_EVENT.equals(action)) {
+            if (DEBUG) {
+                Log.d(TAG, "Received ACTION_AG_EVENT");
+            }
+
+            processActionAgEvent(intent);
+        } else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
+            int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+
+            if (DEBUG) {
+                int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
+                Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED event: "
+                        + oldState + " -> " + newState);
+            }
+            BluetoothDevice device =
+                    (BluetoothDevice) intent.getExtra(BluetoothDevice.EXTRA_DEVICE);
+            updateViewVisibility(device, newState);
+        }
+    }
+
+    /**
+     * Processes an {@link Intent} that had an action of
+     * {@link BluetoothHeadsetClient#ACTION_AG_EVENT}.
+     */
+    private void processActionAgEvent(Intent intent) {
+        int networkStatus = intent.getIntExtra(BluetoothHeadsetClient.EXTRA_NETWORK_STATUS,
+                INVALID_SIGNAL);
+        if (networkStatus != INVALID_SIGNAL) {
+            if (DEBUG) {
+                Log.d(TAG, "EXTRA_NETWORK_STATUS: " + " " + networkStatus);
+            }
+
+            if (networkStatus == NETWORK_UNAVAILABLE) {
+                setNetworkSignalIcon(NETWORK_UNAVAILABLE_ICON_ID);
+            }
+        }
+
+        int signalStrength = intent.getIntExtra(
+                BluetoothHeadsetClient.EXTRA_NETWORK_SIGNAL_STRENGTH, INVALID_SIGNAL);
+        if (signalStrength != INVALID_SIGNAL) {
+            if (DEBUG) {
+                Log.d(TAG, "EXTRA_NETWORK_SIGNAL_STRENGTH: " + signalStrength);
+            }
+
+            setNetworkSignalIcon(SIGNAL_STRENGTH_ICONS[signalStrength]);
+        }
+
+        int roamingStatus = intent.getIntExtra(BluetoothHeadsetClient.EXTRA_NETWORK_ROAMING,
+                INVALID_SIGNAL);
+        if (roamingStatus != INVALID_SIGNAL) {
+            if (DEBUG) {
+                Log.d(TAG, "EXTRA_NETWORK_ROAMING: " + roamingStatus);
+            }
+        }
+    }
+
+    private void setNetworkSignalIcon(int level) {
+        // Setting the icon on a child view of mSignalView, so toggle this container visible.
+        mSignalsView.setVisibility(View.VISIBLE);
+
+        mSignalDrawable.setLevel(SignalDrawable.getState(level,
+                SignalStrength.NUM_SIGNAL_STRENGTH_BINS, false));
+        mNetworkSignalView.setVisibility(View.VISIBLE);
+    }
+
+    private void updateViewVisibility(BluetoothDevice device, int newState) {
+        if (newState == BluetoothProfile.STATE_CONNECTED) {
+            if (DEBUG) {
+                Log.d(TAG, "Device connected");
+            }
+
+            if (mBluetoothHeadsetClient == null || device == null) {
+                return;
+            }
+
+            // Check if battery information is available and immediately update.
+            Bundle featuresBundle = mBluetoothHeadsetClient.getCurrentAgEvents(device);
+            if (featuresBundle == null) {
+                return;
+            }
+
+            int signalStrength = featuresBundle.getInt(
+                    BluetoothHeadsetClient.EXTRA_NETWORK_SIGNAL_STRENGTH, INVALID_SIGNAL);
+            if (signalStrength != INVALID_SIGNAL) {
+                if (DEBUG) {
+                    Log.d(TAG, "EXTRA_NETWORK_SIGNAL_STRENGTH: " + signalStrength);
+                }
+
+                setNetworkSignalIcon(SIGNAL_STRENGTH_ICONS[signalStrength]);
+            }
+        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+            if (DEBUG) {
+                Log.d(TAG, "Device disconnected");
+            }
+
+            mNetworkSignalView.setVisibility(View.GONE);
+            mSignalsView.setVisibility(View.GONE);
+        }
+    }
+
+    private final ServiceListener mHfpServiceListener = new ServiceListener() {
+        @Override
+        public void onServiceConnected(int profile, BluetoothProfile proxy) {
+            if (profile == BluetoothProfile.HEADSET_CLIENT) {
+                mBluetoothHeadsetClient = (BluetoothHeadsetClient) proxy;
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(int profile) {
+            if (profile == BluetoothProfile.HEADSET_CLIENT) {
+                mBluetoothHeadsetClient = null;
+            }
+        }
+    };
+}
diff --git a/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java b/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
new file mode 100644
index 0000000..172c62a
--- /dev/null
+++ b/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
@@ -0,0 +1,173 @@
+/*
+ * 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.car;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.res.Resources;
+import android.os.CountDownTimer;
+import android.view.View;
+import android.view.ViewStub;
+import android.widget.ProgressBar;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+
+/**
+ * Manages the fullscreen user switcher.
+ */
+public class FullscreenUserSwitcher {
+    private final View mContainer;
+    private final View mParent;
+    private final UserGridView mUserGridView;
+    private final UserSwitcherController mUserSwitcherController;
+    private final ProgressBar mProgressBar;
+    private final ProgressBar mSwitchingUsers;
+    private final int mLoginTimeoutMs;
+    private final int mAnimUpdateIntervalMs;
+    private final int mShortAnimDuration;
+
+    private boolean mShowing;
+
+    private CountDownTimer mTimer;
+
+    public FullscreenUserSwitcher(StatusBar statusBar,
+            UserSwitcherController userSwitcherController,
+            ViewStub containerStub) {
+        mUserSwitcherController = userSwitcherController;
+        mParent = containerStub.inflate();
+        mContainer = mParent.findViewById(R.id.container);
+        mUserGridView = mContainer.findViewById(R.id.user_grid);
+        mUserGridView.init(statusBar, mUserSwitcherController, true /* showInitially */);
+        mUserGridView.setUserSelectionListener(record -> {
+            if (!record.isCurrent) {
+                toggleSwitchInProgress(true);
+            }
+        });
+
+        PageIndicator pageIndicator = mContainer.findViewById(R.id.user_switcher_page_indicator);
+        pageIndicator.setupWithViewPager(mUserGridView);
+
+        mProgressBar = mContainer.findViewById(R.id.countdown_progress);
+        Resources res = mContainer.getResources();
+        mLoginTimeoutMs = res.getInteger(R.integer.car_user_switcher_timeout_ms);
+        mAnimUpdateIntervalMs = res.getInteger(R.integer.car_user_switcher_anim_update_ms);
+        mShortAnimDuration = res.getInteger(android.R.integer.config_shortAnimTime);
+
+        mContainer.findViewById(R.id.start_driving).setOnClickListener(v -> {
+            cancelTimer();
+            automaticallySelectUser();
+        });
+
+        // Any interaction with the screen should cancel the timer.
+        mContainer.setOnClickListener(v -> {
+            cancelTimer();
+        });
+        mUserGridView.setOnTouchListener((v, e) -> {
+            cancelTimer();
+            return false;
+        });
+
+        mSwitchingUsers = mParent.findViewById(R.id.switching_users);
+    }
+
+    public void onUserSwitched(int newUserId) {
+        mUserGridView.onUserSwitched(newUserId);
+    }
+
+    private void toggleSwitchInProgress(boolean inProgress) {
+        if (inProgress) {
+            crossFade(mSwitchingUsers, mContainer);
+        } else {
+            crossFade(mContainer, mSwitchingUsers);
+        }
+    }
+
+    private void crossFade(View incoming, View outgoing) {
+        incoming.animate()
+            .alpha(1.0f)
+            .setDuration(mShortAnimDuration)
+            .setListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(Animator animator) {
+                    incoming.setAlpha(0.0f);
+                    incoming.setVisibility(View.VISIBLE);
+                }
+            });
+
+        outgoing.animate()
+            .alpha(0.0f)
+            .setDuration(mShortAnimDuration)
+            .setListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    outgoing.setVisibility(View.GONE);
+                }
+            });
+    }
+
+    public void show() {
+        if (mShowing) {
+            return;
+        }
+        mShowing = true;
+        mParent.setVisibility(View.VISIBLE);
+        cancelTimer();
+
+        // This would be the case if we were in the middle of a switch.
+        if (mProgressBar.getVisibility() != View.VISIBLE) {
+            return;
+        }
+
+        mTimer = new CountDownTimer(mLoginTimeoutMs, mAnimUpdateIntervalMs) {
+            @Override
+            public void onTick(long msUntilFinished) {
+                int elapsed = mLoginTimeoutMs - (int) msUntilFinished;
+                mProgressBar.setProgress((int) elapsed, true /* animate */);
+            }
+
+            @Override
+            public void onFinish() {
+                mProgressBar.setProgress(mLoginTimeoutMs, true /* animate */);
+                automaticallySelectUser();
+            }
+        };
+        mTimer.start();
+    }
+
+    public void hide() {
+        mShowing = false;
+        cancelTimer();
+        toggleSwitchInProgress(false);
+        mParent.setVisibility(View.GONE);
+    }
+
+    private void cancelTimer() {
+        if (mTimer != null) {
+            mTimer.cancel();
+            mTimer = null;
+            mProgressBar.setProgress(0, true /* animate */);
+        }
+    }
+
+    private void automaticallySelectUser() {
+        // TODO: Switch according to some policy. This implementation just tries to drop the
+        //       keyguard for the current user.
+        mUserGridView.showOfflineAuthUi();
+    }
+}
diff --git a/com/android/systemui/statusbar/car/PageIndicator.java b/com/android/systemui/statusbar/car/PageIndicator.java
new file mode 100644
index 0000000..c830ff8
--- /dev/null
+++ b/com/android/systemui/statusbar/car/PageIndicator.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2017 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.car;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+import com.android.systemui.R;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Displays the dots underneath the ViewPager on the lock screen. This is really just a simplified
+ * version of PagerTitleStrip. We don't inherit from there because it's impossible to bypass some
+ * of the overriden logic in that class.
+ */
+public class PageIndicator extends View {
+    private static final String TAG = "PageIndicator";
+    // These can be made a styleable attribute in the future if necessary.
+    private static final int SELECTED_COLOR = 0xFFF5F5F5;  // grey 100
+    private static final int UNSELECTED_COLOR = 0xFFBDBDBD;  // grey 400
+    private final PageListener mPageListener = new PageListener();
+
+    private ViewPager mPager;
+    private WeakReference<PagerAdapter> mWatchingAdapter;
+
+    private int mPageCount;
+    private int mCurrentPosition;
+    private Paint mPaint;
+    private int mRadius;
+    private int mStep;
+
+    public PageIndicator(Context context) {
+        super(context);
+        init();
+    }
+
+    public PageIndicator(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    private void init() {
+        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mPaint.setStyle(Paint.Style.FILL);
+        mRadius = getResources().getDimensionPixelSize(R.dimen.car_page_indicator_dot_diameter) / 2;
+        mStep = mRadius * 3;
+    }
+
+    public void setupWithViewPager(ViewPager pager) {
+        mPager = pager;
+
+        final PagerAdapter adapter = (PagerAdapter) pager.getAdapter();
+        pager.addOnPageChangeListener(mPageListener);
+        pager.addOnAdapterChangeListener(mPageListener);
+        updateAdapter(mWatchingAdapter != null ? mWatchingAdapter.get() : null, adapter);
+        invalidate();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (mPager != null) {
+            updateAdapter(mPager.getAdapter(), null);
+            mPager.removeOnPageChangeListener(mPageListener);
+            mPager.removeOnAdapterChangeListener(mPageListener);
+            mPager = null;
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        // Don't draw anything unless there's multiple pages to scroll through.  No need to clear
+        // any previous dots, since onDraw provides a canvas that's already cleared.
+        if (mPageCount <= 1)
+            return;
+
+        int x = canvas.getWidth() / 2 - (mPageCount / 2) * mStep;
+        int y = canvas.getHeight() / 2;
+
+        for (int i = 0; i < mPageCount; i++) {
+            if (i == mCurrentPosition) {
+                mPaint.setColor(SELECTED_COLOR);
+            } else {
+                mPaint.setColor(UNSELECTED_COLOR);
+            }
+
+            canvas.drawCircle(x, y, mRadius, mPaint);
+            x += mStep;
+        }
+    }
+
+    void updateAdapter(PagerAdapter oldAdapter, PagerAdapter newAdapter) {
+        if (oldAdapter != null) {
+            oldAdapter.unregisterDataSetObserver(mPageListener);
+            mWatchingAdapter = null;
+        }
+
+        if (newAdapter != null) {
+            newAdapter.registerDataSetObserver(mPageListener);
+            mWatchingAdapter = new WeakReference<>(newAdapter);
+        }
+
+        updateDots();
+
+        if (mPager != null) {
+            requestLayout();
+        }
+    }
+
+    private <T> T getRef(WeakReference<T> weakRef) {
+        if (weakRef == null) {
+            return null;
+        }
+        return weakRef.get();
+    }
+
+    private void updateDots() {
+        PagerAdapter adapter = getRef(mWatchingAdapter);
+        if (adapter == null) {
+            return;
+        }
+
+        int count = adapter.getCount();
+        if (mPageCount == count) {
+            // Nothing to be done.
+            return;
+        }
+
+        mPageCount = count;
+        mCurrentPosition = 0;
+        invalidate();
+    }
+
+    private class PageListener extends DataSetObserver implements ViewPager.OnPageChangeListener,
+            ViewPager.OnAdapterChangeListener {
+
+        @Override
+        public void onPageScrolled(int unused1, float unused2, int unused3) { }
+
+        @Override
+        public void onPageSelected(int position) {
+            if (mCurrentPosition == position) {
+                return;
+            }
+
+            if (mPageCount <= position) {
+                Log.e(TAG, "Position out of bounds, position=" + position + " size=" + mPageCount);
+                return;
+            }
+
+            mCurrentPosition = position;
+            invalidate();
+        }
+
+        @Override
+        public void onPageScrollStateChanged(int state) { }
+
+        @Override
+        public void onAdapterChanged(ViewPager viewPager, PagerAdapter oldAdapter,
+                PagerAdapter newAdapter) {
+            updateAdapter(oldAdapter, newAdapter);
+        }
+
+        @Override
+        public void onChanged() {
+            updateDots();
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/car/UserGridView.java b/com/android/systemui/statusbar/car/UserGridView.java
new file mode 100644
index 0000000..e551801
--- /dev/null
+++ b/com/android/systemui/statusbar/car/UserGridView.java
@@ -0,0 +1,388 @@
+/*
+ * 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.car;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+
+/**
+ * Displays a ViewPager with icons for the users in the system to allow switching between users.
+ * One of the uses of this is for the lock screen in auto.
+ */
+public class UserGridView extends ViewPager {
+    private static final int EXPAND_ANIMATION_TIME_MS = 200;
+    private static final int HIDE_ANIMATION_TIME_MS = 133;
+
+    private StatusBar mStatusBar;
+    private UserSwitcherController mUserSwitcherController;
+    private Adapter mAdapter;
+    private UserSelectionListener mUserSelectionListener;
+    private ValueAnimator mHeightAnimator;
+    private int mTargetHeight;
+    private int mHeightChildren;
+    private boolean mShowing;
+
+    public UserGridView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void init(StatusBar statusBar, UserSwitcherController userSwitcherController,
+            boolean showInitially) {
+        mStatusBar = statusBar;
+        mUserSwitcherController = userSwitcherController;
+        mAdapter = new Adapter(mUserSwitcherController);
+        addOnLayoutChangeListener(mAdapter);
+        setAdapter(mAdapter);
+        mShowing = showInitially;
+    }
+
+    public boolean isShowing() {
+        return mShowing;
+    }
+
+    public void show() {
+        mShowing = true;
+        animateHeightChange(getMeasuredHeight(), mHeightChildren);
+    }
+
+    public void hide() {
+        mShowing = false;
+        animateHeightChange(getMeasuredHeight(), 0);
+    }
+
+    public void onUserSwitched(int newUserId) {
+        // Bring up security view after user switch is completed.
+        post(this::showOfflineAuthUi);
+    }
+
+    public void setUserSelectionListener(UserSelectionListener userSelectionListener) {
+        mUserSelectionListener = userSelectionListener;
+    }
+
+    void showOfflineAuthUi() {
+        // TODO: Show keyguard UI in-place.
+        mStatusBar.executeRunnableDismissingKeyguard(null, null, true, true, true);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // Wrap content doesn't work in ViewPagers, so simulate the behavior in code.
+        int height = 0;
+        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
+            height = MeasureSpec.getSize(heightMeasureSpec);
+        } else {
+            for (int i = 0; i < getChildCount(); i++) {
+                View child = getChildAt(i);
+                child.measure(widthMeasureSpec,
+                        MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+                height = Math.max(child.getMeasuredHeight(), height);
+            }
+
+            mHeightChildren = height;
+
+            // Override the height if it's not showing.
+            if (!mShowing) {
+                height = 0;
+            }
+
+            // Respect the AT_MOST request from parent.
+            if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
+                height = Math.min(MeasureSpec.getSize(heightMeasureSpec), height);
+            }
+        }
+        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    private void animateHeightChange(int oldHeight, int newHeight) {
+        // If there is no change in height or an animation is already in progress towards the
+        // desired height, then there's no need to make any changes.
+        if (oldHeight == newHeight || newHeight == mTargetHeight) {
+            return;
+        }
+
+        // Animation in progress is not going towards the new target, so cancel it.
+        if (mHeightAnimator != null){
+            mHeightAnimator.cancel();
+        }
+
+        mTargetHeight = newHeight;
+        mHeightAnimator = ValueAnimator.ofInt(oldHeight, mTargetHeight);
+        mHeightAnimator.addUpdateListener(valueAnimator -> {
+            ViewGroup.LayoutParams layoutParams = getLayoutParams();
+            layoutParams.height = (Integer) valueAnimator.getAnimatedValue();
+            requestLayout();
+        });
+        mHeightAnimator.addListener(new AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animator) {}
+
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                // ValueAnimator does not guarantee that the update listener will get an update
+                // to the final value, so here, the final value is set.  Though the final calculated
+                // height (mTargetHeight) could be set, WRAP_CONTENT is more appropriate.
+                ViewGroup.LayoutParams layoutParams = getLayoutParams();
+                layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+                requestLayout();
+                mHeightAnimator = null;
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animator) {}
+
+            @Override
+            public void onAnimationRepeat(Animator animator) {}
+        });
+
+        mHeightAnimator.setInterpolator(new FastOutSlowInInterpolator());
+        if (oldHeight < newHeight) {
+            // Expanding
+            mHeightAnimator.setDuration(EXPAND_ANIMATION_TIME_MS);
+        } else {
+            // Hiding
+            mHeightAnimator.setDuration(HIDE_ANIMATION_TIME_MS);
+        }
+        mHeightAnimator.start();
+    }
+
+    /**
+     * This is a ViewPager.PagerAdapter which deletegates the work to a
+     * UserSwitcherController.BaseUserAdapter. Java doesn't support multiple inheritance so we have
+     * to use composition instead to achieve the same goal since both the base classes are abstract
+     * classes and not interfaces.
+     */
+    private final class Adapter extends PagerAdapter implements View.OnLayoutChangeListener {
+        private final int mPodWidth;
+        private final int mPodMarginBetween;
+        private final int mPodImageAvatarWidth;
+        private final int mPodImageAvatarHeight;
+
+        private final WrappedBaseUserAdapter mUserAdapter;
+        private int mContainerWidth;
+
+        public Adapter(UserSwitcherController controller) {
+            super();
+            mUserAdapter = new WrappedBaseUserAdapter(controller, this);
+
+            Resources res = getResources();
+            mPodWidth = res.getDimensionPixelSize(R.dimen.car_fullscreen_user_pod_width);
+            mPodMarginBetween = res.getDimensionPixelSize(
+                    R.dimen.car_fullscreen_user_pod_margin_between);
+            mPodImageAvatarWidth = res.getDimensionPixelSize(
+                    R.dimen.car_fullscreen_user_pod_image_avatar_width);
+            mPodImageAvatarHeight = res.getDimensionPixelSize(
+                    R.dimen.car_fullscreen_user_pod_image_avatar_height);
+        }
+
+        @Override
+        public void destroyItem(ViewGroup container, int position, Object object) {
+            container.removeView((View) object);
+        }
+
+        private int getIconsPerPage() {
+            // We need to know how many pods we need in this page. Each pod has its own width and
+            // a margin between them. We can then divide the measured width of the parent by the
+            // sum of pod width and margin to get the number of pods that will completely fit.
+            // There is one less margin than the number of pods (eg. for 5 pods, there are 4
+            // margins), so need to add the margin to the measured width to account for that.
+            return (mContainerWidth + mPodMarginBetween) /
+                    (mPodWidth + mPodMarginBetween);
+        }
+
+        @Override
+        public Object instantiateItem(ViewGroup container, int position) {
+            Context context = getContext();
+            LayoutInflater inflater = LayoutInflater.from(context);
+
+            ViewGroup pods = (ViewGroup) inflater.inflate(
+                    R.layout.car_fullscreen_user_pod_container, null);
+
+            int iconsPerPage = getIconsPerPage();
+            int limit = Math.min(mUserAdapter.getCount(), (position + 1) * iconsPerPage);
+            for (int i = position * iconsPerPage; i < limit; i++) {
+                View v = makeUserPod(inflater, context, i, pods);
+                pods.addView(v);
+                // This is hacky, but the dividers on the pod container LinearLayout don't seem
+                // to work for whatever reason.  Instead, set a right margin on the pod if it's not
+                // the right-most pod and there is more than one pod in the container.
+                if (i < limit - 1 && limit > 1) {
+                    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
+                            LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+                    params.setMargins(0, 0, mPodMarginBetween, 0);
+                    v.setLayoutParams(params);
+                }
+            }
+            container.addView(pods);
+            return pods;
+        }
+
+        /**
+         * Returns the default user icon.  This icon is a circle with a letter in it.  The letter is
+         * the first character in the username.
+         *
+         * @param userName the username of the user for which the icon is to be created
+         */
+        private Bitmap getDefaultUserIcon(CharSequence userName) {
+            CharSequence displayText = userName.subSequence(0, 1);
+            Bitmap out = Bitmap.createBitmap(mPodImageAvatarWidth, mPodImageAvatarHeight,
+                    Bitmap.Config.ARGB_8888);
+            Canvas canvas = new Canvas(out);
+
+            // Draw the circle background.
+            GradientDrawable shape = new GradientDrawable();
+            shape.setShape(GradientDrawable.RADIAL_GRADIENT);
+            shape.setGradientRadius(1.0f);
+            shape.setColor(getContext().getColor(R.color.car_user_switcher_no_user_image_bgcolor));
+            shape.setBounds(0, 0, mPodImageAvatarWidth, mPodImageAvatarHeight);
+            shape.draw(canvas);
+
+            // Draw the letter in the center.
+            Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+            paint.setColor(getContext().getColor(R.color.car_user_switcher_no_user_image_fgcolor));
+            paint.setTextAlign(Align.CENTER);
+            paint.setTextSize(getResources().getDimensionPixelSize(
+                    R.dimen.car_fullscreen_user_pod_icon_text_size));
+            Paint.FontMetricsInt metrics = paint.getFontMetricsInt();
+            // The Y coordinate is measured by taking half the height of the pod, but that would
+            // draw the character putting the bottom of the font in the middle of the pod.  To
+            // correct this, half the difference between the top and bottom distance metrics of the
+            // font gives the offset of the font.  Bottom is a positive value, top is negative, so
+            // the different is actually a sum.  The "half" operation is then factored out.
+            canvas.drawText(displayText.toString(), mPodImageAvatarWidth / 2,
+                    (mPodImageAvatarHeight - (metrics.bottom + metrics.top)) / 2, paint);
+
+            return out;
+        }
+
+        private View makeUserPod(LayoutInflater inflater, Context context,
+                int position, ViewGroup parent) {
+            final UserSwitcherController.UserRecord record = mUserAdapter.getItem(position);
+            View view = inflater.inflate(R.layout.car_fullscreen_user_pod, parent, false);
+
+            TextView nameView = view.findViewById(R.id.user_name);
+            if (record != null) {
+                nameView.setText(mUserAdapter.getName(context, record));
+                view.setActivated(record.isCurrent);
+            } else {
+                nameView.setText(context.getString(R.string.unknown_user_label));
+            }
+
+            ImageView iconView = (ImageView) view.findViewById(R.id.user_avatar);
+            if (record == null || (record.picture == null && !record.isAddUser)) {
+                iconView.setImageBitmap(getDefaultUserIcon(nameView.getText()));
+            } else if (record.isAddUser) {
+                Drawable icon = context.getDrawable(R.drawable.ic_add_circle_qs);
+                icon.setTint(context.getColor(R.color.car_user_switcher_no_user_image_bgcolor));
+                iconView.setImageDrawable(icon);
+            } else {
+                iconView.setImageBitmap(record.picture);
+            }
+
+            iconView.setOnClickListener(v -> {
+                if (record == null) {
+                    return;
+                }
+
+                if (mUserSelectionListener != null) {
+                    mUserSelectionListener.onUserSelected(record);
+                }
+
+                if (record.isCurrent) {
+                    showOfflineAuthUi();
+                } else {
+                    mUserSwitcherController.switchTo(record);
+                }
+            });
+
+            return view;
+        }
+
+        @Override
+        public int getCount() {
+            int iconsPerPage = getIconsPerPage();
+            if (iconsPerPage == 0) {
+                return 0;
+            }
+            return (int) Math.ceil((double) mUserAdapter.getCount() / getIconsPerPage());
+        }
+
+        public void refresh() {
+            mUserAdapter.refresh();
+        }
+
+        @Override
+        public boolean isViewFromObject(View view, Object object) {
+            return view == object;
+        }
+
+        @Override
+        public void onLayoutChange(View v, int left, int top, int right, int bottom,
+                int oldLeft, int oldTop, int oldRight, int oldBottom) {
+            mContainerWidth = Math.max(left - right, right - left);
+            notifyDataSetChanged();
+        }
+    }
+
+    private final class WrappedBaseUserAdapter extends UserSwitcherController.BaseUserAdapter {
+        private Adapter mContainer;
+
+        public WrappedBaseUserAdapter(UserSwitcherController controller, Adapter container) {
+            super(controller);
+            mContainer = container;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            throw new UnsupportedOperationException("unused");
+        }
+
+        @Override
+        public void notifyDataSetChanged() {
+            super.notifyDataSetChanged();
+            mContainer.notifyDataSetChanged();
+        }
+    }
+
+    interface UserSelectionListener {
+        void onUserSelected(UserSwitcherController.UserRecord record);
+    };
+}
diff --git a/com/android/systemui/statusbar/notification/AboveShelfChangedListener.java b/com/android/systemui/statusbar/notification/AboveShelfChangedListener.java
new file mode 100644
index 0000000..07baedc
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/AboveShelfChangedListener.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+/**
+ * A listener for when the above shelf state of notification changes
+ */
+public interface AboveShelfChangedListener {
+
+    /**
+     * Notifies a listener that the above shelf state changed
+     */
+    void onAboveShelfStateChanged(boolean aboveShelf);
+}
diff --git a/com/android/systemui/statusbar/notification/AboveShelfObserver.java b/com/android/systemui/statusbar/notification/AboveShelfObserver.java
new file mode 100644
index 0000000..f10d2d7
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/AboveShelfObserver.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+
+/**
+ * An observer that listens to the above shelf state and can notify listeners
+ */
+public class AboveShelfObserver implements AboveShelfChangedListener {
+
+    private final ViewGroup mHostLayout;
+    private boolean mHasViewsAboveShelf = false;
+    private HasViewAboveShelfChangedListener mListener;
+
+    public AboveShelfObserver(ViewGroup hostLayout) {
+        mHostLayout = hostLayout;
+    }
+
+    public void setListener(HasViewAboveShelfChangedListener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public void onAboveShelfStateChanged(boolean aboveShelf) {
+        boolean hasViewsAboveShelf = aboveShelf;
+        if (!hasViewsAboveShelf && mHostLayout != null) {
+            int n = mHostLayout.getChildCount();
+            for (int i = 0; i < n; i++) {
+                View child = mHostLayout.getChildAt(i);
+                if (child instanceof ExpandableNotificationRow) {
+                    if (((ExpandableNotificationRow) child).isAboveShelf()) {
+                        hasViewsAboveShelf = true;
+                        break;
+                    }
+                }
+            }
+        }
+        if (mHasViewsAboveShelf != hasViewsAboveShelf) {
+            mHasViewsAboveShelf = hasViewsAboveShelf;
+            if (mListener != null) {
+                mListener.onHasViewsAboveShelfChanged(hasViewsAboveShelf);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    boolean hasViewsAboveShelf() {
+        return mHasViewsAboveShelf;
+    }
+
+    public interface HasViewAboveShelfChangedListener {
+        void onHasViewsAboveShelfChanged(boolean hasViewsAboveShelf);
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/ActionListTransformState.java b/com/android/systemui/statusbar/notification/ActionListTransformState.java
new file mode 100644
index 0000000..8c72544
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/ActionListTransformState.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.text.Layout;
+import android.text.TextUtils;
+import android.util.Pools;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * A transform state of the action list
+*/
+public class ActionListTransformState extends TransformState {
+
+    private static Pools.SimplePool<ActionListTransformState> sInstancePool
+            = new Pools.SimplePool<>(40);
+
+    @Override
+    protected boolean sameAs(TransformState otherState) {
+        return otherState instanceof ActionListTransformState;
+    }
+
+    public static ActionListTransformState obtain() {
+        ActionListTransformState instance = sInstancePool.acquire();
+        if (instance != null) {
+            return instance;
+        }
+        return new ActionListTransformState();
+    }
+
+    @Override
+    public void transformViewFullyFrom(TransformState otherState, float transformationAmount) {
+        // Don't do Y transform - let the wrapper handle this based on the content height
+    }
+
+    @Override
+    public void transformViewFullyTo(TransformState otherState, float transformationAmount) {
+        // Don't do Y transform - let the wrapper handle this based on the content height
+    }
+
+    @Override
+    protected void resetTransformedView() {
+        // We need to keep the Y transformation, because this is used to keep the action list
+        // aligned at the bottom, unrelated to transforms.
+        float y = getTransformedView().getTranslationY();
+        super.resetTransformedView();
+        getTransformedView().setTranslationY(y);
+    }
+
+    @Override
+    public void recycle() {
+        super.recycle();
+        sInstancePool.release(this);
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/CustomInterpolatorTransformation.java b/com/android/systemui/statusbar/notification/CustomInterpolatorTransformation.java
new file mode 100644
index 0000000..de4c312
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/CustomInterpolatorTransformation.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.TransformableView;
+import com.android.systemui.statusbar.ViewTransformationHelper;
+
+import static com.android.systemui.statusbar.TransformableView.TRANSFORMING_VIEW_TITLE;
+import static com.android.systemui.statusbar.notification.TransformState.TRANSFORM_Y;
+
+/**
+ * A custom transformation that modifies the interpolator
+ */
+public abstract class CustomInterpolatorTransformation
+        extends ViewTransformationHelper.CustomTransformation {
+
+    private final int mViewType;
+
+    public CustomInterpolatorTransformation(int viewType) {
+        mViewType = viewType;
+    }
+
+    @Override
+    public boolean transformTo(TransformState ownState, TransformableView notification,
+            float transformationAmount) {
+        if (!hasCustomTransformation()) {
+            return false;
+        }
+        TransformState otherState = notification.getCurrentState(mViewType);
+        if (otherState == null) {
+            return false;
+        }
+        View view = ownState.getTransformedView();
+        CrossFadeHelper.fadeOut(view, transformationAmount);
+        ownState.transformViewFullyTo(otherState, this, transformationAmount);
+        otherState.recycle();
+        return true;
+    }
+
+    protected boolean hasCustomTransformation() {
+        return true;
+    }
+
+    @Override
+    public boolean transformFrom(TransformState ownState,
+            TransformableView notification, float transformationAmount) {
+        if (!hasCustomTransformation()) {
+            return false;
+        }
+        TransformState otherState = notification.getCurrentState(mViewType);
+        if (otherState == null) {
+            return false;
+        }
+        View view = ownState.getTransformedView();
+        CrossFadeHelper.fadeIn(view, transformationAmount);
+        ownState.transformViewFullyFrom(otherState, this, transformationAmount);
+        otherState.recycle();
+        return true;
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/FakeShadowView.java b/com/android/systemui/statusbar/notification/FakeShadowView.java
new file mode 100644
index 0000000..0c1891e
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/FakeShadowView.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Outline;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.widget.LinearLayout;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.AlphaOptimizedFrameLayout;
+
+/**
+ * A view used to cast a shadow of a certain size on another view
+ */
+public class FakeShadowView extends AlphaOptimizedFrameLayout {
+    public static final float SHADOW_SIBLING_TRESHOLD = 0.1f;
+    private final int mShadowMinHeight;
+
+    private View mFakeShadow;
+    private float mOutlineAlpha;
+
+    public FakeShadowView(Context context) {
+        this(context, null);
+    }
+
+    public FakeShadowView(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public FakeShadowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public FakeShadowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        mFakeShadow = new View(context);
+        mFakeShadow.setVisibility(INVISIBLE);
+        mFakeShadow.setLayoutParams(new LinearLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                (int) (48 * getResources().getDisplayMetrics().density)));
+        mFakeShadow.setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                outline.setRect(0, 0, getWidth(), mFakeShadow.getHeight());
+                outline.setAlpha(mOutlineAlpha);
+            }
+        });
+        addView(mFakeShadow);
+        mShadowMinHeight = Math.max(1, context.getResources()
+                .getDimensionPixelSize(R.dimen.notification_divider_height));
+    }
+
+    public void setFakeShadowTranslationZ(float fakeShadowTranslationZ, float outlineAlpha,
+            int shadowYEnd, int outlineTranslation) {
+        if (fakeShadowTranslationZ == 0.0f) {
+            mFakeShadow.setVisibility(INVISIBLE);
+        } else {
+            mFakeShadow.setVisibility(VISIBLE);
+            fakeShadowTranslationZ = Math.max(mShadowMinHeight, fakeShadowTranslationZ);
+            mFakeShadow.setTranslationZ(fakeShadowTranslationZ);
+            mFakeShadow.setTranslationX(outlineTranslation);
+            mFakeShadow.setTranslationY(shadowYEnd - mFakeShadow.getHeight());
+            if (outlineAlpha != mOutlineAlpha) {
+                mOutlineAlpha = outlineAlpha;
+                mFakeShadow.invalidateOutline();
+            }
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/HybridGroupManager.java b/com/android/systemui/statusbar/notification/HybridGroupManager.java
new file mode 100644
index 0000000..3ed8cce
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/HybridGroupManager.java
@@ -0,0 +1,162 @@
+/*
+ * 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.notification;
+
+import android.app.Notification;
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+
+/**
+ * A class managing hybrid groups that include {@link HybridNotificationView} and the notification
+ * group overflow.
+ */
+public class HybridGroupManager {
+
+    private final Context mContext;
+    private final NotificationDozeHelper mDozer;
+    private final ViewGroup mParent;
+
+    private final float mOverflowNumberSizeDark;
+    private final int mOverflowNumberPaddingDark;
+    private final float mOverflowNumberSize;
+    private final int mOverflowNumberPadding;
+
+    private int mOverflowNumberColor;
+    private int mOverflowNumberColorDark;
+    private float mDarkAmount = 0f;
+
+    public HybridGroupManager(Context ctx, ViewGroup parent) {
+        mContext = ctx;
+        mParent = parent;
+        mDozer = new NotificationDozeHelper();
+
+        Resources res = mContext.getResources();
+        mOverflowNumberSize = res.getDimensionPixelSize(
+                R.dimen.group_overflow_number_size);
+        mOverflowNumberSizeDark = res.getDimensionPixelSize(
+                R.dimen.group_overflow_number_size_dark);
+        mOverflowNumberPadding = res.getDimensionPixelSize(
+                R.dimen.group_overflow_number_padding);
+        mOverflowNumberPaddingDark = mOverflowNumberPadding + res.getDimensionPixelSize(
+                R.dimen.group_overflow_number_extra_padding_dark);
+    }
+
+    private HybridNotificationView inflateHybridViewWithStyle(int style) {
+        LayoutInflater inflater = new ContextThemeWrapper(mContext, style)
+                .getSystemService(LayoutInflater.class);
+        HybridNotificationView hybrid = (HybridNotificationView) inflater.inflate(
+                R.layout.hybrid_notification, mParent, false);
+        mParent.addView(hybrid);
+        return hybrid;
+    }
+
+    private TextView inflateOverflowNumber() {
+        LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class);
+        TextView numberView = (TextView) inflater.inflate(
+                R.layout.hybrid_overflow_number, mParent, false);
+        mParent.addView(numberView);
+        updateOverFlowNumberColor(numberView);
+        return numberView;
+    }
+
+    private void updateOverFlowNumberColor(TextView numberView) {
+        numberView.setTextColor(NotificationUtils.interpolateColors(
+                mOverflowNumberColor, mOverflowNumberColorDark, mDarkAmount));
+    }
+
+    public void setOverflowNumberColor(TextView numberView, int colorRegular, int colorDark) {
+        mOverflowNumberColor = colorRegular;
+        mOverflowNumberColorDark = colorDark;
+        if (numberView != null) {
+            updateOverFlowNumberColor(numberView);
+        }
+    }
+
+    public HybridNotificationView bindFromNotification(HybridNotificationView reusableView,
+            Notification notification) {
+        return bindFromNotificationWithStyle(reusableView, notification,
+                R.style.HybridNotification);
+    }
+
+    public HybridNotificationView bindAmbientFromNotification(HybridNotificationView reusableView,
+            Notification notification) {
+        return bindFromNotificationWithStyle(reusableView, notification,
+                R.style.HybridNotification_Ambient);
+    }
+
+    private HybridNotificationView bindFromNotificationWithStyle(
+            HybridNotificationView reusableView, Notification notification, int style) {
+        if (reusableView == null) {
+            reusableView = inflateHybridViewWithStyle(style);
+        }
+        CharSequence titleText = resolveTitle(notification);
+        CharSequence contentText = resolveText(notification);
+        reusableView.bind(titleText, contentText);
+        return reusableView;
+    }
+
+    private CharSequence resolveText(Notification notification) {
+        CharSequence contentText = notification.extras.getCharSequence(Notification.EXTRA_TEXT);
+        if (contentText == null) {
+            contentText = notification.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
+        }
+        return contentText;
+    }
+
+    private CharSequence resolveTitle(Notification notification) {
+        CharSequence titleText = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
+        if (titleText == null) {
+            titleText = notification.extras.getCharSequence(Notification.EXTRA_TITLE_BIG);
+        }
+        return titleText;
+    }
+
+    public TextView bindOverflowNumber(TextView reusableView, int number) {
+        if (reusableView == null) {
+            reusableView = inflateOverflowNumber();
+        }
+        String text = mContext.getResources().getString(
+                R.string.notification_group_overflow_indicator, number);
+        if (!text.equals(reusableView.getText())) {
+            reusableView.setText(text);
+        }
+        String contentDescription = String.format(mContext.getResources().getQuantityString(
+                R.plurals.notification_group_overflow_description, number), number);
+
+        reusableView.setContentDescription(contentDescription);
+        return reusableView;
+    }
+
+    public void setOverflowNumberDark(TextView view, boolean dark, boolean fade, long delay) {
+        mDozer.setIntensityDark((f)->{
+            mDarkAmount = f;
+            updateOverFlowNumberColor(view);
+        }, dark, fade, delay);
+        view.setTextSize(TypedValue.COMPLEX_UNIT_PX,
+                dark ? mOverflowNumberSizeDark : mOverflowNumberSize);
+        int paddingEnd = dark ? mOverflowNumberPaddingDark : mOverflowNumberPadding;
+        view.setPaddingRelative(view.getPaddingStart(), view.getPaddingTop(), paddingEnd,
+                view.getPaddingBottom());
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/HybridNotificationView.java b/com/android/systemui/statusbar/notification/HybridNotificationView.java
new file mode 100644
index 0000000..0a1795f
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/HybridNotificationView.java
@@ -0,0 +1,163 @@
+/*
+ * 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.notification;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.keyguard.AlphaOptimizedLinearLayout;
+import com.android.systemui.R;
+import com.android.systemui.ViewInvertHelper;
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.TransformableView;
+import com.android.systemui.statusbar.ViewTransformationHelper;
+import com.android.systemui.statusbar.phone.NotificationPanelView;
+
+/**
+ * A hybrid view which may contain information about one ore more notifications.
+ */
+public class HybridNotificationView extends AlphaOptimizedLinearLayout
+        implements TransformableView {
+
+    private ViewTransformationHelper mTransformationHelper;
+
+    protected TextView mTitleView;
+    protected TextView mTextView;
+    private ViewInvertHelper mInvertHelper;
+
+    public HybridNotificationView(Context context) {
+        this(context, null);
+    }
+
+    public HybridNotificationView(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public HybridNotificationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public HybridNotificationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    public TextView getTitleView() {
+        return mTitleView;
+    }
+
+    public TextView getTextView() {
+        return mTextView;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mTitleView = (TextView) findViewById(R.id.notification_title);
+        mTextView = (TextView) findViewById(R.id.notification_text);
+        mInvertHelper = new ViewInvertHelper(this, NotificationPanelView.DOZE_ANIMATION_DURATION);
+        mTransformationHelper = new ViewTransformationHelper();
+        mTransformationHelper.setCustomTransformation(
+                new ViewTransformationHelper.CustomTransformation() {
+                    @Override
+                    public boolean transformTo(TransformState ownState, TransformableView notification,
+                            float transformationAmount) {
+                        // We want to transform to the same y location as the title
+                        TransformState otherState = notification.getCurrentState(
+                                TRANSFORMING_VIEW_TITLE);
+                        CrossFadeHelper.fadeOut(mTextView, transformationAmount);
+                        if (otherState != null) {
+                            ownState.transformViewVerticalTo(otherState, transformationAmount);
+                            otherState.recycle();
+                        }
+                        return true;
+                    }
+
+                    @Override
+                    public boolean transformFrom(TransformState ownState,
+                            TransformableView notification, float transformationAmount) {
+                        // We want to transform from the same y location as the title
+                        TransformState otherState = notification.getCurrentState(
+                                TRANSFORMING_VIEW_TITLE);
+                        CrossFadeHelper.fadeIn(mTextView, transformationAmount);
+                        if (otherState != null) {
+                            ownState.transformViewVerticalFrom(otherState, transformationAmount);
+                            otherState.recycle();
+                        }
+                        return true;
+                    }
+                }, TRANSFORMING_VIEW_TEXT);
+        mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TITLE, mTitleView);
+        mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TEXT, mTextView);
+    }
+
+    public void bind(CharSequence title) {
+        bind(title, null);
+    }
+
+    public void bind(CharSequence title, CharSequence text) {
+        mTitleView.setText(title);
+        mTitleView.setVisibility(TextUtils.isEmpty(title) ? GONE : VISIBLE);
+        if (TextUtils.isEmpty(text)) {
+            mTextView.setVisibility(GONE);
+            mTextView.setText(null);
+        } else {
+            mTextView.setVisibility(VISIBLE);
+            mTextView.setText(text.toString());
+        }
+        requestLayout();
+    }
+
+    public void setDark(boolean dark, boolean fade, long delay) {
+        mInvertHelper.setInverted(dark, fade, delay);
+    }
+
+    @Override
+    public TransformState getCurrentState(int fadingView) {
+        return mTransformationHelper.getCurrentState(fadingView);
+    }
+
+    @Override
+    public void transformTo(TransformableView notification, Runnable endRunnable) {
+        mTransformationHelper.transformTo(notification, endRunnable);
+    }
+
+    @Override
+    public void transformTo(TransformableView notification, float transformationAmount) {
+        mTransformationHelper.transformTo(notification, transformationAmount);
+    }
+
+    @Override
+    public void transformFrom(TransformableView notification) {
+        mTransformationHelper.transformFrom(notification);
+    }
+
+    @Override
+    public void transformFrom(TransformableView notification, float transformationAmount) {
+        mTransformationHelper.transformFrom(notification, transformationAmount);
+    }
+
+    @Override
+    public void setVisible(boolean visible) {
+        setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+        mTransformationHelper.setVisible(visible);
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/ImageGradientColorizer.java b/com/android/systemui/statusbar/notification/ImageGradientColorizer.java
new file mode 100644
index 0000000..454edbb
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/ImageGradientColorizer.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Shader;
+import android.graphics.Xfermode;
+import android.graphics.drawable.Drawable;
+
+/**
+ * A utility class to colorize bitmaps with a color gradient and a special blending mode
+ */
+public class ImageGradientColorizer {
+    public Bitmap colorize(Drawable drawable, int backgroundColor, boolean isRtl) {
+        int width = drawable.getIntrinsicWidth();
+        int height = drawable.getIntrinsicHeight();
+        int size = Math.min(width, height);
+        int widthInset = (width - size) / 2;
+        int heightInset = (height - size) / 2;
+        drawable = drawable.mutate();
+        drawable.setBounds(- widthInset, - heightInset, width - widthInset, height - heightInset);
+        Bitmap newBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(newBitmap);
+
+        // Values to calculate the luminance of a color
+        float lr = 0.2126f;
+        float lg = 0.7152f;
+        float lb = 0.0722f;
+
+        // Extract the red, green, blue components of the color extraction color in
+        // float and int form
+        int tri = Color.red(backgroundColor);
+        int tgi = Color.green(backgroundColor);
+        int tbi = Color.blue(backgroundColor);
+
+        float tr = tri / 255f;
+        float tg = tgi / 255f;
+        float tb = tbi / 255f;
+
+        // Calculate the luminance of the color extraction color
+        float cLum = (tr * lr + tg * lg + tb * lb) * 255;
+
+        ColorMatrix m = new ColorMatrix(new float[] {
+                lr, lg, lb, 0, tri - cLum,
+                lr, lg, lb, 0, tgi - cLum,
+                lr, lg, lb, 0, tbi - cLum,
+                0, 0, 0, 1, 0,
+        });
+
+        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        LinearGradient linearGradient =  new LinearGradient(0, 0, size, 0,
+                new int[] {0, Color.argb(0.5f, 1, 1, 1), Color.BLACK},
+                new float[] {0.0f, 0.4f, 1.0f}, Shader.TileMode.CLAMP);
+        paint.setShader(linearGradient);
+        Bitmap fadeIn = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+        Canvas fadeInCanvas = new Canvas(fadeIn);
+        drawable.clearColorFilter();
+        drawable.draw(fadeInCanvas);
+
+        if (isRtl) {
+            // Let's flip the gradient
+            fadeInCanvas.translate(size, 0);
+            fadeInCanvas.scale(-1, 1);
+        }
+        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
+        fadeInCanvas.drawPaint(paint);
+
+        Paint coloredPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        coloredPaint.setColorFilter(new ColorMatrixColorFilter(m));
+        coloredPaint.setAlpha((int) (0.5f * 255));
+        canvas.drawBitmap(fadeIn, 0, 0, coloredPaint);
+
+        linearGradient =  new LinearGradient(0, 0, size, 0,
+                new int[] {0, Color.argb(0.5f, 1, 1, 1), Color.BLACK},
+                new float[] {0.0f, 0.6f, 1.0f}, Shader.TileMode.CLAMP);
+        paint.setShader(linearGradient);
+        fadeInCanvas.drawPaint(paint);
+        canvas.drawBitmap(fadeIn, 0, 0, null);
+
+        return newBitmap;
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/ImageTransformState.java b/com/android/systemui/statusbar/notification/ImageTransformState.java
new file mode 100644
index 0000000..92f26d6
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/ImageTransformState.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.graphics.drawable.Icon;
+import android.util.Pools;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.TransformableView;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+/**
+ * A transform state of a image view.
+*/
+public class ImageTransformState extends TransformState {
+    public static final long ANIMATION_DURATION_LENGTH = 210;
+
+    public static final int ICON_TAG = R.id.image_icon_tag;
+    private static Pools.SimplePool<ImageTransformState> sInstancePool
+            = new Pools.SimplePool<>(40);
+    private Icon mIcon;
+
+    @Override
+    public void initFrom(View view) {
+        super.initFrom(view);
+        if (view instanceof ImageView) {
+            mIcon = (Icon) view.getTag(ICON_TAG);
+        }
+    }
+
+    @Override
+    protected boolean sameAs(TransformState otherState) {
+        if (super.sameAs(otherState)) {
+            return true;
+        }
+        if (otherState instanceof ImageTransformState) {
+            return mIcon != null && mIcon.sameAs(((ImageTransformState) otherState).getIcon());
+        }
+        return false;
+    }
+
+    @Override
+    public void appear(float transformationAmount, TransformableView otherView) {
+        if (otherView instanceof HybridNotificationView) {
+            if (transformationAmount == 0.0f) {
+                mTransformedView.setPivotY(0);
+                mTransformedView.setPivotX(mTransformedView.getWidth() / 2);
+                prepareFadeIn();
+            }
+            transformationAmount = mapToDuration(transformationAmount);
+            CrossFadeHelper.fadeIn(mTransformedView, transformationAmount, false /* remap */);
+            transformationAmount = Interpolators.LINEAR_OUT_SLOW_IN.getInterpolation(
+                    transformationAmount);
+            mTransformedView.setScaleX(transformationAmount);
+            mTransformedView.setScaleY(transformationAmount);
+        } else {
+            super.appear(transformationAmount, otherView);
+        }
+    }
+
+    @Override
+    public void disappear(float transformationAmount, TransformableView otherView) {
+        if (otherView instanceof HybridNotificationView) {
+            if (transformationAmount == 0.0f) {
+                mTransformedView.setPivotY(0);
+                mTransformedView.setPivotX(mTransformedView.getWidth() / 2);
+            }
+            transformationAmount = mapToDuration(1.0f - transformationAmount);
+            CrossFadeHelper.fadeOut(mTransformedView, 1.0f - transformationAmount,
+                    false /* remap */);
+            transformationAmount = Interpolators.LINEAR_OUT_SLOW_IN.getInterpolation(
+                    transformationAmount);
+            mTransformedView.setScaleX(transformationAmount);
+            mTransformedView.setScaleY(transformationAmount);
+        } else {
+            super.disappear(transformationAmount, otherView);
+        }
+    }
+
+    private static float mapToDuration(float scaleAmount) {
+        // Assuming a linear interpolator, we can easily map it to our new duration
+        scaleAmount = (scaleAmount * StackStateAnimator.ANIMATION_DURATION_STANDARD
+                - (StackStateAnimator.ANIMATION_DURATION_STANDARD - ANIMATION_DURATION_LENGTH))
+                        / ANIMATION_DURATION_LENGTH;
+        return Math.max(Math.min(scaleAmount, 1.0f), 0.0f);
+    }
+
+    public Icon getIcon() {
+        return mIcon;
+    }
+
+    public static ImageTransformState obtain() {
+        ImageTransformState instance = sInstancePool.acquire();
+        if (instance != null) {
+            return instance;
+        }
+        return new ImageTransformState();
+    }
+
+    @Override
+    protected boolean transformScale(TransformState otherState) {
+        return true;
+    }
+
+    @Override
+    public void recycle() {
+        super.recycle();
+        sInstancePool.release(this);
+    }
+
+    @Override
+    protected void reset() {
+        super.reset();
+        mIcon = null;
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/InflationException.java b/com/android/systemui/statusbar/notification/InflationException.java
new file mode 100644
index 0000000..b484138
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/InflationException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+/**
+ * An exception that something went wrong during the inflation
+ */
+public class InflationException extends Exception {
+    public InflationException(String error) {
+        super(error);
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java b/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java
new file mode 100644
index 0000000..80854ec
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+import android.app.Notification;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.graphics.Palette;
+import android.util.LayoutDirection;
+
+import com.android.internal.util.NotificationColorUtil;
+import com.android.systemui.R;
+
+import java.util.List;
+
+/**
+ * A class the processes media notifications and extracts the right text and background colors.
+ */
+public class MediaNotificationProcessor {
+
+    /**
+     * The fraction below which we select the vibrant instead of the light/dark vibrant color
+     */
+    private static final float POPULATION_FRACTION_FOR_MORE_VIBRANT = 1.0f;
+
+    /**
+     * Minimum saturation that a muted color must have if there exists if deciding between two
+     * colors
+     */
+    private static final float MIN_SATURATION_WHEN_DECIDING = 0.19f;
+
+    /**
+     * Minimum fraction that any color must have to be picked up as a text color
+     */
+    private static final double MINIMUM_IMAGE_FRACTION = 0.002;
+
+    /**
+     * The population fraction to select the dominant color as the text color over a the colored
+     * ones.
+     */
+    private static final float POPULATION_FRACTION_FOR_DOMINANT = 0.01f;
+
+    /**
+     * The population fraction to select a white or black color as the background over a color.
+     */
+    private static final float POPULATION_FRACTION_FOR_WHITE_OR_BLACK = 2.5f;
+    private static final float BLACK_MAX_LIGHTNESS = 0.08f;
+    private static final float WHITE_MIN_LIGHTNESS = 0.90f;
+    private static final int RESIZE_BITMAP_AREA = 150 * 150;
+    private final ImageGradientColorizer mColorizer;
+    private final Context mContext;
+    private float[] mFilteredBackgroundHsl = null;
+    private Palette.Filter mBlackWhiteFilter = (rgb, hsl) -> !isWhiteOrBlack(hsl);
+
+    /**
+     * The context of the notification. This is the app context of the package posting the
+     * notification.
+     */
+    private final Context mPackageContext;
+    private boolean mIsLowPriority;
+
+    public MediaNotificationProcessor(Context context, Context packageContext) {
+        this(context, packageContext, new ImageGradientColorizer());
+    }
+
+    @VisibleForTesting
+    MediaNotificationProcessor(Context context, Context packageContext,
+            ImageGradientColorizer colorizer) {
+        mContext = context;
+        mPackageContext = packageContext;
+        mColorizer = colorizer;
+    }
+
+    /**
+     * Processes a builder of a media notification and calculates the appropriate colors that should
+     * be used.
+     *
+     * @param notification the notification that is being processed
+     * @param builder the recovered builder for the notification. this will be modified
+     */
+    public void processNotification(Notification notification, Notification.Builder builder) {
+        Icon largeIcon = notification.getLargeIcon();
+        Bitmap bitmap = null;
+        Drawable drawable = null;
+        if (largeIcon != null) {
+            // We're transforming the builder, let's make sure all baked in RemoteViews are
+            // rebuilt!
+            builder.setRebuildStyledRemoteViews(true);
+            drawable = largeIcon.loadDrawable(mPackageContext);
+            int backgroundColor = 0;
+            if (notification.isColorizedMedia()) {
+                int width = drawable.getIntrinsicWidth();
+                int height = drawable.getIntrinsicHeight();
+                int area = width * height;
+                if (area > RESIZE_BITMAP_AREA) {
+                    double factor = Math.sqrt((float) RESIZE_BITMAP_AREA / area);
+                    width = (int) (factor * width);
+                    height = (int) (factor * height);
+                }
+                bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+                Canvas canvas = new Canvas(bitmap);
+                drawable.setBounds(0, 0, width, height);
+                drawable.draw(canvas);
+
+                // for the background we only take the left side of the image to ensure
+                // a smooth transition
+                Palette.Builder paletteBuilder = Palette.from(bitmap)
+                        .setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight())
+                        .clearFilters() // we want all colors, red / white / black ones too!
+                        .resizeBitmapArea(RESIZE_BITMAP_AREA);
+                Palette palette = paletteBuilder.generate();
+                backgroundColor = findBackgroundColorAndFilter(palette);
+                // we want most of the full region again, slightly shifted to the right
+                float textColorStartWidthFraction = 0.4f;
+                paletteBuilder.setRegion((int) (bitmap.getWidth() * textColorStartWidthFraction), 0,
+                        bitmap.getWidth(),
+                        bitmap.getHeight());
+                if (mFilteredBackgroundHsl != null) {
+                    paletteBuilder.addFilter((rgb, hsl) -> {
+                        // at least 10 degrees hue difference
+                        float diff = Math.abs(hsl[0] - mFilteredBackgroundHsl[0]);
+                        return diff > 10 && diff < 350;
+                    });
+                }
+                paletteBuilder.addFilter(mBlackWhiteFilter);
+                palette = paletteBuilder.generate();
+                int foregroundColor = selectForegroundColor(backgroundColor, palette);
+                builder.setColorPalette(backgroundColor, foregroundColor);
+            } else {
+                int id = mIsLowPriority
+                        ? R.color.notification_material_background_low_priority_color
+                        : R.color.notification_material_background_color;
+                backgroundColor = mContext.getColor(id);
+            }
+            Bitmap colorized = mColorizer.colorize(drawable, backgroundColor,
+                    mContext.getResources().getConfiguration().getLayoutDirection() ==
+                            LayoutDirection.RTL);
+            builder.setLargeIcon(Icon.createWithBitmap(colorized));
+        }
+    }
+
+    private int selectForegroundColor(int backgroundColor, Palette palette) {
+        if (NotificationColorUtil.isColorLight(backgroundColor)) {
+            return selectForegroundColorForSwatches(palette.getDarkVibrantSwatch(),
+                    palette.getVibrantSwatch(),
+                    palette.getDarkMutedSwatch(),
+                    palette.getMutedSwatch(),
+                    palette.getDominantSwatch(),
+                    Color.BLACK);
+        } else {
+            return selectForegroundColorForSwatches(palette.getLightVibrantSwatch(),
+                    palette.getVibrantSwatch(),
+                    palette.getLightMutedSwatch(),
+                    palette.getMutedSwatch(),
+                    palette.getDominantSwatch(),
+                    Color.WHITE);
+        }
+    }
+
+    private int selectForegroundColorForSwatches(Palette.Swatch moreVibrant,
+            Palette.Swatch vibrant, Palette.Swatch moreMutedSwatch, Palette.Swatch mutedSwatch,
+            Palette.Swatch dominantSwatch, int fallbackColor) {
+        Palette.Swatch coloredCandidate = selectVibrantCandidate(moreVibrant, vibrant);
+        if (coloredCandidate == null) {
+            coloredCandidate = selectMutedCandidate(mutedSwatch, moreMutedSwatch);
+        }
+        if (coloredCandidate != null) {
+            if (dominantSwatch == coloredCandidate) {
+                return coloredCandidate.getRgb();
+            } else if ((float) coloredCandidate.getPopulation() / dominantSwatch.getPopulation()
+                    < POPULATION_FRACTION_FOR_DOMINANT
+                    && dominantSwatch.getHsl()[1] > MIN_SATURATION_WHEN_DECIDING) {
+                return dominantSwatch.getRgb();
+            } else {
+                return coloredCandidate.getRgb();
+            }
+        } else if (hasEnoughPopulation(dominantSwatch)) {
+            return dominantSwatch.getRgb();
+        } else {
+            return fallbackColor;
+        }
+    }
+
+    private Palette.Swatch selectMutedCandidate(Palette.Swatch first,
+            Palette.Swatch second) {
+        boolean firstValid = hasEnoughPopulation(first);
+        boolean secondValid = hasEnoughPopulation(second);
+        if (firstValid && secondValid) {
+            float firstSaturation = first.getHsl()[1];
+            float secondSaturation = second.getHsl()[1];
+            float populationFraction = first.getPopulation() / (float) second.getPopulation();
+            if (firstSaturation * populationFraction > secondSaturation) {
+                return first;
+            } else {
+                return second;
+            }
+        } else if (firstValid) {
+            return first;
+        } else if (secondValid) {
+            return second;
+        }
+        return null;
+    }
+
+    private Palette.Swatch selectVibrantCandidate(Palette.Swatch first, Palette.Swatch second) {
+        boolean firstValid = hasEnoughPopulation(first);
+        boolean secondValid = hasEnoughPopulation(second);
+        if (firstValid && secondValid) {
+            int firstPopulation = first.getPopulation();
+            int secondPopulation = second.getPopulation();
+            if (firstPopulation / (float) secondPopulation
+                    < POPULATION_FRACTION_FOR_MORE_VIBRANT) {
+                return second;
+            } else {
+                return first;
+            }
+        } else if (firstValid) {
+            return first;
+        } else if (secondValid) {
+            return second;
+        }
+        return null;
+    }
+
+    private boolean hasEnoughPopulation(Palette.Swatch swatch) {
+        // We want a fraction that is at least 1% of the image
+        return swatch != null
+                && (swatch.getPopulation() / (float) RESIZE_BITMAP_AREA > MINIMUM_IMAGE_FRACTION);
+    }
+
+    private int findBackgroundColorAndFilter(Palette palette) {
+        // by default we use the dominant palette
+        Palette.Swatch dominantSwatch = palette.getDominantSwatch();
+        if (dominantSwatch == null) {
+            // We're not filtering on white or black
+            mFilteredBackgroundHsl = null;
+            return Color.WHITE;
+        }
+
+        if (!isWhiteOrBlack(dominantSwatch.getHsl())) {
+            mFilteredBackgroundHsl = dominantSwatch.getHsl();
+            return dominantSwatch.getRgb();
+        }
+        // Oh well, we selected black or white. Lets look at the second color!
+        List<Palette.Swatch> swatches = palette.getSwatches();
+        float highestNonWhitePopulation = -1;
+        Palette.Swatch second = null;
+        for (Palette.Swatch swatch: swatches) {
+            if (swatch != dominantSwatch
+                    && swatch.getPopulation() > highestNonWhitePopulation
+                    && !isWhiteOrBlack(swatch.getHsl())) {
+                second = swatch;
+                highestNonWhitePopulation = swatch.getPopulation();
+            }
+        }
+        if (second == null) {
+            // We're not filtering on white or black
+            mFilteredBackgroundHsl = null;
+            return dominantSwatch.getRgb();
+        }
+        if (dominantSwatch.getPopulation() / highestNonWhitePopulation
+                > POPULATION_FRACTION_FOR_WHITE_OR_BLACK) {
+            // The dominant swatch is very dominant, lets take it!
+            // We're not filtering on white or black
+            mFilteredBackgroundHsl = null;
+            return dominantSwatch.getRgb();
+        } else {
+            mFilteredBackgroundHsl = second.getHsl();
+            return second.getRgb();
+        }
+    }
+
+    private boolean isWhiteOrBlack(float[] hsl) {
+        return isBlack(hsl) || isWhite(hsl);
+    }
+
+
+    /**
+     * @return true if the color represents a color which is close to black.
+     */
+    private boolean isBlack(float[] hslColor) {
+        return hslColor[2] <= BLACK_MAX_LIGHTNESS;
+    }
+
+    /**
+     * @return true if the color represents a color which is close to white.
+     */
+    private boolean isWhite(float[] hslColor) {
+        return hslColor[2] >= WHITE_MIN_LIGHTNESS;
+    }
+
+    public void setIsLowPriority(boolean isLowPriority) {
+        mIsLowPriority = isLowPriority;
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationBigPictureTemplateViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationBigPictureTemplateViewWrapper.java
new file mode 100644
index 0000000..cf12e94
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationBigPictureTemplateViewWrapper.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.app.Notification;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.service.notification.StatusBarNotification;
+import android.view.View;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+
+/**
+ * Wraps a notification containing a big picture template
+ */
+public class NotificationBigPictureTemplateViewWrapper extends NotificationTemplateViewWrapper {
+
+    protected NotificationBigPictureTemplateViewWrapper(Context ctx, View view,
+            ExpandableNotificationRow row) {
+        super(ctx, view, row);
+    }
+
+    @Override
+    public void onContentUpdated(ExpandableNotificationRow row) {
+        super.onContentUpdated(row);
+        updateImageTag(row.getStatusBarNotification());
+    }
+
+    private void updateImageTag(StatusBarNotification notification) {
+        final Bundle extras = notification.getNotification().extras;
+        Icon overRiddenIcon = extras.getParcelable(Notification.EXTRA_LARGE_ICON_BIG);
+        if (overRiddenIcon != null) {
+            mPicture.setTag(ImageTransformState.ICON_TAG, overRiddenIcon);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationBigTextTemplateViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationBigTextTemplateViewWrapper.java
new file mode 100644
index 0000000..20a3d8f
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationBigTextTemplateViewWrapper.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.content.Context;
+import android.service.notification.StatusBarNotification;
+import android.view.View;
+
+import com.android.internal.widget.ImageFloatingTextView;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.TransformableView;
+
+/**
+ * Wraps a notification containing a big text template
+ */
+public class NotificationBigTextTemplateViewWrapper extends NotificationTemplateViewWrapper {
+
+    private ImageFloatingTextView mBigtext;
+
+    protected NotificationBigTextTemplateViewWrapper(Context ctx, View view,
+            ExpandableNotificationRow row) {
+        super(ctx, view, row);
+    }
+
+    private void resolveViews(StatusBarNotification notification) {
+        mBigtext = (ImageFloatingTextView) mView.findViewById(com.android.internal.R.id.big_text);
+    }
+
+    @Override
+    public void onContentUpdated(ExpandableNotificationRow row) {
+        // Reinspect the notification. Before the super call, because the super call also updates
+        // the transformation types and we need to have our values set by then.
+        resolveViews(row.getStatusBarNotification());
+        super.onContentUpdated(row);
+    }
+
+    @Override
+    protected void updateTransformedTypes() {
+        // This also clears the existing types
+        super.updateTransformedTypes();
+        if (mBigtext != null) {
+            mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TEXT,
+                    mBigtext);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationCustomViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationCustomViewWrapper.java
new file mode 100644
index 0000000..bca4b43
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationCustomViewWrapper.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2014 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.notification;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.view.View;
+
+import com.android.systemui.R;
+import com.android.systemui.ViewInvertHelper;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.phone.NotificationPanelView;
+
+/**
+ * Wraps a notification containing a custom view.
+ */
+public class NotificationCustomViewWrapper extends NotificationViewWrapper {
+
+    private final ViewInvertHelper mInvertHelper;
+    private final Paint mGreyPaint = new Paint();
+    private boolean mIsLegacy;
+    private int mLegacyColor;
+
+    protected NotificationCustomViewWrapper(Context ctx, View view, ExpandableNotificationRow row) {
+        super(ctx, view, row);
+        mInvertHelper = new ViewInvertHelper(view, NotificationPanelView.DOZE_ANIMATION_DURATION);
+        mLegacyColor = row.getContext().getColor(R.color.notification_legacy_background_color);
+    }
+
+    @Override
+    public void setDark(boolean dark, boolean fade, long delay) {
+        if (dark == mDark && mDarkInitialized) {
+            return;
+        }
+        super.setDark(dark, fade, delay);
+        if (!mIsLegacy && mShouldInvertDark) {
+            if (fade) {
+                mInvertHelper.fade(dark, delay);
+            } else {
+                mInvertHelper.update(dark);
+            }
+        } else {
+            mView.setLayerType(dark ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE, null);
+            if (fade) {
+                fadeGrayscale(dark, delay);
+            } else {
+                updateGrayscale(dark);
+            }
+        }
+    }
+
+    protected void fadeGrayscale(final boolean dark, long delay) {
+        getDozer().startIntensityAnimation(animation -> {
+            getDozer().updateGrayscaleMatrix((float) animation.getAnimatedValue());
+            mGreyPaint.setColorFilter(
+                    new ColorMatrixColorFilter(getDozer().getGrayscaleColorMatrix()));
+            mView.setLayerPaint(mGreyPaint);
+        }, dark, delay, new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (!dark) {
+                    mView.setLayerType(View.LAYER_TYPE_NONE, null);
+                }
+            }
+        });
+    }
+
+    protected void updateGrayscale(boolean dark) {
+        if (dark) {
+            getDozer().updateGrayscaleMatrix(1f);
+            mGreyPaint.setColorFilter(
+                    new ColorMatrixColorFilter(getDozer().getGrayscaleColorMatrix()));
+            mView.setLayerPaint(mGreyPaint);
+        }
+    }
+
+    @Override
+    public void setVisible(boolean visible) {
+        super.setVisible(visible);
+        mView.setAlpha(visible ? 1.0f : 0.0f);
+    }
+
+    @Override
+    protected boolean shouldClearBackgroundOnReapply() {
+        return false;
+    }
+
+    @Override
+    public int getCustomBackgroundColor() {
+        int customBackgroundColor = super.getCustomBackgroundColor();
+        if (customBackgroundColor == 0 && mIsLegacy) {
+            return mLegacyColor;
+        }
+        return customBackgroundColor;
+    }
+
+    public void setLegacy(boolean legacy) {
+        super.setLegacy(legacy);
+        mIsLegacy = legacy;
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationDozeHelper.java b/com/android/systemui/statusbar/notification/NotificationDozeHelper.java
new file mode 100644
index 0000000..0b3b3cb
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationDozeHelper.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.widget.ImageView;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.statusbar.phone.NotificationPanelView;
+
+import java.util.function.Consumer;
+
+public class NotificationDozeHelper {
+    private final ColorMatrix mGrayscaleColorMatrix = new ColorMatrix();
+
+    public void fadeGrayscale(final ImageView target, final boolean dark, long delay) {
+        startIntensityAnimation(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                updateGrayscale(target, (float) animation.getAnimatedValue());
+            }
+        }, dark, delay, new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (!dark) {
+                    target.setColorFilter(null);
+                }
+            }
+        });
+    }
+
+    public void updateGrayscale(ImageView target, boolean dark) {
+        updateGrayscale(target, dark ? 1 : 0);
+    }
+
+    public void updateGrayscale(ImageView target, float darkAmount) {
+        if (darkAmount > 0) {
+            updateGrayscaleMatrix(darkAmount);
+            target.setColorFilter(new ColorMatrixColorFilter(mGrayscaleColorMatrix));
+        } else {
+            target.setColorFilter(null);
+        }
+    }
+
+    public void startIntensityAnimation(ValueAnimator.AnimatorUpdateListener updateListener,
+            boolean dark, long delay, Animator.AnimatorListener listener) {
+        float startIntensity = dark ? 0f : 1f;
+        float endIntensity = dark ? 1f : 0f;
+        ValueAnimator animator = ValueAnimator.ofFloat(startIntensity, endIntensity);
+        animator.addUpdateListener(updateListener);
+        animator.setDuration(NotificationPanelView.DOZE_ANIMATION_DURATION);
+        animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+        animator.setStartDelay(delay);
+        if (listener != null) {
+            animator.addListener(listener);
+        }
+        animator.start();
+    }
+
+    public void setIntensityDark(Consumer<Float> listener, boolean dark,
+            boolean animate, long delay) {
+        if (animate) {
+            startIntensityAnimation(a -> listener.accept((Float) a.getAnimatedValue()), dark, delay,
+                    null /* listener */);
+        } else {
+            listener.accept(dark ? 1f : 0f);
+        }
+    }
+
+    public void updateGrayscaleMatrix(float intensity) {
+        mGrayscaleColorMatrix.setSaturation(1 - intensity);
+    }
+
+    public ColorMatrix getGrayscaleColorMatrix() {
+        return mGrayscaleColorMatrix;
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationHeaderViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationHeaderViewWrapper.java
new file mode 100644
index 0000000..b95b8a3
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationHeaderViewWrapper.java
@@ -0,0 +1,281 @@
+/*
+ * 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.notification;
+
+import android.app.Notification;
+import android.content.Context;
+import android.graphics.ColorFilter;
+import android.graphics.PorterDuffColorFilter;
+import android.util.ArraySet;
+import android.view.NotificationHeaderView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.internal.widget.NotificationExpandButton;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.ViewInvertHelper;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.TransformableView;
+import com.android.systemui.statusbar.ViewTransformationHelper;
+import com.android.systemui.statusbar.phone.NotificationPanelView;
+
+import java.util.Stack;
+
+import static com.android.systemui.statusbar.notification.TransformState.TRANSFORM_Y;
+
+/**
+ * Wraps a notification header view.
+ */
+public class NotificationHeaderViewWrapper extends NotificationViewWrapper {
+
+    private static final Interpolator LOW_PRIORITY_HEADER_CLOSE
+            = new PathInterpolator(0.4f, 0f, 0.7f, 1f);
+
+    protected final ViewInvertHelper mInvertHelper;
+    protected final ViewTransformationHelper mTransformationHelper;
+
+    protected int mColor;
+    private ImageView mIcon;
+
+    private NotificationExpandButton mExpandButton;
+    private NotificationHeaderView mNotificationHeader;
+    private TextView mHeaderText;
+    private ImageView mWorkProfileImage;
+    private boolean mIsLowPriority;
+    private boolean mTransformLowPriorityTitle;
+    private boolean mShowExpandButtonAtEnd;
+
+    protected NotificationHeaderViewWrapper(Context ctx, View view, ExpandableNotificationRow row) {
+        super(ctx, view, row);
+        mShowExpandButtonAtEnd = ctx.getResources().getBoolean(
+                R.bool.config_showNotificationExpandButtonAtEnd);
+        mInvertHelper = new ViewInvertHelper(ctx, NotificationPanelView.DOZE_ANIMATION_DURATION);
+        mTransformationHelper = new ViewTransformationHelper();
+
+        // we want to avoid that the header clashes with the other text when transforming
+        // low-priority
+        mTransformationHelper.setCustomTransformation(
+                new CustomInterpolatorTransformation(TRANSFORMING_VIEW_TITLE) {
+
+                    @Override
+                    public Interpolator getCustomInterpolator(int interpolationType,
+                            boolean isFrom) {
+                        boolean isLowPriority = mView instanceof NotificationHeaderView;
+                        if (interpolationType == TRANSFORM_Y) {
+                            if (isLowPriority && !isFrom
+                                    || !isLowPriority && isFrom) {
+                                return Interpolators.LINEAR_OUT_SLOW_IN;
+                            } else {
+                                return LOW_PRIORITY_HEADER_CLOSE;
+                            }
+                        }
+                        return null;
+                    }
+
+                    @Override
+                    protected boolean hasCustomTransformation() {
+                        return mIsLowPriority && mTransformLowPriorityTitle;
+                    }
+                }, TRANSFORMING_VIEW_TITLE);
+        resolveHeaderViews();
+        updateInvertHelper();
+    }
+
+    @Override
+    protected NotificationDozeHelper createDozer(Context ctx) {
+        return new NotificationIconDozeHelper(ctx);
+    }
+
+    @Override
+    protected NotificationIconDozeHelper getDozer() {
+        return (NotificationIconDozeHelper) super.getDozer();
+    }
+
+    protected void resolveHeaderViews() {
+        mIcon = mView.findViewById(com.android.internal.R.id.icon);
+        mHeaderText = mView.findViewById(com.android.internal.R.id.header_text);
+        mExpandButton = mView.findViewById(com.android.internal.R.id.expand_button);
+        mWorkProfileImage = mView.findViewById(com.android.internal.R.id.profile_badge);
+        mColor = resolveColor(mExpandButton);
+        mNotificationHeader = mView.findViewById(com.android.internal.R.id.notification_header);
+        mNotificationHeader.setShowExpandButtonAtEnd(mShowExpandButtonAtEnd);
+        getDozer().setColor(mColor);
+    }
+
+    private int resolveColor(ImageView icon) {
+        if (icon != null && icon.getDrawable() != null) {
+            ColorFilter filter = icon.getDrawable().getColorFilter();
+            if (filter instanceof PorterDuffColorFilter) {
+                return ((PorterDuffColorFilter) filter).getColor();
+            }
+        }
+        return 0;
+    }
+
+    @Override
+    public void onContentUpdated(ExpandableNotificationRow row) {
+        super.onContentUpdated(row);
+        mIsLowPriority = row.isLowPriority();
+        mTransformLowPriorityTitle = !row.isChildInGroup() && !row.isSummaryWithChildren();
+        ArraySet<View> previousViews = mTransformationHelper.getAllTransformingViews();
+
+        // Reinspect the notification.
+        resolveHeaderViews();
+        updateInvertHelper();
+        updateTransformedTypes();
+        addRemainingTransformTypes();
+        updateCropToPaddingForImageViews();
+        Notification notification = row.getStatusBarNotification().getNotification();
+        mIcon.setTag(ImageTransformState.ICON_TAG, notification.getSmallIcon());
+        // The work profile image is always the same lets just set the icon tag for it not to
+        // animate
+        mWorkProfileImage.setTag(ImageTransformState.ICON_TAG, notification.getSmallIcon());
+
+        // We need to reset all views that are no longer transforming in case a view was previously
+        // transformed, but now we decided to transform its container instead.
+        ArraySet<View> currentViews = mTransformationHelper.getAllTransformingViews();
+        for (int i = 0; i < previousViews.size(); i++) {
+            View view = previousViews.valueAt(i);
+            if (!currentViews.contains(view)) {
+                mTransformationHelper.resetTransformedView(view);
+            }
+        }
+    }
+
+    /**
+     * Adds the remaining TransformTypes to the TransformHelper. This is done to make sure that each
+     * child is faded automatically and doesn't have to be manually added.
+     * The keys used for the views are the ids.
+     */
+    private void addRemainingTransformTypes() {
+        mTransformationHelper.addRemainingTransformTypes(mView);
+    }
+
+    /**
+     * Since we are deactivating the clipping when transforming the ImageViews don't get clipped
+     * anymore during these transitions. We can avoid that by using
+     * {@link ImageView#setCropToPadding(boolean)} on all ImageViews.
+     */
+    private void updateCropToPaddingForImageViews() {
+        Stack<View> stack = new Stack<>();
+        stack.push(mView);
+        while (!stack.isEmpty()) {
+            View child = stack.pop();
+            if (child instanceof ImageView) {
+                ((ImageView) child).setCropToPadding(true);
+            } else if (child instanceof ViewGroup){
+                ViewGroup group = (ViewGroup) child;
+                for (int i = 0; i < group.getChildCount(); i++) {
+                    stack.push(group.getChildAt(i));
+                }
+            }
+        }
+    }
+
+    protected void updateInvertHelper() {
+        mInvertHelper.clearTargets();
+        for (int i = 0; i < mNotificationHeader.getChildCount(); i++) {
+            View child = mNotificationHeader.getChildAt(i);
+            if (child != mIcon) {
+                mInvertHelper.addTarget(child);
+            }
+        }
+    }
+
+    protected void updateTransformedTypes() {
+        mTransformationHelper.reset();
+        mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_ICON, mIcon);
+        if (mIsLowPriority) {
+            mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TITLE,
+                    mHeaderText);
+        }
+    }
+
+    @Override
+    public void setDark(boolean dark, boolean fade, long delay) {
+        if (dark == mDark && mDarkInitialized) {
+            return;
+        }
+        super.setDark(dark, fade, delay);
+        if (fade) {
+            mInvertHelper.fade(dark, delay);
+        } else {
+            mInvertHelper.update(dark);
+        }
+        if (mIcon != null && !mRow.isChildInGroup()) {
+            // We don't update the color for children views / their icon is invisible anyway.
+            // It also may lead to bugs where the icon isn't correctly greyed out.
+            boolean hadColorFilter = mNotificationHeader.getOriginalIconColor()
+                    != NotificationHeaderView.NO_COLOR;
+
+            getDozer().setImageDark(mIcon, dark, fade, delay, !hadColorFilter);
+        }
+    }
+
+    @Override
+    public void updateExpandability(boolean expandable, View.OnClickListener onClickListener) {
+        mExpandButton.setVisibility(expandable ? View.VISIBLE : View.GONE);
+        mNotificationHeader.setOnClickListener(expandable ? onClickListener : null);
+    }
+
+    @Override
+    public NotificationHeaderView getNotificationHeader() {
+        return mNotificationHeader;
+    }
+
+    @Override
+    public TransformState getCurrentState(int fadingView) {
+        return mTransformationHelper.getCurrentState(fadingView);
+    }
+
+    @Override
+    public void transformTo(TransformableView notification, Runnable endRunnable) {
+        mTransformationHelper.transformTo(notification, endRunnable);
+    }
+
+    @Override
+    public void transformTo(TransformableView notification, float transformationAmount) {
+        mTransformationHelper.transformTo(notification, transformationAmount);
+    }
+
+    @Override
+    public void transformFrom(TransformableView notification) {
+        mTransformationHelper.transformFrom(notification);
+    }
+
+    @Override
+    public void transformFrom(TransformableView notification, float transformationAmount) {
+        mTransformationHelper.transformFrom(notification, transformationAmount);
+    }
+
+    @Override
+    public void setIsChildInGroup(boolean isChildInGroup) {
+        super.setIsChildInGroup(isChildInGroup);
+        mTransformLowPriorityTitle = !isChildInGroup;
+    }
+
+    @Override
+    public void setVisible(boolean visible) {
+        super.setVisible(visible);
+        mTransformationHelper.setVisible(visible);
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationIconDozeHelper.java b/com/android/systemui/statusbar/notification/NotificationIconDozeHelper.java
new file mode 100644
index 0000000..9f79ef2
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationIconDozeHelper.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.Drawable;
+import android.widget.ImageView;
+
+import com.android.systemui.R;
+
+public class NotificationIconDozeHelper extends NotificationDozeHelper {
+
+    private final int mImageDarkAlpha;
+    private final int mImageDarkColor = 0xffffffff;
+    private final PorterDuffColorFilter mImageColorFilter = new PorterDuffColorFilter(
+            0, PorterDuff.Mode.SRC_ATOP);
+
+    private int mColor = Color.BLACK;
+
+    public NotificationIconDozeHelper(Context ctx) {
+        mImageDarkAlpha = ctx.getResources().getInteger(R.integer.doze_small_icon_alpha);
+    }
+
+    public void setColor(int color) {
+        mColor = color;
+    }
+
+    public void setImageDark(ImageView target, boolean dark, boolean fade, long delay,
+            boolean useGrayscale) {
+        if (fade) {
+            if (!useGrayscale) {
+                fadeImageColorFilter(target, dark, delay);
+                fadeImageAlpha(target, dark, delay);
+            } else {
+                fadeGrayscale(target, dark, delay);
+            }
+        } else {
+            if (!useGrayscale) {
+                updateImageColorFilter(target, dark);
+                updateImageAlpha(target, dark);
+            } else {
+                updateGrayscale(target, dark);
+            }
+        }
+    }
+
+    private void fadeImageColorFilter(final ImageView target, boolean dark, long delay) {
+        startIntensityAnimation(animation -> {
+            updateImageColorFilter(target, (Float) animation.getAnimatedValue());
+        }, dark, delay, null /* listener */);
+    }
+
+    private void fadeImageAlpha(final ImageView target, boolean dark, long delay) {
+        startIntensityAnimation(animation -> {
+            float t = (float) animation.getAnimatedValue();
+            target.setImageAlpha((int) (255 * (1f - t) + mImageDarkAlpha * t));
+        }, dark, delay, null /* listener */);
+    }
+
+    private void updateImageColorFilter(ImageView target, boolean dark) {
+        updateImageColorFilter(target, dark ? 1f : 0f);
+    }
+
+    private void updateImageColorFilter(ImageView target, float intensity) {
+        int color = NotificationUtils.interpolateColors(mColor, mImageDarkColor, intensity);
+        mImageColorFilter.setColor(color);
+        Drawable imageDrawable = target.getDrawable();
+
+        // Also, the notification might have been modified during the animation, so background
+        // might be null here.
+        if (imageDrawable != null) {
+            Drawable d = imageDrawable.mutate();
+            // DrawableContainer ignores the color filter if it's already set, so clear it first to
+            // get it set and invalidated properly.
+            d.setColorFilter(null);
+            d.setColorFilter(mImageColorFilter);
+        }
+    }
+
+    private void updateImageAlpha(ImageView target, boolean dark) {
+        target.setImageAlpha(dark ? mImageDarkAlpha : 255);
+    }
+
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationInflater.java b/com/android/systemui/statusbar/notification/NotificationInflater.java
new file mode 100644
index 0000000..f967118
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationInflater.java
@@ -0,0 +1,711 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.CancellationSignal;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+import android.view.View;
+import android.widget.RemoteViews;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.InflationTask;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationContentView;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.util.Assert;
+
+import java.util.HashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A utility that inflates the right kind of contentView based on the state
+ */
+public class NotificationInflater {
+
+    public static final String TAG = "NotificationInflater";
+    @VisibleForTesting
+    static final int FLAG_REINFLATE_ALL = ~0;
+    private static final int FLAG_REINFLATE_CONTENT_VIEW = 1<<0;
+    @VisibleForTesting
+    static final int FLAG_REINFLATE_EXPANDED_VIEW = 1<<1;
+    private static final int FLAG_REINFLATE_HEADS_UP_VIEW = 1<<2;
+    private static final int FLAG_REINFLATE_PUBLIC_VIEW = 1<<3;
+    private static final int FLAG_REINFLATE_AMBIENT_VIEW = 1<<4;
+    private static final InflationExecutor EXECUTOR = new InflationExecutor();
+
+    private final ExpandableNotificationRow mRow;
+    private boolean mIsLowPriority;
+    private boolean mUsesIncreasedHeight;
+    private boolean mUsesIncreasedHeadsUpHeight;
+    private RemoteViews.OnClickHandler mRemoteViewClickHandler;
+    private boolean mIsChildInGroup;
+    private InflationCallback mCallback;
+    private boolean mRedactAmbient;
+
+    public NotificationInflater(ExpandableNotificationRow row) {
+        mRow = row;
+    }
+
+    public void setIsLowPriority(boolean isLowPriority) {
+        mIsLowPriority = isLowPriority;
+    }
+
+    /**
+     * Set whether the notification is a child in a group
+     *
+     * @return whether the view was re-inflated
+     */
+    public void setIsChildInGroup(boolean childInGroup) {
+        if (childInGroup != mIsChildInGroup) {
+            mIsChildInGroup = childInGroup;
+            if (mIsLowPriority) {
+                int flags = FLAG_REINFLATE_CONTENT_VIEW | FLAG_REINFLATE_EXPANDED_VIEW;
+                inflateNotificationViews(flags);
+            }
+        } ;
+    }
+
+    public void setUsesIncreasedHeight(boolean usesIncreasedHeight) {
+        mUsesIncreasedHeight = usesIncreasedHeight;
+    }
+
+    public void setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight) {
+        mUsesIncreasedHeadsUpHeight = usesIncreasedHeight;
+    }
+
+    public void setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler) {
+        mRemoteViewClickHandler = remoteViewClickHandler;
+    }
+
+    public void setRedactAmbient(boolean redactAmbient) {
+        if (mRedactAmbient != redactAmbient) {
+            mRedactAmbient = redactAmbient;
+            if (mRow.getEntry() == null) {
+                return;
+            }
+            inflateNotificationViews(FLAG_REINFLATE_AMBIENT_VIEW);
+        }
+    }
+
+    /**
+     * Inflate all views of this notification on a background thread. This is asynchronous and will
+     * notify the callback once it's finished.
+     */
+    public void inflateNotificationViews() {
+        inflateNotificationViews(FLAG_REINFLATE_ALL);
+    }
+
+    /**
+     * Reinflate all views for the specified flags on a background thread. This is asynchronous and
+     * will notify the callback once it's finished.
+     *
+     * @param reInflateFlags flags which views should be reinflated. Use {@link #FLAG_REINFLATE_ALL}
+     *                       to reinflate all of views.
+     */
+    @VisibleForTesting
+    void inflateNotificationViews(int reInflateFlags) {
+        if (mRow.isRemoved()) {
+            // We don't want to reinflate anything for removed notifications. Otherwise views might
+            // be readded to the stack, leading to leaks. This may happen with low-priority groups
+            // where the removal of already removed children can lead to a reinflation.
+            return;
+        }
+        StatusBarNotification sbn = mRow.getEntry().notification;
+        new AsyncInflationTask(sbn, reInflateFlags, mRow, mIsLowPriority,
+                mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient,
+                mCallback, mRemoteViewClickHandler).execute();
+    }
+
+    @VisibleForTesting
+    InflationProgress inflateNotificationViews(int reInflateFlags,
+            Notification.Builder builder, Context packageContext) {
+        InflationProgress result = createRemoteViews(reInflateFlags, builder, mIsLowPriority,
+                mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight,
+                mRedactAmbient, packageContext);
+        apply(result, reInflateFlags, mRow, mRedactAmbient, mRemoteViewClickHandler, null);
+        return result;
+    }
+
+    private static InflationProgress createRemoteViews(int reInflateFlags,
+            Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup,
+            boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient,
+            Context packageContext) {
+        InflationProgress result = new InflationProgress();
+        isLowPriority = isLowPriority && !isChildInGroup;
+        if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) {
+            result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight);
+        }
+
+        if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) {
+            result.newExpandedView = createExpandedView(builder, isLowPriority);
+        }
+
+        if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) {
+            result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight);
+        }
+
+        if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) {
+            result.newPublicView = builder.makePublicContentView();
+        }
+
+        if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) {
+            result.newAmbientView = redactAmbient ? builder.makePublicAmbientNotification()
+                    : builder.makeAmbientNotification();
+        }
+        result.packageContext = packageContext;
+        return result;
+    }
+
+    public static CancellationSignal apply(InflationProgress result, int reInflateFlags,
+            ExpandableNotificationRow row, boolean redactAmbient,
+            RemoteViews.OnClickHandler remoteViewClickHandler,
+            @Nullable InflationCallback callback) {
+        NotificationData.Entry entry = row.getEntry();
+        NotificationContentView privateLayout = row.getPrivateLayout();
+        NotificationContentView publicLayout = row.getPublicLayout();
+        final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>();
+
+        int flag = FLAG_REINFLATE_CONTENT_VIEW;
+        if ((reInflateFlags & flag) != 0) {
+            boolean isNewView = !canReapplyRemoteView(result.newContentView, entry.cachedContentView);
+            ApplyCallback applyCallback = new ApplyCallback() {
+                @Override
+                public void setResultView(View v) {
+                    result.inflatedContentView = v;
+                }
+
+                @Override
+                public RemoteViews getRemoteView() {
+                    return result.newContentView;
+                }
+            };
+            applyRemoteView(result, reInflateFlags, flag, row, redactAmbient,
+                    isNewView, remoteViewClickHandler, callback, entry, privateLayout,
+                    privateLayout.getContractedChild(), privateLayout.getVisibleWrapper(
+                            NotificationContentView.VISIBLE_TYPE_CONTRACTED),
+                    runningInflations, applyCallback);
+        }
+
+        flag = FLAG_REINFLATE_EXPANDED_VIEW;
+        if ((reInflateFlags & flag) != 0) {
+            if (result.newExpandedView != null) {
+                boolean isNewView = !canReapplyRemoteView(result.newExpandedView,
+                        entry.cachedBigContentView);
+                ApplyCallback applyCallback = new ApplyCallback() {
+                    @Override
+                    public void setResultView(View v) {
+                        result.inflatedExpandedView = v;
+                    }
+
+                    @Override
+                    public RemoteViews getRemoteView() {
+                        return result.newExpandedView;
+                    }
+                };
+                applyRemoteView(result, reInflateFlags, flag, row,
+                        redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
+                        privateLayout, privateLayout.getExpandedChild(),
+                        privateLayout.getVisibleWrapper(
+                                NotificationContentView.VISIBLE_TYPE_EXPANDED), runningInflations,
+                        applyCallback);
+            }
+        }
+
+        flag = FLAG_REINFLATE_HEADS_UP_VIEW;
+        if ((reInflateFlags & flag) != 0) {
+            if (result.newHeadsUpView != null) {
+                boolean isNewView = !canReapplyRemoteView(result.newHeadsUpView,
+                        entry.cachedHeadsUpContentView);
+                ApplyCallback applyCallback = new ApplyCallback() {
+                    @Override
+                    public void setResultView(View v) {
+                        result.inflatedHeadsUpView = v;
+                    }
+
+                    @Override
+                    public RemoteViews getRemoteView() {
+                        return result.newHeadsUpView;
+                    }
+                };
+                applyRemoteView(result, reInflateFlags, flag, row,
+                        redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
+                        privateLayout, privateLayout.getHeadsUpChild(),
+                        privateLayout.getVisibleWrapper(
+                                NotificationContentView.VISIBLE_TYPE_HEADSUP), runningInflations,
+                        applyCallback);
+            }
+        }
+
+        flag = FLAG_REINFLATE_PUBLIC_VIEW;
+        if ((reInflateFlags & flag) != 0) {
+            boolean isNewView = !canReapplyRemoteView(result.newPublicView,
+                    entry.cachedPublicContentView);
+            ApplyCallback applyCallback = new ApplyCallback() {
+                @Override
+                public void setResultView(View v) {
+                    result.inflatedPublicView = v;
+                }
+
+                @Override
+                public RemoteViews getRemoteView() {
+                    return result.newPublicView;
+                }
+            };
+            applyRemoteView(result, reInflateFlags, flag, row,
+                    redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
+                    publicLayout, publicLayout.getContractedChild(),
+                    publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED),
+                    runningInflations, applyCallback);
+        }
+
+        flag = FLAG_REINFLATE_AMBIENT_VIEW;
+        if ((reInflateFlags & flag) != 0) {
+            NotificationContentView newParent = redactAmbient ? publicLayout : privateLayout;
+            boolean isNewView = !canReapplyAmbient(row, redactAmbient) ||
+                    !canReapplyRemoteView(result.newAmbientView, entry.cachedAmbientContentView);
+            ApplyCallback applyCallback = new ApplyCallback() {
+                @Override
+                public void setResultView(View v) {
+                    result.inflatedAmbientView = v;
+                }
+
+                @Override
+                public RemoteViews getRemoteView() {
+                    return result.newAmbientView;
+                }
+            };
+            applyRemoteView(result, reInflateFlags, flag, row,
+                    redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
+                    newParent, newParent.getAmbientChild(), newParent.getVisibleWrapper(
+                            NotificationContentView.VISIBLE_TYPE_AMBIENT), runningInflations,
+                    applyCallback);
+        }
+
+        // Let's try to finish, maybe nobody is even inflating anything
+        finishIfDone(result, reInflateFlags, runningInflations, callback, row,
+                redactAmbient);
+        CancellationSignal cancellationSignal = new CancellationSignal();
+        cancellationSignal.setOnCancelListener(
+                () -> runningInflations.values().forEach(CancellationSignal::cancel));
+        return cancellationSignal;
+    }
+
+    @VisibleForTesting
+    static void applyRemoteView(final InflationProgress result,
+            final int reInflateFlags, int inflationId,
+            final ExpandableNotificationRow row,
+            final boolean redactAmbient, boolean isNewView,
+            RemoteViews.OnClickHandler remoteViewClickHandler,
+            @Nullable final InflationCallback callback, NotificationData.Entry entry,
+            NotificationContentView parentLayout, View existingView,
+            NotificationViewWrapper existingWrapper,
+            final HashMap<Integer, CancellationSignal> runningInflations,
+            ApplyCallback applyCallback) {
+        RemoteViews newContentView = applyCallback.getRemoteView();
+        RemoteViews.OnViewAppliedListener listener
+                = new RemoteViews.OnViewAppliedListener() {
+
+            @Override
+            public void onViewApplied(View v) {
+                if (isNewView) {
+                    v.setIsRootNamespace(true);
+                    applyCallback.setResultView(v);
+                } else if (existingWrapper != null) {
+                    existingWrapper.onReinflated();
+                }
+                runningInflations.remove(inflationId);
+                finishIfDone(result, reInflateFlags, runningInflations, callback, row,
+                        redactAmbient);
+            }
+
+            @Override
+            public void onError(Exception e) {
+                // Uh oh the async inflation failed. Due to some bugs (see b/38190555), this could
+                // actually also be a system issue, so let's try on the UI thread again to be safe.
+                try {
+                    View newView = existingView;
+                    if (isNewView) {
+                        newView = newContentView.apply(
+                                result.packageContext,
+                                parentLayout,
+                                remoteViewClickHandler);
+                    } else {
+                        newContentView.reapply(
+                                result.packageContext,
+                                existingView,
+                                remoteViewClickHandler);
+                    }
+                    Log.wtf(TAG, "Async Inflation failed but normal inflation finished normally.",
+                            e);
+                    onViewApplied(newView);
+                } catch (Exception anotherException) {
+                    runningInflations.remove(inflationId);
+                    handleInflationError(runningInflations, e, entry.notification, callback);
+                }
+            }
+        };
+        CancellationSignal cancellationSignal;
+        if (isNewView) {
+            cancellationSignal = newContentView.applyAsync(
+                    result.packageContext,
+                    parentLayout,
+                    EXECUTOR,
+                    listener,
+                    remoteViewClickHandler);
+        } else {
+            cancellationSignal = newContentView.reapplyAsync(
+                    result.packageContext,
+                    existingView,
+                    EXECUTOR,
+                    listener,
+                    remoteViewClickHandler);
+        }
+        runningInflations.put(inflationId, cancellationSignal);
+    }
+
+    private static void handleInflationError(HashMap<Integer, CancellationSignal> runningInflations,
+            Exception e, StatusBarNotification notification, @Nullable InflationCallback callback) {
+        Assert.isMainThread();
+        runningInflations.values().forEach(CancellationSignal::cancel);
+        if (callback != null) {
+            callback.handleInflationException(notification, e);
+        }
+    }
+
+    /**
+     * Finish the inflation of the views
+     *
+     * @return true if the inflation was finished
+     */
+    private static boolean finishIfDone(InflationProgress result, int reInflateFlags,
+            HashMap<Integer, CancellationSignal> runningInflations,
+            @Nullable InflationCallback endListener, ExpandableNotificationRow row,
+            boolean redactAmbient) {
+        Assert.isMainThread();
+        NotificationData.Entry entry = row.getEntry();
+        NotificationContentView privateLayout = row.getPrivateLayout();
+        NotificationContentView publicLayout = row.getPublicLayout();
+        if (runningInflations.isEmpty()) {
+            if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) {
+                if (result.inflatedContentView != null) {
+                    privateLayout.setContractedChild(result.inflatedContentView);
+                }
+                entry.cachedContentView = result.newContentView;
+            }
+
+            if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) {
+                if (result.inflatedExpandedView != null) {
+                    privateLayout.setExpandedChild(result.inflatedExpandedView);
+                } else if (result.newExpandedView == null) {
+                    privateLayout.setExpandedChild(null);
+                }
+                entry.cachedBigContentView = result.newExpandedView;
+                row.setExpandable(result.newExpandedView != null);
+            }
+
+            if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) {
+                if (result.inflatedHeadsUpView != null) {
+                    privateLayout.setHeadsUpChild(result.inflatedHeadsUpView);
+                } else if (result.newHeadsUpView == null) {
+                    privateLayout.setHeadsUpChild(null);
+                }
+                entry.cachedHeadsUpContentView = result.newHeadsUpView;
+            }
+
+            if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) {
+                if (result.inflatedPublicView != null) {
+                    publicLayout.setContractedChild(result.inflatedPublicView);
+                }
+                entry.cachedPublicContentView = result.newPublicView;
+            }
+
+            if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) {
+                if (result.inflatedAmbientView != null) {
+                    NotificationContentView newParent = redactAmbient
+                            ? publicLayout : privateLayout;
+                    NotificationContentView otherParent = !redactAmbient
+                            ? publicLayout : privateLayout;
+                    newParent.setAmbientChild(result.inflatedAmbientView);
+                    otherParent.setAmbientChild(null);
+                }
+                entry.cachedAmbientContentView = result.newAmbientView;
+            }
+            if (endListener != null) {
+                endListener.onAsyncInflationFinished(row.getEntry());
+            }
+            return true;
+        }
+        return false;
+    }
+
+    private static RemoteViews createExpandedView(Notification.Builder builder,
+            boolean isLowPriority) {
+        RemoteViews bigContentView = builder.createBigContentView();
+        if (bigContentView != null) {
+            return bigContentView;
+        }
+        if (isLowPriority) {
+            RemoteViews contentView = builder.createContentView();
+            Notification.Builder.makeHeaderExpanded(contentView);
+            return contentView;
+        }
+        return null;
+    }
+
+    private static RemoteViews createContentView(Notification.Builder builder,
+            boolean isLowPriority, boolean useLarge) {
+        if (isLowPriority) {
+            return builder.makeLowPriorityContentView(false /* useRegularSubtext */);
+        }
+        return builder.createContentView(useLarge);
+    }
+
+    /**
+     * @param newView The new view that will be applied
+     * @param oldView The old view that was applied to the existing view before
+     * @return {@code true} if the RemoteViews are the same and the view can be reused to reapply.
+     */
+     @VisibleForTesting
+     static boolean canReapplyRemoteView(final RemoteViews newView,
+            final RemoteViews oldView) {
+        return (newView == null && oldView == null) ||
+                (newView != null && oldView != null
+                        && oldView.getPackage() != null
+                        && newView.getPackage() != null
+                        && newView.getPackage().equals(oldView.getPackage())
+                        && newView.getLayoutId() == oldView.getLayoutId()
+                        && !oldView.isReapplyDisallowed());
+    }
+
+    public void setInflationCallback(InflationCallback callback) {
+        mCallback = callback;
+    }
+
+    public interface InflationCallback {
+        void handleInflationException(StatusBarNotification notification, Exception e);
+        void onAsyncInflationFinished(NotificationData.Entry entry);
+    }
+
+    public void onDensityOrFontScaleChanged() {
+        NotificationData.Entry entry = mRow.getEntry();
+        entry.cachedAmbientContentView = null;
+        entry.cachedBigContentView = null;
+        entry.cachedContentView = null;
+        entry.cachedHeadsUpContentView = null;
+        entry.cachedPublicContentView = null;
+        inflateNotificationViews();
+    }
+
+    private static boolean canReapplyAmbient(ExpandableNotificationRow row, boolean redactAmbient) {
+        NotificationContentView ambientView = redactAmbient ? row.getPublicLayout()
+                : row.getPrivateLayout();            ;
+        return ambientView.getAmbientChild() != null;
+    }
+
+    public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress>
+            implements InflationCallback, InflationTask {
+
+        private final StatusBarNotification mSbn;
+        private final Context mContext;
+        private final boolean mIsLowPriority;
+        private final boolean mIsChildInGroup;
+        private final boolean mUsesIncreasedHeight;
+        private final InflationCallback mCallback;
+        private final boolean mUsesIncreasedHeadsUpHeight;
+        private final boolean mRedactAmbient;
+        private int mReInflateFlags;
+        private ExpandableNotificationRow mRow;
+        private Exception mError;
+        private RemoteViews.OnClickHandler mRemoteViewClickHandler;
+        private CancellationSignal mCancellationSignal;
+
+        private AsyncInflationTask(StatusBarNotification notification,
+                int reInflateFlags, ExpandableNotificationRow row, boolean isLowPriority,
+                boolean isChildInGroup, boolean usesIncreasedHeight,
+                boolean usesIncreasedHeadsUpHeight, boolean redactAmbient,
+                InflationCallback callback,
+                RemoteViews.OnClickHandler remoteViewClickHandler) {
+            mRow = row;
+            mSbn = notification;
+            mReInflateFlags = reInflateFlags;
+            mContext = mRow.getContext();
+            mIsLowPriority = isLowPriority;
+            mIsChildInGroup = isChildInGroup;
+            mUsesIncreasedHeight = usesIncreasedHeight;
+            mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight;
+            mRedactAmbient = redactAmbient;
+            mRemoteViewClickHandler = remoteViewClickHandler;
+            mCallback = callback;
+            NotificationData.Entry entry = row.getEntry();
+            entry.setInflationTask(this);
+        }
+
+        @VisibleForTesting
+        public int getReInflateFlags() {
+            return mReInflateFlags;
+        }
+
+        @Override
+        protected InflationProgress doInBackground(Void... params) {
+            try {
+                final Notification.Builder recoveredBuilder
+                        = Notification.Builder.recoverBuilder(mContext,
+                        mSbn.getNotification());
+                Context packageContext = mSbn.getPackageContext(mContext);
+                Notification notification = mSbn.getNotification();
+                if (mIsLowPriority) {
+                    int backgroundColor = mContext.getColor(
+                            R.color.notification_material_background_low_priority_color);
+                    recoveredBuilder.setBackgroundColorHint(backgroundColor);
+                }
+                if (notification.isMediaNotification()) {
+                    MediaNotificationProcessor processor = new MediaNotificationProcessor(mContext,
+                            packageContext);
+                    processor.setIsLowPriority(mIsLowPriority);
+                    processor.processNotification(notification, recoveredBuilder);
+                }
+                return createRemoteViews(mReInflateFlags,
+                        recoveredBuilder, mIsLowPriority, mIsChildInGroup,
+                        mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient,
+                        packageContext);
+            } catch (Exception e) {
+                mError = e;
+                return null;
+            }
+        }
+
+        @Override
+        protected void onPostExecute(InflationProgress result) {
+            if (mError == null) {
+                mCancellationSignal = apply(result, mReInflateFlags, mRow, mRedactAmbient,
+                        mRemoteViewClickHandler, this);
+            } else {
+                handleError(mError);
+            }
+        }
+
+        private void handleError(Exception e) {
+            mRow.getEntry().onInflationTaskFinished();
+            StatusBarNotification sbn = mRow.getStatusBarNotification();
+            final String ident = sbn.getPackageName() + "/0x"
+                    + Integer.toHexString(sbn.getId());
+            Log.e(StatusBar.TAG, "couldn't inflate view for notification " + ident, e);
+            mCallback.handleInflationException(sbn,
+                    new InflationException("Couldn't inflate contentViews" + e));
+        }
+
+        @Override
+        public void abort() {
+            cancel(true /* mayInterruptIfRunning */);
+            if (mCancellationSignal != null) {
+                mCancellationSignal.cancel();
+            }
+        }
+
+        @Override
+        public void supersedeTask(InflationTask task) {
+            if (task instanceof AsyncInflationTask) {
+                // We want to inflate all flags of the previous task as well
+                mReInflateFlags |= ((AsyncInflationTask) task).mReInflateFlags;
+            }
+        }
+
+        @Override
+        public void handleInflationException(StatusBarNotification notification, Exception e) {
+            handleError(e);
+        }
+
+        @Override
+        public void onAsyncInflationFinished(NotificationData.Entry entry) {
+            mRow.getEntry().onInflationTaskFinished();
+            mRow.onNotificationUpdated();
+            mCallback.onAsyncInflationFinished(mRow.getEntry());
+        }
+    }
+
+    @VisibleForTesting
+    static class InflationProgress {
+        private RemoteViews newContentView;
+        private RemoteViews newHeadsUpView;
+        private RemoteViews newExpandedView;
+        private RemoteViews newAmbientView;
+        private RemoteViews newPublicView;
+
+        @VisibleForTesting
+        Context packageContext;
+
+        private View inflatedContentView;
+        private View inflatedHeadsUpView;
+        private View inflatedExpandedView;
+        private View inflatedAmbientView;
+        private View inflatedPublicView;
+    }
+
+    @VisibleForTesting
+    abstract static class ApplyCallback {
+        public abstract void setResultView(View v);
+        public abstract RemoteViews getRemoteView();
+    }
+
+    /**
+     * A custom executor that allows more tasks to be queued. Default values are copied from
+     * AsyncTask
+      */
+    private static class InflationExecutor implements Executor {
+        private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
+        // We want at least 2 threads and at most 4 threads in the core pool,
+        // preferring to have 1 less than the CPU count to avoid saturating
+        // the CPU with background work
+        private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
+        private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
+        private static final int KEEP_ALIVE_SECONDS = 30;
+
+        private static final ThreadFactory sThreadFactory = new ThreadFactory() {
+            private final AtomicInteger mCount = new AtomicInteger(1);
+
+            public Thread newThread(Runnable r) {
+                return new Thread(r, "InflaterThread #" + mCount.getAndIncrement());
+            }
+        };
+
+        private final ThreadPoolExecutor mExecutor;
+
+        private InflationExecutor() {
+            mExecutor = new ThreadPoolExecutor(
+                    CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
+                    new LinkedBlockingQueue<>(), sThreadFactory);
+            mExecutor.allowCoreThreadTimeOut(true);
+        }
+
+        @Override
+        public void execute(Runnable runnable) {
+            mExecutor.execute(runnable);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationMediaTemplateViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationMediaTemplateViewWrapper.java
new file mode 100644
index 0000000..eb211a1
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationMediaTemplateViewWrapper.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.content.Context;
+import android.view.View;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.TransformableView;
+
+/**
+ * Wraps a notification containing a media template
+ */
+public class NotificationMediaTemplateViewWrapper extends NotificationTemplateViewWrapper {
+
+    protected NotificationMediaTemplateViewWrapper(Context ctx, View view,
+            ExpandableNotificationRow row) {
+        super(ctx, view, row);
+    }
+
+    View mActions;
+
+    private void resolveViews() {
+        mActions = mView.findViewById(com.android.internal.R.id.media_actions);
+    }
+
+    @Override
+    public void onContentUpdated(ExpandableNotificationRow row) {
+        // Reinspect the notification. Before the super call, because the super call also updates
+        // the transformation types and we need to have our values set by then.
+        resolveViews();
+        super.onContentUpdated(row);
+    }
+
+    @Override
+    protected void updateTransformedTypes() {
+        // This also clears the existing types
+        super.updateTransformedTypes();
+        if (mActions != null) {
+            mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_ACTIONS,
+                    mActions);
+        }
+    }
+
+    @Override
+    public boolean isDimmable() {
+        return false;
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationMessagingTemplateViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationMessagingTemplateViewWrapper.java
new file mode 100644
index 0000000..f6ee1ca
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationMessagingTemplateViewWrapper.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import com.android.internal.widget.MessagingLinearLayout;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.TransformableView;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+
+/**
+ * Wraps a notification containing a messaging template
+ */
+public class NotificationMessagingTemplateViewWrapper extends NotificationTemplateViewWrapper {
+
+    private View mContractedMessage;
+    private ArrayList<View> mHistoricMessages = new ArrayList<View>();
+
+    protected NotificationMessagingTemplateViewWrapper(Context ctx, View view,
+            ExpandableNotificationRow row) {
+        super(ctx, view, row);
+    }
+
+    private void resolveViews() {
+        mContractedMessage = null;
+
+        View container = mView.findViewById(com.android.internal.R.id.notification_messaging);
+        if (container instanceof MessagingLinearLayout
+                && ((MessagingLinearLayout) container).getChildCount() > 0) {
+            MessagingLinearLayout messagingContainer = (MessagingLinearLayout) container;
+
+            int childCount = messagingContainer.getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                View child = messagingContainer.getChildAt(i);
+
+                if (child.getVisibility() == View.GONE
+                        && child instanceof TextView
+                        && !TextUtils.isEmpty(((TextView) child).getText())) {
+                    mHistoricMessages.add(child);
+                }
+
+                // Only consider the first visible child - transforming to a position other than the
+                // first looks bad because we have to move across other messages that are fading in.
+                if (child.getId() == messagingContainer.getContractedChildId()) {
+                    mContractedMessage = child;
+                } else if (child.getVisibility() == View.VISIBLE) {
+                    break;
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onContentUpdated(ExpandableNotificationRow row) {
+        // Reinspect the notification. Before the super call, because the super call also updates
+        // the transformation types and we need to have our values set by then.
+        resolveViews();
+        super.onContentUpdated(row);
+    }
+
+    @Override
+    protected void updateTransformedTypes() {
+        // This also clears the existing types
+        super.updateTransformedTypes();
+        if (mContractedMessage != null) {
+            mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TEXT,
+                    mContractedMessage);
+        }
+    }
+
+    @Override
+    public void setRemoteInputVisible(boolean visible) {
+        for (int i = 0; i < mHistoricMessages.size(); i++) {
+            mHistoricMessages.get(i).setVisibility(visible ? View.VISIBLE : View.GONE);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
new file mode 100644
index 0000000..bb979eb
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2014 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.notification;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.service.notification.StatusBarNotification;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.TransformableView;
+import com.android.systemui.statusbar.ViewTransformationHelper;
+
+/**
+ * Wraps a notification view inflated from a template.
+ */
+public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapper {
+
+    private static final int mDarkProgressTint = 0xffffffff;
+
+    protected ImageView mPicture;
+    private ProgressBar mProgressBar;
+    private TextView mTitle;
+    private TextView mText;
+    private View mActionsContainer;
+    private View mReplyAction;
+    private Rect mTmpRect = new Rect();
+
+    private int mContentHeight;
+    private int mMinHeightHint;
+
+    protected NotificationTemplateViewWrapper(Context ctx, View view,
+            ExpandableNotificationRow row) {
+        super(ctx, view, row);
+        mTransformationHelper.setCustomTransformation(
+                new ViewTransformationHelper.CustomTransformation() {
+                    @Override
+                    public boolean transformTo(TransformState ownState,
+                            TransformableView notification, final float transformationAmount) {
+                        if (!(notification instanceof HybridNotificationView)) {
+                            return false;
+                        }
+                        TransformState otherState = notification.getCurrentState(
+                                TRANSFORMING_VIEW_TITLE);
+                        final View text = ownState.getTransformedView();
+                        CrossFadeHelper.fadeOut(text, transformationAmount);
+                        if (otherState != null) {
+                            ownState.transformViewVerticalTo(otherState, this,
+                                    transformationAmount);
+                            otherState.recycle();
+                        }
+                        return true;
+                    }
+
+                    @Override
+                    public boolean customTransformTarget(TransformState ownState,
+                            TransformState otherState) {
+                        float endY = getTransformationY(ownState, otherState);
+                        ownState.setTransformationEndY(endY);
+                        return true;
+                    }
+
+                    @Override
+                    public boolean transformFrom(TransformState ownState,
+                            TransformableView notification, float transformationAmount) {
+                        if (!(notification instanceof HybridNotificationView)) {
+                            return false;
+                        }
+                        TransformState otherState = notification.getCurrentState(
+                                TRANSFORMING_VIEW_TITLE);
+                        final View text = ownState.getTransformedView();
+                        CrossFadeHelper.fadeIn(text, transformationAmount);
+                        if (otherState != null) {
+                            ownState.transformViewVerticalFrom(otherState, this,
+                                    transformationAmount);
+                            otherState.recycle();
+                        }
+                        return true;
+                    }
+
+                    @Override
+                    public boolean initTransformation(TransformState ownState,
+                            TransformState otherState) {
+                        float startY = getTransformationY(ownState, otherState);
+                        ownState.setTransformationStartY(startY);
+                        return true;
+                    }
+
+                    private float getTransformationY(TransformState ownState,
+                            TransformState otherState) {
+                        int[] otherStablePosition = otherState.getLaidOutLocationOnScreen();
+                        int[] ownStablePosition = ownState.getLaidOutLocationOnScreen();
+                        return (otherStablePosition[1]
+                                + otherState.getTransformedView().getHeight()
+                                - ownStablePosition[1]) * 0.33f;
+                    }
+
+                }, TRANSFORMING_VIEW_TEXT);
+    }
+
+    private void resolveTemplateViews(StatusBarNotification notification) {
+        mPicture = (ImageView) mView.findViewById(com.android.internal.R.id.right_icon);
+        if (mPicture != null) {
+            mPicture.setTag(ImageTransformState.ICON_TAG,
+                    notification.getNotification().getLargeIcon());
+        }
+        mTitle = (TextView) mView.findViewById(com.android.internal.R.id.title);
+        mText = (TextView) mView.findViewById(com.android.internal.R.id.text);
+        final View progress = mView.findViewById(com.android.internal.R.id.progress);
+        if (progress instanceof ProgressBar) {
+            mProgressBar = (ProgressBar) progress;
+        } else {
+            // It's still a viewstub
+            mProgressBar = null;
+        }
+        mActionsContainer = mView.findViewById(com.android.internal.R.id.actions_container);
+        mReplyAction = mView.findViewById(com.android.internal.R.id.reply_icon_action);
+    }
+
+    @Override
+    public boolean disallowSingleClick(float x, float y) {
+        if (mReplyAction != null && mReplyAction.getVisibility() == View.VISIBLE) {
+            if (isOnView(mReplyAction, x, y) || isOnView(mPicture, x, y)) {
+                return true;
+            }
+        }
+        return super.disallowSingleClick(x, y);
+    }
+
+    private boolean isOnView(View view, float x, float y) {
+        View searchView = (View) view.getParent();
+        while (searchView != null && !(searchView instanceof ExpandableNotificationRow)) {
+            searchView.getHitRect(mTmpRect);
+            x -= mTmpRect.left;
+            y -= mTmpRect.top;
+            searchView = (View) searchView.getParent();
+        }
+        view.getHitRect(mTmpRect);
+        return mTmpRect.contains((int) x,(int) y);
+    }
+
+    @Override
+    public void onContentUpdated(ExpandableNotificationRow row) {
+        // Reinspect the notification. Before the super call, because the super call also updates
+        // the transformation types and we need to have our values set by then.
+        resolveTemplateViews(row.getStatusBarNotification());
+        super.onContentUpdated(row);
+    }
+
+    @Override
+    protected void updateInvertHelper() {
+        super.updateInvertHelper();
+        View mainColumn = mView.findViewById(com.android.internal.R.id.notification_main_column);
+        if (mainColumn != null) {
+            mInvertHelper.addTarget(mainColumn);
+        }
+    }
+
+    @Override
+    protected void updateTransformedTypes() {
+        // This also clears the existing types
+        super.updateTransformedTypes();
+        if (mTitle != null) {
+            mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TITLE,
+                    mTitle);
+        }
+        if (mText != null) {
+            mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TEXT,
+                    mText);
+        }
+        if (mPicture != null) {
+            mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_IMAGE,
+                    mPicture);
+        }
+        if (mProgressBar != null) {
+            mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_PROGRESS,
+                    mProgressBar);
+        }
+    }
+
+    @Override
+    public void setDark(boolean dark, boolean fade, long delay) {
+        if (dark == mDark && mDarkInitialized) {
+            return;
+        }
+        super.setDark(dark, fade, delay);
+        setPictureDark(dark, fade, delay);
+        setProgressBarDark(dark, fade, delay);
+    }
+
+    private void setProgressBarDark(boolean dark, boolean fade, long delay) {
+        if (mProgressBar != null) {
+            if (fade) {
+                fadeProgressDark(mProgressBar, dark, delay);
+            } else {
+                updateProgressDark(mProgressBar, dark);
+            }
+        }
+    }
+
+    private void fadeProgressDark(final ProgressBar target, final boolean dark, long delay) {
+        getDozer().startIntensityAnimation(animation -> {
+            float t = (float) animation.getAnimatedValue();
+            updateProgressDark(target, t);
+        }, dark, delay, null /* listener */);
+    }
+
+    private void updateProgressDark(ProgressBar target, float intensity) {
+        int color = interpolateColor(mColor, mDarkProgressTint, intensity);
+        target.getIndeterminateDrawable().mutate().setTint(color);
+        target.getProgressDrawable().mutate().setTint(color);
+    }
+
+    private void updateProgressDark(ProgressBar target, boolean dark) {
+        updateProgressDark(target, dark ? 1f : 0f);
+    }
+
+    private void setPictureDark(boolean dark, boolean fade, long delay) {
+        if (mPicture != null) {
+            getDozer().setImageDark(mPicture, dark, fade, delay, true /* useGrayscale */);
+        }
+    }
+
+    private static int interpolateColor(int source, int target, float t) {
+        int aSource = Color.alpha(source);
+        int rSource = Color.red(source);
+        int gSource = Color.green(source);
+        int bSource = Color.blue(source);
+        int aTarget = Color.alpha(target);
+        int rTarget = Color.red(target);
+        int gTarget = Color.green(target);
+        int bTarget = Color.blue(target);
+        return Color.argb(
+                (int) (aSource * (1f - t) + aTarget * t),
+                (int) (rSource * (1f - t) + rTarget * t),
+                (int) (gSource * (1f - t) + gTarget * t),
+                (int) (bSource * (1f - t) + bTarget * t));
+    }
+
+    @Override
+    public void setContentHeight(int contentHeight, int minHeightHint) {
+        super.setContentHeight(contentHeight, minHeightHint);
+
+        mContentHeight = contentHeight;
+        mMinHeightHint = minHeightHint;
+        updateActionOffset();
+    }
+
+    private void updateActionOffset() {
+        if (mActionsContainer != null) {
+            // We should never push the actions higher than they are in the headsup view.
+            int constrainedContentHeight = Math.max(mContentHeight, mMinHeightHint);
+            mActionsContainer.setTranslationY(constrainedContentHeight - mView.getHeight());
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationUtils.java b/com/android/systemui/statusbar/notification/NotificationUtils.java
new file mode 100644
index 0000000..3115361
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationUtils.java
@@ -0,0 +1,69 @@
+/*
+ * 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.notification;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.internal.util.NotificationColorUtil;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.stack.NotificationChildrenContainer;
+
+/**
+ * A util class for various reusable functions
+ */
+public class NotificationUtils {
+    private static final int[] sLocationBase = new int[2];
+    private static final int[] sLocationOffset = new int[2];
+    public static boolean isGrayscale(ImageView v, NotificationColorUtil colorUtil) {
+        Object isGrayscale = v.getTag(R.id.icon_is_grayscale);
+        if (isGrayscale != null) {
+            return Boolean.TRUE.equals(isGrayscale);
+        }
+        boolean grayscale = colorUtil.isGrayscaleIcon(v.getDrawable());
+        v.setTag(R.id.icon_is_grayscale, grayscale);
+        return grayscale;
+    }
+
+    public static float interpolate(float start, float end, float amount) {
+        return start * (1.0f - amount) + end * amount;
+    }
+
+    public static int interpolateColors(int startColor, int endColor, float amount) {
+        return Color.argb(
+                (int) interpolate(Color.alpha(startColor), Color.alpha(endColor), amount),
+                (int) interpolate(Color.red(startColor), Color.red(endColor), amount),
+                (int) interpolate(Color.green(startColor), Color.green(endColor), amount),
+                (int) interpolate(Color.blue(startColor), Color.blue(endColor), amount));
+    }
+
+    public static float getRelativeYOffset(View offsetView, View baseView) {
+        baseView.getLocationOnScreen(sLocationBase);
+        offsetView.getLocationOnScreen(sLocationOffset);
+        return sLocationOffset[1] - sLocationBase[1];
+    }
+
+    public static boolean isHapticFeedbackDisabled(Context context) {
+        return Settings.System.getIntForUser(context.getContentResolver(),
+                Settings.System.HAPTIC_FEEDBACK_ENABLED, 0, UserHandle.USER_CURRENT) == 0;
+    }
+
+}
diff --git a/com/android/systemui/statusbar/notification/NotificationViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
new file mode 100644
index 0000000..5200d69
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2014 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.notification;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.support.v4.graphics.ColorUtils;
+import android.view.NotificationHeaderView;
+import android.view.View;
+
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.TransformableView;
+
+/**
+ * Wraps the actual notification content view; used to implement behaviors which are different for
+ * the individual templates and custom views.
+ */
+public abstract class NotificationViewWrapper implements TransformableView {
+
+    protected final View mView;
+    protected final ExpandableNotificationRow mRow;
+    private final NotificationDozeHelper mDozer;
+
+    protected boolean mDark;
+    private int mBackgroundColor = 0;
+    protected boolean mShouldInvertDark;
+    protected boolean mDarkInitialized = false;
+
+    public static NotificationViewWrapper wrap(Context ctx, View v, ExpandableNotificationRow row) {
+        if (v.getId() == com.android.internal.R.id.status_bar_latest_event_content) {
+            if ("bigPicture".equals(v.getTag())) {
+                return new NotificationBigPictureTemplateViewWrapper(ctx, v, row);
+            } else if ("bigText".equals(v.getTag())) {
+                return new NotificationBigTextTemplateViewWrapper(ctx, v, row);
+            } else if ("media".equals(v.getTag()) || "bigMediaNarrow".equals(v.getTag())) {
+                return new NotificationMediaTemplateViewWrapper(ctx, v, row);
+            } else if ("messaging".equals(v.getTag())) {
+                return new NotificationMessagingTemplateViewWrapper(ctx, v, row);
+            }
+            return new NotificationTemplateViewWrapper(ctx, v, row);
+        } else if (v instanceof NotificationHeaderView) {
+            return new NotificationHeaderViewWrapper(ctx, v, row);
+        } else {
+            return new NotificationCustomViewWrapper(ctx, v, row);
+        }
+    }
+
+    protected NotificationViewWrapper(Context ctx, View view, ExpandableNotificationRow row) {
+        mView = view;
+        mRow = row;
+        mDozer = createDozer(ctx);
+        onReinflated();
+    }
+
+    protected NotificationDozeHelper createDozer(Context ctx) {
+        return new NotificationDozeHelper();
+    }
+
+    protected NotificationDozeHelper getDozer() {
+        return mDozer;
+    }
+
+    /**
+     * In dark mode, we draw as little as possible, assuming a black background.
+     *
+     * @param dark whether we should display ourselves in dark mode
+     * @param fade whether to animate the transition if the mode changes
+     * @param delay if fading, the delay of the animation
+     */
+    public void setDark(boolean dark, boolean fade, long delay) {
+        mDark = dark;
+        mDarkInitialized = true;
+    }
+
+    /**
+     * Notifies this wrapper that the content of the view might have changed.
+     * @param row the row this wrapper is attached to
+     */
+    public void onContentUpdated(ExpandableNotificationRow row) {
+        mDarkInitialized = false;
+    }
+
+    public void onReinflated() {
+        if (shouldClearBackgroundOnReapply()) {
+            mBackgroundColor = 0;
+        }
+        Drawable background = mView.getBackground();
+        if (background instanceof ColorDrawable) {
+            mBackgroundColor = ((ColorDrawable) background).getColor();
+            mView.setBackground(null);
+        }
+        mShouldInvertDark = mBackgroundColor == 0 || isColorLight(mBackgroundColor);
+    }
+
+    protected boolean shouldClearBackgroundOnReapply() {
+        return true;
+    }
+
+    private boolean isColorLight(int backgroundColor) {
+        return Color.alpha(backgroundColor) == 0
+                || ColorUtils.calculateLuminance(backgroundColor) > 0.5;
+    }
+
+    /**
+     * Update the appearance of the expand button.
+     *
+     * @param expandable should this view be expandable
+     * @param onClickListener the listener to invoke when the expand affordance is clicked on
+     */
+    public void updateExpandability(boolean expandable, View.OnClickListener onClickListener) {}
+
+    /**
+     * @return the notification header if it exists
+     */
+    public NotificationHeaderView getNotificationHeader() {
+        return null;
+    }
+
+    @Override
+    public TransformState getCurrentState(int fadingView) {
+        return null;
+    }
+
+    @Override
+    public void transformTo(TransformableView notification, Runnable endRunnable) {
+        // By default we are fading out completely
+        CrossFadeHelper.fadeOut(mView, endRunnable);
+    }
+
+    @Override
+    public void transformTo(TransformableView notification, float transformationAmount) {
+        CrossFadeHelper.fadeOut(mView, transformationAmount);
+    }
+
+    @Override
+    public void transformFrom(TransformableView notification) {
+        // By default we are fading in completely
+        CrossFadeHelper.fadeIn(mView);
+    }
+
+    @Override
+    public void transformFrom(TransformableView notification, float transformationAmount) {
+        CrossFadeHelper.fadeIn(mView, transformationAmount);
+    }
+
+    @Override
+    public void setVisible(boolean visible) {
+        mView.animate().cancel();
+        mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+    }
+
+    public int getCustomBackgroundColor() {
+        // Parent notifications should always use the normal background color
+        return mRow.isSummaryWithChildren() ? 0 : mBackgroundColor;
+    }
+
+    public void setLegacy(boolean legacy) {
+    }
+
+    public void setContentHeight(int contentHeight, int minHeightHint) {
+    }
+
+    public void setRemoteInputVisible(boolean visible) {
+    }
+
+    public void setIsChildInGroup(boolean isChildInGroup) {
+    }
+
+    public boolean isDimmable() {
+        return true;
+    }
+
+    public boolean disallowSingleClick(float x, float y) {
+        return false;
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/ProgressTransformState.java b/com/android/systemui/statusbar/notification/ProgressTransformState.java
new file mode 100644
index 0000000..bf78194
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/ProgressTransformState.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.util.Pools;
+
+/**
+ * A transform state of a progress view.
+*/
+public class ProgressTransformState extends TransformState {
+
+    private static Pools.SimplePool<ProgressTransformState> sInstancePool
+            = new Pools.SimplePool<>(40);
+
+    @Override
+    protected boolean sameAs(TransformState otherState) {
+        if (otherState instanceof ProgressTransformState) {
+            return true;
+        }
+        return super.sameAs(otherState);
+    }
+
+    public static ProgressTransformState obtain() {
+        ProgressTransformState instance = sInstancePool.acquire();
+        if (instance != null) {
+            return instance;
+        }
+        return new ProgressTransformState();
+    }
+
+    @Override
+    public void recycle() {
+        super.recycle();
+        sInstancePool.release(this);
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/PropertyAnimator.java b/com/android/systemui/statusbar/notification/PropertyAnimator.java
new file mode 100644
index 0000000..80ba943
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/PropertyAnimator.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.util.Property;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.statusbar.stack.AnimationFilter;
+import com.android.systemui.statusbar.stack.AnimationProperties;
+import com.android.systemui.statusbar.stack.ViewState;
+
+/**
+ * An animator to animate properties
+ */
+public class PropertyAnimator {
+
+    public static <T extends View> void startAnimation(final T view,
+            AnimatableProperty animatableProperty, float newEndValue,
+            AnimationProperties properties) {
+        Property<T, Float> property = animatableProperty.getProperty();
+        int animationStartTag = animatableProperty.getAnimationStartTag();
+        int animationEndTag = animatableProperty.getAnimationEndTag();
+        Float previousStartValue = ViewState.getChildTag(view, animationStartTag);
+        Float previousEndValue = ViewState.getChildTag(view, animationEndTag);
+        if (previousEndValue != null && previousEndValue == newEndValue) {
+            return;
+        }
+        int animatorTag = animatableProperty.getAnimatorTag();
+        ValueAnimator previousAnimator = ViewState.getChildTag(view, animatorTag);
+        AnimationFilter filter = properties.getAnimationFilter();
+        if (!filter.shouldAnimateProperty(property)) {
+            // just a local update was performed
+            if (previousAnimator != null) {
+                // we need to increase all animation keyframes of the previous animator by the
+                // relative change to the end value
+                PropertyValuesHolder[] values = previousAnimator.getValues();
+                float relativeDiff = newEndValue - previousEndValue;
+                float newStartValue = previousStartValue + relativeDiff;
+                values[0].setFloatValues(newStartValue, newEndValue);
+                view.setTag(animationStartTag, newStartValue);
+                view.setTag(animationEndTag, newEndValue);
+                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+                return;
+            } else {
+                // no new animation needed, let's just apply the value
+                property.set(view, newEndValue);
+                return;
+            }
+        }
+
+        Float currentValue = property.get(view);
+        ValueAnimator animator = ValueAnimator.ofFloat(currentValue, newEndValue);
+        animator.addUpdateListener(
+                animation -> property.set(view, (Float) animation.getAnimatedValue()));
+        Interpolator customInterpolator = properties.getCustomInterpolator(view, property);
+        Interpolator interpolator =  customInterpolator != null ? customInterpolator
+                : Interpolators.FAST_OUT_SLOW_IN;
+        animator.setInterpolator(interpolator);
+        long newDuration = ViewState.cancelAnimatorAndGetNewDuration(properties.duration,
+                previousAnimator);
+        animator.setDuration(newDuration);
+        if (properties.delay > 0 && (previousAnimator == null
+                || previousAnimator.getAnimatedFraction() == 0)) {
+            animator.setStartDelay(properties.delay);
+        }
+        AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+        if (listener != null) {
+            animator.addListener(listener);
+        }
+        // remove the tag when the animation is finished
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                view.setTag(animatorTag, null);
+                view.setTag(animationStartTag, null);
+                view.setTag(animationEndTag, null);
+            }
+        });
+        ViewState.startAnimator(animator, listener);
+        view.setTag(animatorTag, animator);
+        view.setTag(animationStartTag, currentValue);
+        view.setTag(animationEndTag, newEndValue);
+    }
+
+    public interface AnimatableProperty {
+        int getAnimationStartTag();
+        int getAnimationEndTag();
+        int getAnimatorTag();
+        Property getProperty();
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/RowInflaterTask.java b/com/android/systemui/statusbar/notification/RowInflaterTask.java
new file mode 100644
index 0000000..3491f81
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/RowInflaterTask.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2017 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.notification;
+
+import android.content.Context;
+import android.support.v4.view.AsyncLayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.InflationTask;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+
+/**
+ * An inflater task that asynchronously inflates a ExpandableNotificationRow
+ */
+public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInflateFinishedListener {
+    private RowInflationFinishedListener mListener;
+    private NotificationData.Entry mEntry;
+    private boolean mCancelled;
+
+    /**
+     * Inflates a new notificationView. This should not be called twice on this object
+     */
+    public void inflate(Context context, ViewGroup parent, NotificationData.Entry entry,
+            RowInflationFinishedListener listener) {
+        mListener = listener;
+        AsyncLayoutInflater inflater = new AsyncLayoutInflater(context);
+        mEntry = entry;
+        entry.setInflationTask(this);
+        inflater.inflate(R.layout.status_bar_notification_row, parent, this);
+    }
+
+    @Override
+    public void abort() {
+        mCancelled = true;
+    }
+
+    @Override
+    public void onInflateFinished(View view, int resid, ViewGroup parent) {
+        if (!mCancelled) {
+            mEntry.onInflationTaskFinished();
+            mListener.onInflationFinished((ExpandableNotificationRow) view);
+        }
+    }
+
+    public interface RowInflationFinishedListener {
+        void onInflationFinished(ExpandableNotificationRow row);
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/TextViewTransformState.java b/com/android/systemui/statusbar/notification/TextViewTransformState.java
new file mode 100644
index 0000000..c4aabe4
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/TextViewTransformState.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.text.Layout;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.util.Pools;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * A transform state of a mText view.
+*/
+public class TextViewTransformState extends TransformState {
+
+    private static Pools.SimplePool<TextViewTransformState> sInstancePool
+            = new Pools.SimplePool<>(40);
+    private TextView mText;
+
+    @Override
+    public void initFrom(View view) {
+        super.initFrom(view);
+        if (view instanceof TextView) {
+            mText = (TextView) view;
+        }
+    }
+
+    @Override
+    protected boolean sameAs(TransformState otherState) {
+        if (super.sameAs(otherState)) {
+            return true;
+        }
+        if (otherState instanceof TextViewTransformState) {
+            TextViewTransformState otherTvs = (TextViewTransformState) otherState;
+            if(TextUtils.equals(otherTvs.mText.getText(), mText.getText())) {
+                int ownEllipsized = getEllipsisCount();
+                int otherEllipsized = otherTvs.getEllipsisCount();
+                return ownEllipsized == otherEllipsized
+                        && mText.getLineCount() == otherTvs.mText.getLineCount()
+                        && hasSameSpans(otherTvs);
+            }
+        }
+        return false;
+    }
+
+    private boolean hasSameSpans(TextViewTransformState otherTvs) {
+        boolean hasSpans = mText instanceof Spanned;
+        boolean otherHasSpans = otherTvs.mText instanceof Spanned;
+        if (hasSpans != otherHasSpans) {
+            return false;
+        } else if (!hasSpans) {
+            return true;
+        }
+        // Actually both have spans, let's try to compare them
+        Spanned ownSpanned = (Spanned) mText;
+        Object[] spans = ownSpanned.getSpans(0, ownSpanned.length(), Object.class);
+        Spanned otherSpanned = (Spanned) otherTvs.mText;
+        Object[] otherSpans = otherSpanned.getSpans(0, otherSpanned.length(), Object.class);
+        if (spans.length != otherSpans.length) {
+            return false;
+        }
+        for (int i = 0; i < spans.length; i++) {
+            Object span = spans[i];
+            Object otherSpan = otherSpans[i];
+            if (!span.getClass().equals(otherSpan.getClass())) {
+                return false;
+            }
+            if (ownSpanned.getSpanStart(span) != otherSpanned.getSpanStart(otherSpan)
+                    || ownSpanned.getSpanEnd(span) != otherSpanned.getSpanEnd(otherSpan)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    protected boolean transformScale(TransformState otherState) {
+        if (!(otherState instanceof TextViewTransformState)) {
+            return false;
+        }
+        TextViewTransformState otherTvs = (TextViewTransformState) otherState;
+        int lineCount = mText.getLineCount();
+        return lineCount == 1 && lineCount == otherTvs.mText.getLineCount()
+                && getEllipsisCount() == otherTvs.getEllipsisCount()
+                && getViewHeight() != otherTvs.getViewHeight();
+    }
+
+    @Override
+    protected int getViewWidth() {
+        Layout l = mText.getLayout();
+        if (l != null) {
+            return (int) l.getLineWidth(0);
+        }
+        return super.getViewWidth();
+    }
+
+    @Override
+    protected int getViewHeight() {
+        return mText.getLineHeight();
+    }
+
+    private int getInnerHeight(TextView text) {
+        return text.getHeight() - text.getPaddingTop() - text.getPaddingBottom();
+    }
+
+    private int getEllipsisCount() {
+        Layout l = mText.getLayout();
+        if (l != null) {
+            int lines = l.getLineCount();
+            if (lines > 0) {
+                // we only care about the first line
+                return l.getEllipsisCount(0);
+            }
+        }
+        return 0;
+    }
+
+    public static TextViewTransformState obtain() {
+        TextViewTransformState instance = sInstancePool.acquire();
+        if (instance != null) {
+            return instance;
+        }
+        return new TextViewTransformState();
+    }
+
+    @Override
+    public void recycle() {
+        super.recycle();
+        sInstancePool.release(this);
+    }
+
+    @Override
+    protected void reset() {
+        super.reset();
+        mText = null;
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/TransformState.java b/com/android/systemui/statusbar/notification/TransformState.java
new file mode 100644
index 0000000..bafedff
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/TransformState.java
@@ -0,0 +1,581 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.util.ArraySet;
+import android.util.Pools;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.animation.Interpolator;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.TransformableView;
+import com.android.systemui.statusbar.ViewTransformationHelper;
+
+/**
+ * A transform state of a view.
+*/
+public class TransformState {
+
+    public static final int TRANSFORM_X = 0x1;
+    public static final int TRANSFORM_Y = 0x10;
+    public static final int TRANSFORM_ALL = TRANSFORM_X | TRANSFORM_Y;
+
+    private static final float UNDEFINED = -1f;
+    private static final int CLIP_CLIPPING_SET = R.id.clip_children_set_tag;
+    private static final int CLIP_CHILDREN_TAG = R.id.clip_children_tag;
+    private static final int CLIP_TO_PADDING = R.id.clip_to_padding_tag;
+    private static final int TRANSFORMATION_START_X = R.id.transformation_start_x_tag;
+    private static final int TRANSFORMATION_START_Y = R.id.transformation_start_y_tag;
+    private static final int TRANSFORMATION_START_SCLALE_X = R.id.transformation_start_scale_x_tag;
+    private static final int TRANSFORMATION_START_SCLALE_Y = R.id.transformation_start_scale_y_tag;
+    private static Pools.SimplePool<TransformState> sInstancePool = new Pools.SimplePool<>(40);
+
+    protected View mTransformedView;
+    private int[] mOwnPosition = new int[2];
+    private boolean mSameAsAny;
+    private float mTransformationEndY = UNDEFINED;
+    private float mTransformationEndX = UNDEFINED;
+
+    public void initFrom(View view) {
+        mTransformedView = view;
+    }
+
+    /**
+     * Transforms the {@link #mTransformedView} from the given transformviewstate
+     * @param otherState the state to transform from
+     * @param transformationAmount how much to transform
+     */
+    public void transformViewFrom(TransformState otherState, float transformationAmount) {
+        mTransformedView.animate().cancel();
+        if (sameAs(otherState)) {
+            if (mTransformedView.getVisibility() == View.INVISIBLE
+                    || mTransformedView.getAlpha() != 1.0f) {
+                // We have the same content, lets show ourselves
+                mTransformedView.setAlpha(1.0f);
+                mTransformedView.setVisibility(View.VISIBLE);
+            }
+        } else {
+            CrossFadeHelper.fadeIn(mTransformedView, transformationAmount);
+        }
+        transformViewFullyFrom(otherState, transformationAmount);
+    }
+
+    public void transformViewFullyFrom(TransformState otherState, float transformationAmount) {
+        transformViewFrom(otherState, TRANSFORM_ALL, null, transformationAmount);
+    }
+
+    public void transformViewFullyFrom(TransformState otherState,
+            ViewTransformationHelper.CustomTransformation customTransformation,
+            float transformationAmount) {
+        transformViewFrom(otherState, TRANSFORM_ALL, customTransformation, transformationAmount);
+    }
+
+    public void transformViewVerticalFrom(TransformState otherState,
+            ViewTransformationHelper.CustomTransformation customTransformation,
+            float transformationAmount) {
+        transformViewFrom(otherState, TRANSFORM_Y, customTransformation, transformationAmount);
+    }
+
+    public void transformViewVerticalFrom(TransformState otherState, float transformationAmount) {
+        transformViewFrom(otherState, TRANSFORM_Y, null, transformationAmount);
+    }
+
+    private void transformViewFrom(TransformState otherState, int transformationFlags,
+            ViewTransformationHelper.CustomTransformation customTransformation,
+            float transformationAmount) {
+        final View transformedView = mTransformedView;
+        boolean transformX = (transformationFlags & TRANSFORM_X) != 0;
+        boolean transformY = (transformationFlags & TRANSFORM_Y) != 0;
+        boolean transformScale = transformScale(otherState);
+        // lets animate the positions correctly
+        if (transformationAmount == 0.0f
+                || transformX && getTransformationStartX() == UNDEFINED
+                || transformY && getTransformationStartY() == UNDEFINED
+                || transformScale && getTransformationStartScaleX() == UNDEFINED
+                || transformScale && getTransformationStartScaleY() == UNDEFINED) {
+            int[] otherPosition;
+            if (transformationAmount != 0.0f) {
+                otherPosition = otherState.getLaidOutLocationOnScreen();
+            } else {
+                otherPosition = otherState.getLocationOnScreen();
+            }
+            int[] ownStablePosition = getLaidOutLocationOnScreen();
+            if (customTransformation == null
+                    || !customTransformation.initTransformation(this, otherState)) {
+                if (transformX) {
+                    setTransformationStartX(otherPosition[0] - ownStablePosition[0]);
+                }
+                if (transformY) {
+                    setTransformationStartY(otherPosition[1] - ownStablePosition[1]);
+                }
+                // we also want to animate the scale if we're the same
+                View otherView = otherState.getTransformedView();
+                if (transformScale && otherState.getViewWidth() != getViewWidth()) {
+                    setTransformationStartScaleX(otherState.getViewWidth() * otherView.getScaleX()
+                            / (float) getViewWidth());
+                    transformedView.setPivotX(0);
+                } else {
+                    setTransformationStartScaleX(UNDEFINED);
+                }
+                if (transformScale && otherState.getViewHeight() != getViewHeight()) {
+                    setTransformationStartScaleY(otherState.getViewHeight() * otherView.getScaleY()
+                            / (float) getViewHeight());
+                    transformedView.setPivotY(0);
+                } else {
+                    setTransformationStartScaleY(UNDEFINED);
+                }
+            }
+            if (!transformX) {
+                setTransformationStartX(UNDEFINED);
+            }
+            if (!transformY) {
+                setTransformationStartY(UNDEFINED);
+            }
+            if (!transformScale) {
+                setTransformationStartScaleX(UNDEFINED);
+                setTransformationStartScaleY(UNDEFINED);
+            }
+            setClippingDeactivated(transformedView, true);
+        }
+        float interpolatedValue = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
+                transformationAmount);
+        if (transformX) {
+            float interpolation = interpolatedValue;
+            if (customTransformation != null) {
+                Interpolator customInterpolator =
+                        customTransformation.getCustomInterpolator(TRANSFORM_X, true /* isFrom */);
+                if (customInterpolator != null) {
+                    interpolation = customInterpolator.getInterpolation(transformationAmount);
+                }
+            }
+            transformedView.setTranslationX(NotificationUtils.interpolate(getTransformationStartX(),
+                    0.0f,
+                    interpolation));
+        }
+        if (transformY) {
+            float interpolation = interpolatedValue;
+            if (customTransformation != null) {
+                Interpolator customInterpolator =
+                        customTransformation.getCustomInterpolator(TRANSFORM_Y, true /* isFrom */);
+                if (customInterpolator != null) {
+                    interpolation = customInterpolator.getInterpolation(transformationAmount);
+                }
+            }
+            transformedView.setTranslationY(NotificationUtils.interpolate(getTransformationStartY(),
+                    0.0f,
+                    interpolation));
+        }
+        if (transformScale) {
+            float transformationStartScaleX = getTransformationStartScaleX();
+            if (transformationStartScaleX != UNDEFINED) {
+                transformedView.setScaleX(
+                        NotificationUtils.interpolate(transformationStartScaleX,
+                                1.0f,
+                                interpolatedValue));
+            }
+            float transformationStartScaleY = getTransformationStartScaleY();
+            if (transformationStartScaleY != UNDEFINED) {
+                transformedView.setScaleY(
+                        NotificationUtils.interpolate(transformationStartScaleY,
+                                1.0f,
+                                interpolatedValue));
+            }
+        }
+    }
+
+    protected int getViewWidth() {
+        return mTransformedView.getWidth();
+    }
+
+    protected int getViewHeight() {
+        return mTransformedView.getHeight();
+    }
+
+    protected boolean transformScale(TransformState otherState) {
+        return false;
+    }
+
+    /**
+     * Transforms the {@link #mTransformedView} to the given transformviewstate
+     * @param otherState the state to transform from
+     * @param transformationAmount how much to transform
+     * @return whether an animation was started
+     */
+    public boolean transformViewTo(TransformState otherState, float transformationAmount) {
+        mTransformedView.animate().cancel();
+        if (sameAs(otherState)) {
+            // We have the same text, lets show ourselfs
+            if (mTransformedView.getVisibility() == View.VISIBLE) {
+                mTransformedView.setAlpha(0.0f);
+                mTransformedView.setVisibility(View.INVISIBLE);
+            }
+            return false;
+        } else {
+            CrossFadeHelper.fadeOut(mTransformedView, transformationAmount);
+        }
+        transformViewFullyTo(otherState, transformationAmount);
+        return true;
+    }
+
+    public void transformViewFullyTo(TransformState otherState, float transformationAmount) {
+        transformViewTo(otherState, TRANSFORM_ALL, null, transformationAmount);
+    }
+
+    public void transformViewFullyTo(TransformState otherState,
+            ViewTransformationHelper.CustomTransformation customTransformation,
+            float transformationAmount) {
+        transformViewTo(otherState, TRANSFORM_ALL, customTransformation, transformationAmount);
+    }
+
+    public void transformViewVerticalTo(TransformState otherState,
+            ViewTransformationHelper.CustomTransformation customTransformation,
+            float transformationAmount) {
+        transformViewTo(otherState, TRANSFORM_Y, customTransformation, transformationAmount);
+    }
+
+    public void transformViewVerticalTo(TransformState otherState, float transformationAmount) {
+        transformViewTo(otherState, TRANSFORM_Y, null, transformationAmount);
+    }
+
+    private void transformViewTo(TransformState otherState, int transformationFlags,
+            ViewTransformationHelper.CustomTransformation customTransformation,
+            float transformationAmount) {
+        // lets animate the positions correctly
+
+        final View transformedView = mTransformedView;
+        boolean transformX = (transformationFlags & TRANSFORM_X) != 0;
+        boolean transformY = (transformationFlags & TRANSFORM_Y) != 0;
+        boolean transformScale = transformScale(otherState);
+        // lets animate the positions correctly
+        if (transformationAmount == 0.0f) {
+            if (transformX) {
+                float transformationStartX = getTransformationStartX();
+                float start = transformationStartX != UNDEFINED ? transformationStartX
+                        : transformedView.getTranslationX();
+                setTransformationStartX(start);
+            }
+            if (transformY) {
+                float transformationStartY = getTransformationStartY();
+                float start = transformationStartY != UNDEFINED ? transformationStartY
+                        : transformedView.getTranslationY();
+                setTransformationStartY(start);
+            }
+            View otherView = otherState.getTransformedView();
+            if (transformScale && otherState.getViewWidth() != getViewWidth()) {
+                setTransformationStartScaleX(transformedView.getScaleX());
+                transformedView.setPivotX(0);
+            } else {
+                setTransformationStartScaleX(UNDEFINED);
+            }
+            if (transformScale && otherState.getViewHeight() != getViewHeight()) {
+                setTransformationStartScaleY(transformedView.getScaleY());
+                transformedView.setPivotY(0);
+            } else {
+                setTransformationStartScaleY(UNDEFINED);
+            }
+            setClippingDeactivated(transformedView, true);
+        }
+        float interpolatedValue = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
+                transformationAmount);
+        int[] otherStablePosition = otherState.getLaidOutLocationOnScreen();
+        int[] ownPosition = getLaidOutLocationOnScreen();
+        if (transformX) {
+            float endX = otherStablePosition[0] - ownPosition[0];
+            float interpolation = interpolatedValue;
+            if (customTransformation != null) {
+                if (customTransformation.customTransformTarget(this, otherState)) {
+                    endX = mTransformationEndX;
+                }
+                Interpolator customInterpolator =
+                        customTransformation.getCustomInterpolator(TRANSFORM_X, false /* isFrom */);
+                if (customInterpolator != null) {
+                    interpolation = customInterpolator.getInterpolation(transformationAmount);
+                }
+            }
+            transformedView.setTranslationX(NotificationUtils.interpolate(getTransformationStartX(),
+                    endX,
+                    interpolation));
+        }
+        if (transformY) {
+            float endY = otherStablePosition[1] - ownPosition[1];
+            float interpolation = interpolatedValue;
+            if (customTransformation != null) {
+                if (customTransformation.customTransformTarget(this, otherState)) {
+                    endY = mTransformationEndY;
+                }
+                Interpolator customInterpolator =
+                        customTransformation.getCustomInterpolator(TRANSFORM_Y, false /* isFrom */);
+                if (customInterpolator != null) {
+                    interpolation = customInterpolator.getInterpolation(transformationAmount);
+                }
+            }
+            transformedView.setTranslationY(NotificationUtils.interpolate(getTransformationStartY(),
+                    endY,
+                    interpolation));
+        }
+        if (transformScale) {
+            View otherView = otherState.getTransformedView();
+            float transformationStartScaleX = getTransformationStartScaleX();
+            if (transformationStartScaleX != UNDEFINED) {
+                transformedView.setScaleX(
+                        NotificationUtils.interpolate(transformationStartScaleX,
+                                (otherState.getViewWidth() / (float) getViewWidth()),
+                                interpolatedValue));
+            }
+            float transformationStartScaleY = getTransformationStartScaleY();
+            if (transformationStartScaleY != UNDEFINED) {
+                transformedView.setScaleY(
+                        NotificationUtils.interpolate(transformationStartScaleY,
+                                (otherState.getViewHeight() / (float) getViewHeight()),
+                                interpolatedValue));
+            }
+        }
+    }
+
+    public static void setClippingDeactivated(final View transformedView, boolean deactivated) {
+        if (!(transformedView.getParent() instanceof ViewGroup)) {
+            return;
+        }
+        ViewGroup view = (ViewGroup) transformedView.getParent();
+        while (true) {
+            ArraySet<View> clipSet = (ArraySet<View>) view.getTag(CLIP_CLIPPING_SET);
+            if (clipSet == null) {
+                clipSet = new ArraySet<>();
+                view.setTag(CLIP_CLIPPING_SET, clipSet);
+            }
+            Boolean clipChildren = (Boolean) view.getTag(CLIP_CHILDREN_TAG);
+            if (clipChildren == null) {
+                clipChildren = view.getClipChildren();
+                view.setTag(CLIP_CHILDREN_TAG, clipChildren);
+            }
+            Boolean clipToPadding = (Boolean) view.getTag(CLIP_TO_PADDING);
+            if (clipToPadding == null) {
+                clipToPadding = view.getClipToPadding();
+                view.setTag(CLIP_TO_PADDING, clipToPadding);
+            }
+            ExpandableNotificationRow row = view instanceof ExpandableNotificationRow
+                    ? (ExpandableNotificationRow) view
+                    : null;
+            if (!deactivated) {
+                clipSet.remove(transformedView);
+                if (clipSet.isEmpty()) {
+                    view.setClipChildren(clipChildren);
+                    view.setClipToPadding(clipToPadding);
+                    view.setTag(CLIP_CLIPPING_SET, null);
+                    if (row != null) {
+                        row.setClipToActualHeight(true);
+                    }
+                }
+            } else {
+                clipSet.add(transformedView);
+                view.setClipChildren(false);
+                view.setClipToPadding(false);
+                if (row != null && row.isChildInGroup()) {
+                    // We still want to clip to the parent's height
+                    row.setClipToActualHeight(false);
+                }
+            }
+            if (row != null && !row.isChildInGroup()) {
+                return;
+            }
+            final ViewParent parent = view.getParent();
+            if (parent instanceof ViewGroup) {
+                view = (ViewGroup) parent;
+            } else {
+                return;
+            }
+        }
+    }
+
+    public int[] getLaidOutLocationOnScreen() {
+        int[] location = getLocationOnScreen();
+        // remove translation
+        location[0] -= mTransformedView.getTranslationX();
+        location[1] -= mTransformedView.getTranslationY();
+        return location;
+    }
+
+    public int[] getLocationOnScreen() {
+        mTransformedView.getLocationOnScreen(mOwnPosition);
+
+        // remove scale
+        mOwnPosition[0] -= (1.0f - mTransformedView.getScaleX()) * mTransformedView.getPivotX();
+        mOwnPosition[1] -= (1.0f - mTransformedView.getScaleY()) * mTransformedView.getPivotY();
+        return mOwnPosition;
+    }
+
+    protected boolean sameAs(TransformState otherState) {
+        return mSameAsAny;
+    }
+
+    public void appear(float transformationAmount, TransformableView otherView) {
+        // There's no other view, lets fade us in
+        // Certain views need to prepare the fade in and make sure its children are
+        // completely visible. An example is the notification header.
+        if (transformationAmount == 0.0f) {
+            prepareFadeIn();
+        }
+        CrossFadeHelper.fadeIn(mTransformedView, transformationAmount);
+    }
+
+    public void disappear(float transformationAmount, TransformableView otherView) {
+        CrossFadeHelper.fadeOut(mTransformedView, transformationAmount);
+    }
+
+    public static TransformState createFrom(View view) {
+        if (view instanceof TextView) {
+            TextViewTransformState result = TextViewTransformState.obtain();
+            result.initFrom(view);
+            return result;
+        }
+        if (view.getId() == com.android.internal.R.id.actions_container) {
+            ActionListTransformState result = ActionListTransformState.obtain();
+            result.initFrom(view);
+            return result;
+        }
+        if (view instanceof ImageView) {
+            ImageTransformState result = ImageTransformState.obtain();
+            result.initFrom(view);
+            if (view.getId() == com.android.internal.R.id.reply_icon_action) {
+                ((TransformState) result).setIsSameAsAnyView(true);
+            }
+            return result;
+        }
+        if (view instanceof ProgressBar) {
+            ProgressTransformState result = ProgressTransformState.obtain();
+            result.initFrom(view);
+            return result;
+        }
+        TransformState result = obtain();
+        result.initFrom(view);
+        return result;
+    }
+
+    private void setIsSameAsAnyView(boolean sameAsAny) {
+        mSameAsAny = sameAsAny;
+    }
+
+    public void recycle() {
+        reset();
+        if (getClass() == TransformState.class) {
+            sInstancePool.release(this);
+        }
+    }
+
+    public void setTransformationEndY(float transformationEndY) {
+        mTransformationEndY = transformationEndY;
+    }
+
+    public void setTransformationEndX(float transformationEndX) {
+        mTransformationEndX = transformationEndX;
+    }
+
+    public float getTransformationStartX() {
+        Object tag = mTransformedView.getTag(TRANSFORMATION_START_X);
+        return tag == null ? UNDEFINED : (float) tag;
+    }
+
+    public float getTransformationStartY() {
+        Object tag = mTransformedView.getTag(TRANSFORMATION_START_Y);
+        return tag == null ? UNDEFINED : (float) tag;
+    }
+
+    public float getTransformationStartScaleX() {
+        Object tag = mTransformedView.getTag(TRANSFORMATION_START_SCLALE_X);
+        return tag == null ? UNDEFINED : (float) tag;
+    }
+
+    public float getTransformationStartScaleY() {
+        Object tag = mTransformedView.getTag(TRANSFORMATION_START_SCLALE_Y);
+        return tag == null ? UNDEFINED : (float) tag;
+    }
+
+    public void setTransformationStartX(float transformationStartX) {
+        mTransformedView.setTag(TRANSFORMATION_START_X, transformationStartX);
+    }
+
+    public void setTransformationStartY(float transformationStartY) {
+        mTransformedView.setTag(TRANSFORMATION_START_Y, transformationStartY);
+    }
+
+    private void setTransformationStartScaleX(float startScaleX) {
+        mTransformedView.setTag(TRANSFORMATION_START_SCLALE_X, startScaleX);
+    }
+
+    private void setTransformationStartScaleY(float startScaleY) {
+        mTransformedView.setTag(TRANSFORMATION_START_SCLALE_Y, startScaleY);
+    }
+
+    protected void reset() {
+        mTransformedView = null;
+        mSameAsAny = false;
+        mTransformationEndX = UNDEFINED;
+        mTransformationEndY = UNDEFINED;
+    }
+
+    public void setVisible(boolean visible, boolean force) {
+        if (!force && mTransformedView.getVisibility() == View.GONE) {
+            return;
+        }
+        if (mTransformedView.getVisibility() != View.GONE) {
+            mTransformedView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+        }
+        mTransformedView.animate().cancel();
+        mTransformedView.setAlpha(visible ? 1.0f : 0.0f);
+        resetTransformedView();
+    }
+
+    public void prepareFadeIn() {
+        resetTransformedView();
+    }
+
+    protected void resetTransformedView() {
+        mTransformedView.setTranslationX(0);
+        mTransformedView.setTranslationY(0);
+        mTransformedView.setScaleX(1.0f);
+        mTransformedView.setScaleY(1.0f);
+        setClippingDeactivated(mTransformedView, false);
+        abortTransformation();
+    }
+
+    public void abortTransformation() {
+        mTransformedView.setTag(TRANSFORMATION_START_X, UNDEFINED);
+        mTransformedView.setTag(TRANSFORMATION_START_Y, UNDEFINED);
+        mTransformedView.setTag(TRANSFORMATION_START_SCLALE_X, UNDEFINED);
+        mTransformedView.setTag(TRANSFORMATION_START_SCLALE_Y, UNDEFINED);
+    }
+
+    public static TransformState obtain() {
+        TransformState instance = sInstancePool.acquire();
+        if (instance != null) {
+            return instance;
+        }
+        return new TransformState();
+    }
+
+    public View getTransformedView() {
+        return mTransformedView;
+    }
+}
diff --git a/com/android/systemui/statusbar/notification/VisibilityLocationProvider.java b/com/android/systemui/statusbar/notification/VisibilityLocationProvider.java
new file mode 100644
index 0000000..4a52acc
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/VisibilityLocationProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+
+/**
+ * An object that can determine the visibility of a Notification.
+ */
+public interface VisibilityLocationProvider {
+
+    /**
+     * @return whether the view is in a visible location right now.
+     */
+    boolean isInVisibleLocation(ExpandableNotificationRow row);
+}
diff --git a/com/android/systemui/statusbar/notification/VisualStabilityManager.java b/com/android/systemui/statusbar/notification/VisualStabilityManager.java
new file mode 100644
index 0000000..09aff1b
--- /dev/null
+++ b/com/android/systemui/statusbar/notification/VisualStabilityManager.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.support.v4.util.ArraySet;
+import android.view.View;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+
+import java.util.ArrayList;
+
+/**
+ * A manager that ensures that notifications are visually stable. It will suppress reorderings
+ * and reorder at the right time when they are out of view.
+ */
+public class VisualStabilityManager implements OnHeadsUpChangedListener {
+
+    private final ArrayList<Callback> mCallbacks =  new ArrayList<>();
+
+    private boolean mPanelExpanded;
+    private boolean mScreenOn;
+    private boolean mReorderingAllowed;
+    private VisibilityLocationProvider mVisibilityLocationProvider;
+    private ArraySet<View> mAllowedReorderViews = new ArraySet<>();
+    private ArraySet<View> mLowPriorityReorderingViews = new ArraySet<>();
+    private ArraySet<View> mAddedChildren = new ArraySet<>();
+    private boolean mPulsing;
+
+    /**
+     * Add a callback to invoke when reordering is allowed again.
+     * @param callback
+     */
+    public void addReorderingAllowedCallback(Callback callback) {
+        if (mCallbacks.contains(callback)) {
+            return;
+        }
+        mCallbacks.add(callback);
+    }
+
+    /**
+     * Set the panel to be expanded.
+     */
+    public void setPanelExpanded(boolean expanded) {
+        mPanelExpanded = expanded;
+        updateReorderingAllowed();
+    }
+
+    /**
+     * @param screenOn whether the screen is on
+     */
+    public void setScreenOn(boolean screenOn) {
+        mScreenOn = screenOn;
+        updateReorderingAllowed();
+    }
+
+    /**
+     * @param pulsing whether we are currently pulsing for ambient display.
+     */
+    public void setPulsing(boolean pulsing) {
+        if (mPulsing == pulsing) {
+            return;
+        }
+        mPulsing = pulsing;
+        updateReorderingAllowed();
+    }
+
+    private void updateReorderingAllowed() {
+        boolean reorderingAllowed = (!mScreenOn || !mPanelExpanded) && !mPulsing;
+        boolean changed = reorderingAllowed && !mReorderingAllowed;
+        mReorderingAllowed = reorderingAllowed;
+        if (changed) {
+            notifyCallbacks();
+        }
+    }
+
+    private void notifyCallbacks() {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            Callback callback = mCallbacks.get(i);
+            callback.onReorderingAllowed();
+        }
+        mCallbacks.clear();
+    }
+
+    /**
+     * @return whether reordering is currently allowed in general.
+     */
+    public boolean isReorderingAllowed() {
+        return mReorderingAllowed;
+    }
+
+    /**
+     * @return whether a specific notification is allowed to reorder. Certain notifications are
+     * allowed to reorder even if {@link #isReorderingAllowed()} returns false, like newly added
+     * notifications or heads-up notifications that are out of view.
+     */
+    public boolean canReorderNotification(ExpandableNotificationRow row) {
+        if (mReorderingAllowed) {
+            return true;
+        }
+        if (mAddedChildren.contains(row)) {
+            return true;
+        }
+        if (mLowPriorityReorderingViews.contains(row)) {
+            return true;
+        }
+        if (mAllowedReorderViews.contains(row)
+                && !mVisibilityLocationProvider.isInVisibleLocation(row)) {
+            return true;
+        }
+        return false;
+    }
+
+    public void setVisibilityLocationProvider(
+            VisibilityLocationProvider visibilityLocationProvider) {
+        mVisibilityLocationProvider = visibilityLocationProvider;
+    }
+
+    public void onReorderingFinished() {
+        mAllowedReorderViews.clear();
+        mAddedChildren.clear();
+        mLowPriorityReorderingViews.clear();
+    }
+
+    @Override
+    public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
+        if (isHeadsUp) {
+            // Heads up notifications should in general be allowed to reorder if they are out of
+            // view and stay at the current location if they aren't.
+            mAllowedReorderViews.add(entry.row);
+        }
+    }
+
+    public void onLowPriorityUpdated(NotificationData.Entry entry) {
+        mLowPriorityReorderingViews.add(entry.row);
+    }
+
+    /**
+     * Notify the visual stability manager that a new view was added and should be allowed to
+     * reorder next time.
+     */
+    public void notifyViewAddition(View view) {
+        mAddedChildren.add(view);
+    }
+
+    public interface Callback {
+        /**
+         * Called when reordering is allowed again.
+         */
+        void onReorderingAllowed();
+    }
+
+}
diff --git a/com/android/systemui/statusbar/phone/AppButtonData.java b/com/android/systemui/statusbar/phone/AppButtonData.java
new file mode 100644
index 0000000..f6c1062
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/AppButtonData.java
@@ -0,0 +1,60 @@
+/*
+ * 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.phone;
+
+import android.app.ActivityManager.RecentTaskInfo;
+
+import java.util.ArrayList;
+
+/**
+ * Data associated with an app button.
+ */
+class AppButtonData {
+    public final AppInfo appInfo;
+    public boolean pinned;
+    // Recent tasks for this app, sorted by lastActiveTime, descending.
+    public ArrayList<RecentTaskInfo> tasks;
+
+    public AppButtonData(AppInfo appInfo, boolean pinned) {
+        this.appInfo = appInfo;
+        this.pinned = pinned;
+    }
+
+    public int getTaskCount() {
+        return tasks == null ? 0 : tasks.size();
+    }
+
+    /**
+     * Returns true if the button contains no useful information and should be removed.
+     */
+    public boolean isEmpty() {
+        return !pinned && getTaskCount() == 0;
+    }
+
+    public void addTask(RecentTaskInfo task) {
+        if (tasks == null) {
+            tasks = new ArrayList<RecentTaskInfo>();
+        }
+        tasks.add(task);
+    }
+
+    public void clearTasks() {
+        if (tasks != null) {
+            tasks.clear();
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/AppIconDragShadowBuilder.java b/com/android/systemui/statusbar/phone/AppIconDragShadowBuilder.java
new file mode 100644
index 0000000..b55b0a5
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/AppIconDragShadowBuilder.java
@@ -0,0 +1,57 @@
+/*
+ * 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.phone;
+
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.widget.ImageView;
+
+/** Creates a scaled-up version of an app icon for dragging. */
+class AppIconDragShadowBuilder extends View.DragShadowBuilder {
+    private final static int ICON_SCALE = 2;
+    final Drawable mDrawable;
+    final int mIconSize;  // Height and width in device-pixels.
+
+    public AppIconDragShadowBuilder(ImageView icon) {
+        mDrawable = icon.getDrawable();
+        // The Drawable may not be the same size as the ImageView, so use the ImageView size.
+        // The ImageView is not square because it has additional left and right padding to create
+        // a wider drop target, so use the height to create a square drag shadow.
+        mIconSize = icon.getHeight() * ICON_SCALE;
+    }
+
+    @Override
+    public void onProvideShadowMetrics(Point size, Point touch) {
+        size.set(mIconSize, mIconSize);
+        // Shift the drag shadow up slightly because the apps are at the bottom edge of the
+        // screen.
+        touch.set(mIconSize / 2, mIconSize * 2 / 3);
+    }
+
+    @Override
+    public void onDrawShadow(Canvas canvas) {
+        // The Drawable's native bounds may be different than the source ImageView. Force it
+        // to the correct size.
+        Rect oldBounds = mDrawable.copyBounds();
+        mDrawable.setBounds(0, 0, mIconSize, mIconSize);
+        mDrawable.draw(canvas);
+        mDrawable.setBounds(oldBounds);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/AppInfo.java b/com/android/systemui/statusbar/phone/AppInfo.java
new file mode 100644
index 0000000..8f0b532
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/AppInfo.java
@@ -0,0 +1,54 @@
+/*
+ * 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.phone;
+
+import android.content.ComponentName;
+import android.os.UserHandle;
+
+/**
+ * Navigation bar app information.
+ */
+class AppInfo {
+    private final ComponentName mComponentName;
+    private final UserHandle mUser;
+
+    public AppInfo(ComponentName componentName, UserHandle user) {
+        if (componentName == null || user == null) throw new IllegalArgumentException();
+        mComponentName = componentName;
+        mUser = user;
+    }
+
+    public ComponentName getComponentName() {
+        return mComponentName;
+    }
+
+    public UserHandle getUser() {
+        return mUser;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final AppInfo other = (AppInfo) obj;
+        return mComponentName.equals(other.mComponentName) && mUser.equals(other.mUser);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/AutoTileManager.java b/com/android/systemui/statusbar/phone/AutoTileManager.java
new file mode 100644
index 0000000..1bd90fa
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/AutoTileManager.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2016 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.phone;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings.Secure;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.NightDisplayController;
+import com.android.systemui.Dependency;
+import com.android.systemui.Prefs;
+import com.android.systemui.Prefs.Key;
+import com.android.systemui.qs.AutoAddTracker;
+import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.SecureSetting;
+import com.android.systemui.statusbar.policy.DataSaverController;
+import com.android.systemui.statusbar.policy.DataSaverController.Listener;
+import com.android.systemui.statusbar.policy.HotspotController;
+import com.android.systemui.statusbar.policy.HotspotController.Callback;
+
+/**
+ * Manages which tiles should be automatically added to QS.
+ */
+public class AutoTileManager {
+
+    public static final String HOTSPOT = "hotspot";
+    public static final String SAVER = "saver";
+    public static final String INVERSION = "inversion";
+    public static final String WORK = "work";
+    public static final String NIGHT = "night";
+    private final Context mContext;
+    private final QSTileHost mHost;
+    private final Handler mHandler;
+    private final AutoAddTracker mAutoTracker;
+
+    public AutoTileManager(Context context, QSTileHost host) {
+        mAutoTracker = new AutoAddTracker(context);
+        mContext = context;
+        mHost = host;
+        mHandler = new Handler((Looper) Dependency.get(Dependency.BG_LOOPER));
+        if (!mAutoTracker.isAdded(HOTSPOT)) {
+            Dependency.get(HotspotController.class).addCallback(mHotspotCallback);
+        }
+        if (!mAutoTracker.isAdded(SAVER)) {
+            Dependency.get(DataSaverController.class).addCallback(mDataSaverListener);
+        }
+        if (!mAutoTracker.isAdded(INVERSION)) {
+            mColorsSetting = new SecureSetting(mContext, mHandler,
+                    Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) {
+                @Override
+                protected void handleValueChanged(int value, boolean observedChange) {
+                    if (mAutoTracker.isAdded(INVERSION)) return;
+                    if (value != 0) {
+                        mHost.addTile(INVERSION);
+                        mAutoTracker.setTileAdded(INVERSION);
+                        mHandler.post(() -> mColorsSetting.setListening(false));
+                    }
+                }
+            };
+            mColorsSetting.setListening(true);
+        }
+        if (!mAutoTracker.isAdded(WORK)) {
+            Dependency.get(ManagedProfileController.class).addCallback(mProfileCallback);
+        }
+
+        if (!mAutoTracker.isAdded(NIGHT)
+                && NightDisplayController.isAvailable(mContext)) {
+            Dependency.get(NightDisplayController.class).setListener(mNightDisplayCallback);
+        }
+    }
+
+    public void destroy() {
+        mColorsSetting.setListening(false);
+        mAutoTracker.destroy();
+        Dependency.get(HotspotController.class).removeCallback(mHotspotCallback);
+        Dependency.get(DataSaverController.class).removeCallback(mDataSaverListener);
+        Dependency.get(ManagedProfileController.class).removeCallback(mProfileCallback);
+        Dependency.get(NightDisplayController.class).setListener(null);
+    }
+
+    private final ManagedProfileController.Callback mProfileCallback =
+            new ManagedProfileController.Callback() {
+                @Override
+                public void onManagedProfileChanged() {
+                    if (mAutoTracker.isAdded(WORK)) return;
+                    if (Dependency.get(ManagedProfileController.class).hasActiveProfile()) {
+                        mHost.addTile(WORK);
+                        mAutoTracker.setTileAdded(WORK);
+                        mHandler.post(() -> Dependency.get(ManagedProfileController.class)
+                                .removeCallback(mProfileCallback));
+                    }
+                }
+
+                @Override
+                public void onManagedProfileRemoved() {
+                }
+            };
+
+    private SecureSetting mColorsSetting;
+
+    private final DataSaverController.Listener mDataSaverListener = new Listener() {
+        @Override
+        public void onDataSaverChanged(boolean isDataSaving) {
+            if (mAutoTracker.isAdded(SAVER)) return;
+            if (isDataSaving) {
+                mHost.addTile(SAVER);
+                mAutoTracker.setTileAdded(SAVER);
+                mHandler.post(() -> Dependency.get(DataSaverController.class).removeCallback(
+                        mDataSaverListener));
+            }
+        }
+    };
+
+    private final HotspotController.Callback mHotspotCallback = new Callback() {
+        @Override
+        public void onHotspotChanged(boolean enabled) {
+            if (mAutoTracker.isAdded(HOTSPOT)) return;
+            if (enabled) {
+                mHost.addTile(HOTSPOT);
+                mAutoTracker.setTileAdded(HOTSPOT);
+                mHandler.post(() -> Dependency.get(HotspotController.class)
+                        .removeCallback(mHotspotCallback));
+            }
+        }
+    };
+
+    @VisibleForTesting
+    final NightDisplayController.Callback mNightDisplayCallback =
+            new NightDisplayController.Callback() {
+        @Override
+        public void onActivated(boolean activated) {
+            if (activated) {
+                addNightTile();
+            }
+        }
+
+        @Override
+        public void onAutoModeChanged(int autoMode) {
+            if (autoMode == NightDisplayController.AUTO_MODE_CUSTOM
+                    || autoMode == NightDisplayController.AUTO_MODE_TWILIGHT) {
+                addNightTile();
+            }
+        }
+
+        private void addNightTile() {
+            if (mAutoTracker.isAdded(NIGHT)) return;
+            mHost.addTile(NIGHT);
+            mAutoTracker.setTileAdded(NIGHT);
+            mHandler.post(() -> Dependency.get(NightDisplayController.class)
+                    .setListener(null));
+        }
+    };
+}
diff --git a/com/android/systemui/statusbar/phone/BarTransitions.java b/com/android/systemui/statusbar/phone/BarTransitions.java
new file mode 100644
index 0000000..1f44abe
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/BarTransitions.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2013 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.phone;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.drawable.Drawable;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.View;
+
+import com.android.settingslib.Utils;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+
+public class BarTransitions {
+    private static final boolean DEBUG = false;
+    private static final boolean DEBUG_COLORS = false;
+
+    public static final int MODE_OPAQUE = 0;
+    public static final int MODE_SEMI_TRANSPARENT = 1;
+    public static final int MODE_TRANSLUCENT = 2;
+    public static final int MODE_LIGHTS_OUT = 3;
+    public static final int MODE_TRANSPARENT = 4;
+    public static final int MODE_WARNING = 5;
+    public static final int MODE_LIGHTS_OUT_TRANSPARENT = 6;
+
+    public static final int LIGHTS_IN_DURATION = 250;
+    public static final int LIGHTS_OUT_DURATION = 750;
+    public static final int BACKGROUND_DURATION = 200;
+
+    private final String mTag;
+    private final View mView;
+    private final BarBackgroundDrawable mBarBackground;
+
+    private int mMode;
+    private boolean mAlwaysOpaque = false;
+
+    public BarTransitions(View view, int gradientResourceId) {
+        mTag = "BarTransitions." + view.getClass().getSimpleName();
+        mView = view;
+        mBarBackground = new BarBackgroundDrawable(mView.getContext(), gradientResourceId);
+        mView.setBackground(mBarBackground);
+    }
+
+    public int getMode() {
+        return mMode;
+    }
+
+    public void setAutoDim(boolean autoDim) {
+        // Default is don't care.
+    }
+
+    /**
+     * @param alwaysOpaque if {@code true}, the bar's background will always be opaque, regardless
+     *         of what mode it is currently set to.
+     */
+    public void setAlwaysOpaque(boolean alwaysOpaque) {
+        mAlwaysOpaque = alwaysOpaque;
+    }
+
+    public boolean isAlwaysOpaque() {
+        // Low-end devices do not support translucent modes, fallback to opaque
+        return mAlwaysOpaque;
+    }
+
+    public void transitionTo(int mode, boolean animate) {
+        if (isAlwaysOpaque() && (mode == MODE_SEMI_TRANSPARENT || mode == MODE_TRANSLUCENT
+                || mode == MODE_TRANSPARENT)) {
+            mode = MODE_OPAQUE;
+        }
+        if (isAlwaysOpaque() && (mode == MODE_LIGHTS_OUT_TRANSPARENT)) {
+            mode = MODE_LIGHTS_OUT;
+        }
+        if (mMode == mode) return;
+        int oldMode = mMode;
+        mMode = mode;
+        if (DEBUG) Log.d(mTag, String.format("%s -> %s animate=%s",
+                modeToString(oldMode), modeToString(mode),  animate));
+        onTransition(oldMode, mMode, animate);
+    }
+
+    protected void onTransition(int oldMode, int newMode, boolean animate) {
+        applyModeBackground(oldMode, newMode, animate);
+    }
+
+    protected void applyModeBackground(int oldMode, int newMode, boolean animate) {
+        if (DEBUG) Log.d(mTag, String.format("applyModeBackground oldMode=%s newMode=%s animate=%s",
+                modeToString(oldMode), modeToString(newMode), animate));
+        mBarBackground.applyModeBackground(oldMode, newMode, animate);
+    }
+
+    public static String modeToString(int mode) {
+        if (mode == MODE_OPAQUE) return "MODE_OPAQUE";
+        if (mode == MODE_SEMI_TRANSPARENT) return "MODE_SEMI_TRANSPARENT";
+        if (mode == MODE_TRANSLUCENT) return "MODE_TRANSLUCENT";
+        if (mode == MODE_LIGHTS_OUT) return "MODE_LIGHTS_OUT";
+        if (mode == MODE_TRANSPARENT) return "MODE_TRANSPARENT";
+        if (mode == MODE_WARNING) return "MODE_WARNING";
+        if (mode == MODE_LIGHTS_OUT_TRANSPARENT) return "MODE_LIGHTS_OUT_TRANSPARENT";
+        throw new IllegalArgumentException("Unknown mode " + mode);
+    }
+
+    public void finishAnimations() {
+        mBarBackground.finishAnimation();
+    }
+
+    protected boolean isLightsOut(int mode) {
+        return mode == MODE_LIGHTS_OUT || mode == MODE_LIGHTS_OUT_TRANSPARENT;
+    }
+
+    private static class BarBackgroundDrawable extends Drawable {
+        private final int mOpaque;
+        private final int mSemiTransparent;
+        private final int mTransparent;
+        private final int mWarning;
+        private final Drawable mGradient;
+
+        private int mMode = -1;
+        private boolean mAnimating;
+        private long mStartTime;
+        private long mEndTime;
+
+        private int mGradientAlpha;
+        private int mColor;
+        private PorterDuffColorFilter mTintFilter;
+        private Paint mPaint = new Paint();
+
+        private int mGradientAlphaStart;
+        private int mColorStart;
+
+
+        public BarBackgroundDrawable(Context context, int gradientResourceId) {
+            final Resources res = context.getResources();
+            if (DEBUG_COLORS) {
+                mOpaque = 0xff0000ff;
+                mSemiTransparent = 0x7f0000ff;
+                mTransparent = 0x2f0000ff;
+                mWarning = 0xffff0000;
+            } else {
+                mOpaque = context.getColor(R.color.system_bar_background_opaque);
+                mSemiTransparent = context.getColor(
+                        com.android.internal.R.color.system_bar_background_semi_transparent);
+                mTransparent = context.getColor(R.color.system_bar_background_transparent);
+                mWarning = Utils.getColorAttr(context, android.R.attr.colorError);
+            }
+            mGradient = context.getDrawable(gradientResourceId);
+        }
+
+        @Override
+        public void setAlpha(int alpha) {
+            // noop
+        }
+
+        @Override
+        public void setColorFilter(ColorFilter colorFilter) {
+            // noop
+        }
+
+        @Override
+        public void setTint(int color) {
+            if (mTintFilter == null) {
+                mTintFilter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN);
+            } else {
+                mTintFilter.setColor(color);
+            }
+            invalidateSelf();
+        }
+
+        @Override
+        public void setTintMode(Mode tintMode) {
+            if (mTintFilter == null) {
+                mTintFilter = new PorterDuffColorFilter(0, tintMode);
+            } else {
+                mTintFilter.setMode(tintMode);
+            }
+            invalidateSelf();
+        }
+
+        @Override
+        protected void onBoundsChange(Rect bounds) {
+            super.onBoundsChange(bounds);
+            mGradient.setBounds(bounds);
+        }
+
+        public void applyModeBackground(int oldMode, int newMode, boolean animate) {
+            if (mMode == newMode) return;
+            mMode = newMode;
+            mAnimating = animate;
+            if (animate) {
+                long now = SystemClock.elapsedRealtime();
+                mStartTime = now;
+                mEndTime = now + BACKGROUND_DURATION;
+                mGradientAlphaStart = mGradientAlpha;
+                mColorStart = mColor;
+            }
+            invalidateSelf();
+        }
+
+        @Override
+        public int getOpacity() {
+            return PixelFormat.TRANSLUCENT;
+        }
+
+        public void finishAnimation() {
+            if (mAnimating) {
+                mAnimating = false;
+                invalidateSelf();
+            }
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            int targetGradientAlpha = 0, targetColor = 0;
+            if (mMode == MODE_WARNING) {
+                targetColor = mWarning;
+            } else if (mMode == MODE_TRANSLUCENT) {
+                targetColor = mSemiTransparent;
+            } else if (mMode == MODE_SEMI_TRANSPARENT) {
+                targetColor = mSemiTransparent;
+            } else if (mMode == MODE_TRANSPARENT || mMode == MODE_LIGHTS_OUT_TRANSPARENT) {
+                targetColor = mTransparent;
+            } else {
+                targetColor = mOpaque;
+            }
+
+            if (!mAnimating) {
+                mColor = targetColor;
+                mGradientAlpha = targetGradientAlpha;
+            } else {
+                final long now = SystemClock.elapsedRealtime();
+                if (now >= mEndTime) {
+                    mAnimating = false;
+                    mColor = targetColor;
+                    mGradientAlpha = targetGradientAlpha;
+                } else {
+                    final float t = (now - mStartTime) / (float)(mEndTime - mStartTime);
+                    final float v = Math.max(0, Math.min(
+                            Interpolators.LINEAR.getInterpolation(t), 1));
+                    mGradientAlpha = (int)(v * targetGradientAlpha + mGradientAlphaStart * (1 - v));
+                    mColor = Color.argb(
+                          (int)(v * Color.alpha(targetColor) + Color.alpha(mColorStart) * (1 - v)),
+                          (int)(v * Color.red(targetColor) + Color.red(mColorStart) * (1 - v)),
+                          (int)(v * Color.green(targetColor) + Color.green(mColorStart) * (1 - v)),
+                          (int)(v * Color.blue(targetColor) + Color.blue(mColorStart) * (1 - v)));
+                }
+            }
+            if (mGradientAlpha > 0) {
+                mGradient.setAlpha(mGradientAlpha);
+                mGradient.draw(canvas);
+            }
+            if (Color.alpha(mColor) > 0) {
+                mPaint.setColor(mColor);
+                if (mTintFilter != null) {
+                    mPaint.setColorFilter(mTintFilter);
+                }
+                canvas.drawPaint(mPaint);
+            }
+            if (mAnimating) {
+                invalidateSelf();  // keep going
+            }
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/BounceInterpolator.java b/com/android/systemui/statusbar/phone/BounceInterpolator.java
new file mode 100644
index 0000000..0fdc185
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/BounceInterpolator.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.view.animation.Interpolator;
+
+/**
+ * An implementation of a bouncer interpolator optimized for unlock hinting.
+ */
+public class BounceInterpolator implements Interpolator {
+
+    private final static float SCALE_FACTOR = 7.5625f;
+
+    @Override
+    public float getInterpolation(float t) {
+        t *= 11f / 10f;
+        if (t < 4f / 11f) {
+            return SCALE_FACTOR * t * t;
+        } else if (t < 8f / 11f) {
+            float t2 = t - 6f / 11f;
+            return SCALE_FACTOR * t2 * t2 + 3f / 4f;
+        } else if (t < 10f / 11f) {
+            float t2 = t - 9f / 11f;
+            return SCALE_FACTOR * t2 * t2 + 15f / 16f;
+        } else {
+            return 1;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/ButtonDispatcher.java b/com/android/systemui/statusbar/phone/ButtonDispatcher.java
new file mode 100644
index 0000000..a83e659
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/ButtonDispatcher.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2016 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.phone;
+
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider.ButtonInterface;
+import com.android.systemui.statusbar.policy.KeyButtonDrawable;
+
+import java.util.ArrayList;
+
+/**
+ * Dispatches common view calls to multiple views.  This is used to handle
+ * multiples of the same nav bar icon appearing.
+ */
+public class ButtonDispatcher {
+
+    private final ArrayList<View> mViews = new ArrayList<>();
+
+    private final int mId;
+
+    private View.OnClickListener mClickListener;
+    private View.OnTouchListener mTouchListener;
+    private View.OnLongClickListener mLongClickListener;
+    private Boolean mLongClickable;
+    private Integer mAlpha;
+    private Float mDarkIntensity;
+    private Integer mVisibility = -1;
+    private KeyButtonDrawable mImageDrawable;
+    private View mCurrentView;
+    private boolean mVertical;
+
+    public ButtonDispatcher(int id) {
+        mId = id;
+    }
+
+    void clear() {
+        mViews.clear();
+    }
+
+    void addView(View view) {
+        mViews.add(view);
+        view.setOnClickListener(mClickListener);
+        view.setOnTouchListener(mTouchListener);
+        view.setOnLongClickListener(mLongClickListener);
+        if (mLongClickable != null) {
+            view.setLongClickable(mLongClickable);
+        }
+        if (mAlpha != null) {
+            view.setAlpha(mAlpha);
+        }
+        if (mDarkIntensity != null) {
+            ((ButtonInterface) view).setDarkIntensity(mDarkIntensity);
+        }
+        if (mVisibility != null) {
+            view.setVisibility(mVisibility);
+        }
+        if (mImageDrawable != null) {
+            ((ButtonInterface) view).setImageDrawable(mImageDrawable);
+        }
+
+        if (view instanceof  ButtonInterface) {
+            ((ButtonInterface) view).setVertical(mVertical);
+        }
+    }
+
+    public int getId() {
+        return mId;
+    }
+
+    public int getVisibility() {
+        return mVisibility != null ? mVisibility : View.VISIBLE;
+    }
+
+    public float getAlpha() {
+        return mAlpha != null ? mAlpha : 1;
+    }
+
+    public void setImageDrawable(KeyButtonDrawable drawable) {
+        mImageDrawable = drawable;
+        final int N = mViews.size();
+        for (int i = 0; i < N; i++) {
+            ((ButtonInterface) mViews.get(i)).setImageDrawable(mImageDrawable);
+        }
+    }
+
+    public void setVisibility(int visibility) {
+        if (mVisibility == visibility) return;
+        mVisibility = visibility;
+        final int N = mViews.size();
+        for (int i = 0; i < N; i++) {
+            mViews.get(i).setVisibility(mVisibility);
+        }
+    }
+
+    public void abortCurrentGesture() {
+        // This seems to be an instantaneous thing, so not going to persist it.
+        final int N = mViews.size();
+        for (int i = 0; i < N; i++) {
+            ((ButtonInterface) mViews.get(i)).abortCurrentGesture();
+        }
+    }
+
+    public void setAlpha(int alpha) {
+        mAlpha = alpha;
+        final int N = mViews.size();
+        for (int i = 0; i < N; i++) {
+            mViews.get(i).setAlpha(alpha);
+        }
+    }
+
+    public void setDarkIntensity(float darkIntensity) {
+        mDarkIntensity = darkIntensity;
+        final int N = mViews.size();
+        for (int i = 0; i < N; i++) {
+            ((ButtonInterface) mViews.get(i)).setDarkIntensity(darkIntensity);
+        }
+    }
+
+    public void setOnClickListener(View.OnClickListener clickListener) {
+        mClickListener = clickListener;
+        final int N = mViews.size();
+        for (int i = 0; i < N; i++) {
+            mViews.get(i).setOnClickListener(mClickListener);
+        }
+    }
+
+    public void setOnTouchListener(View.OnTouchListener touchListener) {
+        mTouchListener = touchListener;
+        final int N = mViews.size();
+        for (int i = 0; i < N; i++) {
+            mViews.get(i).setOnTouchListener(mTouchListener);
+        }
+    }
+
+    public void setLongClickable(boolean isLongClickable) {
+        mLongClickable = isLongClickable;
+        final int N = mViews.size();
+        for (int i = 0; i < N; i++) {
+            mViews.get(i).setLongClickable(mLongClickable);
+        }
+    }
+
+    public void setOnLongClickListener(View.OnLongClickListener longClickListener) {
+        mLongClickListener = longClickListener;
+        final int N = mViews.size();
+        for (int i = 0; i < N; i++) {
+            mViews.get(i).setOnLongClickListener(mLongClickListener);
+        }
+    }
+
+    public ArrayList<View> getViews() {
+        return mViews;
+    }
+
+    public View getCurrentView() {
+        return mCurrentView;
+    }
+
+    public void setCurrentView(View currentView) {
+        mCurrentView = currentView.findViewById(mId);
+    }
+
+    public void setVertical(boolean vertical) {
+        mVertical = vertical;
+        final int N = mViews.size();
+        for (int i = 0; i < N; i++) {
+            final View view = mViews.get(i);
+            if (view instanceof ButtonInterface) {
+                ((ButtonInterface) view).setVertical(vertical);
+            }
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
new file mode 100644
index 0000000..2c3f452
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2017 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.phone;
+
+import static android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS;
+import static android.app.StatusBarManager.DISABLE_SYSTEM_INFO;
+
+import static com.android.systemui.statusbar.phone.StatusBar.reinflateSignalCluster;
+
+import android.annotation.Nullable;
+import android.app.Fragment;
+import android.app.StatusBarManager;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.widget.LinearLayout;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.SignalClusterView;
+import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+import com.android.systemui.statusbar.policy.EncryptionHelper;
+import com.android.systemui.statusbar.policy.KeyguardMonitor;
+import com.android.systemui.statusbar.policy.NetworkController;
+import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+
+/**
+ * Contains the collapsed status bar and handles hiding/showing based on disable flags
+ * and keyguard state. Also manages lifecycle to make sure the views it contains are being
+ * updated by the StatusBarIconController and DarkIconManager while it is attached.
+ */
+public class CollapsedStatusBarFragment extends Fragment implements CommandQueue.Callbacks {
+
+    public static final String TAG = "CollapsedStatusBarFragment";
+    private static final String EXTRA_PANEL_STATE = "panel_state";
+    private PhoneStatusBarView mStatusBar;
+    private KeyguardMonitor mKeyguardMonitor;
+    private NetworkController mNetworkController;
+    private LinearLayout mSystemIconArea;
+    private View mNotificationIconAreaInner;
+    private int mDisabled1;
+    private StatusBar mStatusBarComponent;
+    private DarkIconManager mDarkIconManager;
+    private SignalClusterView mSignalClusterView;
+
+    private SignalCallback mSignalCallback = new SignalCallback() {
+        @Override
+        public void setIsAirplaneMode(NetworkController.IconState icon) {
+            mStatusBarComponent.recomputeDisableFlags(true /* animate */);
+        }
+    };
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
+        mNetworkController = Dependency.get(NetworkController.class);
+        mStatusBarComponent = SysUiServiceProvider.getComponent(getContext(), StatusBar.class);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+            Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.status_bar, container, false);
+    }
+
+    @Override
+    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        mStatusBar = (PhoneStatusBarView) view;
+        if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_PANEL_STATE)) {
+            mStatusBar.go(savedInstanceState.getInt(EXTRA_PANEL_STATE));
+        }
+        mDarkIconManager = new DarkIconManager(view.findViewById(R.id.statusIcons));
+        Dependency.get(StatusBarIconController.class).addIconGroup(mDarkIconManager);
+        mSystemIconArea = mStatusBar.findViewById(R.id.system_icon_area);
+        mSignalClusterView = mStatusBar.findViewById(R.id.signal_cluster);
+        Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mSignalClusterView);
+        // Default to showing until we know otherwise.
+        showSystemIconArea(false);
+        initEmergencyCryptkeeperText();
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putInt(EXTRA_PANEL_STATE, mStatusBar.getState());
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).addCallbacks(this);
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).removeCallbacks(this);
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mSignalClusterView);
+        Dependency.get(StatusBarIconController.class).removeIconGroup(mDarkIconManager);
+        if (mNetworkController.hasEmergencyCryptKeeperText()) {
+            mNetworkController.removeCallback(mSignalCallback);
+        }
+    }
+
+    public void initNotificationIconArea(NotificationIconAreaController
+            notificationIconAreaController) {
+        ViewGroup notificationIconArea = mStatusBar.findViewById(R.id.notification_icon_area);
+        mNotificationIconAreaInner =
+                notificationIconAreaController.getNotificationInnerAreaView();
+        if (mNotificationIconAreaInner.getParent() != null) {
+            ((ViewGroup) mNotificationIconAreaInner.getParent())
+                    .removeView(mNotificationIconAreaInner);
+        }
+        notificationIconArea.addView(mNotificationIconAreaInner);
+        // Default to showing until we know otherwise.
+        showNotificationIconArea(false);
+    }
+
+    @Override
+    public void disable(int state1, int state2, boolean animate) {
+        state1 = adjustDisableFlags(state1);
+        final int old1 = mDisabled1;
+        final int diff1 = state1 ^ old1;
+        mDisabled1 = state1;
+        if ((diff1 & DISABLE_SYSTEM_INFO) != 0) {
+            if ((state1 & DISABLE_SYSTEM_INFO) != 0) {
+                hideSystemIconArea(animate);
+            } else {
+                showSystemIconArea(animate);
+            }
+        }
+        if ((diff1 & DISABLE_NOTIFICATION_ICONS) != 0) {
+            if ((state1 & DISABLE_NOTIFICATION_ICONS) != 0) {
+                hideNotificationIconArea(animate);
+            } else {
+                showNotificationIconArea(animate);
+            }
+        }
+    }
+
+    protected int adjustDisableFlags(int state) {
+        if (!mStatusBarComponent.isLaunchTransitionFadingAway()
+                && !mKeyguardMonitor.isKeyguardFadingAway()
+                && shouldHideNotificationIcons()) {
+            state |= DISABLE_NOTIFICATION_ICONS;
+            state |= DISABLE_SYSTEM_INFO;
+        }
+        if (mNetworkController != null && EncryptionHelper.IS_DATA_ENCRYPTED) {
+            if (mNetworkController.hasEmergencyCryptKeeperText()) {
+                state |= DISABLE_NOTIFICATION_ICONS;
+            }
+            if (!mNetworkController.isRadioOn()) {
+                state |= DISABLE_SYSTEM_INFO;
+            }
+        }
+        return state;
+    }
+
+    private boolean shouldHideNotificationIcons() {
+        if (!mStatusBar.isClosed() && mStatusBarComponent.hideStatusBarIconsWhenExpanded()) {
+            return true;
+        }
+        if (mStatusBarComponent.hideStatusBarIconsForBouncer()) {
+            return true;
+        }
+        return false;
+    }
+
+    public void hideSystemIconArea(boolean animate) {
+        animateHide(mSystemIconArea, animate);
+    }
+
+    public void showSystemIconArea(boolean animate) {
+        animateShow(mSystemIconArea, animate);
+    }
+
+    public void hideNotificationIconArea(boolean animate) {
+        animateHide(mNotificationIconAreaInner, animate);
+    }
+
+    public void showNotificationIconArea(boolean animate) {
+        animateShow(mNotificationIconAreaInner, animate);
+    }
+
+    /**
+     * Hides a view.
+     */
+    private void animateHide(final View v, boolean animate) {
+        v.animate().cancel();
+        if (!animate) {
+            v.setAlpha(0f);
+            v.setVisibility(View.INVISIBLE);
+            return;
+        }
+        v.animate()
+                .alpha(0f)
+                .setDuration(160)
+                .setStartDelay(0)
+                .setInterpolator(Interpolators.ALPHA_OUT)
+                .withEndAction(() -> v.setVisibility(View.INVISIBLE));
+    }
+
+    /**
+     * Shows a view, and synchronizes the animation with Keyguard exit animations, if applicable.
+     */
+    private void animateShow(View v, boolean animate) {
+        v.animate().cancel();
+        v.setVisibility(View.VISIBLE);
+        if (!animate) {
+            v.setAlpha(1f);
+            return;
+        }
+        v.animate()
+                .alpha(1f)
+                .setDuration(320)
+                .setInterpolator(Interpolators.ALPHA_IN)
+                .setStartDelay(50)
+
+                // We need to clean up any pending end action from animateHide if we call
+                // both hide and show in the same frame before the animation actually gets started.
+                // cancel() doesn't really remove the end action.
+                .withEndAction(null);
+
+        // Synchronize the motion with the Keyguard fading if necessary.
+        if (mKeyguardMonitor.isKeyguardFadingAway()) {
+            v.animate()
+                    .setDuration(mKeyguardMonitor.getKeyguardFadingAwayDuration())
+                    .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
+                    .setStartDelay(mKeyguardMonitor.getKeyguardFadingAwayDelay())
+                    .start();
+        }
+    }
+
+    private void initEmergencyCryptkeeperText() {
+        View emergencyViewStub = mStatusBar.findViewById(R.id.emergency_cryptkeeper_text);
+        if (mNetworkController.hasEmergencyCryptKeeperText()) {
+            if (emergencyViewStub != null) {
+                ((ViewStub) emergencyViewStub).inflate();
+            }
+            mNetworkController.addCallback(mSignalCallback);
+        } else if (emergencyViewStub != null) {
+            ViewGroup parent = (ViewGroup) emergencyViewStub.getParent();
+            parent.removeView(emergencyViewStub);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.java b/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.java
new file mode 100644
index 0000000..6f53844
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2017 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.phone;
+
+import android.content.Context;
+import android.content.om.IOverlayManager;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.os.LocaleList;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+
+import com.android.systemui.ConfigurationChangedReceiver;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ConfigurationControllerImpl implements ConfigurationController,
+        ConfigurationChangedReceiver {
+
+    private final ArrayList<ConfigurationListener> mListeners = new ArrayList<>();
+    private final Configuration mLastConfig = new Configuration();
+    private int mDensity;
+    private float mFontScale;
+    private boolean mInCarMode;
+    private int mUiMode;
+    private LocaleList mLocaleList;
+
+    public ConfigurationControllerImpl(Context context) {
+        Configuration currentConfig = context.getResources().getConfiguration();
+        mFontScale = currentConfig.fontScale;
+        mDensity = currentConfig.densityDpi;
+        mInCarMode = (currentConfig.uiMode  & Configuration.UI_MODE_TYPE_MASK)
+                == Configuration.UI_MODE_TYPE_CAR;
+        mUiMode = currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
+        mLocaleList = currentConfig.getLocales();
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        // Avoid concurrent modification exception
+        ArrayList<ConfigurationListener> listeners = new ArrayList<>(mListeners);
+
+        listeners.forEach(l -> {
+            if (mListeners.contains(l)) {
+                l.onConfigChanged(newConfig);
+            }
+        });
+        final float fontScale = newConfig.fontScale;
+        final int density = newConfig.densityDpi;
+        int uiMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
+        if (density != mDensity || fontScale != mFontScale
+                || (mInCarMode && uiMode != mUiMode)) {
+            listeners.forEach(l -> {
+                if (mListeners.contains(l)) {
+                    l.onDensityOrFontScaleChanged();
+                }
+            });
+            mDensity = density;
+            mFontScale = fontScale;
+            mUiMode = uiMode;
+        }
+
+        final LocaleList localeList = newConfig.getLocales();
+        if (!localeList.equals(mLocaleList)) {
+            mLocaleList = localeList;
+            listeners.forEach(l -> {
+                if (mListeners.contains(l)) {
+                    l.onLocaleListChanged();
+                }
+            });
+        }
+
+        if ((mLastConfig.updateFrom(newConfig) & ActivityInfo.CONFIG_ASSETS_PATHS) != 0) {
+                listeners.forEach(l -> {
+                    if (mListeners.contains(l)) {
+                        l.onOverlayChanged();
+                    }
+                });
+        }
+    }
+
+    @Override
+    public void addCallback(ConfigurationListener listener) {
+        mListeners.add(listener);
+        listener.onDensityOrFontScaleChanged();
+    }
+
+    @Override
+    public void removeCallback(ConfigurationListener listener) {
+        mListeners.remove(listener);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java b/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java
new file mode 100644
index 0000000..3f9ae80
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2017 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.phone;
+
+import static com.android.systemui.statusbar.policy.DarkIconDispatcher.getTint;
+
+import android.animation.ArgbEvaluator;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Rect;
+import android.util.ArrayMap;
+import android.widget.ImageView;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+
+public class DarkIconDispatcherImpl implements DarkIconDispatcher {
+
+    private final LightBarTransitionsController mTransitionsController;
+    private final Rect mTintArea = new Rect();
+    private final ArrayMap<Object, DarkReceiver> mReceivers = new ArrayMap<>();
+
+    private int mIconTint = DEFAULT_ICON_TINT;
+    private float mDarkIntensity;
+    private int mDarkModeIconColorSingleTone;
+    private int mLightModeIconColorSingleTone;
+
+    public DarkIconDispatcherImpl(Context context) {
+        mDarkModeIconColorSingleTone = context.getColor(R.color.dark_mode_icon_color_single_tone);
+        mLightModeIconColorSingleTone = context.getColor(R.color.light_mode_icon_color_single_tone);
+
+        mTransitionsController = new LightBarTransitionsController(context,
+                this::setIconTintInternal);
+    }
+
+    public LightBarTransitionsController getTransitionsController() {
+        return mTransitionsController;
+    }
+
+    public void addDarkReceiver(DarkReceiver receiver) {
+        mReceivers.put(receiver, receiver);
+        receiver.onDarkChanged(mTintArea, mDarkIntensity, mIconTint);
+    }
+
+    public void addDarkReceiver(ImageView imageView) {
+        DarkReceiver receiver = (area, darkIntensity, tint) -> imageView.setImageTintList(
+                ColorStateList.valueOf(getTint(mTintArea, imageView, mIconTint)));
+        mReceivers.put(imageView, receiver);
+        receiver.onDarkChanged(mTintArea, mDarkIntensity, mIconTint);
+    }
+
+    public void removeDarkReceiver(DarkReceiver object) {
+        mReceivers.remove(object);
+    }
+
+    public void removeDarkReceiver(ImageView object) {
+        mReceivers.remove(object);
+    }
+
+    public void applyDark(ImageView object) {
+        mReceivers.get(object).onDarkChanged(mTintArea, mDarkIntensity, mIconTint);
+    }
+
+    /**
+     * Sets the dark area so {@link #setIconsDark} only affects the icons in the specified area.
+     *
+     * @param darkArea the area in which icons should change it's tint, in logical screen
+     *                 coordinates
+     */
+    public void setIconsDarkArea(Rect darkArea) {
+        if (darkArea == null && mTintArea.isEmpty()) {
+            return;
+        }
+        if (darkArea == null) {
+            mTintArea.setEmpty();
+        } else {
+            mTintArea.set(darkArea);
+        }
+        applyIconTint();
+    }
+
+    private void setIconTintInternal(float darkIntensity) {
+        mDarkIntensity = darkIntensity;
+        mIconTint = (int) ArgbEvaluator.getInstance().evaluate(darkIntensity,
+                mLightModeIconColorSingleTone, mDarkModeIconColorSingleTone);
+        applyIconTint();
+    }
+
+    private void applyIconTint() {
+        for (int i = 0; i < mReceivers.size(); i++) {
+            mReceivers.valueAt(i).onDarkChanged(mTintArea, mDarkIntensity, mIconTint);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/com/android/systemui/statusbar/phone/DemoStatusIcons.java
new file mode 100644
index 0000000..c499619
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/DemoStatusIcons.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2013 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.phone;
+
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.DemoMode;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.policy.LocationControllerImpl;
+
+public class DemoStatusIcons extends LinearLayout implements DemoMode {
+    private final LinearLayout mStatusIcons;
+    private final int mIconSize;
+
+    private boolean mDemoMode;
+
+    public DemoStatusIcons(LinearLayout statusIcons, int iconSize) {
+        super(statusIcons.getContext());
+        mStatusIcons = statusIcons;
+        mIconSize = iconSize;
+
+        setLayoutParams(mStatusIcons.getLayoutParams());
+        setOrientation(mStatusIcons.getOrientation());
+        setGravity(Gravity.CENTER_VERTICAL); // no LL.getGravity()
+        ViewGroup p = (ViewGroup) mStatusIcons.getParent();
+        p.addView(this, p.indexOfChild(mStatusIcons));
+    }
+
+    @Override
+    public void dispatchDemoCommand(String command, Bundle args) {
+        if (!mDemoMode && command.equals(COMMAND_ENTER)) {
+            mDemoMode = true;
+            mStatusIcons.setVisibility(View.GONE);
+            setVisibility(View.VISIBLE);
+        } else if (mDemoMode && command.equals(COMMAND_EXIT)) {
+            mDemoMode = false;
+            mStatusIcons.setVisibility(View.VISIBLE);
+            setVisibility(View.GONE);
+        } else if (mDemoMode && command.equals(COMMAND_STATUS)) {
+            String volume = args.getString("volume");
+            if (volume != null) {
+                int iconId = volume.equals("vibrate") ? R.drawable.stat_sys_ringer_vibrate
+                        : 0;
+                updateSlot("volume", null, iconId);
+            }
+            String zen = args.getString("zen");
+            if (zen != null) {
+                int iconId = zen.equals("important") ? R.drawable.stat_sys_zen_important
+                        : zen.equals("none") ? R.drawable.stat_sys_zen_none
+                        : 0;
+                updateSlot("zen", null, iconId);
+            }
+            String bt = args.getString("bluetooth");
+            if (bt != null) {
+                int iconId = bt.equals("disconnected") ? R.drawable.stat_sys_data_bluetooth
+                        : bt.equals("connected") ? R.drawable.stat_sys_data_bluetooth_connected
+                        : 0;
+                updateSlot("bluetooth", null, iconId);
+            }
+            String location = args.getString("location");
+            if (location != null) {
+                int iconId = location.equals("show") ? PhoneStatusBarPolicy.LOCATION_STATUS_ICON_ID
+                        : 0;
+                updateSlot("location", null, iconId);
+            }
+            String alarm = args.getString("alarm");
+            if (alarm != null) {
+                int iconId = alarm.equals("show") ? R.drawable.stat_sys_alarm
+                        : 0;
+                updateSlot("alarm_clock", null, iconId);
+            }
+            String tty = args.getString("tty");
+            if (tty != null) {
+                int iconId = tty.equals("show") ? R.drawable.stat_sys_tty_mode
+                        : 0;
+                updateSlot("tty", null, iconId);
+            }
+            String mute = args.getString("mute");
+            if (mute != null) {
+                int iconId = mute.equals("show") ? android.R.drawable.stat_notify_call_mute
+                        : 0;
+                updateSlot("mute", null, iconId);
+            }
+            String speakerphone = args.getString("speakerphone");
+            if (speakerphone != null) {
+                int iconId = speakerphone.equals("show") ? android.R.drawable.stat_sys_speakerphone
+                        : 0;
+                updateSlot("speakerphone", null, iconId);
+            }
+            String cast = args.getString("cast");
+            if (cast != null) {
+                int iconId = cast.equals("show") ? R.drawable.stat_sys_cast : 0;
+                updateSlot("cast", null, iconId);
+            }
+            String hotspot = args.getString("hotspot");
+            if (hotspot != null) {
+                int iconId = hotspot.equals("show") ? R.drawable.stat_sys_hotspot : 0;
+                updateSlot("hotspot", null, iconId);
+            }
+        }
+    }
+
+    private void updateSlot(String slot, String iconPkg, int iconId) {
+        if (!mDemoMode) return;
+        if (iconPkg == null) {
+            iconPkg = mContext.getPackageName();
+        }
+        int removeIndex = -1;
+        for (int i = 0; i < getChildCount(); i++) {
+            StatusBarIconView v = (StatusBarIconView) getChildAt(i);
+            if (slot.equals(v.getTag())) {
+                if (iconId == 0) {
+                    removeIndex = i;
+                    break;
+                } else {
+                    StatusBarIcon icon = v.getStatusBarIcon();
+                    icon.icon = Icon.createWithResource(icon.icon.getResPackage(), iconId);
+                    v.set(icon);
+                    v.updateDrawable();
+                    return;
+                }
+            }
+        }
+        if (iconId == 0) {
+            if (removeIndex != -1) {
+                removeViewAt(removeIndex);
+            }
+            return;
+        }
+        StatusBarIcon icon = new StatusBarIcon(iconPkg, UserHandle.SYSTEM, iconId, 0, 0, "Demo");
+        StatusBarIconView v = new StatusBarIconView(getContext(), null, null);
+        v.setTag(slot);
+        v.set(icon);
+        addView(v, 0, new LinearLayout.LayoutParams(mIconSize, mIconSize));
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/DoubleTapHelper.java b/com/android/systemui/statusbar/phone/DoubleTapHelper.java
new file mode 100644
index 0000000..dcb6a38
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/DoubleTapHelper.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2017 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.phone;
+
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import com.android.systemui.R;
+
+/**
+ * Detects a double tap.
+ */
+public class DoubleTapHelper {
+
+    private static final long DOUBLETAP_TIMEOUT_MS = 1200;
+
+    private final View mView;
+    private final ActivationListener mActivationListener;
+    private final DoubleTapListener mDoubleTapListener;
+    private final SlideBackListener mSlideBackListener;
+    private final DoubleTapLogListener mDoubleTapLogListener;
+
+    private float mTouchSlop;
+    private float mDoubleTapSlop;
+
+    private boolean mActivated;
+
+    private float mDownX;
+    private float mDownY;
+    private boolean mTrackTouch;
+
+    private float mActivationX;
+    private float mActivationY;
+    private Runnable mTapTimeoutRunnable = this::makeInactive;
+
+    public DoubleTapHelper(View view, ActivationListener activationListener,
+            DoubleTapListener doubleTapListener, SlideBackListener slideBackListener,
+            DoubleTapLogListener doubleTapLogListener) {
+        mTouchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop();
+        mDoubleTapSlop = view.getResources().getDimension(R.dimen.double_tap_slop);
+        mView = view;
+
+        mActivationListener = activationListener;
+        mDoubleTapListener = doubleTapListener;
+        mSlideBackListener = slideBackListener;
+        mDoubleTapLogListener = doubleTapLogListener;
+    }
+
+    public boolean onTouchEvent(MotionEvent event) {
+        return onTouchEvent(event, Integer.MAX_VALUE);
+    }
+
+    public boolean onTouchEvent(MotionEvent event, int maxTouchableHeight) {
+        int action = event.getActionMasked();
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                mDownX = event.getX();
+                mDownY = event.getY();
+                mTrackTouch = true;
+                if (mDownY > maxTouchableHeight) {
+                    mTrackTouch = false;
+                }
+                break;
+            case MotionEvent.ACTION_MOVE:
+                if (!isWithinTouchSlop(event)) {
+                    makeInactive();
+                    mTrackTouch = false;
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                if (isWithinTouchSlop(event)) {
+                    if (mSlideBackListener != null && mSlideBackListener.onSlideBack()) {
+                        return true;
+                    }
+                    if (!mActivated) {
+                        makeActive();
+                        mView.postDelayed(mTapTimeoutRunnable, DOUBLETAP_TIMEOUT_MS);
+                        mActivationX = event.getX();
+                        mActivationY = event.getY();
+                    } else {
+                        boolean withinDoubleTapSlop = isWithinDoubleTapSlop(event);
+                        if (mDoubleTapLogListener != null) {
+                            mDoubleTapLogListener.onDoubleTapLog(withinDoubleTapSlop,
+                                    event.getX() - mActivationX,
+                                    event.getY() - mActivationY);
+                        }
+                        if (withinDoubleTapSlop) {
+                            if (!mDoubleTapListener.onDoubleTap()) {
+                                return false;
+                            }
+                        } else {
+                            makeInactive();
+                            mTrackTouch = false;
+                        }
+                    }
+                } else {
+                    makeInactive();
+                    mTrackTouch = false;
+                }
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                makeInactive();
+                mTrackTouch = false;
+                break;
+            default:
+                break;
+        }
+        return mTrackTouch;
+    }
+
+    private void makeActive() {
+        if (!mActivated) {
+            mActivated = true;
+            mActivationListener.onActiveChanged(true);
+        }
+    }
+
+    private void makeInactive() {
+        if (mActivated) {
+            mActivated = false;
+            mActivationListener.onActiveChanged(false);
+        }
+    }
+
+    private boolean isWithinTouchSlop(MotionEvent event) {
+        return Math.abs(event.getX() - mDownX) < mTouchSlop
+                && Math.abs(event.getY() - mDownY) < mTouchSlop;
+    }
+
+    private boolean isWithinDoubleTapSlop(MotionEvent event) {
+        if (!mActivated) {
+            // If we're not activated there's no double tap slop to satisfy.
+            return true;
+        }
+
+        return Math.abs(event.getX() - mActivationX) < mDoubleTapSlop
+                && Math.abs(event.getY() - mActivationY) < mDoubleTapSlop;
+    }
+
+    @FunctionalInterface
+    public interface ActivationListener {
+        void onActiveChanged(boolean active);
+    }
+
+    @FunctionalInterface
+    public interface DoubleTapListener {
+        boolean onDoubleTap();
+    }
+
+    @FunctionalInterface
+    public interface SlideBackListener {
+        boolean onSlideBack();
+    }
+
+    @FunctionalInterface
+    public interface DoubleTapLogListener {
+        void onDoubleTapLog(boolean accepted, float dx, float dy);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/DozeParameters.java b/com/android/systemui/statusbar/phone/DozeParameters.java
new file mode 100644
index 0000000..6b7397b
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.content.Context;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.MathUtils;
+import android.util.SparseBooleanArray;
+
+import com.android.internal.hardware.AmbientDisplayConfiguration;
+import com.android.systemui.R;
+
+import java.io.PrintWriter;
+
+public class DozeParameters {
+    private static final int MAX_DURATION = 60 * 1000;
+    public static final String DOZE_SENSORS_WAKE_UP_FULLY = "doze_sensors_wake_up_fully";
+
+    private final Context mContext;
+    private final AmbientDisplayConfiguration mAmbientDisplayConfiguration;
+
+    private static IntInOutMatcher sPickupSubtypePerformsProxMatcher;
+
+    public DozeParameters(Context context) {
+        mContext = context;
+        mAmbientDisplayConfiguration = new AmbientDisplayConfiguration(mContext);
+    }
+
+    public void dump(PrintWriter pw) {
+        pw.println("  DozeParameters:");
+        pw.print("    getDisplayStateSupported(): "); pw.println(getDisplayStateSupported());
+        pw.print("    getPulseDuration(pickup=false): "); pw.println(getPulseDuration(false));
+        pw.print("    getPulseDuration(pickup=true): "); pw.println(getPulseDuration(true));
+        pw.print("    getPulseInDuration(pickup=false): "); pw.println(getPulseInDuration(false));
+        pw.print("    getPulseInDuration(pickup=true): "); pw.println(getPulseInDuration(true));
+        pw.print("    getPulseInVisibleDuration(): "); pw.println(getPulseVisibleDuration());
+        pw.print("    getPulseOutDuration(): "); pw.println(getPulseOutDuration());
+        pw.print("    getPulseOnSigMotion(): "); pw.println(getPulseOnSigMotion());
+        pw.print("    getVibrateOnSigMotion(): "); pw.println(getVibrateOnSigMotion());
+        pw.print("    getVibrateOnPickup(): "); pw.println(getVibrateOnPickup());
+        pw.print("    getProxCheckBeforePulse(): "); pw.println(getProxCheckBeforePulse());
+        pw.print("    getPickupVibrationThreshold(): "); pw.println(getPickupVibrationThreshold());
+        pw.print("    getPickupSubtypePerformsProxCheck(): ");pw.println(
+                dumpPickupSubtypePerformsProxCheck());
+    }
+
+    private String dumpPickupSubtypePerformsProxCheck() {
+        // Refresh sPickupSubtypePerformsProxMatcher
+        getPickupSubtypePerformsProxCheck(0);
+
+        if (sPickupSubtypePerformsProxMatcher == null) {
+            return "fallback: " + mContext.getResources().getBoolean(
+                    R.bool.doze_pickup_performs_proximity_check);
+        } else {
+            return "spec: " + sPickupSubtypePerformsProxMatcher.mSpec;
+        }
+    }
+
+    public boolean getDisplayStateSupported() {
+        return getBoolean("doze.display.supported", R.bool.doze_display_state_supported);
+    }
+
+    public boolean getDozeSuspendDisplayStateSupported() {
+        return mContext.getResources().getBoolean(R.bool.doze_suspend_display_state_supported);
+    }
+
+    public int getPulseDuration(boolean pickup) {
+        return getPulseInDuration(pickup) + getPulseVisibleDuration() + getPulseOutDuration();
+    }
+
+    public int getPulseInDuration(boolean pickupOrDoubleTap) {
+        return pickupOrDoubleTap
+                ? getInt("doze.pulse.duration.in.pickup", R.integer.doze_pulse_duration_in_pickup)
+                : getInt("doze.pulse.duration.in", R.integer.doze_pulse_duration_in);
+    }
+
+    public int getPulseVisibleDuration() {
+        return getInt("doze.pulse.duration.visible", R.integer.doze_pulse_duration_visible);
+    }
+
+    public int getPulseOutDuration() {
+        return getInt("doze.pulse.duration.out", R.integer.doze_pulse_duration_out);
+    }
+
+    public boolean getPulseOnSigMotion() {
+        return getBoolean("doze.pulse.sigmotion", R.bool.doze_pulse_on_significant_motion);
+    }
+
+    public boolean getVibrateOnSigMotion() {
+        return SystemProperties.getBoolean("doze.vibrate.sigmotion", false);
+    }
+
+    public boolean getVibrateOnPickup() {
+        return SystemProperties.getBoolean("doze.vibrate.pickup", false);
+    }
+
+    public boolean getProxCheckBeforePulse() {
+        return getBoolean("doze.pulse.proxcheck", R.bool.doze_proximity_check_before_pulse);
+    }
+
+    public int getPickupVibrationThreshold() {
+        return getInt("doze.pickup.vibration.threshold", R.integer.doze_pickup_vibration_threshold);
+    }
+
+    public boolean getAlwaysOn() {
+        return mAmbientDisplayConfiguration.alwaysOnEnabled(UserHandle.USER_CURRENT);
+    }
+
+    private boolean getBoolean(String propName, int resId) {
+        return SystemProperties.getBoolean(propName, mContext.getResources().getBoolean(resId));
+    }
+
+    private int getInt(String propName, int resId) {
+        int value = SystemProperties.getInt(propName, mContext.getResources().getInteger(resId));
+        return MathUtils.constrain(value, 0, MAX_DURATION);
+    }
+
+    private String getString(String propName, int resId) {
+        return SystemProperties.get(propName, mContext.getString(resId));
+    }
+
+    public boolean getPickupSubtypePerformsProxCheck(int subType) {
+        String spec = getString("doze.pickup.proxcheck",
+                R.string.doze_pickup_subtype_performs_proximity_check);
+
+        if (TextUtils.isEmpty(spec)) {
+            // Fall back to non-subtype based property.
+            return mContext.getResources().getBoolean(R.bool.doze_pickup_performs_proximity_check);
+        }
+
+        if (sPickupSubtypePerformsProxMatcher == null
+                || !TextUtils.equals(spec, sPickupSubtypePerformsProxMatcher.mSpec)) {
+            sPickupSubtypePerformsProxMatcher = new IntInOutMatcher(spec);
+        }
+
+        return sPickupSubtypePerformsProxMatcher.isIn(subType);
+    }
+
+    public int getPulseVisibleDurationExtended() {
+        return 2 * getPulseVisibleDuration();
+    }
+
+    public boolean doubleTapReportsTouchCoordinates() {
+        return mContext.getResources().getBoolean(R.bool.doze_double_tap_reports_touch_coordinates);
+    }
+
+
+    /**
+     * Parses a spec of the form `1,2,3,!5,*`. The resulting object will match numbers that are
+     * listed, will not match numbers that are listed with a ! prefix, and will match / not match
+     * unlisted numbers depending on whether * or !* is present.
+     *
+     * *  -> match any numbers that are not explicitly listed
+     * !* -> don't match any numbers that are not explicitly listed
+     * 2  -> match 2
+     * !3 -> don't match 3
+     *
+     * It is illegal to specify:
+     * - an empty spec
+     * - a spec containing that are empty, or a lone !
+     * - a spec for anything other than numbers or *
+     * - multiple terms for the same number / multiple *s
+     */
+    public static class IntInOutMatcher {
+        private static final String WILDCARD = "*";
+        private static final char OUT_PREFIX = '!';
+
+        private final SparseBooleanArray mIsIn;
+        private final boolean mDefaultIsIn;
+        final String mSpec;
+
+        public IntInOutMatcher(String spec) {
+            if (TextUtils.isEmpty(spec)) {
+                throw new IllegalArgumentException("Spec must not be empty");
+            }
+
+            boolean defaultIsIn = false;
+            boolean foundWildcard = false;
+
+            mSpec = spec;
+            mIsIn = new SparseBooleanArray();
+
+            for (String itemPrefixed : spec.split(",", -1)) {
+                if (itemPrefixed.length() == 0) {
+                    throw new IllegalArgumentException(
+                            "Illegal spec, must not have zero-length items: `" + spec + "`");
+                }
+                boolean isIn = itemPrefixed.charAt(0) != OUT_PREFIX;
+                String item = isIn ? itemPrefixed : itemPrefixed.substring(1);
+
+                if (itemPrefixed.length() == 0) {
+                    throw new IllegalArgumentException(
+                            "Illegal spec, must not have zero-length items: `" + spec + "`");
+                }
+
+                if (WILDCARD.equals(item)) {
+                    if (foundWildcard) {
+                        throw new IllegalArgumentException("Illegal spec, `" + WILDCARD +
+                                "` must not appear multiple times in `" + spec + "`");
+                    }
+                    defaultIsIn = isIn;
+                    foundWildcard = true;
+                } else {
+                    int key = Integer.parseInt(item);
+                    if (mIsIn.indexOfKey(key) >= 0) {
+                        throw new IllegalArgumentException("Illegal spec, `" + key +
+                                "` must not appear multiple times in `" + spec + "`");
+                    }
+                    mIsIn.put(key, isIn);
+                }
+            }
+
+            if (!foundWildcard) {
+                throw new IllegalArgumentException("Illegal spec, must specify either * or !*");
+            }
+
+            mDefaultIsIn = defaultIsIn;
+        }
+
+        public boolean isIn(int value) {
+            return (mIsIn.get(value, mDefaultIsIn));
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/DozeScrimController.java b/com/android/systemui/statusbar/phone/DozeScrimController.java
new file mode 100644
index 0000000..021b451
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/DozeScrimController.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.doze.DozeHost;
+import com.android.systemui.doze.DozeLog;
+
+/**
+ * Controller which handles all the doze animations of the scrims.
+ */
+public class DozeScrimController {
+    private static final String TAG = "DozeScrimController";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private final DozeParameters mDozeParameters;
+    private final Handler mHandler = new Handler();
+    private final ScrimController mScrimController;
+
+    private final Context mContext;
+
+    private boolean mDozing;
+    private DozeHost.PulseCallback mPulseCallback;
+    private int mPulseReason;
+    private Animator mInFrontAnimator;
+    private Animator mBehindAnimator;
+    private float mInFrontTarget;
+    private float mBehindTarget;
+    private boolean mDozingAborted;
+    private boolean mWakeAndUnlocking;
+    private boolean mFullyPulsing;
+
+    private float mAodFrontScrimOpacity = 0;
+    private Runnable mSetDozeInFrontAlphaDelayed;
+
+    public DozeScrimController(ScrimController scrimController, Context context) {
+        mContext = context;
+        mScrimController = scrimController;
+        mDozeParameters = new DozeParameters(context);
+    }
+
+    public void setDozing(boolean dozing, boolean animate) {
+        if (mDozing == dozing) return;
+        mDozing = dozing;
+        mWakeAndUnlocking = false;
+        if (mDozing) {
+            mDozingAborted = false;
+            abortAnimations();
+            mScrimController.setDozeBehindAlpha(1f);
+            setDozeInFrontAlpha(mDozeParameters.getAlwaysOn() ? mAodFrontScrimOpacity : 1f);
+        } else {
+            cancelPulsing();
+            if (animate) {
+                startScrimAnimation(false /* inFront */, 0f /* target */,
+                        NotificationPanelView.DOZE_ANIMATION_DURATION,
+                        Interpolators.LINEAR_OUT_SLOW_IN);
+                startScrimAnimation(true /* inFront */, 0f /* target */,
+                        NotificationPanelView.DOZE_ANIMATION_DURATION,
+                        Interpolators.LINEAR_OUT_SLOW_IN);
+            } else {
+                abortAnimations();
+                mScrimController.setDozeBehindAlpha(0f);
+                setDozeInFrontAlpha(0f);
+            }
+        }
+    }
+
+    /**
+     * Set the opacity of the front scrim when showing AOD1
+     *
+     * Used to emulate lower brightness values than the hardware supports natively.
+     */
+    public void setAodDimmingScrim(float scrimOpacity) {
+        mAodFrontScrimOpacity = scrimOpacity;
+        if (mDozing && !isPulsing() && !mDozingAborted && !mWakeAndUnlocking
+                && mDozeParameters.getAlwaysOn()) {
+            setDozeInFrontAlpha(mAodFrontScrimOpacity);
+        }
+    }
+
+    public void setWakeAndUnlocking() {
+        // Immediately abort the doze scrims in case of wake-and-unlock
+        // for pulsing so the Keyguard fade-out animation scrim can take over.
+        if (!mWakeAndUnlocking) {
+            mWakeAndUnlocking = true;
+            mScrimController.setDozeBehindAlpha(0f);
+            setDozeInFrontAlpha(0f);
+        }
+    }
+
+    /** When dozing, fade screen contents in and out using the front scrim. */
+    public void pulse(@NonNull DozeHost.PulseCallback callback, int reason) {
+        if (callback == null) {
+            throw new IllegalArgumentException("callback must not be null");
+        }
+
+        if (!mDozing || mPulseCallback != null) {
+            // Pulse suppressed.
+            callback.onPulseFinished();
+            return;
+        }
+
+        // Begin pulse.  Note that it's very important that the pulse finished callback
+        // be invoked when we're done so that the caller can drop the pulse wakelock.
+        mPulseCallback = callback;
+        mPulseReason = reason;
+        setDozeInFrontAlpha(1f);
+        mHandler.post(mPulseIn);
+    }
+
+    /**
+     * Aborts pulsing immediately.
+     */
+    public void abortPulsing() {
+        cancelPulsing();
+        if (mDozing && !mWakeAndUnlocking) {
+            mScrimController.setDozeBehindAlpha(1f);
+            setDozeInFrontAlpha(mDozeParameters.getAlwaysOn() && !mDozingAborted
+                    ? mAodFrontScrimOpacity : 1f);
+        }
+    }
+
+    /**
+     * Aborts dozing immediately.
+     */
+    public void abortDoze() {
+        mDozingAborted = true;
+        abortPulsing();
+    }
+
+    public void pulseOutNow() {
+        if (mPulseCallback != null && mFullyPulsing) {
+            mPulseOut.run();
+        }
+    }
+
+    public void onScreenTurnedOn() {
+        if (isPulsing()) {
+            final boolean pickupOrDoubleTap = mPulseReason == DozeLog.PULSE_REASON_SENSOR_PICKUP
+                    || mPulseReason == DozeLog.PULSE_REASON_SENSOR_DOUBLE_TAP;
+            startScrimAnimation(true /* inFront */, 0f,
+                    mDozeParameters.getPulseInDuration(pickupOrDoubleTap),
+                    pickupOrDoubleTap ? Interpolators.LINEAR_OUT_SLOW_IN : Interpolators.ALPHA_OUT,
+                    mPulseInFinished);
+        }
+    }
+
+    public boolean isPulsing() {
+        return mPulseCallback != null;
+    }
+
+    public boolean isDozing() {
+        return mDozing;
+    }
+
+    public void extendPulse() {
+        mHandler.removeCallbacks(mPulseOut);
+    }
+
+    private void cancelPulsing() {
+        if (DEBUG) Log.d(TAG, "Cancel pulsing");
+
+        if (mPulseCallback != null) {
+            mFullyPulsing = false;
+            mHandler.removeCallbacks(mPulseIn);
+            mHandler.removeCallbacks(mPulseOut);
+            mHandler.removeCallbacks(mPulseOutExtended);
+            pulseFinished();
+        }
+    }
+
+    private void pulseStarted() {
+        if (mPulseCallback != null) {
+            mPulseCallback.onPulseStarted();
+        }
+    }
+
+    private void pulseFinished() {
+        if (mPulseCallback != null) {
+            mPulseCallback.onPulseFinished();
+            mPulseCallback = null;
+        }
+    }
+
+    private void abortAnimations() {
+        if (mInFrontAnimator != null) {
+            mInFrontAnimator.cancel();
+        }
+        if (mBehindAnimator != null) {
+            mBehindAnimator.cancel();
+        }
+    }
+
+    private void startScrimAnimation(final boolean inFront, float target, long duration,
+            Interpolator interpolator) {
+        startScrimAnimation(inFront, target, duration, interpolator, null /* endRunnable */);
+    }
+
+    private void startScrimAnimation(final boolean inFront, float target, long duration,
+            Interpolator interpolator, final Runnable endRunnable) {
+        Animator current = getCurrentAnimator(inFront);
+        if (current != null) {
+            float currentTarget = getCurrentTarget(inFront);
+            if (currentTarget == target) {
+                return;
+            }
+            current.cancel();
+        }
+        ValueAnimator anim = ValueAnimator.ofFloat(getDozeAlpha(inFront), target);
+        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                float value = (float) animation.getAnimatedValue();
+                setDozeAlpha(inFront, value);
+            }
+        });
+        anim.setInterpolator(interpolator);
+        anim.setDuration(duration);
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                setCurrentAnimator(inFront, null);
+                if (endRunnable != null) {
+                    endRunnable.run();
+                }
+            }
+        });
+        anim.start();
+        setCurrentAnimator(inFront, anim);
+        setCurrentTarget(inFront, target);
+    }
+
+    private float getCurrentTarget(boolean inFront) {
+        return inFront ? mInFrontTarget : mBehindTarget;
+    }
+
+    private void setCurrentTarget(boolean inFront, float target) {
+        if (inFront) {
+            mInFrontTarget = target;
+        } else {
+            mBehindTarget = target;
+        }
+    }
+
+    private Animator getCurrentAnimator(boolean inFront) {
+        return inFront ? mInFrontAnimator : mBehindAnimator;
+    }
+
+    private void setCurrentAnimator(boolean inFront, Animator animator) {
+        if (inFront) {
+            mInFrontAnimator = animator;
+        } else {
+            mBehindAnimator = animator;
+        }
+    }
+
+    private void setDozeAlpha(boolean inFront, float alpha) {
+        if (mWakeAndUnlocking) {
+            return;
+        }
+        if (inFront) {
+            mScrimController.setDozeInFrontAlpha(alpha);
+        } else {
+            mScrimController.setDozeBehindAlpha(alpha);
+        }
+    }
+
+    private float getDozeAlpha(boolean inFront) {
+        return inFront
+                ? mScrimController.getDozeInFrontAlpha()
+                : mScrimController.getDozeBehindAlpha();
+    }
+
+    private void setDozeInFrontAlpha(float opacity) {
+        setDozeInFrontAlphaDelayed(opacity, 0 /* delay */);
+
+    }
+
+    private void setDozeInFrontAlphaDelayed(float opacity, long delayMs) {
+        if (mSetDozeInFrontAlphaDelayed != null) {
+            mHandler.removeCallbacks(mSetDozeInFrontAlphaDelayed);
+            mSetDozeInFrontAlphaDelayed = null;
+        }
+        if (delayMs <= 0) {
+            mScrimController.setDozeInFrontAlpha(opacity);
+        } else {
+            mHandler.postDelayed(mSetDozeInFrontAlphaDelayed = () -> {
+                setDozeInFrontAlpha(opacity);
+            }, delayMs);
+        }
+    }
+
+    private final Runnable mPulseIn = new Runnable() {
+        @Override
+        public void run() {
+            if (DEBUG) Log.d(TAG, "Pulse in, mDozing=" + mDozing + " mPulseReason="
+                    + DozeLog.pulseReasonToString(mPulseReason));
+            if (!mDozing) return;
+            DozeLog.tracePulseStart(mPulseReason);
+
+            // Signal that the pulse is ready to turn the screen on and draw.
+            pulseStarted();
+        }
+    };
+
+    private final Runnable mPulseInFinished = new Runnable() {
+        @Override
+        public void run() {
+            if (DEBUG) Log.d(TAG, "Pulse in finished, mDozing=" + mDozing);
+            if (!mDozing) return;
+            mHandler.postDelayed(mPulseOut, mDozeParameters.getPulseVisibleDuration());
+            mHandler.postDelayed(mPulseOutExtended,
+                    mDozeParameters.getPulseVisibleDurationExtended());
+            mFullyPulsing = true;
+        }
+    };
+
+    private final Runnable mPulseOutExtended = new Runnable() {
+        @Override
+        public void run() {
+            mHandler.removeCallbacks(mPulseOut);
+            mPulseOut.run();
+        }
+    };
+
+    private final Runnable mPulseOut = new Runnable() {
+        @Override
+        public void run() {
+            mFullyPulsing = false;
+            mHandler.removeCallbacks(mPulseOut);
+            mHandler.removeCallbacks(mPulseOutExtended);
+            if (DEBUG) Log.d(TAG, "Pulse out, mDozing=" + mDozing);
+            if (!mDozing) return;
+            startScrimAnimation(true /* inFront */, 1,
+                    mDozeParameters.getPulseOutDuration(),
+                    Interpolators.ALPHA_IN, mPulseOutFinishing);
+        }
+    };
+
+    private final Runnable mPulseOutFinishing = new Runnable() {
+        @Override
+        public void run() {
+            if (DEBUG) Log.d(TAG, "Pulse out finished");
+            DozeLog.tracePulseFinish();
+            if (mDozeParameters.getAlwaysOn() && mDozing) {
+                // Setting power states can block rendering. For AOD, delay finishing the pulse and
+                // setting the power state until the fully black scrim had time to hit the
+                // framebuffer.
+                mHandler.postDelayed(mPulseOutFinished, 30);
+            } else {
+                mPulseOutFinished.run();
+            }
+        }
+    };
+
+    private final Runnable mPulseOutFinished = new Runnable() {
+        @Override
+        public void run() {
+            // Signal that the pulse is all finished so we can turn the screen off now.
+            DozeScrimController.this.pulseFinished();
+            if (mDozeParameters.getAlwaysOn()) {
+                // Setting power states can happen after we push out the frame. Make sure we
+                // stay fully opaque until the power state request reaches the lower levels.
+                setDozeInFrontAlphaDelayed(mAodFrontScrimOpacity, 30);
+            }
+        }
+    };
+}
diff --git a/com/android/systemui/statusbar/phone/ExpandableIndicator.java b/com/android/systemui/statusbar/phone/ExpandableIndicator.java
new file mode 100644
index 0000000..8f49c85
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/ExpandableIndicator.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2016 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.phone;
+
+import android.content.Context;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import com.android.systemui.R;
+
+public class ExpandableIndicator extends ImageView {
+
+    private boolean mExpanded;
+    private boolean mIsDefaultDirection = true;
+
+    public ExpandableIndicator(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        updateIndicatorDrawable();
+        setContentDescription(getContentDescription(mExpanded));
+    }
+
+    public void setExpanded(boolean expanded) {
+        if (expanded == mExpanded) return;
+        mExpanded = expanded;
+        final int res = getDrawableResourceId(!mExpanded);
+        // workaround to reset drawable
+        final AnimatedVectorDrawable avd = (AnimatedVectorDrawable) getContext()
+                .getDrawable(res).getConstantState().newDrawable();
+        setImageDrawable(avd);
+        avd.forceAnimationOnUI();
+        avd.start();
+        setContentDescription(getContentDescription(expanded));
+    }
+
+    /** Whether the icons are using the default direction or the opposite */
+    public void setDefaultDirection(boolean isDefaultDirection) {
+        mIsDefaultDirection = isDefaultDirection;
+        updateIndicatorDrawable();
+    }
+
+    private int getDrawableResourceId(boolean expanded) {
+        if (mIsDefaultDirection) {
+            return expanded ? R.drawable.ic_volume_collapse_animation
+                    : R.drawable.ic_volume_expand_animation;
+        } else {
+            return expanded ? R.drawable.ic_volume_expand_animation
+                    : R.drawable.ic_volume_collapse_animation;
+        }
+    }
+
+    private String getContentDescription(boolean expanded) {
+        return expanded ? mContext.getString(R.string.accessibility_quick_settings_collapse)
+                : mContext.getString(R.string.accessibility_quick_settings_expand);
+    }
+
+    private void updateIndicatorDrawable() {
+        final int res = getDrawableResourceId(mExpanded);
+        setImageResource(res);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/FingerprintUnlockController.java b/com/android/systemui/statusbar/phone/FingerprintUnlockController.java
new file mode 100644
index 0000000..91369db
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/FingerprintUnlockController.java
@@ -0,0 +1,405 @@
+/*
+ * 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.phone;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.util.Log;
+
+import com.android.keyguard.KeyguardConstants;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.LatencyTracker;
+import com.android.systemui.Dependency;
+import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
+
+import java.io.PrintWriter;
+
+/**
+ * Controller which coordinates all the fingerprint unlocking actions with the UI.
+ */
+public class FingerprintUnlockController extends KeyguardUpdateMonitorCallback {
+
+    private static final String TAG = "FingerprintController";
+    private static final boolean DEBUG_FP_WAKELOCK = KeyguardConstants.DEBUG_FP_WAKELOCK;
+    private static final long FINGERPRINT_WAKELOCK_TIMEOUT_MS = 15 * 1000;
+    private static final String FINGERPRINT_WAKE_LOCK_NAME = "wake-and-unlock wakelock";
+
+    /**
+     * Mode in which we don't need to wake up the device when we get a fingerprint.
+     */
+    public static final int MODE_NONE = 0;
+
+    /**
+     * Mode in which we wake up the device, and directly dismiss Keyguard. Active when we acquire
+     * a fingerprint while the screen is off and the device was sleeping.
+     */
+    public static final int MODE_WAKE_AND_UNLOCK = 1;
+
+    /**
+     * Mode in which we wake the device up, and fade out the Keyguard contents because they were
+     * already visible while pulsing in doze mode.
+     */
+    public static final int MODE_WAKE_AND_UNLOCK_PULSING = 2;
+
+    /**
+     * Mode in which we wake up the device, but play the normal dismiss animation. Active when we
+     * acquire a fingerprint pulsing in doze mode.
+     */
+    public static final int MODE_SHOW_BOUNCER = 3;
+
+    /**
+     * Mode in which we only wake up the device, and keyguard was not showing when we acquired a
+     * fingerprint.
+     * */
+    public static final int MODE_ONLY_WAKE = 4;
+
+    /**
+     * Mode in which fingerprint unlocks the device.
+     */
+    public static final int MODE_UNLOCK = 5;
+
+    /**
+     * Mode in which fingerprint brings up the bouncer because fingerprint unlocking is currently
+     * not allowed.
+     */
+    public static final int MODE_DISMISS_BOUNCER = 6;
+
+    /**
+     * Mode in which fingerprint wakes and unlocks the device from a dream.
+     */
+    public static final int MODE_WAKE_AND_UNLOCK_FROM_DREAM = 7;
+
+    /**
+     * How much faster we collapse the lockscreen when authenticating with fingerprint.
+     */
+    private static final float FINGERPRINT_COLLAPSE_SPEEDUP_FACTOR = 1.1f;
+
+    private PowerManager mPowerManager;
+    private Handler mHandler = new Handler();
+    private PowerManager.WakeLock mWakeLock;
+    private KeyguardUpdateMonitor mUpdateMonitor;
+    private int mMode;
+    private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    private StatusBarWindowManager mStatusBarWindowManager;
+    private DozeScrimController mDozeScrimController;
+    private KeyguardViewMediator mKeyguardViewMediator;
+    private ScrimController mScrimController;
+    private StatusBar mStatusBar;
+    private final UnlockMethodCache mUnlockMethodCache;
+    private final Context mContext;
+    private int mPendingAuthenticatedUserId = -1;
+    private boolean mPendingShowBouncer;
+    private boolean mHasScreenTurnedOnSinceAuthenticating;
+
+    public FingerprintUnlockController(Context context,
+            DozeScrimController dozeScrimController,
+            KeyguardViewMediator keyguardViewMediator,
+            ScrimController scrimController,
+            StatusBar statusBar,
+            UnlockMethodCache unlockMethodCache) {
+        mContext = context;
+        mPowerManager = context.getSystemService(PowerManager.class);
+        mUpdateMonitor = KeyguardUpdateMonitor.getInstance(context);
+        mUpdateMonitor.registerCallback(this);
+        Dependency.get(WakefulnessLifecycle.class).addObserver(mWakefulnessObserver);
+        Dependency.get(ScreenLifecycle.class).addObserver(mScreenObserver);
+        mStatusBarWindowManager = Dependency.get(StatusBarWindowManager.class);
+        mDozeScrimController = dozeScrimController;
+        mKeyguardViewMediator = keyguardViewMediator;
+        mScrimController = scrimController;
+        mStatusBar = statusBar;
+        mUnlockMethodCache = unlockMethodCache;
+    }
+
+    public void setStatusBarKeyguardViewManager(
+            StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
+        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+    }
+
+    private final Runnable mReleaseFingerprintWakeLockRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (DEBUG_FP_WAKELOCK) {
+                Log.i(TAG, "fp wakelock: TIMEOUT!!");
+            }
+            releaseFingerprintWakeLock();
+        }
+    };
+
+    private void releaseFingerprintWakeLock() {
+        if (mWakeLock != null) {
+            mHandler.removeCallbacks(mReleaseFingerprintWakeLockRunnable);
+            if (DEBUG_FP_WAKELOCK) {
+                Log.i(TAG, "releasing fp wakelock");
+            }
+            mWakeLock.release();
+            mWakeLock = null;
+        }
+    }
+
+    @Override
+    public void onFingerprintAcquired() {
+        Trace.beginSection("FingerprintUnlockController#onFingerprintAcquired");
+        releaseFingerprintWakeLock();
+        if (!mUpdateMonitor.isDeviceInteractive()) {
+            if (LatencyTracker.isEnabled(mContext)) {
+                LatencyTracker.getInstance(mContext).onActionStart(
+                        LatencyTracker.ACTION_FINGERPRINT_WAKE_AND_UNLOCK);
+            }
+            mWakeLock = mPowerManager.newWakeLock(
+                    PowerManager.PARTIAL_WAKE_LOCK, FINGERPRINT_WAKE_LOCK_NAME);
+            Trace.beginSection("acquiring wake-and-unlock");
+            mWakeLock.acquire();
+            Trace.endSection();
+            if (DEBUG_FP_WAKELOCK) {
+                Log.i(TAG, "fingerprint acquired, grabbing fp wakelock");
+            }
+            mHandler.postDelayed(mReleaseFingerprintWakeLockRunnable,
+                    FINGERPRINT_WAKELOCK_TIMEOUT_MS);
+        }
+        Trace.endSection();
+    }
+
+    private boolean pulsingOrAod() {
+        boolean pulsing = mDozeScrimController.isPulsing();
+        boolean dozingWithScreenOn = mStatusBar.isDozing() && !mStatusBar.isScreenFullyOff();
+        return pulsing || dozingWithScreenOn;
+    }
+
+    @Override
+    public void onFingerprintAuthenticated(int userId) {
+        Trace.beginSection("FingerprintUnlockController#onFingerprintAuthenticated");
+        if (mUpdateMonitor.isGoingToSleep()) {
+            mPendingAuthenticatedUserId = userId;
+            Trace.endSection();
+            return;
+        }
+        startWakeAndUnlock(calculateMode());
+    }
+
+    public void startWakeAndUnlock(int mode) {
+        // TODO(b/62444020): remove when this bug is fixed
+        Log.v(TAG, "startWakeAndUnlock(" + mode + ")");
+        boolean wasDeviceInteractive = mUpdateMonitor.isDeviceInteractive();
+        mMode = mode;
+        mHasScreenTurnedOnSinceAuthenticating = false;
+        if (mMode == MODE_WAKE_AND_UNLOCK_PULSING && pulsingOrAod()) {
+            // If we are waking the device up while we are pulsing the clock and the
+            // notifications would light up first, creating an unpleasant animation.
+            // Defer changing the screen brightness by forcing doze brightness on our window
+            // until the clock and the notifications are faded out.
+            mStatusBarWindowManager.setForceDozeBrightness(true);
+        }
+        if (!wasDeviceInteractive) {
+            if (DEBUG_FP_WAKELOCK) {
+                Log.i(TAG, "fp wakelock: Authenticated, waking up...");
+            }
+            mPowerManager.wakeUp(SystemClock.uptimeMillis(), "android.policy:FINGERPRINT");
+        }
+        Trace.beginSection("release wake-and-unlock");
+        releaseFingerprintWakeLock();
+        Trace.endSection();
+        switch (mMode) {
+            case MODE_DISMISS_BOUNCER:
+                Trace.beginSection("MODE_DISMISS");
+                mStatusBarKeyguardViewManager.notifyKeyguardAuthenticated(
+                        false /* strongAuth */);
+                Trace.endSection();
+                break;
+            case MODE_UNLOCK:
+            case MODE_SHOW_BOUNCER:
+                Trace.beginSection("MODE_UNLOCK or MODE_SHOW_BOUNCER");
+                if (!wasDeviceInteractive) {
+                    mStatusBarKeyguardViewManager.notifyDeviceWakeUpRequested();
+                    mPendingShowBouncer = true;
+                } else {
+                    showBouncer();
+                }
+                Trace.endSection();
+                break;
+            case MODE_WAKE_AND_UNLOCK_FROM_DREAM:
+            case MODE_WAKE_AND_UNLOCK_PULSING:
+            case MODE_WAKE_AND_UNLOCK:
+                if (mMode == MODE_WAKE_AND_UNLOCK_PULSING) {
+                    Trace.beginSection("MODE_WAKE_AND_UNLOCK_PULSING");
+                    mStatusBar.updateMediaMetaData(false /* metaDataChanged */,
+                            true /* allowEnterAnimation */);
+                } else if (mMode == MODE_WAKE_AND_UNLOCK){
+                    Trace.beginSection("MODE_WAKE_AND_UNLOCK");
+                    mDozeScrimController.abortDoze();
+                } else {
+                    Trace.beginSection("MODE_WAKE_AND_UNLOCK_FROM_DREAM");
+                    mUpdateMonitor.awakenFromDream();
+                }
+                mStatusBarWindowManager.setStatusBarFocusable(false);
+                mKeyguardViewMediator.onWakeAndUnlocking();
+                mScrimController.setWakeAndUnlocking();
+                mDozeScrimController.setWakeAndUnlocking();
+                if (mStatusBar.getNavigationBarView() != null) {
+                    mStatusBar.getNavigationBarView().setWakeAndUnlocking(true);
+                }
+                Trace.endSection();
+                break;
+            case MODE_ONLY_WAKE:
+            case MODE_NONE:
+                break;
+        }
+        mStatusBar.notifyFpAuthModeChanged();
+        Trace.endSection();
+    }
+
+    private void showBouncer() {
+        mStatusBarKeyguardViewManager.animateCollapsePanels(
+                FINGERPRINT_COLLAPSE_SPEEDUP_FACTOR);
+        mPendingShowBouncer = false;
+    }
+
+    @Override
+    public void onStartedGoingToSleep(int why) {
+        resetMode();
+        mPendingAuthenticatedUserId = -1;
+    }
+
+    @Override
+    public void onFinishedGoingToSleep(int why) {
+        Trace.beginSection("FingerprintUnlockController#onFinishedGoingToSleep");
+        if (mPendingAuthenticatedUserId != -1) {
+
+            // Post this to make sure it's executed after the device is fully locked.
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    onFingerprintAuthenticated(mPendingAuthenticatedUserId);
+                }
+            });
+        }
+        mPendingAuthenticatedUserId = -1;
+        Trace.endSection();
+    }
+
+    public boolean hasPendingAuthentication() {
+        return mPendingAuthenticatedUserId != -1
+                && mUpdateMonitor.isUnlockingWithFingerprintAllowed()
+                && mPendingAuthenticatedUserId == KeyguardUpdateMonitor.getCurrentUser();
+    }
+
+    public int getMode() {
+        return mMode;
+    }
+
+    private int calculateMode() {
+        boolean unlockingAllowed = mUpdateMonitor.isUnlockingWithFingerprintAllowed();
+        boolean deviceDreaming = mUpdateMonitor.isDreaming();
+
+        if (!mUpdateMonitor.isDeviceInteractive()) {
+            if (!mStatusBarKeyguardViewManager.isShowing()) {
+                return MODE_ONLY_WAKE;
+            } else if (mDozeScrimController.isPulsing() && unlockingAllowed) {
+                return MODE_WAKE_AND_UNLOCK_PULSING;
+            } else if (unlockingAllowed || !mUnlockMethodCache.isMethodSecure()) {
+                return MODE_WAKE_AND_UNLOCK;
+            } else {
+                return MODE_SHOW_BOUNCER;
+            }
+        }
+        if (unlockingAllowed && deviceDreaming) {
+            return MODE_WAKE_AND_UNLOCK_FROM_DREAM;
+        }
+        if (mStatusBarKeyguardViewManager.isShowing()) {
+            if (mStatusBarKeyguardViewManager.isBouncerShowing() && unlockingAllowed) {
+                return MODE_DISMISS_BOUNCER;
+            } else if (unlockingAllowed) {
+                return MODE_UNLOCK;
+            } else if (!mStatusBarKeyguardViewManager.isBouncerShowing()) {
+                return MODE_SHOW_BOUNCER;
+            }
+        }
+        return MODE_NONE;
+    }
+
+    @Override
+    public void onFingerprintAuthFailed() {
+        cleanup();
+    }
+
+    @Override
+    public void onFingerprintError(int msgId, String errString) {
+        cleanup();
+    }
+
+    private void cleanup() {
+        releaseFingerprintWakeLock();
+    }
+
+    public void startKeyguardFadingAway() {
+
+        // Disable brightness override when the ambient contents are fully invisible.
+        mHandler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                mStatusBarWindowManager.setForceDozeBrightness(false);
+            }
+        }, StatusBar.FADE_KEYGUARD_DURATION_PULSING);
+    }
+
+    public void finishKeyguardFadingAway() {
+        resetMode();
+    }
+
+    private void resetMode() {
+        mMode = MODE_NONE;
+        mStatusBarWindowManager.setForceDozeBrightness(false);
+        if (mStatusBar.getNavigationBarView() != null) {
+            mStatusBar.getNavigationBarView().setWakeAndUnlocking(false);
+        }
+        mStatusBar.notifyFpAuthModeChanged();
+    }
+
+    private final WakefulnessLifecycle.Observer mWakefulnessObserver =
+            new WakefulnessLifecycle.Observer() {
+        @Override
+        public void onFinishedWakingUp() {
+            if (mPendingShowBouncer) {
+                FingerprintUnlockController.this.showBouncer();
+            }
+        }
+    };
+
+    private final ScreenLifecycle.Observer mScreenObserver =
+            new ScreenLifecycle.Observer() {
+                @Override
+                public void onScreenTurnedOn() {
+                    mHasScreenTurnedOnSinceAuthenticating = true;
+                }
+            };
+
+    public boolean hasScreenTurnedOnSinceAuthenticating() {
+        return mHasScreenTurnedOnSinceAuthenticating;
+    }
+
+    public void dump(PrintWriter pw) {
+        pw.println(" FingerprintUnlockController:");
+        pw.print("   mMode="); pw.println(mMode);
+        pw.print("   mWakeLock="); pw.println(mWakeLock);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java b/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
new file mode 100644
index 0000000..c85571c
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
@@ -0,0 +1,168 @@
+/*
+ * 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.phone;
+
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import com.android.systemui.Gefingerpoken;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+/**
+ * A helper class to handle touches on the heads-up views.
+ */
+public class HeadsUpTouchHelper implements Gefingerpoken {
+
+    private HeadsUpManager mHeadsUpManager;
+    private NotificationStackScrollLayout mStackScroller;
+    private int mTrackingPointer;
+    private float mTouchSlop;
+    private float mInitialTouchX;
+    private float mInitialTouchY;
+    private boolean mTouchingHeadsUpView;
+    private boolean mTrackingHeadsUp;
+    private boolean mCollapseSnoozes;
+    private NotificationPanelView mPanel;
+    private ExpandableNotificationRow mPickedChild;
+
+    public HeadsUpTouchHelper(HeadsUpManager headsUpManager,
+            NotificationStackScrollLayout stackScroller,
+            NotificationPanelView notificationPanelView) {
+        mHeadsUpManager = headsUpManager;
+        mStackScroller = stackScroller;
+        mPanel = notificationPanelView;
+        Context context = stackScroller.getContext();
+        final ViewConfiguration configuration = ViewConfiguration.get(context);
+        mTouchSlop = configuration.getScaledTouchSlop();
+    }
+
+    public boolean isTrackingHeadsUp() {
+        return mTrackingHeadsUp;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        if (!mTouchingHeadsUpView && event.getActionMasked() != MotionEvent.ACTION_DOWN) {
+            return false;
+        }
+        int pointerIndex = event.findPointerIndex(mTrackingPointer);
+        if (pointerIndex < 0) {
+            pointerIndex = 0;
+            mTrackingPointer = event.getPointerId(pointerIndex);
+        }
+        final float x = event.getX(pointerIndex);
+        final float y = event.getY(pointerIndex);
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                mInitialTouchY = y;
+                mInitialTouchX = x;
+                setTrackingHeadsUp(false);
+                ExpandableView child = mStackScroller.getChildAtRawPosition(x, y);
+                mTouchingHeadsUpView = false;
+                if (child instanceof ExpandableNotificationRow) {
+                    mPickedChild = (ExpandableNotificationRow) child;
+                    mTouchingHeadsUpView = !mStackScroller.isExpanded()
+                            && mPickedChild.isHeadsUp() && mPickedChild.isPinned();
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                final int upPointer = event.getPointerId(event.getActionIndex());
+                if (mTrackingPointer == upPointer) {
+                    // gesture is ongoing, find a new pointer to track
+                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+                    mTrackingPointer = event.getPointerId(newIndex);
+                    mInitialTouchX = event.getX(newIndex);
+                    mInitialTouchY = event.getY(newIndex);
+                }
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                final float h = y - mInitialTouchY;
+                if (mTouchingHeadsUpView && Math.abs(h) > mTouchSlop
+                        && Math.abs(h) > Math.abs(x - mInitialTouchX)) {
+                    setTrackingHeadsUp(true);
+                    mCollapseSnoozes = h < 0;
+                    mInitialTouchX = x;
+                    mInitialTouchY = y;
+                    int expandedHeight = mPickedChild.getActualHeight();
+                    mPanel.setPanelScrimMinFraction((float) expandedHeight
+                            / mPanel.getMaxPanelHeight());
+                    mPanel.startExpandMotion(x, y, true /* startTracking */, expandedHeight);
+                    mPanel.startExpandingFromPeek();
+                    // This call needs to be after the expansion start otherwise we will get a
+                    // flicker of one frame as it's not expanded yet.
+                    mHeadsUpManager.unpinAll();
+                    mPanel.clearNotificationEffects();
+                    endMotion();
+                    return true;
+                }
+                break;
+
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                if (mPickedChild != null && mTouchingHeadsUpView) {
+                    // We may swallow this click if the heads up just came in.
+                    if (mHeadsUpManager.shouldSwallowClick(
+                            mPickedChild.getStatusBarNotification().getKey())) {
+                        endMotion();
+                        return true;
+                    }
+                }
+                endMotion();
+                break;
+        }
+        return false;
+    }
+
+    private void setTrackingHeadsUp(boolean tracking) {
+        mTrackingHeadsUp = tracking;
+        mHeadsUpManager.setTrackingHeadsUp(tracking);
+        mPanel.setTrackingHeadsUp(tracking);
+    }
+
+    public void notifyFling(boolean collapse) {
+        if (collapse && mCollapseSnoozes) {
+            mHeadsUpManager.snooze();
+        }
+        mCollapseSnoozes = false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (!mTrackingHeadsUp) {
+            return false;
+        }
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                endMotion();
+                setTrackingHeadsUp(false);
+                break;
+        }
+        return true;
+    }
+
+    private void endMotion() {
+        mTrackingPointer = -1;
+        mPickedChild = null;
+        mTouchingHeadsUpView = false;
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java b/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java
new file mode 100644
index 0000000..df1ffda
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java
@@ -0,0 +1,587 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.statusbar.FlingAnimationUtils;
+import com.android.systemui.statusbar.KeyguardAffordanceView;
+
+/**
+ * A touch handler of the keyguard which is responsible for launching phone and camera affordances.
+ */
+public class KeyguardAffordanceHelper {
+
+    public static final float SWIPE_RESTING_ALPHA_AMOUNT = 0.5f;
+    public static final long HINT_PHASE1_DURATION = 200;
+    private static final long HINT_PHASE2_DURATION = 350;
+    private static final float BACKGROUND_RADIUS_SCALE_FACTOR = 0.25f;
+    private static final int HINT_CIRCLE_OPEN_DURATION = 500;
+
+    private final Context mContext;
+    private final Callback mCallback;
+
+    private FlingAnimationUtils mFlingAnimationUtils;
+    private VelocityTracker mVelocityTracker;
+    private boolean mSwipingInProgress;
+    private float mInitialTouchX;
+    private float mInitialTouchY;
+    private float mTranslation;
+    private float mTranslationOnDown;
+    private int mTouchSlop;
+    private int mMinTranslationAmount;
+    private int mMinFlingVelocity;
+    private int mHintGrowAmount;
+    private KeyguardAffordanceView mLeftIcon;
+    private KeyguardAffordanceView mCenterIcon;
+    private KeyguardAffordanceView mRightIcon;
+    private Animator mSwipeAnimator;
+    private FalsingManager mFalsingManager;
+    private int mMinBackgroundRadius;
+    private boolean mMotionCancelled;
+    private int mTouchTargetSize;
+    private View mTargetedView;
+    private boolean mTouchSlopExeeded;
+    private AnimatorListenerAdapter mFlingEndListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mSwipeAnimator = null;
+            mSwipingInProgress = false;
+            mTargetedView = null;
+        }
+    };
+    private Runnable mAnimationEndRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mCallback.onAnimationToSideEnded();
+        }
+    };
+
+    KeyguardAffordanceHelper(Callback callback, Context context) {
+        mContext = context;
+        mCallback = callback;
+        initIcons();
+        updateIcon(mLeftIcon, 0.0f, mLeftIcon.getRestingAlpha(), false, false, true, false);
+        updateIcon(mCenterIcon, 0.0f, mCenterIcon.getRestingAlpha(), false, false, true, false);
+        updateIcon(mRightIcon, 0.0f, mRightIcon.getRestingAlpha(), false, false, true, false);
+        initDimens();
+    }
+
+    private void initDimens() {
+        final ViewConfiguration configuration = ViewConfiguration.get(mContext);
+        mTouchSlop = configuration.getScaledPagingTouchSlop();
+        mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity();
+        mMinTranslationAmount = mContext.getResources().getDimensionPixelSize(
+                R.dimen.keyguard_min_swipe_amount);
+        mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize(
+                R.dimen.keyguard_affordance_min_background_radius);
+        mTouchTargetSize = mContext.getResources().getDimensionPixelSize(
+                R.dimen.keyguard_affordance_touch_target_size);
+        mHintGrowAmount =
+                mContext.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways);
+        mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.4f);
+        mFalsingManager = FalsingManager.getInstance(mContext);
+    }
+
+    private void initIcons() {
+        mLeftIcon = mCallback.getLeftIcon();
+        mCenterIcon = mCallback.getCenterIcon();
+        mRightIcon = mCallback.getRightIcon();
+        updatePreviews();
+    }
+
+    public void updatePreviews() {
+        mLeftIcon.setPreviewView(mCallback.getLeftPreview());
+        mRightIcon.setPreviewView(mCallback.getRightPreview());
+    }
+
+    public boolean onTouchEvent(MotionEvent event) {
+        int action = event.getActionMasked();
+        if (mMotionCancelled && action != MotionEvent.ACTION_DOWN) {
+            return false;
+        }
+        final float y = event.getY();
+        final float x = event.getX();
+
+        boolean isUp = false;
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                View targetView = getIconAtPosition(x, y);
+                if (targetView == null || (mTargetedView != null && mTargetedView != targetView)) {
+                    mMotionCancelled = true;
+                    return false;
+                }
+                if (mTargetedView != null) {
+                    cancelAnimation();
+                } else {
+                    mTouchSlopExeeded = false;
+                }
+                startSwiping(targetView);
+                mInitialTouchX = x;
+                mInitialTouchY = y;
+                mTranslationOnDown = mTranslation;
+                initVelocityTracker();
+                trackMovement(event);
+                mMotionCancelled = false;
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN:
+                mMotionCancelled = true;
+                endMotion(true /* forceSnapBack */, x, y);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                trackMovement(event);
+                float xDist = x - mInitialTouchX;
+                float yDist = y - mInitialTouchY;
+                float distance = (float) Math.hypot(xDist, yDist);
+                if (!mTouchSlopExeeded && distance > mTouchSlop) {
+                    mTouchSlopExeeded = true;
+                }
+                if (mSwipingInProgress) {
+                    if (mTargetedView == mRightIcon) {
+                        distance = mTranslationOnDown - distance;
+                        distance = Math.min(0, distance);
+                    } else {
+                        distance = mTranslationOnDown + distance;
+                        distance = Math.max(0, distance);
+                    }
+                    setTranslation(distance, false /* isReset */, false /* animateReset */);
+                }
+                break;
+
+            case MotionEvent.ACTION_UP:
+                isUp = true;
+            case MotionEvent.ACTION_CANCEL:
+                boolean hintOnTheRight = mTargetedView == mRightIcon;
+                trackMovement(event);
+                endMotion(!isUp, x, y);
+                if (!mTouchSlopExeeded && isUp) {
+                    mCallback.onIconClicked(hintOnTheRight);
+                }
+                break;
+        }
+        return true;
+    }
+
+    private void startSwiping(View targetView) {
+        mCallback.onSwipingStarted(targetView == mRightIcon);
+        mSwipingInProgress = true;
+        mTargetedView = targetView;
+    }
+
+    private View getIconAtPosition(float x, float y) {
+        if (leftSwipePossible() && isOnIcon(mLeftIcon, x, y)) {
+            return mLeftIcon;
+        }
+        if (rightSwipePossible() && isOnIcon(mRightIcon, x, y)) {
+            return mRightIcon;
+        }
+        return null;
+    }
+
+    public boolean isOnAffordanceIcon(float x, float y) {
+        return isOnIcon(mLeftIcon, x, y) || isOnIcon(mRightIcon, x, y);
+    }
+
+    private boolean isOnIcon(View icon, float x, float y) {
+        float iconX = icon.getX() + icon.getWidth() / 2.0f;
+        float iconY = icon.getY() + icon.getHeight() / 2.0f;
+        double distance = Math.hypot(x - iconX, y - iconY);
+        return distance <= mTouchTargetSize / 2;
+    }
+
+    private void endMotion(boolean forceSnapBack, float lastX, float lastY) {
+        if (mSwipingInProgress) {
+            flingWithCurrentVelocity(forceSnapBack, lastX, lastY);
+        } else {
+            mTargetedView = null;
+        }
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    private boolean rightSwipePossible() {
+        return mRightIcon.getVisibility() == View.VISIBLE;
+    }
+
+    private boolean leftSwipePossible() {
+        return mLeftIcon.getVisibility() == View.VISIBLE;
+    }
+
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        return false;
+    }
+
+    public void startHintAnimation(boolean right,
+            Runnable onFinishedListener) {
+        cancelAnimation();
+        startHintAnimationPhase1(right, onFinishedListener);
+    }
+
+    private void startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener) {
+        final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
+        ValueAnimator animator = getAnimatorToRadius(right, mHintGrowAmount);
+        animator.addListener(new AnimatorListenerAdapter() {
+            private boolean mCancelled;
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mCancelled = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mCancelled) {
+                    mSwipeAnimator = null;
+                    mTargetedView = null;
+                    onFinishedListener.run();
+                } else {
+                    startUnlockHintAnimationPhase2(right, onFinishedListener);
+                }
+            }
+        });
+        animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+        animator.setDuration(HINT_PHASE1_DURATION);
+        animator.start();
+        mSwipeAnimator = animator;
+        mTargetedView = targetView;
+    }
+
+    /**
+     * Phase 2: Move back.
+     */
+    private void startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener) {
+        ValueAnimator animator = getAnimatorToRadius(right, 0);
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mSwipeAnimator = null;
+                mTargetedView = null;
+                onFinishedListener.run();
+            }
+        });
+        animator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+        animator.setDuration(HINT_PHASE2_DURATION);
+        animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION);
+        animator.start();
+        mSwipeAnimator = animator;
+    }
+
+    private ValueAnimator getAnimatorToRadius(final boolean right, int radius) {
+        final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
+        ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius);
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                float newRadius = (float) animation.getAnimatedValue();
+                targetView.setCircleRadiusWithoutAnimation(newRadius);
+                float translation = getTranslationFromRadius(newRadius);
+                mTranslation = right ? -translation : translation;
+                updateIconsFromTranslation(targetView);
+            }
+        });
+        return animator;
+    }
+
+    private void cancelAnimation() {
+        if (mSwipeAnimator != null) {
+            mSwipeAnimator.cancel();
+        }
+    }
+
+    private void flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY) {
+        float vel = getCurrentVelocity(lastX, lastY);
+
+        // We snap back if the current translation is not far enough
+        boolean snapBack = false;
+        if (mCallback.needsAntiFalsing()) {
+            snapBack = snapBack || mFalsingManager.isFalseTouch();
+        }
+        snapBack = snapBack || isBelowFalsingThreshold();
+
+        // or if the velocity is in the opposite direction.
+        boolean velIsInWrongDirection = vel * mTranslation < 0;
+        snapBack |= Math.abs(vel) > mMinFlingVelocity && velIsInWrongDirection;
+        vel = snapBack ^ velIsInWrongDirection ? 0 : vel;
+        fling(vel, snapBack || forceSnapBack, mTranslation < 0);
+    }
+
+    private boolean isBelowFalsingThreshold() {
+        return Math.abs(mTranslation) < Math.abs(mTranslationOnDown) + getMinTranslationAmount();
+    }
+
+    private int getMinTranslationAmount() {
+        float factor = mCallback.getAffordanceFalsingFactor();
+        return (int) (mMinTranslationAmount * factor);
+    }
+
+    private void fling(float vel, final boolean snapBack, boolean right) {
+        float target = right ? -mCallback.getMaxTranslationDistance()
+                : mCallback.getMaxTranslationDistance();
+        target = snapBack ? 0 : target;
+
+        ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target);
+        mFlingAnimationUtils.apply(animator, mTranslation, target, vel);
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                mTranslation = (float) animation.getAnimatedValue();
+            }
+        });
+        animator.addListener(mFlingEndListener);
+        if (!snapBack) {
+            startFinishingCircleAnimation(vel * 0.375f, mAnimationEndRunnable, right);
+            mCallback.onAnimationToSideStarted(right, mTranslation, vel);
+        } else {
+            reset(true);
+        }
+        animator.start();
+        mSwipeAnimator = animator;
+        if (snapBack) {
+            mCallback.onSwipingAborted();
+        }
+    }
+
+    private void startFinishingCircleAnimation(float velocity, Runnable mAnimationEndRunnable,
+            boolean right) {
+        KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
+        targetView.finishAnimation(velocity, mAnimationEndRunnable);
+    }
+
+    private void setTranslation(float translation, boolean isReset, boolean animateReset) {
+        translation = rightSwipePossible() ? translation : Math.max(0, translation);
+        translation = leftSwipePossible() ? translation : Math.min(0, translation);
+        float absTranslation = Math.abs(translation);
+        if (translation != mTranslation || isReset) {
+            KeyguardAffordanceView targetView = translation > 0 ? mLeftIcon : mRightIcon;
+            KeyguardAffordanceView otherView = translation > 0 ? mRightIcon : mLeftIcon;
+            float alpha = absTranslation / getMinTranslationAmount();
+
+            // We interpolate the alpha of the other icons to 0
+            float fadeOutAlpha = 1.0f - alpha;
+            fadeOutAlpha = Math.max(fadeOutAlpha, 0.0f);
+
+            boolean animateIcons = isReset && animateReset;
+            boolean forceNoCircleAnimation = isReset && !animateReset;
+            float radius = getRadiusFromTranslation(absTranslation);
+            boolean slowAnimation = isReset && isBelowFalsingThreshold();
+            if (!isReset) {
+                updateIcon(targetView, radius, alpha + fadeOutAlpha * targetView.getRestingAlpha(),
+                        false, false, false, false);
+            } else {
+                updateIcon(targetView, 0.0f, fadeOutAlpha * targetView.getRestingAlpha(),
+                        animateIcons, slowAnimation, true /* isReset */, forceNoCircleAnimation);
+            }
+            updateIcon(otherView, 0.0f, fadeOutAlpha * otherView.getRestingAlpha(),
+                    animateIcons, slowAnimation, isReset, forceNoCircleAnimation);
+            updateIcon(mCenterIcon, 0.0f, fadeOutAlpha * mCenterIcon.getRestingAlpha(),
+                    animateIcons, slowAnimation, isReset, forceNoCircleAnimation);
+
+            mTranslation = translation;
+        }
+    }
+
+    private void updateIconsFromTranslation(KeyguardAffordanceView targetView) {
+        float absTranslation = Math.abs(mTranslation);
+        float alpha = absTranslation / getMinTranslationAmount();
+
+        // We interpolate the alpha of the other icons to 0
+        float fadeOutAlpha =  1.0f - alpha;
+        fadeOutAlpha = Math.max(0.0f, fadeOutAlpha);
+
+        // We interpolate the alpha of the targetView to 1
+        KeyguardAffordanceView otherView = targetView == mRightIcon ? mLeftIcon : mRightIcon;
+        updateIconAlpha(targetView, alpha + fadeOutAlpha * targetView.getRestingAlpha(), false);
+        updateIconAlpha(otherView, fadeOutAlpha * otherView.getRestingAlpha(), false);
+        updateIconAlpha(mCenterIcon, fadeOutAlpha * mCenterIcon.getRestingAlpha(), false);
+    }
+
+    private float getTranslationFromRadius(float circleSize) {
+        float translation = (circleSize - mMinBackgroundRadius)
+                / BACKGROUND_RADIUS_SCALE_FACTOR;
+        return translation > 0.0f ? translation + mTouchSlop : 0.0f;
+    }
+
+    private float getRadiusFromTranslation(float translation) {
+        if (translation <= mTouchSlop) {
+            return 0.0f;
+        }
+        return (translation - mTouchSlop)  * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius;
+    }
+
+    public void animateHideLeftRightIcon() {
+        cancelAnimation();
+        updateIcon(mRightIcon, 0f, 0f, true, false, false, false);
+        updateIcon(mLeftIcon, 0f, 0f, true, false, false, false);
+    }
+
+    private void updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha,
+                            boolean animate, boolean slowRadiusAnimation, boolean force,
+                            boolean forceNoCircleAnimation) {
+        if (view.getVisibility() != View.VISIBLE && !force) {
+            return;
+        }
+        if (forceNoCircleAnimation) {
+            view.setCircleRadiusWithoutAnimation(circleRadius);
+        } else {
+            view.setCircleRadius(circleRadius, slowRadiusAnimation);
+        }
+        updateIconAlpha(view, alpha, animate);
+    }
+
+    private void updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate) {
+        float scale = getScale(alpha, view);
+        alpha = Math.min(1.0f, alpha);
+        view.setImageAlpha(alpha, animate);
+        view.setImageScale(scale, animate);
+    }
+
+    private float getScale(float alpha, KeyguardAffordanceView icon) {
+        float scale = alpha / icon.getRestingAlpha() * 0.2f +
+                KeyguardAffordanceView.MIN_ICON_SCALE_AMOUNT;
+        return Math.min(scale, KeyguardAffordanceView.MAX_ICON_SCALE_AMOUNT);
+    }
+
+    private void trackMovement(MotionEvent event) {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.addMovement(event);
+        }
+    }
+
+    private void initVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+        }
+        mVelocityTracker = VelocityTracker.obtain();
+    }
+
+    private float getCurrentVelocity(float lastX, float lastY) {
+        if (mVelocityTracker == null) {
+            return 0;
+        }
+        mVelocityTracker.computeCurrentVelocity(1000);
+        float aX = mVelocityTracker.getXVelocity();
+        float aY = mVelocityTracker.getYVelocity();
+        float bX = lastX - mInitialTouchX;
+        float bY = lastY - mInitialTouchY;
+        float bLen = (float) Math.hypot(bX, bY);
+        // Project the velocity onto the distance vector: a * b / |b|
+        float projectedVelocity = (aX * bX + aY * bY) / bLen;
+        if (mTargetedView == mRightIcon) {
+            projectedVelocity = -projectedVelocity;
+        }
+        return projectedVelocity;
+    }
+
+    public void onConfigurationChanged() {
+        initDimens();
+        initIcons();
+    }
+
+    public void onRtlPropertiesChanged() {
+        initIcons();
+    }
+
+    public void reset(boolean animate) {
+        cancelAnimation();
+        setTranslation(0.0f, true /* isReset */, animate);
+        mMotionCancelled = true;
+        if (mSwipingInProgress) {
+            mCallback.onSwipingAborted();
+            mSwipingInProgress = false;
+        }
+    }
+
+    public boolean isSwipingInProgress() {
+        return mSwipingInProgress;
+    }
+
+    public void launchAffordance(boolean animate, boolean left) {
+        if (mSwipingInProgress) {
+            // We don't want to mess with the state if the user is actually swiping already.
+            return;
+        }
+        KeyguardAffordanceView targetView = left ? mLeftIcon : mRightIcon;
+        KeyguardAffordanceView otherView = left ? mRightIcon : mLeftIcon;
+        startSwiping(targetView);
+        if (animate) {
+            fling(0, false, !left);
+            updateIcon(otherView, 0.0f, 0, true, false, true, false);
+            updateIcon(mCenterIcon, 0.0f, 0, true, false, true, false);
+        } else {
+            mCallback.onAnimationToSideStarted(!left, mTranslation, 0);
+            mTranslation = left ? mCallback.getMaxTranslationDistance()
+                    : mCallback.getMaxTranslationDistance();
+            updateIcon(mCenterIcon, 0.0f, 0.0f, false, false, true, false);
+            updateIcon(otherView, 0.0f, 0.0f, false, false, true, false);
+            targetView.instantFinishAnimation();
+            mFlingEndListener.onAnimationEnd(null);
+            mAnimationEndRunnable.run();
+        }
+    }
+
+    public interface Callback {
+
+        /**
+         * Notifies the callback when an animation to a side page was started.
+         *
+         * @param rightPage Is the page animated to the right page?
+         */
+        void onAnimationToSideStarted(boolean rightPage, float translation, float vel);
+
+        /**
+         * Notifies the callback the animation to a side page has ended.
+         */
+        void onAnimationToSideEnded();
+
+        float getMaxTranslationDistance();
+
+        void onSwipingStarted(boolean rightIcon);
+
+        void onSwipingAborted();
+
+        void onIconClicked(boolean rightIcon);
+
+        KeyguardAffordanceView getLeftIcon();
+
+        KeyguardAffordanceView getCenterIcon();
+
+        KeyguardAffordanceView getRightIcon();
+
+        View getLeftPreview();
+
+        View getRightPreview();
+
+        /**
+         * @return The factor the minimum swipe amount should be multiplied with.
+         */
+        float getAffordanceFalsingFactor();
+
+        boolean needsAntiFalsing();
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
new file mode 100644
index 0000000..f058862
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -0,0 +1,875 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
+import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+
+import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_LEFT_BUTTON;
+import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_LEFT_UNLOCK;
+import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_RIGHT_BUTTON;
+import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_RIGHT_UNLOCK;
+
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.MediaStore;
+import android.service.media.CameraPrewarmService;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.systemui.EventLogTags;
+import com.android.systemui.Dependency;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.plugins.IntentButtonProvider;
+import com.android.systemui.plugins.IntentButtonProvider.IntentButton;
+import com.android.systemui.plugins.IntentButtonProvider.IntentButton.IconState;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.KeyguardAffordanceView;
+import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.policy.AccessibilityController;
+import com.android.systemui.statusbar.policy.ExtensionController;
+import com.android.systemui.statusbar.policy.ExtensionController.Extension;
+import com.android.systemui.statusbar.policy.FlashlightController;
+import com.android.systemui.statusbar.policy.PreviewInflater;
+import com.android.systemui.tuner.LockscreenFragment.LockButtonFactory;
+import com.android.systemui.tuner.TunerService;
+
+/**
+ * Implementation for the bottom area of the Keyguard, including camera/phone affordance and status
+ * text.
+ */
+public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickListener,
+        UnlockMethodCache.OnUnlockMethodChangedListener,
+        AccessibilityController.AccessibilityStateChangedCallback, View.OnLongClickListener {
+
+    final static String TAG = "StatusBar/KeyguardBottomAreaView";
+
+    public static final String CAMERA_LAUNCH_SOURCE_AFFORDANCE = "lockscreen_affordance";
+    public static final String CAMERA_LAUNCH_SOURCE_WIGGLE = "wiggle_gesture";
+    public static final String CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP = "power_double_tap";
+    public static final String CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER = "lift_to_launch_ml";
+
+    public static final String EXTRA_CAMERA_LAUNCH_SOURCE
+            = "com.android.systemui.camera_launch_source";
+
+    private static final String LEFT_BUTTON_PLUGIN
+            = "com.android.systemui.action.PLUGIN_LOCKSCREEN_LEFT_BUTTON";
+    private static final String RIGHT_BUTTON_PLUGIN
+            = "com.android.systemui.action.PLUGIN_LOCKSCREEN_RIGHT_BUTTON";
+
+    private static final Intent SECURE_CAMERA_INTENT =
+            new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE)
+                    .addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+    public static final Intent INSECURE_CAMERA_INTENT =
+            new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
+    private static final Intent PHONE_INTENT = new Intent(Intent.ACTION_DIAL);
+    private static final int DOZE_ANIMATION_STAGGER_DELAY = 48;
+    private static final int DOZE_ANIMATION_ELEMENT_DURATION = 250;
+
+    private KeyguardAffordanceView mRightAffordanceView;
+    private KeyguardAffordanceView mLeftAffordanceView;
+    private LockIcon mLockIcon;
+    private ViewGroup mIndicationArea;
+    private TextView mEnterpriseDisclosure;
+    private TextView mIndicationText;
+    private ViewGroup mPreviewContainer;
+    private ViewGroup mOverlayContainer;
+
+    private View mLeftPreview;
+    private View mCameraPreview;
+
+    private ActivityStarter mActivityStarter;
+    private UnlockMethodCache mUnlockMethodCache;
+    private LockPatternUtils mLockPatternUtils;
+    private FlashlightController mFlashlightController;
+    private PreviewInflater mPreviewInflater;
+    private KeyguardIndicationController mIndicationController;
+    private AccessibilityController mAccessibilityController;
+    private StatusBar mStatusBar;
+    private KeyguardAffordanceHelper mAffordanceHelper;
+
+    private boolean mUserSetupComplete;
+    private boolean mPrewarmBound;
+    private Messenger mPrewarmMessenger;
+    private final ServiceConnection mPrewarmConnection = new ServiceConnection() {
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            mPrewarmMessenger = new Messenger(service);
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            mPrewarmMessenger = null;
+        }
+    };
+
+    private boolean mLeftIsVoiceAssist;
+    private AssistManager mAssistManager;
+    private Drawable mLeftAssistIcon;
+
+    private IntentButton mRightButton = new DefaultRightButton();
+    private Extension<IntentButton> mRightExtension;
+    private String mRightButtonStr;
+    private IntentButton mLeftButton = new DefaultLeftButton();
+    private Extension<IntentButton> mLeftExtension;
+    private String mLeftButtonStr;
+    private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
+    private boolean mDozing;
+
+    public KeyguardBottomAreaView(Context context) {
+        this(context, null);
+    }
+
+    public KeyguardBottomAreaView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public KeyguardBottomAreaView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public KeyguardBottomAreaView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() {
+        @Override
+        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+            super.onInitializeAccessibilityNodeInfo(host, info);
+            String label = null;
+            if (host == mLockIcon) {
+                label = getResources().getString(R.string.unlock_label);
+            } else if (host == mRightAffordanceView) {
+                label = getResources().getString(R.string.camera_label);
+            } else if (host == mLeftAffordanceView) {
+                if (mLeftIsVoiceAssist) {
+                    label = getResources().getString(R.string.voice_assist_label);
+                } else {
+                    label = getResources().getString(R.string.phone_label);
+                }
+            }
+            info.addAction(new AccessibilityAction(ACTION_CLICK, label));
+        }
+
+        @Override
+        public boolean performAccessibilityAction(View host, int action, Bundle args) {
+            if (action == ACTION_CLICK) {
+                if (host == mLockIcon) {
+                    mStatusBar.animateCollapsePanels(
+                            CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */);
+                    return true;
+                } else if (host == mRightAffordanceView) {
+                    launchCamera(CAMERA_LAUNCH_SOURCE_AFFORDANCE);
+                    return true;
+                } else if (host == mLeftAffordanceView) {
+                    launchLeftAffordance();
+                    return true;
+                }
+            }
+            return super.performAccessibilityAction(host, action, args);
+        }
+    };
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mLockPatternUtils = new LockPatternUtils(mContext);
+        mPreviewContainer = findViewById(R.id.preview_container);
+        mOverlayContainer = findViewById(R.id.overlay_container);
+        mRightAffordanceView = findViewById(R.id.camera_button);
+        mLeftAffordanceView = findViewById(R.id.left_button);
+        mLockIcon = findViewById(R.id.lock_icon);
+        mIndicationArea = findViewById(R.id.keyguard_indication_area);
+        mEnterpriseDisclosure = findViewById(
+                R.id.keyguard_indication_enterprise_disclosure);
+        mIndicationText = findViewById(R.id.keyguard_indication_text);
+        updateCameraVisibility();
+        mUnlockMethodCache = UnlockMethodCache.getInstance(getContext());
+        mUnlockMethodCache.addListener(this);
+        KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+        mLockIcon.setScreenOn(updateMonitor.isScreenOn());
+        mLockIcon.setDeviceInteractive(updateMonitor.isDeviceInteractive());
+        mLockIcon.update();
+        setClipChildren(false);
+        setClipToPadding(false);
+        mPreviewInflater = new PreviewInflater(mContext, new LockPatternUtils(mContext));
+        inflateCameraPreview();
+        mLockIcon.setOnClickListener(this);
+        mLockIcon.setOnLongClickListener(this);
+        mRightAffordanceView.setOnClickListener(this);
+        mLeftAffordanceView.setOnClickListener(this);
+        initAccessibility();
+        mActivityStarter = Dependency.get(ActivityStarter.class);
+        mFlashlightController = Dependency.get(FlashlightController.class);
+        mAccessibilityController = Dependency.get(AccessibilityController.class);
+        mAssistManager = Dependency.get(AssistManager.class);
+        mLockIcon.setAccessibilityController(mAccessibilityController);
+        updateLeftAffordance();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mAccessibilityController.addStateChangedCallback(this);
+        mRightExtension = Dependency.get(ExtensionController.class).newExtension(IntentButton.class)
+                .withPlugin(IntentButtonProvider.class, RIGHT_BUTTON_PLUGIN,
+                        p -> p.getIntentButton())
+                .withTunerFactory(new LockButtonFactory(mContext, LOCKSCREEN_RIGHT_BUTTON))
+                .withDefault(() -> new DefaultRightButton())
+                .withCallback(button -> setRightButton(button))
+                .build();
+        mLeftExtension = Dependency.get(ExtensionController.class).newExtension(IntentButton.class)
+                .withPlugin(IntentButtonProvider.class, LEFT_BUTTON_PLUGIN,
+                        p -> p.getIntentButton())
+                .withTunerFactory(new LockButtonFactory(mContext, LOCKSCREEN_LEFT_BUTTON))
+                .withDefault(() -> new DefaultLeftButton())
+                .withCallback(button -> setLeftButton(button))
+                .build();
+        final IntentFilter filter = new IntentFilter();
+        filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED);
+        getContext().registerReceiverAsUser(mDevicePolicyReceiver,
+                UserHandle.ALL, filter, null, null);
+        KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateMonitorCallback);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mAccessibilityController.removeStateChangedCallback(this);
+        mRightExtension.destroy();
+        mLeftExtension.destroy();
+        getContext().unregisterReceiver(mDevicePolicyReceiver);
+        KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateMonitorCallback);
+    }
+
+    private void initAccessibility() {
+        mLockIcon.setAccessibilityDelegate(mAccessibilityDelegate);
+        mLeftAffordanceView.setAccessibilityDelegate(mAccessibilityDelegate);
+        mRightAffordanceView.setAccessibilityDelegate(mAccessibilityDelegate);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        int indicationBottomMargin = getResources().getDimensionPixelSize(
+                R.dimen.keyguard_indication_margin_bottom);
+        MarginLayoutParams mlp = (MarginLayoutParams) mIndicationArea.getLayoutParams();
+        if (mlp.bottomMargin != indicationBottomMargin) {
+            mlp.bottomMargin = indicationBottomMargin;
+            mIndicationArea.setLayoutParams(mlp);
+        }
+
+        // Respect font size setting.
+        mEnterpriseDisclosure.setTextSize(TypedValue.COMPLEX_UNIT_PX,
+                getResources().getDimensionPixelSize(
+                        com.android.internal.R.dimen.text_size_small_material));
+        mIndicationText.setTextSize(TypedValue.COMPLEX_UNIT_PX,
+                getResources().getDimensionPixelSize(
+                        com.android.internal.R.dimen.text_size_small_material));
+
+        ViewGroup.LayoutParams lp = mRightAffordanceView.getLayoutParams();
+        lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_width);
+        lp.height = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_height);
+        mRightAffordanceView.setLayoutParams(lp);
+        updateRightAffordanceIcon();
+
+        lp = mLockIcon.getLayoutParams();
+        lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_width);
+        lp.height = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_height);
+        mLockIcon.setLayoutParams(lp);
+        mLockIcon.setContentDescription(getContext().getText(R.string.accessibility_unlock_button));
+        mLockIcon.update(true /* force */);
+
+        lp = mLeftAffordanceView.getLayoutParams();
+        lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_width);
+        lp.height = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_height);
+        mLeftAffordanceView.setLayoutParams(lp);
+        updateLeftAffordanceIcon();
+    }
+
+    private void updateRightAffordanceIcon() {
+        IconState state = mRightButton.getIcon();
+        mRightAffordanceView.setVisibility(!mDozing && state.isVisible ? View.VISIBLE : View.GONE);
+        mRightAffordanceView.setImageDrawable(state.drawable, state.tint);
+        mRightAffordanceView.setContentDescription(state.contentDescription);
+    }
+
+    public void setStatusBar(StatusBar statusBar) {
+        mStatusBar = statusBar;
+        updateCameraVisibility(); // in case onFinishInflate() was called too early
+    }
+
+    public void setAffordanceHelper(KeyguardAffordanceHelper affordanceHelper) {
+        mAffordanceHelper = affordanceHelper;
+    }
+
+    public void setUserSetupComplete(boolean userSetupComplete) {
+        mUserSetupComplete = userSetupComplete;
+        updateCameraVisibility();
+        updateLeftAffordanceIcon();
+    }
+
+    private Intent getCameraIntent() {
+        return mRightButton.getIntent();
+    }
+
+    /**
+     * Resolves the intent to launch the camera application.
+     */
+    public ResolveInfo resolveCameraIntent() {
+        return mContext.getPackageManager().resolveActivityAsUser(getCameraIntent(),
+                PackageManager.MATCH_DEFAULT_ONLY,
+                KeyguardUpdateMonitor.getCurrentUser());
+    }
+
+    private void updateCameraVisibility() {
+        if (mRightAffordanceView == null) {
+            // Things are not set up yet; reply hazy, ask again later
+            return;
+        }
+        mRightAffordanceView.setVisibility(!mDozing && mRightButton.getIcon().isVisible
+                ? View.VISIBLE : View.GONE);
+    }
+
+    /**
+     * Set an alternate icon for the left assist affordance (replace the mic icon)
+     */
+    public void setLeftAssistIcon(Drawable drawable) {
+        mLeftAssistIcon = drawable;
+        updateLeftAffordanceIcon();
+    }
+
+    private void updateLeftAffordanceIcon() {
+        IconState state = mLeftButton.getIcon();
+        mLeftAffordanceView.setVisibility(!mDozing && state.isVisible ? View.VISIBLE : View.GONE);
+        mLeftAffordanceView.setImageDrawable(state.drawable, state.tint);
+        mLeftAffordanceView.setContentDescription(state.contentDescription);
+    }
+
+    public boolean isLeftVoiceAssist() {
+        return mLeftIsVoiceAssist;
+    }
+
+    private boolean isPhoneVisible() {
+        PackageManager pm = mContext.getPackageManager();
+        return pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
+                && pm.resolveActivity(PHONE_INTENT, 0) != null;
+    }
+
+    @Override
+    public void onStateChanged(boolean accessibilityEnabled, boolean touchExplorationEnabled) {
+        mRightAffordanceView.setClickable(touchExplorationEnabled);
+        mLeftAffordanceView.setClickable(touchExplorationEnabled);
+        mRightAffordanceView.setFocusable(accessibilityEnabled);
+        mLeftAffordanceView.setFocusable(accessibilityEnabled);
+        mLockIcon.update();
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (v == mRightAffordanceView) {
+            launchCamera(CAMERA_LAUNCH_SOURCE_AFFORDANCE);
+        } else if (v == mLeftAffordanceView) {
+            launchLeftAffordance();
+        } if (v == mLockIcon) {
+            if (!mAccessibilityController.isAccessibilityEnabled()) {
+                handleTrustCircleClick();
+            } else {
+                mStatusBar.animateCollapsePanels(
+                        CommandQueue.FLAG_EXCLUDE_NONE, true /* force */);
+            }
+        }
+    }
+
+    @Override
+    public boolean onLongClick(View v) {
+        handleTrustCircleClick();
+        return true;
+    }
+
+    private void handleTrustCircleClick() {
+        mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_LOCK, 0 /* lengthDp - N/A */,
+                0 /* velocityDp - N/A */);
+        mIndicationController.showTransientIndication(
+                R.string.keyguard_indication_trust_disabled);
+        mLockPatternUtils.requireCredentialEntry(KeyguardUpdateMonitor.getCurrentUser());
+    }
+
+    public void bindCameraPrewarmService() {
+        Intent intent = getCameraIntent();
+        ActivityInfo targetInfo = PreviewInflater.getTargetActivityInfo(mContext, intent,
+                KeyguardUpdateMonitor.getCurrentUser(), true /* onlyDirectBootAware */);
+        if (targetInfo != null && targetInfo.metaData != null) {
+            String clazz = targetInfo.metaData.getString(
+                    MediaStore.META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE);
+            if (clazz != null) {
+                Intent serviceIntent = new Intent();
+                serviceIntent.setClassName(targetInfo.packageName, clazz);
+                serviceIntent.setAction(CameraPrewarmService.ACTION_PREWARM);
+                try {
+                    if (getContext().bindServiceAsUser(serviceIntent, mPrewarmConnection,
+                            Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
+                            new UserHandle(UserHandle.USER_CURRENT))) {
+                        mPrewarmBound = true;
+                    }
+                } catch (SecurityException e) {
+                    Log.w(TAG, "Unable to bind to prewarm service package=" + targetInfo.packageName
+                            + " class=" + clazz, e);
+                }
+            }
+        }
+    }
+
+    public void unbindCameraPrewarmService(boolean launched) {
+        if (mPrewarmBound) {
+            if (mPrewarmMessenger != null && launched) {
+                try {
+                    mPrewarmMessenger.send(Message.obtain(null /* handler */,
+                            CameraPrewarmService.MSG_CAMERA_FIRED));
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Error sending camera fired message", e);
+                }
+            }
+            mContext.unbindService(mPrewarmConnection);
+            mPrewarmBound = false;
+        }
+    }
+
+    public void launchCamera(String source) {
+        final Intent intent = getCameraIntent();
+        intent.putExtra(EXTRA_CAMERA_LAUNCH_SOURCE, source);
+        boolean wouldLaunchResolverActivity = PreviewInflater.wouldLaunchResolverActivity(
+                mContext, intent, KeyguardUpdateMonitor.getCurrentUser());
+        if (intent == SECURE_CAMERA_INTENT && !wouldLaunchResolverActivity) {
+            AsyncTask.execute(new Runnable() {
+                @Override
+                public void run() {
+                    int result = ActivityManager.START_CANCELED;
+
+                    // Normally an activity will set it's requested rotation
+                    // animation on its window. However when launching an activity
+                    // causes the orientation to change this is too late. In these cases
+                    // the default animation is used. This doesn't look good for
+                    // the camera (as it rotates the camera contents out of sync
+                    // with physical reality). So, we ask the WindowManager to
+                    // force the crossfade animation if an orientation change
+                    // happens to occur during the launch.
+                    ActivityOptions o = ActivityOptions.makeBasic();
+                    o.setDisallowEnterPictureInPictureWhileLaunching(true);
+                    o.setRotationAnimationHint(
+                            WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS);
+                    try {
+                        result = ActivityManager.getService().startActivityAsUser(
+                                null, getContext().getBasePackageName(),
+                                intent,
+                                intent.resolveTypeIfNeeded(getContext().getContentResolver()),
+                                null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null, o.toBundle(),
+                                UserHandle.CURRENT.getIdentifier());
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "Unable to start camera activity", e);
+                    }
+                    final boolean launched = isSuccessfulLaunch(result);
+                    post(new Runnable() {
+                        @Override
+                        public void run() {
+                            unbindCameraPrewarmService(launched);
+                        }
+                    });
+                }
+            });
+        } else {
+
+            // We need to delay starting the activity because ResolverActivity finishes itself if
+            // launched behind lockscreen.
+            mActivityStarter.startActivity(intent, false /* dismissShade */,
+                    new ActivityStarter.Callback() {
+                        @Override
+                        public void onActivityStarted(int resultCode) {
+                            unbindCameraPrewarmService(isSuccessfulLaunch(resultCode));
+                        }
+                    });
+        }
+    }
+
+    private static boolean isSuccessfulLaunch(int result) {
+        return result == ActivityManager.START_SUCCESS
+                || result == ActivityManager.START_DELIVERED_TO_TOP
+                || result == ActivityManager.START_TASK_TO_FRONT;
+    }
+
+    public void launchLeftAffordance() {
+        if (mLeftIsVoiceAssist) {
+            launchVoiceAssist();
+        } else {
+            launchPhone();
+        }
+    }
+
+    private void launchVoiceAssist() {
+        Runnable runnable = new Runnable() {
+            @Override
+            public void run() {
+                mAssistManager.launchVoiceAssistFromKeyguard();
+            }
+        };
+        if (mStatusBar.isKeyguardCurrentlySecure()) {
+            AsyncTask.execute(runnable);
+        } else {
+            boolean dismissShade = !TextUtils.isEmpty(mRightButtonStr)
+                    && Dependency.get(TunerService.class).getValue(LOCKSCREEN_RIGHT_UNLOCK, 1) != 0;
+            mStatusBar.executeRunnableDismissingKeyguard(runnable, null /* cancelAction */,
+                    dismissShade, false /* afterKeyguardGone */, true /* deferred */);
+        }
+    }
+
+    private boolean canLaunchVoiceAssist() {
+        return mAssistManager.canVoiceAssistBeLaunchedFromKeyguard();
+    }
+
+    private void launchPhone() {
+        final TelecomManager tm = TelecomManager.from(mContext);
+        if (tm.isInCall()) {
+            AsyncTask.execute(new Runnable() {
+                @Override
+                public void run() {
+                    tm.showInCallScreen(false /* showDialpad */);
+                }
+            });
+        } else {
+            boolean dismissShade = !TextUtils.isEmpty(mLeftButtonStr)
+                    && Dependency.get(TunerService.class).getValue(LOCKSCREEN_LEFT_UNLOCK, 1) != 0;
+            mActivityStarter.startActivity(mLeftButton.getIntent(), dismissShade);
+        }
+    }
+
+
+    @Override
+    protected void onVisibilityChanged(View changedView, int visibility) {
+        super.onVisibilityChanged(changedView, visibility);
+        if (changedView == this && visibility == VISIBLE) {
+            mLockIcon.update();
+            updateCameraVisibility();
+        }
+    }
+
+    public KeyguardAffordanceView getLeftView() {
+        return mLeftAffordanceView;
+    }
+
+    public KeyguardAffordanceView getRightView() {
+        return mRightAffordanceView;
+    }
+
+    public View getLeftPreview() {
+        return mLeftPreview;
+    }
+
+    public View getRightPreview() {
+        return mCameraPreview;
+    }
+
+    public LockIcon getLockIcon() {
+        return mLockIcon;
+    }
+
+    public View getIndicationArea() {
+        return mIndicationArea;
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    @Override
+    public void onUnlockMethodStateChanged() {
+        mLockIcon.update();
+        updateCameraVisibility();
+    }
+
+    private void inflateCameraPreview() {
+        View previewBefore = mCameraPreview;
+        boolean visibleBefore = false;
+        if (previewBefore != null) {
+            mPreviewContainer.removeView(previewBefore);
+            visibleBefore = previewBefore.getVisibility() == View.VISIBLE;
+        }
+        mCameraPreview = mPreviewInflater.inflatePreview(getCameraIntent());
+        if (mCameraPreview != null) {
+            mPreviewContainer.addView(mCameraPreview);
+            mCameraPreview.setVisibility(visibleBefore ? View.VISIBLE : View.INVISIBLE);
+        }
+        if (mAffordanceHelper != null) {
+            mAffordanceHelper.updatePreviews();
+        }
+    }
+
+    private void updateLeftPreview() {
+        View previewBefore = mLeftPreview;
+        if (previewBefore != null) {
+            mPreviewContainer.removeView(previewBefore);
+        }
+        if (mLeftIsVoiceAssist) {
+            mLeftPreview = mPreviewInflater.inflatePreviewFromService(
+                    mAssistManager.getVoiceInteractorComponentName());
+        } else {
+            mLeftPreview = mPreviewInflater.inflatePreview(mLeftButton.getIntent());
+        }
+        if (mLeftPreview != null) {
+            mPreviewContainer.addView(mLeftPreview);
+            mLeftPreview.setVisibility(View.INVISIBLE);
+        }
+        if (mAffordanceHelper != null) {
+            mAffordanceHelper.updatePreviews();
+        }
+    }
+
+    public void startFinishDozeAnimation() {
+        long delay = 0;
+        if (mLeftAffordanceView.getVisibility() == View.VISIBLE) {
+            startFinishDozeAnimationElement(mLeftAffordanceView, delay);
+            delay += DOZE_ANIMATION_STAGGER_DELAY;
+        }
+        startFinishDozeAnimationElement(mLockIcon, delay);
+        delay += DOZE_ANIMATION_STAGGER_DELAY;
+        if (mRightAffordanceView.getVisibility() == View.VISIBLE) {
+            startFinishDozeAnimationElement(mRightAffordanceView, delay);
+        }
+        mIndicationArea.setAlpha(0f);
+        mIndicationArea.animate()
+                .alpha(1f)
+                .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
+                .setDuration(NotificationPanelView.DOZE_ANIMATION_DURATION);
+    }
+
+    private void startFinishDozeAnimationElement(View element, long delay) {
+        element.setAlpha(0f);
+        element.setTranslationY(element.getHeight() / 2);
+        element.animate()
+                .alpha(1f)
+                .translationY(0f)
+                .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
+                .setStartDelay(delay)
+                .setDuration(DOZE_ANIMATION_ELEMENT_DURATION);
+    }
+
+    private final BroadcastReceiver mDevicePolicyReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    updateCameraVisibility();
+                }
+            });
+        }
+    };
+
+    private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback =
+            new KeyguardUpdateMonitorCallback() {
+                @Override
+                public void onUserSwitchComplete(int userId) {
+                    updateCameraVisibility();
+                }
+
+                @Override
+                public void onStartedWakingUp() {
+                    mLockIcon.setDeviceInteractive(true);
+                }
+
+                @Override
+                public void onFinishedGoingToSleep(int why) {
+                    mLockIcon.setDeviceInteractive(false);
+                }
+
+                @Override
+                public void onScreenTurnedOn() {
+                    mLockIcon.setScreenOn(true);
+                }
+
+                @Override
+                public void onScreenTurnedOff() {
+                    mLockIcon.setScreenOn(false);
+                }
+
+                @Override
+                public void onKeyguardVisibilityChanged(boolean showing) {
+                    mLockIcon.update();
+                }
+
+                @Override
+                public void onFingerprintRunningStateChanged(boolean running) {
+                    mLockIcon.update();
+                }
+
+                @Override
+                public void onStrongAuthStateChanged(int userId) {
+                    mLockIcon.update();
+        }
+
+                @Override
+                public void onUserUnlocked() {
+                    inflateCameraPreview();
+                    updateCameraVisibility();
+                    updateLeftAffordance();
+                }
+            };
+
+    public void setKeyguardIndicationController(
+            KeyguardIndicationController keyguardIndicationController) {
+        mIndicationController = keyguardIndicationController;
+    }
+
+    public void updateLeftAffordance() {
+        updateLeftAffordanceIcon();
+        updateLeftPreview();
+    }
+
+    public void onKeyguardShowingChanged() {
+        updateLeftAffordance();
+        inflateCameraPreview();
+    }
+
+    private void setRightButton(IntentButton button) {
+        mRightButton = button;
+        updateRightAffordanceIcon();
+        updateCameraVisibility();
+        inflateCameraPreview();
+    }
+
+    private void setLeftButton(IntentButton button) {
+        mLeftButton = button;
+        if (!(mLeftButton instanceof DefaultLeftButton)) {
+            mLeftIsVoiceAssist = false;
+        }
+        updateLeftAffordance();
+    }
+
+    public void setDozing(boolean dozing, boolean animate) {
+        mDozing = dozing;
+
+        updateCameraVisibility();
+        updateLeftAffordanceIcon();
+
+        if (dozing) {
+            mLockIcon.setVisibility(INVISIBLE);
+            mOverlayContainer.setVisibility(INVISIBLE);
+        } else {
+            mLockIcon.setVisibility(VISIBLE);
+            mOverlayContainer.setVisibility(VISIBLE);
+            if (animate) {
+                startFinishDozeAnimation();
+            }
+        }
+    }
+
+    private class DefaultLeftButton implements IntentButton {
+
+        private IconState mIconState = new IconState();
+
+        @Override
+        public IconState getIcon() {
+            mLeftIsVoiceAssist = canLaunchVoiceAssist();
+            if (mLeftIsVoiceAssist) {
+                mIconState.isVisible = mUserSetupComplete;
+                if (mLeftAssistIcon == null) {
+                    mIconState.drawable = mContext.getDrawable(R.drawable.ic_mic_26dp);
+                } else {
+                    mIconState.drawable = mLeftAssistIcon;
+                }
+                mIconState.contentDescription = mContext.getString(
+                        R.string.accessibility_voice_assist_button);
+            } else {
+                mIconState.isVisible = mUserSetupComplete && isPhoneVisible();
+                mIconState.drawable = mContext.getDrawable(R.drawable.ic_phone_24dp);
+                mIconState.contentDescription = mContext.getString(
+                        R.string.accessibility_phone_button);
+            }
+            return mIconState;
+        }
+
+        @Override
+        public Intent getIntent() {
+            return PHONE_INTENT;
+        }
+    }
+
+    private class DefaultRightButton implements IntentButton {
+
+        private IconState mIconState = new IconState();
+
+        @Override
+        public IconState getIcon() {
+            ResolveInfo resolved = resolveCameraIntent();
+            boolean isCameraDisabled = (mStatusBar != null) && !mStatusBar.isCameraAllowedByAdmin();
+            mIconState.isVisible = !isCameraDisabled && resolved != null
+                    && getResources().getBoolean(R.bool.config_keyguardShowCameraAffordance)
+                    && mUserSetupComplete;
+            mIconState.drawable = mContext.getDrawable(R.drawable.ic_camera_alt_24dp);
+            mIconState.contentDescription =
+                    mContext.getString(R.string.accessibility_camera_button);
+            return mIconState;
+        }
+
+        @Override
+        public Intent getIntent() {
+            KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+            boolean canSkipBouncer = updateMonitor.getUserCanSkipBouncer(
+                    KeyguardUpdateMonitor.getCurrentUser());
+            boolean secure = mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser());
+            return (secure && !canSkipBouncer) ? SECURE_CAMERA_INTENT : INSECURE_CAMERA_INTENT;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/com/android/systemui/statusbar/phone/KeyguardBouncer.java
new file mode 100644
index 0000000..165ed78
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Slog;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.KeyguardHostView;
+import com.android.keyguard.KeyguardSecurityView;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.R;
+import com.android.keyguard.ViewMediatorCallback;
+import com.android.systemui.DejankUtils;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.keyguard.DismissCallbackRegistry;
+
+import static com.android.keyguard.KeyguardHostView.OnDismissAction;
+import static com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+
+/**
+ * A class which manages the bouncer on the lockscreen.
+ */
+public class KeyguardBouncer {
+
+    final static private String TAG = "KeyguardBouncer";
+
+    protected final Context mContext;
+    protected final ViewMediatorCallback mCallback;
+    protected final LockPatternUtils mLockPatternUtils;
+    protected final ViewGroup mContainer;
+    private final FalsingManager mFalsingManager;
+    private final DismissCallbackRegistry mDismissCallbackRegistry;
+    private final Handler mHandler;
+    protected KeyguardHostView mKeyguardView;
+    protected ViewGroup mRoot;
+    private boolean mShowingSoon;
+    private int mBouncerPromptReason;
+    private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback =
+            new KeyguardUpdateMonitorCallback() {
+                @Override
+                public void onStrongAuthStateChanged(int userId) {
+                    mBouncerPromptReason = mCallback.getBouncerPromptReason();
+                }
+            };
+    private final Runnable mRemoveViewRunnable = this::removeView;
+
+    public KeyguardBouncer(Context context, ViewMediatorCallback callback,
+            LockPatternUtils lockPatternUtils, ViewGroup container,
+            DismissCallbackRegistry dismissCallbackRegistry) {
+        mContext = context;
+        mCallback = callback;
+        mLockPatternUtils = lockPatternUtils;
+        mContainer = container;
+        KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateMonitorCallback);
+        mFalsingManager = FalsingManager.getInstance(mContext);
+        mDismissCallbackRegistry = dismissCallbackRegistry;
+        mHandler = new Handler();
+    }
+
+    public void show(boolean resetSecuritySelection) {
+        final int keyguardUserId = KeyguardUpdateMonitor.getCurrentUser();
+        if (keyguardUserId == UserHandle.USER_SYSTEM && UserManager.isSplitSystemUser()) {
+            // In split system user mode, we never unlock system user.
+            return;
+        }
+        mFalsingManager.onBouncerShown();
+        ensureView();
+        if (resetSecuritySelection) {
+            // showPrimarySecurityScreen() updates the current security method. This is needed in
+            // case we are already showing and the current security method changed.
+            mKeyguardView.showPrimarySecurityScreen();
+        }
+        if (mRoot.getVisibility() == View.VISIBLE || mShowingSoon) {
+            return;
+        }
+
+        final int activeUserId = ActivityManager.getCurrentUser();
+        final boolean isSystemUser =
+                UserManager.isSplitSystemUser() && activeUserId == UserHandle.USER_SYSTEM;
+        final boolean allowDismissKeyguard = !isSystemUser && activeUserId == keyguardUserId;
+
+        // If allowed, try to dismiss the Keyguard. If no security auth (password/pin/pattern) is
+        // set, this will dismiss the whole Keyguard. Otherwise, show the bouncer.
+        if (allowDismissKeyguard && mKeyguardView.dismiss(activeUserId)) {
+            return;
+        }
+
+        // This condition may indicate an error on Android, so log it.
+        if (!allowDismissKeyguard) {
+            Slog.w(TAG, "User can't dismiss keyguard: " + activeUserId + " != " + keyguardUserId);
+        }
+
+        mShowingSoon = true;
+
+        // Split up the work over multiple frames.
+        DejankUtils.postAfterTraversal(mShowRunnable);
+    }
+
+    private final Runnable mShowRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mRoot.setVisibility(View.VISIBLE);
+            mKeyguardView.onResume();
+            showPromptReason(mBouncerPromptReason);
+            if (mKeyguardView.getHeight() != 0) {
+                mKeyguardView.startAppearAnimation();
+            } else {
+                mKeyguardView.getViewTreeObserver().addOnPreDrawListener(
+                        new ViewTreeObserver.OnPreDrawListener() {
+                            @Override
+                            public boolean onPreDraw() {
+                                mKeyguardView.getViewTreeObserver().removeOnPreDrawListener(this);
+                                mKeyguardView.startAppearAnimation();
+                                return true;
+                            }
+                        });
+                mKeyguardView.requestLayout();
+            }
+            mShowingSoon = false;
+            mKeyguardView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+        }
+    };
+
+    /**
+     * Show a string explaining why the security view needs to be solved.
+     *
+     * @param reason a flag indicating which string should be shown, see
+     *               {@link KeyguardSecurityView#PROMPT_REASON_NONE}
+     *               and {@link KeyguardSecurityView#PROMPT_REASON_RESTART}
+     */
+    public void showPromptReason(int reason) {
+        mKeyguardView.showPromptReason(reason);
+    }
+
+    public void showMessage(String message, int color) {
+        mKeyguardView.showMessage(message, color);
+    }
+
+    private void cancelShowRunnable() {
+        DejankUtils.removeCallbacks(mShowRunnable);
+        mShowingSoon = false;
+    }
+
+    public void showWithDismissAction(OnDismissAction r, Runnable cancelAction) {
+        ensureView();
+        mKeyguardView.setOnDismissAction(r, cancelAction);
+        show(false /* resetSecuritySelection */);
+    }
+
+    public void hide(boolean destroyView) {
+        if (isShowing()) {
+            mDismissCallbackRegistry.notifyDismissCancelled();
+        }
+        mFalsingManager.onBouncerHidden();
+        cancelShowRunnable();
+        if (mKeyguardView != null) {
+            mKeyguardView.cancelDismissAction();
+            mKeyguardView.cleanUp();
+        }
+        if (mRoot != null) {
+            mRoot.setVisibility(View.INVISIBLE);
+            if (destroyView) {
+
+                // We have a ViewFlipper that unregisters a broadcast when being detached, which may
+                // be slow because of AM lock contention during unlocking. We can delay it a bit.
+                mHandler.postDelayed(mRemoveViewRunnable, 50);
+            }
+        }
+    }
+
+    /**
+     * See {@link StatusBarKeyguardViewManager#startPreHideAnimation}.
+     */
+    public void startPreHideAnimation(Runnable runnable) {
+        if (mKeyguardView != null) {
+            mKeyguardView.startDisappearAnimation(runnable);
+        } else if (runnable != null) {
+            runnable.run();
+        }
+    }
+
+    /**
+     * Reset the state of the view.
+     */
+    public void reset() {
+        cancelShowRunnable();
+        inflateView();
+        mFalsingManager.onBouncerHidden();
+    }
+
+    public void onScreenTurnedOff() {
+        if (mKeyguardView != null && mRoot != null && mRoot.getVisibility() == View.VISIBLE) {
+            mKeyguardView.onPause();
+        }
+    }
+
+    public boolean isShowing() {
+        return mShowingSoon || (mRoot != null && mRoot.getVisibility() == View.VISIBLE);
+    }
+
+    public void prepare() {
+        boolean wasInitialized = mRoot != null;
+        ensureView();
+        if (wasInitialized) {
+            mKeyguardView.showPrimarySecurityScreen();
+        }
+        mBouncerPromptReason = mCallback.getBouncerPromptReason();
+    }
+
+    protected void ensureView() {
+        // Removal of the view might be deferred to reduce unlock latency,
+        // in this case we need to force the removal, otherwise we'll
+        // end up in an unpredictable state.
+        boolean forceRemoval = mHandler.hasCallbacks(mRemoveViewRunnable);
+        if (mRoot == null || forceRemoval) {
+            inflateView();
+        }
+    }
+
+    protected void inflateView() {
+        removeView();
+        mHandler.removeCallbacks(mRemoveViewRunnable);
+        mRoot = (ViewGroup) LayoutInflater.from(mContext).inflate(R.layout.keyguard_bouncer, null);
+        mKeyguardView = mRoot.findViewById(R.id.keyguard_host_view);
+        mKeyguardView.setLockPatternUtils(mLockPatternUtils);
+        mKeyguardView.setViewMediatorCallback(mCallback);
+        mContainer.addView(mRoot, mContainer.getChildCount());
+        mRoot.setVisibility(View.INVISIBLE);
+
+        final WindowInsets rootInsets = mRoot.getRootWindowInsets();
+        if (rootInsets != null) {
+            mRoot.dispatchApplyWindowInsets(rootInsets);
+        }
+    }
+
+    protected void removeView() {
+        if (mRoot != null && mRoot.getParent() == mContainer) {
+            mContainer.removeView(mRoot);
+            mRoot = null;
+        }
+    }
+
+    public boolean onBackPressed() {
+        return mKeyguardView != null && mKeyguardView.handleBackKey();
+    }
+
+    /**
+     * @return True if and only if the security method should be shown before showing the
+     * notifications on Keyguard, like SIM PIN/PUK.
+     */
+    public boolean needsFullscreenBouncer() {
+        ensureView();
+        if (mKeyguardView != null) {
+            SecurityMode mode = mKeyguardView.getSecurityMode();
+            return mode == SecurityMode.SimPin || mode == SecurityMode.SimPuk;
+        }
+        return false;
+    }
+
+    /**
+     * Like {@link #needsFullscreenBouncer}, but uses the currently visible security method, which
+     * makes this method much faster.
+     */
+    public boolean isFullscreenBouncer() {
+        if (mKeyguardView != null) {
+            SecurityMode mode = mKeyguardView.getCurrentSecurityMode();
+            return mode == SecurityMode.SimPin || mode == SecurityMode.SimPuk;
+        }
+        return false;
+    }
+
+    /**
+     * WARNING: This method might cause Binder calls.
+     */
+    public boolean isSecure() {
+        return mKeyguardView == null || mKeyguardView.getSecurityMode() != SecurityMode.None;
+    }
+
+    public boolean shouldDismissOnMenuPressed() {
+        return mKeyguardView.shouldEnableMenuKey();
+    }
+
+    public boolean interceptMediaKey(KeyEvent event) {
+        ensureView();
+        return mKeyguardView.interceptMediaKey(event);
+    }
+
+    public void notifyKeyguardAuthenticated(boolean strongAuth) {
+        ensureView();
+        mKeyguardView.finish(strongAuth, KeyguardUpdateMonitor.getCurrentUser());
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
new file mode 100644
index 0000000..f7aa818
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import static com.android.systemui.statusbar.notification.NotificationUtils.interpolate;
+
+import android.content.res.Resources;
+import android.graphics.Path;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.PathInterpolator;
+
+import com.android.systemui.R;
+
+/**
+ * Utility class to calculate the clock position and top padding of notifications on Keyguard.
+ */
+public class KeyguardClockPositionAlgorithm {
+
+    private static final float SLOW_DOWN_FACTOR = 0.4f;
+
+    private static final float CLOCK_RUBBERBAND_FACTOR_MIN = 0.08f;
+    private static final float CLOCK_RUBBERBAND_FACTOR_MAX = 0.8f;
+    private static final float CLOCK_SCALE_FADE_START = 0.95f;
+    private static final float CLOCK_SCALE_FADE_END = 0.75f;
+    private static final float CLOCK_SCALE_FADE_END_NO_NOTIFS = 0.5f;
+
+    private static final float CLOCK_ADJ_TOP_PADDING_MULTIPLIER_MIN = 1.4f;
+    private static final float CLOCK_ADJ_TOP_PADDING_MULTIPLIER_MAX = 3.2f;
+
+    private static final long MILLIS_PER_MINUTES = 1000 * 60;
+    private static final float BURN_IN_PREVENTION_PERIOD_Y = 521;
+    private static final float BURN_IN_PREVENTION_PERIOD_X = 83;
+
+    private int mClockNotificationsMarginMin;
+    private int mClockNotificationsMarginMax;
+    private float mClockYFractionMin;
+    private float mClockYFractionMax;
+    private int mMaxKeyguardNotifications;
+    private int mMaxPanelHeight;
+    private float mExpandedHeight;
+    private int mNotificationCount;
+    private int mHeight;
+    private int mKeyguardStatusHeight;
+    private float mEmptyDragAmount;
+    private float mDensity;
+    private int mBurnInPreventionOffsetX;
+    private int mBurnInPreventionOffsetY;
+
+    /**
+     * The number (fractional) of notifications the "more" card counts when calculating how many
+     * notifications are currently visible for the y positioning of the clock.
+     */
+    private float mMoreCardNotificationAmount;
+
+    private static final PathInterpolator sSlowDownInterpolator;
+
+    static {
+        Path path = new Path();
+        path.moveTo(0, 0);
+        path.cubicTo(0.3f, 0.875f, 0.6f, 1f, 1f, 1f);
+        sSlowDownInterpolator = new PathInterpolator(path);
+    }
+
+    private AccelerateInterpolator mAccelerateInterpolator = new AccelerateInterpolator();
+    private int mClockBottom;
+    private float mDarkAmount;
+    private int mDozingStackPadding;
+
+    /**
+     * Refreshes the dimension values.
+     */
+    public void loadDimens(Resources res) {
+        mClockNotificationsMarginMin = res.getDimensionPixelSize(
+                R.dimen.keyguard_clock_notifications_margin_min);
+        mClockNotificationsMarginMax = res.getDimensionPixelSize(
+                R.dimen.keyguard_clock_notifications_margin_max);
+        mClockYFractionMin = res.getFraction(R.fraction.keyguard_clock_y_fraction_min, 1, 1);
+        mClockYFractionMax = res.getFraction(R.fraction.keyguard_clock_y_fraction_max, 1, 1);
+        mMoreCardNotificationAmount =
+                (float) res.getDimensionPixelSize(R.dimen.notification_shelf_height) /
+                        res.getDimensionPixelSize(R.dimen.notification_min_height);
+        mDensity = res.getDisplayMetrics().density;
+        mBurnInPreventionOffsetX = res.getDimensionPixelSize(
+                R.dimen.burn_in_prevention_offset_x);
+        mBurnInPreventionOffsetY = res.getDimensionPixelSize(
+                R.dimen.burn_in_prevention_offset_y);
+        mDozingStackPadding = res.getDimensionPixelSize(R.dimen.dozing_stack_padding);
+    }
+
+    public void setup(int maxKeyguardNotifications, int maxPanelHeight, float expandedHeight,
+            int notificationCount, int height, int keyguardStatusHeight, float emptyDragAmount,
+            int clockBottom, float dark) {
+        mMaxKeyguardNotifications = maxKeyguardNotifications;
+        mMaxPanelHeight = maxPanelHeight;
+        mExpandedHeight = expandedHeight;
+        mNotificationCount = notificationCount;
+        mHeight = height;
+        mKeyguardStatusHeight = keyguardStatusHeight;
+        mEmptyDragAmount = emptyDragAmount;
+        mClockBottom = clockBottom;
+        mDarkAmount = dark;
+    }
+
+    public float getMinStackScrollerPadding(int height, int keyguardStatusHeight) {
+        return mClockYFractionMin * height + keyguardStatusHeight / 2
+                + mClockNotificationsMarginMin;
+    }
+
+    public void run(Result result) {
+        int y = getClockY() - mKeyguardStatusHeight / 2;
+        float clockAdjustment = getClockYExpansionAdjustment();
+        float topPaddingAdjMultiplier = getTopPaddingAdjMultiplier();
+        result.stackScrollerPaddingAdjustment = (int) (clockAdjustment*topPaddingAdjMultiplier);
+        int clockNotificationsPadding = getClockNotificationsPadding()
+                + result.stackScrollerPaddingAdjustment;
+        int padding = y + clockNotificationsPadding;
+        result.clockY = y;
+        result.stackScrollerPadding = mKeyguardStatusHeight + padding;
+        result.clockScale = getClockScale(result.stackScrollerPadding,
+                result.clockY,
+                y + getClockNotificationsPadding() + mKeyguardStatusHeight);
+        result.clockAlpha = getClockAlpha(result.clockScale);
+
+        result.stackScrollerPadding = (int) interpolate(
+                result.stackScrollerPadding,
+                mClockBottom + y + mDozingStackPadding,
+                mDarkAmount);
+
+        result.clockX = (int) interpolate(0, burnInPreventionOffsetX(), mDarkAmount);
+    }
+
+    private float getClockScale(int notificationPadding, int clockY, int startPadding) {
+        float scaleMultiplier = getNotificationAmountT() == 0 ? 6.0f : 5.0f;
+        float scaleEnd = clockY - mKeyguardStatusHeight * scaleMultiplier;
+        float distanceToScaleEnd = notificationPadding - scaleEnd;
+        float progress = distanceToScaleEnd / (startPadding - scaleEnd);
+        progress = Math.max(0.0f, Math.min(progress, 1.0f));
+        progress = mAccelerateInterpolator.getInterpolation(progress);
+        progress *= Math.pow(1 + mEmptyDragAmount / mDensity / 300, 0.3f);
+        return interpolate(progress, 1, mDarkAmount);
+    }
+
+    private int getClockNotificationsPadding() {
+        float t = getNotificationAmountT();
+        t = Math.min(t, 1.0f);
+        return (int) (t * mClockNotificationsMarginMin + (1 - t) * mClockNotificationsMarginMax);
+    }
+
+    private float getClockYFraction() {
+        float t = getNotificationAmountT();
+        t = Math.min(t, 1.0f);
+        return (1 - t) * mClockYFractionMax + t * mClockYFractionMin;
+    }
+
+    private int getClockY() {
+        // Dark: Align the bottom edge of the clock at one third:
+        // clockBottomEdge = result - mKeyguardStatusHeight / 2 + mClockBottom
+        float clockYDark = (0.33f * mHeight + (float) mKeyguardStatusHeight / 2 - mClockBottom)
+                + burnInPreventionOffsetY();
+        float clockYRegular = getClockYFraction() * mHeight;
+        return (int) interpolate(clockYRegular, clockYDark, mDarkAmount);
+    }
+
+    private float burnInPreventionOffsetY() {
+        return zigzag(System.currentTimeMillis() / MILLIS_PER_MINUTES,
+                mBurnInPreventionOffsetY * 2,
+                BURN_IN_PREVENTION_PERIOD_Y)
+                - mBurnInPreventionOffsetY;
+    }
+
+    private float burnInPreventionOffsetX() {
+        return zigzag(System.currentTimeMillis() / MILLIS_PER_MINUTES,
+                mBurnInPreventionOffsetX * 2,
+                BURN_IN_PREVENTION_PERIOD_X)
+                - mBurnInPreventionOffsetX;
+    }
+
+    /**
+     * Implements a continuous, piecewise linear, periodic zig-zag function
+     *
+     * Can be thought of as a linear approximation of abs(sin(x)))
+     *
+     * @param period period of the function, ie. zigzag(x + period) == zigzag(x)
+     * @param amplitude maximum value of the function
+     * @return a value between 0 and amplitude
+     */
+    private float zigzag(float x, float amplitude, float period) {
+        float xprime = (x % period) / (period / 2);
+        float interpolationAmount = (xprime <= 1) ? xprime : (2 - xprime);
+        return interpolate(0, amplitude, interpolationAmount);
+    }
+
+    private float getClockYExpansionAdjustment() {
+        float rubberbandFactor = getClockYExpansionRubberbandFactor();
+        float value = (rubberbandFactor * (mMaxPanelHeight - mExpandedHeight));
+        float t = value / mMaxPanelHeight;
+        float slowedDownValue = -sSlowDownInterpolator.getInterpolation(t) * SLOW_DOWN_FACTOR
+                * mMaxPanelHeight;
+        if (mNotificationCount == 0) {
+            return (-2*value + slowedDownValue)/3;
+        } else {
+            return slowedDownValue;
+        }
+    }
+
+    private float getClockYExpansionRubberbandFactor() {
+        float t = getNotificationAmountT();
+        t = Math.min(t, 1.0f);
+        t = (float) Math.pow(t, 0.3f);
+        return (1 - t) * CLOCK_RUBBERBAND_FACTOR_MAX + t * CLOCK_RUBBERBAND_FACTOR_MIN;
+    }
+
+    private float getTopPaddingAdjMultiplier() {
+        float t = getNotificationAmountT();
+        t = Math.min(t, 1.0f);
+        return (1 - t) * CLOCK_ADJ_TOP_PADDING_MULTIPLIER_MIN
+                + t * CLOCK_ADJ_TOP_PADDING_MULTIPLIER_MAX;
+    }
+
+    private float getClockAlpha(float scale) {
+        float fadeEnd = getNotificationAmountT() == 0.0f
+                ? CLOCK_SCALE_FADE_END_NO_NOTIFS
+                : CLOCK_SCALE_FADE_END;
+        float alpha = (scale - fadeEnd)
+                / (CLOCK_SCALE_FADE_START - fadeEnd);
+        return Math.max(0, Math.min(1, alpha));
+    }
+
+    /**
+     * @return a value from 0 to 1 depending on how many notification there are
+     */
+    private float getNotificationAmountT() {
+        return mNotificationCount
+                / (mMaxKeyguardNotifications + mMoreCardNotificationAmount);
+    }
+
+    public static class Result {
+
+        /**
+         * The y translation of the clock.
+         */
+        public int clockY;
+
+        /**
+         * The scale of the Clock
+         */
+        public float clockScale;
+
+        /**
+         * The alpha value of the clock.
+         */
+        public float clockAlpha;
+
+        /**
+         * The top padding of the stack scroller, in pixels.
+         */
+        public int stackScrollerPadding;
+
+        /**
+         * The top padding adjustment of the stack scroller, in pixels. This value is used to adjust
+         * the padding, but not the overall panel size.
+         */
+        public int stackScrollerPaddingAdjustment;
+
+        /** The x translation of the clock. */
+        public int clockX;
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java b/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java
new file mode 100644
index 0000000..c5c3fff
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * A view to show hints on Keyguard ("Swipe up to unlock", "Tap again to open").
+ */
+public class KeyguardIndicationTextView extends TextView {
+
+    public KeyguardIndicationTextView(Context context) {
+        super(context);
+    }
+
+    public KeyguardIndicationTextView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public KeyguardIndicationTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public KeyguardIndicationTextView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    /**
+     * Changes the text with an animation and makes sure a single indication is shown long enough.
+     *
+     * @param text The text to show.
+     */
+    public void switchIndication(CharSequence text) {
+
+        // TODO: Animation, make sure that we will show one indication long enough.
+        if (TextUtils.isEmpty(text)) {
+            setVisibility(View.INVISIBLE);
+        } else {
+            setVisibility(View.VISIBLE);
+            setText(text);
+        }
+    }
+
+    /**
+     * See {@link #switchIndication}.
+     */
+    public void switchIndication(int textResId) {
+        switchIndication(getResources().getText(textResId));
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/KeyguardPreviewContainer.java b/com/android/systemui/statusbar/phone/KeyguardPreviewContainer.java
new file mode 100644
index 0000000..076e5f1
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardPreviewContainer.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.WindowInsets;
+import android.widget.FrameLayout;
+
+/**
+ * A view group which contains the preview of phone/camera and draws a black bar at the bottom as
+ * the fake navigation bar.
+ */
+public class KeyguardPreviewContainer extends FrameLayout {
+
+    private Drawable mBlackBarDrawable = new Drawable() {
+        @Override
+        public void draw(Canvas canvas) {
+            canvas.save();
+            canvas.clipRect(0, getHeight() - getPaddingBottom(), getWidth(), getHeight());
+            canvas.drawColor(Color.BLACK);
+            canvas.restore();
+        }
+
+        @Override
+        public void setAlpha(int alpha) {
+            // noop
+        }
+
+        @Override
+        public void setColorFilter(ColorFilter colorFilter) {
+            // noop
+        }
+
+        @Override
+        public int getOpacity() {
+            return android.graphics.PixelFormat.OPAQUE;
+        }
+    };
+
+    public KeyguardPreviewContainer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setBackground(mBlackBarDrawable);
+    }
+
+    @Override
+    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+        setPadding(0, 0, 0, insets.getStableInsetBottom());
+        return super.onApplyWindowInsets(insets);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
new file mode 100644
index 0000000..a6691b1
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.annotation.ColorInt;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.settingslib.Utils;
+import com.android.systemui.BatteryMeterView;
+import com.android.systemui.Dependency;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.qs.QSPanel;
+import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager;
+import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
+import com.android.systemui.statusbar.policy.UserInfoController;
+import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener;
+import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+
+/**
+ * The header group on Keyguard.
+ */
+public class KeyguardStatusBarView extends RelativeLayout
+        implements BatteryStateChangeCallback, OnUserInfoChangedListener, ConfigurationListener {
+
+    private boolean mBatteryCharging;
+    private boolean mKeyguardUserSwitcherShowing;
+    private boolean mBatteryListening;
+
+    private TextView mCarrierLabel;
+    private View mSystemIconsSuperContainer;
+    private MultiUserSwitch mMultiUserSwitch;
+    private ImageView mMultiUserAvatar;
+    private BatteryMeterView mBatteryView;
+
+    private BatteryController mBatteryController;
+    private KeyguardUserSwitcher mKeyguardUserSwitcher;
+    private UserSwitcherController mUserSwitcherController;
+
+    private int mSystemIconsSwitcherHiddenExpandedMargin;
+    private int mSystemIconsBaseMargin;
+    private View mSystemIconsContainer;
+    private TintedIconManager mIconManager;
+
+    public KeyguardStatusBarView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mSystemIconsSuperContainer = findViewById(R.id.system_icons_super_container);
+        mSystemIconsContainer = findViewById(R.id.system_icons_container);
+        mMultiUserSwitch = (MultiUserSwitch) findViewById(R.id.multi_user_switch);
+        mMultiUserAvatar = (ImageView) findViewById(R.id.multi_user_avatar);
+        mCarrierLabel = (TextView) findViewById(R.id.keyguard_carrier_text);
+        mBatteryView = (BatteryMeterView) mSystemIconsContainer.findViewById(R.id.battery);
+
+        loadDimens();
+        updateUserSwitcher();
+        mBatteryController = Dependency.get(BatteryController.class);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+
+        MarginLayoutParams lp = (MarginLayoutParams) mMultiUserAvatar.getLayoutParams();
+        lp.width = lp.height = getResources().getDimensionPixelSize(
+                R.dimen.multi_user_avatar_keyguard_size);
+        mMultiUserAvatar.setLayoutParams(lp);
+
+        lp = (MarginLayoutParams) mMultiUserSwitch.getLayoutParams();
+        lp.width = getResources().getDimensionPixelSize(
+                R.dimen.multi_user_switch_width_keyguard);
+        lp.setMarginEnd(getResources().getDimensionPixelSize(
+                R.dimen.multi_user_switch_keyguard_margin));
+        mMultiUserSwitch.setLayoutParams(lp);
+
+        lp = (MarginLayoutParams) mSystemIconsSuperContainer.getLayoutParams();
+        lp.height = getResources().getDimensionPixelSize(
+                R.dimen.status_bar_header_height);
+        lp.setMarginStart(getResources().getDimensionPixelSize(
+                R.dimen.system_icons_super_container_margin_start));
+        mSystemIconsSuperContainer.setLayoutParams(lp);
+        mSystemIconsSuperContainer.setPaddingRelative(mSystemIconsSuperContainer.getPaddingStart(),
+                mSystemIconsSuperContainer.getPaddingTop(),
+                getResources().getDimensionPixelSize(R.dimen.system_icons_keyguard_padding_end),
+                mSystemIconsSuperContainer.getPaddingBottom());
+
+        lp = (MarginLayoutParams) mSystemIconsContainer.getLayoutParams();
+        lp.height = getResources().getDimensionPixelSize(
+                R.dimen.status_bar_height);
+        mSystemIconsContainer.setLayoutParams(lp);
+
+        // Respect font size setting.
+        mCarrierLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX,
+                getResources().getDimensionPixelSize(
+                        com.android.internal.R.dimen.text_size_small_material));
+        lp = (MarginLayoutParams) mCarrierLabel.getLayoutParams();
+        lp.setMarginStart(
+                getResources().getDimensionPixelSize(R.dimen.keyguard_carrier_text_margin));
+        mCarrierLabel.setLayoutParams(lp);
+
+        lp = (MarginLayoutParams) getLayoutParams();
+        lp.height =  getResources().getDimensionPixelSize(
+                R.dimen.status_bar_header_height_keyguard);
+        setLayoutParams(lp);
+    }
+
+    private void loadDimens() {
+        Resources res = getResources();
+        mSystemIconsSwitcherHiddenExpandedMargin = res.getDimensionPixelSize(
+                R.dimen.system_icons_switcher_hidden_expanded_margin);
+        mSystemIconsBaseMargin = res.getDimensionPixelSize(
+                R.dimen.system_icons_super_container_avatarless_margin_end);
+    }
+
+    private void updateVisibilities() {
+        if (mMultiUserSwitch.getParent() != this && !mKeyguardUserSwitcherShowing) {
+            if (mMultiUserSwitch.getParent() != null) {
+                getOverlay().remove(mMultiUserSwitch);
+            }
+            addView(mMultiUserSwitch, 0);
+        } else if (mMultiUserSwitch.getParent() == this && mKeyguardUserSwitcherShowing) {
+            removeView(mMultiUserSwitch);
+        }
+        if (mKeyguardUserSwitcher == null) {
+            // If we have no keyguard switcher, the screen width is under 600dp. In this case,
+            // we don't show the multi-user avatar unless there is more than 1 user on the device.
+            if (mUserSwitcherController != null
+                    && mUserSwitcherController.getSwitchableUserCount() > 1) {
+                mMultiUserSwitch.setVisibility(View.VISIBLE);
+            } else {
+                mMultiUserSwitch.setVisibility(View.GONE);
+            }
+        }
+        mBatteryView.setForceShowPercent(mBatteryCharging);
+    }
+
+    private void updateSystemIconsLayoutParams() {
+        RelativeLayout.LayoutParams lp =
+                (LayoutParams) mSystemIconsSuperContainer.getLayoutParams();
+        // If the avatar icon is gone, we need to have some end margin to display the system icons
+        // correctly.
+        int baseMarginEnd = mMultiUserSwitch.getVisibility() == View.GONE
+                ? mSystemIconsBaseMargin
+                : 0;
+        int marginEnd = mKeyguardUserSwitcherShowing ? mSystemIconsSwitcherHiddenExpandedMargin :
+                baseMarginEnd;
+        if (marginEnd != lp.getMarginEnd()) {
+            lp.setMarginEnd(marginEnd);
+            mSystemIconsSuperContainer.setLayoutParams(lp);
+        }
+    }
+
+    public void setListening(boolean listening) {
+        if (listening == mBatteryListening) {
+            return;
+        }
+        mBatteryListening = listening;
+        if (mBatteryListening) {
+            mBatteryController.addCallback(this);
+        } else {
+            mBatteryController.removeCallback(this);
+        }
+    }
+
+    private void updateUserSwitcher() {
+        boolean keyguardSwitcherAvailable = mKeyguardUserSwitcher != null;
+        mMultiUserSwitch.setClickable(keyguardSwitcherAvailable);
+        mMultiUserSwitch.setFocusable(keyguardSwitcherAvailable);
+        mMultiUserSwitch.setKeyguardMode(keyguardSwitcherAvailable);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        UserInfoController userInfoController = Dependency.get(UserInfoController.class);
+        userInfoController.addCallback(this);
+        mUserSwitcherController = Dependency.get(UserSwitcherController.class);
+        mMultiUserSwitch.setUserSwitcherController(mUserSwitcherController);
+        userInfoController.reloadUserInfo();
+        Dependency.get(ConfigurationController.class).addCallback(this);
+        mIconManager = new TintedIconManager(findViewById(R.id.statusIcons));
+        Dependency.get(StatusBarIconController.class).addIconGroup(mIconManager);
+        onOverlayChanged();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        Dependency.get(UserInfoController.class).removeCallback(this);
+        Dependency.get(StatusBarIconController.class).removeIconGroup(mIconManager);
+        Dependency.get(ConfigurationController.class).removeCallback(this);
+    }
+
+    @Override
+    public void onUserInfoChanged(String name, Drawable picture, String userAccount) {
+        mMultiUserAvatar.setImageDrawable(picture);
+    }
+
+    public void setQSPanel(QSPanel qsp) {
+        mMultiUserSwitch.setQsPanel(qsp);
+    }
+
+    @Override
+    public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
+        if (mBatteryCharging != charging) {
+            mBatteryCharging = charging;
+            updateVisibilities();
+        }
+    }
+
+    @Override
+    public void onPowerSaveChanged(boolean isPowerSave) {
+        // could not care less
+    }
+
+    public void setKeyguardUserSwitcher(KeyguardUserSwitcher keyguardUserSwitcher) {
+        mKeyguardUserSwitcher = keyguardUserSwitcher;
+        mMultiUserSwitch.setKeyguardUserSwitcher(keyguardUserSwitcher);
+        updateUserSwitcher();
+    }
+
+    public void setKeyguardUserSwitcherShowing(boolean showing, boolean animate) {
+        mKeyguardUserSwitcherShowing = showing;
+        if (animate) {
+            animateNextLayoutChange();
+        }
+        updateVisibilities();
+        updateSystemIconsLayoutParams();
+    }
+
+    private void animateNextLayoutChange() {
+        final int systemIconsCurrentX = mSystemIconsSuperContainer.getLeft();
+        final boolean userSwitcherVisible = mMultiUserSwitch.getParent() == this;
+        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+            @Override
+            public boolean onPreDraw() {
+                getViewTreeObserver().removeOnPreDrawListener(this);
+                boolean userSwitcherHiding = userSwitcherVisible
+                        && mMultiUserSwitch.getParent() != KeyguardStatusBarView.this;
+                mSystemIconsSuperContainer.setX(systemIconsCurrentX);
+                mSystemIconsSuperContainer.animate()
+                        .translationX(0)
+                        .setDuration(400)
+                        .setStartDelay(userSwitcherHiding ? 300 : 0)
+                        .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                        .start();
+                if (userSwitcherHiding) {
+                    getOverlay().add(mMultiUserSwitch);
+                    mMultiUserSwitch.animate()
+                            .alpha(0f)
+                            .setDuration(300)
+                            .setStartDelay(0)
+                            .setInterpolator(Interpolators.ALPHA_OUT)
+                            .withEndAction(new Runnable() {
+                                @Override
+                                public void run() {
+                                    mMultiUserSwitch.setAlpha(1f);
+                                    getOverlay().remove(mMultiUserSwitch);
+                                }
+                            })
+                            .start();
+
+                } else {
+                    mMultiUserSwitch.setAlpha(0f);
+                    mMultiUserSwitch.animate()
+                            .alpha(1f)
+                            .setDuration(300)
+                            .setStartDelay(200)
+                            .setInterpolator(Interpolators.ALPHA_IN);
+                }
+                return true;
+            }
+        });
+
+    }
+
+    @Override
+    public void setVisibility(int visibility) {
+        super.setVisibility(visibility);
+        if (visibility != View.VISIBLE) {
+            mSystemIconsSuperContainer.animate().cancel();
+            mSystemIconsSuperContainer.setTranslationX(0);
+            mMultiUserSwitch.animate().cancel();
+            mMultiUserSwitch.setAlpha(1f);
+        } else {
+            updateVisibilities();
+            updateSystemIconsLayoutParams();
+        }
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    public void onOverlayChanged() {
+        @ColorInt int textColor = Utils.getColorAttr(mContext, R.attr.wallpaperTextColor);
+        @ColorInt int iconColor = Utils.getDefaultColor(mContext, Color.luminance(textColor) < 0.5 ?
+                R.color.dark_mode_icon_color_single_tone :
+                R.color.light_mode_icon_color_single_tone);
+        float intensity = textColor == Color.WHITE ? 0 : 1;
+        mCarrierLabel.setTextColor(iconColor);
+        mBatteryView.setFillColor(iconColor);
+        mIconManager.setTint(iconColor);
+        Rect tintArea = new Rect(0, 0, 0, 0);
+
+        applyDarkness(R.id.signal_cluster, tintArea, intensity, iconColor);
+        applyDarkness(R.id.battery, tintArea, intensity, iconColor);
+        applyDarkness(R.id.clock, tintArea, intensity, iconColor);
+        // Reload user avatar
+        ((UserInfoControllerImpl) Dependency.get(UserInfoController.class))
+                .onDensityOrFontScaleChanged();
+    }
+
+    private void applyDarkness(int id, Rect tintArea, float intensity, int color) {
+        View v = findViewById(id);
+        if (v instanceof DarkReceiver) {
+            ((DarkReceiver) v).onDarkChanged(tintArea, intensity, color);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/LightBarController.java b/com/android/systemui/statusbar/phone/LightBarController.java
new file mode 100644
index 0000000..533771a
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/LightBarController.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2016 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.phone;
+
+import android.app.WallpaperColors;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.view.View;
+
+import com.android.internal.colorextraction.ColorExtractor.GradientColors;
+import com.android.systemui.Dependency;
+import com.android.systemui.Dumpable;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
+
+/**
+ * Controls how light status bar flag applies to the icons.
+ */
+public class LightBarController implements BatteryController.BatteryStateChangeCallback, Dumpable {
+
+    private static final float NAV_BAR_INVERSION_SCRIM_ALPHA_THRESHOLD = 0.1f;
+
+    private final DarkIconDispatcher mStatusBarIconController;
+    private final BatteryController mBatteryController;
+    private FingerprintUnlockController mFingerprintUnlockController;
+
+    private LightBarTransitionsController mNavigationBarController;
+    private int mSystemUiVisibility;
+    private int mFullscreenStackVisibility;
+    private int mDockedStackVisibility;
+    private boolean mFullscreenLight;
+    private boolean mDockedLight;
+    private int mLastStatusBarMode;
+    private int mLastNavigationBarMode;
+    private final Color mDarkModeColor;
+
+    /**
+     * Whether the navigation bar should be light factoring in already how much alpha the scrim has
+     */
+    private boolean mNavigationLight;
+
+    /**
+     * Whether the flags indicate that a light status bar is requested. This doesn't factor in the
+     * scrim alpha yet.
+     */
+    private boolean mHasLightNavigationBar;
+    private boolean mScrimAlphaBelowThreshold;
+    private boolean mInvertLightNavBarWithScrim;
+    private float mScrimAlpha;
+
+    private final Rect mLastFullscreenBounds = new Rect();
+    private final Rect mLastDockedBounds = new Rect();
+    private boolean mQsCustomizing;
+
+    public LightBarController(Context ctx) {
+        mDarkModeColor = Color.valueOf(ctx.getColor(R.color.dark_mode_icon_color_single_tone));
+        mStatusBarIconController = Dependency.get(DarkIconDispatcher.class);
+        mBatteryController = Dependency.get(BatteryController.class);
+        mBatteryController.addCallback(this);
+    }
+
+    public void setNavigationBar(LightBarTransitionsController navigationBar) {
+        mNavigationBarController = navigationBar;
+        updateNavigation();
+    }
+
+    public void setFingerprintUnlockController(
+            FingerprintUnlockController fingerprintUnlockController) {
+        mFingerprintUnlockController = fingerprintUnlockController;
+    }
+
+    public void onSystemUiVisibilityChanged(int fullscreenStackVis, int dockedStackVis,
+            int mask, Rect fullscreenStackBounds, Rect dockedStackBounds, boolean sbModeChanged,
+            int statusBarMode) {
+        int oldFullscreen = mFullscreenStackVisibility;
+        int newFullscreen = (oldFullscreen & ~mask) | (fullscreenStackVis & mask);
+        int diffFullscreen = newFullscreen ^ oldFullscreen;
+        int oldDocked = mDockedStackVisibility;
+        int newDocked = (oldDocked & ~mask) | (dockedStackVis & mask);
+        int diffDocked = newDocked ^ oldDocked;
+        if ((diffFullscreen & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0
+                || (diffDocked & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0
+                || sbModeChanged
+                || !mLastFullscreenBounds.equals(fullscreenStackBounds)
+                || !mLastDockedBounds.equals(dockedStackBounds)) {
+
+            mFullscreenLight = isLight(newFullscreen, statusBarMode,
+                    View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+            mDockedLight = isLight(newDocked, statusBarMode, View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+            updateStatus(fullscreenStackBounds, dockedStackBounds);
+        }
+
+        mFullscreenStackVisibility = newFullscreen;
+        mDockedStackVisibility = newDocked;
+        mLastStatusBarMode = statusBarMode;
+        mLastFullscreenBounds.set(fullscreenStackBounds);
+        mLastDockedBounds.set(dockedStackBounds);
+    }
+
+    public void onNavigationVisibilityChanged(int vis, int mask, boolean nbModeChanged,
+            int navigationBarMode) {
+        int oldVis = mSystemUiVisibility;
+        int newVis = (oldVis & ~mask) | (vis & mask);
+        int diffVis = newVis ^ oldVis;
+        if ((diffVis & View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0
+                || nbModeChanged) {
+            boolean last = mNavigationLight;
+            mHasLightNavigationBar = isLight(vis, navigationBarMode,
+                    View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
+            mNavigationLight = mHasLightNavigationBar
+                    && (mScrimAlphaBelowThreshold || !mInvertLightNavBarWithScrim)
+                    && !mQsCustomizing;
+            if (mNavigationLight != last) {
+                updateNavigation();
+            }
+        }
+        mSystemUiVisibility = newVis;
+        mLastNavigationBarMode = navigationBarMode;
+    }
+
+    private void reevaluate() {
+        onSystemUiVisibilityChanged(mFullscreenStackVisibility,
+                mDockedStackVisibility, 0 /* mask */, mLastFullscreenBounds, mLastDockedBounds,
+                true /* sbModeChange*/, mLastStatusBarMode);
+        onNavigationVisibilityChanged(mSystemUiVisibility, 0 /* mask */, true /* nbModeChanged */,
+                mLastNavigationBarMode);
+    }
+
+    public void setQsCustomizing(boolean customizing) {
+        if (mQsCustomizing == customizing) return;
+        mQsCustomizing = customizing;
+        reevaluate();
+    }
+
+    public void setScrimAlpha(float alpha) {
+        mScrimAlpha = alpha;
+        boolean belowThresholdBefore = mScrimAlphaBelowThreshold;
+        mScrimAlphaBelowThreshold = mScrimAlpha < NAV_BAR_INVERSION_SCRIM_ALPHA_THRESHOLD;
+        if (mHasLightNavigationBar && belowThresholdBefore != mScrimAlphaBelowThreshold) {
+            reevaluate();
+        }
+    }
+
+    public void setScrimColor(GradientColors colors) {
+        boolean invertLightNavBarWithScrimBefore = mInvertLightNavBarWithScrim;
+        mInvertLightNavBarWithScrim = !colors.supportsDarkText();
+        if (mHasLightNavigationBar
+                && invertLightNavBarWithScrimBefore != mInvertLightNavBarWithScrim) {
+            reevaluate();
+        }
+    }
+
+    private boolean isLight(int vis, int barMode, int flag) {
+        boolean isTransparentBar = (barMode == MODE_TRANSPARENT
+                || barMode == MODE_LIGHTS_OUT_TRANSPARENT);
+        boolean allowLight = isTransparentBar && !mBatteryController.isPowerSave();
+        boolean light = (vis & flag) != 0;
+        return allowLight && light;
+    }
+
+    private boolean animateChange() {
+        if (mFingerprintUnlockController == null) {
+            return false;
+        }
+        int unlockMode = mFingerprintUnlockController.getMode();
+        return unlockMode != FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING
+                && unlockMode != FingerprintUnlockController.MODE_WAKE_AND_UNLOCK;
+    }
+
+    private void updateStatus(Rect fullscreenStackBounds, Rect dockedStackBounds) {
+        boolean hasDockedStack = !dockedStackBounds.isEmpty();
+
+        // If both are light or fullscreen is light and there is no docked stack, all icons get
+        // dark.
+        if ((mFullscreenLight && mDockedLight) || (mFullscreenLight && !hasDockedStack)) {
+            mStatusBarIconController.setIconsDarkArea(null);
+            mStatusBarIconController.getTransitionsController().setIconsDark(true, animateChange());
+
+        }
+
+        // If no one is light or the fullscreen is not light and there is no docked stack,
+        // all icons become white.
+        else if ((!mFullscreenLight && !mDockedLight) || (!mFullscreenLight && !hasDockedStack)) {
+            mStatusBarIconController.getTransitionsController().setIconsDark(
+                    false, animateChange());
+        }
+
+        // Not the same for every stack, magic!
+        else {
+            Rect bounds = mFullscreenLight ? fullscreenStackBounds : dockedStackBounds;
+            if (bounds.isEmpty()) {
+                mStatusBarIconController.setIconsDarkArea(null);
+            } else {
+                mStatusBarIconController.setIconsDarkArea(bounds);
+            }
+            mStatusBarIconController.getTransitionsController().setIconsDark(true, animateChange());
+        }
+    }
+
+    private void updateNavigation() {
+        if (mNavigationBarController != null) {
+            mNavigationBarController.setIconsDark(
+                    mNavigationLight, animateChange());
+        }
+    }
+
+    @Override
+    public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
+
+    }
+
+    @Override
+    public void onPowerSaveChanged(boolean isPowerSave) {
+        reevaluate();
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("LightBarController: ");
+        pw.print(" mSystemUiVisibility=0x"); pw.print(
+                Integer.toHexString(mSystemUiVisibility));
+        pw.print(" mFullscreenStackVisibility=0x"); pw.print(
+                Integer.toHexString(mFullscreenStackVisibility));
+        pw.print(" mDockedStackVisibility=0x"); pw.println(
+                Integer.toHexString(mDockedStackVisibility));
+
+        pw.print(" mFullscreenLight="); pw.print(mFullscreenLight);
+        pw.print(" mDockedLight="); pw.println(mDockedLight);
+
+        pw.print(" mLastFullscreenBounds="); pw.print(mLastFullscreenBounds);
+        pw.print(" mLastDockedBounds="); pw.println(mLastDockedBounds);
+
+        pw.print(" mNavigationLight="); pw.print(mNavigationLight);
+        pw.print(" mHasLightNavigationBar="); pw.println(mHasLightNavigationBar);
+
+        pw.print(" mLastStatusBarMode="); pw.print(mLastStatusBarMode);
+        pw.print(" mLastNavigationBarMode="); pw.println(mLastNavigationBarMode);
+
+        pw.print(" mScrimAlpha="); pw.print(mScrimAlpha);
+        pw.print(" mScrimAlphaBelowThreshold="); pw.println(mScrimAlphaBelowThreshold);
+        pw.println();
+        pw.println(" StatusBarTransitionsController:");
+        mStatusBarIconController.getTransitionsController().dump(fd, pw, args);
+        pw.println();
+        pw.println(" NavigationBarTransitionsController:");
+        mNavigationBarController.dump(fd, pw, args);
+        pw.println();
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/LightBarTransitionsController.java b/com/android/systemui/statusbar/phone/LightBarTransitionsController.java
new file mode 100644
index 0000000..b0ac6ec
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/LightBarTransitionsController.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2016 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.phone;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.TimeUtils;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.Dumpable;
+import com.android.systemui.Interpolators;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.CommandQueue.Callbacks;
+import com.android.systemui.statusbar.policy.KeyguardMonitor;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * Class to control all aspects about light bar changes.
+ */
+public class LightBarTransitionsController implements Dumpable, Callbacks {
+
+    public static final long DEFAULT_TINT_ANIMATION_DURATION = 120;
+    private static final String EXTRA_DARK_INTENSITY = "dark_intensity";
+
+    private final Handler mHandler;
+    private final DarkIntensityApplier mApplier;
+    private final KeyguardMonitor mKeyguardMonitor;
+
+    private boolean mTransitionDeferring;
+    private long mTransitionDeferringStartTime;
+    private long mTransitionDeferringDuration;
+    private boolean mTransitionPending;
+    private boolean mTintChangePending;
+    private float mPendingDarkIntensity;
+    private ValueAnimator mTintAnimator;
+    private float mDarkIntensity;
+    private float mNextDarkIntensity;
+    private final Runnable mTransitionDeferringDoneRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mTransitionDeferring = false;
+        }
+    };
+
+    public LightBarTransitionsController(Context context, DarkIntensityApplier applier) {
+        mApplier = applier;
+        mHandler = new Handler();
+        mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
+        SysUiServiceProvider.getComponent(context, CommandQueue.class)
+                .addCallbacks(this);
+    }
+
+    public void destroy(Context context) {
+        SysUiServiceProvider.getComponent(context, CommandQueue.class)
+                .removeCallbacks(this);
+    }
+
+    public void saveState(Bundle outState) {
+        float intensity = mTintAnimator != null && mTintAnimator.isRunning()
+                ?  mNextDarkIntensity : mDarkIntensity;
+        outState.putFloat(EXTRA_DARK_INTENSITY, intensity);
+    }
+
+    public void restoreState(Bundle savedInstanceState) {
+        setIconTintInternal(savedInstanceState.getFloat(EXTRA_DARK_INTENSITY, 0));
+    }
+
+    @Override
+    public void appTransitionPending(boolean forced) {
+        if (mKeyguardMonitor.isKeyguardGoingAway() && !forced) {
+            return;
+        }
+        mTransitionPending = true;
+    }
+
+    @Override
+    public void appTransitionCancelled() {
+        if (mTransitionPending && mTintChangePending) {
+            mTintChangePending = false;
+            animateIconTint(mPendingDarkIntensity, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION);
+        }
+        mTransitionPending = false;
+    }
+
+    @Override
+    public void appTransitionStarting(long startTime, long duration, boolean forced) {
+        if (mKeyguardMonitor.isKeyguardGoingAway() && !forced) {
+            return;
+        }
+        if (mTransitionPending && mTintChangePending) {
+            mTintChangePending = false;
+            animateIconTint(mPendingDarkIntensity,
+                    Math.max(0, startTime - SystemClock.uptimeMillis()),
+                    duration);
+
+        } else if (mTransitionPending) {
+
+            // If we don't have a pending tint change yet, the change might come in the future until
+            // startTime is reached.
+            mTransitionDeferring = true;
+            mTransitionDeferringStartTime = startTime;
+            mTransitionDeferringDuration = duration;
+            mHandler.removeCallbacks(mTransitionDeferringDoneRunnable);
+            mHandler.postAtTime(mTransitionDeferringDoneRunnable, startTime);
+        }
+        mTransitionPending = false;
+    }
+
+    public void setIconsDark(boolean dark, boolean animate) {
+        if (!animate) {
+            setIconTintInternal(dark ? 1.0f : 0.0f);
+            mNextDarkIntensity = dark ? 1.0f : 0.0f;
+        } else if (mTransitionPending) {
+            deferIconTintChange(dark ? 1.0f : 0.0f);
+        } else if (mTransitionDeferring) {
+            animateIconTint(dark ? 1.0f : 0.0f,
+                    Math.max(0, mTransitionDeferringStartTime - SystemClock.uptimeMillis()),
+                    mTransitionDeferringDuration);
+        } else {
+            animateIconTint(dark ? 1.0f : 0.0f, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION);
+        }
+    }
+
+    public float getCurrentDarkIntensity() {
+        return mDarkIntensity;
+    }
+
+    private void deferIconTintChange(float darkIntensity) {
+        if (mTintChangePending && darkIntensity == mPendingDarkIntensity) {
+            return;
+        }
+        mTintChangePending = true;
+        mPendingDarkIntensity = darkIntensity;
+    }
+
+    private void animateIconTint(float targetDarkIntensity, long delay,
+            long duration) {
+        if (mNextDarkIntensity == targetDarkIntensity) {
+            return;
+        }
+        if (mTintAnimator != null) {
+            mTintAnimator.cancel();
+        }
+        mNextDarkIntensity = targetDarkIntensity;
+        mTintAnimator = ValueAnimator.ofFloat(mDarkIntensity, targetDarkIntensity);
+        mTintAnimator.addUpdateListener(
+                animation -> setIconTintInternal((Float) animation.getAnimatedValue()));
+        mTintAnimator.setDuration(duration);
+        mTintAnimator.setStartDelay(delay);
+        mTintAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+        mTintAnimator.start();
+    }
+
+    private void setIconTintInternal(float darkIntensity) {
+        mDarkIntensity = darkIntensity;
+        mApplier.applyDarkIntensity(darkIntensity);
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.print("  mTransitionDeferring="); pw.print(mTransitionDeferring);
+        if (mTransitionDeferring) {
+            pw.println();
+            pw.print("   mTransitionDeferringStartTime=");
+            pw.println(TimeUtils.formatUptime(mTransitionDeferringStartTime));
+
+            pw.print("   mTransitionDeferringDuration=");
+            TimeUtils.formatDuration(mTransitionDeferringDuration, pw);
+            pw.println();
+        }
+        pw.print("  mTransitionPending="); pw.print(mTransitionPending);
+        pw.print(" mTintChangePending="); pw.println(mTintChangePending);
+
+        pw.print("  mPendingDarkIntensity="); pw.print(mPendingDarkIntensity);
+        pw.print(" mDarkIntensity="); pw.print(mDarkIntensity);
+        pw.print(" mNextDarkIntensity="); pw.println(mNextDarkIntensity);
+    }
+
+    /**
+     * Interface to apply a specific dark intensity.
+     */
+    public interface DarkIntensityApplier {
+        void applyDarkIntensity(float darkIntensity);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/LockIcon.java b/com/android/systemui/statusbar/phone/LockIcon.java
new file mode 100644
index 0000000..5c9446c
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/LockIcon.java
@@ -0,0 +1,335 @@
+/*
+ * 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.phone;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.KeyguardAffordanceView;
+import com.android.systemui.statusbar.policy.AccessibilityController;
+import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener;
+
+/**
+ * Manages the different states and animations of the unlock icon.
+ */
+public class LockIcon extends KeyguardAffordanceView implements OnUserInfoChangedListener {
+
+    private static final int FP_DRAW_OFF_TIMEOUT = 800;
+
+    private static final int STATE_LOCKED = 0;
+    private static final int STATE_LOCK_OPEN = 1;
+    private static final int STATE_FACE_UNLOCK = 2;
+    private static final int STATE_FINGERPRINT = 3;
+    private static final int STATE_FINGERPRINT_ERROR = 4;
+
+    private int mLastState = 0;
+    private boolean mLastDeviceInteractive;
+    private boolean mTransientFpError;
+    private boolean mDeviceInteractive;
+    private boolean mScreenOn;
+    private boolean mLastScreenOn;
+    private Drawable mUserAvatarIcon;
+    private TrustDrawable mTrustDrawable;
+    private final UnlockMethodCache mUnlockMethodCache;
+    private AccessibilityController mAccessibilityController;
+    private boolean mHasFingerPrintIcon;
+    private int mDensity;
+
+    private final Runnable mDrawOffTimeout = () -> update(true /* forceUpdate */);
+
+    public LockIcon(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mTrustDrawable = new TrustDrawable(context);
+        setBackground(mTrustDrawable);
+        mUnlockMethodCache = UnlockMethodCache.getInstance(context);
+    }
+
+    @Override
+    protected void onVisibilityChanged(View changedView, int visibility) {
+        super.onVisibilityChanged(changedView, visibility);
+        if (isShown()) {
+            mTrustDrawable.start();
+        } else {
+            mTrustDrawable.stop();
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mTrustDrawable.stop();
+    }
+
+    @Override
+    public void onUserInfoChanged(String name, Drawable picture, String userAccount) {
+        mUserAvatarIcon = picture;
+        update();
+    }
+
+    public void setTransientFpError(boolean transientFpError) {
+        mTransientFpError = transientFpError;
+        update();
+    }
+
+    public void setDeviceInteractive(boolean deviceInteractive) {
+        mDeviceInteractive = deviceInteractive;
+        update();
+    }
+
+    public void setScreenOn(boolean screenOn) {
+        mScreenOn = screenOn;
+        update();
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        final int density = newConfig.densityDpi;
+        if (density != mDensity) {
+            mDensity = density;
+            mTrustDrawable.stop();
+            mTrustDrawable = new TrustDrawable(getContext());
+            setBackground(mTrustDrawable);
+            update();
+        }
+    }
+
+    public void update() {
+        update(false /* force */);
+    }
+
+    public void update(boolean force) {
+        boolean visible = isShown()
+                && KeyguardUpdateMonitor.getInstance(mContext).isDeviceInteractive();
+        if (visible) {
+            mTrustDrawable.start();
+        } else {
+            mTrustDrawable.stop();
+        }
+        int state = getState();
+        boolean anyFingerprintIcon = state == STATE_FINGERPRINT || state == STATE_FINGERPRINT_ERROR;
+        boolean useAdditionalPadding = anyFingerprintIcon;
+        boolean trustHidden = anyFingerprintIcon;
+        if (state != mLastState || mDeviceInteractive != mLastDeviceInteractive
+                || mScreenOn != mLastScreenOn || force) {
+            int iconAnimRes =
+                getAnimationResForTransition(mLastState, state, mLastDeviceInteractive,
+                    mDeviceInteractive, mLastScreenOn, mScreenOn);
+            boolean isAnim = iconAnimRes != -1;
+            if (iconAnimRes == R.drawable.lockscreen_fingerprint_draw_off_animation) {
+                anyFingerprintIcon = true;
+                useAdditionalPadding = true;
+                trustHidden = true;
+            } else if (iconAnimRes == R.drawable.trusted_state_to_error_animation) {
+                anyFingerprintIcon = true;
+                useAdditionalPadding = false;
+                trustHidden = true;
+            } else if (iconAnimRes == R.drawable.error_to_trustedstate_animation) {
+                anyFingerprintIcon = true;
+                useAdditionalPadding = false;
+                trustHidden = false;
+            }
+
+            Drawable icon;
+            if (isAnim) {
+                // Load the animation resource.
+                icon = mContext.getDrawable(iconAnimRes);
+            } else {
+                // Load the static icon resource based on the current state.
+                icon = getIconForState(state, mScreenOn, mDeviceInteractive);
+            }
+
+            final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable
+                    ? (AnimatedVectorDrawable) icon
+                    : null;
+            int iconHeight = getResources().getDimensionPixelSize(
+                    R.dimen.keyguard_affordance_icon_height);
+            int iconWidth = getResources().getDimensionPixelSize(
+                    R.dimen.keyguard_affordance_icon_width);
+            if (!anyFingerprintIcon && (icon.getIntrinsicHeight() != iconHeight
+                    || icon.getIntrinsicWidth() != iconWidth)) {
+                icon = new IntrinsicSizeDrawable(icon, iconWidth, iconHeight);
+            }
+            setPaddingRelative(0, 0, 0, useAdditionalPadding
+                    ? getResources().getDimensionPixelSize(
+                    R.dimen.fingerprint_icon_additional_padding)
+                    : 0);
+            setRestingAlpha(
+                    anyFingerprintIcon ? 1f : KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT);
+            setImageDrawable(icon, false);
+            mHasFingerPrintIcon = anyFingerprintIcon;
+            if (animation != null && isAnim) {
+                animation.forceAnimationOnUI();
+                animation.start();
+            }
+
+            if (iconAnimRes == R.drawable.lockscreen_fingerprint_draw_off_animation) {
+                removeCallbacks(mDrawOffTimeout);
+                postDelayed(mDrawOffTimeout, FP_DRAW_OFF_TIMEOUT);
+            } else {
+                removeCallbacks(mDrawOffTimeout);
+            }
+
+            mLastState = state;
+            mLastDeviceInteractive = mDeviceInteractive;
+            mLastScreenOn = mScreenOn;
+        }
+
+        // Hide trust circle when fingerprint is running.
+        boolean trustManaged = mUnlockMethodCache.isTrustManaged() && !trustHidden;
+        mTrustDrawable.setTrustManaged(trustManaged);
+        updateClickability();
+    }
+
+    private void updateClickability() {
+        if (mAccessibilityController == null) {
+            return;
+        }
+        boolean clickToUnlock = mAccessibilityController.isAccessibilityEnabled();
+        boolean clickToForceLock = mUnlockMethodCache.isTrustManaged()
+                && !clickToUnlock;
+        boolean longClickToForceLock = mUnlockMethodCache.isTrustManaged()
+                && !clickToForceLock;
+        setClickable(clickToForceLock || clickToUnlock);
+        setLongClickable(longClickToForceLock);
+        setFocusable(mAccessibilityController.isAccessibilityEnabled());
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        if (mHasFingerPrintIcon) {
+            AccessibilityNodeInfo.AccessibilityAction unlock
+                    = new AccessibilityNodeInfo.AccessibilityAction(
+                    AccessibilityNodeInfo.ACTION_CLICK,
+                    getContext().getString(R.string.accessibility_unlock_without_fingerprint));
+            info.addAction(unlock);
+            info.setHintText(getContext().getString(
+                    R.string.accessibility_waiting_for_fingerprint));
+        }
+    }
+
+    public void setAccessibilityController(AccessibilityController accessibilityController) {
+        mAccessibilityController = accessibilityController;
+    }
+
+    private Drawable getIconForState(int state, boolean screenOn, boolean deviceInteractive) {
+        int iconRes;
+        switch (state) {
+            case STATE_LOCKED:
+                iconRes = R.drawable.ic_lock_24dp;
+                break;
+            case STATE_LOCK_OPEN:
+                if (mUnlockMethodCache.isTrustManaged() && mUnlockMethodCache.isTrusted()
+                    && mUserAvatarIcon != null) {
+                    return mUserAvatarIcon;
+                } else {
+                    iconRes = R.drawable.ic_lock_open_24dp;
+                }
+                break;
+            case STATE_FACE_UNLOCK:
+                iconRes = com.android.internal.R.drawable.ic_account_circle;
+                break;
+            case STATE_FINGERPRINT:
+                // If screen is off and device asleep, use the draw on animation so the first frame
+                // gets drawn.
+                iconRes = screenOn && deviceInteractive
+                        ? R.drawable.ic_fingerprint
+                        : R.drawable.lockscreen_fingerprint_draw_on_animation;
+                break;
+            case STATE_FINGERPRINT_ERROR:
+                iconRes = R.drawable.ic_fingerprint_error;
+                break;
+            default:
+                throw new IllegalArgumentException();
+        }
+
+        return mContext.getDrawable(iconRes);
+    }
+
+    private int getAnimationResForTransition(int oldState, int newState,
+            boolean oldDeviceInteractive, boolean deviceInteractive,
+            boolean oldScreenOn, boolean screenOn) {
+        if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_ERROR) {
+            return R.drawable.lockscreen_fingerprint_fp_to_error_state_animation;
+        } else if (oldState == STATE_LOCK_OPEN && newState == STATE_FINGERPRINT_ERROR) {
+            return R.drawable.trusted_state_to_error_animation;
+        } else if (oldState == STATE_FINGERPRINT_ERROR && newState == STATE_LOCK_OPEN) {
+            return R.drawable.error_to_trustedstate_animation;
+        } else if (oldState == STATE_FINGERPRINT_ERROR && newState == STATE_FINGERPRINT) {
+            return R.drawable.lockscreen_fingerprint_error_state_to_fp_animation;
+        } else if (oldState == STATE_FINGERPRINT && newState == STATE_LOCK_OPEN
+                && !mUnlockMethodCache.isTrusted()) {
+            return R.drawable.lockscreen_fingerprint_draw_off_animation;
+        } else if (newState == STATE_FINGERPRINT && (!oldScreenOn && screenOn && deviceInteractive
+                || screenOn && !oldDeviceInteractive && deviceInteractive)) {
+            return R.drawable.lockscreen_fingerprint_draw_on_animation;
+        } else {
+            return -1;
+        }
+    }
+
+    private int getState() {
+        KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+        boolean fingerprintRunning = updateMonitor.isFingerprintDetectionRunning();
+        boolean unlockingAllowed = updateMonitor.isUnlockingWithFingerprintAllowed();
+        if (mTransientFpError) {
+            return STATE_FINGERPRINT_ERROR;
+        } else if (mUnlockMethodCache.canSkipBouncer()) {
+            return STATE_LOCK_OPEN;
+        } else if (mUnlockMethodCache.isFaceUnlockRunning()) {
+            return STATE_FACE_UNLOCK;
+        } else if (fingerprintRunning && unlockingAllowed) {
+            return STATE_FINGERPRINT;
+        } else {
+            return STATE_LOCKED;
+        }
+    }
+
+    /**
+     * A wrapper around another Drawable that overrides the intrinsic size.
+     */
+    private static class IntrinsicSizeDrawable extends InsetDrawable {
+
+        private final int mIntrinsicWidth;
+        private final int mIntrinsicHeight;
+
+        public IntrinsicSizeDrawable(Drawable drawable, int intrinsicWidth, int intrinsicHeight) {
+            super(drawable, 0);
+            mIntrinsicWidth = intrinsicWidth;
+            mIntrinsicHeight = intrinsicHeight;
+        }
+
+        @Override
+        public int getIntrinsicWidth() {
+            return mIntrinsicWidth;
+        }
+
+        @Override
+        public int getIntrinsicHeight() {
+            return mIntrinsicHeight;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/LockscreenGestureLogger.java b/com/android/systemui/statusbar/phone/LockscreenGestureLogger.java
new file mode 100644
index 0000000..4d99a46
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/LockscreenGestureLogger.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 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.phone;
+
+import android.metrics.LogMaker;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.Dependency;
+import com.android.systemui.EventLogConstants;
+import com.android.systemui.EventLogTags;
+
+/**
+ * Wrapper that emits both new- and old-style gesture logs.
+ * TODO: delete this once the old logs are no longer needed.
+ */
+public class LockscreenGestureLogger {
+    private ArrayMap<Integer, Integer> mLegacyMap;
+    private LogMaker mLogMaker = new LogMaker(MetricsEvent.VIEW_UNKNOWN)
+            .setType(MetricsEvent.TYPE_ACTION);
+    private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
+
+    public LockscreenGestureLogger() {
+        mLegacyMap = new ArrayMap<>(EventLogConstants.METRICS_GESTURE_TYPE_MAP.length);
+        for (int i = 0; i < EventLogConstants.METRICS_GESTURE_TYPE_MAP.length ; i++) {
+            mLegacyMap.put(EventLogConstants.METRICS_GESTURE_TYPE_MAP[i], i);
+        }
+    }
+
+    public void write(int gesture, int length, int velocity) {
+        mMetricsLogger.write(mLogMaker.setCategory(gesture)
+                .setType(MetricsEvent.TYPE_ACTION)
+                .addTaggedData(MetricsEvent.FIELD_GESTURE_LENGTH, length)
+                .addTaggedData(MetricsEvent.FIELD_GESTURE_VELOCITY, velocity));
+        // also write old-style logs for backward-0compatibility
+        EventLogTags.writeSysuiLockscreenGesture(safeLookup(gesture), length, velocity);
+    }
+
+    private int safeLookup(int gesture) {
+        Integer value = mLegacyMap.get(gesture);
+        if (value == null) {
+            return MetricsEvent.VIEW_UNKNOWN;
+        }
+        return value;
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/LockscreenWallpaper.java b/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
new file mode 100644
index 0000000..87f5ca7
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2016 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.phone;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.IWallpaperManager;
+import android.app.IWallpaperManagerCallback;
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+import android.graphics.Xfermode;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.DrawableWrapper;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.app.WallpaperColors;
+import android.util.Log;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
+
+import libcore.io.IoUtils;
+
+import java.util.Objects;
+
+/**
+ * Manages the lockscreen wallpaper.
+ */
+public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implements Runnable {
+
+    private static final String TAG = "LockscreenWallpaper";
+
+    private final StatusBar mBar;
+    private final WallpaperManager mWallpaperManager;
+    private final Handler mH;
+    private final KeyguardUpdateMonitor mUpdateMonitor;
+
+    private boolean mCached;
+    private Bitmap mCache;
+    private int mCurrentUserId;
+    // The user selected in the UI, or null if no user is selected or UI doesn't support selecting
+    // users.
+    private UserHandle mSelectedUser;
+    private AsyncTask<Void, Void, LoaderResult> mLoader;
+
+    public LockscreenWallpaper(Context ctx, StatusBar bar, Handler h) {
+        mBar = bar;
+        mH = h;
+        mWallpaperManager = (WallpaperManager) ctx.getSystemService(Context.WALLPAPER_SERVICE);
+        mCurrentUserId = ActivityManager.getCurrentUser();
+        mUpdateMonitor = KeyguardUpdateMonitor.getInstance(ctx);
+
+        IWallpaperManager service = IWallpaperManager.Stub.asInterface(
+                ServiceManager.getService(Context.WALLPAPER_SERVICE));
+        try {
+            service.setLockWallpaperCallback(this);
+        } catch (RemoteException e) {
+            Log.e(TAG, "System dead?" + e);
+        }
+    }
+
+    public Bitmap getBitmap() {
+        if (mCached) {
+            return mCache;
+        }
+        if (!mWallpaperManager.isWallpaperSupported()) {
+            mCached = true;
+            mCache = null;
+            return null;
+        }
+
+        LoaderResult result = loadBitmap(mCurrentUserId, mSelectedUser);
+        if (result.success) {
+            mCached = true;
+            mUpdateMonitor.setHasLockscreenWallpaper(result.bitmap != null);
+            mCache = result.bitmap;
+        }
+        return mCache;
+    }
+
+    public LoaderResult loadBitmap(int currentUserId, UserHandle selectedUser) {
+        // May be called on any thread - only use thread safe operations.
+
+        // Prefer the selected user (when specified) over the current user for the FLAG_SET_LOCK
+        // wallpaper.
+        final int lockWallpaperUserId =
+                selectedUser != null ? selectedUser.getIdentifier() : currentUserId;
+        ParcelFileDescriptor fd = mWallpaperManager.getWallpaperFile(
+                WallpaperManager.FLAG_LOCK, lockWallpaperUserId);
+
+        if (fd != null) {
+            try {
+                BitmapFactory.Options options = new BitmapFactory.Options();
+                return LoaderResult.success(BitmapFactory.decodeFileDescriptor(
+                        fd.getFileDescriptor(), null, options));
+            } catch (OutOfMemoryError e) {
+                Log.w(TAG, "Can't decode file", e);
+                return LoaderResult.fail();
+            } finally {
+                IoUtils.closeQuietly(fd);
+            }
+        } else {
+            if (selectedUser != null) {
+                // Show the selected user's static wallpaper.
+                return LoaderResult.success(
+                        mWallpaperManager.getBitmapAsUser(selectedUser.getIdentifier()));
+
+            } else {
+                // When there is no selected user, show the system wallpaper
+                return LoaderResult.success(null);
+            }
+        }
+    }
+
+    public void setCurrentUser(int user) {
+        if (user != mCurrentUserId) {
+            if (mSelectedUser == null || user != mSelectedUser.getIdentifier()) {
+                mCached = false;
+            }
+            mCurrentUserId = user;
+        }
+    }
+
+    public void setSelectedUser(UserHandle selectedUser) {
+        if (Objects.equals(selectedUser, mSelectedUser)) {
+            return;
+        }
+        mSelectedUser = selectedUser;
+        postUpdateWallpaper();
+    }
+
+    @Override
+    public void onWallpaperChanged() {
+        // Called on Binder thread.
+        postUpdateWallpaper();
+    }
+
+    @Override
+    public void onWallpaperColorsChanged(WallpaperColors colors, int which, int userId) {
+
+    }
+
+    private void postUpdateWallpaper() {
+        mH.removeCallbacks(this);
+        mH.post(this);
+    }
+
+    @Override
+    public void run() {
+        // Called in response to onWallpaperChanged on the main thread.
+
+        if (mLoader != null) {
+            mLoader.cancel(false /* interrupt */);
+        }
+
+        final int currentUser = mCurrentUserId;
+        final UserHandle selectedUser = mSelectedUser;
+        mLoader = new AsyncTask<Void, Void, LoaderResult>() {
+            @Override
+            protected LoaderResult doInBackground(Void... params) {
+                return loadBitmap(currentUser, selectedUser);
+            }
+
+            @Override
+            protected void onPostExecute(LoaderResult result) {
+                super.onPostExecute(result);
+                if (isCancelled()) {
+                    return;
+                }
+                if (result.success) {
+                    mCached = true;
+                    mCache = result.bitmap;
+                    mUpdateMonitor.setHasLockscreenWallpaper(result.bitmap != null);
+                    mBar.updateMediaMetaData(
+                            true /* metaDataChanged */, true /* allowEnterAnimation */);
+                }
+                mLoader = null;
+            }
+        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+    }
+
+    private static class LoaderResult {
+        public final boolean success;
+        public final Bitmap bitmap;
+
+        LoaderResult(boolean success, Bitmap bitmap) {
+            this.success = success;
+            this.bitmap = bitmap;
+        }
+
+        static LoaderResult success(Bitmap b) {
+            return new LoaderResult(true, b);
+        }
+
+        static LoaderResult fail() {
+            return new LoaderResult(false, null);
+        }
+    }
+
+    /**
+     * Drawable that aligns left horizontally and center vertically (like ImageWallpaper).
+     */
+    public static class WallpaperDrawable extends DrawableWrapper {
+
+        private final ConstantState mState;
+        private final Rect mTmpRect = new Rect();
+
+        public WallpaperDrawable(Resources r, Bitmap b) {
+            this(r, new ConstantState(b));
+        }
+
+        private WallpaperDrawable(Resources r, ConstantState state) {
+            super(new BitmapDrawable(r, state.mBackground));
+            mState = state;
+        }
+
+        @Override
+        public void setXfermode(@Nullable Xfermode mode) {
+            // DrawableWrapper does not call this for us.
+            getDrawable().setXfermode(mode);
+        }
+
+        @Override
+        public int getIntrinsicWidth() {
+            return -1;
+        }
+
+        @Override
+        public int getIntrinsicHeight() {
+            return -1;
+        }
+
+        @Override
+        protected void onBoundsChange(Rect bounds) {
+            int vwidth = getBounds().width();
+            int vheight = getBounds().height();
+            int dwidth = mState.mBackground.getWidth();
+            int dheight = mState.mBackground.getHeight();
+            float scale;
+            float dx = 0, dy = 0;
+
+            if (dwidth * vheight > vwidth * dheight) {
+                scale = (float) vheight / (float) dheight;
+            } else {
+                scale = (float) vwidth / (float) dwidth;
+            }
+
+            if (scale <= 1f) {
+                scale = 1f;
+            }
+            dy = (vheight - dheight * scale) * 0.5f;
+
+            mTmpRect.set(
+                    bounds.left,
+                    bounds.top + Math.round(dy),
+                    bounds.left + Math.round(dwidth * scale),
+                    bounds.top + Math.round(dheight * scale + dy));
+
+            super.onBoundsChange(mTmpRect);
+        }
+
+        @Override
+        public ConstantState getConstantState() {
+            return mState;
+        }
+
+        static class ConstantState extends Drawable.ConstantState {
+
+            private final Bitmap mBackground;
+
+            ConstantState(Bitmap background) {
+                mBackground = background;
+            }
+
+            @Override
+            public Drawable newDrawable() {
+                return newDrawable(null);
+            }
+
+            @Override
+            public Drawable newDrawable(@Nullable Resources res) {
+                return new WallpaperDrawable(res, this);
+            }
+
+            @Override
+            public int getChangingConfigurations() {
+                // DrawableWrapper already handles this for us.
+                return 0;
+            }
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/ManagedProfileController.java b/com/android/systemui/statusbar/phone/ManagedProfileController.java
new file mode 100644
index 0000000..4969a1c
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/ManagedProfileController.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 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.phone;
+
+import com.android.systemui.statusbar.phone.ManagedProfileController.Callback;
+import com.android.systemui.statusbar.policy.CallbackController;
+
+public interface ManagedProfileController extends CallbackController<Callback> {
+
+    void setWorkModeEnabled(boolean enabled);
+
+    boolean hasActiveProfile();
+
+    boolean isWorkModeEnabled();
+
+    public interface Callback {
+        void onManagedProfileChanged();
+        void onManagedProfileRemoved();
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java b/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
new file mode 100644
index 0000000..316bd5b
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2016 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.phone;
+
+import android.app.ActivityManager;
+import android.app.StatusBarManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.UserInfo;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+public class ManagedProfileControllerImpl implements ManagedProfileController {
+
+    private final List<Callback> mCallbacks = new ArrayList<>();
+
+    private final Context mContext;
+    private final UserManager mUserManager;
+    private final LinkedList<UserInfo> mProfiles;
+    private boolean mListening;
+    private int mCurrentUser;
+
+    public ManagedProfileControllerImpl(Context context) {
+        mContext = context;
+        mUserManager = UserManager.get(mContext);
+        mProfiles = new LinkedList<UserInfo>();
+    }
+
+    public void addCallback(Callback callback) {
+        mCallbacks.add(callback);
+        if (mCallbacks.size() == 1) {
+            setListening(true);
+        }
+        callback.onManagedProfileChanged();
+    }
+
+    public void removeCallback(Callback callback) {
+        if (mCallbacks.remove(callback) && mCallbacks.size() == 0) {
+            setListening(false);
+        }
+    }
+
+    public void setWorkModeEnabled(boolean enableWorkMode) {
+        synchronized (mProfiles) {
+            for (UserInfo ui : mProfiles) {
+                if (enableWorkMode) {
+                    if (!mUserManager.trySetQuietModeDisabled(ui.id, null)) {
+                        StatusBarManager statusBarManager = (StatusBarManager) mContext
+                                .getSystemService(android.app.Service.STATUS_BAR_SERVICE);
+                        statusBarManager.collapsePanels();
+                    }
+                } else {
+                    mUserManager.setQuietModeEnabled(ui.id, true);
+                }
+            }
+        }
+    }
+
+    private void reloadManagedProfiles() {
+        synchronized (mProfiles) {
+            boolean hadProfile = mProfiles.size() > 0;
+            int user = ActivityManager.getCurrentUser();
+            mProfiles.clear();
+
+            for (UserInfo ui : mUserManager.getEnabledProfiles(user)) {
+                if (ui.isManagedProfile()) {
+                    mProfiles.add(ui);
+                }
+            }
+            if (mProfiles.size() == 0 && hadProfile && (user == mCurrentUser)) {
+                for (Callback callback : mCallbacks) {
+                    callback.onManagedProfileRemoved();
+                }
+            }
+            mCurrentUser = user;
+        }
+    }
+
+    public boolean hasActiveProfile() {
+        if (!mListening) reloadManagedProfiles();
+        synchronized (mProfiles) {
+            return mProfiles.size() > 0;
+        }
+    }
+
+    public boolean isWorkModeEnabled() {
+        if (!mListening) reloadManagedProfiles();
+        synchronized (mProfiles) {
+            for (UserInfo ui : mProfiles) {
+                if (ui.isQuietModeEnabled()) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    private void setListening(boolean listening) {
+        mListening = listening;
+        if (listening) {
+            reloadManagedProfiles();
+
+            final IntentFilter filter = new IntentFilter();
+            filter.addAction(Intent.ACTION_USER_SWITCHED);
+            filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
+            filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
+            filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
+            filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
+            mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, null);
+        } else {
+            mContext.unregisterReceiver(mReceiver);
+        }
+    }
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            reloadManagedProfiles();
+            for (Callback callback : mCallbacks) {
+                callback.onManagedProfileChanged();
+            }
+        }
+    };
+}
diff --git a/com/android/systemui/statusbar/phone/MultiUserSwitch.java b/com/android/systemui/statusbar/phone/MultiUserSwitch.java
new file mode 100644
index 0000000..f393dcd
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/MultiUserSwitch.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserManager;
+import android.provider.ContactsContract;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.Button;
+import android.widget.FrameLayout;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.Prefs;
+import com.android.systemui.Prefs.Key;
+import com.android.systemui.R;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.qs.DetailAdapter;
+import com.android.systemui.qs.QSPanel;
+import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+
+/**
+ * Container for image of the multi user switcher (tappable).
+ */
+public class MultiUserSwitch extends FrameLayout implements View.OnClickListener {
+
+    protected QSPanel mQsPanel;
+    private KeyguardUserSwitcher mKeyguardUserSwitcher;
+    private boolean mKeyguardMode;
+    private UserSwitcherController.BaseUserAdapter mUserListener;
+
+    final UserManager mUserManager;
+
+    private final int[] mTmpInt2 = new int[2];
+
+    protected UserSwitcherController mUserSwitcherController;
+
+    public MultiUserSwitch(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mUserManager = UserManager.get(getContext());
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        setOnClickListener(this);
+        refreshContentDescription();
+    }
+
+    public void setQsPanel(QSPanel qsPanel) {
+        mQsPanel = qsPanel;
+        setUserSwitcherController(Dependency.get(UserSwitcherController.class));
+    }
+
+    public boolean hasMultipleUsers() {
+        if (mUserListener == null) {
+            return false;
+        }
+        return mUserListener.getUserCount() != 0
+                && Prefs.getBoolean(getContext(), Key.SEEN_MULTI_USER, false);
+    }
+
+    public void setUserSwitcherController(UserSwitcherController userSwitcherController) {
+        mUserSwitcherController = userSwitcherController;
+        registerListener();
+        refreshContentDescription();
+    }
+
+    public void setKeyguardUserSwitcher(KeyguardUserSwitcher keyguardUserSwitcher) {
+        mKeyguardUserSwitcher = keyguardUserSwitcher;
+    }
+
+    public void setKeyguardMode(boolean keyguardShowing) {
+        mKeyguardMode = keyguardShowing;
+        registerListener();
+    }
+
+    private void registerListener() {
+        if (mUserManager.isUserSwitcherEnabled() && mUserListener == null) {
+
+            final UserSwitcherController controller = mUserSwitcherController;
+            if (controller != null) {
+                mUserListener = new UserSwitcherController.BaseUserAdapter(controller) {
+                    @Override
+                    public void notifyDataSetChanged() {
+                        refreshContentDescription();
+                    }
+
+                    @Override
+                    public View getView(int position, View convertView, ViewGroup parent) {
+                        return null;
+                    }
+                };
+                refreshContentDescription();
+            }
+        }
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (mUserManager.isUserSwitcherEnabled()) {
+            if (mKeyguardMode) {
+                if (mKeyguardUserSwitcher != null) {
+                    mKeyguardUserSwitcher.show(true /* animate */);
+                }
+            } else if (mQsPanel != null && mUserSwitcherController != null) {
+                View center = getChildCount() > 0 ? getChildAt(0) : this;
+
+                center.getLocationInWindow(mTmpInt2);
+                mTmpInt2[0] += center.getWidth() / 2;
+                mTmpInt2[1] += center.getHeight() / 2;
+
+                mQsPanel.showDetailAdapter(true,
+                        getUserDetailAdapter(),
+                        mTmpInt2);
+            }
+        } else {
+            if (mQsPanel != null) {
+                Intent intent = ContactsContract.QuickContact.composeQuickContactsIntent(
+                        getContext(), v, ContactsContract.Profile.CONTENT_URI,
+                        ContactsContract.QuickContact.MODE_LARGE, null);
+                Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(intent, 0);
+            }
+        }
+    }
+
+    @Override
+    public void setClickable(boolean clickable) {
+        super.setClickable(clickable);
+        refreshContentDescription();
+    }
+
+    private void refreshContentDescription() {
+        String currentUser = null;
+        if (mUserManager.isUserSwitcherEnabled()
+                && mUserSwitcherController != null) {
+            currentUser = mUserSwitcherController.getCurrentUserName(mContext);
+        }
+
+        String text = null;
+
+        if (!TextUtils.isEmpty(currentUser)) {
+            text = mContext.getString(
+                    R.string.accessibility_quick_settings_user,
+                    currentUser);
+        }
+
+        if (!TextUtils.equals(getContentDescription(), text)) {
+            setContentDescription(text);
+        }
+    }
+
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEvent(event);
+        event.setClassName(Button.class.getName());
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        info.setClassName(Button.class.getName());
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    protected DetailAdapter getUserDetailAdapter() {
+        return mUserSwitcherController.userDetailAdapter;
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/NavigationBarFragment.java b/com/android/systemui/statusbar/phone/NavigationBarFragment.java
new file mode 100644
index 0000000..cfe0a4a
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NavigationBarFragment.java
@@ -0,0 +1,698 @@
+/*
+ * Copyright (C) 2017 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.phone;
+
+import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
+import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
+import static android.app.StatusBarManager.windowStateToString;
+
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT;
+import static com.android.systemui.statusbar.phone.StatusBar.DEBUG_WINDOW_STATE;
+import static com.android.systemui.statusbar.phone.StatusBar.dumpBarTransitions;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityManagerNative;
+import android.app.Fragment;
+import android.app.IActivityManager;
+import android.app.StatusBarManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.database.ContentObserver;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.inputmethodservice.InputMethodService;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.IRotationWatcher.Stub;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.view.WindowManagerGlobal;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.keyguard.LatencyTracker;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
+import com.android.systemui.recents.Recents;
+import com.android.systemui.stackdivider.Divider;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.CommandQueue.Callbacks;
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
+import com.android.systemui.statusbar.policy.KeyButtonView;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Fragment containing the NavigationBarFragment. Contains logic for what happens
+ * on clicks and view states of the nav bar.
+ */
+public class NavigationBarFragment extends Fragment implements Callbacks {
+
+    public static final String TAG = "NavigationBar";
+    private static final boolean DEBUG = false;
+    private static final String EXTRA_DISABLE_STATE = "disabled_state";
+
+    /** Allow some time inbetween the long press for back and recents. */
+    private static final int LOCK_TO_APP_GESTURE_TOLERENCE = 200;
+
+    protected NavigationBarView mNavigationBarView = null;
+    protected AssistManager mAssistManager;
+
+    private int mNavigationBarWindowState = WINDOW_STATE_SHOWING;
+
+    private int mNavigationIconHints = 0;
+    private int mNavigationBarMode;
+    private AccessibilityManager mAccessibilityManager;
+    private MagnificationContentObserver mMagnificationObserver;
+    private ContentResolver mContentResolver;
+
+    private int mDisabledFlags1;
+    private StatusBar mStatusBar;
+    private Recents mRecents;
+    private Divider mDivider;
+    private WindowManager mWindowManager;
+    private CommandQueue mCommandQueue;
+    private long mLastLockToAppLongPress;
+
+    private Locale mLocale;
+    private int mLayoutDirection;
+
+    private int mSystemUiVisibility;
+    private LightBarController mLightBarController;
+
+    public boolean mHomeBlockedThisTouch;
+
+    // ----- Fragment Lifecycle Callbacks -----
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mCommandQueue = SysUiServiceProvider.getComponent(getContext(), CommandQueue.class);
+        mCommandQueue.addCallbacks(this);
+        mStatusBar = SysUiServiceProvider.getComponent(getContext(), StatusBar.class);
+        mRecents = SysUiServiceProvider.getComponent(getContext(), Recents.class);
+        mDivider = SysUiServiceProvider.getComponent(getContext(), Divider.class);
+        mWindowManager = getContext().getSystemService(WindowManager.class);
+        mAccessibilityManager = getContext().getSystemService(AccessibilityManager.class);
+        Dependency.get(AccessibilityManagerWrapper.class).addCallback(
+                mAccessibilityListener);
+        mContentResolver = getContext().getContentResolver();
+        mMagnificationObserver = new MagnificationContentObserver(
+                getContext().getMainThreadHandler());
+        mContentResolver.registerContentObserver(Settings.Secure.getUriFor(
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED), false,
+                mMagnificationObserver, UserHandle.USER_ALL);
+
+        if (savedInstanceState != null) {
+            mDisabledFlags1 = savedInstanceState.getInt(EXTRA_DISABLE_STATE, 0);
+        }
+        mAssistManager = Dependency.get(AssistManager.class);
+
+        try {
+            WindowManagerGlobal.getWindowManagerService()
+                    .watchRotation(mRotationWatcher, getContext().getDisplay().getDisplayId());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mCommandQueue.removeCallbacks(this);
+        Dependency.get(AccessibilityManagerWrapper.class).removeCallback(
+                mAccessibilityListener);
+        mContentResolver.unregisterContentObserver(mMagnificationObserver);
+        try {
+            WindowManagerGlobal.getWindowManagerService()
+                    .removeRotationWatcher(mRotationWatcher);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+            Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.navigation_bar, container, false);
+    }
+
+    @Override
+    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        mNavigationBarView = (NavigationBarView) view;
+
+        mNavigationBarView.setDisabledFlags(mDisabledFlags1);
+        mNavigationBarView.setComponents(mRecents, mDivider);
+        mNavigationBarView.setOnVerticalChangedListener(this::onVerticalChanged);
+        mNavigationBarView.setOnTouchListener(this::onNavigationTouch);
+        if (savedInstanceState != null) {
+            mNavigationBarView.getLightTransitionsController().restoreState(savedInstanceState);
+        }
+
+        prepareNavigationBarView();
+        checkNavBarModes();
+
+        IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
+        filter.addAction(Intent.ACTION_SCREEN_ON);
+        getContext().registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null, null);
+        notifyNavigationBarScreenOn();
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        mNavigationBarView.getLightTransitionsController().destroy(getContext());
+        getContext().unregisterReceiver(mBroadcastReceiver);
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putInt(EXTRA_DISABLE_STATE, mDisabledFlags1);
+        if (mNavigationBarView != null) {
+            mNavigationBarView.getLightTransitionsController().saveState(outState);
+        }
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        final Locale locale = getContext().getResources().getConfiguration().locale;
+        final int ld = TextUtils.getLayoutDirectionFromLocale(locale);
+        if (!locale.equals(mLocale) || ld != mLayoutDirection) {
+            if (DEBUG) {
+                Log.v(TAG, String.format(
+                        "config changed locale/LD: %s (%d) -> %s (%d)", mLocale, mLayoutDirection,
+                        locale, ld));
+            }
+            mLocale = locale;
+            mLayoutDirection = ld;
+            refreshLayout(ld);
+        }
+        repositionNavigationBar();
+    }
+
+    @Override
+    public void dump(String prefix, FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (mNavigationBarView != null) {
+            pw.print("  mNavigationBarWindowState=");
+            pw.println(windowStateToString(mNavigationBarWindowState));
+            pw.print("  mNavigationBarMode=");
+            pw.println(BarTransitions.modeToString(mNavigationBarMode));
+            dumpBarTransitions(pw, "mNavigationBarView", mNavigationBarView.getBarTransitions());
+        }
+
+        pw.print("  mNavigationBarView=");
+        if (mNavigationBarView == null) {
+            pw.println("null");
+        } else {
+            mNavigationBarView.dump(fd, pw, args);
+        }
+    }
+
+    // ----- CommandQueue Callbacks -----
+
+    @Override
+    public void setImeWindowStatus(IBinder token, int vis, int backDisposition,
+            boolean showImeSwitcher) {
+        boolean imeShown = (vis & InputMethodService.IME_VISIBLE) != 0;
+        int hints = mNavigationIconHints;
+        if ((backDisposition == InputMethodService.BACK_DISPOSITION_WILL_DISMISS) || imeShown) {
+            hints |= NAVIGATION_HINT_BACK_ALT;
+        } else {
+            hints &= ~NAVIGATION_HINT_BACK_ALT;
+        }
+        if (showImeSwitcher) {
+            hints |= NAVIGATION_HINT_IME_SHOWN;
+        } else {
+            hints &= ~NAVIGATION_HINT_IME_SHOWN;
+        }
+        if (hints == mNavigationIconHints) return;
+
+        mNavigationIconHints = hints;
+
+        if (mNavigationBarView != null) {
+            mNavigationBarView.setNavigationIconHints(hints);
+        }
+        mStatusBar.checkBarModes();
+    }
+
+    @Override
+    public void topAppWindowChanged(boolean showMenu) {
+        if (mNavigationBarView != null) {
+            mNavigationBarView.setMenuVisibility(showMenu);
+        }
+    }
+
+    @Override
+    public void setWindowState(int window, int state) {
+        if (mNavigationBarView != null
+                && window == StatusBarManager.WINDOW_NAVIGATION_BAR
+                && mNavigationBarWindowState != state) {
+            mNavigationBarWindowState = state;
+            if (DEBUG_WINDOW_STATE) Log.d(TAG, "Navigation bar " + windowStateToString(state));
+        }
+    }
+
+    // Injected from StatusBar at creation.
+    public void setCurrentSysuiVisibility(int systemUiVisibility) {
+        mSystemUiVisibility = systemUiVisibility;
+        mNavigationBarMode = mStatusBar.computeBarMode(0, mSystemUiVisibility,
+                View.NAVIGATION_BAR_TRANSIENT, View.NAVIGATION_BAR_TRANSLUCENT,
+                View.NAVIGATION_BAR_TRANSPARENT);
+        checkNavBarModes();
+        mStatusBar.touchAutoHide();
+        mLightBarController.onNavigationVisibilityChanged(mSystemUiVisibility, 0 /* mask */,
+                true /* nbModeChanged */, mNavigationBarMode);
+    }
+
+    @Override
+    public void setSystemUiVisibility(int vis, int fullscreenStackVis, int dockedStackVis,
+            int mask, Rect fullscreenStackBounds, Rect dockedStackBounds) {
+        final int oldVal = mSystemUiVisibility;
+        final int newVal = (oldVal & ~mask) | (vis & mask);
+        final int diff = newVal ^ oldVal;
+        boolean nbModeChanged = false;
+        if (diff != 0) {
+            mSystemUiVisibility = newVal;
+
+            // update navigation bar mode
+            final int nbMode = getView() == null
+                    ? -1 : mStatusBar.computeBarMode(oldVal, newVal,
+                    View.NAVIGATION_BAR_TRANSIENT, View.NAVIGATION_BAR_TRANSLUCENT,
+                    View.NAVIGATION_BAR_TRANSPARENT);
+            nbModeChanged = nbMode != -1;
+            if (nbModeChanged) {
+                if (mNavigationBarMode != nbMode) {
+                    mNavigationBarMode = nbMode;
+                    checkNavBarModes();
+                }
+                mStatusBar.touchAutoHide();
+            }
+        }
+
+        mLightBarController.onNavigationVisibilityChanged(vis, mask, nbModeChanged,
+                mNavigationBarMode);
+    }
+
+    @Override
+    public void disable(int state1, int state2, boolean animate) {
+        // All navigation bar flags are in state1.
+        int masked = state1 & (StatusBarManager.DISABLE_HOME
+                | StatusBarManager.DISABLE_RECENT
+                | StatusBarManager.DISABLE_BACK
+                | StatusBarManager.DISABLE_SEARCH);
+        if (masked != mDisabledFlags1) {
+            mDisabledFlags1 = masked;
+            if (mNavigationBarView != null) mNavigationBarView.setDisabledFlags(state1);
+        }
+    }
+
+    // ----- Internal stuffz -----
+
+    private void refreshLayout(int layoutDirection) {
+        if (mNavigationBarView != null) {
+            mNavigationBarView.setLayoutDirection(layoutDirection);
+        }
+    }
+
+    private boolean shouldDisableNavbarGestures() {
+        return !mStatusBar.isDeviceProvisioned()
+                || (mDisabledFlags1 & StatusBarManager.DISABLE_SEARCH) != 0;
+    }
+
+    private void repositionNavigationBar() {
+        if (mNavigationBarView == null || !mNavigationBarView.isAttachedToWindow()) return;
+
+        prepareNavigationBarView();
+
+        mWindowManager.updateViewLayout((View) mNavigationBarView.getParent(),
+                ((View) mNavigationBarView.getParent()).getLayoutParams());
+    }
+
+    private void notifyNavigationBarScreenOn() {
+        mNavigationBarView.notifyScreenOn();
+    }
+
+    private void prepareNavigationBarView() {
+        mNavigationBarView.reorient();
+
+        ButtonDispatcher recentsButton = mNavigationBarView.getRecentsButton();
+        recentsButton.setOnClickListener(this::onRecentsClick);
+        recentsButton.setOnTouchListener(this::onRecentsTouch);
+        recentsButton.setLongClickable(true);
+        recentsButton.setOnLongClickListener(this::onLongPressBackRecents);
+
+        ButtonDispatcher backButton = mNavigationBarView.getBackButton();
+        backButton.setLongClickable(true);
+        backButton.setOnLongClickListener(this::onLongPressBackRecents);
+
+        ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
+        homeButton.setOnTouchListener(this::onHomeTouch);
+        homeButton.setOnLongClickListener(this::onHomeLongClick);
+
+        ButtonDispatcher accessibilityButton = mNavigationBarView.getAccessibilityButton();
+        accessibilityButton.setOnClickListener(this::onAccessibilityClick);
+        accessibilityButton.setOnLongClickListener(this::onAccessibilityLongClick);
+        updateAccessibilityServicesState(mAccessibilityManager);
+    }
+
+    private boolean onHomeTouch(View v, MotionEvent event) {
+        if (mHomeBlockedThisTouch && event.getActionMasked() != MotionEvent.ACTION_DOWN) {
+            return true;
+        }
+        // If an incoming call is ringing, HOME is totally disabled.
+        // (The user is already on the InCallUI at this point,
+        // and his ONLY options are to answer or reject the call.)
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mHomeBlockedThisTouch = false;
+                TelecomManager telecomManager =
+                        getContext().getSystemService(TelecomManager.class);
+                if (telecomManager != null && telecomManager.isRinging()) {
+                    if (mStatusBar.isKeyguardShowing()) {
+                        Log.i(TAG, "Ignoring HOME; there's a ringing incoming call. " +
+                                "No heads up");
+                        mHomeBlockedThisTouch = true;
+                        return true;
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                mStatusBar.awakenDreams();
+                break;
+        }
+        return false;
+    }
+
+    private void onVerticalChanged(boolean isVertical) {
+        mStatusBar.setQsScrimEnabled(!isVertical);
+    }
+
+    private boolean onNavigationTouch(View v, MotionEvent event) {
+        mStatusBar.checkUserAutohide(v, event);
+        return false;
+    }
+
+    @VisibleForTesting
+    boolean onHomeLongClick(View v) {
+        if (shouldDisableNavbarGestures()) {
+            return false;
+        }
+        MetricsLogger.action(getContext(), MetricsEvent.ACTION_ASSIST_LONG_PRESS);
+        mAssistManager.startAssist(new Bundle() /* args */);
+        mStatusBar.awakenDreams();
+        if (mNavigationBarView != null) {
+            mNavigationBarView.abortCurrentGesture();
+        }
+        return true;
+    }
+
+    // additional optimization when we have software system buttons - start loading the recent
+    // tasks on touch down
+    private boolean onRecentsTouch(View v, MotionEvent event) {
+        int action = event.getAction() & MotionEvent.ACTION_MASK;
+        if (action == MotionEvent.ACTION_DOWN) {
+            mCommandQueue.preloadRecentApps();
+        } else if (action == MotionEvent.ACTION_CANCEL) {
+            mCommandQueue.cancelPreloadRecentApps();
+        } else if (action == MotionEvent.ACTION_UP) {
+            if (!v.isPressed()) {
+                mCommandQueue.cancelPreloadRecentApps();
+            }
+        }
+        return false;
+    }
+
+    private void onRecentsClick(View v) {
+        if (LatencyTracker.isEnabled(getContext())) {
+            LatencyTracker.getInstance(getContext()).onActionStart(
+                    LatencyTracker.ACTION_TOGGLE_RECENTS);
+        }
+        mStatusBar.awakenDreams();
+        mCommandQueue.toggleRecentApps();
+    }
+
+    /**
+     * This handles long-press of both back and recents.  They are
+     * handled together to capture them both being long-pressed
+     * at the same time to exit screen pinning (lock task).
+     *
+     * When accessibility mode is on, only a long-press from recents
+     * is required to exit.
+     *
+     * In all other circumstances we try to pass through long-press events
+     * for Back, so that apps can still use it.  Which can be from two things.
+     * 1) Not currently in screen pinning (lock task).
+     * 2) Back is long-pressed without recents.
+     */
+    private boolean onLongPressBackRecents(View v) {
+        try {
+            boolean sendBackLongPress = false;
+            IActivityManager activityManager = ActivityManagerNative.getDefault();
+            boolean touchExplorationEnabled = mAccessibilityManager.isTouchExplorationEnabled();
+            boolean inLockTaskMode = activityManager.isInLockTaskMode();
+            if (inLockTaskMode && !touchExplorationEnabled) {
+                long time = System.currentTimeMillis();
+                // If we recently long-pressed the other button then they were
+                // long-pressed 'together'
+                if ((time - mLastLockToAppLongPress) < LOCK_TO_APP_GESTURE_TOLERENCE) {
+                    activityManager.stopSystemLockTaskMode();
+                    // When exiting refresh disabled flags.
+                    mNavigationBarView.setDisabledFlags(mDisabledFlags1, true);
+                    return true;
+                } else if ((v.getId() == R.id.back)
+                        && !mNavigationBarView.getRecentsButton().getCurrentView().isPressed()) {
+                    // If we aren't pressing recents right now then they presses
+                    // won't be together, so send the standard long-press action.
+                    sendBackLongPress = true;
+                }
+                mLastLockToAppLongPress = time;
+            } else {
+                // If this is back still need to handle sending the long-press event.
+                if (v.getId() == R.id.back) {
+                    sendBackLongPress = true;
+                } else if (touchExplorationEnabled && inLockTaskMode) {
+                    // When in accessibility mode a long press that is recents (not back)
+                    // should stop lock task.
+                    activityManager.stopSystemLockTaskMode();
+                    // When exiting refresh disabled flags.
+                    mNavigationBarView.setDisabledFlags(mDisabledFlags1, true);
+                    return true;
+                } else if (v.getId() == R.id.recent_apps) {
+                    return onLongPressRecents();
+                }
+            }
+            if (sendBackLongPress) {
+                KeyButtonView keyButtonView = (KeyButtonView) v;
+                keyButtonView.sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);
+                keyButtonView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
+                return true;
+            }
+        } catch (RemoteException e) {
+            Log.d(TAG, "Unable to reach activity manager", e);
+        }
+        return false;
+    }
+
+    private boolean onLongPressRecents() {
+        if (mRecents == null || !ActivityManager.supportsMultiWindow(getContext())
+                || !mDivider.getView().getSnapAlgorithm().isSplitScreenFeasible()
+                || Recents.getConfiguration().isLowRamDevice) {
+            return false;
+        }
+
+        return mStatusBar.toggleSplitScreenMode(MetricsEvent.ACTION_WINDOW_DOCK_LONGPRESS,
+                MetricsEvent.ACTION_WINDOW_UNDOCK_LONGPRESS);
+    }
+
+    private void onAccessibilityClick(View v) {
+        mAccessibilityManager.notifyAccessibilityButtonClicked();
+    }
+
+    private boolean onAccessibilityLongClick(View v) {
+        Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        v.getContext().startActivityAsUser(intent, UserHandle.CURRENT);
+        return true;
+    }
+
+    private void updateAccessibilityServicesState(AccessibilityManager accessibilityManager) {
+        int requestingServices = 0;
+        try {
+            if (Settings.Secure.getIntForUser(mContentResolver,
+                    Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED,
+                    UserHandle.USER_CURRENT) == 1) {
+                requestingServices++;
+            }
+        } catch (Settings.SettingNotFoundException e) {
+        }
+
+        // AccessibilityManagerService resolves services for the current user since the local
+        // AccessibilityManager is created from a Context with the INTERACT_ACROSS_USERS permission
+        final List<AccessibilityServiceInfo> services =
+                accessibilityManager.getEnabledAccessibilityServiceList(
+                        AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
+        for (int i = services.size() - 1; i >= 0; --i) {
+            AccessibilityServiceInfo info = services.get(i);
+            if ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0) {
+                requestingServices++;
+            }
+        }
+
+        final boolean showAccessibilityButton = requestingServices >= 1;
+        final boolean targetSelection = requestingServices >= 2;
+        mNavigationBarView.setAccessibilityButtonState(showAccessibilityButton, targetSelection);
+    }
+
+    // ----- Methods that StatusBar talks to (should be minimized) -----
+
+    public void setLightBarController(LightBarController lightBarController) {
+        mLightBarController = lightBarController;
+        mLightBarController.setNavigationBar(mNavigationBarView.getLightTransitionsController());
+    }
+
+    public boolean isSemiTransparent() {
+        return mNavigationBarMode == MODE_SEMI_TRANSPARENT;
+    }
+
+    public void disableAnimationsDuringHide(long delay) {
+        mNavigationBarView.setLayoutTransitionsEnabled(false);
+        mNavigationBarView.postDelayed(() -> mNavigationBarView.setLayoutTransitionsEnabled(true),
+                delay + StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE);
+    }
+
+    public BarTransitions getBarTransitions() {
+        return mNavigationBarView.getBarTransitions();
+    }
+
+    public void checkNavBarModes() {
+        mStatusBar.checkBarMode(mNavigationBarMode,
+                mNavigationBarWindowState, mNavigationBarView.getBarTransitions());
+    }
+
+    public void finishBarAnimations() {
+        mNavigationBarView.getBarTransitions().finishAnimations();
+    }
+
+    private final AccessibilityServicesStateChangeListener mAccessibilityListener =
+            this::updateAccessibilityServicesState;
+
+    private class MagnificationContentObserver extends ContentObserver {
+
+        public MagnificationContentObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            NavigationBarFragment.this.updateAccessibilityServicesState(mAccessibilityManager);
+        }
+    }
+
+    private final Stub mRotationWatcher = new Stub() {
+        @Override
+        public void onRotationChanged(int rotation) throws RemoteException {
+            // We need this to be scheduled as early as possible to beat the redrawing of
+            // window in response to the orientation change.
+            Handler h = getView().getHandler();
+            Message msg = Message.obtain(h, () -> {
+                if (mNavigationBarView != null
+                        && mNavigationBarView.needsReorient(rotation)) {
+                    repositionNavigationBar();
+                }
+            });
+            msg.setAsynchronous(true);
+            h.sendMessageAtFrontOfQueue(msg);
+        }
+    };
+
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (Intent.ACTION_SCREEN_OFF.equals(action)
+                    || Intent.ACTION_SCREEN_ON.equals(action)) {
+                notifyNavigationBarScreenOn();
+            }
+        }
+    };
+
+    public static View create(Context context, FragmentListener listener) {
+        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
+                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
+                WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
+                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                        | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+                        | WindowManager.LayoutParams.FLAG_SLIPPERY,
+                PixelFormat.TRANSLUCENT);
+        lp.token = new Binder();
+        lp.setTitle("NavigationBar");
+        lp.windowAnimations = 0;
+
+        View navigationBarView = LayoutInflater.from(context).inflate(
+                R.layout.navigation_bar_window, null);
+
+        if (DEBUG) Log.v(TAG, "addNavigationBar: about to add " + navigationBarView);
+        if (navigationBarView == null) return null;
+
+        context.getSystemService(WindowManager.class).addView(navigationBarView, lp);
+        FragmentHostManager fragmentHost = FragmentHostManager.get(navigationBarView);
+        NavigationBarFragment fragment = new NavigationBarFragment();
+        fragmentHost.getFragmentManager().beginTransaction()
+                .replace(R.id.navigation_bar_frame, fragment, TAG)
+                .commit();
+        fragmentHost.addTagListener(TAG, listener);
+        return navigationBarView;
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/NavigationBarFrame.java b/com/android/systemui/statusbar/phone/NavigationBarFrame.java
new file mode 100644
index 0000000..741f783
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NavigationBarFrame.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 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.phone;
+
+import static android.view.MotionEvent.ACTION_OUTSIDE;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.FrameLayout;
+
+import com.android.systemui.statusbar.policy.DeadZone;
+
+public class NavigationBarFrame extends FrameLayout {
+
+    private DeadZone mDeadZone = null;
+
+    public NavigationBarFrame(@NonNull Context context) {
+        super(context);
+    }
+
+    public NavigationBarFrame(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public NavigationBarFrame(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public void setDeadZone(@NonNull DeadZone deadZone) {
+        mDeadZone = deadZone;
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent event) {
+        if (event.getAction() == ACTION_OUTSIDE) {
+            if (mDeadZone != null) {
+                return mDeadZone.onTouchEvent(event);
+            }
+        }
+        return super.dispatchTouchEvent(event);
+    }
+}
\ No newline at end of file
diff --git a/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java b/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java
new file mode 100644
index 0000000..ee9a791
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.policy.DividerSnapAlgorithm.SnapTarget;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.RecentsComponent;
+import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper;
+import com.android.systemui.stackdivider.Divider;
+import com.android.systemui.tuner.TunerService;
+
+import static android.view.WindowManager.DOCKED_INVALID;
+import static android.view.WindowManager.DOCKED_LEFT;
+import static android.view.WindowManager.DOCKED_TOP;
+
+/**
+ * Class to detect gestures on the navigation bar.
+ */
+public class NavigationBarGestureHelper extends GestureDetector.SimpleOnGestureListener
+        implements TunerService.Tunable, GestureHelper {
+
+    private static final String KEY_DOCK_WINDOW_GESTURE = "overview_nav_bar_gesture";
+    /**
+     * When dragging from the navigation bar, we drag in recents.
+     */
+    public static final int DRAG_MODE_NONE = -1;
+
+    /**
+     * When dragging from the navigation bar, we drag in recents.
+     */
+    public static final int DRAG_MODE_RECENTS = 0;
+
+    /**
+     * When dragging from the navigation bar, we drag the divider.
+     */
+    public static final int DRAG_MODE_DIVIDER = 1;
+
+    private RecentsComponent mRecentsComponent;
+    private Divider mDivider;
+    private Context mContext;
+    private NavigationBarView mNavigationBarView;
+    private boolean mIsVertical;
+    private boolean mIsRTL;
+
+    private final GestureDetector mTaskSwitcherDetector;
+    private final int mScrollTouchSlop;
+    private final int mMinFlingVelocity;
+    private int mTouchDownX;
+    private int mTouchDownY;
+    private boolean mDownOnRecents;
+    private VelocityTracker mVelocityTracker;
+
+    private boolean mDockWindowEnabled;
+    private boolean mDockWindowTouchSlopExceeded;
+    private int mDragMode;
+
+    public NavigationBarGestureHelper(Context context) {
+        mContext = context;
+        ViewConfiguration configuration = ViewConfiguration.get(context);
+        Resources r = context.getResources();
+        mScrollTouchSlop = r.getDimensionPixelSize(R.dimen.navigation_bar_min_swipe_distance);
+        mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity();
+        mTaskSwitcherDetector = new GestureDetector(context, this);
+        Dependency.get(TunerService.class).addTunable(this, KEY_DOCK_WINDOW_GESTURE);
+    }
+
+    public void destroy() {
+        Dependency.get(TunerService.class).removeTunable(this);
+    }
+
+    public void setComponents(RecentsComponent recentsComponent, Divider divider,
+            NavigationBarView navigationBarView) {
+        mRecentsComponent = recentsComponent;
+        mDivider = divider;
+        mNavigationBarView = navigationBarView;
+    }
+
+    public void setBarState(boolean isVertical, boolean isRTL) {
+        mIsVertical = isVertical;
+        mIsRTL = isRTL;
+    }
+
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        // If we move more than a fixed amount, then start capturing for the
+        // task switcher detector
+        mTaskSwitcherDetector.onTouchEvent(event);
+        int action = event.getAction();
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN: {
+                mTouchDownX = (int) event.getX();
+                mTouchDownY = (int) event.getY();
+                break;
+            }
+            case MotionEvent.ACTION_MOVE: {
+                int x = (int) event.getX();
+                int y = (int) event.getY();
+                int xDiff = Math.abs(x - mTouchDownX);
+                int yDiff = Math.abs(y - mTouchDownY);
+                boolean exceededTouchSlop = !mIsVertical
+                        ? xDiff > mScrollTouchSlop && xDiff > yDiff
+                        : yDiff > mScrollTouchSlop && yDiff > xDiff;
+                if (exceededTouchSlop) {
+                    return true;
+                }
+                break;
+            }
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                break;
+        }
+        return mDockWindowEnabled && interceptDockWindowEvent(event);
+    }
+
+    private boolean interceptDockWindowEvent(MotionEvent event) {
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                handleDragActionDownEvent(event);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                return handleDragActionMoveEvent(event);
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                handleDragActionUpEvent(event);
+                break;
+        }
+        return false;
+    }
+
+    private boolean handleDockWindowEvent(MotionEvent event) {
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                handleDragActionDownEvent(event);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                handleDragActionMoveEvent(event);
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                handleDragActionUpEvent(event);
+                break;
+        }
+        return true;
+    }
+
+    private void handleDragActionDownEvent(MotionEvent event) {
+        mVelocityTracker = VelocityTracker.obtain();
+        mVelocityTracker.addMovement(event);
+        mDockWindowTouchSlopExceeded = false;
+        mTouchDownX = (int) event.getX();
+        mTouchDownY = (int) event.getY();
+
+        if (mNavigationBarView != null) {
+            View recentsButton = mNavigationBarView.getRecentsButton().getCurrentView();
+            if (recentsButton != null) {
+                mDownOnRecents = mTouchDownX >= recentsButton.getLeft()
+                        && mTouchDownX <= recentsButton.getRight()
+                        && mTouchDownY >= recentsButton.getTop()
+                        && mTouchDownY <= recentsButton.getBottom();
+            } else {
+                mDownOnRecents = false;
+            }
+        }
+    }
+
+    private boolean handleDragActionMoveEvent(MotionEvent event) {
+        mVelocityTracker.addMovement(event);
+        int x = (int) event.getX();
+        int y = (int) event.getY();
+        int xDiff = Math.abs(x - mTouchDownX);
+        int yDiff = Math.abs(y - mTouchDownY);
+        if (mDivider == null || mRecentsComponent == null) {
+            return false;
+        }
+        if (!mDockWindowTouchSlopExceeded) {
+            boolean touchSlopExceeded = !mIsVertical
+                    ? yDiff > mScrollTouchSlop && yDiff > xDiff
+                    : xDiff > mScrollTouchSlop && xDiff > yDiff;
+            if (mDownOnRecents && touchSlopExceeded
+                    && mDivider.getView().getWindowManagerProxy().getDockSide() == DOCKED_INVALID) {
+                Rect initialBounds = null;
+                int dragMode = calculateDragMode();
+                int createMode = ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT;
+                if (dragMode == DRAG_MODE_DIVIDER) {
+                    initialBounds = new Rect();
+                    mDivider.getView().calculateBoundsForPosition(mIsVertical
+                                    ? (int) event.getRawX()
+                                    : (int) event.getRawY(),
+                            mDivider.getView().isHorizontalDivision()
+                                    ? DOCKED_TOP
+                                    : DOCKED_LEFT,
+                            initialBounds);
+                } else if (dragMode == DRAG_MODE_RECENTS && mTouchDownX
+                        < mContext.getResources().getDisplayMetrics().widthPixels / 2) {
+                    createMode = ActivityManager.DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT;
+                }
+                boolean docked = mRecentsComponent.dockTopTask(dragMode, createMode, initialBounds,
+                        MetricsEvent.ACTION_WINDOW_DOCK_SWIPE);
+                if (docked) {
+                    mDragMode = dragMode;
+                    if (mDragMode == DRAG_MODE_DIVIDER) {
+                        mDivider.getView().startDragging(false /* animate */, true /* touching*/);
+                    }
+                    mDockWindowTouchSlopExceeded = true;
+                    return true;
+                }
+            }
+        } else {
+            if (mDragMode == DRAG_MODE_DIVIDER) {
+                int position = !mIsVertical ? (int) event.getRawY() : (int) event.getRawX();
+                SnapTarget snapTarget = mDivider.getView().getSnapAlgorithm()
+                        .calculateSnapTarget(position, 0f /* velocity */, false /* hardDismiss */);
+                mDivider.getView().resizeStack(position, snapTarget.position, snapTarget);
+            } else if (mDragMode == DRAG_MODE_RECENTS) {
+                mRecentsComponent.onDraggingInRecents(event.getRawY());
+            }
+        }
+        return false;
+    }
+
+    private void handleDragActionUpEvent(MotionEvent event) {
+        mVelocityTracker.addMovement(event);
+        mVelocityTracker.computeCurrentVelocity(1000);
+        if (mDockWindowTouchSlopExceeded && mDivider != null && mRecentsComponent != null) {
+            if (mDragMode == DRAG_MODE_DIVIDER) {
+                mDivider.getView().stopDragging(mIsVertical
+                                ? (int) event.getRawX()
+                                : (int) event.getRawY(),
+                        mIsVertical
+                                ? mVelocityTracker.getXVelocity()
+                                : mVelocityTracker.getYVelocity(),
+                        true /* avoidDismissStart */, false /* logMetrics */);
+            } else if (mDragMode == DRAG_MODE_RECENTS) {
+                mRecentsComponent.onDraggingInRecentsEnded(mVelocityTracker.getYVelocity());
+            }
+        }
+        mVelocityTracker.recycle();
+        mVelocityTracker = null;
+    }
+
+    private int calculateDragMode() {
+        if (mIsVertical && !mDivider.getView().isHorizontalDivision()) {
+            return DRAG_MODE_DIVIDER;
+        }
+        if (!mIsVertical && mDivider.getView().isHorizontalDivision()) {
+            return DRAG_MODE_DIVIDER;
+        }
+        return DRAG_MODE_RECENTS;
+    }
+
+    public boolean onTouchEvent(MotionEvent event) {
+        boolean result = mTaskSwitcherDetector.onTouchEvent(event);
+        if (mDockWindowEnabled) {
+            result |= handleDockWindowEvent(event);
+        }
+        return result;
+    }
+
+    @Override
+    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+        float absVelX = Math.abs(velocityX);
+        float absVelY = Math.abs(velocityY);
+        boolean isValidFling = absVelX > mMinFlingVelocity &&
+                mIsVertical ? (absVelY > absVelX) : (absVelX > absVelY);
+        if (isValidFling && mRecentsComponent != null) {
+            boolean showNext;
+            if (!mIsRTL) {
+                showNext = mIsVertical ? (velocityY < 0) : (velocityX < 0);
+            } else {
+                // In RTL, vertical is still the same, but horizontal is flipped
+                showNext = mIsVertical ? (velocityY < 0) : (velocityX > 0);
+            }
+            if (showNext) {
+                mRecentsComponent.showNextAffiliatedTask();
+            } else {
+                mRecentsComponent.showPrevAffiliatedTask();
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public void onTuningChanged(String key, String newValue) {
+        switch (key) {
+            case KEY_DOCK_WINDOW_GESTURE:
+                mDockWindowEnabled = newValue != null && (Integer.parseInt(newValue) != 0);
+                break;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
new file mode 100644
index 0000000..4e79314
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
@@ -0,0 +1,435 @@
+/*
+ * Copyright (C) 2016 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.phone;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.drawable.Icon;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Display;
+import android.view.Display.Mode;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.Space;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.PluginManager;
+import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider;
+import com.android.systemui.statusbar.phone.ReverseLinearLayout.ReverseFrameLayout;
+import com.android.systemui.statusbar.policy.KeyButtonView;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.tuner.TunerService.Tunable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+public class NavigationBarInflaterView extends FrameLayout
+        implements Tunable, PluginListener<NavBarButtonProvider> {
+
+    private static final String TAG = "NavBarInflater";
+
+    public static final String NAV_BAR_VIEWS = "sysui_nav_bar";
+    public static final String NAV_BAR_LEFT = "sysui_nav_bar_left";
+    public static final String NAV_BAR_RIGHT = "sysui_nav_bar_right";
+
+    public static final String MENU_IME = "menu_ime";
+    public static final String BACK = "back";
+    public static final String HOME = "home";
+    public static final String RECENT = "recent";
+    public static final String NAVSPACE = "space";
+    public static final String CLIPBOARD = "clipboard";
+    public static final String KEY = "key";
+    public static final String LEFT = "left";
+    public static final String RIGHT = "right";
+
+    public static final String GRAVITY_SEPARATOR = ";";
+    public static final String BUTTON_SEPARATOR = ",";
+
+    public static final String SIZE_MOD_START = "[";
+    public static final String SIZE_MOD_END = "]";
+
+    public static final String KEY_CODE_START = "(";
+    public static final String KEY_IMAGE_DELIM = ":";
+    public static final String KEY_CODE_END = ")";
+    private static final String WEIGHT_SUFFIX = "W";
+    private static final String WEIGHT_CENTERED_SUFFIX = "WC";
+
+    private final List<NavBarButtonProvider> mPlugins = new ArrayList<>();
+
+    protected LayoutInflater mLayoutInflater;
+    protected LayoutInflater mLandscapeInflater;
+
+    protected FrameLayout mRot0;
+    protected FrameLayout mRot90;
+    private boolean isRot0Landscape;
+
+    private SparseArray<ButtonDispatcher> mButtonDispatchers;
+    private String mCurrentLayout;
+
+    private View mLastPortrait;
+    private View mLastLandscape;
+
+    private boolean mAlternativeOrder;
+
+    public NavigationBarInflaterView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        createInflaters();
+        Display display = ((WindowManager)
+                context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+        Mode displayMode = display.getMode();
+        isRot0Landscape = displayMode.getPhysicalWidth() > displayMode.getPhysicalHeight();
+    }
+
+    private void createInflaters() {
+        mLayoutInflater = LayoutInflater.from(mContext);
+        Configuration landscape = new Configuration();
+        landscape.setTo(mContext.getResources().getConfiguration());
+        landscape.orientation = Configuration.ORIENTATION_LANDSCAPE;
+        mLandscapeInflater = LayoutInflater.from(mContext.createConfigurationContext(landscape));
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        inflateChildren();
+        clearViews();
+        inflateLayout(getDefaultLayout());
+    }
+
+    private void inflateChildren() {
+        removeAllViews();
+        mRot0 = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout, this, false);
+        mRot0.setId(R.id.rot0);
+        addView(mRot0);
+        mRot90 = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout_rot90, this,
+                false);
+        mRot90.setId(R.id.rot90);
+        addView(mRot90);
+        updateAlternativeOrder();
+    }
+
+    protected String getDefaultLayout() {
+        return mContext.getString(R.string.config_navBarLayout);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        Dependency.get(TunerService.class).addTunable(this, NAV_BAR_VIEWS, NAV_BAR_LEFT,
+                NAV_BAR_RIGHT);
+        Dependency.get(PluginManager.class).addPluginListener(this,
+                NavBarButtonProvider.class, true /* Allow multiple */);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        Dependency.get(TunerService.class).removeTunable(this);
+        Dependency.get(PluginManager.class).removePluginListener(this);
+        super.onDetachedFromWindow();
+    }
+
+    @Override
+    public void onTuningChanged(String key, String newValue) {
+        if (NAV_BAR_VIEWS.equals(key)) {
+            if (!Objects.equals(mCurrentLayout, newValue)) {
+                clearViews();
+                inflateLayout(newValue);
+            }
+        } else if (NAV_BAR_LEFT.equals(key) || NAV_BAR_RIGHT.equals(key)) {
+            clearViews();
+            inflateLayout(mCurrentLayout);
+        }
+    }
+
+    public void setButtonDispatchers(SparseArray<ButtonDispatcher> buttonDisatchers) {
+        mButtonDispatchers = buttonDisatchers;
+        for (int i = 0; i < buttonDisatchers.size(); i++) {
+            initiallyFill(buttonDisatchers.valueAt(i));
+        }
+    }
+
+    public void setAlternativeOrder(boolean alternativeOrder) {
+        if (alternativeOrder != mAlternativeOrder) {
+            mAlternativeOrder = alternativeOrder;
+            updateAlternativeOrder();
+        }
+    }
+
+    private void updateAlternativeOrder() {
+        updateAlternativeOrder(mRot0.findViewById(R.id.ends_group));
+        updateAlternativeOrder(mRot0.findViewById(R.id.center_group));
+        updateAlternativeOrder(mRot90.findViewById(R.id.ends_group));
+        updateAlternativeOrder(mRot90.findViewById(R.id.center_group));
+    }
+
+    private void updateAlternativeOrder(View v) {
+        if (v instanceof ReverseLinearLayout) {
+            ((ReverseLinearLayout) v).setAlternativeOrder(mAlternativeOrder);
+        }
+    }
+
+    private void initiallyFill(ButtonDispatcher buttonDispatcher) {
+        addAll(buttonDispatcher, (ViewGroup) mRot0.findViewById(R.id.ends_group));
+        addAll(buttonDispatcher, (ViewGroup) mRot0.findViewById(R.id.center_group));
+        addAll(buttonDispatcher, (ViewGroup) mRot90.findViewById(R.id.ends_group));
+        addAll(buttonDispatcher, (ViewGroup) mRot90.findViewById(R.id.center_group));
+    }
+
+    private void addAll(ButtonDispatcher buttonDispatcher, ViewGroup parent) {
+        for (int i = 0; i < parent.getChildCount(); i++) {
+            // Need to manually search for each id, just in case each group has more than one
+            // of a single id.  It probably mostly a waste of time, but shouldn't take long
+            // and will only happen once.
+            if (parent.getChildAt(i).getId() == buttonDispatcher.getId()) {
+                buttonDispatcher.addView(parent.getChildAt(i));
+            } else if (parent.getChildAt(i) instanceof ViewGroup) {
+                addAll(buttonDispatcher, (ViewGroup) parent.getChildAt(i));
+            }
+        }
+    }
+
+    protected void inflateLayout(String newLayout) {
+        mCurrentLayout = newLayout;
+        if (newLayout == null) {
+            newLayout = getDefaultLayout();
+        }
+        String[] sets = newLayout.split(GRAVITY_SEPARATOR, 3);
+        String[] start = sets[0].split(BUTTON_SEPARATOR);
+        String[] center = sets[1].split(BUTTON_SEPARATOR);
+        String[] end = sets[2].split(BUTTON_SEPARATOR);
+        // Inflate these in start to end order or accessibility traversal will be messed up.
+        inflateButtons(start, mRot0.findViewById(R.id.ends_group), isRot0Landscape, true);
+        inflateButtons(start, mRot90.findViewById(R.id.ends_group), !isRot0Landscape, true);
+
+        inflateButtons(center, mRot0.findViewById(R.id.center_group), isRot0Landscape, false);
+        inflateButtons(center, mRot90.findViewById(R.id.center_group), !isRot0Landscape, false);
+
+        addGravitySpacer(mRot0.findViewById(R.id.ends_group));
+        addGravitySpacer(mRot90.findViewById(R.id.ends_group));
+
+        inflateButtons(end, mRot0.findViewById(R.id.ends_group), isRot0Landscape, false);
+        inflateButtons(end, mRot90.findViewById(R.id.ends_group), !isRot0Landscape, false);
+    }
+
+    private void addGravitySpacer(LinearLayout layout) {
+        layout.addView(new Space(mContext), new LinearLayout.LayoutParams(0, 0, 1));
+    }
+
+    private void inflateButtons(String[] buttons, ViewGroup parent, boolean landscape,
+            boolean start) {
+        for (int i = 0; i < buttons.length; i++) {
+            inflateButton(buttons[i], parent, landscape, start);
+        }
+    }
+
+    private ViewGroup.LayoutParams copy(ViewGroup.LayoutParams layoutParams) {
+        if (layoutParams instanceof LinearLayout.LayoutParams) {
+            return new LinearLayout.LayoutParams(layoutParams.width, layoutParams.height,
+                    ((LinearLayout.LayoutParams) layoutParams).weight);
+        }
+        return new LayoutParams(layoutParams.width, layoutParams.height);
+    }
+
+    @Nullable
+    protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape,
+            boolean start) {
+        LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater;
+        View v = createView(buttonSpec, parent, inflater);
+        if (v == null) return null;
+
+        v = applySize(v, buttonSpec, landscape, start);
+        parent.addView(v);
+        addToDispatchers(v);
+        View lastView = landscape ? mLastLandscape : mLastPortrait;
+        View accessibilityView = v;
+        if (v instanceof ReverseFrameLayout) {
+            accessibilityView = ((ReverseFrameLayout) v).getChildAt(0);
+        }
+        if (lastView != null) {
+            accessibilityView.setAccessibilityTraversalAfter(lastView.getId());
+        }
+        if (landscape) {
+            mLastLandscape = accessibilityView;
+        } else {
+            mLastPortrait = accessibilityView;
+        }
+        return v;
+    }
+
+    private View applySize(View v, String buttonSpec, boolean landscape, boolean start) {
+        String sizeStr = extractSize(buttonSpec);
+        if (sizeStr == null) return v;
+
+        if (sizeStr.contains(WEIGHT_SUFFIX)) {
+            float weight = Float.parseFloat(sizeStr.substring(0, sizeStr.indexOf(WEIGHT_SUFFIX)));
+            FrameLayout frame = new ReverseFrameLayout(mContext);
+            LayoutParams childParams = new LayoutParams(v.getLayoutParams());
+            if (sizeStr.endsWith(WEIGHT_CENTERED_SUFFIX)) {
+                childParams.gravity = Gravity.CENTER;
+            } else {
+                childParams.gravity = landscape ? (start ? Gravity.BOTTOM : Gravity.TOP)
+                        : (start ? Gravity.START : Gravity.END);
+            }
+            frame.addView(v, childParams);
+            frame.setLayoutParams(new LinearLayout.LayoutParams(0, MATCH_PARENT, weight));
+            frame.setClipChildren(false);
+            frame.setClipToPadding(false);
+            return frame;
+        }
+        float size = Float.parseFloat(sizeStr);
+        ViewGroup.LayoutParams params = v.getLayoutParams();
+        params.width = (int) (params.width * size);
+        return v;
+    }
+
+    private View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater) {
+        View v = null;
+        String button = extractButton(buttonSpec);
+        if (LEFT.equals(button)) {
+            String s = Dependency.get(TunerService.class).getValue(NAV_BAR_LEFT, NAVSPACE);
+            button = extractButton(s);
+        } else if (RIGHT.equals(button)) {
+            String s = Dependency.get(TunerService.class).getValue(NAV_BAR_RIGHT, MENU_IME);
+            button = extractButton(s);
+        }
+        // Let plugins go first so they can override a standard view if they want.
+        for (NavBarButtonProvider provider : mPlugins) {
+            v = provider.createView(buttonSpec, parent);
+            if (v != null) return v;
+        }
+        if (HOME.equals(button)) {
+            v = inflater.inflate(R.layout.home, parent, false);
+        } else if (BACK.equals(button)) {
+            v = inflater.inflate(R.layout.back, parent, false);
+        } else if (RECENT.equals(button)) {
+            v = inflater.inflate(R.layout.recent_apps, parent, false);
+        } else if (MENU_IME.equals(button)) {
+            v = inflater.inflate(R.layout.menu_ime, parent, false);
+        } else if (NAVSPACE.equals(button)) {
+            v = inflater.inflate(R.layout.nav_key_space, parent, false);
+        } else if (CLIPBOARD.equals(button)) {
+            v = inflater.inflate(R.layout.clipboard, parent, false);
+        } else if (button.startsWith(KEY)) {
+            String uri = extractImage(button);
+            int code = extractKeycode(button);
+            v = inflater.inflate(R.layout.custom_key, parent, false);
+            ((KeyButtonView) v).setCode(code);
+            if (uri != null) {
+                if (uri.contains(":")) {
+                    ((KeyButtonView) v).loadAsync(Icon.createWithContentUri(uri));
+                } else if (uri.contains("/")) {
+                    int index = uri.indexOf('/');
+                    String pkg = uri.substring(0, index);
+                    int id = Integer.parseInt(uri.substring(index + 1));
+                    ((KeyButtonView) v).loadAsync(Icon.createWithResource(pkg, id));
+                }
+            }
+        }
+        return v;
+    }
+
+    public static String extractImage(String buttonSpec) {
+        if (!buttonSpec.contains(KEY_IMAGE_DELIM)) {
+            return null;
+        }
+        final int start = buttonSpec.indexOf(KEY_IMAGE_DELIM);
+        String subStr = buttonSpec.substring(start + 1, buttonSpec.indexOf(KEY_CODE_END));
+        return subStr;
+    }
+
+    public static int extractKeycode(String buttonSpec) {
+        if (!buttonSpec.contains(KEY_CODE_START)) {
+            return 1;
+        }
+        final int start = buttonSpec.indexOf(KEY_CODE_START);
+        String subStr = buttonSpec.substring(start + 1, buttonSpec.indexOf(KEY_IMAGE_DELIM));
+        return Integer.parseInt(subStr);
+    }
+
+    public static String extractSize(String buttonSpec) {
+        if (!buttonSpec.contains(SIZE_MOD_START)) {
+            return null;
+        }
+        final int sizeStart = buttonSpec.indexOf(SIZE_MOD_START);
+        return buttonSpec.substring(sizeStart + 1, buttonSpec.indexOf(SIZE_MOD_END));
+    }
+
+    public static String extractButton(String buttonSpec) {
+        if (!buttonSpec.contains(SIZE_MOD_START)) {
+            return buttonSpec;
+        }
+        return buttonSpec.substring(0, buttonSpec.indexOf(SIZE_MOD_START));
+    }
+
+    private void addToDispatchers(View v) {
+        if (mButtonDispatchers != null) {
+            final int indexOfKey = mButtonDispatchers.indexOfKey(v.getId());
+            if (indexOfKey >= 0) {
+                mButtonDispatchers.valueAt(indexOfKey).addView(v);
+            } else if (v instanceof ViewGroup) {
+                final ViewGroup viewGroup = (ViewGroup)v;
+                final int N = viewGroup.getChildCount();
+                for (int i = 0; i < N; i++) {
+                    addToDispatchers(viewGroup.getChildAt(i));
+                }
+            }
+        }
+    }
+
+
+
+    private void clearViews() {
+        if (mButtonDispatchers != null) {
+            for (int i = 0; i < mButtonDispatchers.size(); i++) {
+                mButtonDispatchers.valueAt(i).clear();
+            }
+        }
+        clearAllChildren(mRot0.findViewById(R.id.nav_buttons));
+        clearAllChildren(mRot90.findViewById(R.id.nav_buttons));
+    }
+
+    private void clearAllChildren(ViewGroup group) {
+        for (int i = 0; i < group.getChildCount(); i++) {
+            ((ViewGroup) group.getChildAt(i)).removeAllViews();
+        }
+    }
+
+    @Override
+    public void onPluginConnected(NavBarButtonProvider plugin, Context context) {
+        mPlugins.add(plugin);
+        clearViews();
+        inflateLayout(mCurrentLayout);
+    }
+
+    @Override
+    public void onPluginDisconnected(NavBarButtonProvider plugin) {
+        mPlugins.remove(plugin);
+        clearViews();
+        inflateLayout(mCurrentLayout);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/NavigationBarTransitions.java b/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
new file mode 100644
index 0000000..f3ca66f
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2013 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.phone;
+
+import android.content.Context;
+import android.os.ServiceManager;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.systemui.R;
+
+public final class NavigationBarTransitions extends BarTransitions {
+
+    private final NavigationBarView mView;
+    private final IStatusBarService mBarService;
+    private final LightBarTransitionsController mLightTransitionsController;
+
+    private boolean mLightsOut;
+    private boolean mAutoDim;
+
+    public NavigationBarTransitions(NavigationBarView view) {
+        super(view, R.drawable.nav_background);
+        mView = view;
+        mBarService = IStatusBarService.Stub.asInterface(
+                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+        mLightTransitionsController = new LightBarTransitionsController(view.getContext(),
+                this::applyDarkIntensity);
+    }
+
+    public void init() {
+        applyModeBackground(-1, getMode(), false /*animate*/);
+        applyLightsOut(false /*animate*/, true /*force*/);
+    }
+
+    @Override
+    public void setAutoDim(boolean autoDim) {
+        if (mAutoDim == autoDim) return;
+        mAutoDim = autoDim;
+        applyLightsOut(true, false);
+    }
+
+    @Override
+    protected boolean isLightsOut(int mode) {
+        return super.isLightsOut(mode) || mAutoDim;
+    }
+
+    public LightBarTransitionsController getLightTransitionsController() {
+        return mLightTransitionsController;
+    }
+
+    @Override
+    protected void onTransition(int oldMode, int newMode, boolean animate) {
+        super.onTransition(oldMode, newMode, animate);
+        applyLightsOut(animate, false /*force*/);
+    }
+
+    private void applyLightsOut(boolean animate, boolean force) {
+        // apply to lights out
+        applyLightsOut(isLightsOut(getMode()), animate, force);
+    }
+
+    private void applyLightsOut(boolean lightsOut, boolean animate, boolean force) {
+        if (!force && lightsOut == mLightsOut) return;
+
+        mLightsOut = lightsOut;
+
+        final View navButtons = mView.getCurrentView().findViewById(R.id.nav_buttons);
+
+        // ok, everyone, stop it right there
+        navButtons.animate().cancel();
+
+        final float navButtonsAlpha = lightsOut ? 0.5f : 1f;
+
+        if (!animate) {
+            navButtons.setAlpha(navButtonsAlpha);
+        } else {
+            final int duration = lightsOut ? LIGHTS_OUT_DURATION : LIGHTS_IN_DURATION;
+            navButtons.animate()
+                .alpha(navButtonsAlpha)
+                .setDuration(duration)
+                .start();
+        }
+    }
+
+    public void reapplyDarkIntensity() {
+        applyDarkIntensity(mLightTransitionsController.getCurrentDarkIntensity());
+    }
+
+    public void applyDarkIntensity(float darkIntensity) {
+        SparseArray<ButtonDispatcher> buttonDispatchers = mView.getButtonDispatchers();
+        for (int i = buttonDispatchers.size() - 1; i >= 0; i--) {
+            buttonDispatchers.valueAt(i).setDarkIntensity(darkIntensity);
+        }
+    }
+
+    private final View.OnTouchListener mLightsOutListener = new View.OnTouchListener() {
+        @Override
+        public boolean onTouch(View v, MotionEvent ev) {
+            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+                // even though setting the systemUI visibility below will turn these views
+                // on, we need them to come up faster so that they can catch this motion
+                // event
+                applyLightsOut(false, false, false);
+
+                try {
+                    mBarService.setSystemUiVisibility(0, View.SYSTEM_UI_FLAG_LOW_PROFILE,
+                            "LightsOutListener");
+                } catch (android.os.RemoteException ex) {
+                }
+            }
+            return false;
+        }
+    };
+}
diff --git a/com/android/systemui/statusbar/phone/NavigationBarView.java b/com/android/systemui/statusbar/phone/NavigationBarView.java
new file mode 100644
index 0000000..9a7039a
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -0,0 +1,838 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.animation.LayoutTransition;
+import android.animation.LayoutTransition.TransitionListener;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.annotation.DrawableRes;
+import android.app.ActivityManager;
+import android.app.StatusBarManager;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.ContextThemeWrapper;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.FrameLayout;
+
+import com.android.settingslib.Utils;
+import com.android.systemui.Dependency;
+import com.android.systemui.DockedStackExistsListener;
+import com.android.systemui.R;
+import com.android.systemui.RecentsComponent;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.PluginManager;
+import com.android.systemui.plugins.statusbar.phone.NavGesture;
+import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper;
+import com.android.systemui.stackdivider.Divider;
+import com.android.systemui.statusbar.policy.DeadZone;
+import com.android.systemui.statusbar.policy.KeyButtonDrawable;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+public class NavigationBarView extends FrameLayout implements PluginListener<NavGesture> {
+    final static boolean DEBUG = false;
+    final static String TAG = "StatusBar/NavBarView";
+
+    // slippery nav bar when everything is disabled, e.g. during setup
+    final static boolean SLIPPERY_WHEN_DISABLED = true;
+
+    final static boolean ALTERNATE_CAR_MODE_UI = false;
+
+    final Display mDisplay;
+    View mCurrentView = null;
+    View[] mRotatedViews = new View[4];
+
+    boolean mVertical;
+    private int mCurrentRotation = -1;
+
+    boolean mShowMenu;
+    boolean mShowAccessibilityButton;
+    boolean mLongClickableAccessibilityButton;
+    int mDisabledFlags = 0;
+    int mNavigationIconHints = 0;
+
+    private KeyButtonDrawable mBackIcon, mBackLandIcon, mBackAltIcon, mBackAltLandIcon;
+    private KeyButtonDrawable mBackCarModeIcon, mBackLandCarModeIcon;
+    private KeyButtonDrawable mBackAltCarModeIcon, mBackAltLandCarModeIcon;
+    private KeyButtonDrawable mHomeDefaultIcon, mHomeCarModeIcon;
+    private KeyButtonDrawable mRecentIcon;
+    private KeyButtonDrawable mDockedIcon;
+    private KeyButtonDrawable mImeIcon;
+    private KeyButtonDrawable mMenuIcon;
+    private KeyButtonDrawable mAccessibilityIcon;
+
+    private GestureHelper mGestureHelper;
+    private DeadZone mDeadZone;
+    private final NavigationBarTransitions mBarTransitions;
+
+    // workaround for LayoutTransitions leaving the nav buttons in a weird state (bug 5549288)
+    final static boolean WORKAROUND_INVALID_LAYOUT = true;
+    final static int MSG_CHECK_INVALID_LAYOUT = 8686;
+
+    // performs manual animation in sync with layout transitions
+    private final NavTransitionListener mTransitionListener = new NavTransitionListener();
+
+    private OnVerticalChangedListener mOnVerticalChangedListener;
+    private boolean mLayoutTransitionsEnabled = true;
+    private boolean mWakeAndUnlocking;
+    private boolean mUseCarModeUi = false;
+    private boolean mInCarMode = false;
+    private boolean mDockedStackExists;
+
+    private final SparseArray<ButtonDispatcher> mButtonDispatchers = new SparseArray<>();
+    private Configuration mConfiguration;
+
+    private NavigationBarInflaterView mNavigationInflaterView;
+    private RecentsComponent mRecentsComponent;
+    private Divider mDivider;
+
+    private class NavTransitionListener implements TransitionListener {
+        private boolean mBackTransitioning;
+        private boolean mHomeAppearing;
+        private long mStartDelay;
+        private long mDuration;
+        private TimeInterpolator mInterpolator;
+
+        @Override
+        public void startTransition(LayoutTransition transition, ViewGroup container,
+                View view, int transitionType) {
+            if (view.getId() == R.id.back) {
+                mBackTransitioning = true;
+            } else if (view.getId() == R.id.home && transitionType == LayoutTransition.APPEARING) {
+                mHomeAppearing = true;
+                mStartDelay = transition.getStartDelay(transitionType);
+                mDuration = transition.getDuration(transitionType);
+                mInterpolator = transition.getInterpolator(transitionType);
+            }
+        }
+
+        @Override
+        public void endTransition(LayoutTransition transition, ViewGroup container,
+                View view, int transitionType) {
+            if (view.getId() == R.id.back) {
+                mBackTransitioning = false;
+            } else if (view.getId() == R.id.home && transitionType == LayoutTransition.APPEARING) {
+                mHomeAppearing = false;
+            }
+        }
+
+        public void onBackAltCleared() {
+            ButtonDispatcher backButton = getBackButton();
+
+            // When dismissing ime during unlock, force the back button to run the same appearance
+            // animation as home (if we catch this condition early enough).
+            if (!mBackTransitioning && backButton.getVisibility() == VISIBLE
+                    && mHomeAppearing && getHomeButton().getAlpha() == 0) {
+                getBackButton().setAlpha(0);
+                ValueAnimator a = ObjectAnimator.ofFloat(backButton, "alpha", 0, 1);
+                a.setStartDelay(mStartDelay);
+                a.setDuration(mDuration);
+                a.setInterpolator(mInterpolator);
+                a.start();
+            }
+        }
+    }
+
+    private final OnClickListener mImeSwitcherClickListener = new OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            mContext.getSystemService(InputMethodManager.class)
+                    .showInputMethodPicker(true /* showAuxiliarySubtypes */);
+        }
+    };
+
+    private class H extends Handler {
+        public void handleMessage(Message m) {
+            switch (m.what) {
+                case MSG_CHECK_INVALID_LAYOUT:
+                    final String how = "" + m.obj;
+                    final int w = getWidth();
+                    final int h = getHeight();
+                    final int vw = getCurrentView().getWidth();
+                    final int vh = getCurrentView().getHeight();
+
+                    if (h != vh || w != vw) {
+                        Log.w(TAG, String.format(
+                            "*** Invalid layout in navigation bar (%s this=%dx%d cur=%dx%d)",
+                            how, w, h, vw, vh));
+                        if (WORKAROUND_INVALID_LAYOUT) {
+                            requestLayout();
+                        }
+                    }
+                    break;
+            }
+        }
+    }
+
+    public NavigationBarView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        mDisplay = ((WindowManager) context.getSystemService(
+                Context.WINDOW_SERVICE)).getDefaultDisplay();
+
+        mVertical = false;
+        mShowMenu = false;
+
+        mShowAccessibilityButton = false;
+        mLongClickableAccessibilityButton = false;
+
+        mConfiguration = new Configuration();
+        mConfiguration.updateFrom(context.getResources().getConfiguration());
+        updateIcons(context, Configuration.EMPTY, mConfiguration);
+
+        mBarTransitions = new NavigationBarTransitions(this);
+
+        mButtonDispatchers.put(R.id.back, new ButtonDispatcher(R.id.back));
+        mButtonDispatchers.put(R.id.home, new ButtonDispatcher(R.id.home));
+        mButtonDispatchers.put(R.id.recent_apps, new ButtonDispatcher(R.id.recent_apps));
+        mButtonDispatchers.put(R.id.menu, new ButtonDispatcher(R.id.menu));
+        mButtonDispatchers.put(R.id.ime_switcher, new ButtonDispatcher(R.id.ime_switcher));
+        mButtonDispatchers.put(R.id.accessibility_button,
+                new ButtonDispatcher(R.id.accessibility_button));
+    }
+
+    public BarTransitions getBarTransitions() {
+        return mBarTransitions;
+    }
+
+    public LightBarTransitionsController getLightTransitionsController() {
+        return mBarTransitions.getLightTransitionsController();
+    }
+
+    public void setComponents(RecentsComponent recentsComponent, Divider divider) {
+        mRecentsComponent = recentsComponent;
+        mDivider = divider;
+        if (mGestureHelper instanceof NavigationBarGestureHelper) {
+            ((NavigationBarGestureHelper) mGestureHelper).setComponents(
+                    recentsComponent, divider, this);
+        }
+    }
+
+    public void setOnVerticalChangedListener(OnVerticalChangedListener onVerticalChangedListener) {
+        mOnVerticalChangedListener = onVerticalChangedListener;
+        notifyVerticalChangedListener(mVertical);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mGestureHelper.onTouchEvent(event)) {
+            return true;
+        }
+        return super.onTouchEvent(event);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        return mGestureHelper.onInterceptTouchEvent(event);
+    }
+
+    public void abortCurrentGesture() {
+        getHomeButton().abortCurrentGesture();
+    }
+
+    private H mHandler = new H();
+
+    public View getCurrentView() {
+        return mCurrentView;
+    }
+
+    public View[] getAllViews() {
+        return mRotatedViews;
+    }
+
+    public ButtonDispatcher getRecentsButton() {
+        return mButtonDispatchers.get(R.id.recent_apps);
+    }
+
+    public ButtonDispatcher getMenuButton() {
+        return mButtonDispatchers.get(R.id.menu);
+    }
+
+    public ButtonDispatcher getBackButton() {
+        return mButtonDispatchers.get(R.id.back);
+    }
+
+    public ButtonDispatcher getHomeButton() {
+        return mButtonDispatchers.get(R.id.home);
+    }
+
+    public ButtonDispatcher getImeSwitchButton() {
+        return mButtonDispatchers.get(R.id.ime_switcher);
+    }
+
+    public ButtonDispatcher getAccessibilityButton() {
+        return mButtonDispatchers.get(R.id.accessibility_button);
+    }
+
+    public SparseArray<ButtonDispatcher> getButtonDispatchers() {
+        return mButtonDispatchers;
+    }
+
+    private void updateCarModeIcons(Context ctx) {
+        mBackCarModeIcon = getDrawable(ctx,
+                R.drawable.ic_sysbar_back_carmode, R.drawable.ic_sysbar_back_carmode);
+        mBackLandCarModeIcon = mBackCarModeIcon;
+        mBackAltCarModeIcon = getDrawable(ctx,
+                R.drawable.ic_sysbar_back_ime_carmode, R.drawable.ic_sysbar_back_ime_carmode);
+        mBackAltLandCarModeIcon = mBackAltCarModeIcon;
+        mHomeCarModeIcon = getDrawable(ctx,
+                R.drawable.ic_sysbar_home_carmode, R.drawable.ic_sysbar_home_carmode);
+    }
+
+    private void updateIcons(Context ctx, Configuration oldConfig, Configuration newConfig) {
+        if (oldConfig.orientation != newConfig.orientation
+                || oldConfig.densityDpi != newConfig.densityDpi) {
+            mDockedIcon = getDrawable(ctx,
+                    R.drawable.ic_sysbar_docked, R.drawable.ic_sysbar_docked_dark);
+        }
+        if (oldConfig.densityDpi != newConfig.densityDpi
+                || oldConfig.getLayoutDirection() != newConfig.getLayoutDirection()) {
+            mBackIcon = getDrawable(ctx, R.drawable.ic_sysbar_back, R.drawable.ic_sysbar_back_dark);
+            mBackLandIcon = mBackIcon;
+            mBackAltIcon = getDrawable(ctx,
+                    R.drawable.ic_sysbar_back_ime, R.drawable.ic_sysbar_back_ime_dark);
+            mBackAltLandIcon = mBackAltIcon;
+
+            mHomeDefaultIcon = getDrawable(ctx,
+                    R.drawable.ic_sysbar_home, R.drawable.ic_sysbar_home_dark);
+            mRecentIcon = getDrawable(ctx,
+                    R.drawable.ic_sysbar_recent, R.drawable.ic_sysbar_recent_dark);
+            mMenuIcon = getDrawable(ctx, R.drawable.ic_sysbar_menu, R.drawable.ic_sysbar_menu_dark);
+            mAccessibilityIcon = getDrawable(ctx, R.drawable.ic_sysbar_accessibility_button,
+                    R.drawable.ic_sysbar_accessibility_button_dark);
+
+            int dualToneDarkTheme = Utils.getThemeAttr(ctx, R.attr.darkIconTheme);
+            int dualToneLightTheme = Utils.getThemeAttr(ctx, R.attr.lightIconTheme);
+            Context darkContext = new ContextThemeWrapper(ctx, dualToneDarkTheme);
+            Context lightContext = new ContextThemeWrapper(ctx, dualToneLightTheme);
+            mImeIcon = getDrawable(darkContext, lightContext,
+                    R.drawable.ic_ime_switcher_default, R.drawable.ic_ime_switcher_default);
+
+            if (ALTERNATE_CAR_MODE_UI) {
+                updateCarModeIcons(ctx);
+            }
+        }
+    }
+
+    private KeyButtonDrawable getDrawable(Context ctx, @DrawableRes int lightIcon,
+            @DrawableRes int darkIcon) {
+        return getDrawable(ctx, ctx, lightIcon, darkIcon);
+    }
+
+    private KeyButtonDrawable getDrawable(Context darkContext, Context lightContext,
+            @DrawableRes int lightIcon, @DrawableRes int darkIcon) {
+        return KeyButtonDrawable.create(lightContext.getDrawable(lightIcon),
+                darkContext.getDrawable(darkIcon));
+    }
+
+    @Override
+    public void setLayoutDirection(int layoutDirection) {
+        // Reload all the icons
+        updateIcons(getContext(), Configuration.EMPTY, mConfiguration);
+
+        super.setLayoutDirection(layoutDirection);
+    }
+
+    public void notifyScreenOn() {
+        setDisabledFlags(mDisabledFlags, true);
+    }
+
+    public void setNavigationIconHints(int hints) {
+        setNavigationIconHints(hints, false);
+    }
+
+    private KeyButtonDrawable getBackIconWithAlt(boolean carMode, boolean landscape) {
+        return landscape
+                ? carMode ? mBackAltLandCarModeIcon : mBackAltLandIcon
+                : carMode ? mBackAltCarModeIcon : mBackAltIcon;
+    }
+
+    private KeyButtonDrawable getBackIcon(boolean carMode, boolean landscape) {
+        return landscape
+                ? carMode ? mBackLandCarModeIcon : mBackLandIcon
+                : carMode ? mBackCarModeIcon : mBackIcon;
+    }
+
+    public void setNavigationIconHints(int hints, boolean force) {
+        if (!force && hints == mNavigationIconHints) return;
+        final boolean backAlt = (hints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0;
+        if ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0 && !backAlt) {
+            mTransitionListener.onBackAltCleared();
+        }
+        if (DEBUG) {
+            android.widget.Toast.makeText(getContext(),
+                "Navigation icon hints = " + hints,
+                500).show();
+        }
+
+        mNavigationIconHints = hints;
+
+        // We have to replace or restore the back and home button icons when exiting or entering
+        // carmode, respectively. Recents are not available in CarMode in nav bar so change
+        // to recent icon is not required.
+        KeyButtonDrawable backIcon = (backAlt)
+                ? getBackIconWithAlt(mUseCarModeUi, mVertical)
+                : getBackIcon(mUseCarModeUi, mVertical);
+
+        getBackButton().setImageDrawable(backIcon);
+
+        updateRecentsIcon();
+
+        if (mUseCarModeUi) {
+            getHomeButton().setImageDrawable(mHomeCarModeIcon);
+        } else {
+            getHomeButton().setImageDrawable(mHomeDefaultIcon);
+        }
+
+        // The Accessibility button always overrides the appearance of the IME switcher
+        final boolean showImeButton =
+                !mShowAccessibilityButton && ((hints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN)
+                        != 0);
+        getImeSwitchButton().setVisibility(showImeButton ? View.VISIBLE : View.INVISIBLE);
+        getImeSwitchButton().setImageDrawable(mImeIcon);
+
+        // Update menu button in case the IME state has changed.
+        setMenuVisibility(mShowMenu, true);
+        getMenuButton().setImageDrawable(mMenuIcon);
+
+        setAccessibilityButtonState(mShowAccessibilityButton, mLongClickableAccessibilityButton);
+        getAccessibilityButton().setImageDrawable(mAccessibilityIcon);
+
+        setDisabledFlags(mDisabledFlags, true);
+
+        mBarTransitions.reapplyDarkIntensity();
+    }
+
+    public void setDisabledFlags(int disabledFlags) {
+        setDisabledFlags(disabledFlags, false);
+    }
+
+    public void setDisabledFlags(int disabledFlags, boolean force) {
+        if (!force && mDisabledFlags == disabledFlags) return;
+
+        mDisabledFlags = disabledFlags;
+
+        final boolean disableHome = ((disabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0);
+
+        // Always disable recents when alternate car mode UI is active.
+        boolean disableRecent = mUseCarModeUi
+                        || ((disabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0);
+        final boolean disableBack = ((disabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0)
+                && ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) == 0);
+
+        ViewGroup navButtons = (ViewGroup) getCurrentView().findViewById(R.id.nav_buttons);
+        if (navButtons != null) {
+            LayoutTransition lt = navButtons.getLayoutTransition();
+            if (lt != null) {
+                if (!lt.getTransitionListeners().contains(mTransitionListener)) {
+                    lt.addTransitionListener(mTransitionListener);
+                }
+            }
+        }
+        if (inLockTask() && disableRecent && !disableHome) {
+            // Don't hide recents when in lock task, it is used for exiting.
+            // Unless home is hidden, then in DPM locked mode and no exit available.
+            disableRecent = false;
+        }
+
+        getBackButton().setVisibility(disableBack      ? View.INVISIBLE : View.VISIBLE);
+        getHomeButton().setVisibility(disableHome      ? View.INVISIBLE : View.VISIBLE);
+        getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE);
+    }
+
+    private boolean inLockTask() {
+        try {
+            return ActivityManager.getService().isInLockTaskMode();
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public void setLayoutTransitionsEnabled(boolean enabled) {
+        mLayoutTransitionsEnabled = enabled;
+        updateLayoutTransitionsEnabled();
+    }
+
+    public void setWakeAndUnlocking(boolean wakeAndUnlocking) {
+        setUseFadingAnimations(wakeAndUnlocking);
+        mWakeAndUnlocking = wakeAndUnlocking;
+        updateLayoutTransitionsEnabled();
+    }
+
+    private void updateLayoutTransitionsEnabled() {
+        boolean enabled = !mWakeAndUnlocking && mLayoutTransitionsEnabled;
+        ViewGroup navButtons = (ViewGroup) getCurrentView().findViewById(R.id.nav_buttons);
+        LayoutTransition lt = navButtons.getLayoutTransition();
+        if (lt != null) {
+            if (enabled) {
+                lt.enableTransitionType(LayoutTransition.APPEARING);
+                lt.enableTransitionType(LayoutTransition.DISAPPEARING);
+                lt.enableTransitionType(LayoutTransition.CHANGE_APPEARING);
+                lt.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
+            } else {
+                lt.disableTransitionType(LayoutTransition.APPEARING);
+                lt.disableTransitionType(LayoutTransition.DISAPPEARING);
+                lt.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
+                lt.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
+            }
+        }
+    }
+
+    private void setUseFadingAnimations(boolean useFadingAnimations) {
+        WindowManager.LayoutParams lp = (WindowManager.LayoutParams) ((ViewGroup) getParent())
+                .getLayoutParams();
+        if (lp != null) {
+            boolean old = lp.windowAnimations != 0;
+            if (!old && useFadingAnimations) {
+                lp.windowAnimations = R.style.Animation_NavigationBarFadeIn;
+            } else if (old && !useFadingAnimations) {
+                lp.windowAnimations = 0;
+            } else {
+                return;
+            }
+            WindowManager wm = (WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE);
+            wm.updateViewLayout((View) getParent(), lp);
+        }
+    }
+
+    public void setMenuVisibility(final boolean show) {
+        setMenuVisibility(show, false);
+    }
+
+    public void setMenuVisibility(final boolean show, final boolean force) {
+        if (!force && mShowMenu == show) return;
+
+        mShowMenu = show;
+
+        // Only show Menu if IME switcher and Accessibility button not shown.
+        final boolean shouldShow = mShowMenu && !mShowAccessibilityButton &&
+                ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) == 0);
+
+        getMenuButton().setVisibility(shouldShow ? View.VISIBLE : View.INVISIBLE);
+    }
+
+    public void setAccessibilityButtonState(final boolean visible, final boolean longClickable) {
+        mShowAccessibilityButton = visible;
+        mLongClickableAccessibilityButton = longClickable;
+        if (visible) {
+            // Accessibility button overrides Menu and IME switcher buttons.
+            setMenuVisibility(false, true);
+            getImeSwitchButton().setVisibility(View.INVISIBLE);
+        }
+
+        getAccessibilityButton().setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+        getAccessibilityButton().setLongClickable(longClickable);
+    }
+
+    @Override
+    public void onFinishInflate() {
+        mNavigationInflaterView = (NavigationBarInflaterView) findViewById(
+                R.id.navigation_inflater);
+        mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers);
+
+        getImeSwitchButton().setOnClickListener(mImeSwitcherClickListener);
+
+        DockedStackExistsListener.register(mDockedListener);
+        updateRotatedViews();
+    }
+
+    private void updateRotatedViews() {
+        mRotatedViews[Surface.ROTATION_0] =
+                mRotatedViews[Surface.ROTATION_180] = findViewById(R.id.rot0);
+        mRotatedViews[Surface.ROTATION_270] =
+                mRotatedViews[Surface.ROTATION_90] = findViewById(R.id.rot90);
+
+        updateCurrentView();
+    }
+
+    public boolean needsReorient(int rotation) {
+        return mCurrentRotation != rotation;
+    }
+
+    private void updateCurrentView() {
+        final int rot = mDisplay.getRotation();
+        for (int i=0; i<4; i++) {
+            mRotatedViews[i].setVisibility(View.GONE);
+        }
+        mCurrentView = mRotatedViews[rot];
+        mCurrentView.setVisibility(View.VISIBLE);
+        mNavigationInflaterView.setAlternativeOrder(rot == Surface.ROTATION_90);
+        for (int i = 0; i < mButtonDispatchers.size(); i++) {
+            mButtonDispatchers.valueAt(i).setCurrentView(mCurrentView);
+        }
+        updateLayoutTransitionsEnabled();
+        mCurrentRotation = rot;
+    }
+
+    private void updateRecentsIcon() {
+        getRecentsButton().setImageDrawable(mDockedStackExists ? mDockedIcon : mRecentIcon);
+        mBarTransitions.reapplyDarkIntensity();
+    }
+
+    public boolean isVertical() {
+        return mVertical;
+    }
+
+    public void reorient() {
+        updateCurrentView();
+
+        mDeadZone = (DeadZone) mCurrentView.findViewById(R.id.deadzone);
+
+        ((NavigationBarFrame) getRootView()).setDeadZone(mDeadZone);
+        mDeadZone.setDisplayRotation(mCurrentRotation);
+
+        // force the low profile & disabled states into compliance
+        mBarTransitions.init();
+        setDisabledFlags(mDisabledFlags, true /* force */);
+        setMenuVisibility(mShowMenu, true /* force */);
+
+        if (DEBUG) {
+            Log.d(TAG, "reorient(): rot=" + mCurrentRotation);
+        }
+
+        updateTaskSwitchHelper();
+        setNavigationIconHints(mNavigationIconHints, true);
+
+        getHomeButton().setVertical(mVertical);
+    }
+
+    private void updateTaskSwitchHelper() {
+        if (mGestureHelper == null) return;
+        boolean isRtl = (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
+        mGestureHelper.setBarState(mVertical, isRtl);
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        if (DEBUG) Log.d(TAG, String.format(
+                    "onSizeChanged: (%dx%d) old: (%dx%d)", w, h, oldw, oldh));
+
+        final boolean newVertical = w > 0 && h > w;
+        if (newVertical != mVertical) {
+            mVertical = newVertical;
+            //Log.v(TAG, String.format("onSizeChanged: h=%d, w=%d, vert=%s", h, w, mVertical?"y":"n"));
+            reorient();
+            notifyVerticalChangedListener(newVertical);
+        }
+
+        postCheckForInvalidLayout("sizeChanged");
+        super.onSizeChanged(w, h, oldw, oldh);
+    }
+
+    private void notifyVerticalChangedListener(boolean newVertical) {
+        if (mOnVerticalChangedListener != null) {
+            mOnVerticalChangedListener.onVerticalChanged(newVertical);
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        boolean uiCarModeChanged = updateCarMode(newConfig);
+        updateTaskSwitchHelper();
+        updateIcons(getContext(), mConfiguration, newConfig);
+        updateRecentsIcon();
+        if (uiCarModeChanged || mConfiguration.densityDpi != newConfig.densityDpi
+                || mConfiguration.getLayoutDirection() != newConfig.getLayoutDirection()) {
+            // If car mode or density changes, we need to reset the icons.
+            setNavigationIconHints(mNavigationIconHints, true);
+        }
+        mConfiguration.updateFrom(newConfig);
+    }
+
+    /**
+     * If the configuration changed, update the carmode and return that it was updated.
+     */
+    private boolean updateCarMode(Configuration newConfig) {
+        boolean uiCarModeChanged = false;
+        if (newConfig != null) {
+            int uiMode = newConfig.uiMode & Configuration.UI_MODE_TYPE_MASK;
+            final boolean isCarMode = (uiMode == Configuration.UI_MODE_TYPE_CAR);
+
+            if (isCarMode != mInCarMode) {
+                mInCarMode = isCarMode;
+                if (ALTERNATE_CAR_MODE_UI) {
+                    mUseCarModeUi = isCarMode;
+                    uiCarModeChanged = true;
+                } else {
+                    // Don't use car mode behavior if ALTERNATE_CAR_MODE_UI not set.
+                    mUseCarModeUi = false;
+                }
+            }
+        }
+        return uiCarModeChanged;
+    }
+
+    /*
+    @Override
+    protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
+        if (DEBUG) Log.d(TAG, String.format(
+                    "onLayout: %s (%d,%d,%d,%d)",
+                    changed?"changed":"notchanged", left, top, right, bottom));
+        super.onLayout(changed, left, top, right, bottom);
+    }
+
+    // uncomment this for extra defensiveness in WORKAROUND_INVALID_LAYOUT situations: if all else
+    // fails, any touch on the display will fix the layout.
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (DEBUG) Log.d(TAG, "onInterceptTouchEvent: " + ev.toString());
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            postCheckForInvalidLayout("touch");
+        }
+        return super.onInterceptTouchEvent(ev);
+    }
+    */
+
+
+    private String getResourceName(int resId) {
+        if (resId != 0) {
+            final android.content.res.Resources res = getContext().getResources();
+            try {
+                return res.getResourceName(resId);
+            } catch (android.content.res.Resources.NotFoundException ex) {
+                return "(unknown)";
+            }
+        } else {
+            return "(null)";
+        }
+    }
+
+    private void postCheckForInvalidLayout(final String how) {
+        mHandler.obtainMessage(MSG_CHECK_INVALID_LAYOUT, 0, 0, how).sendToTarget();
+    }
+
+    private static String visibilityToString(int vis) {
+        switch (vis) {
+            case View.INVISIBLE:
+                return "INVISIBLE";
+            case View.GONE:
+                return "GONE";
+            default:
+                return "VISIBLE";
+        }
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        reorient();
+        onPluginDisconnected(null); // Create default gesture helper
+        Dependency.get(PluginManager.class).addPluginListener(this,
+                NavGesture.class, false /* Only one */);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        Dependency.get(PluginManager.class).removePluginListener(this);
+        if (mGestureHelper != null) {
+            mGestureHelper.destroy();
+        }
+    }
+
+    @Override
+    public void onPluginConnected(NavGesture plugin, Context context) {
+        mGestureHelper = plugin.getGestureHelper();
+        updateTaskSwitchHelper();
+    }
+
+    @Override
+    public void onPluginDisconnected(NavGesture plugin) {
+        NavigationBarGestureHelper defaultHelper = new NavigationBarGestureHelper(getContext());
+        defaultHelper.setComponents(mRecentsComponent, mDivider, this);
+        if (mGestureHelper != null) {
+            mGestureHelper.destroy();
+        }
+        mGestureHelper = defaultHelper;
+        updateTaskSwitchHelper();
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("NavigationBarView {");
+        final Rect r = new Rect();
+        final Point size = new Point();
+        mDisplay.getRealSize(size);
+
+        pw.println(String.format("      this: " + StatusBar.viewInfo(this)
+                        + " " + visibilityToString(getVisibility())));
+
+        getWindowVisibleDisplayFrame(r);
+        final boolean offscreen = r.right > size.x || r.bottom > size.y;
+        pw.println("      window: "
+                + r.toShortString()
+                + " " + visibilityToString(getWindowVisibility())
+                + (offscreen ? " OFFSCREEN!" : ""));
+
+        pw.println(String.format("      mCurrentView: id=%s (%dx%d) %s",
+                        getResourceName(getCurrentView().getId()),
+                        getCurrentView().getWidth(), getCurrentView().getHeight(),
+                        visibilityToString(getCurrentView().getVisibility())));
+
+        pw.println(String.format("      disabled=0x%08x vertical=%s menu=%s",
+                        mDisabledFlags,
+                        mVertical ? "true" : "false",
+                        mShowMenu ? "true" : "false"));
+
+        dumpButton(pw, "back", getBackButton());
+        dumpButton(pw, "home", getHomeButton());
+        dumpButton(pw, "rcnt", getRecentsButton());
+        dumpButton(pw, "menu", getMenuButton());
+        dumpButton(pw, "a11y", getAccessibilityButton());
+
+        pw.println("    }");
+    }
+
+    private static void dumpButton(PrintWriter pw, String caption, ButtonDispatcher button) {
+        pw.print("      " + caption + ": ");
+        if (button == null) {
+            pw.print("null");
+        } else {
+            pw.print(visibilityToString(button.getVisibility())
+                    + " alpha=" + button.getAlpha()
+                    );
+        }
+        pw.println();
+    }
+
+    public interface OnVerticalChangedListener {
+        void onVerticalChanged(boolean isVertical);
+    }
+
+    private final Consumer<Boolean> mDockedListener = exists -> mHandler.post(() -> {
+        mDockedStackExists = exists;
+        updateRecentsIcon();
+    });
+}
diff --git a/com/android/systemui/statusbar/phone/NearestTouchFrame.java b/com/android/systemui/statusbar/phone/NearestTouchFrame.java
new file mode 100644
index 0000000..1452e0c
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NearestTouchFrame.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2017 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.phone;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.support.annotation.VisibleForTesting;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Pair;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.systemui.R;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+
+/**
+ * Redirects touches that aren't handled by any child view to the nearest
+ * clickable child. Only takes effect on <sw600dp.
+ */
+public class NearestTouchFrame extends FrameLayout {
+
+    private final ArrayList<View> mClickableChildren = new ArrayList<>();
+    private final boolean mIsActive;
+    private final int[] mTmpInt = new int[2];
+    private final int[] mOffset = new int[2];
+    private View mTouchingChild;
+
+    public NearestTouchFrame(Context context, AttributeSet attrs) {
+        this(context, attrs, context.getResources().getConfiguration());
+    }
+
+    @VisibleForTesting
+    NearestTouchFrame(Context context, AttributeSet attrs, Configuration c) {
+        super(context, attrs);
+        mIsActive = c.smallestScreenWidthDp < 600;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        mClickableChildren.clear();
+        addClickableChildren(this);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        getLocationInWindow(mOffset);
+    }
+
+    private void addClickableChildren(ViewGroup group) {
+        final int N = group.getChildCount();
+        for (int i = 0; i < N; i++) {
+            View child = group.getChildAt(i);
+            if (child.isClickable()) {
+                mClickableChildren.add(child);
+            } else if (child instanceof ViewGroup) {
+                addClickableChildren((ViewGroup) child);
+            }
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mIsActive) {
+            if (event.getAction() == MotionEvent.ACTION_DOWN) {
+                mTouchingChild = findNearestChild(event);
+            }
+            if (mTouchingChild != null) {
+                event.offsetLocation(mTouchingChild.getWidth() / 2 - event.getX(),
+                        mTouchingChild.getHeight() / 2 - event.getY());
+                return mTouchingChild.getVisibility() == VISIBLE
+                        && mTouchingChild.dispatchTouchEvent(event);
+            }
+        }
+        return super.onTouchEvent(event);
+    }
+
+    private View findNearestChild(MotionEvent event) {
+        return mClickableChildren.stream().map(v -> new Pair<>(distance(v, event), v))
+                .min(Comparator.comparingInt(f -> f.first)).get().second;
+    }
+
+    private int distance(View v, MotionEvent event) {
+        v.getLocationInWindow(mTmpInt);
+        int left = mTmpInt[0] - mOffset[0];
+        int top = mTmpInt[1] - mOffset[1];
+        int right = left + v.getWidth();
+        int bottom = top + v.getHeight();
+
+        int x = Math.min(Math.abs(left - (int) event.getX()),
+                Math.abs((int) event.getX() - right));
+        int y = Math.min(Math.abs(top - (int) event.getY()),
+                Math.abs((int) event.getY() - bottom));
+
+        return Math.max(x, y);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/NoisyVelocityTracker.java b/com/android/systemui/statusbar/phone/NoisyVelocityTracker.java
new file mode 100644
index 0000000..214dda2
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NoisyVelocityTracker.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.util.Log;
+import android.util.Pools;
+import android.view.MotionEvent;
+
+import java.util.ArrayDeque;
+import java.util.Iterator;
+
+/**
+ * A very simple low-pass velocity filter for motion events for noisy touch screens.
+ */
+public class NoisyVelocityTracker implements VelocityTrackerInterface {
+
+    private static final Pools.SynchronizedPool<NoisyVelocityTracker> sNoisyPool =
+            new Pools.SynchronizedPool<>(2);
+
+    private static final float DECAY = 0.75f;
+    private static final boolean DEBUG = false;
+
+    private final int MAX_EVENTS = 8;
+    private ArrayDeque<MotionEventCopy> mEventBuf = new ArrayDeque<MotionEventCopy>(MAX_EVENTS);
+    private float mVX, mVY = 0;
+
+    private static class MotionEventCopy {
+        public MotionEventCopy(float x2, float y2, long eventTime) {
+            this.x = x2;
+            this.y = y2;
+            this.t = eventTime;
+        }
+        float x, y;
+        long t;
+    }
+
+    public static NoisyVelocityTracker obtain() {
+        NoisyVelocityTracker instance = sNoisyPool.acquire();
+        return (instance != null) ? instance : new NoisyVelocityTracker();
+    }
+
+    private NoisyVelocityTracker() {
+    }
+
+    public void addMovement(MotionEvent event) {
+        if (mEventBuf.size() == MAX_EVENTS) {
+            mEventBuf.remove();
+        }
+        mEventBuf.add(new MotionEventCopy(event.getX(), event.getY(), event.getEventTime()));
+    }
+
+    public void computeCurrentVelocity(int units) {
+        if (NoisyVelocityTracker.DEBUG) {
+            Log.v("FlingTracker", "computing velocities for " + mEventBuf.size() + " events");
+        }
+        mVX = mVY = 0;
+        MotionEventCopy last = null;
+        int i = 0;
+        float totalweight = 0f;
+        float weight = 10f;
+        for (final Iterator<MotionEventCopy> iter = mEventBuf.iterator();
+                iter.hasNext();) {
+            final MotionEventCopy event = iter.next();
+            if (last != null) {
+                final float dt = (float) (event.t - last.t) / units;
+                final float dx = (event.x - last.x);
+                final float dy = (event.y - last.y);
+                if (NoisyVelocityTracker.DEBUG) {
+                    Log.v("FlingTracker", String.format(
+                            "   [%d] (t=%d %.1f,%.1f) dx=%.1f dy=%.1f dt=%f vx=%.1f vy=%.1f",
+                            i, event.t, event.x, event.y,
+                            dx, dy, dt,
+                            (dx/dt),
+                            (dy/dt)
+                    ));
+                }
+                if (event.t == last.t) {
+                    // Really not sure what to do with events that happened at the same time,
+                    // so we'll skip subsequent events.
+                    continue;
+                }
+                mVX += weight * dx / dt;
+                mVY += weight * dy / dt;
+                totalweight += weight;
+                weight *= DECAY;
+            }
+            last = event;
+            i++;
+        }
+        if (totalweight > 0) {
+            mVX /= totalweight;
+            mVY /= totalweight;
+        } else {
+            // so as not to contaminate the velocities with NaN
+            mVX = mVY = 0;
+        }
+
+        if (NoisyVelocityTracker.DEBUG) {
+            Log.v("FlingTracker", "computed: vx=" + mVX + " vy=" + mVY);
+        }
+    }
+
+    public float getXVelocity() {
+        if (Float.isNaN(mVX) || Float.isInfinite(mVX)) {
+            mVX = 0;
+        }
+        return mVX;
+    }
+
+    public float getYVelocity() {
+        if (Float.isNaN(mVY) || Float.isInfinite(mVX)) {
+            mVY = 0;
+        }
+        return mVY;
+    }
+
+    public void recycle() {
+        mEventBuf.clear();
+        sNoisyPool.release(this);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/NotificationGroupManager.java b/com/android/systemui/statusbar/phone/NotificationGroupManager.java
new file mode 100644
index 0000000..b75c7e0
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NotificationGroupManager.java
@@ -0,0 +1,530 @@
+/*
+ * 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.phone;
+
+import android.service.notification.StatusBarNotification;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * A class to handle notifications and their corresponding groups.
+ */
+public class NotificationGroupManager implements OnHeadsUpChangedListener {
+
+    private static final String TAG = "NotificationGroupManager";
+    private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>();
+    private OnGroupChangeListener mListener;
+    private int mBarState = -1;
+    private HashMap<String, StatusBarNotification> mIsolatedEntries = new HashMap<>();
+    private HeadsUpManager mHeadsUpManager;
+    private boolean mIsUpdatingUnchangedGroup;
+
+    public void setOnGroupChangeListener(OnGroupChangeListener listener) {
+        mListener = listener;
+    }
+
+    public boolean isGroupExpanded(StatusBarNotification sbn) {
+        NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
+        if (group == null) {
+            return false;
+        }
+        return group.expanded;
+    }
+
+    public void setGroupExpanded(StatusBarNotification sbn, boolean expanded) {
+        NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
+        if (group == null) {
+            return;
+        }
+        setGroupExpanded(group, expanded);
+    }
+
+    private void setGroupExpanded(NotificationGroup group, boolean expanded) {
+        group.expanded = expanded;
+        if (group.summary != null) {
+            mListener.onGroupExpansionChanged(group.summary.row, expanded);
+        }
+    }
+
+    public void onEntryRemoved(NotificationData.Entry removed) {
+        onEntryRemovedInternal(removed, removed.notification);
+        mIsolatedEntries.remove(removed.key);
+    }
+
+    /**
+     * An entry was removed.
+     *
+     * @param removed the removed entry
+     * @param sbn the notification the entry has, which doesn't need to be the same as it's internal
+     *            notification
+     */
+    private void onEntryRemovedInternal(NotificationData.Entry removed,
+            final StatusBarNotification sbn) {
+        String groupKey = getGroupKey(sbn);
+        final NotificationGroup group = mGroupMap.get(groupKey);
+        if (group == null) {
+            // When an app posts 2 different notifications as summary of the same group, then a
+            // cancellation of the first notification removes this group.
+            // This situation is not supported and we will not allow such notifications anymore in
+            // the close future. See b/23676310 for reference.
+            return;
+        }
+        if (isGroupChild(sbn)) {
+            group.children.remove(removed.key);
+        } else {
+            group.summary = null;
+        }
+        updateSuppression(group);
+        if (group.children.isEmpty()) {
+            if (group.summary == null) {
+                mGroupMap.remove(groupKey);
+            }
+        }
+    }
+
+    public void onEntryAdded(final NotificationData.Entry added) {
+        if (added.row.isRemoved()) {
+            added.setDebugThrowable(new Throwable());
+        }
+        final StatusBarNotification sbn = added.notification;
+        boolean isGroupChild = isGroupChild(sbn);
+        String groupKey = getGroupKey(sbn);
+        NotificationGroup group = mGroupMap.get(groupKey);
+        if (group == null) {
+            group = new NotificationGroup();
+            mGroupMap.put(groupKey, group);
+        }
+        if (isGroupChild) {
+            NotificationData.Entry existing = group.children.get(added.key);
+            if (existing != null && existing != added) {
+                Throwable existingThrowable = existing.getDebugThrowable();
+                Log.wtf(TAG, "Inconsistent entries found with the same key " + added.key
+                        + "existing removed: " + existing.row.isRemoved()
+                        + (existingThrowable != null
+                                ? Log.getStackTraceString(existingThrowable) + "\n": "")
+                        + " added removed" + added.row.isRemoved()
+                        , new Throwable());
+            }
+            group.children.put(added.key, added);
+            updateSuppression(group);
+        } else {
+            group.summary = added;
+            group.expanded = added.row.areChildrenExpanded();
+            updateSuppression(group);
+            if (!group.children.isEmpty()) {
+                ArrayList<NotificationData.Entry> childrenCopy
+                        = new ArrayList<>(group.children.values());
+                for (NotificationData.Entry child : childrenCopy) {
+                    onEntryBecomingChild(child);
+                }
+                mListener.onGroupCreatedFromChildren(group);
+            }
+        }
+    }
+
+    private void onEntryBecomingChild(NotificationData.Entry entry) {
+        if (entry.row.isHeadsUp()) {
+            onHeadsUpStateChanged(entry, true);
+        }
+    }
+
+    private void updateSuppression(NotificationGroup group) {
+        if (group == null) {
+            return;
+        }
+        boolean prevSuppressed = group.suppressed;
+        group.suppressed = group.summary != null && !group.expanded
+                && (group.children.size() == 1
+                || (group.children.size() == 0
+                        && group.summary.notification.getNotification().isGroupSummary()
+                        && hasIsolatedChildren(group)));
+        if (prevSuppressed != group.suppressed) {
+            if (group.suppressed) {
+                handleSuppressedSummaryHeadsUpped(group.summary);
+            }
+            if (!mIsUpdatingUnchangedGroup) {
+                mListener.onGroupsChanged();
+            }
+        }
+    }
+
+    private boolean hasIsolatedChildren(NotificationGroup group) {
+        return getNumberOfIsolatedChildren(group.summary.notification.getGroupKey()) != 0;
+    }
+
+    private int getNumberOfIsolatedChildren(String groupKey) {
+        int count = 0;
+        for (StatusBarNotification sbn : mIsolatedEntries.values()) {
+            if (sbn.getGroupKey().equals(groupKey) && isIsolated(sbn)) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    private NotificationData.Entry getIsolatedChild(String groupKey) {
+        for (StatusBarNotification sbn : mIsolatedEntries.values()) {
+            if (sbn.getGroupKey().equals(groupKey) && isIsolated(sbn)) {
+                return mGroupMap.get(sbn.getKey()).summary;
+            }
+        }
+        return null;
+    }
+
+    public void onEntryUpdated(NotificationData.Entry entry,
+            StatusBarNotification oldNotification) {
+        String oldKey = oldNotification.getGroupKey();
+        String newKey = entry.notification.getGroupKey();
+        boolean groupKeysChanged = !oldKey.equals(newKey);
+        boolean wasGroupChild = isGroupChild(oldNotification);
+        boolean isGroupChild = isGroupChild(entry.notification);
+        mIsUpdatingUnchangedGroup = !groupKeysChanged && wasGroupChild == isGroupChild;
+        if (mGroupMap.get(getGroupKey(oldNotification)) != null) {
+            onEntryRemovedInternal(entry, oldNotification);
+        }
+        onEntryAdded(entry);
+        mIsUpdatingUnchangedGroup = false;
+        if (isIsolated(entry.notification)) {
+            mIsolatedEntries.put(entry.key, entry.notification);
+            if (groupKeysChanged) {
+                updateSuppression(mGroupMap.get(oldKey));
+                updateSuppression(mGroupMap.get(newKey));
+            }
+        } else if (!wasGroupChild && isGroupChild) {
+            onEntryBecomingChild(entry);
+        }
+    }
+
+    public boolean isSummaryOfSuppressedGroup(StatusBarNotification sbn) {
+        return isGroupSuppressed(getGroupKey(sbn)) && sbn.getNotification().isGroupSummary();
+    }
+
+    private boolean isOnlyChild(StatusBarNotification sbn) {
+        return !sbn.getNotification().isGroupSummary()
+                && getTotalNumberOfChildren(sbn) == 1;
+    }
+
+    public boolean isOnlyChildInGroup(StatusBarNotification sbn) {
+        if (!isOnlyChild(sbn)) {
+            return false;
+        }
+        ExpandableNotificationRow logicalGroupSummary = getLogicalGroupSummary(sbn);
+        return logicalGroupSummary != null
+                && !logicalGroupSummary.getStatusBarNotification().equals(sbn);
+    }
+
+    private int getTotalNumberOfChildren(StatusBarNotification sbn) {
+        int isolatedChildren = getNumberOfIsolatedChildren(sbn.getGroupKey());
+        NotificationGroup group = mGroupMap.get(sbn.getGroupKey());
+        int realChildren = group != null ? group.children.size() : 0;
+        return isolatedChildren + realChildren;
+    }
+
+    private boolean isGroupSuppressed(String groupKey) {
+        NotificationGroup group = mGroupMap.get(groupKey);
+        return group != null && group.suppressed;
+    }
+
+    public void setStatusBarState(int newState) {
+        if (mBarState == newState) {
+            return;
+        }
+        mBarState = newState;
+        if (mBarState == StatusBarState.KEYGUARD) {
+            collapseAllGroups();
+        }
+    }
+
+    public void collapseAllGroups() {
+        // Because notifications can become isolated when the group becomes suppressed it can
+        // lead to concurrent modifications while looping. We need to make a copy.
+        ArrayList<NotificationGroup> groupCopy = new ArrayList<>(mGroupMap.values());
+        int size = groupCopy.size();
+        for (int i = 0; i < size; i++) {
+            NotificationGroup group =  groupCopy.get(i);
+            if (group.expanded) {
+                setGroupExpanded(group, false);
+            }
+            updateSuppression(group);
+        }
+    }
+
+    /**
+     * @return whether a given notification is a child in a group which has a summary
+     */
+    public boolean isChildInGroupWithSummary(StatusBarNotification sbn) {
+        if (!isGroupChild(sbn)) {
+            return false;
+        }
+        NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
+        if (group == null || group.summary == null || group.suppressed) {
+            return false;
+        }
+        if (group.children.isEmpty()) {
+            // If the suppression of a group changes because the last child was removed, this can
+            // still be called temporarily because the child hasn't been fully removed yet. Let's
+            // make sure we still return false in that case.
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * @return whether a given notification is a summary in a group which has children
+     */
+    public boolean isSummaryOfGroup(StatusBarNotification sbn) {
+        if (!isGroupSummary(sbn)) {
+            return false;
+        }
+        NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
+        if (group == null) {
+            return false;
+        }
+        return !group.children.isEmpty();
+    }
+
+    /**
+     * Get the summary of a specified status bar notification. For isolated notification this return
+     * itself.
+     */
+    public ExpandableNotificationRow getGroupSummary(StatusBarNotification sbn) {
+        return getGroupSummary(getGroupKey(sbn));
+    }
+
+    /**
+     * Similar to {@link #getGroupSummary(StatusBarNotification)} but doesn't get the visual summary
+     * but the logical summary, i.e when a child is isolated, it still returns the summary as if
+     * it wasn't isolated.
+     */
+    public ExpandableNotificationRow getLogicalGroupSummary(
+            StatusBarNotification sbn) {
+        return getGroupSummary(sbn.getGroupKey());
+    }
+
+    @Nullable
+    private ExpandableNotificationRow getGroupSummary(String groupKey) {
+        NotificationGroup group = mGroupMap.get(groupKey);
+        return group == null ? null
+                : group.summary == null ? null
+                        : group.summary.row;
+    }
+
+    /** @return group expansion state after toggling. */
+    public boolean toggleGroupExpansion(StatusBarNotification sbn) {
+        NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
+        if (group == null) {
+            return false;
+        }
+        setGroupExpanded(group, !group.expanded);
+        return group.expanded;
+    }
+
+    private boolean isIsolated(StatusBarNotification sbn) {
+        return mIsolatedEntries.containsKey(sbn.getKey());
+    }
+
+    private boolean isGroupSummary(StatusBarNotification sbn) {
+        if (isIsolated(sbn)) {
+            return true;
+        }
+        return sbn.getNotification().isGroupSummary();
+    }
+
+    private boolean isGroupChild(StatusBarNotification sbn) {
+        if (isIsolated(sbn)) {
+            return false;
+        }
+        return sbn.isGroup() && !sbn.getNotification().isGroupSummary();
+    }
+
+    private String getGroupKey(StatusBarNotification sbn) {
+        if (isIsolated(sbn)) {
+            return sbn.getKey();
+        }
+        return sbn.getGroupKey();
+    }
+
+    @Override
+    public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {
+    }
+
+    @Override
+    public void onHeadsUpPinned(ExpandableNotificationRow headsUp) {
+    }
+
+    @Override
+    public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {
+    }
+
+    @Override
+    public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
+        final StatusBarNotification sbn = entry.notification;
+        if (entry.row.isHeadsUp()) {
+            if (shouldIsolate(sbn)) {
+                // We will be isolated now, so lets update the groups
+                onEntryRemovedInternal(entry, entry.notification);
+
+                mIsolatedEntries.put(sbn.getKey(), sbn);
+
+                onEntryAdded(entry);
+                // We also need to update the suppression of the old group, because this call comes
+                // even before the groupManager knows about the notification at all.
+                // When the notification gets added afterwards it is already isolated and therefore
+                // it doesn't lead to an update.
+                updateSuppression(mGroupMap.get(entry.notification.getGroupKey()));
+                mListener.onGroupsChanged();
+            } else {
+                handleSuppressedSummaryHeadsUpped(entry);
+            }
+        } else {
+            if (mIsolatedEntries.containsKey(sbn.getKey())) {
+                // not isolated anymore, we need to update the groups
+                onEntryRemovedInternal(entry, entry.notification);
+                mIsolatedEntries.remove(sbn.getKey());
+                onEntryAdded(entry);
+                mListener.onGroupsChanged();
+            }
+        }
+    }
+
+    private void handleSuppressedSummaryHeadsUpped(NotificationData.Entry entry) {
+        StatusBarNotification sbn = entry.notification;
+        if (!isGroupSuppressed(sbn.getGroupKey())
+                || !sbn.getNotification().isGroupSummary()
+                || !entry.row.isHeadsUp()) {
+            return;
+        }
+        // The parent of a suppressed group got huned, lets hun the child!
+        NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey());
+        if (notificationGroup != null) {
+            Iterator<NotificationData.Entry> iterator
+                    = notificationGroup.children.values().iterator();
+            NotificationData.Entry child = iterator.hasNext() ? iterator.next() : null;
+            if (child == null) {
+                child = getIsolatedChild(sbn.getGroupKey());
+            }
+            if (child != null) {
+                if (child.row.keepInParent() || child.row.isRemoved() || child.row.isDismissed()) {
+                    // the notification is actually already removed, no need to do heads-up on it.
+                    return;
+                }
+                if (mHeadsUpManager.isHeadsUp(child.key)) {
+                    mHeadsUpManager.updateNotification(child, true);
+                } else {
+                    mHeadsUpManager.showNotification(child);
+                }
+            }
+        }
+        mHeadsUpManager.releaseImmediately(entry.key);
+    }
+
+    private boolean shouldIsolate(StatusBarNotification sbn) {
+        NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey());
+        return (sbn.isGroup() && !sbn.getNotification().isGroupSummary())
+                && (sbn.getNotification().fullScreenIntent != null
+                        || notificationGroup == null
+                        || !notificationGroup.expanded
+                        || isGroupNotFullyVisible(notificationGroup));
+    }
+
+    private boolean isGroupNotFullyVisible(NotificationGroup notificationGroup) {
+        return notificationGroup.summary == null
+                || notificationGroup.summary.row.getClipTopAmount() > 0
+                || notificationGroup.summary.row.getTranslationY() < 0;
+    }
+
+    public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+        mHeadsUpManager = headsUpManager;
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("GroupManager state:");
+        pw.println("  number of groups: " +  mGroupMap.size());
+        for (Map.Entry<String, NotificationGroup>  entry : mGroupMap.entrySet()) {
+            pw.println("\n    key: " + entry.getKey()); pw.println(entry.getValue());
+        }
+        pw.println("\n    isolated entries: " +  mIsolatedEntries.size());
+        for (Map.Entry<String, StatusBarNotification> entry : mIsolatedEntries.entrySet()) {
+            pw.print("      "); pw.print(entry.getKey());
+            pw.print(", "); pw.println(entry.getValue());
+        }
+    }
+
+    public static class NotificationGroup {
+        public final HashMap<String, NotificationData.Entry> children = new HashMap<>();
+        public NotificationData.Entry summary;
+        public boolean expanded;
+        /**
+         * Is this notification group suppressed, i.e its summary is hidden
+         */
+        public boolean suppressed;
+
+        @Override
+        public String toString() {
+            String result = "    summary:\n      "
+                    + (summary != null ? summary.notification : "null")
+                    + (summary != null && summary.getDebugThrowable() != null
+                            ? Log.getStackTraceString(summary.getDebugThrowable())
+                            : "");
+            result += "\n    children size: " + children.size();
+            for (NotificationData.Entry child : children.values()) {
+                result += "\n      " + child.notification
+                + (child.getDebugThrowable() != null
+                        ? Log.getStackTraceString(child.getDebugThrowable())
+                        : "");
+            }
+            return result;
+        }
+    }
+
+    public interface OnGroupChangeListener {
+        /**
+         * The expansion of a group has changed.
+         *
+         * @param changedRow the row for which the expansion has changed, which is also the summary
+         * @param expanded a boolean indicating the new expanded state
+         */
+        void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded);
+
+        /**
+         * A group of children just received a summary notification and should therefore become
+         * children of it.
+         *
+         * @param group the group created
+         */
+        void onGroupCreatedFromChildren(NotificationGroup group);
+
+        /**
+         * The groups have changed. This can happen if the isolation of a child has changes or if a
+         * group became suppressed / unsuppressed
+         */
+        void onGroupsChanged();
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
new file mode 100644
index 0000000..41a69b4
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
@@ -0,0 +1,288 @@
+package com.android.systemui.statusbar.phone;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.Icon;
+import android.support.annotation.NonNull;
+import android.support.v4.util.ArrayMap;
+import android.support.v4.util.ArraySet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.internal.util.NotificationColorUtil;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+import java.util.ArrayList;
+import java.util.function.Function;
+
+/**
+ * A controller for the space in the status bar to the left of the system icons. This area is
+ * normally reserved for notifications.
+ */
+public class NotificationIconAreaController implements DarkReceiver {
+    private final NotificationColorUtil mNotificationColorUtil;
+
+    private int mIconSize;
+    private int mIconHPadding;
+    private int mIconTint = Color.WHITE;
+
+    private StatusBar mStatusBar;
+    protected View mNotificationIconArea;
+    private NotificationIconContainer mNotificationIcons;
+    private NotificationIconContainer mShelfIcons;
+    private final Rect mTintArea = new Rect();
+    private NotificationStackScrollLayout mNotificationScrollLayout;
+    private Context mContext;
+
+    public NotificationIconAreaController(Context context, StatusBar statusBar) {
+        mStatusBar = statusBar;
+        mNotificationColorUtil = NotificationColorUtil.getInstance(context);
+        mContext = context;
+
+        initializeNotificationAreaViews(context);
+    }
+
+    protected View inflateIconArea(LayoutInflater inflater) {
+        return inflater.inflate(R.layout.notification_icon_area, null);
+    }
+
+    /**
+     * Initializes the views that will represent the notification area.
+     */
+    protected void initializeNotificationAreaViews(Context context) {
+        reloadDimens(context);
+
+        LayoutInflater layoutInflater = LayoutInflater.from(context);
+        mNotificationIconArea = inflateIconArea(layoutInflater);
+        mNotificationIcons = (NotificationIconContainer) mNotificationIconArea.findViewById(
+                R.id.notificationIcons);
+
+        mNotificationScrollLayout = mStatusBar.getNotificationScrollLayout();
+    }
+
+    public void setupShelf(NotificationShelf shelf) {
+        mShelfIcons = shelf.getShelfIcons();
+        shelf.setCollapsedIcons(mNotificationIcons);
+    }
+
+    public void onDensityOrFontScaleChanged(Context context) {
+        reloadDimens(context);
+        final FrameLayout.LayoutParams params = generateIconLayoutParams();
+        for (int i = 0; i < mNotificationIcons.getChildCount(); i++) {
+            View child = mNotificationIcons.getChildAt(i);
+            child.setLayoutParams(params);
+        }
+        for (int i = 0; i < mShelfIcons.getChildCount(); i++) {
+            View child = mShelfIcons.getChildAt(i);
+            child.setLayoutParams(params);
+        }
+    }
+
+    @NonNull
+    private FrameLayout.LayoutParams generateIconLayoutParams() {
+        return new FrameLayout.LayoutParams(
+                mIconSize + 2 * mIconHPadding, getHeight());
+    }
+
+    private void reloadDimens(Context context) {
+        Resources res = context.getResources();
+        mIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size);
+        mIconHPadding = res.getDimensionPixelSize(R.dimen.status_bar_icon_padding);
+    }
+
+    /**
+     * Returns the view that represents the notification area.
+     */
+    public View getNotificationInnerAreaView() {
+        return mNotificationIconArea;
+    }
+
+    /**
+     * See {@link com.android.systemui.statusbar.policy.DarkIconDispatcher#setIconsDarkArea}.
+     * Sets the color that should be used to tint any icons in the notification area.
+     *
+     * @param tintArea the area in which to tint the icons, specified in screen coordinates
+     * @param darkIntensity
+     */
+    public void onDarkChanged(Rect tintArea, float darkIntensity, int iconTint) {
+        if (tintArea == null) {
+            mTintArea.setEmpty();
+        } else {
+            mTintArea.set(tintArea);
+        }
+        mIconTint = iconTint;
+        applyNotificationIconsTint();
+    }
+
+    protected int getHeight() {
+        return mStatusBar.getStatusBarHeight();
+    }
+
+    protected boolean shouldShowNotificationIcon(NotificationData.Entry entry,
+            NotificationData notificationData, boolean showAmbient) {
+        if (notificationData.isAmbient(entry.key) && !showAmbient) {
+            return false;
+        }
+        if (!StatusBar.isTopLevelChild(entry)) {
+            return false;
+        }
+        if (entry.row.getVisibility() == View.GONE) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Updates the notifications with the given list of notifications to display.
+     */
+    public void updateNotificationIcons(NotificationData notificationData) {
+
+        updateIconsForLayout(notificationData, entry -> entry.icon, mNotificationIcons,
+                false /* showAmbient */);
+        updateIconsForLayout(notificationData, entry -> entry.expandedIcon, mShelfIcons,
+                NotificationShelf.SHOW_AMBIENT_ICONS);
+
+        applyNotificationIconsTint();
+    }
+
+    /**
+     * Updates the notification icons for a host layout. This will ensure that the notification
+     * host layout will have the same icons like the ones in here.
+     *
+     * @param notificationData the notification data to look up which notifications are relevant
+     * @param function A function to look up an icon view based on an entry
+     * @param hostLayout which layout should be updated
+     * @param showAmbient should ambient notification icons be shown
+     */
+    private void updateIconsForLayout(NotificationData notificationData,
+            Function<NotificationData.Entry, StatusBarIconView> function,
+            NotificationIconContainer hostLayout, boolean showAmbient) {
+        ArrayList<StatusBarIconView> toShow = new ArrayList<>(
+                mNotificationScrollLayout.getChildCount());
+
+        // Filter out ambient notifications and notification children.
+        for (int i = 0; i < mNotificationScrollLayout.getChildCount(); i++) {
+            View view = mNotificationScrollLayout.getChildAt(i);
+            if (view instanceof ExpandableNotificationRow) {
+                NotificationData.Entry ent = ((ExpandableNotificationRow) view).getEntry();
+                if (shouldShowNotificationIcon(ent, notificationData, showAmbient)) {
+                    toShow.add(function.apply(ent));
+                }
+            }
+        }
+
+        // In case we are changing the suppression of a group, the replacement shouldn't flicker
+        // and it should just be replaced instead. We therefore look for notifications that were
+        // just replaced by the child or vice-versa to suppress this.
+
+        ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons = new ArrayMap<>();
+        ArrayList<View> toRemove = new ArrayList<>();
+        for (int i = 0; i < hostLayout.getChildCount(); i++) {
+            View child = hostLayout.getChildAt(i);
+            if (!(child instanceof StatusBarIconView)) {
+                continue;
+            }
+            if (!toShow.contains(child)) {
+                boolean iconWasReplaced = false;
+                StatusBarIconView removedIcon = (StatusBarIconView) child;
+                String removedGroupKey = removedIcon.getNotification().getGroupKey();
+                for (int j = 0; j < toShow.size(); j++) {
+                    StatusBarIconView candidate = toShow.get(j);
+                    if (candidate.getSourceIcon().sameAs((removedIcon.getSourceIcon()))
+                            && candidate.getNotification().getGroupKey().equals(removedGroupKey)) {
+                        if (!iconWasReplaced) {
+                            iconWasReplaced = true;
+                        } else {
+                            iconWasReplaced = false;
+                            break;
+                        }
+                    }
+                }
+                if (iconWasReplaced) {
+                    ArrayList<StatusBarIcon> statusBarIcons = replacingIcons.get(removedGroupKey);
+                    if (statusBarIcons == null) {
+                        statusBarIcons = new ArrayList<>();
+                        replacingIcons.put(removedGroupKey, statusBarIcons);
+                    }
+                    statusBarIcons.add(removedIcon.getStatusBarIcon());
+                }
+                toRemove.add(removedIcon);
+            }
+        }
+        // removing all duplicates
+        ArrayList<String> duplicates = new ArrayList<>();
+        for (String key : replacingIcons.keySet()) {
+            ArrayList<StatusBarIcon> statusBarIcons = replacingIcons.get(key);
+            if (statusBarIcons.size() != 1) {
+                duplicates.add(key);
+            }
+        }
+        replacingIcons.removeAll(duplicates);
+        hostLayout.setReplacingIcons(replacingIcons);
+
+        final int toRemoveCount = toRemove.size();
+        for (int i = 0; i < toRemoveCount; i++) {
+            hostLayout.removeView(toRemove.get(i));
+        }
+
+        final FrameLayout.LayoutParams params = generateIconLayoutParams();
+        for (int i = 0; i < toShow.size(); i++) {
+            View v = toShow.get(i);
+            // The view might still be transiently added if it was just removed and added again
+            hostLayout.removeTransientView(v);
+            if (v.getParent() == null) {
+                hostLayout.addView(v, i, params);
+            }
+        }
+
+        hostLayout.setChangingViewPositions(true);
+        // Re-sort notification icons
+        final int childCount = hostLayout.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View actual = hostLayout.getChildAt(i);
+            StatusBarIconView expected = toShow.get(i);
+            if (actual == expected) {
+                continue;
+            }
+            hostLayout.removeView(expected);
+            hostLayout.addView(expected, i);
+        }
+        hostLayout.setChangingViewPositions(false);
+        hostLayout.setReplacingIcons(null);
+    }
+
+    /**
+     * Applies {@link #mIconTint} to the notification icons.
+     */
+    private void applyNotificationIconsTint() {
+        for (int i = 0; i < mNotificationIcons.getChildCount(); i++) {
+            StatusBarIconView v = (StatusBarIconView) mNotificationIcons.getChildAt(i);
+            boolean isPreL = Boolean.TRUE.equals(v.getTag(R.id.icon_is_pre_L));
+            int color = StatusBarIconView.NO_COLOR;
+            boolean colorize = !isPreL || NotificationUtils.isGrayscale(v, mNotificationColorUtil);
+            if (colorize) {
+                color = DarkIconDispatcher.getTint(mTintArea, v, mIconTint);
+            }
+            v.setStaticDrawableColor(color);
+            v.setDecorColor(mIconTint);
+        }
+    }
+
+    public void setDark(boolean dark) {
+        mNotificationIcons.setDark(dark, false, 0);
+        mShelfIcons.setDark(dark, false, 0);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/com/android/systemui/statusbar/phone/NotificationIconContainer.java
new file mode 100644
index 0000000..88a5626
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -0,0 +1,675 @@
+/*
+ * Copyright (C) 2016 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.phone;
+
+import static com.android.systemui.statusbar.notification.NotificationUtils.isHapticFeedbackDisabled;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.drawable.Icon;
+import android.os.AsyncTask;
+import android.os.UserHandle;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.support.v4.util.ArrayMap;
+import android.support.v4.util.ArraySet;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.AlphaOptimizedFrameLayout;
+import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.stack.AnimationFilter;
+import com.android.systemui.statusbar.stack.AnimationProperties;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+import com.android.systemui.statusbar.stack.ViewState;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * A container for notification icons. It handles overflowing icons properly and positions them
+ * correctly on the screen.
+ */
+public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
+    /**
+     * A float value indicating how much before the overflow start the icons should transform into
+     * a dot. A value of 0 means that they are exactly at the end and a value of 1 means it starts
+     * 1 icon width early.
+     */
+    public static final float OVERFLOW_EARLY_AMOUNT = 0.2f;
+    private static final int NO_VALUE = Integer.MIN_VALUE;
+    private static final String TAG = "NotificationIconContainer";
+    private static final boolean DEBUG = false;
+    private static final int CANNED_ANIMATION_DURATION = 100;
+    private static final AnimationProperties DOT_ANIMATION_PROPERTIES = new AnimationProperties() {
+        private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
+
+        @Override
+        public AnimationFilter getAnimationFilter() {
+            return mAnimationFilter;
+        }
+    }.setDuration(200);
+
+    private static final AnimationProperties ICON_ANIMATION_PROPERTIES = new AnimationProperties() {
+        private AnimationFilter mAnimationFilter = new AnimationFilter().animateY().animateAlpha()
+                .animateScale();
+
+        @Override
+        public AnimationFilter getAnimationFilter() {
+            return mAnimationFilter;
+        }
+
+    }.setDuration(CANNED_ANIMATION_DURATION)
+            .setCustomInterpolator(View.TRANSLATION_Y, Interpolators.ICON_OVERSHOT);
+
+    private static final AnimationProperties mTempProperties = new AnimationProperties() {
+        private AnimationFilter mAnimationFilter = new AnimationFilter();
+
+        @Override
+        public AnimationFilter getAnimationFilter() {
+            return mAnimationFilter;
+        }
+    };
+
+    private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
+        private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
+
+        @Override
+        public AnimationFilter getAnimationFilter() {
+            return mAnimationFilter;
+        }
+    }.setDuration(200).setDelay(50);
+
+    private static final AnimationProperties UNDARK_PROPERTIES = new AnimationProperties() {
+        private AnimationFilter mAnimationFilter = new AnimationFilter()
+                .animateX();
+
+        @Override
+        public AnimationFilter getAnimationFilter() {
+            return mAnimationFilter;
+        }
+    }.setDuration(StackStateAnimator.ANIMATION_DURATION_WAKEUP);
+    public static final int MAX_VISIBLE_ICONS_WHEN_DARK = 5;
+
+    private boolean mShowAllIcons = true;
+    private final HashMap<View, IconState> mIconStates = new HashMap<>();
+    private int mDotPadding;
+    private int mStaticDotRadius;
+    private int mActualLayoutWidth = NO_VALUE;
+    private float mActualPaddingEnd = NO_VALUE;
+    private float mActualPaddingStart = NO_VALUE;
+    private boolean mDark;
+    private boolean mChangingViewPositions;
+    private int mAddAnimationStartIndex = -1;
+    private int mCannedAnimationStartIndex = -1;
+    private int mSpeedBumpIndex = -1;
+    private int mIconSize;
+    private float mOpenedAmount = 0.0f;
+    private float mVisualOverflowAdaption;
+    private boolean mDisallowNextAnimation;
+    private boolean mAnimationsEnabled = true;
+    private boolean mVibrateOnAnimation;
+    private Vibrator mVibrator;
+    private ArrayMap<String, ArrayList<StatusBarIcon>> mReplacingIcons;
+    private int mDarkOffsetX;
+
+    public NotificationIconContainer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        initDimens();
+        setWillNotDraw(!DEBUG);
+        mVibrator = mContext.getSystemService(Vibrator.class);
+    }
+
+    private void initDimens() {
+        mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
+        mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        Paint paint = new Paint();
+        paint.setColor(Color.RED);
+        paint.setStyle(Paint.Style.STROKE);
+        canvas.drawRect(getActualPaddingStart(), 0, getLayoutEnd(), getHeight(), paint);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        initDimens();
+    }
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        float centerY = getHeight() / 2.0f;
+        // we layout all our children on the left at the top
+        mIconSize = 0;
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            // We need to layout all children even the GONE ones, such that the heights are
+            // calculated correctly as they are used to calculate how many we can fit on the screen
+            int width = child.getMeasuredWidth();
+            int height = child.getMeasuredHeight();
+            int top = (int) (centerY - height / 2.0f);
+            child.layout(0, top, width, top + height);
+            if (i == 0) {
+                mIconSize = child.getWidth();
+            }
+        }
+        if (mShowAllIcons) {
+            resetViewStates();
+            calculateIconTranslations();
+            applyIconStates();
+        }
+    }
+
+    public void applyIconStates() {
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            ViewState childState = mIconStates.get(child);
+            if (childState != null) {
+                childState.applyToView(child);
+            }
+        }
+        mAddAnimationStartIndex = -1;
+        mCannedAnimationStartIndex = -1;
+        mDisallowNextAnimation = false;
+    }
+
+    @Override
+    public void onViewAdded(View child) {
+        super.onViewAdded(child);
+        boolean isReplacingIcon = isReplacingIcon(child);
+        if (!mChangingViewPositions) {
+            IconState v = new IconState();
+            if (isReplacingIcon) {
+                v.justAdded = false;
+                v.justReplaced = true;
+            }
+            mIconStates.put(child, v);
+        }
+        int childIndex = indexOfChild(child);
+        if (childIndex < getChildCount() - 1 && !isReplacingIcon
+            && mIconStates.get(getChildAt(childIndex + 1)).iconAppearAmount > 0.0f) {
+            if (mAddAnimationStartIndex < 0) {
+                mAddAnimationStartIndex = childIndex;
+            } else {
+                mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, childIndex);
+            }
+        }
+        if (mDark && child instanceof StatusBarIconView) {
+            ((StatusBarIconView) child).setDark(mDark, false, 0);
+        }
+    }
+
+    private boolean isReplacingIcon(View child) {
+        if (mReplacingIcons == null) {
+            return false;
+        }
+        if (!(child instanceof StatusBarIconView)) {
+            return false;
+        }
+        StatusBarIconView iconView = (StatusBarIconView) child;
+        Icon sourceIcon = iconView.getSourceIcon();
+        String groupKey = iconView.getNotification().getGroupKey();
+        ArrayList<StatusBarIcon> statusBarIcons = mReplacingIcons.get(groupKey);
+        if (statusBarIcons != null) {
+            StatusBarIcon replacedIcon = statusBarIcons.get(0);
+            if (sourceIcon.sameAs(replacedIcon.icon)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void onViewRemoved(View child) {
+        super.onViewRemoved(child);
+        if (child instanceof StatusBarIconView) {
+            boolean isReplacingIcon = isReplacingIcon(child);
+            final StatusBarIconView icon = (StatusBarIconView) child;
+            if (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
+                    && child.getVisibility() == VISIBLE && isReplacingIcon) {
+                int animationStartIndex = findFirstViewIndexAfter(icon.getTranslationX());
+                if (mAddAnimationStartIndex < 0) {
+                    mAddAnimationStartIndex = animationStartIndex;
+                } else {
+                    mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, animationStartIndex);
+                }
+            }
+            if (!mChangingViewPositions) {
+                mIconStates.remove(child);
+                if (!isReplacingIcon) {
+                    addTransientView(icon, 0);
+                    icon.setVisibleState(StatusBarIconView.STATE_HIDDEN, true /* animate */,
+                            () -> removeTransientView(icon));
+                }
+            }
+        }
+    }
+
+    /**
+     * Finds the first view with a translation bigger then a given value
+     */
+    private int findFirstViewIndexAfter(float translationX) {
+        for (int i = 0; i < getChildCount(); i++) {
+            View view = getChildAt(i);
+            if (view.getTranslationX() > translationX) {
+                return i;
+            }
+        }
+        return getChildCount();
+    }
+
+    public void resetViewStates() {
+        for (int i = 0; i < getChildCount(); i++) {
+            View view = getChildAt(i);
+            ViewState iconState = mIconStates.get(view);
+            iconState.initFrom(view);
+            iconState.alpha = 1.0f;
+            iconState.hidden = false;
+        }
+    }
+
+    /**
+     * Calulate the horizontal translations for each notification based on how much the icons
+     * are inserted into the notification container.
+     * If this is not a whole number, the fraction means by how much the icon is appearing.
+     */
+    public void calculateIconTranslations() {
+        float translationX = getActualPaddingStart();
+        int firstOverflowIndex = -1;
+        int childCount = getChildCount();
+        int maxVisibleIcons = mDark ? MAX_VISIBLE_ICONS_WHEN_DARK : childCount;
+        float layoutEnd = getLayoutEnd();
+        float overflowStart = layoutEnd - mIconSize * (2 + OVERFLOW_EARLY_AMOUNT);
+        boolean hasAmbient = mSpeedBumpIndex != -1 && mSpeedBumpIndex < getChildCount();
+        float visualOverflowStart = 0;
+        for (int i = 0; i < childCount; i++) {
+            View view = getChildAt(i);
+            IconState iconState = mIconStates.get(view);
+            iconState.xTranslation = translationX;
+            boolean forceOverflow = mSpeedBumpIndex != -1 && i >= mSpeedBumpIndex
+                    && iconState.iconAppearAmount > 0.0f || i >= maxVisibleIcons;
+            boolean noOverflowAfter = i == childCount - 1;
+            float drawingScale = mDark && view instanceof StatusBarIconView
+                    ? ((StatusBarIconView) view).getIconScaleFullyDark()
+                    : 1f;
+            if (mOpenedAmount != 0.0f) {
+                noOverflowAfter = noOverflowAfter && !hasAmbient && !forceOverflow;
+            }
+            iconState.visibleState = StatusBarIconView.STATE_ICON;
+            if (firstOverflowIndex == -1 && (forceOverflow
+                    || (translationX >= (noOverflowAfter ? layoutEnd - mIconSize : overflowStart)))) {
+                firstOverflowIndex = noOverflowAfter && !forceOverflow ? i - 1 : i;
+                int totalDotLength = mStaticDotRadius * 6 + 2 * mDotPadding;
+                visualOverflowStart = overflowStart + mIconSize * (1 + OVERFLOW_EARLY_AMOUNT)
+                        - totalDotLength / 2
+                        - mIconSize * 0.5f + mStaticDotRadius;
+                if (forceOverflow) {
+                    visualOverflowStart = Math.min(translationX, visualOverflowStart
+                            + mStaticDotRadius * 2 + mDotPadding);
+                } else {
+                    visualOverflowStart += (translationX - overflowStart) / mIconSize
+                            * (mStaticDotRadius * 2 + mDotPadding);
+                }
+                if (mShowAllIcons) {
+                    // We want to perfectly position the overflow in the static state, such that
+                    // it's perfectly centered instead of measuring it from the end.
+                    mVisualOverflowAdaption = 0;
+                    if (firstOverflowIndex != -1) {
+                        View firstOverflowView = getChildAt(i);
+                        IconState overflowState = mIconStates.get(firstOverflowView);
+                        float totalAmount = layoutEnd - overflowState.xTranslation;
+                        float newPosition = overflowState.xTranslation + totalAmount / 2
+                                - totalDotLength / 2
+                                - mIconSize * 0.5f + mStaticDotRadius;
+                        mVisualOverflowAdaption = newPosition - visualOverflowStart;
+                        visualOverflowStart = newPosition;
+                    }
+                } else {
+                    visualOverflowStart += mVisualOverflowAdaption * (1f - mOpenedAmount);
+                }
+            }
+            translationX += iconState.iconAppearAmount * view.getWidth() * drawingScale;
+        }
+        if (firstOverflowIndex != -1) {
+            int numDots = 1;
+            translationX = visualOverflowStart;
+            for (int i = firstOverflowIndex; i < childCount; i++) {
+                View view = getChildAt(i);
+                IconState iconState = mIconStates.get(view);
+                int dotWidth = mStaticDotRadius * 2 + mDotPadding;
+                iconState.xTranslation = translationX;
+                if (numDots <= 3) {
+                    if (numDots == 1 && iconState.iconAppearAmount < 0.8f) {
+                        iconState.visibleState = StatusBarIconView.STATE_ICON;
+                        numDots--;
+                    } else {
+                        iconState.visibleState = StatusBarIconView.STATE_DOT;
+                    }
+                    translationX += (numDots == 3 ? 3 * dotWidth : dotWidth)
+                            * iconState.iconAppearAmount;
+                } else {
+                    iconState.visibleState = StatusBarIconView.STATE_HIDDEN;
+                }
+                numDots++;
+            }
+        }
+        boolean center = mDark;
+        if (center && translationX < getLayoutEnd()) {
+            float delta = (getLayoutEnd() - translationX) / 2;
+            if (firstOverflowIndex != -1) {
+                // If we have an overflow, only count those half for centering because the dots
+                // don't have a lot of visual weight.
+                float deltaIgnoringOverflow = (getLayoutEnd() - visualOverflowStart) / 2;
+                delta = (deltaIgnoringOverflow + delta) / 2;
+            }
+            for (int i = 0; i < childCount; i++) {
+                View view = getChildAt(i);
+                IconState iconState = mIconStates.get(view);
+                iconState.xTranslation += delta;
+            }
+        }
+
+        if (isLayoutRtl()) {
+            for (int i = 0; i < childCount; i++) {
+                View view = getChildAt(i);
+                IconState iconState = mIconStates.get(view);
+                iconState.xTranslation = getWidth() - iconState.xTranslation - view.getWidth();
+            }
+        }
+
+        if (mDark && mDarkOffsetX != 0) {
+            for (int i = 0; i < childCount; i++) {
+                View view = getChildAt(i);
+                IconState iconState = mIconStates.get(view);
+                iconState.xTranslation += mDarkOffsetX;
+            }
+        }
+    }
+
+    private float getLayoutEnd() {
+        return getActualWidth() - getActualPaddingEnd();
+    }
+
+    private float getActualPaddingEnd() {
+        if (mActualPaddingEnd == NO_VALUE) {
+            return getPaddingEnd();
+        }
+        return mActualPaddingEnd;
+    }
+
+    private float getActualPaddingStart() {
+        if (mActualPaddingStart == NO_VALUE) {
+            return getPaddingStart();
+        }
+        return mActualPaddingStart;
+    }
+
+    /**
+     * Sets whether the layout should always show all icons.
+     * If this is true, the icon positions will be updated on layout.
+     * If this if false, the layout is managed from the outside and layouting won't trigger a
+     * repositioning of the icons.
+     */
+    public void setShowAllIcons(boolean showAllIcons) {
+        mShowAllIcons = showAllIcons;
+    }
+
+    public void setActualLayoutWidth(int actualLayoutWidth) {
+        mActualLayoutWidth = actualLayoutWidth;
+        if (DEBUG) {
+            invalidate();
+        }
+    }
+
+    public void setActualPaddingEnd(float paddingEnd) {
+        mActualPaddingEnd = paddingEnd;
+        if (DEBUG) {
+            invalidate();
+        }
+    }
+
+    public void setActualPaddingStart(float paddingStart) {
+        mActualPaddingStart = paddingStart;
+        if (DEBUG) {
+            invalidate();
+        }
+    }
+
+    public int getActualWidth() {
+        if (mActualLayoutWidth == NO_VALUE) {
+            return getWidth();
+        }
+        return mActualLayoutWidth;
+    }
+
+    public void setChangingViewPositions(boolean changingViewPositions) {
+        mChangingViewPositions = changingViewPositions;
+    }
+
+    public void setDark(boolean dark, boolean fade, long delay) {
+        mDark = dark;
+        mDisallowNextAnimation |= !fade;
+        for (int i = 0; i < getChildCount(); i++) {
+            View view = getChildAt(i);
+            if (view instanceof StatusBarIconView) {
+                ((StatusBarIconView) view).setDark(dark, fade, delay);
+                if (!dark && fade) {
+                    getIconState((StatusBarIconView) view).justUndarkened = true;
+                }
+            }
+        }
+    }
+
+    public IconState getIconState(StatusBarIconView icon) {
+        return mIconStates.get(icon);
+    }
+
+    public void setSpeedBumpIndex(int speedBumpIndex) {
+        mSpeedBumpIndex = speedBumpIndex;
+    }
+
+    public void setOpenedAmount(float expandAmount) {
+        mOpenedAmount = expandAmount;
+    }
+
+    public float getVisualOverflowAdaption() {
+        return mVisualOverflowAdaption;
+    }
+
+    public void setVisualOverflowAdaption(float visualOverflowAdaption) {
+        mVisualOverflowAdaption = visualOverflowAdaption;
+    }
+
+    public boolean hasOverflow() {
+        float width = (getChildCount() + OVERFLOW_EARLY_AMOUNT) * mIconSize;
+        return width - (getWidth() - getActualPaddingStart() - getActualPaddingEnd()) > 0;
+    }
+
+    public void setVibrateOnAnimation(boolean vibrateOnAnimation) {
+        mVibrateOnAnimation = vibrateOnAnimation;
+    }
+
+    public int getIconSize() {
+        return mIconSize;
+    }
+
+    public void setAnimationsEnabled(boolean enabled) {
+        if (!enabled && mAnimationsEnabled) {
+            for (int i = 0; i < getChildCount(); i++) {
+                View child = getChildAt(i);
+                ViewState childState = mIconStates.get(child);
+                if (childState != null) {
+                    childState.cancelAnimations(child);
+                    childState.applyToView(child);
+                }
+            }
+        }
+        mAnimationsEnabled = enabled;
+    }
+
+    public void setDarkOffsetX(int offsetX) {
+        mDarkOffsetX = offsetX;
+    }
+
+    public void setReplacingIcons(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons) {
+        mReplacingIcons = replacingIcons;
+    }
+
+    public class IconState extends ViewState {
+        public static final int NO_VALUE = NotificationIconContainer.NO_VALUE;
+        public float iconAppearAmount = 1.0f;
+        public float clampedAppearAmount = 1.0f;
+        public int visibleState;
+        public boolean justAdded = true;
+        private boolean justReplaced;
+        public boolean needsCannedAnimation;
+        public boolean useFullTransitionAmount;
+        public boolean useLinearTransitionAmount;
+        public boolean translateContent;
+        public boolean justUndarkened;
+        public int iconColor = StatusBarIconView.NO_COLOR;
+        public boolean noAnimations;
+        public boolean isLastExpandIcon;
+        public int customTransformHeight = NO_VALUE;
+
+        @Override
+        public void applyToView(View view) {
+            if (view instanceof StatusBarIconView) {
+                StatusBarIconView icon = (StatusBarIconView) view;
+                boolean animate = false;
+                AnimationProperties animationProperties = null;
+                boolean animationsAllowed = (mAnimationsEnabled || justUndarkened)
+                        && !mDisallowNextAnimation
+                        && !noAnimations;
+                if (animationsAllowed) {
+                    if (justAdded || justReplaced) {
+                        super.applyToView(icon);
+                        if (justAdded && iconAppearAmount != 0.0f) {
+                            icon.setAlpha(0.0f);
+                            icon.setVisibleState(StatusBarIconView.STATE_HIDDEN,
+                                    false /* animate */);
+                            animationProperties = ADD_ICON_PROPERTIES;
+                            animate = true;
+                        }
+                    } else if (justUndarkened) {
+                        animationProperties = UNDARK_PROPERTIES;
+                        animate = true;
+                    } else if (visibleState != icon.getVisibleState()) {
+                        animationProperties = DOT_ANIMATION_PROPERTIES;
+                        animate = true;
+                    }
+                    if (!animate && mAddAnimationStartIndex >= 0
+                            && indexOfChild(view) >= mAddAnimationStartIndex
+                            && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
+                            || visibleState != StatusBarIconView.STATE_HIDDEN)) {
+                        animationProperties = DOT_ANIMATION_PROPERTIES;
+                        animate = true;
+                    }
+                    if (needsCannedAnimation) {
+                        AnimationFilter animationFilter = mTempProperties.getAnimationFilter();
+                        animationFilter.reset();
+                        animationFilter.combineFilter(
+                                ICON_ANIMATION_PROPERTIES.getAnimationFilter());
+                        mTempProperties.resetCustomInterpolators();
+                        mTempProperties.combineCustomInterpolators(ICON_ANIMATION_PROPERTIES);
+                        if (animationProperties != null) {
+                            animationFilter.combineFilter(animationProperties.getAnimationFilter());
+                            mTempProperties.combineCustomInterpolators(animationProperties);
+                        }
+                        animationProperties = mTempProperties;
+                        animationProperties.setDuration(CANNED_ANIMATION_DURATION);
+                        animate = true;
+                        mCannedAnimationStartIndex = indexOfChild(view);
+                    }
+                    if (!animate && mCannedAnimationStartIndex >= 0
+                            && indexOfChild(view) > mCannedAnimationStartIndex
+                            && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
+                            || visibleState != StatusBarIconView.STATE_HIDDEN)) {
+                        AnimationFilter animationFilter = mTempProperties.getAnimationFilter();
+                        animationFilter.reset();
+                        animationFilter.animateX();
+                        mTempProperties.resetCustomInterpolators();
+                        animationProperties = mTempProperties;
+                        animationProperties.setDuration(CANNED_ANIMATION_DURATION);
+                        animate = true;
+                    }
+                }
+                icon.setVisibleState(visibleState, animationsAllowed);
+                icon.setIconColor(iconColor, needsCannedAnimation && animationsAllowed);
+                if (animate) {
+                    animateTo(icon, animationProperties);
+                } else {
+                    super.applyToView(view);
+                }
+                boolean wasInShelf = icon.isInShelf();
+                boolean inShelf = iconAppearAmount == 1.0f;
+                icon.setIsInShelf(inShelf);
+                if (shouldVibrateChange(wasInShelf != inShelf)) {
+                    AsyncTask.execute(
+                            () -> mVibrator.vibrate(VibrationEffect.get(
+                                    VibrationEffect.EFFECT_TICK)));
+                }
+            }
+            justAdded = false;
+            justReplaced = false;
+            needsCannedAnimation = false;
+            justUndarkened = false;
+        }
+
+        private boolean shouldVibrateChange(boolean inShelfChanged) {
+            if (!mVibrateOnAnimation) {
+                return false;
+            }
+            if (justAdded) {
+                return false;
+            }
+            if (!mAnimationsEnabled) {
+                return false;
+            }
+            if (!inShelfChanged) {
+                return false;
+            }
+            if (isHapticFeedbackDisabled(mContext)) {
+                return false;
+            }
+            return true;
+        }
+
+        public boolean hasCustomTransformHeight() {
+            return isLastExpandIcon && customTransformHeight != NO_VALUE;
+        }
+
+        @Override
+        public void initFrom(View view) {
+            super.initFrom(view);
+            if (view instanceof StatusBarIconView) {
+                iconColor = ((StatusBarIconView) view).getStaticDrawableColor();
+            }
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/NotificationListenerWithPlugins.java b/com/android/systemui/statusbar/phone/NotificationListenerWithPlugins.java
new file mode 100644
index 0000000..9ff907b
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NotificationListenerWithPlugins.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2017 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.phone;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.RemoteException;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.plugins.NotificationListenerController;
+import com.android.systemui.plugins.NotificationListenerController.NotificationProvider;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.PluginManager;
+
+import java.util.ArrayList;
+
+/**
+ * A version of NotificationListenerService that passes all info to
+ * any plugins connected. Also allows those plugins the chance to cancel
+ * any incoming callbacks or to trigger new ones.
+ */
+public class NotificationListenerWithPlugins extends NotificationListenerService implements
+        PluginListener<NotificationListenerController> {
+
+    private ArrayList<NotificationListenerController> mPlugins = new ArrayList<>();
+    private boolean mConnected;
+
+    @Override
+    public void registerAsSystemService(Context context, ComponentName componentName,
+            int currentUser) throws RemoteException {
+        super.registerAsSystemService(context, componentName, currentUser);
+        Dependency.get(PluginManager.class).addPluginListener(this,
+                NotificationListenerController.class);
+    }
+
+    @Override
+    public void unregisterAsSystemService() throws RemoteException {
+        super.unregisterAsSystemService();
+        Dependency.get(PluginManager.class).removePluginListener(this);
+    }
+
+    @Override
+    public StatusBarNotification[] getActiveNotifications() {
+        StatusBarNotification[] activeNotifications = super.getActiveNotifications();
+        for (NotificationListenerController plugin : mPlugins) {
+            activeNotifications = plugin.getActiveNotifications(activeNotifications);
+        }
+        return activeNotifications;
+    }
+
+    @Override
+    public RankingMap getCurrentRanking() {
+        RankingMap currentRanking = super.getCurrentRanking();
+        for (NotificationListenerController plugin : mPlugins) {
+            currentRanking = plugin.getCurrentRanking(currentRanking);
+        }
+        return currentRanking;
+    }
+
+    public void onPluginConnected() {
+        mConnected = true;
+        mPlugins.forEach(p -> p.onListenerConnected(getProvider()));
+    }
+
+    /**
+     * Called when listener receives a onNotificationPosted.
+     * Returns true to indicate this callback should be skipped.
+     */
+    public boolean onPluginNotificationPosted(StatusBarNotification sbn,
+            final RankingMap rankingMap) {
+        for (NotificationListenerController plugin : mPlugins) {
+            if (plugin.onNotificationPosted(sbn, rankingMap)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Called when listener receives a onNotificationRemoved.
+     * Returns true to indicate this callback should be skipped.
+     */
+    public boolean onPluginNotificationRemoved(StatusBarNotification sbn,
+            final RankingMap rankingMap) {
+        for (NotificationListenerController plugin : mPlugins) {
+            if (plugin.onNotificationRemoved(sbn, rankingMap)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public RankingMap onPluginRankingUpdate(RankingMap rankingMap) {
+        return getCurrentRanking();
+    }
+
+    @Override
+    public void onPluginConnected(NotificationListenerController plugin, Context pluginContext) {
+        mPlugins.add(plugin);
+        if (mConnected) {
+            plugin.onListenerConnected(getProvider());
+        }
+    }
+
+    @Override
+    public void onPluginDisconnected(NotificationListenerController plugin) {
+        mPlugins.remove(plugin);
+    }
+
+    private NotificationProvider getProvider() {
+        return new NotificationProvider() {
+            @Override
+            public StatusBarNotification[] getActiveNotifications() {
+                return NotificationListenerWithPlugins.super.getActiveNotifications();
+            }
+
+            @Override
+            public RankingMap getRankingMap() {
+                return NotificationListenerWithPlugins.super.getCurrentRanking();
+            }
+
+            @Override
+            public void addNotification(StatusBarNotification sbn) {
+                onNotificationPosted(sbn, getRankingMap());
+            }
+
+            @Override
+            public void removeNotification(StatusBarNotification sbn) {
+                onNotificationRemoved(sbn, getRankingMap());
+            }
+
+            @Override
+            public void updateRanking() {
+                onNotificationRankingUpdate(getRankingMap());
+            }
+        };
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/NotificationPanelView.java b/com/android/systemui/statusbar/phone/NotificationPanelView.java
new file mode 100644
index 0000000..078e818
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -0,0 +1,2660 @@
+/*
+ * Copyright (C) 2012 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.phone;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.app.ActivityManager;
+import android.app.Fragment;
+import android.app.StatusBarManager;
+import android.content.Context;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.os.PowerManager;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.MathUtils;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.FrameLayout;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.keyguard.KeyguardStatusView;
+import com.android.systemui.DejankUtils;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.FlingAnimationUtils;
+import com.android.systemui.statusbar.GestureRecorder;
+import com.android.systemui.statusbar.KeyguardAffordanceView;
+import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+import java.util.List;
+
+public class NotificationPanelView extends PanelView implements
+        ExpandableView.OnHeightChangedListener,
+        View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener,
+        KeyguardAffordanceHelper.Callback, NotificationStackScrollLayout.OnEmptySpaceClickListener,
+        OnHeadsUpChangedListener, QS.HeightListener {
+
+    private static final boolean DEBUG = false;
+
+    // Cap and total height of Roboto font. Needs to be adjusted when font for the big clock is
+    // changed.
+    private static final int CAP_HEIGHT = 1456;
+    private static final int FONT_HEIGHT = 2163;
+
+    private static final float LOCK_ICON_ACTIVE_SCALE = 1.2f;
+
+    static final String COUNTER_PANEL_OPEN = "panel_open";
+    static final String COUNTER_PANEL_OPEN_QS = "panel_open_qs";
+    private static final String COUNTER_PANEL_OPEN_PEEK = "panel_open_peek";
+
+    private static final Rect mDummyDirtyRect = new Rect(0, 0, 1, 1);
+
+    public static final long DOZE_ANIMATION_DURATION = 700;
+
+    private static final FloatProperty<NotificationPanelView> SET_DARK_AMOUNT_PROPERTY =
+            new FloatProperty<NotificationPanelView>("mDarkAmount") {
+                @Override
+                public void setValue(NotificationPanelView object, float value) {
+                    object.setDarkAmount(value);
+                }
+
+                @Override
+                public Float get(NotificationPanelView object) {
+                    return object.mDarkAmount;
+                }
+            };
+    private final PowerManager mPowerManager;
+
+    private KeyguardAffordanceHelper mAffordanceHelper;
+    private KeyguardUserSwitcher mKeyguardUserSwitcher;
+    private KeyguardStatusBarView mKeyguardStatusBar;
+    private QS mQs;
+    private FrameLayout mQsFrame;
+    private KeyguardStatusView mKeyguardStatusView;
+    private View mReserveNotificationSpace;
+    private View mQsNavbarScrim;
+    protected NotificationsQuickSettingsContainer mNotificationContainerParent;
+    protected NotificationStackScrollLayout mNotificationStackScroller;
+    private boolean mAnimateNextTopPaddingChange;
+
+    private int mTrackingPointer;
+    private VelocityTracker mQsVelocityTracker;
+    private boolean mQsTracking;
+
+    /**
+     * If set, the ongoing touch gesture might both trigger the expansion in {@link PanelView} and
+     * the expansion for quick settings.
+     */
+    private boolean mConflictingQsExpansionGesture;
+
+    /**
+     * Whether we are currently handling a motion gesture in #onInterceptTouchEvent, but haven't
+     * intercepted yet.
+     */
+    private boolean mIntercepting;
+    private boolean mPanelExpanded;
+    private boolean mQsExpanded;
+    private boolean mQsExpandedWhenExpandingStarted;
+    private boolean mQsFullyExpanded;
+    private boolean mKeyguardShowing;
+    private boolean mDozing;
+    private boolean mDozingOnDown;
+    protected int mStatusBarState;
+    private float mInitialHeightOnTouch;
+    private float mInitialTouchX;
+    private float mInitialTouchY;
+    private float mLastTouchX;
+    private float mLastTouchY;
+    protected float mQsExpansionHeight;
+    protected int mQsMinExpansionHeight;
+    protected int mQsMaxExpansionHeight;
+    private int mQsPeekHeight;
+    private boolean mStackScrollerOverscrolling;
+    private boolean mQsExpansionFromOverscroll;
+    private float mLastOverscroll;
+    protected boolean mQsExpansionEnabled = true;
+    private ValueAnimator mQsExpansionAnimator;
+    private FlingAnimationUtils mFlingAnimationUtils;
+    private int mStatusBarMinHeight;
+    private boolean mUnlockIconActive;
+    private int mNotificationsHeaderCollideDistance;
+    private int mUnlockMoveDistance;
+    private float mEmptyDragAmount;
+
+    private Animator mClockAnimator;
+    private int mClockAnimationTargetX = Integer.MIN_VALUE;
+    private int mClockAnimationTargetY = Integer.MIN_VALUE;
+    private int mTopPaddingAdjustment;
+    private KeyguardClockPositionAlgorithm mClockPositionAlgorithm =
+            new KeyguardClockPositionAlgorithm();
+    private KeyguardClockPositionAlgorithm.Result mClockPositionResult =
+            new KeyguardClockPositionAlgorithm.Result();
+    private boolean mIsExpanding;
+
+    private boolean mBlockTouches;
+    private int mNotificationScrimWaitDistance;
+    // Used for two finger gesture as well as accessibility shortcut to QS.
+    private boolean mQsExpandImmediate;
+    private boolean mTwoFingerQsExpandPossible;
+
+    /**
+     * If we are in a panel collapsing motion, we reset scrollY of our scroll view but still
+     * need to take this into account in our panel height calculation.
+     */
+    private boolean mQsAnimatorExpand;
+    private boolean mIsLaunchTransitionFinished;
+    private boolean mIsLaunchTransitionRunning;
+    private Runnable mLaunchAnimationEndRunnable;
+    private boolean mOnlyAffordanceInThisMotion;
+    private boolean mKeyguardStatusViewAnimating;
+    private ValueAnimator mQsSizeChangeAnimator;
+
+    private boolean mShowEmptyShadeView;
+
+    private boolean mQsScrimEnabled = true;
+    private boolean mLastAnnouncementWasQuickSettings;
+    private boolean mQsTouchAboveFalsingThreshold;
+    private int mQsFalsingThreshold;
+
+    private float mKeyguardStatusBarAnimateAlpha = 1f;
+    private float mQsClockAlphaOverride = 1f;
+    private int mOldLayoutDirection;
+    private HeadsUpTouchHelper mHeadsUpTouchHelper;
+    private boolean mIsExpansionFromHeadsUp;
+    private boolean mListenForHeadsUp;
+    private int mNavigationBarBottomHeight;
+    private boolean mExpandingFromHeadsUp;
+    private boolean mCollapsedOnDown;
+    private int mPositionMinSideMargin;
+    private int mMaxFadeoutHeight;
+    private int mLastOrientation = -1;
+    private boolean mClosingWithAlphaFadeOut;
+    private boolean mHeadsUpAnimatingAway;
+    private boolean mLaunchingAffordance;
+    private FalsingManager mFalsingManager;
+    private String mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE;
+
+    private Runnable mHeadsUpExistenceChangedRunnable = new Runnable() {
+        @Override
+        public void run() {
+            setHeadsUpAnimatingAway(false);
+            notifyBarPanelExpansionChanged();
+        }
+    };
+    private NotificationGroupManager mGroupManager;
+    private boolean mShowIconsWhenExpanded;
+    private int mIndicationBottomPadding;
+    private int mAmbientIndicationBottomPadding;
+    private boolean mIsFullWidth;
+    private float mDarkAmount;
+    private float mDarkAmountTarget;
+    private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
+    private boolean mNoVisibleNotifications = true;
+    private ValueAnimator mDarkAnimator;
+    private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    private boolean mUserSetupComplete;
+
+    public NotificationPanelView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setWillNotDraw(!DEBUG);
+        mFalsingManager = FalsingManager.getInstance(context);
+        mPowerManager = context.getSystemService(PowerManager.class);
+    }
+
+    public void setStatusBar(StatusBar bar) {
+        mStatusBar = bar;
+        mKeyguardBottomArea.setStatusBar(mStatusBar);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mKeyguardStatusBar = findViewById(R.id.keyguard_header);
+        mKeyguardStatusView = findViewById(R.id.keyguard_status_view);
+
+        mNotificationContainerParent = (NotificationsQuickSettingsContainer)
+                findViewById(R.id.notification_container_parent);
+        mNotificationStackScroller = (NotificationStackScrollLayout)
+                findViewById(R.id.notification_stack_scroller);
+        mNotificationStackScroller.setOnHeightChangedListener(this);
+        mNotificationStackScroller.setOverscrollTopChangedListener(this);
+        mNotificationStackScroller.setOnEmptySpaceClickListener(this);
+        mKeyguardBottomArea = findViewById(R.id.keyguard_bottom_area);
+        mQsNavbarScrim = findViewById(R.id.qs_navbar_scrim);
+        mLastOrientation = getResources().getConfiguration().orientation;
+
+        initBottomArea();
+
+        mQsFrame = findViewById(R.id.qs_frame);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        FragmentHostManager.get(this).addTagListener(QS.TAG, mFragmentListener);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        FragmentHostManager.get(this).removeTagListener(QS.TAG, mFragmentListener);
+    }
+
+    @Override
+    protected void loadDimens() {
+        super.loadDimens();
+        mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.4f);
+        mStatusBarMinHeight = getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.status_bar_height);
+        mQsPeekHeight = getResources().getDimensionPixelSize(R.dimen.qs_peek_height);
+        mNotificationsHeaderCollideDistance =
+                getResources().getDimensionPixelSize(R.dimen.header_notifications_collide_distance);
+        mUnlockMoveDistance = getResources().getDimensionPixelOffset(R.dimen.unlock_move_distance);
+        mClockPositionAlgorithm.loadDimens(getResources());
+        mNotificationScrimWaitDistance =
+                getResources().getDimensionPixelSize(R.dimen.notification_scrim_wait_distance);
+        mQsFalsingThreshold = getResources().getDimensionPixelSize(
+                R.dimen.qs_falsing_threshold);
+        mPositionMinSideMargin = getResources().getDimensionPixelSize(
+                R.dimen.notification_panel_min_side_margin);
+        mMaxFadeoutHeight = getResources().getDimensionPixelSize(
+                R.dimen.max_notification_fadeout_height);
+        mIndicationBottomPadding = getResources().getDimensionPixelSize(
+                R.dimen.keyguard_indication_bottom_padding);
+    }
+
+    public void updateResources() {
+        Resources res = getResources();
+        int qsWidth = res.getDimensionPixelSize(R.dimen.qs_panel_width);
+        int panelGravity = getResources().getInteger(R.integer.notification_panel_layout_gravity);
+        FrameLayout.LayoutParams lp =
+                (FrameLayout.LayoutParams) mQsFrame.getLayoutParams();
+        if (lp.width != qsWidth || lp.gravity != panelGravity) {
+            lp.width = qsWidth;
+            lp.gravity = panelGravity;
+            mQsFrame.setLayoutParams(lp);
+        }
+
+        int panelWidth = res.getDimensionPixelSize(R.dimen.notification_panel_width);
+        lp = (FrameLayout.LayoutParams) mNotificationStackScroller.getLayoutParams();
+        if (lp.width != panelWidth || lp.gravity != panelGravity) {
+            lp.width = panelWidth;
+            lp.gravity = panelGravity;
+            mNotificationStackScroller.setLayoutParams(lp);
+        }
+    }
+
+    public void onOverlayChanged() {
+        // Re-inflate the status view group.
+        int index = indexOfChild(mKeyguardStatusView);
+        removeView(mKeyguardStatusView);
+        mKeyguardStatusView = (KeyguardStatusView) LayoutInflater.from(mContext).inflate(
+                R.layout.keyguard_status_view,
+                this,
+                false);
+        addView(mKeyguardStatusView, index);
+
+        // Update keyguard bottom area
+        index = indexOfChild(mKeyguardBottomArea);
+        removeView(mKeyguardBottomArea);
+        mKeyguardBottomArea = (KeyguardBottomAreaView) LayoutInflater.from(mContext).inflate(
+                R.layout.keyguard_bottom_area,
+                this,
+                false);
+        addView(mKeyguardBottomArea, index);
+        initBottomArea();
+        setDarkAmount(mDarkAmount);
+
+        setKeyguardStatusViewVisibility(mStatusBarState, false, false);
+        setKeyguardBottomAreaVisibility(mStatusBarState, false);
+    }
+
+    private void initBottomArea() {
+        mAffordanceHelper = new KeyguardAffordanceHelper(this, getContext());
+        mKeyguardBottomArea.setAffordanceHelper(mAffordanceHelper);
+        mKeyguardBottomArea.setStatusBar(mStatusBar);
+        mKeyguardBottomArea.setUserSetupComplete(mUserSetupComplete);
+    }
+
+    public void setKeyguardIndicationController(KeyguardIndicationController indicationController) {
+        mKeyguardBottomArea.setKeyguardIndicationController(indicationController);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        setIsFullWidth(mNotificationStackScroller.getWidth() == getWidth());
+
+        // Update Clock Pivot
+        mKeyguardStatusView.setPivotX(getWidth() / 2);
+        mKeyguardStatusView.setPivotY((FONT_HEIGHT - CAP_HEIGHT) / 2048f *
+                mKeyguardStatusView.getClockTextSize());
+
+        // Calculate quick setting heights.
+        int oldMaxHeight = mQsMaxExpansionHeight;
+        if (mQs != null) {
+            mQsMinExpansionHeight = mKeyguardShowing ? 0 : mQs.getQsMinExpansionHeight();
+            mQsMaxExpansionHeight = mQs.getDesiredHeight();
+        }
+        positionClockAndNotifications();
+        if (mQsExpanded && mQsFullyExpanded) {
+            mQsExpansionHeight = mQsMaxExpansionHeight;
+            requestScrollerTopPaddingUpdate(false /* animate */);
+            requestPanelHeightUpdate();
+
+            // Size has changed, start an animation.
+            if (mQsMaxExpansionHeight != oldMaxHeight) {
+                startQsSizeChangeAnimation(oldMaxHeight, mQsMaxExpansionHeight);
+            }
+        } else if (!mQsExpanded) {
+            setQsExpansion(mQsMinExpansionHeight + mLastOverscroll);
+        }
+        updateExpandedHeight(getExpandedHeight());
+        updateHeader();
+
+        // If we are running a size change animation, the animation takes care of the height of
+        // the container. However, if we are not animating, we always need to make the QS container
+        // the desired height so when closing the QS detail, it stays smaller after the size change
+        // animation is finished but the detail view is still being animated away (this animation
+        // takes longer than the size change animation).
+        if (mQsSizeChangeAnimator == null && mQs != null) {
+            mQs.setHeightOverride(mQs.getDesiredHeight());
+        }
+        updateMaxHeadsUpTranslation();
+    }
+
+    private void setIsFullWidth(boolean isFullWidth) {
+        mIsFullWidth = isFullWidth;
+        mNotificationStackScroller.setIsFullWidth(isFullWidth);
+    }
+
+    private void startQsSizeChangeAnimation(int oldHeight, final int newHeight) {
+        if (mQsSizeChangeAnimator != null) {
+            oldHeight = (int) mQsSizeChangeAnimator.getAnimatedValue();
+            mQsSizeChangeAnimator.cancel();
+        }
+        mQsSizeChangeAnimator = ValueAnimator.ofInt(oldHeight, newHeight);
+        mQsSizeChangeAnimator.setDuration(300);
+        mQsSizeChangeAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        mQsSizeChangeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                requestScrollerTopPaddingUpdate(false /* animate */);
+                requestPanelHeightUpdate();
+                int height = (int) mQsSizeChangeAnimator.getAnimatedValue();
+                mQs.setHeightOverride(height);
+            }
+        });
+        mQsSizeChangeAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mQsSizeChangeAnimator = null;
+            }
+        });
+        mQsSizeChangeAnimator.start();
+    }
+
+    /**
+     * Positions the clock and notifications dynamically depending on how many notifications are
+     * showing.
+     */
+    private void positionClockAndNotifications() {
+        boolean animate = mNotificationStackScroller.isAddOrRemoveAnimationPending();
+        int stackScrollerPadding;
+        if (mStatusBarState != StatusBarState.KEYGUARD) {
+            stackScrollerPadding = (mQs != null ? mQs.getHeader().getHeight() : 0) + mQsPeekHeight;
+            mTopPaddingAdjustment = 0;
+        } else {
+            mClockPositionAlgorithm.setup(
+                    mStatusBar.getMaxKeyguardNotifications(),
+                    getMaxPanelHeight(),
+                    getExpandedHeight(),
+                    mNotificationStackScroller.getNotGoneChildCount(),
+                    getHeight(),
+                    mKeyguardStatusView.getHeight(),
+                    mEmptyDragAmount,
+                    mKeyguardStatusView.getClockBottom(),
+                    mDarkAmount);
+            mClockPositionAlgorithm.run(mClockPositionResult);
+            if (animate || mClockAnimator != null) {
+                startClockAnimation(mClockPositionResult.clockX, mClockPositionResult.clockY);
+            } else {
+                mKeyguardStatusView.setX(mClockPositionResult.clockX);
+                mKeyguardStatusView.setY(mClockPositionResult.clockY);
+            }
+            updateClock(mClockPositionResult.clockAlpha, mClockPositionResult.clockScale);
+            stackScrollerPadding = mClockPositionResult.stackScrollerPadding;
+            mTopPaddingAdjustment = mClockPositionResult.stackScrollerPaddingAdjustment;
+        }
+        mNotificationStackScroller.setIntrinsicPadding(stackScrollerPadding);
+        mNotificationStackScroller.setDarkShelfOffsetX(mClockPositionResult.clockX);
+        requestScrollerTopPaddingUpdate(animate);
+    }
+
+    /**
+     * @param maximum the maximum to return at most
+     * @return the maximum keyguard notifications that can fit on the screen
+     */
+    public int computeMaxKeyguardNotifications(int maximum) {
+        float minPadding = mClockPositionAlgorithm.getMinStackScrollerPadding(getHeight(),
+                mKeyguardStatusView.getHeight());
+        int notificationPadding = Math.max(1, getResources().getDimensionPixelSize(
+                R.dimen.notification_divider_height));
+        NotificationShelf shelf = mNotificationStackScroller.getNotificationShelf();
+        float shelfSize = shelf.getVisibility() == GONE ? 0
+                : shelf.getIntrinsicHeight() + notificationPadding;
+        float availableSpace = mNotificationStackScroller.getHeight() - minPadding - shelfSize
+                - Math.max(mIndicationBottomPadding, mAmbientIndicationBottomPadding);
+        int count = 0;
+        for (int i = 0; i < mNotificationStackScroller.getChildCount(); i++) {
+            ExpandableView child = (ExpandableView) mNotificationStackScroller.getChildAt(i);
+            if (!(child instanceof ExpandableNotificationRow)) {
+                continue;
+            }
+            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+            boolean suppressedSummary = mGroupManager.isSummaryOfSuppressedGroup(
+                    row.getStatusBarNotification());
+            if (suppressedSummary) {
+                continue;
+            }
+            if (!mStatusBar.shouldShowOnKeyguard(row.getStatusBarNotification())) {
+                continue;
+            }
+            if (row.isRemoved()) {
+                continue;
+            }
+            availableSpace -= child.getMinHeight() + notificationPadding;
+            if (availableSpace >= 0 && count < maximum) {
+                count++;
+            } else if (availableSpace > -shelfSize) {
+                // if we are exactly the last view, then we can show us still!
+                for (int j = i + 1; j < mNotificationStackScroller.getChildCount(); j++) {
+                    if (mNotificationStackScroller.getChildAt(j)
+                            instanceof ExpandableNotificationRow) {
+                        return count;
+                    }
+                }
+                count++;
+                return count;
+            } else {
+                return count;
+            }
+        }
+        return count;
+    }
+
+    private void startClockAnimation(int x, int y) {
+        if (mClockAnimationTargetX == x && mClockAnimationTargetY == y) {
+            return;
+        }
+        mClockAnimationTargetX = x;
+        mClockAnimationTargetY = y;
+        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+            @Override
+            public boolean onPreDraw() {
+                getViewTreeObserver().removeOnPreDrawListener(this);
+                if (mClockAnimator != null) {
+                    mClockAnimator.removeAllListeners();
+                    mClockAnimator.cancel();
+                }
+                AnimatorSet set = new AnimatorSet();
+                set.play(ObjectAnimator.ofFloat(
+                        mKeyguardStatusView, View.Y, mClockAnimationTargetY))
+                        .with(ObjectAnimator.ofFloat(
+                                mKeyguardStatusView, View.X, mClockAnimationTargetX));
+                mClockAnimator = set;
+                mClockAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+                mClockAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+                mClockAnimator.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        mClockAnimator = null;
+                        mClockAnimationTargetX = Integer.MIN_VALUE;
+                        mClockAnimationTargetY = Integer.MIN_VALUE;
+                    }
+                });
+                mClockAnimator.start();
+                return true;
+            }
+        });
+    }
+
+    private void updateClock(float alpha, float scale) {
+        if (!mKeyguardStatusViewAnimating) {
+            mKeyguardStatusView.setAlpha(alpha * mQsClockAlphaOverride);
+        }
+        mKeyguardStatusView.setScaleX(scale);
+        mKeyguardStatusView.setScaleY(scale);
+    }
+
+    public void animateToFullShade(long delay) {
+        mAnimateNextTopPaddingChange = true;
+        mNotificationStackScroller.goToFullShade(delay);
+        requestLayout();
+    }
+
+    public void setQsExpansionEnabled(boolean qsExpansionEnabled) {
+        mQsExpansionEnabled = qsExpansionEnabled;
+        if (mQs == null) return;
+        mQs.setHeaderClickable(qsExpansionEnabled);
+    }
+
+    @Override
+    public void resetViews() {
+        mIsLaunchTransitionFinished = false;
+        mBlockTouches = false;
+        mUnlockIconActive = false;
+        if (!mLaunchingAffordance) {
+            mAffordanceHelper.reset(false);
+            mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE;
+        }
+        closeQs();
+        mStatusBar.closeAndSaveGuts(true /* leavebehind */, true /* force */,
+                true /* controls */, -1 /* x */, -1 /* y */, true /* resetMenu */);
+        mNotificationStackScroller.setOverScrollAmount(0f, true /* onTop */, false /* animate */,
+                true /* cancelAnimators */);
+        mNotificationStackScroller.resetScrollPosition();
+    }
+
+    public void closeQs() {
+        cancelQsAnimation();
+        setQsExpansion(mQsMinExpansionHeight);
+    }
+
+    public void animateCloseQs() {
+        if (mQsExpansionAnimator != null) {
+            if (!mQsAnimatorExpand) {
+                return;
+            }
+            float height = mQsExpansionHeight;
+            mQsExpansionAnimator.cancel();
+            setQsExpansion(height);
+        }
+        flingSettings(0 /* vel */, false);
+    }
+
+    public void openQs() {
+        cancelQsAnimation();
+        if (mQsExpansionEnabled) {
+            setQsExpansion(mQsMaxExpansionHeight);
+        }
+    }
+
+    public void expandWithQs() {
+        if (mQsExpansionEnabled) {
+            mQsExpandImmediate = true;
+        }
+        expand(true /* animate */);
+    }
+
+    @Override
+    public void fling(float vel, boolean expand) {
+        GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
+        if (gr != null) {
+            gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel);
+        }
+        super.fling(vel, expand);
+    }
+
+    @Override
+    protected void flingToHeight(float vel, boolean expand, float target,
+            float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
+        mHeadsUpTouchHelper.notifyFling(!expand);
+        setClosingWithAlphaFadeout(!expand && getFadeoutAlpha() == 1.0f);
+        super.flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+            event.getText().add(getKeyguardOrLockScreenString());
+            mLastAnnouncementWasQuickSettings = false;
+            return true;
+        }
+        return super.dispatchPopulateAccessibilityEventInternal(event);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        if (mBlockTouches || mQs.isCustomizing()) {
+            return false;
+        }
+        initDownStates(event);
+        if (mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
+            mIsExpansionFromHeadsUp = true;
+            MetricsLogger.count(mContext, COUNTER_PANEL_OPEN, 1);
+            MetricsLogger.count(mContext, COUNTER_PANEL_OPEN_PEEK, 1);
+            return true;
+        }
+
+        if (!isFullyCollapsed() && onQsIntercept(event)) {
+            return true;
+        }
+        return super.onInterceptTouchEvent(event);
+    }
+
+    private boolean onQsIntercept(MotionEvent event) {
+        int pointerIndex = event.findPointerIndex(mTrackingPointer);
+        if (pointerIndex < 0) {
+            pointerIndex = 0;
+            mTrackingPointer = event.getPointerId(pointerIndex);
+        }
+        final float x = event.getX(pointerIndex);
+        final float y = event.getY(pointerIndex);
+
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                mIntercepting = true;
+                mInitialTouchY = y;
+                mInitialTouchX = x;
+                initVelocityTracker();
+                trackMovement(event);
+                if (shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) {
+                    getParent().requestDisallowInterceptTouchEvent(true);
+                }
+                if (mQsExpansionAnimator != null) {
+                    onQsExpansionStarted();
+                    mInitialHeightOnTouch = mQsExpansionHeight;
+                    mQsTracking = true;
+                    mIntercepting = false;
+                    mNotificationStackScroller.removeLongPressCallback();
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                final int upPointer = event.getPointerId(event.getActionIndex());
+                if (mTrackingPointer == upPointer) {
+                    // gesture is ongoing, find a new pointer to track
+                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+                    mTrackingPointer = event.getPointerId(newIndex);
+                    mInitialTouchX = event.getX(newIndex);
+                    mInitialTouchY = event.getY(newIndex);
+                }
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                final float h = y - mInitialTouchY;
+                trackMovement(event);
+                if (mQsTracking) {
+
+                    // Already tracking because onOverscrolled was called. We need to update here
+                    // so we don't stop for a frame until the next touch event gets handled in
+                    // onTouchEvent.
+                    setQsExpansion(h + mInitialHeightOnTouch);
+                    trackMovement(event);
+                    mIntercepting = false;
+                    return true;
+                }
+                if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)
+                        && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) {
+                    mQsTracking = true;
+                    onQsExpansionStarted();
+                    notifyExpandingFinished();
+                    mInitialHeightOnTouch = mQsExpansionHeight;
+                    mInitialTouchY = y;
+                    mInitialTouchX = x;
+                    mIntercepting = false;
+                    mNotificationStackScroller.removeLongPressCallback();
+                    return true;
+                }
+                break;
+
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                trackMovement(event);
+                if (mQsTracking) {
+                    flingQsWithCurrentVelocity(y,
+                            event.getActionMasked() == MotionEvent.ACTION_CANCEL);
+                    mQsTracking = false;
+                }
+                mIntercepting = false;
+                break;
+        }
+        return false;
+    }
+
+    @Override
+    protected boolean isInContentBounds(float x, float y) {
+        float stackScrollerX = mNotificationStackScroller.getX();
+        return !mNotificationStackScroller.isBelowLastNotification(x - stackScrollerX, y)
+                && stackScrollerX < x && x < stackScrollerX + mNotificationStackScroller.getWidth();
+    }
+
+    private void initDownStates(MotionEvent event) {
+        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            mOnlyAffordanceInThisMotion = false;
+            mQsTouchAboveFalsingThreshold = mQsFullyExpanded;
+            mDozingOnDown = isDozing();
+            mCollapsedOnDown = isFullyCollapsed();
+            mListenForHeadsUp = mCollapsedOnDown && mHeadsUpManager.hasPinnedHeadsUp();
+        }
+    }
+
+    private void flingQsWithCurrentVelocity(float y, boolean isCancelMotionEvent) {
+        float vel = getCurrentQSVelocity();
+        final boolean expandsQs = flingExpandsQs(vel);
+        if (expandsQs) {
+            logQsSwipeDown(y);
+        }
+        flingSettings(vel, expandsQs && !isCancelMotionEvent);
+    }
+
+    private void logQsSwipeDown(float y) {
+        float vel = getCurrentQSVelocity();
+        final int gesture = mStatusBarState == StatusBarState.KEYGUARD
+                ? MetricsEvent.ACTION_LS_QS
+                : MetricsEvent.ACTION_SHADE_QS_PULL;
+        mLockscreenGestureLogger.write(gesture,
+                (int) ((y - mInitialTouchY) / mStatusBar.getDisplayDensity()),
+                (int) (vel / mStatusBar.getDisplayDensity()));
+    }
+
+    private boolean flingExpandsQs(float vel) {
+        if (isFalseTouch()) {
+            return false;
+        }
+        if (Math.abs(vel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
+            return getQsExpansionFraction() > 0.5f;
+        } else {
+            return vel > 0;
+        }
+    }
+
+    private boolean isFalseTouch() {
+        if (!needsAntiFalsing()) {
+            return false;
+        }
+        if (mFalsingManager.isClassiferEnabled()) {
+            return mFalsingManager.isFalseTouch();
+        }
+        return !mQsTouchAboveFalsingThreshold;
+    }
+
+    private float getQsExpansionFraction() {
+        return Math.min(1f, (mQsExpansionHeight - mQsMinExpansionHeight)
+                / (getTempQsMaxExpansion() - mQsMinExpansionHeight));
+    }
+
+    @Override
+    protected float getOpeningHeight() {
+        return mNotificationStackScroller.getOpeningHeight();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mBlockTouches || (mQs != null && mQs.isCustomizing())) {
+            return false;
+        }
+        initDownStates(event);
+        if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp()
+                && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
+            mIsExpansionFromHeadsUp = true;
+            MetricsLogger.count(mContext, COUNTER_PANEL_OPEN_PEEK, 1);
+        }
+        boolean handled = false;
+        if ((!mIsExpanding || mHintAnimationRunning)
+                && !mQsExpanded
+                && mStatusBar.getBarState() != StatusBarState.SHADE
+                && !mDozing) {
+            handled |= mAffordanceHelper.onTouchEvent(event);
+        }
+        if (mOnlyAffordanceInThisMotion) {
+            return true;
+        }
+        handled |= mHeadsUpTouchHelper.onTouchEvent(event);
+
+        if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) {
+            return true;
+        }
+        if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) {
+            MetricsLogger.count(mContext, COUNTER_PANEL_OPEN, 1);
+            updateVerticalPanelPosition(event.getX());
+            handled = true;
+        }
+        handled |= super.onTouchEvent(event);
+        return mDozing ? handled : true;
+    }
+
+    private boolean handleQsTouch(MotionEvent event) {
+        final int action = event.getActionMasked();
+        if (action == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f
+                && mStatusBar.getBarState() != StatusBarState.KEYGUARD && !mQsExpanded
+                && mQsExpansionEnabled) {
+
+            // Down in the empty area while fully expanded - go to QS.
+            mQsTracking = true;
+            mConflictingQsExpansionGesture = true;
+            onQsExpansionStarted();
+            mInitialHeightOnTouch = mQsExpansionHeight;
+            mInitialTouchY = event.getX();
+            mInitialTouchX = event.getY();
+        }
+        if (!isFullyCollapsed()) {
+            handleQsDown(event);
+        }
+        if (!mQsExpandImmediate && mQsTracking) {
+            onQsTouch(event);
+            if (!mConflictingQsExpansionGesture) {
+                return true;
+            }
+        }
+        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+            mConflictingQsExpansionGesture = false;
+        }
+        if (action == MotionEvent.ACTION_DOWN && isFullyCollapsed()
+                && mQsExpansionEnabled) {
+            mTwoFingerQsExpandPossible = true;
+        }
+        if (mTwoFingerQsExpandPossible && isOpenQsEvent(event)
+                && event.getY(event.getActionIndex()) < mStatusBarMinHeight) {
+            MetricsLogger.count(mContext, COUNTER_PANEL_OPEN_QS, 1);
+            mQsExpandImmediate = true;
+            requestPanelHeightUpdate();
+
+            // Normally, we start listening when the panel is expanded, but here we need to start
+            // earlier so the state is already up to date when dragging down.
+            setListening(true);
+        }
+        return false;
+    }
+
+    private boolean isInQsArea(float x, float y) {
+        return (x >= mQsFrame.getX()
+                && x <= mQsFrame.getX() + mQsFrame.getWidth())
+                && (y <= mNotificationStackScroller.getBottomMostNotificationBottom()
+                || y <= mQs.getView().getY() + mQs.getView().getHeight());
+    }
+
+    private boolean isOpenQsEvent(MotionEvent event) {
+        final int pointerCount = event.getPointerCount();
+        final int action = event.getActionMasked();
+
+        final boolean twoFingerDrag = action == MotionEvent.ACTION_POINTER_DOWN
+                && pointerCount == 2;
+
+        final boolean stylusButtonClickDrag = action == MotionEvent.ACTION_DOWN
+                && (event.isButtonPressed(MotionEvent.BUTTON_STYLUS_PRIMARY)
+                        || event.isButtonPressed(MotionEvent.BUTTON_STYLUS_SECONDARY));
+
+        final boolean mouseButtonClickDrag = action == MotionEvent.ACTION_DOWN
+                && (event.isButtonPressed(MotionEvent.BUTTON_SECONDARY)
+                        || event.isButtonPressed(MotionEvent.BUTTON_TERTIARY));
+
+        return twoFingerDrag || stylusButtonClickDrag || mouseButtonClickDrag;
+    }
+
+    private void handleQsDown(MotionEvent event) {
+        if (event.getActionMasked() == MotionEvent.ACTION_DOWN
+                && shouldQuickSettingsIntercept(event.getX(), event.getY(), -1)) {
+            mFalsingManager.onQsDown();
+            mQsTracking = true;
+            onQsExpansionStarted();
+            mInitialHeightOnTouch = mQsExpansionHeight;
+            mInitialTouchY = event.getX();
+            mInitialTouchX = event.getY();
+
+            // If we interrupt an expansion gesture here, make sure to update the state correctly.
+            notifyExpandingFinished();
+        }
+    }
+
+    @Override
+    protected boolean flingExpands(float vel, float vectorVel, float x, float y) {
+        boolean expands = super.flingExpands(vel, vectorVel, x, y);
+
+        // If we are already running a QS expansion, make sure that we keep the panel open.
+        if (mQsExpansionAnimator != null) {
+            expands = true;
+        }
+        return expands;
+    }
+
+    @Override
+    protected boolean hasConflictingGestures() {
+        return mStatusBar.getBarState() != StatusBarState.SHADE;
+    }
+
+    @Override
+    protected boolean shouldGestureIgnoreXTouchSlop(float x, float y) {
+        return !mAffordanceHelper.isOnAffordanceIcon(x, y);
+    }
+
+    private void onQsTouch(MotionEvent event) {
+        int pointerIndex = event.findPointerIndex(mTrackingPointer);
+        if (pointerIndex < 0) {
+            pointerIndex = 0;
+            mTrackingPointer = event.getPointerId(pointerIndex);
+        }
+        final float y = event.getY(pointerIndex);
+        final float x = event.getX(pointerIndex);
+        final float h = y - mInitialTouchY;
+
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                mQsTracking = true;
+                mInitialTouchY = y;
+                mInitialTouchX = x;
+                onQsExpansionStarted();
+                mInitialHeightOnTouch = mQsExpansionHeight;
+                initVelocityTracker();
+                trackMovement(event);
+                break;
+
+            case MotionEvent.ACTION_POINTER_UP:
+                final int upPointer = event.getPointerId(event.getActionIndex());
+                if (mTrackingPointer == upPointer) {
+                    // gesture is ongoing, find a new pointer to track
+                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+                    final float newY = event.getY(newIndex);
+                    final float newX = event.getX(newIndex);
+                    mTrackingPointer = event.getPointerId(newIndex);
+                    mInitialHeightOnTouch = mQsExpansionHeight;
+                    mInitialTouchY = newY;
+                    mInitialTouchX = newX;
+                }
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                setQsExpansion(h + mInitialHeightOnTouch);
+                if (h >= getFalsingThreshold()) {
+                    mQsTouchAboveFalsingThreshold = true;
+                }
+                trackMovement(event);
+                break;
+
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                mQsTracking = false;
+                mTrackingPointer = -1;
+                trackMovement(event);
+                float fraction = getQsExpansionFraction();
+                if (fraction != 0f || y >= mInitialTouchY) {
+                    flingQsWithCurrentVelocity(y,
+                            event.getActionMasked() == MotionEvent.ACTION_CANCEL);
+                }
+                if (mQsVelocityTracker != null) {
+                    mQsVelocityTracker.recycle();
+                    mQsVelocityTracker = null;
+                }
+                break;
+        }
+    }
+
+    private int getFalsingThreshold() {
+        float factor = mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
+        return (int) (mQsFalsingThreshold * factor);
+    }
+
+    @Override
+    public void onOverscrollTopChanged(float amount, boolean isRubberbanded) {
+        cancelQsAnimation();
+        if (!mQsExpansionEnabled) {
+            amount = 0f;
+        }
+        float rounded = amount >= 1f ? amount : 0f;
+        setOverScrolling(rounded != 0f && isRubberbanded);
+        mQsExpansionFromOverscroll = rounded != 0f;
+        mLastOverscroll = rounded;
+        updateQsState();
+        setQsExpansion(mQsMinExpansionHeight + rounded);
+    }
+
+    @Override
+    public void flingTopOverscroll(float velocity, boolean open) {
+        mLastOverscroll = 0f;
+        mQsExpansionFromOverscroll = false;
+        setQsExpansion(mQsExpansionHeight);
+        flingSettings(!mQsExpansionEnabled && open ? 0f : velocity, open && mQsExpansionEnabled,
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        mStackScrollerOverscrolling = false;
+                        setOverScrolling(false);
+                        updateQsState();
+                    }
+                }, false /* isClick */);
+    }
+
+    private void setOverScrolling(boolean overscrolling) {
+        mStackScrollerOverscrolling = overscrolling;
+        if (mQs == null) return;
+        mQs.setOverscrolling(overscrolling);
+    }
+
+    private void onQsExpansionStarted() {
+        onQsExpansionStarted(0);
+    }
+
+    protected void onQsExpansionStarted(int overscrollAmount) {
+        cancelQsAnimation();
+        cancelHeightAnimator();
+
+        // Reset scroll position and apply that position to the expanded height.
+        float height = mQsExpansionHeight - overscrollAmount;
+        setQsExpansion(height);
+        requestPanelHeightUpdate();
+        mNotificationStackScroller.checkSnoozeLeavebehind();
+    }
+
+    private void setQsExpanded(boolean expanded) {
+        boolean changed = mQsExpanded != expanded;
+        if (changed) {
+            mQsExpanded = expanded;
+            updateQsState();
+            requestPanelHeightUpdate();
+            mFalsingManager.setQsExpanded(expanded);
+            mStatusBar.setQsExpanded(expanded);
+            mNotificationContainerParent.setQsExpanded(expanded);
+        }
+    }
+
+    public void setBarState(int statusBarState, boolean keyguardFadingAway,
+            boolean goingToFullShade) {
+        int oldState = mStatusBarState;
+        boolean keyguardShowing = statusBarState == StatusBarState.KEYGUARD;
+        setKeyguardStatusViewVisibility(statusBarState, keyguardFadingAway, goingToFullShade);
+        setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade);
+
+        mStatusBarState = statusBarState;
+        mKeyguardShowing = keyguardShowing;
+        if (mQs != null) {
+            mQs.setKeyguardShowing(mKeyguardShowing);
+        }
+
+        if (oldState == StatusBarState.KEYGUARD
+                && (goingToFullShade || statusBarState == StatusBarState.SHADE_LOCKED)) {
+            animateKeyguardStatusBarOut();
+            long delay = mStatusBarState == StatusBarState.SHADE_LOCKED
+                    ? 0 : mStatusBar.calculateGoingToFullShadeDelay();
+            mQs.animateHeaderSlidingIn(delay);
+        } else if (oldState == StatusBarState.SHADE_LOCKED
+                && statusBarState == StatusBarState.KEYGUARD) {
+            animateKeyguardStatusBarIn(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+            mQs.animateHeaderSlidingOut();
+        } else {
+            mKeyguardStatusBar.setAlpha(1f);
+            mKeyguardStatusBar.setVisibility(keyguardShowing ? View.VISIBLE : View.INVISIBLE);
+            if (keyguardShowing && oldState != mStatusBarState) {
+                mKeyguardBottomArea.onKeyguardShowingChanged();
+                if (mQs != null) {
+                    mQs.hideImmediately();
+                }
+            }
+        }
+        if (keyguardShowing) {
+            updateDozingVisibilities(false /* animate */);
+        }
+        resetVerticalPanelPosition();
+        updateQsState();
+    }
+
+    private final Runnable mAnimateKeyguardStatusViewInvisibleEndRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mKeyguardStatusViewAnimating = false;
+            mKeyguardStatusView.setVisibility(View.GONE);
+        }
+    };
+
+    private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mKeyguardStatusViewAnimating = false;
+        }
+    };
+
+    private final Runnable mAnimateKeyguardStatusBarInvisibleEndRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mKeyguardStatusBar.setVisibility(View.INVISIBLE);
+            mKeyguardStatusBar.setAlpha(1f);
+            mKeyguardStatusBarAnimateAlpha = 1f;
+        }
+    };
+
+    private void animateKeyguardStatusBarOut() {
+        ValueAnimator anim = ValueAnimator.ofFloat(mKeyguardStatusBar.getAlpha(), 0f);
+        anim.addUpdateListener(mStatusBarAnimateAlphaListener);
+        anim.setStartDelay(mStatusBar.isKeyguardFadingAway()
+                ? mStatusBar.getKeyguardFadingAwayDelay()
+                : 0);
+        anim.setDuration(mStatusBar.isKeyguardFadingAway()
+                ? mStatusBar.getKeyguardFadingAwayDuration() / 2
+                : StackStateAnimator.ANIMATION_DURATION_STANDARD);
+        anim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mAnimateKeyguardStatusBarInvisibleEndRunnable.run();
+            }
+        });
+        anim.start();
+    }
+
+    private final ValueAnimator.AnimatorUpdateListener mStatusBarAnimateAlphaListener =
+            new ValueAnimator.AnimatorUpdateListener() {
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            mKeyguardStatusBarAnimateAlpha = (float) animation.getAnimatedValue();
+            updateHeaderKeyguardAlpha();
+        }
+    };
+
+    private void animateKeyguardStatusBarIn(long duration) {
+        mKeyguardStatusBar.setVisibility(View.VISIBLE);
+        mKeyguardStatusBar.setAlpha(0f);
+        ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
+        anim.addUpdateListener(mStatusBarAnimateAlphaListener);
+        anim.setDuration(duration);
+        anim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+        anim.start();
+    }
+
+    private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mKeyguardBottomArea.setVisibility(View.GONE);
+        }
+    };
+
+    private void setKeyguardBottomAreaVisibility(int statusBarState,
+            boolean goingToFullShade) {
+        mKeyguardBottomArea.animate().cancel();
+        if (goingToFullShade) {
+            mKeyguardBottomArea.animate()
+                    .alpha(0f)
+                    .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
+                    .setDuration(mStatusBar.getKeyguardFadingAwayDuration() / 2)
+                    .setInterpolator(Interpolators.ALPHA_OUT)
+                    .withEndAction(mAnimateKeyguardBottomAreaInvisibleEndRunnable)
+                    .start();
+        } else if (statusBarState == StatusBarState.KEYGUARD
+                || statusBarState == StatusBarState.SHADE_LOCKED) {
+            mKeyguardBottomArea.setVisibility(View.VISIBLE);
+            mKeyguardBottomArea.setAlpha(1f);
+        } else {
+            mKeyguardBottomArea.setVisibility(View.GONE);
+            mKeyguardBottomArea.setAlpha(1f);
+        }
+    }
+
+    private void setKeyguardStatusViewVisibility(int statusBarState, boolean keyguardFadingAway,
+            boolean goingToFullShade) {
+        if ((!keyguardFadingAway && mStatusBarState == StatusBarState.KEYGUARD
+                && statusBarState != StatusBarState.KEYGUARD) || goingToFullShade) {
+            mKeyguardStatusView.animate().cancel();
+            mKeyguardStatusViewAnimating = true;
+            mKeyguardStatusView.animate()
+                    .alpha(0f)
+                    .setStartDelay(0)
+                    .setDuration(160)
+                    .setInterpolator(Interpolators.ALPHA_OUT)
+                    .withEndAction(mAnimateKeyguardStatusViewInvisibleEndRunnable);
+            if (keyguardFadingAway) {
+                mKeyguardStatusView.animate()
+                        .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
+                        .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2)
+                        .start();
+            }
+        } else if (mStatusBarState == StatusBarState.SHADE_LOCKED
+                && statusBarState == StatusBarState.KEYGUARD) {
+            mKeyguardStatusView.animate().cancel();
+            mKeyguardStatusView.setVisibility(View.VISIBLE);
+            mKeyguardStatusViewAnimating = true;
+            mKeyguardStatusView.setAlpha(0f);
+            mKeyguardStatusView.animate()
+                    .alpha(1f)
+                    .setStartDelay(0)
+                    .setDuration(320)
+                    .setInterpolator(Interpolators.ALPHA_IN)
+                    .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable);
+        } else if (statusBarState == StatusBarState.KEYGUARD) {
+            mKeyguardStatusView.animate().cancel();
+            mKeyguardStatusViewAnimating = false;
+            mKeyguardStatusView.setVisibility(View.VISIBLE);
+            mKeyguardStatusView.setAlpha(1f);
+        } else {
+            mKeyguardStatusView.animate().cancel();
+            mKeyguardStatusViewAnimating = false;
+            mKeyguardStatusView.setVisibility(View.GONE);
+            mKeyguardStatusView.setAlpha(1f);
+        }
+    }
+
+    private void updateQsState() {
+        mNotificationStackScroller.setQsExpanded(mQsExpanded);
+        mNotificationStackScroller.setScrollingEnabled(
+                mStatusBarState != StatusBarState.KEYGUARD && (!mQsExpanded
+                        || mQsExpansionFromOverscroll));
+        updateEmptyShadeView();
+        mQsNavbarScrim.setVisibility(mStatusBarState == StatusBarState.SHADE && mQsExpanded
+                && !mStackScrollerOverscrolling && mQsScrimEnabled
+                        ? View.VISIBLE
+                        : View.INVISIBLE);
+        if (mKeyguardUserSwitcher != null && mQsExpanded && !mStackScrollerOverscrolling) {
+            mKeyguardUserSwitcher.hideIfNotSimple(true /* animate */);
+        }
+        if (mQs == null) return;
+        mQs.setExpanded(mQsExpanded);
+    }
+
+    private void setQsExpansion(float height) {
+        height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
+        mQsFullyExpanded = height == mQsMaxExpansionHeight && mQsMaxExpansionHeight != 0;
+        if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) {
+            setQsExpanded(true);
+        } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
+            setQsExpanded(false);
+            if (mLastAnnouncementWasQuickSettings && !mTracking && !isCollapsing()) {
+                announceForAccessibility(getKeyguardOrLockScreenString());
+                mLastAnnouncementWasQuickSettings = false;
+            }
+        }
+        mQsExpansionHeight = height;
+        updateQsExpansion();
+        requestScrollerTopPaddingUpdate(false /* animate */);
+        if (mKeyguardShowing) {
+            updateHeaderKeyguardAlpha();
+        }
+        if (mStatusBarState == StatusBarState.SHADE_LOCKED
+                || mStatusBarState == StatusBarState.KEYGUARD) {
+            updateKeyguardBottomAreaAlpha();
+        }
+        if (mStatusBarState == StatusBarState.SHADE && mQsExpanded
+                && !mStackScrollerOverscrolling && mQsScrimEnabled) {
+            mQsNavbarScrim.setAlpha(getQsExpansionFraction());
+        }
+
+        // Fade clock when QS is on top of it
+        float newClockAlpha = (height - mKeyguardStatusView.getY()) /
+                mKeyguardStatusView.getHeight();
+        newClockAlpha = 1 - MathUtils.constrain(newClockAlpha, 0, 1);
+        if (newClockAlpha != mQsClockAlphaOverride) {
+            mQsClockAlphaOverride = Interpolators.ALPHA_OUT.getInterpolation(newClockAlpha);
+            updateClock(mClockPositionResult.clockAlpha, mClockPositionResult.clockScale);
+        }
+
+        // Upon initialisation when we are not layouted yet we don't want to announce that we are
+        // fully expanded, hence the != 0.0f check.
+        if (height != 0.0f && mQsFullyExpanded && !mLastAnnouncementWasQuickSettings) {
+            announceForAccessibility(getContext().getString(
+                    R.string.accessibility_desc_quick_settings));
+            mLastAnnouncementWasQuickSettings = true;
+        }
+        if (mQsFullyExpanded && mFalsingManager.shouldEnforceBouncer()) {
+            mStatusBar.executeRunnableDismissingKeyguard(null, null /* cancelAction */,
+                    false /* dismissShade */, true /* afterKeyguardGone */, false /* deferred */);
+        }
+        if (DEBUG) {
+            invalidate();
+        }
+    }
+
+    protected void updateQsExpansion() {
+        if (mQs == null) return;
+        mQs.setQsExpansion(getQsExpansionFraction(), getHeaderTranslation());
+    }
+
+    private String getKeyguardOrLockScreenString() {
+        if (mQs != null && mQs.isCustomizing()) {
+            return getContext().getString(R.string.accessibility_desc_quick_settings_edit);
+        } else if (mStatusBarState == StatusBarState.KEYGUARD) {
+            return getContext().getString(R.string.accessibility_desc_lock_screen);
+        } else {
+            return getContext().getString(R.string.accessibility_desc_notification_shade);
+        }
+    }
+
+    private float calculateQsTopPadding() {
+        if (mKeyguardShowing
+                && (mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted)) {
+
+            // Either QS pushes the notifications down when fully expanded, or QS is fully above the
+            // notifications (mostly on tablets). maxNotifications denotes the normal top padding
+            // on Keyguard, maxQs denotes the top padding from the quick settings panel. We need to
+            // take the maximum and linearly interpolate with the panel expansion for a nice motion.
+            int maxNotifications = mClockPositionResult.stackScrollerPadding
+                    - mClockPositionResult.stackScrollerPaddingAdjustment;
+            int maxQs = getTempQsMaxExpansion();
+            int max = mStatusBarState == StatusBarState.KEYGUARD
+                    ? Math.max(maxNotifications, maxQs)
+                    : maxQs;
+            return (int) interpolate(getExpandedFraction(),
+                    mQsMinExpansionHeight, max);
+        } else if (mQsSizeChangeAnimator != null) {
+            return (int) mQsSizeChangeAnimator.getAnimatedValue();
+        } else if (mKeyguardShowing) {
+
+            // We can only do the smoother transition on Keyguard when we also are not collapsing
+            // from a scrolled quick settings.
+            return interpolate(getQsExpansionFraction(),
+                    mNotificationStackScroller.getIntrinsicPadding(),
+                    mQsMaxExpansionHeight);
+        } else {
+            return mQsExpansionHeight;
+        }
+    }
+
+    protected void requestScrollerTopPaddingUpdate(boolean animate) {
+        mNotificationStackScroller.updateTopPadding(calculateQsTopPadding(),
+                mAnimateNextTopPaddingChange || animate,
+                mKeyguardShowing
+                        && (mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted));
+        mAnimateNextTopPaddingChange = false;
+    }
+
+    private void trackMovement(MotionEvent event) {
+        if (mQsVelocityTracker != null) mQsVelocityTracker.addMovement(event);
+        mLastTouchX = event.getX();
+        mLastTouchY = event.getY();
+    }
+
+    private void initVelocityTracker() {
+        if (mQsVelocityTracker != null) {
+            mQsVelocityTracker.recycle();
+        }
+        mQsVelocityTracker = VelocityTracker.obtain();
+    }
+
+    private float getCurrentQSVelocity() {
+        if (mQsVelocityTracker == null) {
+            return 0;
+        }
+        mQsVelocityTracker.computeCurrentVelocity(1000);
+        return mQsVelocityTracker.getYVelocity();
+    }
+
+    private void cancelQsAnimation() {
+        if (mQsExpansionAnimator != null) {
+            mQsExpansionAnimator.cancel();
+        }
+    }
+
+    public void flingSettings(float vel, boolean expand) {
+        flingSettings(vel, expand, null, false /* isClick */);
+    }
+
+    protected void flingSettings(float vel, boolean expand, final Runnable onFinishRunnable,
+            boolean isClick) {
+        float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;
+        if (target == mQsExpansionHeight) {
+            if (onFinishRunnable != null) {
+                onFinishRunnable.run();
+            }
+            return;
+        }
+
+        // If we move in the opposite direction, reset velocity and use a different duration.
+        boolean oppositeDirection = false;
+        if (vel > 0 && !expand || vel < 0 && expand) {
+            vel = 0;
+            oppositeDirection = true;
+        }
+        ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
+        if (isClick) {
+            animator.setInterpolator(Interpolators.TOUCH_RESPONSE);
+            animator.setDuration(368);
+        } else {
+            mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel);
+        }
+        if (oppositeDirection) {
+            animator.setDuration(350);
+        }
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                setQsExpansion((Float) animation.getAnimatedValue());
+            }
+        });
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mNotificationStackScroller.resetCheckSnoozeLeavebehind();
+                mQsExpansionAnimator = null;
+                if (onFinishRunnable != null) {
+                    onFinishRunnable.run();
+                }
+            }
+        });
+        animator.start();
+        mQsExpansionAnimator = animator;
+        mQsAnimatorExpand = expand;
+    }
+
+    /**
+     * @return Whether we should intercept a gesture to open Quick Settings.
+     */
+    private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
+        if (!mQsExpansionEnabled || mCollapsedOnDown) {
+            return false;
+        }
+        View header = mKeyguardShowing ? mKeyguardStatusBar : mQs.getHeader();
+        final boolean onHeader = x >= mQsFrame.getX()
+                && x <= mQsFrame.getX() + mQsFrame.getWidth()
+                && y >= header.getTop() && y <= header.getBottom();
+        if (mQsExpanded) {
+            return onHeader || (yDiff < 0 && isInQsArea(x, y));
+        } else {
+            return onHeader;
+        }
+    }
+
+    @Override
+    protected boolean isScrolledToBottom() {
+        if (!isInSettings()) {
+            return mStatusBar.getBarState() == StatusBarState.KEYGUARD
+                    || mNotificationStackScroller.isScrolledToBottom();
+        } else {
+            return true;
+        }
+    }
+
+    @Override
+    protected int getMaxPanelHeight() {
+        int min = mStatusBarMinHeight;
+        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD
+                && mNotificationStackScroller.getNotGoneChildCount() == 0) {
+            int minHeight = (int) (mQsMinExpansionHeight + getOverExpansionAmount());
+            min = Math.max(min, minHeight);
+        }
+        int maxHeight;
+        if (mQsExpandImmediate || mQsExpanded || mIsExpanding && mQsExpandedWhenExpandingStarted) {
+            maxHeight = calculatePanelHeightQsExpanded();
+        } else {
+            maxHeight = calculatePanelHeightShade();
+        }
+        maxHeight = Math.max(maxHeight, min);
+        return maxHeight;
+    }
+
+    public boolean isInSettings() {
+        return mQsExpanded;
+    }
+
+    public boolean isExpanding() {
+        return mIsExpanding;
+    }
+
+    @Override
+    protected void onHeightUpdated(float expandedHeight) {
+        if (!mQsExpanded || mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted) {
+            positionClockAndNotifications();
+        }
+        if (mQsExpandImmediate || mQsExpanded && !mQsTracking && mQsExpansionAnimator == null
+                && !mQsExpansionFromOverscroll) {
+            float t;
+            if (mKeyguardShowing) {
+
+                // On Keyguard, interpolate the QS expansion linearly to the panel expansion
+                t = expandedHeight / (getMaxPanelHeight());
+            } else {
+
+                // In Shade, interpolate linearly such that QS is closed whenever panel height is
+                // minimum QS expansion + minStackHeight
+                float panelHeightQsCollapsed = mNotificationStackScroller.getIntrinsicPadding()
+                        + mNotificationStackScroller.getLayoutMinHeight();
+                float panelHeightQsExpanded = calculatePanelHeightQsExpanded();
+                t = (expandedHeight - panelHeightQsCollapsed)
+                        / (panelHeightQsExpanded - panelHeightQsCollapsed);
+            }
+            setQsExpansion(mQsMinExpansionHeight
+                    + t * (getTempQsMaxExpansion() - mQsMinExpansionHeight));
+        }
+        updateExpandedHeight(expandedHeight);
+        updateHeader();
+        updateUnlockIcon();
+        updateNotificationTranslucency();
+        updatePanelExpanded();
+        mNotificationStackScroller.setShadeExpanded(!isFullyCollapsed());
+        if (DEBUG) {
+            invalidate();
+        }
+    }
+
+    private void updatePanelExpanded() {
+        boolean isExpanded = !isFullyCollapsed();
+        if (mPanelExpanded != isExpanded) {
+            mHeadsUpManager.setIsExpanded(isExpanded);
+            mStatusBar.setPanelExpanded(isExpanded);
+            mPanelExpanded = isExpanded;
+        }
+    }
+
+    /**
+     * @return a temporary override of {@link #mQsMaxExpansionHeight}, which is needed when
+     *         collapsing QS / the panel when QS was scrolled
+     */
+    private int getTempQsMaxExpansion() {
+        return mQsMaxExpansionHeight;
+    }
+
+    private int calculatePanelHeightShade() {
+        int emptyBottomMargin = mNotificationStackScroller.getEmptyBottomMargin();
+        int maxHeight = mNotificationStackScroller.getHeight() - emptyBottomMargin
+                - mTopPaddingAdjustment;
+        maxHeight += mNotificationStackScroller.getTopPaddingOverflow();
+        return maxHeight;
+    }
+
+    private int calculatePanelHeightQsExpanded() {
+        float notificationHeight = mNotificationStackScroller.getHeight()
+                - mNotificationStackScroller.getEmptyBottomMargin()
+                - mNotificationStackScroller.getTopPadding();
+
+        // When only empty shade view is visible in QS collapsed state, simulate that we would have
+        // it in expanded QS state as well so we don't run into troubles when fading the view in/out
+        // and expanding/collapsing the whole panel from/to quick settings.
+        if (mNotificationStackScroller.getNotGoneChildCount() == 0
+                && mShowEmptyShadeView) {
+            notificationHeight = mNotificationStackScroller.getEmptyShadeViewHeight();
+        }
+        int maxQsHeight = mQsMaxExpansionHeight;
+
+        // If an animation is changing the size of the QS panel, take the animated value.
+        if (mQsSizeChangeAnimator != null) {
+            maxQsHeight = (int) mQsSizeChangeAnimator.getAnimatedValue();
+        }
+        float totalHeight = Math.max(
+                maxQsHeight, mStatusBarState == StatusBarState.KEYGUARD
+                        ? mClockPositionResult.stackScrollerPadding - mTopPaddingAdjustment
+                        : 0)
+                + notificationHeight + mNotificationStackScroller.getTopPaddingOverflow();
+        if (totalHeight > mNotificationStackScroller.getHeight()) {
+            float fullyCollapsedHeight = maxQsHeight
+                    + mNotificationStackScroller.getLayoutMinHeight();
+            totalHeight = Math.max(fullyCollapsedHeight, mNotificationStackScroller.getHeight());
+        }
+        return (int) totalHeight;
+    }
+
+    private void updateNotificationTranslucency() {
+        float alpha = 1f;
+        if (mClosingWithAlphaFadeOut && !mExpandingFromHeadsUp && !mHeadsUpManager.hasPinnedHeadsUp()) {
+            alpha = getFadeoutAlpha();
+        }
+        mNotificationStackScroller.setAlpha(alpha);
+    }
+
+    private float getFadeoutAlpha() {
+        float alpha = (getNotificationsTopY() + mNotificationStackScroller.getFirstItemMinHeight())
+                / mQsMinExpansionHeight;
+        alpha = Math.max(0, Math.min(alpha, 1));
+        alpha = (float) Math.pow(alpha, 0.75);
+        return alpha;
+    }
+
+    @Override
+    protected float getOverExpansionAmount() {
+        return mNotificationStackScroller.getCurrentOverScrollAmount(true /* top */);
+    }
+
+    @Override
+    protected float getOverExpansionPixels() {
+        return mNotificationStackScroller.getCurrentOverScrolledPixels(true /* top */);
+    }
+
+    private void updateUnlockIcon() {
+        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
+                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
+            boolean active = getMaxPanelHeight() - getExpandedHeight() > mUnlockMoveDistance;
+            KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon();
+            if (active && !mUnlockIconActive && mTracking) {
+                lockIcon.setImageAlpha(1.0f, true, 150, Interpolators.FAST_OUT_LINEAR_IN, null);
+                lockIcon.setImageScale(LOCK_ICON_ACTIVE_SCALE, true, 150,
+                        Interpolators.FAST_OUT_LINEAR_IN);
+            } else if (!active && mUnlockIconActive && mTracking) {
+                lockIcon.setImageAlpha(lockIcon.getRestingAlpha(), true /* animate */,
+                        150, Interpolators.FAST_OUT_LINEAR_IN, null);
+                lockIcon.setImageScale(1.0f, true, 150,
+                        Interpolators.FAST_OUT_LINEAR_IN);
+            }
+            mUnlockIconActive = active;
+        }
+    }
+
+    /**
+     * Hides the header when notifications are colliding with it.
+     */
+    private void updateHeader() {
+        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
+            updateHeaderKeyguardAlpha();
+        }
+        updateQsExpansion();
+    }
+
+    protected float getHeaderTranslation() {
+        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
+            return 0;
+        }
+        float translation = NotificationUtils.interpolate(-mQsMinExpansionHeight, 0,
+                mNotificationStackScroller.getAppearFraction(mExpandedHeight));
+        return Math.min(0, translation);
+    }
+
+    /**
+     * @return the alpha to be used to fade out the contents on Keyguard (status bar, bottom area)
+     *         during swiping up
+     */
+    private float getKeyguardContentsAlpha() {
+        float alpha;
+        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
+
+            // When on Keyguard, we hide the header as soon as the top card of the notification
+            // stack scroller is close enough (collision distance) to the bottom of the header.
+            alpha = getNotificationsTopY()
+                    /
+                    (mKeyguardStatusBar.getHeight() + mNotificationsHeaderCollideDistance);
+        } else {
+
+            // In SHADE_LOCKED, the top card is already really close to the header. Hide it as
+            // soon as we start translating the stack.
+            alpha = getNotificationsTopY() / mKeyguardStatusBar.getHeight();
+        }
+        alpha = MathUtils.constrain(alpha, 0, 1);
+        alpha = (float) Math.pow(alpha, 0.75);
+        return alpha;
+    }
+
+    private void updateHeaderKeyguardAlpha() {
+        float alphaQsExpansion = 1 - Math.min(1, getQsExpansionFraction() * 2);
+        mKeyguardStatusBar.setAlpha(Math.min(getKeyguardContentsAlpha(), alphaQsExpansion)
+                * mKeyguardStatusBarAnimateAlpha);
+        mKeyguardStatusBar.setVisibility(mKeyguardStatusBar.getAlpha() != 0f
+                && !mDozing ? VISIBLE : INVISIBLE);
+    }
+
+    private void updateKeyguardBottomAreaAlpha() {
+        float alpha = Math.min(getKeyguardContentsAlpha(), 1 - getQsExpansionFraction());
+        mKeyguardBottomArea.setAlpha(alpha);
+        mKeyguardBottomArea.setImportantForAccessibility(alpha == 0f
+                ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+        View ambientIndicationContainer = mStatusBar.getAmbientIndicationContainer();
+        if (ambientIndicationContainer != null) {
+            ambientIndicationContainer.setAlpha(alpha);
+        }
+    }
+
+    private float getNotificationsTopY() {
+        if (mNotificationStackScroller.getNotGoneChildCount() == 0) {
+            return getExpandedHeight();
+        }
+        return mNotificationStackScroller.getNotificationsTopY();
+    }
+
+    @Override
+    protected void onExpandingStarted() {
+        super.onExpandingStarted();
+        mNotificationStackScroller.onExpansionStarted();
+        mIsExpanding = true;
+        mQsExpandedWhenExpandingStarted = mQsFullyExpanded;
+        if (mQsExpanded) {
+            onQsExpansionStarted();
+        }
+        // Since there are QS tiles in the header now, we need to make sure we start listening
+        // immediately so they can be up to date.
+        if (mQs == null) return;
+        mQs.setHeaderListening(true);
+    }
+
+    @Override
+    protected void onExpandingFinished() {
+        super.onExpandingFinished();
+        mNotificationStackScroller.onExpansionStopped();
+        mHeadsUpManager.onExpandingFinished();
+        mIsExpanding = false;
+        if (isFullyCollapsed()) {
+            DejankUtils.postAfterTraversal(new Runnable() {
+                @Override
+                public void run() {
+                    setListening(false);
+                }
+            });
+
+            // Workaround b/22639032: Make sure we invalidate something because else RenderThread
+            // thinks we are actually drawing a frame put in reality we don't, so RT doesn't go
+            // ahead with rendering and we jank.
+            postOnAnimation(new Runnable() {
+                @Override
+                public void run() {
+                    getParent().invalidateChild(NotificationPanelView.this, mDummyDirtyRect);
+                }
+            });
+        } else {
+            setListening(true);
+        }
+        mQsExpandImmediate = false;
+        mTwoFingerQsExpandPossible = false;
+        mIsExpansionFromHeadsUp = false;
+        mNotificationStackScroller.setTrackingHeadsUp(false);
+        mExpandingFromHeadsUp = false;
+        setPanelScrimMinFraction(0.0f);
+    }
+
+    private void setListening(boolean listening) {
+        mKeyguardStatusBar.setListening(listening);
+        if (mQs == null) return;
+        mQs.setListening(listening);
+    }
+
+    @Override
+    public void expand(boolean animate) {
+        super.expand(animate);
+        setListening(true);
+    }
+
+    @Override
+    protected void setOverExpansion(float overExpansion, boolean isPixels) {
+        if (mConflictingQsExpansionGesture || mQsExpandImmediate) {
+            return;
+        }
+        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
+            mNotificationStackScroller.setOnHeightChangedListener(null);
+            if (isPixels) {
+                mNotificationStackScroller.setOverScrolledPixels(
+                        overExpansion, true /* onTop */, false /* animate */);
+            } else {
+                mNotificationStackScroller.setOverScrollAmount(
+                        overExpansion, true /* onTop */, false /* animate */);
+            }
+            mNotificationStackScroller.setOnHeightChangedListener(this);
+        }
+    }
+
+    @Override
+    protected void onTrackingStarted() {
+        mFalsingManager.onTrackingStarted();
+        super.onTrackingStarted();
+        if (mQsFullyExpanded) {
+            mQsExpandImmediate = true;
+        }
+        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
+                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
+            mAffordanceHelper.animateHideLeftRightIcon();
+        }
+        mNotificationStackScroller.onPanelTrackingStarted();
+    }
+
+    @Override
+    protected void onTrackingStopped(boolean expand) {
+        mFalsingManager.onTrackingStopped();
+        super.onTrackingStopped(expand);
+        if (expand) {
+            mNotificationStackScroller.setOverScrolledPixels(
+                    0.0f, true /* onTop */, true /* animate */);
+        }
+        mNotificationStackScroller.onPanelTrackingStopped();
+        if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
+                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
+            if (!mHintAnimationRunning) {
+                mAffordanceHelper.reset(true);
+            }
+        }
+        if (!expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
+                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
+            KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon();
+            lockIcon.setImageAlpha(0.0f, true, 100, Interpolators.FAST_OUT_LINEAR_IN, null);
+            lockIcon.setImageScale(2.0f, true, 100, Interpolators.FAST_OUT_LINEAR_IN);
+        }
+    }
+
+    @Override
+    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).
+        if (view == null && mQsExpanded) {
+            return;
+        }
+        ExpandableView firstChildNotGone = mNotificationStackScroller.getFirstChildNotGone();
+        ExpandableNotificationRow firstRow = firstChildNotGone instanceof ExpandableNotificationRow
+                ? (ExpandableNotificationRow) firstChildNotGone
+                : null;
+        if (firstRow != null
+                && (view == firstRow || (firstRow.getNotificationParent() == firstRow))) {
+            requestScrollerTopPaddingUpdate(false);
+        }
+        requestPanelHeightUpdate();
+    }
+
+    @Override
+    public void onReset(ExpandableView view) {
+    }
+
+    public void onQsHeightChanged() {
+        mQsMaxExpansionHeight = mQs != null ? mQs.getDesiredHeight() : 0;
+        if (mQsExpanded && mQsFullyExpanded) {
+            mQsExpansionHeight = mQsMaxExpansionHeight;
+            requestScrollerTopPaddingUpdate(false /* animate */);
+            requestPanelHeightUpdate();
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mAffordanceHelper.onConfigurationChanged();
+        if (newConfig.orientation != mLastOrientation) {
+            resetVerticalPanelPosition();
+        }
+        mLastOrientation = newConfig.orientation;
+    }
+
+    @Override
+    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+        mNavigationBarBottomHeight = insets.getStableInsetBottom();
+        updateMaxHeadsUpTranslation();
+        return insets;
+    }
+
+    private void updateMaxHeadsUpTranslation() {
+        mNotificationStackScroller.setHeadsUpBoundaries(getHeight(), mNavigationBarBottomHeight);
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(int layoutDirection) {
+        if (layoutDirection != mOldLayoutDirection) {
+            mAffordanceHelper.onRtlPropertiesChanged();
+            mOldLayoutDirection = layoutDirection;
+        }
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (v.getId() == R.id.expand_indicator) {
+            onQsExpansionStarted();
+            if (mQsExpanded) {
+                flingSettings(0 /* vel */, false /* expand */, null, true /* isClick */);
+            } else if (mQsExpansionEnabled) {
+                mLockscreenGestureLogger.write(MetricsEvent.ACTION_SHADE_QS_TAP, 0, 0);
+                flingSettings(0 /* vel */, true /* expand */, null, true /* isClick */);
+            }
+        }
+    }
+
+    @Override
+    public void onAnimationToSideStarted(boolean rightPage, float translation, float vel) {
+        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage;
+        mIsLaunchTransitionRunning = true;
+        mLaunchAnimationEndRunnable = null;
+        float displayDensity = mStatusBar.getDisplayDensity();
+        int lengthDp = Math.abs((int) (translation / displayDensity));
+        int velocityDp = Math.abs((int) (vel / displayDensity));
+        if (start) {
+            mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_DIALER, lengthDp, velocityDp);
+
+            mFalsingManager.onLeftAffordanceOn();
+            if (mFalsingManager.shouldEnforceBouncer()) {
+                mStatusBar.executeRunnableDismissingKeyguard(new Runnable() {
+                    @Override
+                    public void run() {
+                        mKeyguardBottomArea.launchLeftAffordance();
+                    }
+                }, null, true /* dismissShade */, false /* afterKeyguardGone */,
+                        true /* deferred */);
+            }
+            else {
+                mKeyguardBottomArea.launchLeftAffordance();
+            }
+        } else {
+            if (KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE.equals(
+                    mLastCameraLaunchSource)) {
+                mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_CAMERA, lengthDp, velocityDp);
+            }
+            mFalsingManager.onCameraOn();
+            if (mFalsingManager.shouldEnforceBouncer()) {
+                mStatusBar.executeRunnableDismissingKeyguard(new Runnable() {
+                    @Override
+                    public void run() {
+                        mKeyguardBottomArea.launchCamera(mLastCameraLaunchSource);
+                    }
+                }, null, true /* dismissShade */, false /* afterKeyguardGone */,
+                    true /* deferred */);
+            }
+            else {
+                mKeyguardBottomArea.launchCamera(mLastCameraLaunchSource);
+            }
+        }
+        mStatusBar.startLaunchTransitionTimeout();
+        mBlockTouches = true;
+    }
+
+    @Override
+    public void onAnimationToSideEnded() {
+        mIsLaunchTransitionRunning = false;
+        mIsLaunchTransitionFinished = true;
+        if (mLaunchAnimationEndRunnable != null) {
+            mLaunchAnimationEndRunnable.run();
+            mLaunchAnimationEndRunnable = null;
+        }
+        mStatusBar.readyForKeyguardDone();
+    }
+
+    @Override
+    protected void startUnlockHintAnimation() {
+        if (mPowerManager.isPowerSaveMode()) {
+            onUnlockHintStarted();
+            onUnlockHintFinished();
+            return;
+        }
+        super.startUnlockHintAnimation();
+        startHighlightIconAnimation(getCenterIcon());
+    }
+
+    /**
+     * Starts the highlight (making it fully opaque) animation on an icon.
+     */
+    private void startHighlightIconAnimation(final KeyguardAffordanceView icon) {
+        icon.setImageAlpha(1.0f, true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION,
+                Interpolators.FAST_OUT_SLOW_IN, new Runnable() {
+                    @Override
+                    public void run() {
+                        icon.setImageAlpha(icon.getRestingAlpha(),
+                                true /* animate */, KeyguardAffordanceHelper.HINT_PHASE1_DURATION,
+                                Interpolators.FAST_OUT_SLOW_IN, null);
+                    }
+                });
+    }
+
+    @Override
+    public float getMaxTranslationDistance() {
+        return (float) Math.hypot(getWidth(), getHeight());
+    }
+
+    @Override
+    public void onSwipingStarted(boolean rightIcon) {
+        mFalsingManager.onAffordanceSwipingStarted(rightIcon);
+        boolean camera = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? !rightIcon
+                : rightIcon;
+        if (camera) {
+            mKeyguardBottomArea.bindCameraPrewarmService();
+        }
+        requestDisallowInterceptTouchEvent(true);
+        mOnlyAffordanceInThisMotion = true;
+        mQsTracking = false;
+    }
+
+    @Override
+    public void onSwipingAborted() {
+        mFalsingManager.onAffordanceSwipingAborted();
+        mKeyguardBottomArea.unbindCameraPrewarmService(false /* launched */);
+    }
+
+    @Override
+    public void onIconClicked(boolean rightIcon) {
+        if (mHintAnimationRunning) {
+            return;
+        }
+        mHintAnimationRunning = true;
+        mAffordanceHelper.startHintAnimation(rightIcon, new Runnable() {
+            @Override
+            public void run() {
+                mHintAnimationRunning = false;
+                mStatusBar.onHintFinished();
+            }
+        });
+        rightIcon = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? !rightIcon : rightIcon;
+        if (rightIcon) {
+            mStatusBar.onCameraHintStarted();
+        } else {
+            if (mKeyguardBottomArea.isLeftVoiceAssist()) {
+                mStatusBar.onVoiceAssistHintStarted();
+            } else {
+                mStatusBar.onPhoneHintStarted();
+            }
+        }
+    }
+
+    @Override
+    protected void onUnlockHintFinished() {
+        super.onUnlockHintFinished();
+        mNotificationStackScroller.setUnlockHintRunning(false);
+    }
+
+    @Override
+    protected void onUnlockHintStarted() {
+        super.onUnlockHintStarted();
+        mNotificationStackScroller.setUnlockHintRunning(true);
+    }
+
+    @Override
+    public KeyguardAffordanceView getLeftIcon() {
+        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
+                ? mKeyguardBottomArea.getRightView()
+                : mKeyguardBottomArea.getLeftView();
+    }
+
+    @Override
+    public KeyguardAffordanceView getCenterIcon() {
+        return mKeyguardBottomArea.getLockIcon();
+    }
+
+    @Override
+    public KeyguardAffordanceView getRightIcon() {
+        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
+                ? mKeyguardBottomArea.getLeftView()
+                : mKeyguardBottomArea.getRightView();
+    }
+
+    @Override
+    public View getLeftPreview() {
+        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
+                ? mKeyguardBottomArea.getRightPreview()
+                : mKeyguardBottomArea.getLeftPreview();
+    }
+
+    @Override
+    public View getRightPreview() {
+        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
+                ? mKeyguardBottomArea.getLeftPreview()
+                : mKeyguardBottomArea.getRightPreview();
+    }
+
+    @Override
+    public float getAffordanceFalsingFactor() {
+        return mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
+    }
+
+    @Override
+    public boolean needsAntiFalsing() {
+        return mStatusBarState == StatusBarState.KEYGUARD;
+    }
+
+    @Override
+    protected float getPeekHeight() {
+        if (mNotificationStackScroller.getNotGoneChildCount() > 0) {
+            return mNotificationStackScroller.getPeekHeight();
+        } else {
+            return mQsMinExpansionHeight;
+        }
+    }
+
+    @Override
+    protected boolean shouldUseDismissingAnimation() {
+        return mStatusBarState != StatusBarState.SHADE
+                && (!mStatusBar.isKeyguardCurrentlySecure() || !isTracking());
+    }
+
+    @Override
+    protected boolean fullyExpandedClearAllVisible() {
+        return mNotificationStackScroller.isDismissViewNotGone()
+                && mNotificationStackScroller.isScrolledToBottom() && !mQsExpandImmediate;
+    }
+
+    @Override
+    protected boolean isClearAllVisible() {
+        return mNotificationStackScroller.isDismissViewVisible();
+    }
+
+    @Override
+    protected int getClearAllHeight() {
+        return mNotificationStackScroller.getDismissViewHeight();
+    }
+
+    @Override
+    protected boolean isTrackingBlocked() {
+        return mConflictingQsExpansionGesture && mQsExpanded;
+    }
+
+    public boolean isQsExpanded() {
+        return mQsExpanded;
+    }
+
+    public boolean isQsDetailShowing() {
+        return mQs.isShowingDetail();
+    }
+
+    public void closeQsDetail() {
+        mQs.closeDetail();
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return true;
+    }
+
+    public boolean isLaunchTransitionFinished() {
+        return mIsLaunchTransitionFinished;
+    }
+
+    public boolean isLaunchTransitionRunning() {
+        return mIsLaunchTransitionRunning;
+    }
+
+    public void setLaunchTransitionEndRunnable(Runnable r) {
+        mLaunchAnimationEndRunnable = r;
+    }
+
+    public void setEmptyDragAmount(float amount) {
+        float factor = 0.8f;
+        if (mNotificationStackScroller.getNotGoneChildCount() > 0) {
+            factor = 0.4f;
+        } else if (!mStatusBar.hasActiveNotifications()) {
+            factor = 0.4f;
+        }
+        mEmptyDragAmount = amount * factor;
+        positionClockAndNotifications();
+    }
+
+    private static float interpolate(float t, float start, float end) {
+        return (1 - t) * start + t * end;
+    }
+
+    public void setDozing(boolean dozing, boolean animate) {
+        if (dozing == mDozing) return;
+        mDozing = dozing;
+        if (mStatusBarState == StatusBarState.KEYGUARD) {
+            updateDozingVisibilities(animate);
+        }
+    }
+
+    private void updateDozingVisibilities(boolean animate) {
+        if (mDozing) {
+            mKeyguardStatusBar.setVisibility(View.INVISIBLE);
+            mKeyguardBottomArea.setDozing(mDozing, animate);
+        } else {
+            mKeyguardStatusBar.setVisibility(View.VISIBLE);
+            mKeyguardBottomArea.setDozing(mDozing, animate);
+            if (animate) {
+                animateKeyguardStatusBarIn(DOZE_ANIMATION_DURATION);
+            }
+        }
+    }
+
+    @Override
+    public boolean isDozing() {
+        return mDozing;
+    }
+
+    public void showEmptyShadeView(boolean emptyShadeViewVisible) {
+        mShowEmptyShadeView = emptyShadeViewVisible;
+        updateEmptyShadeView();
+    }
+
+    private void updateEmptyShadeView() {
+
+        // Hide "No notifications" in QS.
+        mNotificationStackScroller.updateEmptyShadeView(mShowEmptyShadeView && !mQsExpanded);
+    }
+
+    public void setQsScrimEnabled(boolean qsScrimEnabled) {
+        boolean changed = mQsScrimEnabled != qsScrimEnabled;
+        mQsScrimEnabled = qsScrimEnabled;
+        if (changed) {
+            updateQsState();
+        }
+    }
+
+    public void setKeyguardUserSwitcher(KeyguardUserSwitcher keyguardUserSwitcher) {
+        mKeyguardUserSwitcher = keyguardUserSwitcher;
+    }
+
+    public void onScreenTurningOn() {
+        mKeyguardStatusView.refreshTime();
+    }
+
+    @Override
+    public void onEmptySpaceClicked(float x, float y) {
+        onEmptySpaceClick(x);
+    }
+
+    @Override
+    protected boolean onMiddleClicked() {
+        switch (mStatusBar.getBarState()) {
+            case StatusBarState.KEYGUARD:
+                if (!mDozingOnDown) {
+                    mLockscreenGestureLogger.write(
+                            MetricsEvent.ACTION_LS_HINT,
+                            0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */);
+                    startUnlockHintAnimation();
+                }
+                return true;
+            case StatusBarState.SHADE_LOCKED:
+                if (!mQsExpanded) {
+                    mStatusBar.goToKeyguard();
+                }
+                return true;
+            case StatusBarState.SHADE:
+
+                // This gets called in the middle of the touch handling, where the state is still
+                // that we are tracking the panel. Collapse the panel after this is done.
+                post(mPostCollapseRunnable);
+                return false;
+            default:
+                return true;
+        }
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+        if (DEBUG) {
+            Paint p = new Paint();
+            p.setColor(Color.RED);
+            p.setStrokeWidth(2);
+            p.setStyle(Paint.Style.STROKE);
+            canvas.drawLine(0, getMaxPanelHeight(), getWidth(), getMaxPanelHeight(), p);
+            p.setColor(Color.BLUE);
+            canvas.drawLine(0, getExpandedHeight(), getWidth(), getExpandedHeight(), p);
+            p.setColor(Color.GREEN);
+            canvas.drawLine(0, calculatePanelHeightQsExpanded(), getWidth(),
+                    calculatePanelHeightQsExpanded(), p);
+            p.setColor(Color.YELLOW);
+            canvas.drawLine(0, calculatePanelHeightShade(), getWidth(),
+                    calculatePanelHeightShade(), p);
+            p.setColor(Color.MAGENTA);
+            canvas.drawLine(0, calculateQsTopPadding(), getWidth(),
+                    calculateQsTopPadding(), p);
+            p.setColor(Color.CYAN);
+            canvas.drawLine(0, mNotificationStackScroller.getTopPadding(), getWidth(),
+                    mNotificationStackScroller.getTopPadding(), p);
+        }
+    }
+
+    @Override
+    public void onHeadsUpPinnedModeChanged(final boolean inPinnedMode) {
+        mNotificationStackScroller.setInHeadsUpPinnedMode(inPinnedMode);
+        if (inPinnedMode) {
+            mHeadsUpExistenceChangedRunnable.run();
+            updateNotificationTranslucency();
+        } else {
+            setHeadsUpAnimatingAway(true);
+            mNotificationStackScroller.runAfterAnimationFinished(
+                    mHeadsUpExistenceChangedRunnable);
+        }
+    }
+
+    public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {
+        mHeadsUpAnimatingAway = headsUpAnimatingAway;
+        mNotificationStackScroller.setHeadsUpAnimatingAway(headsUpAnimatingAway);
+    }
+
+    @Override
+    public void onHeadsUpPinned(ExpandableNotificationRow headsUp) {
+        mNotificationStackScroller.generateHeadsUpAnimation(headsUp, true);
+    }
+
+    @Override
+    public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {
+    }
+
+    @Override
+    public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
+        mNotificationStackScroller.generateHeadsUpAnimation(entry.row, isHeadsUp);
+    }
+
+    @Override
+    public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+        super.setHeadsUpManager(headsUpManager);
+        mHeadsUpTouchHelper = new HeadsUpTouchHelper(headsUpManager, mNotificationStackScroller,
+                this);
+    }
+
+    public void setTrackingHeadsUp(boolean tracking) {
+        if (tracking) {
+            mNotificationStackScroller.setTrackingHeadsUp(true);
+            mExpandingFromHeadsUp = true;
+        }
+        // otherwise we update the state when the expansion is finished
+    }
+
+    @Override
+    protected void onClosingFinished() {
+        super.onClosingFinished();
+        resetVerticalPanelPosition();
+        setClosingWithAlphaFadeout(false);
+    }
+
+    private void setClosingWithAlphaFadeout(boolean closing) {
+        mClosingWithAlphaFadeOut = closing;
+        mNotificationStackScroller.forceNoOverlappingRendering(closing);
+    }
+
+    /**
+     * Updates the vertical position of the panel so it is positioned closer to the touch
+     * responsible for opening the panel.
+     *
+     * @param x the x-coordinate the touch event
+     */
+    protected void updateVerticalPanelPosition(float x) {
+        if (mNotificationStackScroller.getWidth() * 1.75f > getWidth()) {
+            resetVerticalPanelPosition();
+            return;
+        }
+        float leftMost = mPositionMinSideMargin + mNotificationStackScroller.getWidth() / 2;
+        float rightMost = getWidth() - mPositionMinSideMargin
+                - mNotificationStackScroller.getWidth() / 2;
+        if (Math.abs(x - getWidth() / 2) < mNotificationStackScroller.getWidth() / 4) {
+            x = getWidth() / 2;
+        }
+        x = Math.min(rightMost, Math.max(leftMost, x));
+        setVerticalPanelTranslation(x -
+                (mNotificationStackScroller.getLeft() + mNotificationStackScroller.getWidth() / 2));
+     }
+
+    private void resetVerticalPanelPosition() {
+        setVerticalPanelTranslation(0f);
+    }
+
+    protected void setVerticalPanelTranslation(float translation) {
+        mNotificationStackScroller.setTranslationX(translation);
+        mQsFrame.setTranslationX(translation);
+    }
+
+    protected void updateExpandedHeight(float expandedHeight) {
+        if (mTracking) {
+            mNotificationStackScroller.setExpandingVelocity(getCurrentExpandVelocity());
+        }
+        mNotificationStackScroller.setExpandedHeight(expandedHeight);
+        updateKeyguardBottomAreaAlpha();
+        updateStatusBarIcons();
+    }
+
+    /**
+     * @return whether the notifications are displayed full width and don't have any margins on
+     *         the side.
+     */
+    public boolean isFullWidth() {
+        return mIsFullWidth;
+    }
+
+    private void updateStatusBarIcons() {
+        boolean showIconsWhenExpanded = isFullWidth() && getExpandedHeight() < getOpeningHeight();
+        if (showIconsWhenExpanded && mNoVisibleNotifications && isOnKeyguard()) {
+            showIconsWhenExpanded = false;
+        }
+        if (showIconsWhenExpanded != mShowIconsWhenExpanded) {
+            mShowIconsWhenExpanded = showIconsWhenExpanded;
+            mStatusBar.recomputeDisableFlags(false);
+        }
+    }
+
+    private boolean isOnKeyguard() {
+        return mStatusBar.getBarState() == StatusBarState.KEYGUARD;
+    }
+
+    public void setPanelScrimMinFraction(float minFraction) {
+        mBar.panelScrimMinFractionChanged(minFraction);
+    }
+
+    public void clearNotificationEffects() {
+        mStatusBar.clearNotificationEffects();
+    }
+
+    @Override
+    protected boolean isPanelVisibleBecauseOfHeadsUp() {
+        return mHeadsUpManager.hasPinnedHeadsUp() || mHeadsUpAnimatingAway;
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return !mDozing;
+    }
+
+    public void launchCamera(boolean animate, int source) {
+        if (source == StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP) {
+            mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP;
+        } else if (source == StatusBarManager.CAMERA_LAUNCH_SOURCE_WIGGLE) {
+            mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_WIGGLE;
+        } else if (source == StatusBarManager.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER) {
+            mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER;
+        } else {
+
+            // Default.
+            mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE;
+        }
+
+        // If we are launching it when we are occluded already we don't want it to animate,
+        // nor setting these flags, since the occluded state doesn't change anymore, hence it's
+        // never reset.
+        if (!isFullyCollapsed()) {
+            mLaunchingAffordance = true;
+            setLaunchingAffordance(true);
+        } else {
+            animate = false;
+        }
+        mAffordanceHelper.launchAffordance(animate, getLayoutDirection() == LAYOUT_DIRECTION_RTL);
+    }
+
+    public void onAffordanceLaunchEnded() {
+        mLaunchingAffordance = false;
+        setLaunchingAffordance(false);
+    }
+
+    @Override
+    public void setAlpha(float alpha) {
+        super.setAlpha(alpha);
+        updateFullyVisibleState(false /* forceNotFullyVisible */);
+    }
+
+    /**
+     * Must be called before starting a ViewPropertyAnimator alpha animation because those
+     * do NOT call setAlpha and therefore don't properly update the fullyVisibleState.
+     */
+    public void notifyStartFading() {
+        updateFullyVisibleState(true /* forceNotFullyVisible */);
+    }
+
+    @Override
+    public void setVisibility(int visibility) {
+        super.setVisibility(visibility);
+        updateFullyVisibleState(false /* forceNotFullyVisible */);
+    }
+
+    private void updateFullyVisibleState(boolean forceNotFullyVisible) {
+        mNotificationStackScroller.setParentNotFullyVisible(forceNotFullyVisible
+                || getAlpha() != 1.0f
+                || getVisibility() != VISIBLE);
+    }
+
+    /**
+     * Set whether we are currently launching an affordance. This is currently only set when
+     * launched via a camera gesture.
+     */
+    private void setLaunchingAffordance(boolean launchingAffordance) {
+        getLeftIcon().setLaunchingAffordance(launchingAffordance);
+        getRightIcon().setLaunchingAffordance(launchingAffordance);
+        getCenterIcon().setLaunchingAffordance(launchingAffordance);
+    }
+
+    /**
+     * Whether the camera application can be launched for the camera launch gesture.
+     *
+     * @param keyguardIsShowing whether keyguard is being shown
+     */
+    public boolean canCameraGestureBeLaunched(boolean keyguardIsShowing) {
+        if (!mStatusBar.isCameraAllowedByAdmin()) {
+            return false;
+        }
+
+        ResolveInfo resolveInfo = mKeyguardBottomArea.resolveCameraIntent();
+        String packageToLaunch = (resolveInfo == null || resolveInfo.activityInfo == null)
+                ? null : resolveInfo.activityInfo.packageName;
+        return packageToLaunch != null &&
+               (keyguardIsShowing || !isForegroundApp(packageToLaunch)) &&
+               !mAffordanceHelper.isSwipingInProgress();
+    }
+
+    /**
+     * Return true if the applications with the package name is running in foreground.
+     *
+     * @param pkgName application package name.
+     */
+    private boolean isForegroundApp(String pkgName) {
+        ActivityManager am = getContext().getSystemService(ActivityManager.class);
+        List<ActivityManager.RunningTaskInfo> tasks = am.getRunningTasks(1);
+        return !tasks.isEmpty() && pkgName.equals(tasks.get(0).topActivity.getPackageName());
+    }
+
+    public void setGroupManager(NotificationGroupManager groupManager) {
+        mGroupManager = groupManager;
+    }
+
+    public boolean hideStatusBarIconsWhenExpanded() {
+        return !isFullWidth() || !mShowIconsWhenExpanded;
+    }
+
+    private final FragmentListener mFragmentListener = new FragmentListener() {
+        @Override
+        public void onFragmentViewCreated(String tag, Fragment fragment) {
+            mQs = (QS) fragment;
+            mQs.setPanelView(NotificationPanelView.this);
+            mQs.setExpandClickListener(NotificationPanelView.this);
+            mQs.setHeaderClickable(mQsExpansionEnabled);
+            mQs.setKeyguardShowing(mKeyguardShowing);
+            mQs.setOverscrolling(mStackScrollerOverscrolling);
+
+            // recompute internal state when qspanel height changes
+            mQs.getView().addOnLayoutChangeListener(
+                    (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+                        final int height = bottom - top;
+                        final int oldHeight = oldBottom - oldTop;
+                        if (height != oldHeight) {
+                            onQsHeightChanged();
+                        }
+                    });
+            mNotificationStackScroller.setQsContainer((ViewGroup) mQs.getView());
+            updateQsExpansion();
+        }
+
+        @Override
+        public void onFragmentViewDestroyed(String tag, Fragment fragment) {
+            // Manual handling of fragment lifecycle is only required because this bridges
+            // non-fragment and fragment code. Once we are using a fragment for the notification
+            // panel, mQs will not need to be null cause it will be tied to the same lifecycle.
+            if (fragment == mQs) {
+                mQs = null;
+            }
+        }
+    };
+
+    @Override
+    public void setTouchDisabled(boolean disabled) {
+        super.setTouchDisabled(disabled);
+        if (disabled && mAffordanceHelper.isSwipingInProgress() && !mIsLaunchTransitionRunning) {
+            mAffordanceHelper.reset(false /* animate */);
+        }
+    }
+
+    public void setDark(boolean dark, boolean animate) {
+        float darkAmount = dark ? 1 : 0;
+        if (mDarkAmount == darkAmount) {
+            return;
+        }
+        if (mDarkAnimator != null && mDarkAnimator.isRunning()) {
+            if (animate && mDarkAmountTarget == darkAmount) {
+                return;
+            } else {
+                mDarkAnimator.cancel();
+            }
+        }
+        mDarkAmountTarget = darkAmount;
+        if (animate) {
+            mDarkAnimator = ObjectAnimator.ofFloat(this, SET_DARK_AMOUNT_PROPERTY, darkAmount);
+            mDarkAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+            mDarkAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_WAKEUP);
+            mDarkAnimator.start();
+        } else {
+            setDarkAmount(darkAmount);
+        }
+    }
+
+    private void setDarkAmount(float amount) {
+        mDarkAmount = amount;
+        mKeyguardStatusView.setDark(mDarkAmount);
+        positionClockAndNotifications();
+    }
+
+    public void setNoVisibleNotifications(boolean noNotifications) {
+        mNoVisibleNotifications = noNotifications;
+        if (mQs != null) {
+            mQs.setHasNotifications(!noNotifications);
+        }
+    }
+
+    public void setPulsing(boolean pulsing) {
+        mKeyguardStatusView.setPulsing(pulsing);
+    }
+
+    public void setAmbientIndicationBottomPadding(int ambientIndicationBottomPadding) {
+        if (mAmbientIndicationBottomPadding != ambientIndicationBottomPadding) {
+            mAmbientIndicationBottomPadding = ambientIndicationBottomPadding;
+            mStatusBar.updateKeyguardMaxNotifications();
+        }
+    }
+
+    public void refreshTime() {
+        mKeyguardStatusView.refreshTime();
+        if (mDarkAmount > 0) {
+            positionClockAndNotifications();
+        }
+    }
+
+    public void setStatusAccessibilityImportance(int mode) {
+         mKeyguardStatusView.setImportantForAccessibility(mode);
+    }
+
+    /**
+     * TODO: this should be removed.
+     * It's not correct to pass this view forward because other classes will end up adding
+     * children to it. Theme will be out of sync.
+     * @return bottom area view
+     */
+    public KeyguardBottomAreaView getKeyguardBottomAreaView() {
+        return mKeyguardBottomArea;
+    }
+
+    public void setUserSetupComplete(boolean userSetupComplete) {
+        mUserSetupComplete = userSetupComplete;
+        mKeyguardBottomArea.setUserSetupComplete(userSetupComplete);
+    }
+
+    public LockIcon getLockIcon() {
+        return mKeyguardBottomArea.getLockIcon();
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java b/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
new file mode 100644
index 0000000..76cc0ff
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Canvas;
+import android.support.annotation.DimenRes;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewStub;
+import android.view.ViewStub.OnInflateListener;
+import android.view.WindowInsets;
+import android.widget.FrameLayout;
+
+import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.statusbar.NotificationData.Entry;
+import com.android.systemui.statusbar.notification.AboveShelfObserver;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+/**
+ * The container with notification stack scroller and quick settings inside.
+ */
+public class NotificationsQuickSettingsContainer extends FrameLayout
+        implements OnInflateListener, FragmentListener,
+        AboveShelfObserver.HasViewAboveShelfChangedListener {
+
+    private FrameLayout mQsFrame;
+    private View mUserSwitcher;
+    private NotificationStackScrollLayout mStackScroller;
+    private View mKeyguardStatusBar;
+    private boolean mInflated;
+    private boolean mQsExpanded;
+    private boolean mCustomizerAnimating;
+
+    private int mBottomPadding;
+    private int mStackScrollerMargin;
+    private boolean mHasViewsAboveShelf;
+
+    public NotificationsQuickSettingsContainer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mQsFrame = (FrameLayout) findViewById(R.id.qs_frame);
+        mStackScroller = findViewById(R.id.notification_stack_scroller);
+        mStackScrollerMargin = ((LayoutParams) mStackScroller.getLayoutParams()).bottomMargin;
+        mKeyguardStatusBar = findViewById(R.id.keyguard_header);
+        ViewStub userSwitcher = (ViewStub) findViewById(R.id.keyguard_user_switcher);
+        userSwitcher.setOnInflateListener(this);
+        mUserSwitcher = userSwitcher;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        FragmentHostManager.get(this).addTagListener(QS.TAG, this);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        FragmentHostManager.get(this).removeTagListener(QS.TAG, this);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        reloadWidth(mQsFrame, R.dimen.qs_panel_width);
+        reloadWidth(mStackScroller, R.dimen.notification_panel_width);
+    }
+
+    /**
+     * Loads the given width resource and sets it on the given View.
+     */
+    private void reloadWidth(View view, @DimenRes int width) {
+        LayoutParams params = (LayoutParams) view.getLayoutParams();
+        params.width = getResources().getDimensionPixelSize(width);
+        view.setLayoutParams(params);
+    }
+
+    @Override
+    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+        mBottomPadding = insets.getStableInsetBottom();
+        setPadding(0, 0, 0, mBottomPadding);
+        return insets;
+    }
+
+    @Override
+    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+        boolean userSwitcherVisible = mInflated && mUserSwitcher.getVisibility() == View.VISIBLE;
+        boolean statusBarVisible = mKeyguardStatusBar.getVisibility() == View.VISIBLE;
+
+        final boolean qsBottom = mHasViewsAboveShelf;
+        View stackQsTop = qsBottom ? mStackScroller : mQsFrame;
+        View stackQsBottom = !qsBottom ? mStackScroller : mQsFrame;
+        // Invert the order of the scroll view and user switcher such that the notifications receive
+        // touches first but the panel gets drawn above.
+        if (child == mQsFrame) {
+            return super.drawChild(canvas, userSwitcherVisible && statusBarVisible ? mUserSwitcher
+                    : statusBarVisible ? mKeyguardStatusBar
+                    : userSwitcherVisible ? mUserSwitcher
+                    : stackQsBottom, drawingTime);
+        } else if (child == mStackScroller) {
+            return super.drawChild(canvas,
+                    userSwitcherVisible && statusBarVisible ? mKeyguardStatusBar
+                    : statusBarVisible || userSwitcherVisible ? stackQsBottom
+                    : stackQsTop,
+                    drawingTime);
+        } else if (child == mUserSwitcher) {
+            return super.drawChild(canvas,
+                    userSwitcherVisible && statusBarVisible ? stackQsBottom
+                    : stackQsTop,
+                    drawingTime);
+        } else if (child == mKeyguardStatusBar) {
+            return super.drawChild(canvas,
+                    stackQsTop,
+                    drawingTime);
+        } else {
+            return super.drawChild(canvas, child, drawingTime);
+        }
+    }
+
+    @Override
+    public void onInflate(ViewStub stub, View inflated) {
+        if (stub == mUserSwitcher) {
+            mUserSwitcher = inflated;
+            mInflated = true;
+        }
+    }
+
+    @Override
+    public void onFragmentViewCreated(String tag, Fragment fragment) {
+        QS container = (QS) fragment;
+        container.setContainer(this);
+    }
+
+    public void setQsExpanded(boolean expanded) {
+        if (mQsExpanded != expanded) {
+            mQsExpanded = expanded;
+            invalidate();
+        }
+    }
+
+    public void setCustomizerAnimating(boolean isAnimating) {
+        if (mCustomizerAnimating != isAnimating) {
+            mCustomizerAnimating = isAnimating;
+            invalidate();
+        }
+    }
+
+    public void setCustomizerShowing(boolean isShowing) {
+        if (isShowing) {
+            // Clear out bottom paddings/margins so the qs customization can be full height.
+            setPadding(0, 0, 0, 0);
+            setBottomMargin(mStackScroller, 0);
+        } else {
+            setPadding(0, 0, 0, mBottomPadding);
+            setBottomMargin(mStackScroller, mStackScrollerMargin);
+        }
+        mStackScroller.setQsCustomizerShowing(isShowing);
+    }
+
+    private void setBottomMargin(View v, int bottomMargin) {
+        LayoutParams params = (LayoutParams) v.getLayoutParams();
+        params.bottomMargin = bottomMargin;
+        v.setLayoutParams(params);
+    }
+
+    @Override
+    public void onHasViewsAboveShelfChanged(boolean hasViewsAboveShelf) {
+        mHasViewsAboveShelf = hasViewsAboveShelf;
+        invalidate();
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/ObservableScrollView.java b/com/android/systemui/statusbar/phone/ObservableScrollView.java
new file mode 100644
index 0000000..9e5cefd
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/ObservableScrollView.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ScrollView;
+
+/**
+ * A scroll view which can be observed for scroll change events.
+ */
+public class ObservableScrollView extends ScrollView {
+
+    private Listener mListener;
+    private int mLastOverscrollAmount;
+    private boolean mTouchEnabled = true;
+    private boolean mHandlingTouchEvent;
+    private float mLastX;
+    private float mLastY;
+    private boolean mBlockFlinging;
+    private boolean mTouchCancelled;
+
+    public ObservableScrollView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public void setTouchEnabled(boolean touchEnabled) {
+        mTouchEnabled = touchEnabled;
+    }
+
+    public boolean isScrolledToBottom() {
+        return getScrollY() == getMaxScrollY();
+    }
+
+    public boolean isHandlingTouchEvent() {
+        return mHandlingTouchEvent;
+    }
+
+    private int getMaxScrollY() {
+        int scrollRange = 0;
+        if (getChildCount() > 0) {
+            View child = getChildAt(0);
+            scrollRange = Math.max(0,
+                    child.getHeight() - (getHeight() - mPaddingBottom - mPaddingTop));
+        }
+        return scrollRange;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        mHandlingTouchEvent = true;
+        mLastX = ev.getX();
+        mLastY = ev.getY();
+        boolean result = super.onTouchEvent(ev);
+        mHandlingTouchEvent = false;
+        return result;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        mHandlingTouchEvent = true;
+        mLastX = ev.getX();
+        mLastY = ev.getY();
+        boolean result = super.onInterceptTouchEvent(ev);
+        mHandlingTouchEvent = false;
+        return result;
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            if (!mTouchEnabled) {
+                mTouchCancelled = true;
+                return false;
+            }
+            mTouchCancelled = false;
+        } else if (mTouchCancelled) {
+            return false;
+        } else if (!mTouchEnabled) {
+            MotionEvent cancel = MotionEvent.obtain(ev);
+            cancel.setAction(MotionEvent.ACTION_CANCEL);
+            super.dispatchTouchEvent(cancel);
+            cancel.recycle();
+            mTouchCancelled = true;
+            return false;
+        }
+        return super.dispatchTouchEvent(ev);
+    }
+
+    @Override
+    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+        super.onScrollChanged(l, t, oldl, oldt);
+        if (mListener != null) {
+            mListener.onScrollChanged();
+        }
+    }
+
+    @Override
+    protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY,
+            int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY,
+            boolean isTouchEvent) {
+        mLastOverscrollAmount = Math.max(0, scrollY + deltaY - getMaxScrollY());
+        return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY,
+                        maxOverScrollX, maxOverScrollY, isTouchEvent);
+    }
+
+    public void setBlockFlinging(boolean blockFlinging) {
+        mBlockFlinging = blockFlinging;
+    }
+
+    @Override
+    public void fling(int velocityY) {
+        if (!mBlockFlinging) {
+            super.fling(velocityY);
+        }
+    }
+
+    @Override
+    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
+        super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
+        if (mListener != null && mLastOverscrollAmount > 0) {
+            mListener.onOverscrolled(mLastX, mLastY, mLastOverscrollAmount);
+        }
+    }
+
+    public interface Listener {
+        void onScrollChanged();
+        void onOverscrolled(float lastX, float lastY, int amount);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/PanelBar.java b/com/android/systemui/statusbar/phone/PanelBar.java
new file mode 100644
index 0000000..cefe972
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/PanelBar.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2012 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.phone;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+public abstract class PanelBar extends FrameLayout {
+    public static final boolean DEBUG = false;
+    public static final String TAG = PanelBar.class.getSimpleName();
+    private static final boolean SPEW = false;
+
+    public static final void LOG(String fmt, Object... args) {
+        if (!DEBUG) return;
+        Log.v(TAG, String.format(fmt, args));
+    }
+
+    public static final int STATE_CLOSED = 0;
+    public static final int STATE_OPENING = 1;
+    public static final int STATE_OPEN = 2;
+
+    PanelView mPanel;
+    private int mState = STATE_CLOSED;
+    private boolean mTracking;
+
+    public void go(int state) {
+        if (DEBUG) LOG("go state: %d -> %d", mState, state);
+        mState = state;
+    }
+
+    public int getState() {
+        return mState;
+    }
+
+    public PanelBar(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+    }
+
+    public void setPanel(PanelView pv) {
+        mPanel = pv;
+        pv.setBar(this);
+    }
+
+    public void setBouncerShowing(boolean showing) {
+        int important = showing ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                : IMPORTANT_FOR_ACCESSIBILITY_AUTO;
+
+        setImportantForAccessibility(important);
+
+        if (mPanel != null) mPanel.setImportantForAccessibility(important);
+    }
+
+    public boolean panelEnabled() {
+        return true;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        // Allow subclasses to implement enable/disable semantics
+        if (!panelEnabled()) {
+            if (event.getAction() == MotionEvent.ACTION_DOWN) {
+                Log.v(TAG, String.format("onTouch: all panels disabled, ignoring touch at (%d,%d)",
+                        (int) event.getX(), (int) event.getY()));
+            }
+            return false;
+        }
+
+        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+            final PanelView panel = mPanel;
+            if (panel == null) {
+                // panel is not there, so we'll eat the gesture
+                Log.v(TAG, String.format("onTouch: no panel for touch at (%d,%d)",
+                        (int) event.getX(), (int) event.getY()));
+                return true;
+            }
+            boolean enabled = panel.isEnabled();
+            if (DEBUG) LOG("PanelBar.onTouch: state=%d ACTION_DOWN: panel %s %s", mState, panel,
+                    (enabled ? "" : " (disabled)"));
+            if (!enabled) {
+                // panel is disabled, so we'll eat the gesture
+                Log.v(TAG, String.format(
+                        "onTouch: panel (%s) is disabled, ignoring touch at (%d,%d)",
+                        panel, (int) event.getX(), (int) event.getY()));
+                return true;
+            }
+        }
+        return mPanel == null || mPanel.onTouchEvent(event);
+    }
+
+    public abstract void panelScrimMinFractionChanged(float minFraction);
+
+    /**
+     * @param frac the fraction from the expansion in [0, 1]
+     * @param expanded whether the panel is currently expanded; this is independent from the
+     *                 fraction as the panel also might be expanded if the fraction is 0
+     */
+    public void panelExpansionChanged(float frac, boolean expanded) {
+        boolean fullyClosed = true;
+        boolean fullyOpened = false;
+        if (SPEW) LOG("panelExpansionChanged: start state=%d", mState);
+        PanelView pv = mPanel;
+        pv.setVisibility(expanded ? VISIBLE : INVISIBLE);
+        // adjust any other panels that may be partially visible
+        if (expanded) {
+            if (mState == STATE_CLOSED) {
+                go(STATE_OPENING);
+                onPanelPeeked();
+            }
+            fullyClosed = false;
+            final float thisFrac = pv.getExpandedFraction();
+            if (SPEW) LOG("panelExpansionChanged:  -> %s: f=%.1f", pv.getName(), thisFrac);
+            fullyOpened = thisFrac >= 1f;
+        }
+        if (fullyOpened && !mTracking) {
+            go(STATE_OPEN);
+            onPanelFullyOpened();
+        } else if (fullyClosed && !mTracking && mState != STATE_CLOSED) {
+            go(STATE_CLOSED);
+            onPanelCollapsed();
+        }
+
+        if (SPEW) LOG("panelExpansionChanged: end state=%d [%s%s ]", mState,
+                fullyOpened?" fullyOpened":"", fullyClosed?" fullyClosed":"");
+    }
+
+    public void collapsePanel(boolean animate, boolean delayed, float speedUpFactor) {
+        boolean waiting = false;
+        PanelView pv = mPanel;
+        if (animate && !pv.isFullyCollapsed()) {
+            pv.collapse(delayed, speedUpFactor);
+            waiting = true;
+        } else {
+            pv.resetViews();
+            pv.setExpandedFraction(0); // just in case
+            pv.cancelPeek();
+        }
+        if (DEBUG) LOG("collapsePanel: animate=%s waiting=%s", animate, waiting);
+        if (!waiting && mState != STATE_CLOSED) {
+            // it's possible that nothing animated, so we replicate the termination
+            // conditions of panelExpansionChanged here
+            go(STATE_CLOSED);
+            onPanelCollapsed();
+        }
+    }
+
+    public void onPanelPeeked() {
+        if (DEBUG) LOG("onPanelPeeked");
+    }
+
+    public boolean isClosed() {
+        return mState == STATE_CLOSED;
+    }
+
+    public void onPanelCollapsed() {
+        if (DEBUG) LOG("onPanelCollapsed");
+    }
+
+    public void onPanelFullyOpened() {
+        if (DEBUG) LOG("onPanelFullyOpened");
+    }
+
+    public void onTrackingStarted() {
+        mTracking = true;
+    }
+
+    public void onTrackingStopped(boolean expand) {
+        mTracking = false;
+    }
+
+    public void onExpandingFinished() {
+        if (DEBUG) LOG("onExpandingFinished");
+    }
+
+    public void onClosingFinished() {
+
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/PanelView.java b/com/android/systemui/statusbar/phone/PanelView.java
new file mode 100644
index 0000000..afe5c91
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/PanelView.java
@@ -0,0 +1,1237 @@
+/*
+ * Copyright (C) 2012 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.phone;
+
+import static com.android.systemui.statusbar.notification.NotificationUtils.isHapticFeedbackDisabled;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.AsyncTask;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewTreeObserver;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.keyguard.LatencyTracker;
+import com.android.systemui.DejankUtils;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.doze.DozeLog;
+import com.android.systemui.statusbar.FlingAnimationUtils;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+public abstract class PanelView extends FrameLayout {
+    public static final boolean DEBUG = PanelBar.DEBUG;
+    public static final String TAG = PanelView.class.getSimpleName();
+    private static final int INITIAL_OPENING_PEEK_DURATION = 200;
+    private static final int PEEK_ANIMATION_DURATION = 360;
+    private long mDownTime;
+    private float mMinExpandHeight;
+    private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
+    private boolean mPanelUpdateWhenAnimatorEnds;
+    private boolean mVibrateOnOpening;
+
+    private final void logf(String fmt, Object... args) {
+        Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
+    }
+
+    protected StatusBar mStatusBar;
+    protected HeadsUpManager mHeadsUpManager;
+
+    private float mPeekHeight;
+    private float mHintDistance;
+    private float mInitialOffsetOnTouch;
+    private boolean mCollapsedAndHeadsUpOnDown;
+    private float mExpandedFraction = 0;
+    protected float mExpandedHeight = 0;
+    private boolean mPanelClosedOnDown;
+    private boolean mHasLayoutedSinceDown;
+    private float mUpdateFlingVelocity;
+    private boolean mUpdateFlingOnLayout;
+    private boolean mPeekTouching;
+    private boolean mJustPeeked;
+    private boolean mClosing;
+    protected boolean mTracking;
+    private boolean mTouchSlopExceeded;
+    private int mTrackingPointer;
+    protected int mTouchSlop;
+    protected boolean mHintAnimationRunning;
+    private boolean mOverExpandedBeforeFling;
+    private boolean mTouchAboveFalsingThreshold;
+    private int mUnlockFalsingThreshold;
+    private boolean mTouchStartedInEmptyArea;
+    private boolean mMotionAborted;
+    private boolean mUpwardsWhenTresholdReached;
+    private boolean mAnimatingOnDown;
+
+    private ValueAnimator mHeightAnimator;
+    private ObjectAnimator mPeekAnimator;
+    private VelocityTrackerInterface mVelocityTracker;
+    private FlingAnimationUtils mFlingAnimationUtils;
+    private FlingAnimationUtils mFlingAnimationUtilsClosing;
+    private FlingAnimationUtils mFlingAnimationUtilsDismissing;
+    private FalsingManager mFalsingManager;
+    private final Vibrator mVibrator;
+
+    /**
+     * Whether an instant expand request is currently pending and we are just waiting for layout.
+     */
+    private boolean mInstantExpanding;
+    private boolean mAnimateAfterExpanding;
+
+    PanelBar mBar;
+
+    private String mViewName;
+    private float mInitialTouchY;
+    private float mInitialTouchX;
+    private boolean mTouchDisabled;
+
+    /**
+     * Whether or not the PanelView can be expanded or collapsed with a drag.
+     */
+    private boolean mNotificationsDragEnabled;
+
+    private Interpolator mBounceInterpolator;
+    protected KeyguardBottomAreaView mKeyguardBottomArea;
+
+    /**
+     * Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time.
+     */
+    private float mNextCollapseSpeedUpFactor = 1.0f;
+
+    protected boolean mExpanding;
+    private boolean mGestureWaitForTouchSlop;
+    private boolean mIgnoreXTouchSlop;
+    private boolean mExpandLatencyTracking;
+
+    protected void onExpandingFinished() {
+        mBar.onExpandingFinished();
+    }
+
+    protected void onExpandingStarted() {
+    }
+
+    private void notifyExpandingStarted() {
+        if (!mExpanding) {
+            mExpanding = true;
+            onExpandingStarted();
+        }
+    }
+
+    protected final void notifyExpandingFinished() {
+        endClosing();
+        if (mExpanding) {
+            mExpanding = false;
+            onExpandingFinished();
+        }
+    }
+
+    private void runPeekAnimation(long duration, float peekHeight, boolean collapseWhenFinished) {
+        mPeekHeight = peekHeight;
+        if (DEBUG) logf("peek to height=%.1f", mPeekHeight);
+        if (mHeightAnimator != null) {
+            return;
+        }
+        if (mPeekAnimator != null) {
+            mPeekAnimator.cancel();
+        }
+        mPeekAnimator = ObjectAnimator.ofFloat(this, "expandedHeight", mPeekHeight)
+                .setDuration(duration);
+        mPeekAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+        mPeekAnimator.addListener(new AnimatorListenerAdapter() {
+            private boolean mCancelled;
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mCancelled = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mPeekAnimator = null;
+                if (!mCancelled && collapseWhenFinished) {
+                    postOnAnimation(mPostCollapseRunnable);
+                }
+
+            }
+        });
+        notifyExpandingStarted();
+        mPeekAnimator.start();
+        mJustPeeked = true;
+    }
+
+    public PanelView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mFlingAnimationUtils = new FlingAnimationUtils(context, 0.6f /* maxLengthSeconds */,
+                0.6f /* speedUpFactor */);
+        mFlingAnimationUtilsClosing = new FlingAnimationUtils(context, 0.5f /* maxLengthSeconds */,
+                0.6f /* speedUpFactor */);
+        mFlingAnimationUtilsDismissing = new FlingAnimationUtils(context,
+                0.5f /* maxLengthSeconds */, 0.2f /* speedUpFactor */, 0.6f /* x2 */,
+                0.84f /* y2 */);
+        mBounceInterpolator = new BounceInterpolator();
+        mFalsingManager = FalsingManager.getInstance(context);
+        mNotificationsDragEnabled =
+                getResources().getBoolean(R.bool.config_enableNotificationShadeDrag);
+        mVibrator = mContext.getSystemService(Vibrator.class);
+        mVibrateOnOpening = mContext.getResources().getBoolean(
+                R.bool.config_vibrateOnIconAnimation);
+    }
+
+    protected void loadDimens() {
+        final Resources res = getContext().getResources();
+        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
+        mTouchSlop = configuration.getScaledTouchSlop();
+        mHintDistance = res.getDimension(R.dimen.hint_move_distance);
+        mUnlockFalsingThreshold = res.getDimensionPixelSize(R.dimen.unlock_falsing_threshold);
+    }
+
+    private void trackMovement(MotionEvent event) {
+        // Add movement to velocity tracker using raw screen X and Y coordinates instead
+        // of window coordinates because the window frame may be moving at the same time.
+        float deltaX = event.getRawX() - event.getX();
+        float deltaY = event.getRawY() - event.getY();
+        event.offsetLocation(deltaX, deltaY);
+        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
+        event.offsetLocation(-deltaX, -deltaY);
+    }
+
+    public void setTouchDisabled(boolean disabled) {
+        mTouchDisabled = disabled;
+        if (mTouchDisabled) {
+            cancelHeightAnimator();
+            if (mTracking) {
+                onTrackingStopped(true /* expanded */);
+            }
+            notifyExpandingFinished();
+        }
+    }
+
+    public void startExpandLatencyTracking() {
+        if (LatencyTracker.isEnabled(mContext)) {
+            LatencyTracker.getInstance(mContext).onActionStart(
+                    LatencyTracker.ACTION_EXPAND_PANEL);
+            mExpandLatencyTracking = true;
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mInstantExpanding || mTouchDisabled
+                || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
+            return false;
+        }
+
+        // If dragging should not expand the notifications shade, then return false.
+        if (!mNotificationsDragEnabled) {
+            if (mTracking) {
+                // Turn off tracking if it's on or the shade can get stuck in the down position.
+                onTrackingStopped(true /* expand */);
+            }
+            return false;
+        }
+
+        // On expanding, single mouse click expands the panel instead of dragging.
+        if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+            if (event.getAction() == MotionEvent.ACTION_UP) {
+                expand(true);
+            }
+            return true;
+        }
+
+        /*
+         * We capture touch events here and update the expand height here in case according to
+         * the users fingers. This also handles multi-touch.
+         *
+         * If the user just clicks shortly, we show a quick peek of the shade.
+         *
+         * Flinging is also enabled in order to open or close the shade.
+         */
+
+        int pointerIndex = event.findPointerIndex(mTrackingPointer);
+        if (pointerIndex < 0) {
+            pointerIndex = 0;
+            mTrackingPointer = event.getPointerId(pointerIndex);
+        }
+        final float x = event.getX(pointerIndex);
+        final float y = event.getY(pointerIndex);
+
+        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            mGestureWaitForTouchSlop = isFullyCollapsed() || hasConflictingGestures();
+            mIgnoreXTouchSlop = isFullyCollapsed() || shouldGestureIgnoreXTouchSlop(x, y);
+        }
+
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
+                mJustPeeked = false;
+                mMinExpandHeight = 0.0f;
+                mPanelClosedOnDown = isFullyCollapsed();
+                mHasLayoutedSinceDown = false;
+                mUpdateFlingOnLayout = false;
+                mMotionAborted = false;
+                mPeekTouching = mPanelClosedOnDown;
+                mDownTime = SystemClock.uptimeMillis();
+                mTouchAboveFalsingThreshold = false;
+                mCollapsedAndHeadsUpOnDown = isFullyCollapsed()
+                        && mHeadsUpManager.hasPinnedHeadsUp();
+                if (mVelocityTracker == null) {
+                    initVelocityTracker();
+                }
+                trackMovement(event);
+                if (!mGestureWaitForTouchSlop || (mHeightAnimator != null && !mHintAnimationRunning)
+                        || mPeekAnimator != null) {
+                    mTouchSlopExceeded = (mHeightAnimator != null && !mHintAnimationRunning)
+                            || mPeekAnimator != null;
+                    cancelHeightAnimator();
+                    cancelPeek();
+                    onTrackingStarted();
+                }
+                if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()) {
+                    startOpening();
+                }
+                break;
+
+            case MotionEvent.ACTION_POINTER_UP:
+                final int upPointer = event.getPointerId(event.getActionIndex());
+                if (mTrackingPointer == upPointer) {
+                    // gesture is ongoing, find a new pointer to track
+                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+                    final float newY = event.getY(newIndex);
+                    final float newX = event.getX(newIndex);
+                    mTrackingPointer = event.getPointerId(newIndex);
+                    startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN:
+                if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
+                    mMotionAborted = true;
+                    endMotionEvent(event, x, y, true /* forceCancel */);
+                    return false;
+                }
+                break;
+            case MotionEvent.ACTION_MOVE:
+                trackMovement(event);
+                float h = y - mInitialTouchY;
+
+                // If the panel was collapsed when touching, we only need to check for the
+                // y-component of the gesture, as we have no conflicting horizontal gesture.
+                if (Math.abs(h) > mTouchSlop
+                        && (Math.abs(h) > Math.abs(x - mInitialTouchX)
+                        || mIgnoreXTouchSlop)) {
+                    mTouchSlopExceeded = true;
+                    if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) {
+                        if (!mJustPeeked && mInitialOffsetOnTouch != 0f) {
+                            startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
+                            h = 0;
+                        }
+                        cancelHeightAnimator();
+                        onTrackingStarted();
+                    }
+                }
+                float newHeight = Math.max(0, h + mInitialOffsetOnTouch);
+                if (newHeight > mPeekHeight) {
+                    if (mPeekAnimator != null) {
+                        mPeekAnimator.cancel();
+                    }
+                    mJustPeeked = false;
+                } else if (mPeekAnimator == null && mJustPeeked) {
+                    // The initial peek has finished, but we haven't dragged as far yet, lets
+                    // speed it up by starting at the peek height.
+                    mInitialOffsetOnTouch = mExpandedHeight;
+                    mInitialTouchY = y;
+                    mMinExpandHeight = mExpandedHeight;
+                    mJustPeeked = false;
+                }
+                newHeight = Math.max(newHeight, mMinExpandHeight);
+                if (-h >= getFalsingThreshold()) {
+                    mTouchAboveFalsingThreshold = true;
+                    mUpwardsWhenTresholdReached = isDirectionUpwards(x, y);
+                }
+                if (!mJustPeeked && (!mGestureWaitForTouchSlop || mTracking) &&
+                        !isTrackingBlocked()) {
+                    setExpandedHeightInternal(newHeight);
+                }
+                break;
+
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                trackMovement(event);
+                endMotionEvent(event, x, y, false /* forceCancel */);
+                break;
+        }
+        return !mGestureWaitForTouchSlop || mTracking;
+    }
+
+    private void startOpening() {;
+        runPeekAnimation(INITIAL_OPENING_PEEK_DURATION, getOpeningHeight(),
+                false /* collapseWhenFinished */);
+        notifyBarPanelExpansionChanged();
+        if (mVibrateOnOpening && !isHapticFeedbackDisabled(mContext)) {
+            AsyncTask.execute(() ->
+                    mVibrator.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_TICK, false)));
+        }
+    }
+
+    protected abstract float getOpeningHeight();
+
+    /**
+     * @return whether the swiping direction is upwards and above a 45 degree angle compared to the
+     * horizontal direction
+     */
+    private boolean isDirectionUpwards(float x, float y) {
+        float xDiff = x - mInitialTouchX;
+        float yDiff = y - mInitialTouchY;
+        if (yDiff >= 0) {
+            return false;
+        }
+        return Math.abs(yDiff) >= Math.abs(xDiff);
+    }
+
+    protected void startExpandingFromPeek() {
+        mStatusBar.handlePeekToExpandTransistion();
+    }
+
+    protected void startExpandMotion(float newX, float newY, boolean startTracking,
+            float expandedHeight) {
+        mInitialOffsetOnTouch = expandedHeight;
+        mInitialTouchY = newY;
+        mInitialTouchX = newX;
+        if (startTracking) {
+            mTouchSlopExceeded = true;
+            setExpandedHeight(mInitialOffsetOnTouch);
+            onTrackingStarted();
+        }
+    }
+
+    private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) {
+        mTrackingPointer = -1;
+        if ((mTracking && mTouchSlopExceeded)
+                || Math.abs(x - mInitialTouchX) > mTouchSlop
+                || Math.abs(y - mInitialTouchY) > mTouchSlop
+                || event.getActionMasked() == MotionEvent.ACTION_CANCEL
+                || forceCancel) {
+            float vel = 0f;
+            float vectorVel = 0f;
+            if (mVelocityTracker != null) {
+                mVelocityTracker.computeCurrentVelocity(1000);
+                vel = mVelocityTracker.getYVelocity();
+                vectorVel = (float) Math.hypot(
+                        mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+            }
+            boolean expand = flingExpands(vel, vectorVel, x, y)
+                    || event.getActionMasked() == MotionEvent.ACTION_CANCEL
+                    || forceCancel;
+            DozeLog.traceFling(expand, mTouchAboveFalsingThreshold,
+                    mStatusBar.isFalsingThresholdNeeded(),
+                    mStatusBar.isWakeUpComingFromTouch());
+                    // Log collapse gesture if on lock screen.
+                    if (!expand && mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
+                        float displayDensity = mStatusBar.getDisplayDensity();
+                        int heightDp = (int) Math.abs((y - mInitialTouchY) / displayDensity);
+                        int velocityDp = (int) Math.abs(vel / displayDensity);
+                        mLockscreenGestureLogger.write(
+                                MetricsEvent.ACTION_LS_UNLOCK,
+                                heightDp, velocityDp);
+                    }
+            fling(vel, expand, isFalseTouch(x, y));
+            onTrackingStopped(expand);
+            mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown;
+            if (mUpdateFlingOnLayout) {
+                mUpdateFlingVelocity = vel;
+            }
+        } else if (mPanelClosedOnDown && !mHeadsUpManager.hasPinnedHeadsUp() && !mTracking) {
+            long timePassed = SystemClock.uptimeMillis() - mDownTime;
+            if (timePassed < ViewConfiguration.getLongPressTimeout()) {
+                // Lets show the user that he can actually expand the panel
+                runPeekAnimation(PEEK_ANIMATION_DURATION, getPeekHeight(), true /* collapseWhenFinished */);
+            } else {
+                // We need to collapse the panel since we peeked to the small height.
+                postOnAnimation(mPostCollapseRunnable);
+            }
+        } else {
+            boolean expands = onEmptySpaceClick(mInitialTouchX);
+            onTrackingStopped(expands);
+        }
+
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+        mPeekTouching = false;
+    }
+
+    protected float getCurrentExpandVelocity() {
+        if (mVelocityTracker == null) {
+            return 0;
+        }
+        mVelocityTracker.computeCurrentVelocity(1000);
+        return mVelocityTracker.getYVelocity();
+    }
+
+    private int getFalsingThreshold() {
+        float factor = mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
+        return (int) (mUnlockFalsingThreshold * factor);
+    }
+
+    protected abstract boolean hasConflictingGestures();
+
+    protected abstract boolean shouldGestureIgnoreXTouchSlop(float x, float y);
+
+    protected void onTrackingStopped(boolean expand) {
+        mTracking = false;
+        mBar.onTrackingStopped(expand);
+        notifyBarPanelExpansionChanged();
+    }
+
+    protected void onTrackingStarted() {
+        endClosing();
+        mTracking = true;
+        mBar.onTrackingStarted();
+        notifyExpandingStarted();
+        notifyBarPanelExpansionChanged();
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled
+                || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
+            return false;
+        }
+
+        /*
+         * If the user drags anywhere inside the panel we intercept it if the movement is
+         * upwards. This allows closing the shade from anywhere inside the panel.
+         *
+         * We only do this if the current content is scrolled to the bottom,
+         * i.e isScrolledToBottom() is true and therefore there is no conflicting scrolling gesture
+         * possible.
+         */
+        int pointerIndex = event.findPointerIndex(mTrackingPointer);
+        if (pointerIndex < 0) {
+            pointerIndex = 0;
+            mTrackingPointer = event.getPointerId(pointerIndex);
+        }
+        final float x = event.getX(pointerIndex);
+        final float y = event.getY(pointerIndex);
+        boolean scrolledToBottom = isScrolledToBottom();
+
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                mStatusBar.userActivity();
+                mAnimatingOnDown = mHeightAnimator != null;
+                mMinExpandHeight = 0.0f;
+                mDownTime = SystemClock.uptimeMillis();
+                if (mAnimatingOnDown && mClosing && !mHintAnimationRunning
+                        || mPeekAnimator != null) {
+                    cancelHeightAnimator();
+                    cancelPeek();
+                    mTouchSlopExceeded = true;
+                    return true;
+                }
+                mInitialTouchY = y;
+                mInitialTouchX = x;
+                mTouchStartedInEmptyArea = !isInContentBounds(x, y);
+                mTouchSlopExceeded = false;
+                mJustPeeked = false;
+                mMotionAborted = false;
+                mPanelClosedOnDown = isFullyCollapsed();
+                mCollapsedAndHeadsUpOnDown = false;
+                mHasLayoutedSinceDown = false;
+                mUpdateFlingOnLayout = false;
+                mTouchAboveFalsingThreshold = false;
+                initVelocityTracker();
+                trackMovement(event);
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                final int upPointer = event.getPointerId(event.getActionIndex());
+                if (mTrackingPointer == upPointer) {
+                    // gesture is ongoing, find a new pointer to track
+                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+                    mTrackingPointer = event.getPointerId(newIndex);
+                    mInitialTouchX = event.getX(newIndex);
+                    mInitialTouchY = event.getY(newIndex);
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN:
+                if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
+                    mMotionAborted = true;
+                    if (mVelocityTracker != null) {
+                        mVelocityTracker.recycle();
+                        mVelocityTracker = null;
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_MOVE:
+                final float h = y - mInitialTouchY;
+                trackMovement(event);
+                if (scrolledToBottom || mTouchStartedInEmptyArea || mAnimatingOnDown) {
+                    float hAbs = Math.abs(h);
+                    if ((h < -mTouchSlop || (mAnimatingOnDown && hAbs > mTouchSlop))
+                            && hAbs > Math.abs(x - mInitialTouchX)) {
+                        cancelHeightAnimator();
+                        startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
+                        return true;
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                if (mVelocityTracker != null) {
+                    mVelocityTracker.recycle();
+                    mVelocityTracker = null;
+                }
+                break;
+        }
+        return false;
+    }
+
+    /**
+     * @return Whether a pair of coordinates are inside the visible view content bounds.
+     */
+    protected abstract boolean isInContentBounds(float x, float y);
+
+    protected void cancelHeightAnimator() {
+        if (mHeightAnimator != null) {
+            if (mHeightAnimator.isRunning()) {
+                mPanelUpdateWhenAnimatorEnds = false;
+            }
+            mHeightAnimator.cancel();
+        }
+        endClosing();
+    }
+
+    private void endClosing() {
+        if (mClosing) {
+            mClosing = false;
+            onClosingFinished();
+        }
+    }
+
+    private void initVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+        }
+        mVelocityTracker = VelocityTrackerFactory.obtain(getContext());
+    }
+
+    protected boolean isScrolledToBottom() {
+        return true;
+    }
+
+    protected float getContentHeight() {
+        return mExpandedHeight;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        loadDimens();
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        loadDimens();
+    }
+
+    /**
+     * @param vel the current vertical velocity of the motion
+     * @param vectorVel the length of the vectorial velocity
+     * @return whether a fling should expands the panel; contracts otherwise
+     */
+    protected boolean flingExpands(float vel, float vectorVel, float x, float y) {
+        if (isFalseTouch(x, y)) {
+            return true;
+        }
+        if (Math.abs(vectorVel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
+            return getExpandedFraction() > 0.5f;
+        } else {
+            return vel > 0;
+        }
+    }
+
+    /**
+     * @param x the final x-coordinate when the finger was lifted
+     * @param y the final y-coordinate when the finger was lifted
+     * @return whether this motion should be regarded as a false touch
+     */
+    private boolean isFalseTouch(float x, float y) {
+        if (!mStatusBar.isFalsingThresholdNeeded()) {
+            return false;
+        }
+        if (mFalsingManager.isClassiferEnabled()) {
+            return mFalsingManager.isFalseTouch();
+        }
+        if (!mTouchAboveFalsingThreshold) {
+            return true;
+        }
+        if (mUpwardsWhenTresholdReached) {
+            return false;
+        }
+        return !isDirectionUpwards(x, y);
+    }
+
+    protected void fling(float vel, boolean expand) {
+        fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false);
+    }
+
+    protected void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) {
+        fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing);
+    }
+
+    protected void fling(float vel, boolean expand, float collapseSpeedUpFactor,
+            boolean expandBecauseOfFalsing) {
+        cancelPeek();
+        float target = expand ? getMaxPanelHeight() : 0;
+        if (!expand) {
+            mClosing = true;
+        }
+        flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
+    }
+
+    protected void flingToHeight(float vel, boolean expand, float target,
+            float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
+        // Hack to make the expand transition look nice when clear all button is visible - we make
+        // the animation only to the last notification, and then jump to the maximum panel height so
+        // clear all just fades in and the decelerating motion is towards the last notification.
+        final boolean clearAllExpandHack = expand && fullyExpandedClearAllVisible()
+                && mExpandedHeight < getMaxPanelHeight() - getClearAllHeight()
+                && !isClearAllVisible();
+        if (clearAllExpandHack) {
+            target = getMaxPanelHeight() - getClearAllHeight();
+        }
+        if (target == mExpandedHeight || getOverExpansionAmount() > 0f && expand) {
+            notifyExpandingFinished();
+            return;
+        }
+        mOverExpandedBeforeFling = getOverExpansionAmount() > 0f;
+        ValueAnimator animator = createHeightAnimator(target);
+        if (expand) {
+            if (expandBecauseOfFalsing && vel < 0) {
+                vel = 0;
+            }
+            mFlingAnimationUtils.apply(animator, mExpandedHeight, target, vel, getHeight());
+            if (vel == 0) {
+                animator.setDuration(350);
+            }
+        } else {
+            if (shouldUseDismissingAnimation()) {
+                if (vel == 0) {
+                    animator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
+                    long duration = (long) (200 + mExpandedHeight / getHeight() * 100);
+                    animator.setDuration(duration);
+                } else {
+                    mFlingAnimationUtilsDismissing.apply(animator, mExpandedHeight, target, vel,
+                            getHeight());
+                }
+            } else {
+                mFlingAnimationUtilsClosing
+                        .apply(animator, mExpandedHeight, target, vel, getHeight());
+            }
+
+            // Make it shorter if we run a canned animation
+            if (vel == 0) {
+                animator.setDuration((long) (animator.getDuration() / collapseSpeedUpFactor));
+            }
+        }
+        animator.addListener(new AnimatorListenerAdapter() {
+            private boolean mCancelled;
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mCancelled = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (clearAllExpandHack && !mCancelled) {
+                    setExpandedHeightInternal(getMaxPanelHeight());
+                }
+                setAnimator(null);
+                if (!mCancelled) {
+                    notifyExpandingFinished();
+                }
+                notifyBarPanelExpansionChanged();
+            }
+        });
+        setAnimator(animator);
+        animator.start();
+    }
+
+    protected abstract boolean shouldUseDismissingAnimation();
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mViewName = getResources().getResourceName(getId());
+    }
+
+    public String getName() {
+        return mViewName;
+    }
+
+    public void setExpandedHeight(float height) {
+        if (DEBUG) logf("setExpandedHeight(%.1f)", height);
+        setExpandedHeightInternal(height + getOverExpansionPixels());
+    }
+
+    @Override
+    protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        mStatusBar.onPanelLaidOut();
+        requestPanelHeightUpdate();
+        mHasLayoutedSinceDown = true;
+        if (mUpdateFlingOnLayout) {
+            abortAnimations();
+            fling(mUpdateFlingVelocity, true /* expands */);
+            mUpdateFlingOnLayout = false;
+        }
+    }
+
+    protected void requestPanelHeightUpdate() {
+        float currentMaxPanelHeight = getMaxPanelHeight();
+
+        if (isFullyCollapsed()) {
+            return;
+        }
+
+        if (currentMaxPanelHeight == mExpandedHeight) {
+            return;
+        }
+
+        if (mPeekAnimator != null || mPeekTouching) {
+            return;
+        }
+
+        if (mTracking && !isTrackingBlocked()) {
+            return;
+        }
+
+        if (mHeightAnimator != null) {
+            mPanelUpdateWhenAnimatorEnds = true;
+            return;
+        }
+
+        setExpandedHeight(currentMaxPanelHeight);
+    }
+
+    public void setExpandedHeightInternal(float h) {
+        if (mExpandLatencyTracking && h != 0f) {
+            DejankUtils.postAfterTraversal(() -> LatencyTracker.getInstance(mContext).onActionEnd(
+                    LatencyTracker.ACTION_EXPAND_PANEL));
+            mExpandLatencyTracking = false;
+        }
+        float fhWithoutOverExpansion = getMaxPanelHeight() - getOverExpansionAmount();
+        if (mHeightAnimator == null) {
+            float overExpansionPixels = Math.max(0, h - fhWithoutOverExpansion);
+            if (getOverExpansionPixels() != overExpansionPixels && mTracking) {
+                setOverExpansion(overExpansionPixels, true /* isPixels */);
+            }
+            mExpandedHeight = Math.min(h, fhWithoutOverExpansion) + getOverExpansionAmount();
+        } else {
+            mExpandedHeight = h;
+            if (mOverExpandedBeforeFling) {
+                setOverExpansion(Math.max(0, h - fhWithoutOverExpansion), false /* isPixels */);
+            }
+        }
+
+        // If we are closing the panel and we are almost there due to a slow decelerating
+        // interpolator, abort the animation.
+        if (mExpandedHeight < 1f && mExpandedHeight != 0f && mClosing) {
+            mExpandedHeight = 0f;
+            if (mHeightAnimator != null) {
+                mHeightAnimator.end();
+            }
+        }
+        mExpandedFraction = Math.min(1f,
+                fhWithoutOverExpansion == 0 ? 0 : mExpandedHeight / fhWithoutOverExpansion);
+        onHeightUpdated(mExpandedHeight);
+        notifyBarPanelExpansionChanged();
+    }
+
+    /**
+     * @return true if the panel tracking should be temporarily blocked; this is used when a
+     *         conflicting gesture (opening QS) is happening
+     */
+    protected abstract boolean isTrackingBlocked();
+
+    protected abstract void setOverExpansion(float overExpansion, boolean isPixels);
+
+    protected abstract void onHeightUpdated(float expandedHeight);
+
+    protected abstract float getOverExpansionAmount();
+
+    protected abstract float getOverExpansionPixels();
+
+    /**
+     * This returns the maximum height of the panel. Children should override this if their
+     * desired height is not the full height.
+     *
+     * @return the default implementation simply returns the maximum height.
+     */
+    protected abstract int getMaxPanelHeight();
+
+    public void setExpandedFraction(float frac) {
+        setExpandedHeight(getMaxPanelHeight() * frac);
+    }
+
+    public float getExpandedHeight() {
+        return mExpandedHeight;
+    }
+
+    public float getExpandedFraction() {
+        return mExpandedFraction;
+    }
+
+    public boolean isFullyExpanded() {
+        return mExpandedHeight >= getMaxPanelHeight();
+    }
+
+    public boolean isFullyCollapsed() {
+        return mExpandedFraction <= 0.0f;
+    }
+
+    public boolean isCollapsing() {
+        return mClosing;
+    }
+
+    public boolean isTracking() {
+        return mTracking;
+    }
+
+    public void setBar(PanelBar panelBar) {
+        mBar = panelBar;
+    }
+
+    public void collapse(boolean delayed, float speedUpFactor) {
+        if (DEBUG) logf("collapse: " + this);
+        if (canPanelBeCollapsed()) {
+            cancelHeightAnimator();
+            notifyExpandingStarted();
+
+            // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state.
+            mClosing = true;
+            if (delayed) {
+                mNextCollapseSpeedUpFactor = speedUpFactor;
+                postDelayed(mFlingCollapseRunnable, 120);
+            } else {
+                fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */);
+            }
+        }
+    }
+
+    public boolean canPanelBeCollapsed() {
+        return !isFullyCollapsed() && !mTracking && !mClosing;
+    }
+
+    private final Runnable mFlingCollapseRunnable = new Runnable() {
+        @Override
+        public void run() {
+            fling(0, false /* expand */, mNextCollapseSpeedUpFactor,
+                    false /* expandBecauseOfFalsing */);
+        }
+    };
+
+    public void cancelPeek() {
+        boolean cancelled = false;
+        if (mPeekAnimator != null) {
+            cancelled = true;
+            mPeekAnimator.cancel();
+        }
+
+        if (cancelled) {
+            // When peeking, we already tell mBar that we expanded ourselves. Make sure that we also
+            // notify mBar that we might have closed ourselves.
+            notifyBarPanelExpansionChanged();
+        }
+    }
+
+    public void expand(final boolean animate) {
+        if (!isFullyCollapsed() && !isCollapsing()) {
+            return;
+        }
+
+        mInstantExpanding = true;
+        mAnimateAfterExpanding = animate;
+        mUpdateFlingOnLayout = false;
+        abortAnimations();
+        cancelPeek();
+        if (mTracking) {
+            onTrackingStopped(true /* expands */); // The panel is expanded after this call.
+        }
+        if (mExpanding) {
+            notifyExpandingFinished();
+        }
+        notifyBarPanelExpansionChanged();
+
+        // Wait for window manager to pickup the change, so we know the maximum height of the panel
+        // then.
+        getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        if (!mInstantExpanding) {
+                            getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                            return;
+                        }
+                        if (mStatusBar.getStatusBarWindow().getHeight()
+                                != mStatusBar.getStatusBarHeight()) {
+                            getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                            if (mAnimateAfterExpanding) {
+                                notifyExpandingStarted();
+                                fling(0, true /* expand */);
+                            } else {
+                                setExpandedFraction(1f);
+                            }
+                            mInstantExpanding = false;
+                        }
+                    }
+                });
+
+        // Make sure a layout really happens.
+        requestLayout();
+    }
+
+    public void instantCollapse() {
+        abortAnimations();
+        setExpandedFraction(0f);
+        if (mExpanding) {
+            notifyExpandingFinished();
+        }
+        if (mInstantExpanding) {
+            mInstantExpanding = false;
+            notifyBarPanelExpansionChanged();
+        }
+    }
+
+    private void abortAnimations() {
+        cancelPeek();
+        cancelHeightAnimator();
+        removeCallbacks(mPostCollapseRunnable);
+        removeCallbacks(mFlingCollapseRunnable);
+    }
+
+    protected void onClosingFinished() {
+        mBar.onClosingFinished();
+    }
+
+
+    protected void startUnlockHintAnimation() {
+
+        // We don't need to hint the user if an animation is already running or the user is changing
+        // the expansion.
+        if (mHeightAnimator != null || mTracking) {
+            return;
+        }
+        cancelPeek();
+        notifyExpandingStarted();
+        startUnlockHintAnimationPhase1(new Runnable() {
+            @Override
+            public void run() {
+                notifyExpandingFinished();
+                onUnlockHintFinished();
+                mHintAnimationRunning = false;
+            }
+        });
+        onUnlockHintStarted();
+        mHintAnimationRunning = true;
+    }
+
+    protected void onUnlockHintFinished() {
+        mStatusBar.onHintFinished();
+    }
+
+    protected void onUnlockHintStarted() {
+        mStatusBar.onUnlockHintStarted();
+    }
+
+    /**
+     * Phase 1: Move everything upwards.
+     */
+    private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) {
+        float target = Math.max(0, getMaxPanelHeight() - mHintDistance);
+        ValueAnimator animator = createHeightAnimator(target);
+        animator.setDuration(250);
+        animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        animator.addListener(new AnimatorListenerAdapter() {
+            private boolean mCancelled;
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mCancelled = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mCancelled) {
+                    setAnimator(null);
+                    onAnimationFinished.run();
+                } else {
+                    startUnlockHintAnimationPhase2(onAnimationFinished);
+                }
+            }
+        });
+        animator.start();
+        setAnimator(animator);
+
+        View[] viewsToAnimate = {
+                mKeyguardBottomArea.getIndicationArea(),
+                mStatusBar.getAmbientIndicationContainer()};
+        for (View v : viewsToAnimate) {
+            if (v == null) {
+                continue;
+            }
+            v.animate()
+                    .translationY(-mHintDistance)
+                    .setDuration(250)
+                    .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                    .withEndAction(() -> v.animate()
+                            .translationY(0)
+                            .setDuration(450)
+                            .setInterpolator(mBounceInterpolator)
+                            .start())
+                    .start();
+        }
+    }
+
+    private void setAnimator(ValueAnimator animator) {
+        mHeightAnimator = animator;
+        if (animator == null && mPanelUpdateWhenAnimatorEnds) {
+            mPanelUpdateWhenAnimatorEnds = false;
+            requestPanelHeightUpdate();
+        }
+    }
+
+    /**
+     * Phase 2: Bounce down.
+     */
+    private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) {
+        ValueAnimator animator = createHeightAnimator(getMaxPanelHeight());
+        animator.setDuration(450);
+        animator.setInterpolator(mBounceInterpolator);
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                setAnimator(null);
+                onAnimationFinished.run();
+                notifyBarPanelExpansionChanged();
+            }
+        });
+        animator.start();
+        setAnimator(animator);
+    }
+
+    private ValueAnimator createHeightAnimator(float targetHeight) {
+        ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight);
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                setExpandedHeightInternal((Float) animation.getAnimatedValue());
+            }
+        });
+        return animator;
+    }
+
+    protected void notifyBarPanelExpansionChanged() {
+        mBar.panelExpansionChanged(mExpandedFraction, mExpandedFraction > 0f
+                || mPeekAnimator != null || mInstantExpanding || isPanelVisibleBecauseOfHeadsUp()
+                || mTracking || mHeightAnimator != null);
+    }
+
+    protected abstract boolean isPanelVisibleBecauseOfHeadsUp();
+
+    /**
+     * Gets called when the user performs a click anywhere in the empty area of the panel.
+     *
+     * @return whether the panel will be expanded after the action performed by this method
+     */
+    protected boolean onEmptySpaceClick(float x) {
+        if (mHintAnimationRunning) {
+            return true;
+        }
+        return onMiddleClicked();
+    }
+
+    protected final Runnable mPostCollapseRunnable = new Runnable() {
+        @Override
+        public void run() {
+            collapse(false /* delayed */, 1.0f /* speedUpFactor */);
+        }
+    };
+
+    protected abstract boolean onMiddleClicked();
+
+    protected abstract boolean isDozing();
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
+                + " tracking=%s justPeeked=%s peekAnim=%s%s timeAnim=%s%s touchDisabled=%s"
+                + "]",
+                this.getClass().getSimpleName(),
+                getExpandedHeight(),
+                getMaxPanelHeight(),
+                mClosing?"T":"f",
+                mTracking?"T":"f",
+                mJustPeeked?"T":"f",
+                mPeekAnimator, ((mPeekAnimator!=null && mPeekAnimator.isStarted())?" (started)":""),
+                mHeightAnimator, ((mHeightAnimator !=null && mHeightAnimator.isStarted())?" (started)":""),
+                mTouchDisabled?"T":"f"
+        ));
+    }
+
+    public abstract void resetViews();
+
+    protected abstract float getPeekHeight();
+    /**
+     * @return whether "Clear all" button will be visible when the panel is fully expanded
+     */
+    protected abstract boolean fullyExpandedClearAllVisible();
+
+    protected abstract boolean isClearAllVisible();
+
+    /**
+     * @return the height of the clear all button, in pixels
+     */
+    protected abstract int getClearAllHeight();
+
+    public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+        mHeadsUpManager = headsUpManager;
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
new file mode 100644
index 0000000..4ae1393
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -0,0 +1,785 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.StackId;
+import android.app.ActivityManager.StackInfo;
+import android.app.AlarmManager;
+import android.app.AlarmManager.AlarmClockInfo;
+import android.app.AppGlobals;
+import android.app.Notification;
+import android.app.Notification.Action;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.SynchronousUserSwitchObserver;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.graphics.drawable.Icon;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.service.notification.StatusBarNotification;
+import android.telecom.TelecomManager;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Pair;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.internal.telephony.IccCardConstants;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.systemui.Dependency;
+import com.android.systemui.DockedStackExistsListener;
+import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.UiOffloadThread;
+import com.android.systemui.qs.tiles.DndTile;
+import com.android.systemui.qs.tiles.RotationLockTile;
+import com.android.systemui.recents.misc.SystemServicesProxy;
+import com.android.systemui.recents.misc.SystemServicesProxy.TaskStackListener;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.CommandQueue.Callbacks;
+import com.android.systemui.statusbar.policy.BluetoothController;
+import com.android.systemui.statusbar.policy.BluetoothController.Callback;
+import com.android.systemui.statusbar.policy.CastController;
+import com.android.systemui.statusbar.policy.CastController.CastDevice;
+import com.android.systemui.statusbar.policy.DataSaverController;
+import com.android.systemui.statusbar.policy.DataSaverController.Listener;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
+import com.android.systemui.statusbar.policy.HotspotController;
+import com.android.systemui.statusbar.policy.KeyguardMonitor;
+import com.android.systemui.statusbar.policy.LocationController;
+import com.android.systemui.statusbar.policy.LocationController.LocationChangeCallback;
+import com.android.systemui.statusbar.policy.NextAlarmController;
+import com.android.systemui.statusbar.policy.RotationLockController;
+import com.android.systemui.statusbar.policy.RotationLockController.RotationLockControllerCallback;
+import com.android.systemui.statusbar.policy.UserInfoController;
+import com.android.systemui.statusbar.policy.ZenModeController;
+import com.android.systemui.util.NotificationChannels;
+
+import java.util.List;
+
+/**
+ * This class contains all of the policy about which icons are installed in the status
+ * bar at boot time.  It goes through the normal API for icons, even though it probably
+ * strictly doesn't need to.
+ */
+public class PhoneStatusBarPolicy implements Callback, Callbacks,
+        RotationLockControllerCallback, Listener, LocationChangeCallback,
+        ZenModeController.Callback, DeviceProvisionedListener, KeyguardMonitor.Callback {
+    private static final String TAG = "PhoneStatusBarPolicy";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    public static final int LOCATION_STATUS_ICON_ID = R.drawable.stat_sys_location;
+    public static final int NUM_TASKS_FOR_INSTANT_APP_INFO = 5;
+
+    private final String mSlotCast;
+    private final String mSlotHotspot;
+    private final String mSlotBluetooth;
+    private final String mSlotTty;
+    private final String mSlotZen;
+    private final String mSlotVolume;
+    private final String mSlotAlarmClock;
+    private final String mSlotManagedProfile;
+    private final String mSlotRotate;
+    private final String mSlotHeadset;
+    private final String mSlotDataSaver;
+    private final String mSlotLocation;
+
+    private final Context mContext;
+    private final Handler mHandler = new Handler();
+    private final CastController mCast;
+    private final HotspotController mHotspot;
+    private final NextAlarmController mNextAlarm;
+    private final AlarmManager mAlarmManager;
+    private final UserInfoController mUserInfoController;
+    private final UserManager mUserManager;
+    private final StatusBarIconController mIconController;
+    private final RotationLockController mRotationLockController;
+    private final DataSaverController mDataSaver;
+    private final ZenModeController mZenController;
+    private final DeviceProvisionedController mProvisionedController;
+    private final KeyguardMonitor mKeyguardMonitor;
+    private final LocationController mLocationController;
+    private final ArraySet<Pair<String, Integer>> mCurrentNotifs = new ArraySet<>();
+    private final UiOffloadThread mUiOffloadThread = Dependency.get(UiOffloadThread.class);
+
+    // Assume it's all good unless we hear otherwise.  We don't always seem
+    // to get broadcasts that it *is* there.
+    IccCardConstants.State mSimState = IccCardConstants.State.READY;
+
+    private boolean mZenVisible;
+    private boolean mVolumeVisible;
+    private boolean mCurrentUserSetup;
+    private boolean mDockedStackExists;
+
+    private boolean mManagedProfileIconVisible = false;
+    private boolean mManagedProfileInQuietMode = false;
+
+    private BluetoothController mBluetooth;
+
+    public PhoneStatusBarPolicy(Context context, StatusBarIconController iconController) {
+        mContext = context;
+        mIconController = iconController;
+        mCast = Dependency.get(CastController.class);
+        mHotspot = Dependency.get(HotspotController.class);
+        mBluetooth = Dependency.get(BluetoothController.class);
+        mNextAlarm = Dependency.get(NextAlarmController.class);
+        mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+        mUserInfoController = Dependency.get(UserInfoController.class);
+        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+        mRotationLockController = Dependency.get(RotationLockController.class);
+        mDataSaver = Dependency.get(DataSaverController.class);
+        mZenController = Dependency.get(ZenModeController.class);
+        mProvisionedController = Dependency.get(DeviceProvisionedController.class);
+        mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
+        mLocationController = Dependency.get(LocationController.class);
+
+        mSlotCast = context.getString(com.android.internal.R.string.status_bar_cast);
+        mSlotHotspot = context.getString(com.android.internal.R.string.status_bar_hotspot);
+        mSlotBluetooth = context.getString(com.android.internal.R.string.status_bar_bluetooth);
+        mSlotTty = context.getString(com.android.internal.R.string.status_bar_tty);
+        mSlotZen = context.getString(com.android.internal.R.string.status_bar_zen);
+        mSlotVolume = context.getString(com.android.internal.R.string.status_bar_volume);
+        mSlotAlarmClock = context.getString(com.android.internal.R.string.status_bar_alarm_clock);
+        mSlotManagedProfile = context.getString(
+                com.android.internal.R.string.status_bar_managed_profile);
+        mSlotRotate = context.getString(com.android.internal.R.string.status_bar_rotate);
+        mSlotHeadset = context.getString(com.android.internal.R.string.status_bar_headset);
+        mSlotDataSaver = context.getString(com.android.internal.R.string.status_bar_data_saver);
+        mSlotLocation = context.getString(com.android.internal.R.string.status_bar_location);
+
+        // listen for broadcasts
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
+        filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
+        filter.addAction(AudioManager.ACTION_HEADSET_PLUG);
+        filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
+        filter.addAction(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED);
+        filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
+        filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
+        filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
+        mContext.registerReceiver(mIntentReceiver, filter, null, mHandler);
+
+        // listen for user / profile change.
+        try {
+            ActivityManager.getService().registerUserSwitchObserver(mUserSwitchListener, TAG);
+        } catch (RemoteException e) {
+            // Ignore
+        }
+
+        // TTY status
+        updateTTY();
+
+        // bluetooth status
+        updateBluetooth();
+
+        // Alarm clock
+        mIconController.setIcon(mSlotAlarmClock, R.drawable.stat_sys_alarm, null);
+        mIconController.setIconVisibility(mSlotAlarmClock, false);
+
+        // zen
+        mIconController.setIcon(mSlotZen, R.drawable.stat_sys_zen_important, null);
+        mIconController.setIconVisibility(mSlotZen, false);
+
+        // volume
+        mIconController.setIcon(mSlotVolume, R.drawable.stat_sys_ringer_vibrate, null);
+        mIconController.setIconVisibility(mSlotVolume, false);
+        updateVolumeZen();
+
+        // cast
+        mIconController.setIcon(mSlotCast, R.drawable.stat_sys_cast, null);
+        mIconController.setIconVisibility(mSlotCast, false);
+
+        // hotspot
+        mIconController.setIcon(mSlotHotspot, R.drawable.stat_sys_hotspot,
+                mContext.getString(R.string.accessibility_status_bar_hotspot));
+        mIconController.setIconVisibility(mSlotHotspot, mHotspot.isHotspotEnabled());
+
+        // managed profile
+        mIconController.setIcon(mSlotManagedProfile, R.drawable.stat_sys_managed_profile_status,
+                mContext.getString(R.string.accessibility_managed_profile));
+        mIconController.setIconVisibility(mSlotManagedProfile, mManagedProfileIconVisible);
+
+        // data saver
+        mIconController.setIcon(mSlotDataSaver, R.drawable.stat_sys_data_saver,
+                context.getString(R.string.accessibility_data_saver_on));
+        mIconController.setIconVisibility(mSlotDataSaver, false);
+
+        mRotationLockController.addCallback(this);
+        mBluetooth.addCallback(this);
+        mProvisionedController.addCallback(this);
+        mZenController.addCallback(this);
+        mCast.addCallback(mCastCallback);
+        mHotspot.addCallback(mHotspotCallback);
+        mNextAlarm.addCallback(mNextAlarmCallback);
+        mDataSaver.addCallback(this);
+        mKeyguardMonitor.addCallback(this);
+        mLocationController.addCallback(this);
+
+        SysUiServiceProvider.getComponent(mContext, CommandQueue.class).addCallbacks(this);
+        SystemServicesProxy.getInstance(mContext).registerTaskStackListener(mTaskListener);
+
+        // Clear out all old notifications on startup (only present in the case where sysui dies)
+        NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
+        for (StatusBarNotification notification : noMan.getActiveNotifications()) {
+            if (notification.getId() == SystemMessage.NOTE_INSTANT_APPS) {
+                noMan.cancel(notification.getTag(), notification.getId());
+            }
+        }
+        DockedStackExistsListener.register(exists -> {
+            mDockedStackExists = exists;
+            updateForegroundInstantApps();
+        });
+    }
+
+    public void destroy() {
+        mRotationLockController.removeCallback(this);
+        mBluetooth.removeCallback(this);
+        mProvisionedController.removeCallback(this);
+        mZenController.removeCallback(this);
+        mCast.removeCallback(mCastCallback);
+        mHotspot.removeCallback(mHotspotCallback);
+        mNextAlarm.removeCallback(mNextAlarmCallback);
+        mDataSaver.removeCallback(this);
+        mKeyguardMonitor.removeCallback(this);
+        mLocationController.removeCallback(this);
+        SysUiServiceProvider.getComponent(mContext, CommandQueue.class).removeCallbacks(this);
+        mContext.unregisterReceiver(mIntentReceiver);
+
+        NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
+        mCurrentNotifs.forEach(v -> noMan.cancelAsUser(v.first, SystemMessage.NOTE_INSTANT_APPS,
+                new UserHandle(v.second)));
+    }
+
+    @Override
+    public void onZenChanged(int zen) {
+        updateVolumeZen();
+    }
+
+    @Override
+    public void onLocationActiveChanged(boolean active) {
+        updateLocation();
+    }
+
+    // Updates the status view based on the current state of location requests.
+    private void updateLocation() {
+        if (mLocationController.isLocationActive()) {
+            mIconController.setIcon(mSlotLocation, LOCATION_STATUS_ICON_ID,
+                    mContext.getString(R.string.accessibility_location_active));
+        } else {
+            mIconController.removeIcon(mSlotLocation);
+        }
+    }
+
+    private void updateAlarm() {
+        final AlarmClockInfo alarm = mAlarmManager.getNextAlarmClock(UserHandle.USER_CURRENT);
+        final boolean hasAlarm = alarm != null && alarm.getTriggerTime() > 0;
+        int zen = mZenController.getZen();
+        final boolean zenNone = zen == Global.ZEN_MODE_NO_INTERRUPTIONS;
+        mIconController.setIcon(mSlotAlarmClock, zenNone ? R.drawable.stat_sys_alarm_dim
+                : R.drawable.stat_sys_alarm, null);
+        mIconController.setIconVisibility(mSlotAlarmClock, mCurrentUserSetup && hasAlarm);
+    }
+
+    private final void updateSimState(Intent intent) {
+        String stateExtra = intent.getStringExtra(IccCardConstants.INTENT_KEY_ICC_STATE);
+        if (IccCardConstants.INTENT_VALUE_ICC_ABSENT.equals(stateExtra)) {
+            mSimState = IccCardConstants.State.ABSENT;
+        } else if (IccCardConstants.INTENT_VALUE_ICC_CARD_IO_ERROR.equals(stateExtra)) {
+            mSimState = IccCardConstants.State.CARD_IO_ERROR;
+        } else if (IccCardConstants.INTENT_VALUE_ICC_CARD_RESTRICTED.equals(stateExtra)) {
+            mSimState = IccCardConstants.State.CARD_RESTRICTED;
+        } else if (IccCardConstants.INTENT_VALUE_ICC_READY.equals(stateExtra)) {
+            mSimState = IccCardConstants.State.READY;
+        } else if (IccCardConstants.INTENT_VALUE_ICC_LOCKED.equals(stateExtra)) {
+            final String lockedReason =
+                    intent.getStringExtra(IccCardConstants.INTENT_KEY_LOCKED_REASON);
+            if (IccCardConstants.INTENT_VALUE_LOCKED_ON_PIN.equals(lockedReason)) {
+                mSimState = IccCardConstants.State.PIN_REQUIRED;
+            } else if (IccCardConstants.INTENT_VALUE_LOCKED_ON_PUK.equals(lockedReason)) {
+                mSimState = IccCardConstants.State.PUK_REQUIRED;
+            } else {
+                mSimState = IccCardConstants.State.NETWORK_LOCKED;
+            }
+        } else {
+            mSimState = IccCardConstants.State.UNKNOWN;
+        }
+    }
+
+    private final void updateVolumeZen() {
+        AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+
+        boolean zenVisible = false;
+        int zenIconId = 0;
+        String zenDescription = null;
+
+        boolean volumeVisible = false;
+        int volumeIconId = 0;
+        String volumeDescription = null;
+        int zen = mZenController.getZen();
+
+        if (DndTile.isVisible(mContext) || DndTile.isCombinedIcon(mContext)) {
+            zenVisible = zen != Global.ZEN_MODE_OFF;
+            zenIconId = zen == Global.ZEN_MODE_NO_INTERRUPTIONS
+                    ? R.drawable.stat_sys_dnd_total_silence : R.drawable.stat_sys_dnd;
+            zenDescription = mContext.getString(R.string.quick_settings_dnd_label);
+        } else if (zen == Global.ZEN_MODE_NO_INTERRUPTIONS) {
+            zenVisible = true;
+            zenIconId = R.drawable.stat_sys_zen_none;
+            zenDescription = mContext.getString(R.string.interruption_level_none);
+        } else if (zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) {
+            zenVisible = true;
+            zenIconId = R.drawable.stat_sys_zen_important;
+            zenDescription = mContext.getString(R.string.interruption_level_priority);
+        }
+
+        if (DndTile.isVisible(mContext) && !DndTile.isCombinedIcon(mContext)
+                && audioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_SILENT) {
+            volumeVisible = true;
+            volumeIconId = R.drawable.stat_sys_ringer_silent;
+            volumeDescription = mContext.getString(R.string.accessibility_ringer_silent);
+        } else if (zen != Global.ZEN_MODE_NO_INTERRUPTIONS && zen != Global.ZEN_MODE_ALARMS &&
+                audioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_VIBRATE) {
+            volumeVisible = true;
+            volumeIconId = R.drawable.stat_sys_ringer_vibrate;
+            volumeDescription = mContext.getString(R.string.accessibility_ringer_vibrate);
+        }
+
+        if (zenVisible) {
+            mIconController.setIcon(mSlotZen, zenIconId, zenDescription);
+        }
+        if (zenVisible != mZenVisible) {
+            mIconController.setIconVisibility(mSlotZen, zenVisible);
+            mZenVisible = zenVisible;
+        }
+
+        if (volumeVisible) {
+            mIconController.setIcon(mSlotVolume, volumeIconId, volumeDescription);
+        }
+        if (volumeVisible != mVolumeVisible) {
+            mIconController.setIconVisibility(mSlotVolume, volumeVisible);
+            mVolumeVisible = volumeVisible;
+        }
+        updateAlarm();
+    }
+
+    @Override
+    public void onBluetoothDevicesChanged() {
+        updateBluetooth();
+    }
+
+    @Override
+    public void onBluetoothStateChange(boolean enabled) {
+        updateBluetooth();
+    }
+
+    private final void updateBluetooth() {
+        int iconId = R.drawable.stat_sys_data_bluetooth;
+        String contentDescription =
+                mContext.getString(R.string.accessibility_quick_settings_bluetooth_on);
+        boolean bluetoothEnabled = false;
+        if (mBluetooth != null) {
+            bluetoothEnabled = mBluetooth.isBluetoothEnabled();
+            if (mBluetooth.isBluetoothConnected()) {
+                iconId = R.drawable.stat_sys_data_bluetooth_connected;
+                contentDescription = mContext.getString(R.string.accessibility_bluetooth_connected);
+            }
+        }
+
+        mIconController.setIcon(mSlotBluetooth, iconId, contentDescription);
+        mIconController.setIconVisibility(mSlotBluetooth, bluetoothEnabled);
+    }
+
+    private final void updateTTY() {
+        TelecomManager telecomManager =
+                (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
+        if (telecomManager == null) {
+            updateTTY(TelecomManager.TTY_MODE_OFF);
+        } else {
+            updateTTY(telecomManager.getCurrentTtyMode());
+        }
+    }
+
+    private final void updateTTY(int currentTtyMode) {
+        boolean enabled = currentTtyMode != TelecomManager.TTY_MODE_OFF;
+
+        if (DEBUG) Log.v(TAG, "updateTTY: enabled: " + enabled);
+
+        if (enabled) {
+            // TTY is on
+            if (DEBUG) Log.v(TAG, "updateTTY: set TTY on");
+            mIconController.setIcon(mSlotTty, R.drawable.stat_sys_tty_mode,
+                    mContext.getString(R.string.accessibility_tty_enabled));
+            mIconController.setIconVisibility(mSlotTty, true);
+        } else {
+            // TTY is off
+            if (DEBUG) Log.v(TAG, "updateTTY: set TTY off");
+            mIconController.setIconVisibility(mSlotTty, false);
+        }
+    }
+
+    private void updateCast() {
+        boolean isCasting = false;
+        for (CastDevice device : mCast.getCastDevices()) {
+            if (device.state == CastDevice.STATE_CONNECTING
+                    || device.state == CastDevice.STATE_CONNECTED) {
+                isCasting = true;
+                break;
+            }
+        }
+        if (DEBUG) Log.v(TAG, "updateCast: isCasting: " + isCasting);
+        mHandler.removeCallbacks(mRemoveCastIconRunnable);
+        if (isCasting) {
+            mIconController.setIcon(mSlotCast, R.drawable.stat_sys_cast,
+                    mContext.getString(R.string.accessibility_casting));
+            mIconController.setIconVisibility(mSlotCast, true);
+        } else {
+            // don't turn off the screen-record icon for a few seconds, just to make sure the user
+            // has seen it
+            if (DEBUG) Log.v(TAG, "updateCast: hiding icon in 3 sec...");
+            mHandler.postDelayed(mRemoveCastIconRunnable, 3000);
+        }
+    }
+
+    private void updateQuietState() {
+        mManagedProfileInQuietMode = false;
+        int currentUserId = ActivityManager.getCurrentUser();
+        for (UserInfo ui : mUserManager.getEnabledProfiles(currentUserId)) {
+            if (ui.isManagedProfile() && ui.isQuietModeEnabled()) {
+                mManagedProfileInQuietMode = true;
+                return;
+            }
+        }
+    }
+
+    private void updateManagedProfile() {
+        // getLastResumedActivityUserId needds to acquire the AM lock, which may be contended in
+        // some cases. Since it doesn't really matter here whether it's updated in this frame
+        // or in the next one, we call this method from our UI offload thread.
+        mUiOffloadThread.submit(() -> {
+            final int userId;
+            try {
+                userId = ActivityManager.getService().getLastResumedActivityUserId();
+                boolean isManagedProfile = mUserManager.isManagedProfile(userId);
+                mHandler.post(() -> {
+                    final boolean showIcon;
+                    if (isManagedProfile &&
+                            (!mKeyguardMonitor.isShowing() || mKeyguardMonitor.isOccluded())) {
+                        showIcon = true;
+                        mIconController.setIcon(mSlotManagedProfile,
+                                R.drawable.stat_sys_managed_profile_status,
+                                mContext.getString(R.string.accessibility_managed_profile));
+                    } else if (mManagedProfileInQuietMode) {
+                        showIcon = true;
+                        mIconController.setIcon(mSlotManagedProfile,
+                                R.drawable.stat_sys_managed_profile_status_off,
+                                mContext.getString(R.string.accessibility_managed_profile));
+                    } else {
+                        showIcon = false;
+                    }
+                    if (mManagedProfileIconVisible != showIcon) {
+                        mIconController.setIconVisibility(mSlotManagedProfile, showIcon);
+                        mManagedProfileIconVisible = showIcon;
+                    }
+                });
+            } catch (RemoteException e) {
+                Log.w(TAG, "updateManagedProfile: ", e);
+            }
+        });
+    }
+
+    private void updateForegroundInstantApps() {
+        NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
+        ArraySet<Pair<String, Integer>> notifs = new ArraySet<>(mCurrentNotifs);
+        IPackageManager pm = AppGlobals.getPackageManager();
+        mCurrentNotifs.clear();
+        mUiOffloadThread.submit(() -> {
+            try {
+                int focusedId = ActivityManager.getService().getFocusedStackId();
+                if (focusedId == StackId.FULLSCREEN_WORKSPACE_STACK_ID) {
+                    checkStack(StackId.FULLSCREEN_WORKSPACE_STACK_ID, notifs, noMan, pm);
+                }
+                if (mDockedStackExists) {
+                    checkStack(StackId.DOCKED_STACK_ID, notifs, noMan, pm);
+                }
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+            // Cancel all the leftover notifications that don't have a foreground process anymore.
+            notifs.forEach(v -> noMan.cancelAsUser(v.first, SystemMessage.NOTE_INSTANT_APPS,
+                    new UserHandle(v.second)));
+        });
+    }
+
+    private void checkStack(int stackId, ArraySet<Pair<String, Integer>> notifs,
+            NotificationManager noMan, IPackageManager pm) {
+        try {
+            StackInfo info = ActivityManager.getService().getStackInfo(stackId);
+            if (info == null || info.topActivity == null) return;
+            String pkg = info.topActivity.getPackageName();
+            if (!hasNotif(notifs, pkg, info.userId)) {
+                // TODO: Optimize by not always needing to get application info.
+                // Maybe cache non-ephemeral packages?
+                ApplicationInfo appInfo = pm.getApplicationInfo(pkg,
+                        PackageManager.MATCH_UNINSTALLED_PACKAGES, info.userId);
+                if (appInfo.isInstantApp()) {
+                    postEphemeralNotif(pkg, info.userId, appInfo, noMan, info.taskIds[info.taskIds.length - 1]);
+                }
+            }
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    private void postEphemeralNotif(String pkg, int userId, ApplicationInfo appInfo,
+            NotificationManager noMan, int taskId) {
+        final Bundle extras = new Bundle();
+        extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
+                mContext.getString(R.string.instant_apps));
+        mCurrentNotifs.add(new Pair<>(pkg, userId));
+        String message = mContext.getString(R.string.instant_apps_message);
+        PendingIntent appInfoAction = PendingIntent.getActivity(mContext, 0,
+                new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+                        .setData(Uri.fromParts("package", pkg, null)), 0);
+        Action action = new Notification.Action.Builder(null, mContext.getString(R.string.app_info),
+                appInfoAction).build();
+
+        Intent browserIntent = getTaskIntent(taskId, userId);
+        Notification.Builder builder = new Notification.Builder(mContext, NotificationChannels.GENERAL);
+        if (browserIntent != null) {
+            // Make sure that this doesn't resolve back to an instant app
+            browserIntent.setComponent(null)
+                    .setPackage(null)
+                    .addFlags(Intent.FLAG_IGNORE_EPHEMERAL)
+                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+            PendingIntent pendingIntent = PendingIntent.getActivity(mContext,
+                    0 /* requestCode */, browserIntent, 0 /* flags */);
+            ComponentName aiaComponent = null;
+            try {
+                aiaComponent = AppGlobals.getPackageManager().getInstantAppInstallerComponent();
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+            Intent goToWebIntent = new Intent()
+                    .setComponent(aiaComponent)
+                    .setAction(Intent.ACTION_VIEW)
+                    .addCategory(Intent.CATEGORY_BROWSABLE)
+                    .addCategory("unique:" + System.currentTimeMillis())
+                    .putExtra(Intent.EXTRA_PACKAGE_NAME, appInfo.packageName)
+                    .putExtra(Intent.EXTRA_VERSION_CODE, appInfo.versionCode)
+                    .putExtra(Intent.EXTRA_EPHEMERAL_FAILURE, pendingIntent);
+
+            PendingIntent webPendingIntent = PendingIntent.getActivity(mContext, 0, goToWebIntent, 0);
+            Action webAction = new Notification.Action.Builder(null, mContext.getString(R.string.go_to_web),
+                    webPendingIntent).build();
+            builder.addAction(webAction);
+        }
+
+        noMan.notifyAsUser(pkg, SystemMessage.NOTE_INSTANT_APPS, builder
+                        .addExtras(extras)
+                        .addAction(action)
+                        .setContentIntent(appInfoAction)
+                        .setColor(mContext.getColor(R.color.instant_apps_color))
+                        .setContentTitle(appInfo.loadLabel(mContext.getPackageManager()))
+                        .setLargeIcon(Icon.createWithResource(pkg, appInfo.icon))
+                        .setSmallIcon(Icon.createWithResource(mContext.getPackageName(),
+                                R.drawable.instant_icon))
+                        .setContentText(message)
+                        .setOngoing(true)
+                        .build(),
+                new UserHandle(userId));
+    }
+
+    private Intent getTaskIntent(int taskId, int userId) {
+        List<ActivityManager.RecentTaskInfo> tasks = mContext.getSystemService(ActivityManager.class)
+                .getRecentTasksForUser(NUM_TASKS_FOR_INSTANT_APP_INFO, 0, userId);
+        for (int i = 0; i < tasks.size(); i++) {
+            if (tasks.get(i).id == taskId) {
+                return tasks.get(i).baseIntent;
+            }
+        }
+        return null;
+    }
+
+    private boolean hasNotif(ArraySet<Pair<String, Integer>> notifs, String pkg, int userId) {
+        Pair<String, Integer> key = new Pair<>(pkg, userId);
+        if (notifs.remove(key)) {
+            mCurrentNotifs.add(key);
+            return true;
+        }
+        return false;
+    }
+
+    private final SynchronousUserSwitchObserver mUserSwitchListener =
+            new SynchronousUserSwitchObserver() {
+                @Override
+                public void onUserSwitching(int newUserId) throws RemoteException {
+                    mHandler.post(() -> mUserInfoController.reloadUserInfo());
+                }
+
+                @Override
+                public void onUserSwitchComplete(int newUserId) throws RemoteException {
+                    mHandler.post(() -> {
+                        updateAlarm();
+                        updateQuietState();
+                        updateManagedProfile();
+                        updateForegroundInstantApps();
+                    });
+                }
+            };
+
+    private final HotspotController.Callback mHotspotCallback = new HotspotController.Callback() {
+        @Override
+        public void onHotspotChanged(boolean enabled) {
+            mIconController.setIconVisibility(mSlotHotspot, enabled);
+        }
+    };
+
+    private final CastController.Callback mCastCallback = new CastController.Callback() {
+        @Override
+        public void onCastDevicesChanged() {
+            updateCast();
+        }
+    };
+
+    private final NextAlarmController.NextAlarmChangeCallback mNextAlarmCallback =
+            new NextAlarmController.NextAlarmChangeCallback() {
+                @Override
+                public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) {
+                    updateAlarm();
+                }
+            };
+
+    @Override
+    public void appTransitionStarting(long startTime, long duration, boolean forced) {
+        updateManagedProfile();
+        updateForegroundInstantApps();
+    }
+
+    @Override
+    public void onKeyguardShowingChanged() {
+        updateManagedProfile();
+        updateForegroundInstantApps();
+    }
+
+    @Override
+    public void onUserSetupChanged() {
+        boolean userSetup = mProvisionedController.isUserSetup(
+                mProvisionedController.getCurrentUser());
+        if (mCurrentUserSetup == userSetup) return;
+        mCurrentUserSetup = userSetup;
+        updateAlarm();
+        updateQuietState();
+    }
+
+    @Override
+    public void preloadRecentApps() {
+        updateForegroundInstantApps();
+    }
+
+    @Override
+    public void onRotationLockStateChanged(boolean rotationLocked, boolean affordanceVisible) {
+        boolean portrait = RotationLockTile.isCurrentOrientationLockPortrait(
+                mRotationLockController, mContext);
+        if (rotationLocked) {
+            if (portrait) {
+                mIconController.setIcon(mSlotRotate, R.drawable.stat_sys_rotate_portrait,
+                        mContext.getString(R.string.accessibility_rotation_lock_on_portrait));
+            } else {
+                mIconController.setIcon(mSlotRotate, R.drawable.stat_sys_rotate_landscape,
+                        mContext.getString(R.string.accessibility_rotation_lock_on_landscape));
+            }
+            mIconController.setIconVisibility(mSlotRotate, true);
+        } else {
+            mIconController.setIconVisibility(mSlotRotate, false);
+        }
+    }
+
+    private void updateHeadsetPlug(Intent intent) {
+        boolean connected = intent.getIntExtra("state", 0) != 0;
+        boolean hasMic = intent.getIntExtra("microphone", 0) != 0;
+        if (connected) {
+            String contentDescription = mContext.getString(hasMic
+                    ? R.string.accessibility_status_bar_headset
+                    : R.string.accessibility_status_bar_headphones);
+            mIconController.setIcon(mSlotHeadset, hasMic ? R.drawable.ic_headset_mic
+                    : R.drawable.ic_headset, contentDescription);
+            mIconController.setIconVisibility(mSlotHeadset, true);
+        } else {
+            mIconController.setIconVisibility(mSlotHeadset, false);
+        }
+    }
+
+    @Override
+    public void onDataSaverChanged(boolean isDataSaving) {
+        mIconController.setIconVisibility(mSlotDataSaver, isDataSaving);
+    }
+
+    private final TaskStackListener mTaskListener = new TaskStackListener() {
+        @Override
+        public void onTaskStackChanged() {
+            // Listen for changes to stacks and then check which instant apps are foreground.
+            updateForegroundInstantApps();
+        }
+    };
+
+    private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION) ||
+                    action.equals(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION)) {
+                updateVolumeZen();
+            } else if (action.equals(TelephonyIntents.ACTION_SIM_STATE_CHANGED)) {
+                updateSimState(intent);
+            } else if (action.equals(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED)) {
+                updateTTY(intent.getIntExtra(TelecomManager.EXTRA_CURRENT_TTY_MODE,
+                        TelecomManager.TTY_MODE_OFF));
+            } else if (action.equals(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) ||
+                    action.equals(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) ||
+                    action.equals(Intent.ACTION_MANAGED_PROFILE_REMOVED)) {
+                updateQuietState();
+                updateManagedProfile();
+            } else if (action.equals(AudioManager.ACTION_HEADSET_PLUG)) {
+                updateHeadsetPlug(intent);
+            }
+        }
+    };
+
+    private Runnable mRemoveCastIconRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (DEBUG) Log.v(TAG, "updateCast: hiding icon NOW");
+            mIconController.setIconVisibility(mSlotCast, false);
+        }
+    };
+}
diff --git a/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java b/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java
new file mode 100644
index 0000000..fb1addf
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2013 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.phone;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.res.Resources;
+import android.view.View;
+
+import com.android.systemui.R;
+
+public final class PhoneStatusBarTransitions extends BarTransitions {
+    private static final float ICON_ALPHA_WHEN_NOT_OPAQUE = 1;
+    private static final float ICON_ALPHA_WHEN_LIGHTS_OUT_BATTERY_CLOCK = 0.5f;
+    private static final float ICON_ALPHA_WHEN_LIGHTS_OUT_NON_BATTERY_CLOCK = 0;
+
+    private final PhoneStatusBarView mView;
+    private final float mIconAlphaWhenOpaque;
+
+    private View mLeftSide, mStatusIcons, mSignalCluster, mBattery, mClock;
+    private Animator mCurrentAnimation;
+
+    public PhoneStatusBarTransitions(PhoneStatusBarView view) {
+        super(view, R.drawable.status_background);
+        mView = view;
+        final Resources res = mView.getContext().getResources();
+        mIconAlphaWhenOpaque = res.getFraction(R.dimen.status_bar_icon_drawing_alpha, 1, 1);
+    }
+
+    public void init() {
+        mLeftSide = mView.findViewById(R.id.notification_icon_area);
+        mStatusIcons = mView.findViewById(R.id.statusIcons);
+        mSignalCluster = mView.findViewById(R.id.signal_cluster);
+        mBattery = mView.findViewById(R.id.battery);
+        mClock = mView.findViewById(R.id.clock);
+        applyModeBackground(-1, getMode(), false /*animate*/);
+        applyMode(getMode(), false /*animate*/);
+    }
+
+    public ObjectAnimator animateTransitionTo(View v, float toAlpha) {
+        return ObjectAnimator.ofFloat(v, "alpha", v.getAlpha(), toAlpha);
+    }
+
+    private float getNonBatteryClockAlphaFor(int mode) {
+        return isLightsOut(mode) ? ICON_ALPHA_WHEN_LIGHTS_OUT_NON_BATTERY_CLOCK
+                : !isOpaque(mode) ? ICON_ALPHA_WHEN_NOT_OPAQUE
+                : mIconAlphaWhenOpaque;
+    }
+
+    private float getBatteryClockAlpha(int mode) {
+        return isLightsOut(mode) ? ICON_ALPHA_WHEN_LIGHTS_OUT_BATTERY_CLOCK
+                : getNonBatteryClockAlphaFor(mode);
+    }
+
+    private boolean isOpaque(int mode) {
+        return !(mode == MODE_SEMI_TRANSPARENT || mode == MODE_TRANSLUCENT
+                || mode == MODE_TRANSPARENT || mode == MODE_LIGHTS_OUT_TRANSPARENT);
+    }
+
+    @Override
+    protected void onTransition(int oldMode, int newMode, boolean animate) {
+        super.onTransition(oldMode, newMode, animate);
+        applyMode(newMode, animate);
+    }
+
+    private void applyMode(int mode, boolean animate) {
+        if (mLeftSide == null) return; // pre-init
+        float newAlpha = getNonBatteryClockAlphaFor(mode);
+        float newAlphaBC = getBatteryClockAlpha(mode);
+        if (mCurrentAnimation != null) {
+            mCurrentAnimation.cancel();
+        }
+        if (animate) {
+            AnimatorSet anims = new AnimatorSet();
+            anims.playTogether(
+                    animateTransitionTo(mLeftSide, newAlpha),
+                    animateTransitionTo(mStatusIcons, newAlpha),
+                    animateTransitionTo(mSignalCluster, newAlpha),
+                    animateTransitionTo(mBattery, newAlphaBC),
+                    animateTransitionTo(mClock, newAlphaBC)
+                    );
+            if (isLightsOut(mode)) {
+                anims.setDuration(LIGHTS_OUT_DURATION);
+            }
+            anims.start();
+            mCurrentAnimation = anims;
+        } else {
+            mLeftSide.setAlpha(newAlpha);
+            mStatusIcons.setAlpha(newAlpha);
+            mSignalCluster.setAlpha(newAlpha);
+            mBattery.setAlpha(newAlphaBC);
+            mClock.setAlpha(newAlphaBC);
+        }
+    }
+}
\ No newline at end of file
diff --git a/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
new file mode 100644
index 0000000..970d1de
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.EventLog;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.systemui.BatteryMeterView;
+import com.android.systemui.DejankUtils;
+import com.android.systemui.Dependency;
+import com.android.systemui.EventLogTags;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
+
+public class PhoneStatusBarView extends PanelBar {
+    private static final String TAG = "PhoneStatusBarView";
+    private static final boolean DEBUG = StatusBar.DEBUG;
+    private static final boolean DEBUG_GESTURES = false;
+
+    StatusBar mBar;
+
+    boolean mIsFullyOpenedPanel = false;
+    private final PhoneStatusBarTransitions mBarTransitions;
+    private ScrimController mScrimController;
+    private float mMinFraction;
+    private float mPanelFraction;
+    private Runnable mHideExpandedRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (mPanelFraction == 0.0f) {
+                mBar.makeExpandedInvisible();
+            }
+        }
+    };
+    private DarkReceiver mBattery;
+
+    public PhoneStatusBarView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        mBarTransitions = new PhoneStatusBarTransitions(this);
+    }
+
+    public BarTransitions getBarTransitions() {
+        return mBarTransitions;
+    }
+
+    public void setBar(StatusBar bar) {
+        mBar = bar;
+    }
+
+    public void setScrimController(ScrimController scrimController) {
+        mScrimController = scrimController;
+    }
+
+    @Override
+    public void onFinishInflate() {
+        mBarTransitions.init();
+        mBattery = findViewById(R.id.battery);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        // Always have Battery meters in the status bar observe the dark/light modes.
+        Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mBattery);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mBattery);
+    }
+
+    @Override
+    public boolean panelEnabled() {
+        return mBar.panelsEnabled();
+    }
+
+    @Override
+    public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) {
+        if (super.onRequestSendAccessibilityEventInternal(child, event)) {
+            // The status bar is very small so augment the view that the user is touching
+            // with the content of the status bar a whole. This way an accessibility service
+            // may announce the current item as well as the entire content if appropriate.
+            AccessibilityEvent record = AccessibilityEvent.obtain();
+            onInitializeAccessibilityEvent(record);
+            dispatchPopulateAccessibilityEvent(record);
+            event.appendRecord(record);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void onPanelPeeked() {
+        super.onPanelPeeked();
+        mBar.makeExpandedVisible(false);
+    }
+
+    @Override
+    public void onPanelCollapsed() {
+        super.onPanelCollapsed();
+        // Close the status bar in the next frame so we can show the end of the animation.
+        post(mHideExpandedRunnable);
+        mIsFullyOpenedPanel = false;
+    }
+
+    public void removePendingHideExpandedRunnables() {
+        removeCallbacks(mHideExpandedRunnable);
+    }
+
+    @Override
+    public void onPanelFullyOpened() {
+        super.onPanelFullyOpened();
+        if (!mIsFullyOpenedPanel) {
+            mPanel.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+        }
+        mIsFullyOpenedPanel = true;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        boolean barConsumedEvent = mBar.interceptTouchEvent(event);
+
+        if (DEBUG_GESTURES) {
+            if (event.getActionMasked() != MotionEvent.ACTION_MOVE) {
+                EventLog.writeEvent(EventLogTags.SYSUI_PANELBAR_TOUCH,
+                        event.getActionMasked(), (int) event.getX(), (int) event.getY(),
+                        barConsumedEvent ? 1 : 0);
+            }
+        }
+
+        return barConsumedEvent || super.onTouchEvent(event);
+    }
+
+    @Override
+    public void onTrackingStarted() {
+        super.onTrackingStarted();
+        mBar.onTrackingStarted();
+        mScrimController.onTrackingStarted();
+        removePendingHideExpandedRunnables();
+    }
+
+    @Override
+    public void onClosingFinished() {
+        super.onClosingFinished();
+        mBar.onClosingFinished();
+    }
+
+    @Override
+    public void onTrackingStopped(boolean expand) {
+        super.onTrackingStopped(expand);
+        mBar.onTrackingStopped(expand);
+    }
+
+    @Override
+    public void onExpandingFinished() {
+        super.onExpandingFinished();
+        mScrimController.onExpandingFinished();
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        return mBar.interceptTouchEvent(event) || super.onInterceptTouchEvent(event);
+    }
+
+    @Override
+    public void panelScrimMinFractionChanged(float minFraction) {
+        if (mMinFraction != minFraction) {
+            mMinFraction = minFraction;
+            updateScrimFraction();
+        }
+    }
+
+    @Override
+    public void panelExpansionChanged(float frac, boolean expanded) {
+        super.panelExpansionChanged(frac, expanded);
+        mPanelFraction = frac;
+        updateScrimFraction();
+    }
+
+    private void updateScrimFraction() {
+        float scrimFraction = mPanelFraction;
+        if (mMinFraction < 1.0f) {
+            scrimFraction = Math.max((mPanelFraction - mMinFraction) / (1.0f - mMinFraction),
+                    0);
+        }
+        mScrimController.setPanelExpansion(scrimFraction);
+    }
+
+    public void onDensityOrFontScaleChanged() {
+        ViewGroup.LayoutParams layoutParams = getLayoutParams();
+        layoutParams.height = getResources().getDimensionPixelSize(
+                R.dimen.status_bar_height);
+        setLayoutParams(layoutParams);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/PlatformVelocityTracker.java b/com/android/systemui/statusbar/phone/PlatformVelocityTracker.java
new file mode 100644
index 0000000..f589c3d
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/PlatformVelocityTracker.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.util.Pools;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+
+/**
+ * An implementation of {@link VelocityTrackerInterface} using the platform-standard
+ * {@link VelocityTracker}.
+ */
+public class PlatformVelocityTracker implements VelocityTrackerInterface {
+
+    private static final Pools.SynchronizedPool<PlatformVelocityTracker> sPool =
+            new Pools.SynchronizedPool<>(2);
+
+    private VelocityTracker mTracker;
+
+    public static PlatformVelocityTracker obtain() {
+        PlatformVelocityTracker tracker = sPool.acquire();
+        if (tracker == null) {
+            tracker = new PlatformVelocityTracker();
+        }
+        tracker.setTracker(VelocityTracker.obtain());
+        return tracker;
+    }
+
+    public void setTracker(VelocityTracker tracker) {
+        mTracker = tracker;
+    }
+
+    @Override
+    public void addMovement(MotionEvent event) {
+        mTracker.addMovement(event);
+    }
+
+    @Override
+    public void computeCurrentVelocity(int units) {
+        mTracker.computeCurrentVelocity(units);
+    }
+
+    @Override
+    public float getXVelocity() {
+        return mTracker.getXVelocity();
+    }
+
+    @Override
+    public float getYVelocity() {
+        return mTracker.getYVelocity();
+    }
+
+    @Override
+    public void recycle() {
+        mTracker.recycle();
+        sPool.release(this);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/ReverseLinearLayout.java b/com/android/systemui/statusbar/phone/ReverseLinearLayout.java
new file mode 100644
index 0000000..bcbc345
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/ReverseLinearLayout.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2016 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.phone;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import java.util.ArrayList;
+
+/**
+ * Automatically reverses the order of children as they are added.
+ * Also reverse the width and height values of layout params
+ */
+public class ReverseLinearLayout extends LinearLayout {
+
+    /** If true, the layout is reversed vs. a regular linear layout */
+    private boolean mIsLayoutReverse;
+
+    /** If true, the layout is opposite to it's natural reversity from the layout direction */
+    private boolean mIsAlternativeOrder;
+
+    public ReverseLinearLayout(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        updateOrder();
+    }
+
+    @Override
+    public void addView(View child) {
+        reverseParams(child.getLayoutParams(), child);
+        if (mIsLayoutReverse) {
+            super.addView(child, 0);
+        } else {
+            super.addView(child);
+        }
+    }
+
+    @Override
+    public void addView(View child, ViewGroup.LayoutParams params) {
+        reverseParams(params, child);
+        if (mIsLayoutReverse) {
+            super.addView(child, 0, params);
+        } else {
+            super.addView(child, params);
+        }
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+        updateOrder();
+    }
+
+    public void setAlternativeOrder(boolean alternative) {
+        mIsAlternativeOrder = alternative;
+        updateOrder();
+    }
+
+    /**
+     * In landscape, the LinearLayout is not auto mirrored since it is vertical. Therefore we
+     * have to do it manually
+     */
+    private void updateOrder() {
+        boolean isLayoutRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+        boolean isLayoutReverse = isLayoutRtl ^ mIsAlternativeOrder;
+
+        if (mIsLayoutReverse != isLayoutReverse) {
+            // reversity changed, swap the order of all views.
+            int childCount = getChildCount();
+            ArrayList<View> childList = new ArrayList<>(childCount);
+            for (int i = 0; i < childCount; i++) {
+                childList.add(getChildAt(i));
+            }
+            removeAllViews();
+            for (int i = childCount - 1; i >= 0; i--) {
+                super.addView(childList.get(i));
+            }
+            mIsLayoutReverse = isLayoutReverse;
+        }
+    }
+
+    private static void reverseParams(ViewGroup.LayoutParams params, View child) {
+        if (child instanceof Reversable) {
+            ((Reversable) child).reverse();
+        }
+        if (child.getPaddingLeft() == child.getPaddingRight()
+                && child.getPaddingTop() == child.getPaddingBottom()) {
+            child.setPadding(child.getPaddingTop(), child.getPaddingLeft(),
+                    child.getPaddingTop(), child.getPaddingLeft());
+        }
+        if (params == null) {
+            return;
+        }
+        int width = params.width;
+        params.width = params.height;
+        params.height = width;
+    }
+
+    public interface Reversable {
+        void reverse();
+    }
+
+    public static class ReverseFrameLayout extends FrameLayout implements Reversable {
+
+        public ReverseFrameLayout(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void reverse() {
+            for (int i = 0; i < getChildCount(); i++) {
+                View child = getChildAt(i);
+                reverseParams(child.getLayoutParams(), child);
+            }
+        }
+    }
+
+}
diff --git a/com/android/systemui/statusbar/phone/ScrimController.java b/com/android/systemui/statusbar/phone/ScrimController.java
new file mode 100644
index 0000000..702afa3
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/ScrimController.java
@@ -0,0 +1,790 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Trace;
+import android.util.MathUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+import com.android.internal.colorextraction.ColorExtractor;
+import com.android.internal.colorextraction.ColorExtractor.GradientColors;
+import com.android.internal.colorextraction.ColorExtractor.OnColorsChangedListener;
+import com.android.internal.graphics.ColorUtils;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.ScrimView;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+import com.android.systemui.statusbar.stack.ViewState;
+
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+/**
+ * Controls both the scrim behind the notifications and in front of the notifications (when a
+ * security method gets shown).
+ */
+public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
+        OnHeadsUpChangedListener, OnColorsChangedListener {
+    public static final long ANIMATION_DURATION = 220;
+    public static final Interpolator KEYGUARD_FADE_OUT_INTERPOLATOR
+            = new PathInterpolator(0f, 0, 0.7f, 1f);
+    public static final Interpolator KEYGUARD_FADE_OUT_INTERPOLATOR_LOCKED
+            = new PathInterpolator(0.3f, 0f, 0.8f, 1f);
+    // Default alpha value for most scrims, if unsure use this constant
+    public static final float GRADIENT_SCRIM_ALPHA = 0.45f;
+    // A scrim varies its opacity based on a busyness factor, for example
+    // how many notifications are currently visible.
+    public static final float GRADIENT_SCRIM_ALPHA_BUSY = 0.70f;
+    protected static final float SCRIM_BEHIND_ALPHA_KEYGUARD = GRADIENT_SCRIM_ALPHA;
+    protected static final float SCRIM_BEHIND_ALPHA_UNLOCKING = 0.2f;
+    private static final float SCRIM_IN_FRONT_ALPHA = GRADIENT_SCRIM_ALPHA_BUSY;
+    private static final float SCRIM_IN_FRONT_ALPHA_LOCKED = GRADIENT_SCRIM_ALPHA_BUSY;
+    private static final int TAG_KEY_ANIM = R.id.scrim;
+    private static final int TAG_KEY_ANIM_TARGET = R.id.scrim_target;
+    private static final int TAG_START_ALPHA = R.id.scrim_alpha_start;
+    private static final int TAG_END_ALPHA = R.id.scrim_alpha_end;
+    private static final float NOT_INITIALIZED = -1;
+
+    private final LightBarController mLightBarController;
+    protected final ScrimView mScrimBehind;
+    protected final ScrimView mScrimInFront;
+    private final UnlockMethodCache mUnlockMethodCache;
+    private final View mHeadsUpScrim;
+    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+
+    private final SysuiColorExtractor mColorExtractor;
+    private GradientColors mLockColors;
+    private GradientColors mSystemColors;
+    private boolean mNeedsDrawableColorUpdate;
+
+    protected float mScrimBehindAlpha;
+    protected float mScrimBehindAlphaResValue;
+    protected float mScrimBehindAlphaKeyguard = SCRIM_BEHIND_ALPHA_KEYGUARD;
+    protected float mScrimBehindAlphaUnlocking = SCRIM_BEHIND_ALPHA_UNLOCKING;
+
+    protected boolean mKeyguardShowing;
+    private float mFraction;
+
+    private boolean mDarkenWhileDragging;
+    protected boolean mBouncerShowing;
+    protected boolean mBouncerIsKeyguard = false;
+    private boolean mWakeAndUnlocking;
+    protected boolean mAnimateChange;
+    private boolean mUpdatePending;
+    private boolean mTracking;
+    private boolean mAnimateKeyguardFadingOut;
+    protected long mDurationOverride = -1;
+    private long mAnimationDelay;
+    private Runnable mOnAnimationFinished;
+    private boolean mDeferFinishedListener;
+    private final Interpolator mInterpolator = new DecelerateInterpolator();
+    private boolean mDozing;
+    private float mDozeInFrontAlpha;
+    private float mDozeBehindAlpha;
+    private float mCurrentInFrontAlpha  = NOT_INITIALIZED;
+    private float mCurrentBehindAlpha = NOT_INITIALIZED;
+    private float mCurrentHeadsUpAlpha = NOT_INITIALIZED;
+    private int mPinnedHeadsUpCount;
+    private float mTopHeadsUpDragAmount;
+    private View mDraggedHeadsUpView;
+    private boolean mForceHideScrims;
+    private boolean mSkipFirstFrame;
+    private boolean mDontAnimateBouncerChanges;
+    private boolean mKeyguardFadingOutInProgress;
+    private boolean mAnimatingDozeUnlock;
+    private ValueAnimator mKeyguardFadeoutAnimation;
+    /** Wake up from AOD transition is starting; need fully opaque front scrim */
+    private boolean mWakingUpFromAodStarting;
+    /** Wake up from AOD transition is in progress; need black tint */
+    private boolean mWakingUpFromAodInProgress;
+    /** Wake up from AOD transition is animating; need to reset when animation finishes */
+    private boolean mWakingUpFromAodAnimationRunning;
+    private boolean mScrimsVisble;
+    private final Consumer<Boolean> mScrimVisibleListener;
+
+    public ScrimController(LightBarController lightBarController, ScrimView scrimBehind,
+            ScrimView scrimInFront, View headsUpScrim,
+            Consumer<Boolean> scrimVisibleListener) {
+        mScrimBehind = scrimBehind;
+        mScrimInFront = scrimInFront;
+        mHeadsUpScrim = headsUpScrim;
+        mScrimVisibleListener = scrimVisibleListener;
+        final Context context = scrimBehind.getContext();
+        mUnlockMethodCache = UnlockMethodCache.getInstance(context);
+        mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(context);
+        mLightBarController = lightBarController;
+        mScrimBehindAlphaResValue = context.getResources().getFloat(R.dimen.scrim_behind_alpha);
+        // Scrim alpha is initially set to the value on the resource but might be changed
+        // to make sure that text on top of it is legible.
+        mScrimBehindAlpha = mScrimBehindAlphaResValue;
+
+        mColorExtractor = Dependency.get(SysuiColorExtractor.class);
+        mColorExtractor.addOnColorsChangedListener(this);
+        mLockColors = mColorExtractor.getColors(WallpaperManager.FLAG_LOCK,
+                ColorExtractor.TYPE_DARK, true /* ignoreVisibility */);
+        mSystemColors = mColorExtractor.getColors(WallpaperManager.FLAG_SYSTEM,
+                ColorExtractor.TYPE_DARK, true /* ignoreVisibility */);
+        mNeedsDrawableColorUpdate = true;
+
+        updateHeadsUpScrim(false);
+        updateScrims();
+    }
+
+    public void setKeyguardShowing(boolean showing) {
+        mKeyguardShowing = showing;
+
+        // Showing/hiding the keyguard means that scrim colors have to be switched
+        mNeedsDrawableColorUpdate = true;
+        scheduleUpdate();
+    }
+
+    protected void setScrimBehindValues(float scrimBehindAlphaKeyguard,
+            float scrimBehindAlphaUnlocking) {
+        mScrimBehindAlphaKeyguard = scrimBehindAlphaKeyguard;
+        mScrimBehindAlphaUnlocking = scrimBehindAlphaUnlocking;
+        scheduleUpdate();
+    }
+
+    public void onTrackingStarted() {
+        mTracking = true;
+        mDarkenWhileDragging = !mUnlockMethodCache.canSkipBouncer();
+    }
+
+    public void onExpandingFinished() {
+        mTracking = false;
+    }
+
+    public void setPanelExpansion(float fraction) {
+        if (mFraction != fraction) {
+            mFraction = fraction;
+            scheduleUpdate();
+            if (mPinnedHeadsUpCount != 0) {
+                updateHeadsUpScrim(false);
+            }
+            if (mKeyguardFadeoutAnimation != null && mTracking) {
+                mKeyguardFadeoutAnimation.cancel();
+            }
+        }
+    }
+
+    public void setBouncerShowing(boolean showing) {
+        mBouncerShowing = showing;
+        mAnimateChange = !mTracking && !mDontAnimateBouncerChanges && !mKeyguardFadingOutInProgress;
+        scheduleUpdate();
+    }
+
+    /** Prepares the wakeUpFromAod animation (while turning on screen); Forces black scrims. */
+    public void prepareWakeUpFromAod() {
+        if (mWakingUpFromAodInProgress) {
+            return;
+        }
+        mWakingUpFromAodInProgress = true;
+        mWakingUpFromAodStarting = true;
+        mAnimateChange = false;
+        scheduleUpdate();
+        onPreDraw();
+    }
+
+    /** Starts the wakeUpFromAod animation (once screen is on); animate to transparent scrims. */
+    public void wakeUpFromAod() {
+        if (mWakeAndUnlocking || mAnimateKeyguardFadingOut) {
+            // Wake and unlocking has a separate transition that must not be interfered with.
+            mWakingUpFromAodStarting = false;
+            mWakingUpFromAodInProgress = false;
+            return;
+        }
+        if (mWakingUpFromAodStarting) {
+            mWakingUpFromAodInProgress = true;
+            mWakingUpFromAodStarting = false;
+            mAnimateChange = true;
+            scheduleUpdate();
+        }
+    }
+
+    public void setWakeAndUnlocking() {
+        mWakeAndUnlocking = true;
+        mAnimatingDozeUnlock = true;
+        mWakingUpFromAodStarting = false;
+        mWakingUpFromAodInProgress = false;
+        scheduleUpdate();
+    }
+
+    public void animateKeyguardFadingOut(long delay, long duration, Runnable onAnimationFinished,
+            boolean skipFirstFrame) {
+        mWakeAndUnlocking = false;
+        mAnimateKeyguardFadingOut = true;
+        mDurationOverride = duration;
+        mAnimationDelay = delay;
+        mAnimateChange = true;
+        mSkipFirstFrame = skipFirstFrame;
+        mOnAnimationFinished = onAnimationFinished;
+
+        if (!mKeyguardUpdateMonitor.needsSlowUnlockTransition()) {
+            scheduleUpdate();
+
+            // No need to wait for the next frame to be drawn for this case - onPreDraw will execute
+            // the changes we just scheduled.
+            onPreDraw();
+        } else {
+
+            // In case the user isn't unlocked, make sure to delay a bit because the system is hosed
+            // with too many things in this case, in order to not skip the initial frames.
+            mScrimInFront.postOnAnimationDelayed(this::scheduleUpdate, 16);
+        }
+    }
+
+    public void abortKeyguardFadingOut() {
+        if (mAnimateKeyguardFadingOut) {
+            endAnimateKeyguardFadingOut(true /* force */);
+        }
+    }
+
+    public void animateKeyguardUnoccluding(long duration) {
+        mAnimateChange = false;
+        setScrimBehindAlpha(0f);
+        mAnimateChange = true;
+        scheduleUpdate();
+        mDurationOverride = duration;
+    }
+
+    public void animateGoingToFullShade(long delay, long duration) {
+        mDurationOverride = duration;
+        mAnimationDelay = delay;
+        mAnimateChange = true;
+        scheduleUpdate();
+    }
+
+    public void setDozing(boolean dozing) {
+        if (mDozing != dozing) {
+            mDozing = dozing;
+            scheduleUpdate();
+        }
+    }
+
+    public void setDozeInFrontAlpha(float alpha) {
+        mDozeInFrontAlpha = alpha;
+        updateScrimColor(mScrimInFront);
+    }
+
+    public void setDozeBehindAlpha(float alpha) {
+        mDozeBehindAlpha = alpha;
+        updateScrimColor(mScrimBehind);
+    }
+
+    public float getDozeBehindAlpha() {
+        return mDozeBehindAlpha;
+    }
+
+    public float getDozeInFrontAlpha() {
+        return mDozeInFrontAlpha;
+    }
+
+    public void setNotificationCount(int notificationCount) {
+        final float maxNotificationDensity = 3;
+        float notificationDensity = Math.min(notificationCount / maxNotificationDensity, 1f);
+        float newAlpha = MathUtils.map(0, 1,
+                GRADIENT_SCRIM_ALPHA, GRADIENT_SCRIM_ALPHA_BUSY,
+                notificationDensity);
+        if (mScrimBehindAlphaKeyguard != newAlpha) {
+            mScrimBehindAlphaKeyguard = newAlpha;
+            mAnimateChange = true;
+            scheduleUpdate();
+        }
+    }
+
+    private float getScrimInFrontAlpha() {
+        return mKeyguardUpdateMonitor.needsSlowUnlockTransition()
+                ? SCRIM_IN_FRONT_ALPHA_LOCKED
+                : SCRIM_IN_FRONT_ALPHA;
+    }
+
+    /**
+     * Sets the given drawable as the background of the scrim that shows up behind the
+     * notifications.
+     */
+    public void setScrimBehindDrawable(Drawable drawable) {
+        mScrimBehind.setDrawable(drawable);
+    }
+
+    protected void scheduleUpdate() {
+        if (mUpdatePending) return;
+
+        // Make sure that a frame gets scheduled.
+        mScrimBehind.invalidate();
+        mScrimBehind.getViewTreeObserver().addOnPreDrawListener(this);
+        mUpdatePending = true;
+    }
+
+    protected void updateScrims() {
+        // Make sure we have the right gradients and their opacities will satisfy GAR.
+        if (mNeedsDrawableColorUpdate) {
+            mNeedsDrawableColorUpdate = false;
+            final GradientColors currentScrimColors;
+            if (mKeyguardShowing) {
+                // Always animate color changes if we're seeing the keyguard
+                mScrimInFront.setColors(mLockColors, true /* animated */);
+                mScrimBehind.setColors(mLockColors, true /* animated */);
+                currentScrimColors = mLockColors;
+            } else {
+                // Only animate scrim color if the scrim view is actually visible
+                boolean animateScrimInFront = mScrimInFront.getViewAlpha() != 0;
+                boolean animateScrimBehind = mScrimBehind.getViewAlpha() != 0;
+                mScrimInFront.setColors(mSystemColors, animateScrimInFront);
+                mScrimBehind.setColors(mSystemColors, animateScrimBehind);
+                currentScrimColors = mSystemColors;
+            }
+
+            // Calculate minimum scrim opacity for white or black text.
+            int textColor = currentScrimColors.supportsDarkText() ? Color.BLACK : Color.WHITE;
+            int mainColor = currentScrimColors.getMainColor();
+            float minOpacity = ColorUtils.calculateMinimumBackgroundAlpha(textColor, mainColor,
+                    4.5f /* minimumContrast */) / 255f;
+            mScrimBehindAlpha = Math.max(mScrimBehindAlphaResValue, minOpacity);
+            mLightBarController.setScrimColor(mScrimInFront.getColors());
+        }
+
+        if (mAnimateKeyguardFadingOut || mForceHideScrims) {
+            setScrimInFrontAlpha(0f);
+            setScrimBehindAlpha(0f);
+        } else if (mWakeAndUnlocking) {
+            // During wake and unlock, we first hide everything behind a black scrim, which then
+            // gets faded out from animateKeyguardFadingOut. This must never be animated.
+            mAnimateChange = false;
+            if (mDozing) {
+                setScrimInFrontAlpha(0f);
+                setScrimBehindAlpha(1f);
+            } else {
+                setScrimInFrontAlpha(1f);
+                setScrimBehindAlpha(0f);
+            }
+        } else if (!mKeyguardShowing && !mBouncerShowing && !mWakingUpFromAodStarting) {
+            updateScrimNormal();
+            setScrimInFrontAlpha(0);
+        } else {
+            updateScrimKeyguard();
+        }
+        mAnimateChange = false;
+        dispatchScrimsVisible();
+    }
+
+    private void dispatchScrimsVisible() {
+        boolean scrimsVisible = mScrimBehind.getViewAlpha() > 0 || mScrimInFront.getViewAlpha() > 0;
+
+        if (mScrimsVisble != scrimsVisible) {
+            mScrimsVisble = scrimsVisible;
+
+            mScrimVisibleListener.accept(scrimsVisible);
+        }
+    }
+
+    private void updateScrimKeyguard() {
+        if (mTracking && mDarkenWhileDragging) {
+            float behindFraction = Math.max(0, Math.min(mFraction, 1));
+            float fraction = 1 - behindFraction;
+            fraction = (float) Math.pow(fraction, 0.8f);
+            behindFraction = (float) Math.pow(behindFraction, 0.8f);
+            setScrimInFrontAlpha(fraction * getScrimInFrontAlpha());
+            setScrimBehindAlpha(behindFraction * mScrimBehindAlphaKeyguard);
+        } else if (mBouncerShowing && !mBouncerIsKeyguard) {
+            setScrimInFrontAlpha(getScrimInFrontAlpha());
+            updateScrimNormal();
+        } else if (mBouncerShowing) {
+            setScrimInFrontAlpha(0f);
+            setScrimBehindAlpha(mScrimBehindAlpha);
+        } else {
+            float fraction = Math.max(0, Math.min(mFraction, 1));
+            if (mWakingUpFromAodStarting) {
+                setScrimInFrontAlpha(1f);
+            } else {
+                setScrimInFrontAlpha(0f);
+            }
+            setScrimBehindAlpha(fraction
+                    * (mScrimBehindAlphaKeyguard - mScrimBehindAlphaUnlocking)
+                    + mScrimBehindAlphaUnlocking);
+        }
+    }
+
+    private void updateScrimNormal() {
+        float frac = mFraction;
+        // let's start this 20% of the way down the screen
+        frac = frac * 1.2f - 0.2f;
+        if (frac <= 0) {
+            setScrimBehindAlpha(0);
+        } else {
+            // woo, special effects
+            final float k = (float)(1f-0.5f*(1f-Math.cos(3.14159f * Math.pow(1f-frac, 2f))));
+            setScrimBehindAlpha(k * mScrimBehindAlpha);
+        }
+    }
+
+    private void setScrimBehindAlpha(float alpha) {
+        setScrimAlpha(mScrimBehind, alpha);
+    }
+
+    private void setScrimInFrontAlpha(float alpha) {
+        setScrimAlpha(mScrimInFront, alpha);
+        if (alpha == 0f) {
+            mScrimInFront.setClickable(false);
+        } else {
+            // Eat touch events (unless dozing).
+            mScrimInFront.setClickable(!mDozing);
+        }
+    }
+
+    private void setScrimAlpha(View scrim, float alpha) {
+        updateScrim(mAnimateChange, scrim, alpha, getCurrentScrimAlpha(scrim));
+    }
+
+    protected float getDozeAlpha(View scrim) {
+        return scrim == mScrimBehind ? mDozeBehindAlpha : mDozeInFrontAlpha;
+    }
+
+    protected float getCurrentScrimAlpha(View scrim) {
+        return scrim == mScrimBehind ? mCurrentBehindAlpha
+                : scrim == mScrimInFront ? mCurrentInFrontAlpha
+                : mCurrentHeadsUpAlpha;
+    }
+
+    private void setCurrentScrimAlpha(View scrim, float alpha) {
+        if (scrim == mScrimBehind) {
+            mCurrentBehindAlpha = alpha;
+            mLightBarController.setScrimAlpha(mCurrentBehindAlpha);
+        } else if (scrim == mScrimInFront) {
+            mCurrentInFrontAlpha = alpha;
+        } else {
+            alpha = Math.max(0.0f, Math.min(1.0f, alpha));
+            mCurrentHeadsUpAlpha = alpha;
+        }
+    }
+
+    private void updateScrimColor(View scrim) {
+        float alpha1 = getCurrentScrimAlpha(scrim);
+        if (scrim instanceof ScrimView) {
+            ScrimView scrimView = (ScrimView) scrim;
+            float dozeAlpha = getDozeAlpha(scrim);
+            float alpha = 1 - (1 - alpha1) * (1 - dozeAlpha);
+            alpha = Math.max(0, Math.min(1.0f, alpha));
+            scrimView.setViewAlpha(alpha);
+
+            Trace.traceCounter(Trace.TRACE_TAG_APP,
+                    scrim == mScrimInFront ? "front_scrim_alpha" : "back_scrim_alpha",
+                    (int) (alpha * 255));
+
+            int dozeTint = Color.TRANSPARENT;
+
+            boolean dozing = mAnimatingDozeUnlock || mDozing;
+            boolean frontScrimDozing = mWakingUpFromAodInProgress;
+            if (dozing || frontScrimDozing && scrim == mScrimInFront) {
+                dozeTint = Color.BLACK;
+            }
+            Trace.traceCounter(Trace.TRACE_TAG_APP,
+                    scrim == mScrimInFront ? "front_scrim_tint" : "back_scrim_tint",
+                    dozeTint == Color.BLACK ? 1 : 0);
+
+            scrimView.setTint(dozeTint);
+        } else {
+            scrim.setAlpha(alpha1);
+        }
+        dispatchScrimsVisible();
+    }
+
+    private void startScrimAnimation(final View scrim, float target) {
+        float current = getCurrentScrimAlpha(scrim);
+        ValueAnimator anim = ValueAnimator.ofFloat(current, target);
+        anim.addUpdateListener(animation -> {
+            float alpha = (float) animation.getAnimatedValue();
+            setCurrentScrimAlpha(scrim, alpha);
+            updateScrimColor(scrim);
+            dispatchScrimsVisible();
+        });
+        anim.setInterpolator(getInterpolator());
+        anim.setStartDelay(mAnimationDelay);
+        anim.setDuration(mDurationOverride != -1 ? mDurationOverride : ANIMATION_DURATION);
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (!mDeferFinishedListener && mOnAnimationFinished != null) {
+                    mOnAnimationFinished.run();
+                    mOnAnimationFinished = null;
+                }
+                if (mKeyguardFadingOutInProgress) {
+                    mKeyguardFadeoutAnimation = null;
+                    mKeyguardFadingOutInProgress = false;
+                    mAnimatingDozeUnlock = false;
+                }
+                if (mWakingUpFromAodAnimationRunning && !mDeferFinishedListener) {
+                    mWakingUpFromAodAnimationRunning = false;
+                    mWakingUpFromAodInProgress = false;
+                }
+                scrim.setTag(TAG_KEY_ANIM, null);
+                scrim.setTag(TAG_KEY_ANIM_TARGET, null);
+                dispatchScrimsVisible();
+            }
+        });
+        anim.start();
+        if (mAnimateKeyguardFadingOut) {
+            mKeyguardFadingOutInProgress = true;
+            mKeyguardFadeoutAnimation = anim;
+        }
+        if (mWakingUpFromAodInProgress) {
+            mWakingUpFromAodAnimationRunning = true;
+        }
+        if (mSkipFirstFrame) {
+            anim.setCurrentPlayTime(16);
+        }
+        scrim.setTag(TAG_KEY_ANIM, anim);
+        scrim.setTag(TAG_KEY_ANIM_TARGET, target);
+    }
+
+    protected Interpolator getInterpolator() {
+        if (mAnimateKeyguardFadingOut && mKeyguardUpdateMonitor.needsSlowUnlockTransition()) {
+            return KEYGUARD_FADE_OUT_INTERPOLATOR_LOCKED;
+        } else if (mAnimateKeyguardFadingOut) {
+            return KEYGUARD_FADE_OUT_INTERPOLATOR;
+        } else {
+            return mInterpolator;
+        }
+    }
+
+    @Override
+    public boolean onPreDraw() {
+        mScrimBehind.getViewTreeObserver().removeOnPreDrawListener(this);
+        mUpdatePending = false;
+        if (mDontAnimateBouncerChanges) {
+            mDontAnimateBouncerChanges = false;
+        }
+        updateScrims();
+        mDurationOverride = -1;
+        mAnimationDelay = 0;
+        mSkipFirstFrame = false;
+
+        // Make sure that we always call the listener even if we didn't start an animation.
+        endAnimateKeyguardFadingOut(false /* force */);
+        return true;
+    }
+
+    private void endAnimateKeyguardFadingOut(boolean force) {
+        mAnimateKeyguardFadingOut = false;
+        if (force || (!isAnimating(mScrimInFront) && !isAnimating(mScrimBehind))) {
+            if (mOnAnimationFinished != null) {
+                mOnAnimationFinished.run();
+                mOnAnimationFinished = null;
+            }
+            mKeyguardFadingOutInProgress = false;
+            if (!mWakeAndUnlocking || force)
+                mAnimatingDozeUnlock = false;
+        }
+    }
+
+    private boolean isAnimating(View scrim) {
+        return scrim.getTag(TAG_KEY_ANIM) != null;
+    }
+
+    public void setDrawBehindAsSrc(boolean asSrc) {
+        mScrimBehind.setDrawAsSrc(asSrc);
+    }
+
+    @Override
+    public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {
+    }
+
+    @Override
+    public void onHeadsUpPinned(ExpandableNotificationRow headsUp) {
+        mPinnedHeadsUpCount++;
+        updateHeadsUpScrim(true);
+    }
+
+    @Override
+    public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {
+        mPinnedHeadsUpCount--;
+        if (headsUp == mDraggedHeadsUpView) {
+            mDraggedHeadsUpView = null;
+            mTopHeadsUpDragAmount = 0.0f;
+        }
+        updateHeadsUpScrim(true);
+    }
+
+    @Override
+    public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
+    }
+
+    private void updateHeadsUpScrim(boolean animate) {
+        updateScrim(animate, mHeadsUpScrim, calculateHeadsUpAlpha(), mCurrentHeadsUpAlpha);
+    }
+
+    private void updateScrim(boolean animate, View scrim, float alpha, float currentAlpha) {
+        if (mKeyguardFadingOutInProgress && mKeyguardFadeoutAnimation.getCurrentPlayTime() != 0) {
+            return;
+        }
+
+        ValueAnimator previousAnimator = ViewState.getChildTag(scrim,
+                TAG_KEY_ANIM);
+        float animEndValue = -1;
+        if (previousAnimator != null) {
+            if (animate || alpha == currentAlpha) {
+                // We are not done yet! Defer calling the finished listener.
+                if (animate) {
+                    mDeferFinishedListener = true;
+                }
+                previousAnimator.cancel();
+                mDeferFinishedListener = false;
+            } else {
+                animEndValue = ViewState.getChildTag(scrim, TAG_END_ALPHA);
+            }
+        }
+        if (alpha != currentAlpha && alpha != animEndValue) {
+            if (animate) {
+                startScrimAnimation(scrim, alpha);
+                scrim.setTag(TAG_START_ALPHA, currentAlpha);
+                scrim.setTag(TAG_END_ALPHA, alpha);
+            } else {
+                if (previousAnimator != null) {
+                    float previousStartValue = ViewState.getChildTag(scrim, TAG_START_ALPHA);
+                    float previousEndValue = ViewState.getChildTag(scrim, TAG_END_ALPHA);
+                    // we need to increase all animation keyframes of the previous animator by the
+                    // relative change to the end value
+                    PropertyValuesHolder[] values = previousAnimator.getValues();
+                    float relativeDiff = alpha - previousEndValue;
+                    float newStartValue = previousStartValue + relativeDiff;
+                    newStartValue = Math.max(0, Math.min(1.0f, newStartValue));
+                    values[0].setFloatValues(newStartValue, alpha);
+                    scrim.setTag(TAG_START_ALPHA, newStartValue);
+                    scrim.setTag(TAG_END_ALPHA, alpha);
+                    previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+                } else {
+                    // update the alpha directly
+                    setCurrentScrimAlpha(scrim, alpha);
+                    updateScrimColor(scrim);
+                }
+            }
+        }
+    }
+
+    /**
+     * Set the amount the current top heads up view is dragged. The range is from 0 to 1 and 0 means
+     * the heads up is in its resting space and 1 means it's fully dragged out.
+     *
+     * @param draggedHeadsUpView the dragged view
+     * @param topHeadsUpDragAmount how far is it dragged
+     */
+    public void setTopHeadsUpDragAmount(View draggedHeadsUpView, float topHeadsUpDragAmount) {
+        mTopHeadsUpDragAmount = topHeadsUpDragAmount;
+        mDraggedHeadsUpView = draggedHeadsUpView;
+        updateHeadsUpScrim(false);
+    }
+
+    private float calculateHeadsUpAlpha() {
+        float alpha;
+        if (mPinnedHeadsUpCount >= 2) {
+            alpha = 1.0f;
+        } else if (mPinnedHeadsUpCount == 0) {
+            alpha = 0.0f;
+        } else {
+            alpha = 1.0f - mTopHeadsUpDragAmount;
+        }
+        float expandFactor = (1.0f - mFraction);
+        expandFactor = Math.max(expandFactor, 0.0f);
+        return alpha * expandFactor;
+    }
+
+    public void forceHideScrims(boolean hide, boolean animated) {
+        mForceHideScrims = hide;
+        mAnimateChange = animated;
+        scheduleUpdate();
+    }
+
+    public void dontAnimateBouncerChangesUntilNextFrame() {
+        mDontAnimateBouncerChanges = true;
+    }
+
+    public void setExcludedBackgroundArea(Rect area) {
+        mScrimBehind.setExcludedArea(area);
+    }
+
+    public int getBackgroundColor() {
+        int color = mLockColors.getMainColor();
+        return Color.argb((int) (mScrimBehind.getAlpha() * Color.alpha(color)),
+                Color.red(color), Color.green(color), Color.blue(color));
+    }
+
+    public void setScrimBehindChangeRunnable(Runnable changeRunnable) {
+        mScrimBehind.setChangeRunnable(changeRunnable);
+    }
+
+    public void onDensityOrFontScaleChanged() {
+        ViewGroup.LayoutParams layoutParams = mHeadsUpScrim.getLayoutParams();
+        layoutParams.height = mHeadsUpScrim.getResources().getDimensionPixelSize(
+                R.dimen.heads_up_scrim_height);
+        mHeadsUpScrim.setLayoutParams(layoutParams);
+    }
+
+    public void setCurrentUser(int currentUser) {
+        // Don't care in the base class.
+    }
+
+    @Override
+    public void onColorsChanged(ColorExtractor colorExtractor, int which) {
+        if ((which & WallpaperManager.FLAG_LOCK) != 0) {
+            mLockColors = mColorExtractor.getColors(WallpaperManager.FLAG_LOCK,
+                    ColorExtractor.TYPE_DARK, true /* ignoreVisibility */);
+            mNeedsDrawableColorUpdate = true;
+            scheduleUpdate();
+        }
+        if ((which & WallpaperManager.FLAG_SYSTEM) != 0) {
+            mSystemColors = mColorExtractor.getColors(WallpaperManager.FLAG_SYSTEM,
+                    ColorExtractor.TYPE_DARK, mKeyguardShowing);
+            mNeedsDrawableColorUpdate = true;
+            scheduleUpdate();
+        }
+    }
+
+    public void dump(PrintWriter pw) {
+        pw.println(" ScrimController:");
+
+        pw.print("   frontScrim:"); pw.print(" viewAlpha="); pw.print(mScrimInFront.getViewAlpha());
+        pw.print(" alpha="); pw.print(mCurrentInFrontAlpha);
+        pw.print(" dozeAlpha="); pw.print(mDozeInFrontAlpha);
+        pw.print(" tint=0x"); pw.println(Integer.toHexString(mScrimInFront.getTint()));
+
+        pw.print("   backScrim:"); pw.print(" viewAlpha="); pw.print(mScrimBehind.getViewAlpha());
+        pw.print(" alpha="); pw.print(mCurrentBehindAlpha);
+        pw.print(" dozeAlpha="); pw.print(mDozeBehindAlpha);
+        pw.print(" tint=0x"); pw.println(Integer.toHexString(mScrimBehind.getTint()));
+
+        pw.print("   mBouncerShowing="); pw.println(mBouncerShowing);
+        pw.print("   mTracking="); pw.println(mTracking);
+        pw.print("   mForceHideScrims="); pw.println(mForceHideScrims);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/SettingsButton.java b/com/android/systemui/statusbar/phone/SettingsButton.java
new file mode 100644
index 0000000..6220fcb
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/SettingsButton.java
@@ -0,0 +1,174 @@
+/*
+ * 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.phone;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+
+import com.android.keyguard.AlphaOptimizedImageButton;
+import com.android.systemui.Interpolators;
+
+public class SettingsButton extends AlphaOptimizedImageButton {
+
+    private static final long LONG_PRESS_LENGTH = 1000;
+    private static final long ACCEL_LENGTH = 750;
+    private static final long FULL_SPEED_LENGTH = 375;
+    private static final long RUN_DURATION = 350;
+
+    private boolean mUpToSpeed;
+    private ObjectAnimator mAnimator;
+
+    private float mSlop;
+
+    public SettingsButton(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
+    }
+
+    public boolean isAnimating() {
+        return mAnimator != null && mAnimator.isRunning();
+    }
+
+    public boolean isTunerClick() {
+        return mUpToSpeed;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                postDelayed(mLongPressCallback, LONG_PRESS_LENGTH);
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mUpToSpeed) {
+                    startExitAnimation();
+                } else {
+                    cancelLongClick();
+                }
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                cancelLongClick();
+                break;
+            case MotionEvent.ACTION_MOVE:
+                float x = event.getX();
+                float y = event.getY();
+                if ((x < -mSlop) || (y < -mSlop) || (x > getWidth() + mSlop)
+                        || (y > getHeight() + mSlop)) {
+                    cancelLongClick();
+                }
+                break;
+        }
+        return super.onTouchEvent(event);
+    }
+
+    private void cancelLongClick() {
+        cancelAnimation();
+        mUpToSpeed = false;
+        removeCallbacks(mLongPressCallback);
+    }
+
+    private void cancelAnimation() {
+        if (mAnimator != null) {
+            mAnimator.removeAllListeners();
+            mAnimator.cancel();
+            mAnimator = null;
+        }
+    }
+
+    private void startExitAnimation() {
+        animate()
+                .translationX(((View) getParent().getParent()).getWidth() - getX())
+                .alpha(0)
+                .setDuration(RUN_DURATION)
+                .setInterpolator(AnimationUtils.loadInterpolator(mContext,
+                        android.R.interpolator.accelerate_cubic))
+                .setListener(new AnimatorListener() {
+                    @Override
+                    public void onAnimationStart(Animator animation) {
+                    }
+
+                    @Override
+                    public void onAnimationRepeat(Animator animation) {
+                    }
+
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        setAlpha(1f);
+                        setTranslationX(0);
+                        cancelLongClick();
+                    }
+
+                    @Override
+                    public void onAnimationCancel(Animator animation) {
+                    }
+                })
+                .start();
+    }
+
+    protected void startAccelSpin() {
+        cancelAnimation();
+        mAnimator = ObjectAnimator.ofFloat(this, View.ROTATION, 0, 360);
+        mAnimator.setInterpolator(AnimationUtils.loadInterpolator(mContext,
+                android.R.interpolator.accelerate_quad));
+        mAnimator.setDuration(ACCEL_LENGTH);
+        mAnimator.addListener(new AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+            }
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                startContinuousSpin();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+            }
+        });
+        mAnimator.start();
+    }
+
+    protected void startContinuousSpin() {
+        cancelAnimation();
+        performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+        mUpToSpeed = true;
+        mAnimator = ObjectAnimator.ofFloat(this, View.ROTATION, 0, 360);
+        mAnimator.setInterpolator(Interpolators.LINEAR);
+        mAnimator.setDuration(FULL_SPEED_LENGTH);
+        mAnimator.setRepeatCount(Animation.INFINITE);
+        mAnimator.start();
+    }
+
+    private final Runnable mLongPressCallback = new Runnable() {
+        @Override
+        public void run() {
+            startAccelSpin();
+        }
+    };
+}
diff --git a/com/android/systemui/statusbar/phone/SignalDrawable.java b/com/android/systemui/statusbar/phone/SignalDrawable.java
new file mode 100644
index 0000000..15ef742
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/SignalDrawable.java
@@ -0,0 +1,517 @@
+/*
+ * Copyright (C) 2017 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.phone;
+
+import android.animation.ArgbEvaluator;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Path.Direction;
+import android.graphics.Path.FillType;
+import android.graphics.Path.Op;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.util.LayoutDirection;
+
+import com.android.settingslib.R;
+import com.android.settingslib.Utils;
+import com.android.systemui.qs.SlashDrawable;
+
+public class SignalDrawable extends Drawable {
+
+    private static final String TAG = "SignalDrawable";
+
+    private static final int NUM_DOTS = 3;
+
+    private static final float VIEWPORT = 24f;
+    private static final float PAD = 2f / VIEWPORT;
+    private static final float CUT_OUT = 7.9f / VIEWPORT;
+
+    private static final float DOT_SIZE = 3f / VIEWPORT;
+    private static final float DOT_PADDING = 1f / VIEWPORT;
+    private static final float DOT_CUT_WIDTH = (DOT_SIZE * 3) + (DOT_PADDING * 5);
+    private static final float DOT_CUT_HEIGHT = (DOT_SIZE * 1) + (DOT_PADDING * 1);
+
+    private static final float[] FIT = {2.26f, -3.02f, 1.76f};
+
+    // All of these are masks to push all of the drawable state into one int for easy callbacks
+    // and flow through sysui.
+    private static final int LEVEL_MASK = 0xff;
+    private static final int NUM_LEVEL_SHIFT = 8;
+    private static final int NUM_LEVEL_MASK = 0xff << NUM_LEVEL_SHIFT;
+    private static final int STATE_SHIFT = 16;
+    private static final int STATE_MASK = 0xff << STATE_SHIFT;
+    private static final int STATE_NONE = 0;
+    private static final int STATE_EMPTY = 1;
+    private static final int STATE_CUT = 2;
+    private static final int STATE_CARRIER_CHANGE = 3;
+    private static final int STATE_AIRPLANE = 4;
+
+    private static final long DOT_DELAY = 1000;
+
+    private static float[][] X_PATH = new float[][]{
+            {21.9f / VIEWPORT, 17.0f / VIEWPORT},
+            {-1.1f / VIEWPORT, -1.1f / VIEWPORT},
+            {-1.9f / VIEWPORT, 1.9f / VIEWPORT},
+            {-1.9f / VIEWPORT, -1.9f / VIEWPORT},
+            {-1.1f / VIEWPORT, 1.1f / VIEWPORT},
+            {1.9f / VIEWPORT, 1.9f / VIEWPORT},
+            {-1.9f / VIEWPORT, 1.9f / VIEWPORT},
+            {1.1f / VIEWPORT, 1.1f / VIEWPORT},
+            {1.9f / VIEWPORT, -1.9f / VIEWPORT},
+            {1.9f / VIEWPORT, 1.9f / VIEWPORT},
+            {1.1f / VIEWPORT, -1.1f / VIEWPORT},
+            {-1.9f / VIEWPORT, -1.9f / VIEWPORT},
+    };
+
+    // Rounded corners are achieved by arcing a circle of radius `R` from its tangent points along
+    // the curve (curve ≡ triangle). On the top and left corners of the triangle, the tangents are
+    // as follows:
+    //      1) Along the straight lines (y = 0 and x = width):
+    //          Ps = circleOffset + R
+    //      2) Along the diagonal line (y = x):
+    //          Pd = √((Ps^2) / 2)
+    //              or (remember: sin(π/4) ≈ 0.7071)
+    //          Pd = (circleOffset + R - 0.7071, height - R - 0.7071)
+    //         Where Pd is the (x,y) coords of the point that intersects the circle at the bottom
+    //         left of the triangle
+    private static final float RADIUS_RATIO = 0.75f / 17f;
+    private static final float DIAG_OFFSET_MULTIPLIER = 0.707107f;
+    // How far the circle defining the corners is inset from the edges
+    private final float mAppliedCornerInset;
+
+    private static final float INV_TAN = 1f / (float) Math.tan(Math.PI / 8f);
+    private static final float CUT_WIDTH_DP = 1f / 12f;
+
+    // Where the top and left points of the triangle would be if not for rounding
+    private final PointF mVirtualTop  = new PointF();
+    private final PointF mVirtualLeft = new PointF();
+
+    private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private final Paint mForegroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private final int mDarkModeBackgroundColor;
+    private final int mDarkModeFillColor;
+    private final int mLightModeBackgroundColor;
+    private final int mLightModeFillColor;
+    private final Path mFullPath = new Path();
+    private final Path mForegroundPath = new Path();
+    private final Path mXPath = new Path();
+    // Cut out when STATE_EMPTY
+    private final Path mCutPath = new Path();
+    // Draws the slash when in airplane mode
+    private final SlashArtist mSlash = new SlashArtist();
+    private final Handler mHandler;
+    private float mOldDarkIntensity = -1;
+    private float mNumLevels = 1;
+    private int mIntrinsicSize;
+    private int mLevel;
+    private int mState;
+    private boolean mVisible;
+    private boolean mAnimating;
+    private int mCurrentDot;
+
+    public SignalDrawable(Context context) {
+        mDarkModeBackgroundColor =
+                Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_background);
+        mDarkModeFillColor =
+                Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_fill);
+        mLightModeBackgroundColor =
+                Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_background);
+        mLightModeFillColor =
+                Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_fill);
+        mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size);
+
+        mHandler = new Handler();
+        setDarkIntensity(0);
+
+        mAppliedCornerInset = context.getResources()
+                .getDimensionPixelSize(R.dimen.stat_sys_mobile_signal_circle_inset);
+    }
+
+    public void setIntrinsicSize(int size) {
+        mIntrinsicSize = size;
+    }
+
+    @Override
+    public int getIntrinsicWidth() {
+        return mIntrinsicSize;
+    }
+
+    @Override
+    public int getIntrinsicHeight() {
+        return mIntrinsicSize;
+    }
+
+    public void setNumLevels(int levels) {
+        if (levels == mNumLevels) return;
+        mNumLevels = levels;
+        invalidateSelf();
+    }
+
+    private void setSignalState(int state) {
+        if (state == mState) return;
+        mState = state;
+        updateAnimation();
+        invalidateSelf();
+    }
+
+    private void updateAnimation() {
+        boolean shouldAnimate = (mState == STATE_CARRIER_CHANGE) && mVisible;
+        if (shouldAnimate == mAnimating) return;
+        mAnimating = shouldAnimate;
+        if (shouldAnimate) {
+            mChangeDot.run();
+        } else {
+            mHandler.removeCallbacks(mChangeDot);
+        }
+    }
+
+    @Override
+    protected boolean onLevelChange(int state) {
+        setNumLevels(getNumLevels(state));
+        setSignalState(getState(state));
+        int level = getLevel(state);
+        if (level != mLevel) {
+            mLevel = level;
+            invalidateSelf();
+        }
+        return true;
+    }
+
+    public void setColors(int background, int foreground) {
+        mPaint.setColor(background);
+        mForegroundPaint.setColor(foreground);
+    }
+
+    public void setDarkIntensity(float darkIntensity) {
+        if (darkIntensity == mOldDarkIntensity) {
+            return;
+        }
+        mPaint.setColor(getBackgroundColor(darkIntensity));
+        mForegroundPaint.setColor(getFillColor(darkIntensity));
+        mOldDarkIntensity = darkIntensity;
+        invalidateSelf();
+    }
+
+    private int getFillColor(float darkIntensity) {
+        return getColorForDarkIntensity(
+                darkIntensity, mLightModeFillColor, mDarkModeFillColor);
+    }
+
+    private int getBackgroundColor(float darkIntensity) {
+        return getColorForDarkIntensity(
+                darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor);
+    }
+
+    private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
+        return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
+    }
+
+    @Override
+    protected void onBoundsChange(Rect bounds) {
+        super.onBoundsChange(bounds);
+        invalidateSelf();
+    }
+
+    @Override
+    public void draw(@NonNull Canvas canvas) {
+        final float width = getBounds().width();
+        final float height = getBounds().height();
+
+        boolean isRtl = getLayoutDirection() == LayoutDirection.RTL;
+        if (isRtl) {
+            canvas.save();
+            // Mirror the drawable
+            canvas.translate(width, 0);
+            canvas.scale(-1.0f, 1.0f);
+        }
+        mFullPath.reset();
+        mFullPath.setFillType(FillType.WINDING);
+
+        final float padding = Math.round(PAD * width);
+        final float cornerRadius = RADIUS_RATIO * height;
+        // Offset from circle where the hypotenuse meets the circle
+        final float diagOffset = DIAG_OFFSET_MULTIPLIER * cornerRadius;
+
+        // 1 - Bottom right, above corner
+        mFullPath.moveTo(width - padding, height - padding - cornerRadius);
+        // 2 - Line to top right, below corner
+        mFullPath.lineTo(width - padding, padding + cornerRadius + mAppliedCornerInset);
+        // 3 - Arc to top right, on hypotenuse
+        mFullPath.arcTo(
+                width - padding - (2 * cornerRadius),
+                padding + mAppliedCornerInset,
+                width - padding,
+                padding + mAppliedCornerInset + (2 * cornerRadius),
+                0.f, -135.f, false
+        );
+        // 4 - Line to bottom left, on hypotenuse
+        mFullPath.lineTo(padding + mAppliedCornerInset + cornerRadius - diagOffset,
+                height - padding - cornerRadius - diagOffset);
+        // 5 - Arc to bottom left, on leg
+        mFullPath.arcTo(
+                padding + mAppliedCornerInset,
+                height - padding - (2 * cornerRadius),
+                padding + mAppliedCornerInset + ( 2 * cornerRadius),
+                height - padding,
+                -135.f, -135.f, false
+        );
+        // 6 - Line to bottom rght, before corner
+        mFullPath.lineTo(width - padding - cornerRadius, height - padding);
+        // 7 - Arc to beginning (bottom right, above corner)
+        mFullPath.arcTo(
+                width - padding - (2 * cornerRadius),
+                height - padding - (2 * cornerRadius),
+                width - padding,
+                height - padding,
+                90.f, -90.f, false
+        );
+
+        if (mState == STATE_CARRIER_CHANGE) {
+            float cutWidth = (DOT_CUT_WIDTH * width);
+            float cutHeight = (DOT_CUT_HEIGHT * width);
+            float dotSize = (DOT_SIZE * height);
+            float dotPadding = (DOT_PADDING * height);
+
+            mFullPath.moveTo(width - padding, height - padding);
+            mFullPath.rLineTo(-cutWidth, 0);
+            mFullPath.rLineTo(0, -cutHeight);
+            mFullPath.rLineTo(cutWidth, 0);
+            mFullPath.rLineTo(0, cutHeight);
+            float dotSpacing = dotPadding * 2 + dotSize;
+            float x = width - padding - dotSize;
+            float y = height - padding - dotSize;
+            mForegroundPath.reset();
+            drawDot(mFullPath, mForegroundPath, x, y, dotSize, 2);
+            drawDot(mFullPath, mForegroundPath, x - dotSpacing, y, dotSize, 1);
+            drawDot(mFullPath, mForegroundPath, x - dotSpacing * 2, y, dotSize, 0);
+        } else if (mState == STATE_CUT) {
+            float cut = (CUT_OUT * width);
+            mFullPath.moveTo(width - padding, height - padding);
+            mFullPath.rLineTo(-cut, 0);
+            mFullPath.rLineTo(0, -cut);
+            mFullPath.rLineTo(cut, 0);
+            mFullPath.rLineTo(0, cut);
+        }
+
+        if (mState == STATE_EMPTY) {
+            // Where the corners would be if this were a real triangle
+            mVirtualTop.set(
+                    width - padding,
+                    (padding + cornerRadius + mAppliedCornerInset) - (INV_TAN * cornerRadius));
+            mVirtualLeft.set(
+                    (padding + cornerRadius + mAppliedCornerInset) - (INV_TAN * cornerRadius),
+                    height - padding);
+
+            final float cutWidth = CUT_WIDTH_DP * height;
+            final float cutDiagInset = cutWidth * INV_TAN;
+
+            // Cut out a smaller triangle from the center of mFullPath
+            mCutPath.reset();
+            mCutPath.setFillType(FillType.WINDING);
+            mCutPath.moveTo(width - padding - cutWidth, height - padding - cutWidth);
+            mCutPath.lineTo(width - padding - cutWidth, mVirtualTop.y + cutDiagInset);
+            mCutPath.lineTo(mVirtualLeft.x + cutDiagInset, height - padding - cutWidth);
+            mCutPath.lineTo(width - padding - cutWidth, height - padding - cutWidth);
+
+            // Draw empty state as only background
+            mForegroundPath.reset();
+            mFullPath.op(mCutPath, Path.Op.DIFFERENCE);
+        } else if (mState == STATE_AIRPLANE) {
+            // Airplane mode is slashed, fully drawn background
+            mForegroundPath.reset();
+            mSlash.draw((int) height, (int) width, canvas, mPaint);
+        } else if (mState != STATE_CARRIER_CHANGE) {
+            mForegroundPath.reset();
+            int sigWidth = Math.round(calcFit(mLevel / (mNumLevels - 1)) * (width - 2 * padding));
+            mForegroundPath.addRect(padding, padding, padding + sigWidth, height - padding,
+                    Direction.CW);
+            mForegroundPath.op(mFullPath, Op.INTERSECT);
+        }
+
+        canvas.drawPath(mFullPath, mPaint);
+        canvas.drawPath(mForegroundPath, mForegroundPaint);
+        if (mState == STATE_CUT) {
+            mXPath.reset();
+            mXPath.moveTo(X_PATH[0][0] * width, X_PATH[0][1] * height);
+            for (int i = 1; i < X_PATH.length; i++) {
+                mXPath.rLineTo(X_PATH[i][0] * width, X_PATH[i][1] * height);
+            }
+            canvas.drawPath(mXPath, mForegroundPaint);
+        }
+        if (isRtl) {
+            canvas.restore();
+        }
+    }
+
+    private void drawDot(Path fullPath, Path foregroundPath, float x, float y, float dotSize,
+            int i) {
+        Path p = (i == mCurrentDot) ? foregroundPath : fullPath;
+        p.addRect(x, y, x + dotSize, y + dotSize, Direction.CW);
+    }
+
+    // This is a fit line based on previous values of provided in assets, but if
+    // you look at the a plot of this actual fit, it makes a lot of sense, what it does
+    // is compress the areas that are very visually easy to see changes (the middle sections)
+    // and spread out the sections that are hard to see (each end of the icon).
+    // The current fit is cubic, but pretty easy to change the way the code is written (just add
+    // terms to the end of FIT).
+    private float calcFit(float v) {
+        float ret = 0;
+        float t = v;
+        for (int i = 0; i < FIT.length; i++) {
+            ret += FIT[i] * t;
+            t *= v;
+        }
+        return ret;
+    }
+
+    @Override
+    public int getAlpha() {
+        return mPaint.getAlpha();
+    }
+
+    @Override
+    public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
+        mPaint.setAlpha(alpha);
+        mForegroundPaint.setAlpha(alpha);
+    }
+
+    @Override
+    public void setColorFilter(@Nullable ColorFilter colorFilter) {
+        mPaint.setColorFilter(colorFilter);
+        mForegroundPaint.setColorFilter(colorFilter);
+    }
+
+    @Override
+    public int getOpacity() {
+        return 255;
+    }
+
+    @Override
+    public boolean setVisible(boolean visible, boolean restart) {
+        mVisible = visible;
+        updateAnimation();
+        return super.setVisible(visible, restart);
+    }
+
+    private final Runnable mChangeDot = new Runnable() {
+        @Override
+        public void run() {
+            if (++mCurrentDot == NUM_DOTS) {
+                mCurrentDot = 0;
+            }
+            invalidateSelf();
+            mHandler.postDelayed(mChangeDot, DOT_DELAY);
+        }
+    };
+
+    public static int getLevel(int fullState) {
+        return fullState & LEVEL_MASK;
+    }
+
+    public static int getState(int fullState) {
+        return (fullState & STATE_MASK) >> STATE_SHIFT;
+    }
+
+    public static int getNumLevels(int fullState) {
+        return (fullState & NUM_LEVEL_MASK) >> NUM_LEVEL_SHIFT;
+    }
+
+    public static int getState(int level, int numLevels, boolean cutOut) {
+        return ((cutOut ? STATE_CUT : 0) << STATE_SHIFT)
+                | (numLevels << NUM_LEVEL_SHIFT)
+                | level;
+    }
+
+    public static int getCarrierChangeState(int numLevels) {
+        return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
+    }
+
+    public static int getEmptyState(int numLevels) {
+        return (STATE_EMPTY << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
+    }
+
+    public static int getAirplaneModeState(int numLevels) {
+        return (STATE_AIRPLANE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
+    }
+
+    private final class SlashArtist {
+        // These values are derived in un-rotated (vertical) orientation
+        private static final float SLASH_WIDTH = 1.8384776f;
+        private static final float SLASH_HEIGHT = 22f;
+        private static final float CENTER_X = 10.65f;
+        private static final float CENTER_Y = 15.869239f;
+        private static final float SCALE = 24f;
+
+        // Bottom is derived during animation
+        private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE;
+        private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE;
+        private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE;
+        private static final float BOTTOM = (CENTER_Y + (SLASH_HEIGHT / 2)) / SCALE;
+        // Draw the slash washington-monument style; rotate to no-u-turn style
+        private static final float ROTATION = -45f;
+
+        private final Path mPath = new Path();
+        private final RectF mSlashRect = new RectF();
+
+        void draw(int height, int width, @NonNull Canvas canvas, Paint paint) {
+            Matrix m = new Matrix();
+            final float radius = scale(SlashDrawable.CORNER_RADIUS, width);
+            updateRect(
+                    scale(LEFT, width),
+                    scale(TOP, height),
+                    scale(RIGHT, width),
+                    scale(BOTTOM, height));
+
+            mPath.reset();
+            // Draw the slash vertically
+            mPath.addRoundRect(mSlashRect, radius, radius, Direction.CW);
+            m.setRotate(ROTATION, width / 2, height / 2);
+            mPath.transform(m);
+            canvas.drawPath(mPath, paint);
+
+            // Rotate back to vertical, and draw the cut-out rect next to this one
+            m.setRotate(-ROTATION, width / 2, height / 2);
+            mPath.transform(m);
+            m.setTranslate(mSlashRect.width(), 0);
+            mPath.transform(m);
+            mPath.addRoundRect(mSlashRect, radius, radius, Direction.CW);
+            m.setRotate(ROTATION, width / 2, height / 2);
+            mPath.transform(m);
+            canvas.clipOutPath(mPath);
+        }
+
+        void updateRect(float left, float top, float right, float bottom) {
+            mSlashRect.left = left;
+            mSlashRect.top = top;
+            mSlashRect.right = right;
+            mSlashRect.bottom = bottom;
+        }
+
+        private float scale(float frac, int width) {
+            return frac * width;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/StatusBar.java b/com/android/systemui/statusbar/phone/StatusBar.java
new file mode 100644
index 0000000..efc8d8b
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/StatusBar.java
@@ -0,0 +1,7582 @@
+/*
+ * Copyright (C) 2010 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.phone;
+
+import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
+import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
+import static android.app.StatusBarManager.windowStateToString;
+
+import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
+import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE;
+import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING;
+import static com.android.systemui.statusbar.notification.NotificationInflater.InflationCallback;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSLUCENT;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_WARNING;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityManager.StackId;
+import android.app.ActivityOptions;
+import android.app.INotificationManager;
+import android.app.KeyguardManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.app.StatusBarManager;
+import android.app.TaskStackBuilder;
+import android.app.WallpaperColors;
+import android.app.WallpaperManager;
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentCallbacks2;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.content.om.IOverlayManager;
+import android.content.om.OverlayInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioAttributes;
+import android.media.MediaMetadata;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.metrics.LogMaker;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.service.notification.NotificationListenerService.RankingMap;
+import android.service.notification.StatusBarNotification;
+import android.service.vr.IVrManager;
+import android.service.vr.IVrStateCallbacks;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.DisplayMetrics;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.view.Display;
+import android.view.HapticFeedbackConstants;
+import android.view.IWindowManager;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.ThreadedRenderer;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewStub;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.DateTimeView;
+import android.widget.ImageView;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.colorextraction.ColorExtractor;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.statusbar.NotificationVisibility;
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.internal.util.NotificationMessagingUtil;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.KeyguardHostView.OnDismissAction;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.ViewMediatorCallback;
+import com.android.systemui.ActivityStarterDelegate;
+import com.android.systemui.AutoReinflateContainer;
+import com.android.systemui.DejankUtils;
+import com.android.systemui.DemoMode;
+import com.android.systemui.Dependency;
+import com.android.systemui.EventLogTags;
+import com.android.systemui.ForegroundServiceController;
+import com.android.systemui.Interpolators;
+import com.android.systemui.Prefs;
+import com.android.systemui.R;
+import com.android.systemui.RecentsComponent;
+import com.android.systemui.SwipeHelper;
+import com.android.systemui.SystemUI;
+import com.android.systemui.SystemUIFactory;
+import com.android.systemui.UiOffloadThread;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.classifier.FalsingLog;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.doze.DozeHost;
+import com.android.systemui.doze.DozeLog;
+import com.android.systemui.doze.DozeReceiver;
+import com.android.systemui.fragments.ExtensionFragmentListener;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
+import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
+import com.android.systemui.qs.QSFragment;
+import com.android.systemui.qs.QSPanel;
+import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.car.CarQSFragment;
+import com.android.systemui.recents.Recents;
+import com.android.systemui.recents.ScreenPinningRequest;
+import com.android.systemui.recents.events.EventBus;
+import com.android.systemui.recents.events.activity.AppTransitionFinishedEvent;
+import com.android.systemui.recents.events.activity.UndockingTaskEvent;
+import com.android.systemui.recents.misc.SystemServicesProxy;
+import com.android.systemui.stackdivider.Divider;
+import com.android.systemui.stackdivider.WindowManagerProxy;
+import com.android.systemui.statusbar.ActivatableNotificationView;
+import com.android.systemui.statusbar.BackDropView;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.DismissView;
+import com.android.systemui.statusbar.DragDownHelper;
+import com.android.systemui.statusbar.EmptyShadeView;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.GestureRecorder;
+import com.android.systemui.statusbar.KeyboardShortcuts;
+import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.NotificationData.Entry;
+import com.android.systemui.statusbar.NotificationGuts;
+import com.android.systemui.statusbar.NotificationInfo;
+import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.NotificationSnooze;
+import com.android.systemui.statusbar.RemoteInputController;
+import com.android.systemui.statusbar.ScrimView;
+import com.android.systemui.statusbar.SignalClusterView;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.AboveShelfObserver;
+import com.android.systemui.statusbar.notification.InflationException;
+import com.android.systemui.statusbar.notification.RowInflaterTask;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
+import com.android.systemui.statusbar.policy.BrightnessMirrorController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
+import com.android.systemui.statusbar.policy.ExtensionController;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.KeyguardMonitor;
+import com.android.systemui.statusbar.policy.KeyguardMonitorImpl;
+import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
+import com.android.systemui.statusbar.policy.NetworkController;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+import com.android.systemui.statusbar.policy.PreviewInflater;
+import com.android.systemui.statusbar.policy.RemoteInputView;
+import com.android.systemui.statusbar.policy.UserInfoController;
+import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout
+        .OnChildLocationsChangedListener;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+import com.android.systemui.util.NotificationChannels;
+import com.android.systemui.util.leak.LeakDetector;
+import com.android.systemui.volume.VolumeComponent;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+
+public class StatusBar extends SystemUI implements DemoMode,
+        DragDownHelper.DragDownCallback, ActivityStarter, OnUnlockMethodChangedListener,
+        OnHeadsUpChangedListener, VisualStabilityManager.Callback, CommandQueue.Callbacks,
+        ActivatableNotificationView.OnActivatedListener,
+        ExpandableNotificationRow.ExpansionLogger, NotificationData.Environment,
+        ExpandableNotificationRow.OnExpandClickListener, InflationCallback,
+        ColorExtractor.OnColorsChangedListener, ConfigurationListener {
+    public static final boolean MULTIUSER_DEBUG = false;
+
+    public static final boolean ENABLE_REMOTE_INPUT =
+            SystemProperties.getBoolean("debug.enable_remote_input", true);
+    public static final boolean ENABLE_CHILD_NOTIFICATIONS
+            = SystemProperties.getBoolean("debug.child_notifs", true);
+    public static final boolean FORCE_REMOTE_INPUT_HISTORY =
+            SystemProperties.getBoolean("debug.force_remoteinput_history", false);
+    private static boolean ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT = false;
+
+    protected static final int MSG_SHOW_RECENT_APPS = 1019;
+    protected static final int MSG_HIDE_RECENT_APPS = 1020;
+    protected static final int MSG_TOGGLE_RECENTS_APPS = 1021;
+    protected static final int MSG_PRELOAD_RECENT_APPS = 1022;
+    protected static final int MSG_CANCEL_PRELOAD_RECENT_APPS = 1023;
+    protected static final int MSG_TOGGLE_KEYBOARD_SHORTCUTS_MENU = 1026;
+    protected static final int MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU = 1027;
+
+    protected static final boolean ENABLE_HEADS_UP = true;
+    protected static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up";
+
+    // Must match constant in Settings. Used to highlight preferences when linking to Settings.
+    private static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
+
+    private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
+
+    // Should match the values in PhoneWindowManager
+    public static final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey";
+    public static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps";
+    static public final String SYSTEM_DIALOG_REASON_SCREENSHOT = "screenshot";
+
+    private static final String BANNER_ACTION_CANCEL =
+            "com.android.systemui.statusbar.banner_action_cancel";
+    private static final String BANNER_ACTION_SETUP =
+            "com.android.systemui.statusbar.banner_action_setup";
+    private static final String NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION
+            = "com.android.systemui.statusbar.work_challenge_unlocked_notification_action";
+    public static final String TAG = "StatusBar";
+    public static final boolean DEBUG = false;
+    public static final boolean SPEW = false;
+    public static final boolean DUMPTRUCK = true; // extra dumpsys info
+    public static final boolean DEBUG_GESTURES = false;
+    public static final boolean DEBUG_MEDIA = false;
+    public static final boolean DEBUG_MEDIA_FAKE_ARTWORK = false;
+    public static final boolean DEBUG_CAMERA_LIFT = false;
+
+    public static final boolean DEBUG_WINDOW_STATE = false;
+
+    // additional instrumentation for testing purposes; intended to be left on during development
+    public static final boolean CHATTY = DEBUG;
+
+    public static final boolean SHOW_LOCKSCREEN_MEDIA_ARTWORK = true;
+
+    public static final String ACTION_FAKE_ARTWORK = "fake_artwork";
+
+    private static final int MSG_OPEN_NOTIFICATION_PANEL = 1000;
+    private static final int MSG_CLOSE_PANELS = 1001;
+    private static final int MSG_OPEN_SETTINGS_PANEL = 1002;
+    private static final int MSG_LAUNCH_TRANSITION_TIMEOUT = 1003;
+    // 1020-1040 reserved for BaseStatusBar
+
+    // Time after we abort the launch transition.
+    private static final long LAUNCH_TRANSITION_TIMEOUT_MS = 5000;
+
+    private static final boolean CLOSE_PANEL_WHEN_EMPTIED = true;
+
+    private static final int STATUS_OR_NAV_TRANSIENT =
+            View.STATUS_BAR_TRANSIENT | View.NAVIGATION_BAR_TRANSIENT;
+    private static final long AUTOHIDE_TIMEOUT_MS = 3000;
+
+    /** The minimum delay in ms between reports of notification visibility. */
+    private static final int VISIBILITY_REPORT_MIN_DELAY_MS = 500;
+
+    /**
+     * The delay to reset the hint text when the hint animation is finished running.
+     */
+    private static final int HINT_RESET_DELAY_MS = 1200;
+
+    private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
+            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
+            .build();
+
+    public static final int FADE_KEYGUARD_START_DELAY = 100;
+    public static final int FADE_KEYGUARD_DURATION = 300;
+    public static final int FADE_KEYGUARD_DURATION_PULSING = 96;
+
+    /** If true, the system is in the half-boot-to-decryption-screen state.
+     * Prudently disable QS and notifications.  */
+    private static final boolean ONLY_CORE_APPS;
+
+    /** If true, the lockscreen will show a distinct wallpaper */
+    private static final boolean ENABLE_LOCKSCREEN_WALLPAPER = true;
+
+    /* If true, the device supports freeform window management.
+     * This affects the status bar UI. */
+    private static final boolean FREEFORM_WINDOW_MANAGEMENT;
+
+    /**
+     * How long to wait before auto-dismissing a notification that was kept for remote input, and
+     * has now sent a remote input. We auto-dismiss, because the app may not see a reason to cancel
+     * these given that they technically don't exist anymore. We wait a bit in case the app issues
+     * an update.
+     */
+    private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
+
+    /**
+     * Never let the alpha become zero for surfaces that draw with SRC - otherwise the RenderNode
+     * won't draw anything and uninitialized memory will show through
+     * if mScrimSrcModeEnabled. Note that 0.001 is rounded down to 0 in
+     * libhwui.
+     */
+    private static final float SRC_MIN_ALPHA = 0.002f;
+
+    static {
+        boolean onlyCoreApps;
+        boolean freeformWindowManagement;
+        try {
+            IPackageManager packageManager =
+                    IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
+            onlyCoreApps = packageManager.isOnlyCoreApps();
+            freeformWindowManagement = packageManager.hasSystemFeature(
+                    PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT, 0);
+        } catch (RemoteException e) {
+            onlyCoreApps = false;
+            freeformWindowManagement = false;
+        }
+        ONLY_CORE_APPS = onlyCoreApps;
+        FREEFORM_WINDOW_MANAGEMENT = freeformWindowManagement;
+    }
+
+    /**
+     * The {@link StatusBarState} of the status bar.
+     */
+    protected int mState;
+    protected boolean mBouncerShowing;
+    protected boolean mShowLockscreenNotifications;
+    protected boolean mAllowLockscreenRemoteInput;
+
+    PhoneStatusBarPolicy mIconPolicy;
+
+    VolumeComponent mVolumeComponent;
+    BrightnessMirrorController mBrightnessMirrorController;
+    protected FingerprintUnlockController mFingerprintUnlockController;
+    LightBarController mLightBarController;
+    protected LockscreenWallpaper mLockscreenWallpaper;
+
+    int mNaturalBarHeight = -1;
+
+    Point mCurrentDisplaySize = new Point();
+
+    protected StatusBarWindowView mStatusBarWindow;
+    protected PhoneStatusBarView mStatusBarView;
+    private int mStatusBarWindowState = WINDOW_STATE_SHOWING;
+    protected StatusBarWindowManager mStatusBarWindowManager;
+    protected UnlockMethodCache mUnlockMethodCache;
+    private DozeServiceHost mDozeServiceHost = new DozeServiceHost();
+    private boolean mWakeUpComingFromTouch;
+    private PointF mWakeUpTouchLocation;
+
+    int mPixelFormat;
+    Object mQueueLock = new Object();
+
+    protected StatusBarIconController mIconController;
+
+    // expanded notifications
+    protected NotificationPanelView mNotificationPanel; // the sliding/resizing panel within the notification window
+    View mExpandedContents;
+    TextView mNotificationPanelDebugText;
+
+    /**
+     * {@code true} if notifications not part of a group should by default be rendered in their
+     * expanded state. If {@code false}, then only the first notification will be expanded if
+     * possible.
+     */
+    private boolean mAlwaysExpandNonGroupedNotification;
+
+    // settings
+    private QSPanel mQSPanel;
+
+    // top bar
+    protected KeyguardStatusBarView mKeyguardStatusBar;
+    boolean mLeaveOpenOnKeyguardHide;
+    KeyguardIndicationController mKeyguardIndicationController;
+
+    // Keyguard is going away soon.
+    private boolean mKeyguardGoingAway;
+    // Keyguard is actually fading away now.
+    protected boolean mKeyguardFadingAway;
+    protected long mKeyguardFadingAwayDelay;
+    protected long mKeyguardFadingAwayDuration;
+
+    // RemoteInputView to be activated after unlock
+    private View mPendingRemoteInputView;
+    private View mPendingWorkRemoteInputView;
+
+    private View mReportRejectedTouch;
+
+    int mMaxAllowedKeyguardNotifications;
+
+    boolean mExpandedVisible;
+
+    // the tracker view
+    int mTrackingPosition; // the position of the top of the tracking view.
+
+    // Tracking finger for opening/closing.
+    boolean mTracking;
+
+    int[] mAbsPos = new int[2];
+    ArrayList<Runnable> mPostCollapseRunnables = new ArrayList<>();
+
+    // for disabling the status bar
+    int mDisabled1 = 0;
+    int mDisabled2 = 0;
+
+    // tracking calls to View.setSystemUiVisibility()
+    int mSystemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE;
+    private final Rect mLastFullscreenStackBounds = new Rect();
+    private final Rect mLastDockedStackBounds = new Rect();
+    private final Rect mTmpRect = new Rect();
+
+    // last value sent to window manager
+    private int mLastDispatchedSystemUiVisibility = ~View.SYSTEM_UI_FLAG_VISIBLE;
+
+    DisplayMetrics mDisplayMetrics = new DisplayMetrics();
+
+    // XXX: gesture research
+    private final GestureRecorder mGestureRec = DEBUG_GESTURES
+        ? new GestureRecorder("/sdcard/statusbar_gestures.dat")
+        : null;
+
+    private ScreenPinningRequest mScreenPinningRequest;
+
+    private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
+
+    // ensure quick settings is disabled until the current user makes it through the setup wizard
+    private boolean mUserSetup = false;
+    private DeviceProvisionedListener mUserSetupObserver = new DeviceProvisionedListener() {
+        @Override
+        public void onUserSetupChanged() {
+            final boolean userSetup = mDeviceProvisionedController.isUserSetup(
+                    mDeviceProvisionedController.getCurrentUser());
+            if (MULTIUSER_DEBUG) Log.d(TAG, String.format("User setup changed: " +
+                    "userSetup=%s mUserSetup=%s", userSetup, mUserSetup));
+
+            if (userSetup != mUserSetup) {
+                mUserSetup = userSetup;
+                if (!mUserSetup && mStatusBarView != null)
+                    animateCollapseQuickSettings();
+                if (mNotificationPanel != null) {
+                    mNotificationPanel.setUserSetupComplete(mUserSetup);
+                }
+                updateQsExpansionEnabled();
+            }
+        }
+    };
+
+    protected H mHandler = createHandler();
+    final private ContentObserver mHeadsUpObserver = new ContentObserver(mHandler) {
+        @Override
+        public void onChange(boolean selfChange) {
+            boolean wasUsing = mUseHeadsUp;
+            mUseHeadsUp = ENABLE_HEADS_UP && !mDisableNotificationAlerts
+                    && Settings.Global.HEADS_UP_OFF != Settings.Global.getInt(
+                    mContext.getContentResolver(), Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED,
+                    Settings.Global.HEADS_UP_OFF);
+            mHeadsUpTicker = mUseHeadsUp && 0 != Settings.Global.getInt(
+                    mContext.getContentResolver(), SETTING_HEADS_UP_TICKER, 0);
+            Log.d(TAG, "heads up is " + (mUseHeadsUp ? "enabled" : "disabled"));
+            if (wasUsing != mUseHeadsUp) {
+                if (!mUseHeadsUp) {
+                    Log.d(TAG, "dismissing any existing heads up notification on disable event");
+                    mHeadsUpManager.releaseAllImmediately();
+                }
+            }
+        }
+    };
+
+    private int mInteractingWindows;
+    private boolean mAutohideSuspended;
+    private int mStatusBarMode;
+    private int mMaxKeyguardNotifications;
+
+    private ViewMediatorCallback mKeyguardViewMediatorCallback;
+    protected ScrimController mScrimController;
+    protected DozeScrimController mDozeScrimController;
+    private final UiOffloadThread mUiOffloadThread = Dependency.get(UiOffloadThread.class);
+
+    private final Runnable mAutohide = () -> {
+        int requested = mSystemUiVisibility & ~STATUS_OR_NAV_TRANSIENT;
+        if (mSystemUiVisibility != requested) {
+            notifyUiVisibilityChanged(requested);
+        }
+    };
+
+    private boolean mWaitingForKeyguardExit;
+    protected boolean mDozing;
+    private boolean mDozingRequested;
+    protected boolean mScrimSrcModeEnabled;
+
+    public static final Interpolator ALPHA_IN = Interpolators.ALPHA_IN;
+    public static final Interpolator ALPHA_OUT = Interpolators.ALPHA_OUT;
+
+    protected BackDropView mBackdrop;
+    protected ImageView mBackdropFront, mBackdropBack;
+    protected PorterDuffXfermode mSrcXferMode = new PorterDuffXfermode(PorterDuff.Mode.SRC);
+    protected PorterDuffXfermode mSrcOverXferMode =
+            new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER);
+
+    private MediaSessionManager mMediaSessionManager;
+    private MediaController mMediaController;
+    private String mMediaNotificationKey;
+    private MediaMetadata mMediaMetadata;
+    private MediaController.Callback mMediaListener
+            = new MediaController.Callback() {
+        @Override
+        public void onPlaybackStateChanged(PlaybackState state) {
+            super.onPlaybackStateChanged(state);
+            if (DEBUG_MEDIA) Log.v(TAG, "DEBUG_MEDIA: onPlaybackStateChanged: " + state);
+            if (state != null) {
+                if (!isPlaybackActive(state.getState())) {
+                    clearCurrentMediaNotification();
+                    updateMediaMetaData(true, true);
+                }
+            }
+        }
+
+        @Override
+        public void onMetadataChanged(MediaMetadata metadata) {
+            super.onMetadataChanged(metadata);
+            if (DEBUG_MEDIA) Log.v(TAG, "DEBUG_MEDIA: onMetadataChanged: " + metadata);
+            mMediaMetadata = metadata;
+            updateMediaMetaData(true, true);
+        }
+    };
+
+    private final OnChildLocationsChangedListener mOnChildLocationsChangedListener =
+            new OnChildLocationsChangedListener() {
+        @Override
+        public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout) {
+            userActivity();
+        }
+    };
+
+    private int mDisabledUnmodified1;
+    private int mDisabledUnmodified2;
+
+    /** Keys of notifications currently visible to the user. */
+    private final ArraySet<NotificationVisibility> mCurrentlyVisibleNotifications =
+            new ArraySet<>();
+    private long mLastVisibilityReportUptimeMs;
+
+    private Runnable mLaunchTransitionEndRunnable;
+    protected boolean mLaunchTransitionFadingAway;
+    private ExpandableNotificationRow mDraggedDownRow;
+    private boolean mLaunchCameraOnScreenTurningOn;
+    private boolean mLaunchCameraOnFinishedGoingToSleep;
+    private int mLastCameraLaunchSource;
+    private PowerManager.WakeLock mGestureWakeLock;
+    private Vibrator mVibrator;
+    private long[] mCameraLaunchGestureVibePattern;
+
+    private final int[] mTmpInt2 = new int[2];
+
+    // Fingerprint (as computed by getLoggingFingerprint() of the last logged state.
+    private int mLastLoggedStateFingerprint;
+    private boolean mTopHidesStatusBar;
+    private boolean mStatusBarWindowHidden;
+    private boolean mHideIconsForBouncer;
+    private boolean mIsOccluded;
+    private boolean mWereIconsJustHidden;
+    private boolean mBouncerWasShowingWhenHidden;
+
+    public boolean isStartedGoingToSleep() {
+        return mStartedGoingToSleep;
+    }
+
+    /**
+     * If set, the device has started going to sleep but isn't fully non-interactive yet.
+     */
+    protected boolean mStartedGoingToSleep;
+
+    private final OnChildLocationsChangedListener mNotificationLocationsChangedListener =
+            new OnChildLocationsChangedListener() {
+                @Override
+                public void onChildLocationsChanged(
+                        NotificationStackScrollLayout stackScrollLayout) {
+                    if (mHandler.hasCallbacks(mVisibilityReporter)) {
+                        // Visibilities will be reported when the existing
+                        // callback is executed.
+                        return;
+                    }
+                    // Calculate when we're allowed to run the visibility
+                    // reporter. Note that this timestamp might already have
+                    // passed. That's OK, the callback will just be executed
+                    // ASAP.
+                    long nextReportUptimeMs =
+                            mLastVisibilityReportUptimeMs + VISIBILITY_REPORT_MIN_DELAY_MS;
+                    mHandler.postAtTime(mVisibilityReporter, nextReportUptimeMs);
+                }
+            };
+
+    // Tracks notifications currently visible in mNotificationStackScroller and
+    // emits visibility events via NoMan on changes.
+    protected final Runnable mVisibilityReporter = new Runnable() {
+        private final ArraySet<NotificationVisibility> mTmpNewlyVisibleNotifications =
+                new ArraySet<>();
+        private final ArraySet<NotificationVisibility> mTmpCurrentlyVisibleNotifications =
+                new ArraySet<>();
+        private final ArraySet<NotificationVisibility> mTmpNoLongerVisibleNotifications =
+                new ArraySet<>();
+
+        @Override
+        public void run() {
+            mLastVisibilityReportUptimeMs = SystemClock.uptimeMillis();
+            final String mediaKey = getCurrentMediaNotificationKey();
+
+            // 1. Loop over mNotificationData entries:
+            //   A. Keep list of visible notifications.
+            //   B. Keep list of previously hidden, now visible notifications.
+            // 2. Compute no-longer visible notifications by removing currently
+            //    visible notifications from the set of previously visible
+            //    notifications.
+            // 3. Report newly visible and no-longer visible notifications.
+            // 4. Keep currently visible notifications for next report.
+            ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+            int N = activeNotifications.size();
+            for (int i = 0; i < N; i++) {
+                Entry entry = activeNotifications.get(i);
+                String key = entry.notification.getKey();
+                boolean isVisible = mStackScroller.isInVisibleLocation(entry.row);
+                NotificationVisibility visObj = NotificationVisibility.obtain(key, i, isVisible);
+                boolean previouslyVisible = mCurrentlyVisibleNotifications.contains(visObj);
+                if (isVisible) {
+                    // Build new set of visible notifications.
+                    mTmpCurrentlyVisibleNotifications.add(visObj);
+                    if (!previouslyVisible) {
+                        mTmpNewlyVisibleNotifications.add(visObj);
+                    }
+                } else {
+                    // release object
+                    visObj.recycle();
+                }
+            }
+            mTmpNoLongerVisibleNotifications.addAll(mCurrentlyVisibleNotifications);
+            mTmpNoLongerVisibleNotifications.removeAll(mTmpCurrentlyVisibleNotifications);
+
+            logNotificationVisibilityChanges(
+                    mTmpNewlyVisibleNotifications, mTmpNoLongerVisibleNotifications);
+
+            recycleAllVisibilityObjects(mCurrentlyVisibleNotifications);
+            mCurrentlyVisibleNotifications.addAll(mTmpCurrentlyVisibleNotifications);
+
+            recycleAllVisibilityObjects(mTmpNoLongerVisibleNotifications);
+            mTmpCurrentlyVisibleNotifications.clear();
+            mTmpNewlyVisibleNotifications.clear();
+            mTmpNoLongerVisibleNotifications.clear();
+        }
+    };
+
+    private NotificationMessagingUtil mMessagingUtil;
+    private KeyguardUserSwitcher mKeyguardUserSwitcher;
+    private UserSwitcherController mUserSwitcherController;
+    private NetworkController mNetworkController;
+    private KeyguardMonitorImpl mKeyguardMonitor
+            = (KeyguardMonitorImpl) Dependency.get(KeyguardMonitor.class);
+    private BatteryController mBatteryController;
+    protected boolean mPanelExpanded;
+    private IOverlayManager mOverlayManager;
+    private boolean mKeyguardRequested;
+    private boolean mIsKeyguard;
+    private LogMaker mStatusBarStateLog;
+    private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
+    protected NotificationIconAreaController mNotificationIconAreaController;
+    private boolean mReinflateNotificationsOnUserSwitched;
+    private HashMap<String, Entry> mPendingNotifications = new HashMap<>();
+    private boolean mClearAllEnabled;
+    @Nullable private View mAmbientIndicationContainer;
+    private String mKeyToRemoveOnGutsClosed;
+    private SysuiColorExtractor mColorExtractor;
+    private ForegroundServiceController mForegroundServiceController;
+    private ScreenLifecycle mScreenLifecycle;
+    @VisibleForTesting WakefulnessLifecycle mWakefulnessLifecycle;
+
+    private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) {
+        final int N = array.size();
+        for (int i = 0 ; i < N; i++) {
+            array.valueAt(i).recycle();
+        }
+        array.clear();
+    }
+
+    private final View.OnClickListener mGoToLockedShadeListener = v -> {
+        if (mState == StatusBarState.KEYGUARD) {
+            wakeUpIfDozing(SystemClock.uptimeMillis(), v);
+            goToLockedShade(null);
+        }
+    };
+    private HashMap<ExpandableNotificationRow, List<ExpandableNotificationRow>> mTmpChildOrderMap
+            = new HashMap<>();
+    private RankingMap mLatestRankingMap;
+    private boolean mNoAnimationOnNextBarModeChange;
+    private FalsingManager mFalsingManager;
+
+    private KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() {
+        @Override
+        public void onDreamingStateChanged(boolean dreaming) {
+            if (dreaming) {
+                maybeEscalateHeadsUp();
+            }
+        }
+    };
+
+    private NavigationBarFragment mNavigationBar;
+    private View mNavigationBarView;
+
+    @Override
+    public void start() {
+        mNetworkController = Dependency.get(NetworkController.class);
+        mUserSwitcherController = Dependency.get(UserSwitcherController.class);
+        mScreenLifecycle = Dependency.get(ScreenLifecycle.class);
+        mScreenLifecycle.addObserver(mScreenObserver);
+        mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class);
+        mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
+        mBatteryController = Dependency.get(BatteryController.class);
+        mAssistManager = Dependency.get(AssistManager.class);
+        mSystemServicesProxy = SystemServicesProxy.getInstance(mContext);
+        mOverlayManager = IOverlayManager.Stub.asInterface(
+                ServiceManager.getService(Context.OVERLAY_SERVICE));
+
+        mColorExtractor = Dependency.get(SysuiColorExtractor.class);
+        mColorExtractor.addOnColorsChangedListener(this);
+
+        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+
+        mForegroundServiceController = Dependency.get(ForegroundServiceController.class);
+
+        mDisplay = mWindowManager.getDefaultDisplay();
+        updateDisplaySize();
+
+        Resources res = mContext.getResources();
+        mScrimSrcModeEnabled = res.getBoolean(R.bool.config_status_bar_scrim_behind_use_src);
+        mClearAllEnabled = res.getBoolean(R.bool.config_enableNotificationsClearAll);
+        mAlwaysExpandNonGroupedNotification =
+                res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications);
+
+        DateTimeView.setReceiverHandler(Dependency.get(Dependency.TIME_TICK_HANDLER));
+        putComponent(StatusBar.class, this);
+
+        // start old BaseStatusBar.start().
+        mWindowManagerService = WindowManagerGlobal.getWindowManagerService();
+        mDevicePolicyManager = (DevicePolicyManager) mContext.getSystemService(
+                Context.DEVICE_POLICY_SERVICE);
+
+        mNotificationData = new NotificationData(this);
+        mMessagingUtil = new NotificationMessagingUtil(mContext);
+
+        mAccessibilityManager = (AccessibilityManager)
+                mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+
+        mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+
+        mDeviceProvisionedController = Dependency.get(DeviceProvisionedController.class);
+        mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Global.getUriFor(Settings.Global.ZEN_MODE), false,
+                mSettingsObserver);
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS), false,
+                mLockscreenSettingsObserver,
+                UserHandle.USER_ALL);
+        if (ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT) {
+            mContext.getContentResolver().registerContentObserver(
+                    Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_ALLOW_REMOTE_INPUT),
+                    false,
+                    mSettingsObserver,
+                    UserHandle.USER_ALL);
+        }
+
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS),
+                true,
+                mLockscreenSettingsObserver,
+                UserHandle.USER_ALL);
+
+        mBarService = IStatusBarService.Stub.asInterface(
+                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+
+        mRecents = getComponent(Recents.class);
+
+        final Configuration currentConfig = res.getConfiguration();
+        mLocale = currentConfig.locale;
+        mLayoutDirection = TextUtils.getLayoutDirectionFromLocale(mLocale);
+
+        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+        mKeyguardManager = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
+        mLockPatternUtils = new LockPatternUtils(mContext);
+
+        // Connect in to the status bar manager service
+        mCommandQueue = getComponent(CommandQueue.class);
+        mCommandQueue.addCallbacks(this);
+
+        int[] switches = new int[9];
+        ArrayList<IBinder> binders = new ArrayList<>();
+        ArrayList<String> iconSlots = new ArrayList<>();
+        ArrayList<StatusBarIcon> icons = new ArrayList<>();
+        Rect fullscreenStackBounds = new Rect();
+        Rect dockedStackBounds = new Rect();
+        try {
+            mBarService.registerStatusBar(mCommandQueue, iconSlots, icons, switches, binders,
+                    fullscreenStackBounds, dockedStackBounds);
+        } catch (RemoteException ex) {
+            // If the system process isn't there we're doomed anyway.
+        }
+
+        createAndAddWindows();
+
+        mSettingsObserver.onChange(false); // set up
+        mCommandQueue.disable(switches[0], switches[6], false /* animate */);
+        setSystemUiVisibility(switches[1], switches[7], switches[8], 0xffffffff,
+                fullscreenStackBounds, dockedStackBounds);
+        topAppWindowChanged(switches[2] != 0);
+        // StatusBarManagerService has a back up of IME token and it's restored here.
+        setImeWindowStatus(binders.get(0), switches[3], switches[4], switches[5] != 0);
+
+        // Set up the initial icon state
+        int N = iconSlots.size();
+        for (int i=0; i < N; i++) {
+            mCommandQueue.setIcon(iconSlots.get(i), icons.get(i));
+        }
+
+        // Set up the initial notification state.
+        try {
+            mNotificationListener.registerAsSystemService(mContext,
+                    new ComponentName(mContext.getPackageName(), getClass().getCanonicalName()),
+                    UserHandle.USER_ALL);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Unable to register notification listener", e);
+        }
+
+
+        if (DEBUG) {
+            Log.d(TAG, String.format(
+                    "init: icons=%d disabled=0x%08x lights=0x%08x menu=0x%08x imeButton=0x%08x",
+                   icons.size(),
+                   switches[0],
+                   switches[1],
+                   switches[2],
+                   switches[3]
+                   ));
+        }
+
+        mCurrentUserId = ActivityManager.getCurrentUser();
+        setHeadsUpUser(mCurrentUserId);
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_USER_SWITCHED);
+        filter.addAction(Intent.ACTION_USER_ADDED);
+        filter.addAction(Intent.ACTION_USER_PRESENT);
+        mContext.registerReceiver(mBaseBroadcastReceiver, filter);
+
+        IntentFilter internalFilter = new IntentFilter();
+        internalFilter.addAction(NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION);
+        internalFilter.addAction(BANNER_ACTION_CANCEL);
+        internalFilter.addAction(BANNER_ACTION_SETUP);
+        mContext.registerReceiver(mBaseBroadcastReceiver, internalFilter, PERMISSION_SELF, null);
+
+        IntentFilter allUsersFilter = new IntentFilter();
+        allUsersFilter.addAction(
+                DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED);
+        allUsersFilter.addAction(Intent.ACTION_DEVICE_LOCKED_CHANGED);
+        mContext.registerReceiverAsUser(mAllUsersReceiver, UserHandle.ALL, allUsersFilter,
+                null, null);
+        updateCurrentProfilesCache();
+
+        IVrManager vrManager = IVrManager.Stub.asInterface(ServiceManager.getService(
+                Context.VR_SERVICE));
+        try {
+            vrManager.registerListener(mVrStateCallbacks);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to register VR mode state listener: " + e);
+        }
+
+        mNonBlockablePkgs = new HashSet<>();
+        Collections.addAll(mNonBlockablePkgs, res.getStringArray(
+                com.android.internal.R.array.config_nonBlockableNotificationPackages));
+        // end old BaseStatusBar.start().
+
+        mMediaSessionManager
+                = (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
+        // TODO: use MediaSessionManager.SessionListener to hook us up to future updates
+        // in session state
+
+        // Lastly, call to the icon policy to install/update all the icons.
+        mIconPolicy = new PhoneStatusBarPolicy(mContext, mIconController);
+        mSettingsObserver.onChange(false); // set up
+
+        mHeadsUpObserver.onChange(true); // set up
+        if (ENABLE_HEADS_UP) {
+            mContext.getContentResolver().registerContentObserver(
+                    Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED), true,
+                    mHeadsUpObserver);
+            mContext.getContentResolver().registerContentObserver(
+                    Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true,
+                    mHeadsUpObserver);
+        }
+        mUnlockMethodCache = UnlockMethodCache.getInstance(mContext);
+        mUnlockMethodCache.addListener(this);
+        startKeyguard();
+
+        KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateCallback);
+        putComponent(DozeHost.class, mDozeServiceHost);
+
+        notifyUserAboutHiddenNotifications();
+
+        mScreenPinningRequest = new ScreenPinningRequest(mContext);
+        mFalsingManager = FalsingManager.getInstance(mContext);
+
+        Dependency.get(ActivityStarterDelegate.class).setActivityStarterImpl(this);
+
+        Dependency.get(ConfigurationController.class).addCallback(this);
+    }
+
+    protected void createIconController() {
+    }
+
+    // ================================================================================
+    // Constructing the view
+    // ================================================================================
+    protected void makeStatusBarView() {
+        final Context context = mContext;
+        updateDisplaySize(); // populates mDisplayMetrics
+        updateResources();
+        updateTheme();
+
+        inflateStatusBarWindow(context);
+        mStatusBarWindow.setService(this);
+        mStatusBarWindow.setOnTouchListener(getStatusBarWindowTouchListener());
+
+        // TODO: Deal with the ugliness that comes from having some of the statusbar broken out
+        // into fragments, but the rest here, it leaves some awkward lifecycle and whatnot.
+        mNotificationPanel = (NotificationPanelView) mStatusBarWindow.findViewById(
+                R.id.notification_panel);
+        mStackScroller = (NotificationStackScrollLayout) mStatusBarWindow.findViewById(
+                R.id.notification_stack_scroller);
+        mNotificationPanel.setStatusBar(this);
+        mNotificationPanel.setGroupManager(mGroupManager);
+        mAboveShelfObserver = new AboveShelfObserver(mStackScroller);
+        mAboveShelfObserver.setListener(mStatusBarWindow.findViewById(
+                R.id.notification_container_parent));
+        mKeyguardStatusBar = (KeyguardStatusBarView) mStatusBarWindow.findViewById(R.id.keyguard_header);
+
+        mNotificationIconAreaController = SystemUIFactory.getInstance()
+                .createNotificationIconAreaController(context, this);
+        inflateShelf();
+        mNotificationIconAreaController.setupShelf(mNotificationShelf);
+        Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mNotificationIconAreaController);
+        FragmentHostManager.get(mStatusBarWindow)
+                .addTagListener(CollapsedStatusBarFragment.TAG, (tag, fragment) -> {
+                    CollapsedStatusBarFragment statusBarFragment =
+                            (CollapsedStatusBarFragment) fragment;
+                    statusBarFragment.initNotificationIconArea(mNotificationIconAreaController);
+                    mStatusBarView = (PhoneStatusBarView) fragment.getView();
+                    mStatusBarView.setBar(this);
+                    mStatusBarView.setPanel(mNotificationPanel);
+                    mStatusBarView.setScrimController(mScrimController);
+                    mStatusBarView.setBouncerShowing(mBouncerShowing);
+                    setAreThereNotifications();
+                    checkBarModes();
+                }).getFragmentManager()
+                .beginTransaction()
+                .replace(R.id.status_bar_container, new CollapsedStatusBarFragment(),
+                        CollapsedStatusBarFragment.TAG)
+                .commit();
+        mIconController = Dependency.get(StatusBarIconController.class);
+
+        mHeadsUpManager = new HeadsUpManager(context, mStatusBarWindow, mGroupManager);
+        mHeadsUpManager.setBar(this);
+        mHeadsUpManager.addListener(this);
+        mHeadsUpManager.addListener(mNotificationPanel);
+        mHeadsUpManager.addListener(mGroupManager);
+        mHeadsUpManager.addListener(mVisualStabilityManager);
+        mNotificationPanel.setHeadsUpManager(mHeadsUpManager);
+        mNotificationData.setHeadsUpManager(mHeadsUpManager);
+        mGroupManager.setHeadsUpManager(mHeadsUpManager);
+        mHeadsUpManager.setVisualStabilityManager(mVisualStabilityManager);
+
+        if (MULTIUSER_DEBUG) {
+            mNotificationPanelDebugText = (TextView) mNotificationPanel.findViewById(
+                    R.id.header_debug_info);
+            mNotificationPanelDebugText.setVisibility(View.VISIBLE);
+        }
+
+        try {
+            boolean showNav = mWindowManagerService.hasNavigationBar();
+            if (DEBUG) Log.v(TAG, "hasNavigationBar=" + showNav);
+            if (showNav) {
+                createNavigationBar();
+            }
+        } catch (RemoteException ex) {
+            // no window manager? good luck with that
+        }
+
+        // figure out which pixel-format to use for the status bar.
+        mPixelFormat = PixelFormat.OPAQUE;
+
+        mStackScroller.setLongPressListener(getNotificationLongClicker());
+        mStackScroller.setStatusBar(this);
+        mStackScroller.setGroupManager(mGroupManager);
+        mStackScroller.setHeadsUpManager(mHeadsUpManager);
+        mGroupManager.setOnGroupChangeListener(mStackScroller);
+        mVisualStabilityManager.setVisibilityLocationProvider(mStackScroller);
+
+        inflateEmptyShadeView();
+        inflateDismissView();
+        mExpandedContents = mStackScroller;
+
+        mBackdrop = (BackDropView) mStatusBarWindow.findViewById(R.id.backdrop);
+        mBackdropFront = (ImageView) mBackdrop.findViewById(R.id.backdrop_front);
+        mBackdropBack = (ImageView) mBackdrop.findViewById(R.id.backdrop_back);
+
+        if (ENABLE_LOCKSCREEN_WALLPAPER) {
+            mLockscreenWallpaper = new LockscreenWallpaper(mContext, this, mHandler);
+        }
+
+        mKeyguardIndicationController =
+                SystemUIFactory.getInstance().createKeyguardIndicationController(mContext,
+                (ViewGroup) mStatusBarWindow.findViewById(R.id.keyguard_indication_area),
+                mNotificationPanel.getLockIcon());
+        mNotificationPanel.setKeyguardIndicationController(mKeyguardIndicationController);
+
+
+        mAmbientIndicationContainer = mStatusBarWindow.findViewById(
+                R.id.ambient_indication_container);
+
+        // set the initial view visibility
+        setAreThereNotifications();
+
+        // TODO: Find better place for this callback.
+        mBatteryController.addCallback(new BatteryStateChangeCallback() {
+            @Override
+            public void onPowerSaveChanged(boolean isPowerSave) {
+                mHandler.post(mCheckBarModes);
+                if (mDozeServiceHost != null) {
+                    mDozeServiceHost.firePowerSaveChanged(isPowerSave);
+                }
+            }
+
+            @Override
+            public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
+                // noop
+            }
+        });
+
+        mLightBarController = Dependency.get(LightBarController.class);
+        if (mNavigationBar != null) {
+            mNavigationBar.setLightBarController(mLightBarController);
+        }
+
+        ScrimView scrimBehind = (ScrimView) mStatusBarWindow.findViewById(R.id.scrim_behind);
+        ScrimView scrimInFront = (ScrimView) mStatusBarWindow.findViewById(R.id.scrim_in_front);
+        View headsUpScrim = mStatusBarWindow.findViewById(R.id.heads_up_scrim);
+        mScrimController = SystemUIFactory.getInstance().createScrimController(mLightBarController,
+                scrimBehind, scrimInFront, headsUpScrim, mLockscreenWallpaper,
+                scrimsVisible -> {
+                    if (mStatusBarWindowManager != null) {
+                        mStatusBarWindowManager.setScrimsVisible(scrimsVisible);
+                    }
+                });
+        if (mScrimSrcModeEnabled) {
+            Runnable runnable = new Runnable() {
+                @Override
+                public void run() {
+                    boolean asSrc = mBackdrop.getVisibility() != View.VISIBLE;
+                    mScrimController.setDrawBehindAsSrc(asSrc);
+                    mStackScroller.setDrawBackgroundAsSrc(asSrc);
+                }
+            };
+            mBackdrop.setOnVisibilityChangedRunnable(runnable);
+            runnable.run();
+        }
+        mHeadsUpManager.addListener(mScrimController);
+        mStackScroller.setScrimController(mScrimController);
+        mDozeScrimController = new DozeScrimController(mScrimController, context);
+
+        // Other icons
+        mVolumeComponent = getComponent(VolumeComponent.class);
+
+        mNotificationPanel.setUserSetupComplete(mUserSetup);
+        if (UserManager.get(mContext).isUserSwitcherEnabled()) {
+            createUserSwitcher();
+        }
+
+        // Set up the quick settings tile panel
+        View container = mStatusBarWindow.findViewById(R.id.qs_frame);
+        if (container != null) {
+            FragmentHostManager fragmentHostManager = FragmentHostManager.get(container);
+            ExtensionFragmentListener.attachExtensonToFragment(container, QS.TAG, R.id.qs_frame,
+                    Dependency.get(ExtensionController.class).newExtension(QS.class)
+                            .withPlugin(QS.class)
+                            .withFeature(
+                                    PackageManager.FEATURE_AUTOMOTIVE, () -> new CarQSFragment())
+                            .withDefault(() -> new QSFragment())
+                            .build());
+            final QSTileHost qsh = SystemUIFactory.getInstance().createQSTileHost(mContext, this,
+                    mIconController);
+            mBrightnessMirrorController = new BrightnessMirrorController(mStatusBarWindow,
+                    mScrimController);
+            fragmentHostManager.addTagListener(QS.TAG, (tag, f) -> {
+                QS qs = (QS) f;
+                if (qs instanceof QSFragment) {
+                    ((QSFragment) qs).setHost(qsh);
+                    mQSPanel = ((QSFragment) qs).getQsPanel();
+                    mQSPanel.setBrightnessMirror(mBrightnessMirrorController);
+                    mKeyguardStatusBar.setQSPanel(mQSPanel);
+                }
+            });
+        }
+
+        mReportRejectedTouch = mStatusBarWindow.findViewById(R.id.report_rejected_touch);
+        if (mReportRejectedTouch != null) {
+            updateReportRejectedTouchVisibility();
+            mReportRejectedTouch.setOnClickListener(v -> {
+                Uri session = mFalsingManager.reportRejectedTouch();
+                if (session == null) { return; }
+
+                StringWriter message = new StringWriter();
+                message.write("Build info: ");
+                message.write(SystemProperties.get("ro.build.description"));
+                message.write("\nSerial number: ");
+                message.write(SystemProperties.get("ro.serialno"));
+                message.write("\n");
+
+                PrintWriter falsingPw = new PrintWriter(message);
+                FalsingLog.dump(falsingPw);
+                falsingPw.flush();
+
+                startActivityDismissingKeyguard(Intent.createChooser(new Intent(Intent.ACTION_SEND)
+                                .setType("*/*")
+                                .putExtra(Intent.EXTRA_SUBJECT, "Rejected touch report")
+                                .putExtra(Intent.EXTRA_STREAM, session)
+                                .putExtra(Intent.EXTRA_TEXT, message.toString()),
+                        "Share rejected touch report")
+                                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
+                        true /* onlyProvisioned */, true /* dismissShade */);
+            });
+        }
+
+        PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+        if (!pm.isScreenOn()) {
+            mBroadcastReceiver.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF));
+        }
+        mGestureWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
+                "GestureWakeLock");
+        mVibrator = mContext.getSystemService(Vibrator.class);
+        int[] pattern = mContext.getResources().getIntArray(
+                R.array.config_cameraLaunchGestureVibePattern);
+        mCameraLaunchGestureVibePattern = new long[pattern.length];
+        for (int i = 0; i < pattern.length; i++) {
+            mCameraLaunchGestureVibePattern[i] = pattern[i];
+        }
+
+        // receive broadcasts
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+        filter.addAction(Intent.ACTION_SCREEN_OFF);
+        filter.addAction(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG);
+        context.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null, null);
+
+        IntentFilter demoFilter = new IntentFilter();
+        if (DEBUG_MEDIA_FAKE_ARTWORK) {
+            demoFilter.addAction(ACTION_FAKE_ARTWORK);
+        }
+        demoFilter.addAction(ACTION_DEMO);
+        context.registerReceiverAsUser(mDemoReceiver, UserHandle.ALL, demoFilter,
+                android.Manifest.permission.DUMP, null);
+
+        // listen for USER_SETUP_COMPLETE setting (per-user)
+        mDeviceProvisionedController.addCallback(mUserSetupObserver);
+        mUserSetupObserver.onUserSetupChanged();
+
+        // disable profiling bars, since they overlap and clutter the output on app windows
+        ThreadedRenderer.overrideProperty("disableProfileBars", "true");
+
+        // Private API call to make the shadows look better for Recents
+        ThreadedRenderer.overrideProperty("ambientRatio", String.valueOf(1.5f));
+    }
+
+    protected void createNavigationBar() {
+        mNavigationBarView = NavigationBarFragment.create(mContext, (tag, fragment) -> {
+            mNavigationBar = (NavigationBarFragment) fragment;
+            if (mLightBarController != null) {
+                mNavigationBar.setLightBarController(mLightBarController);
+            }
+            mNavigationBar.setCurrentSysuiVisibility(mSystemUiVisibility);
+        });
+    }
+
+    /**
+     * Returns the {@link android.view.View.OnTouchListener} that will be invoked when the
+     * background window of the status bar is clicked.
+     */
+    protected View.OnTouchListener getStatusBarWindowTouchListener() {
+        return (v, event) -> {
+            checkUserAutohide(v, event);
+            checkRemoteInputOutside(event);
+            if (event.getAction() == MotionEvent.ACTION_DOWN) {
+                if (mExpandedVisible) {
+                    animateCollapsePanels();
+                }
+            }
+            return mStatusBarWindow.onTouchEvent(event);
+        };
+    }
+
+    private void inflateShelf() {
+        mNotificationShelf =
+                (NotificationShelf) LayoutInflater.from(mContext).inflate(
+                        R.layout.status_bar_notification_shelf, mStackScroller, false);
+        mNotificationShelf.setOnActivatedListener(this);
+        mStackScroller.setShelf(mNotificationShelf);
+        mNotificationShelf.setOnClickListener(mGoToLockedShadeListener);
+        mNotificationShelf.setStatusBarState(mState);
+    }
+
+    public void onDensityOrFontScaleChanged() {
+        // start old BaseStatusBar.onDensityOrFontScaleChanged().
+        if (!KeyguardUpdateMonitor.getInstance(mContext).isSwitchingUser()) {
+            updateNotificationsOnDensityOrFontScaleChanged();
+        } else {
+            mReinflateNotificationsOnUserSwitched = true;
+        }
+        // end old BaseStatusBar.onDensityOrFontScaleChanged().
+        mScrimController.onDensityOrFontScaleChanged();
+        // TODO: Remove this.
+        if (mStatusBarView != null) mStatusBarView.onDensityOrFontScaleChanged();
+        if (mBrightnessMirrorController != null) {
+            mBrightnessMirrorController.onDensityOrFontScaleChanged();
+        }
+        mStatusBarKeyguardViewManager.onDensityOrFontScaleChanged();
+        // TODO: Bring these out of StatusBar.
+        ((UserInfoControllerImpl) Dependency.get(UserInfoController.class))
+                .onDensityOrFontScaleChanged();
+        Dependency.get(UserSwitcherController.class).onDensityOrFontScaleChanged();
+        if (mKeyguardUserSwitcher != null) {
+            mKeyguardUserSwitcher.onDensityOrFontScaleChanged();
+        }
+        mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext);
+
+        reevaluateStyles();
+    }
+
+    private void reinflateViews() {
+        reevaluateStyles();
+
+        // Clock and bottom icons
+        mNotificationPanel.onOverlayChanged();
+        // The status bar on the keyguard is a special layout.
+        if (mKeyguardStatusBar != null) mKeyguardStatusBar.onOverlayChanged();
+        // Recreate Indication controller because internal references changed
+        mKeyguardIndicationController =
+                SystemUIFactory.getInstance().createKeyguardIndicationController(mContext,
+                        mStatusBarWindow.findViewById(R.id.keyguard_indication_area),
+                        mNotificationPanel.getLockIcon());
+        mNotificationPanel.setKeyguardIndicationController(mKeyguardIndicationController);
+        mKeyguardIndicationController
+                .setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
+        mKeyguardIndicationController.setVisible(mState == StatusBarState.KEYGUARD);
+        mKeyguardIndicationController.setDozing(mDozing);
+        if (mBrightnessMirrorController != null) {
+            mBrightnessMirrorController.onOverlayChanged();
+        }
+        if (mStatusBarKeyguardViewManager != null) {
+            mStatusBarKeyguardViewManager.onOverlayChanged();
+        }
+        if (mAmbientIndicationContainer instanceof AutoReinflateContainer) {
+            ((AutoReinflateContainer) mAmbientIndicationContainer).inflateLayout();
+        }
+    }
+
+    protected void reevaluateStyles() {
+        inflateSignalClusters();
+        inflateDismissView();
+        updateClearAll();
+        inflateEmptyShadeView();
+        updateEmptyShadeView();
+    }
+
+    private void updateNotificationsOnDensityOrFontScaleChanged() {
+        ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+        for (int i = 0; i < activeNotifications.size(); i++) {
+            Entry entry = activeNotifications.get(i);
+            boolean exposedGuts = mNotificationGutsExposed != null
+                    && entry.row.getGuts() == mNotificationGutsExposed;
+            entry.row.onDensityOrFontScaleChanged();
+            if (exposedGuts) {
+                mNotificationGutsExposed = entry.row.getGuts();
+                bindGuts(entry.row, mGutsMenuItem);
+            }
+        }
+    }
+
+    private void inflateSignalClusters() {
+        if (mKeyguardStatusBar != null) reinflateSignalCluster(mKeyguardStatusBar);
+    }
+
+    public static SignalClusterView reinflateSignalCluster(View view) {
+        Context context = view.getContext();
+        SignalClusterView signalCluster =
+                (SignalClusterView) view.findViewById(R.id.signal_cluster);
+        if (signalCluster != null) {
+            ViewParent parent = signalCluster.getParent();
+            if (parent instanceof ViewGroup) {
+                ViewGroup viewParent = (ViewGroup) parent;
+                int index = viewParent.indexOfChild(signalCluster);
+                viewParent.removeView(signalCluster);
+                SignalClusterView newCluster = (SignalClusterView) LayoutInflater.from(context)
+                        .inflate(R.layout.signal_cluster_view, viewParent, false);
+                ViewGroup.MarginLayoutParams layoutParams =
+                        (ViewGroup.MarginLayoutParams) viewParent.getLayoutParams();
+                layoutParams.setMarginsRelative(
+                        context.getResources().getDimensionPixelSize(
+                                R.dimen.signal_cluster_margin_start),
+                        0, 0, 0);
+                newCluster.setLayoutParams(layoutParams);
+                viewParent.addView(newCluster, index);
+                return newCluster;
+            }
+            return signalCluster;
+        }
+        return null;
+    }
+
+    private void inflateEmptyShadeView() {
+        if (mStackScroller == null) {
+            return;
+        }
+        mEmptyShadeView = (EmptyShadeView) LayoutInflater.from(mContext).inflate(
+                R.layout.status_bar_no_notifications, mStackScroller, false);
+        mStackScroller.setEmptyShadeView(mEmptyShadeView);
+    }
+
+    private void inflateDismissView() {
+        if (!mClearAllEnabled || mStackScroller == null) {
+            return;
+        }
+
+        mDismissView = (DismissView) LayoutInflater.from(mContext).inflate(
+                R.layout.status_bar_notification_dismiss_all, mStackScroller, false);
+        mDismissView.setOnButtonClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES);
+                clearAllNotifications();
+            }
+        });
+        mStackScroller.setDismissView(mDismissView);
+    }
+
+    protected void createUserSwitcher() {
+        mKeyguardUserSwitcher = new KeyguardUserSwitcher(mContext,
+                (ViewStub) mStatusBarWindow.findViewById(R.id.keyguard_user_switcher),
+                mKeyguardStatusBar, mNotificationPanel);
+    }
+
+    protected void inflateStatusBarWindow(Context context) {
+        mStatusBarWindow = (StatusBarWindowView) View.inflate(context,
+                R.layout.super_status_bar, null);
+    }
+
+    public void clearAllNotifications() {
+
+        // animate-swipe all dismissable notifications, then animate the shade closed
+        int numChildren = mStackScroller.getChildCount();
+
+        final ArrayList<View> viewsToHide = new ArrayList<View>(numChildren);
+        final ArrayList<ExpandableNotificationRow> viewsToRemove = new ArrayList<>(numChildren);
+        for (int i = 0; i < numChildren; i++) {
+            final View child = mStackScroller.getChildAt(i);
+            if (child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                boolean parentVisible = false;
+                boolean hasClipBounds = child.getClipBounds(mTmpRect);
+                if (mStackScroller.canChildBeDismissed(child)) {
+                    viewsToRemove.add(row);
+                    if (child.getVisibility() == View.VISIBLE
+                            && (!hasClipBounds || mTmpRect.height() > 0)) {
+                        viewsToHide.add(child);
+                        parentVisible = true;
+                    }
+                } else if (child.getVisibility() == View.VISIBLE
+                        && (!hasClipBounds || mTmpRect.height() > 0)) {
+                    parentVisible = true;
+                }
+                List<ExpandableNotificationRow> children = row.getNotificationChildren();
+                if (children != null) {
+                    for (ExpandableNotificationRow childRow : children) {
+                        viewsToRemove.add(childRow);
+                        if (parentVisible && row.areChildrenExpanded()
+                                && mStackScroller.canChildBeDismissed(childRow)) {
+                            hasClipBounds = childRow.getClipBounds(mTmpRect);
+                            if (childRow.getVisibility() == View.VISIBLE
+                                    && (!hasClipBounds || mTmpRect.height() > 0)) {
+                                viewsToHide.add(childRow);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        if (viewsToRemove.isEmpty()) {
+            animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE);
+            return;
+        }
+
+        addPostCollapseAction(new Runnable() {
+            @Override
+            public void run() {
+                mStackScroller.setDismissAllInProgress(false);
+                for (ExpandableNotificationRow rowToRemove : viewsToRemove) {
+                    if (mStackScroller.canChildBeDismissed(rowToRemove)) {
+                        removeNotification(rowToRemove.getEntry().key, null);
+                    } else {
+                        rowToRemove.resetTranslation();
+                    }
+                }
+                try {
+                    mBarService.onClearAllNotifications(mCurrentUserId);
+                } catch (Exception ex) { }
+            }
+        });
+
+        performDismissAllAnimations(viewsToHide);
+
+    }
+
+    private void performDismissAllAnimations(ArrayList<View> hideAnimatedList) {
+        Runnable animationFinishAction = new Runnable() {
+            @Override
+            public void run() {
+                animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE);
+            }
+        };
+
+        // let's disable our normal animations
+        mStackScroller.setDismissAllInProgress(true);
+
+        // Decrease the delay for every row we animate to give the sense of
+        // accelerating the swipes
+        int rowDelayDecrement = 10;
+        int currentDelay = 140;
+        int totalDelay = 180;
+        int numItems = hideAnimatedList.size();
+        for (int i = numItems - 1; i >= 0; i--) {
+            View view = hideAnimatedList.get(i);
+            Runnable endRunnable = null;
+            if (i == 0) {
+                endRunnable = animationFinishAction;
+            }
+            mStackScroller.dismissViewAnimated(view, endRunnable, totalDelay, 260);
+            currentDelay = Math.max(50, currentDelay - rowDelayDecrement);
+            totalDelay += currentDelay;
+        }
+    }
+
+    protected void setZenMode(int mode) {
+        // start old BaseStatusBar.setZenMode().
+        if (isDeviceProvisioned()) {
+            mZenMode = mode;
+            updateNotifications();
+        }
+        // end old BaseStatusBar.setZenMode().
+    }
+
+    protected void startKeyguard() {
+        Trace.beginSection("StatusBar#startKeyguard");
+        KeyguardViewMediator keyguardViewMediator = getComponent(KeyguardViewMediator.class);
+        mFingerprintUnlockController = new FingerprintUnlockController(mContext,
+                mDozeScrimController, keyguardViewMediator,
+                mScrimController, this, UnlockMethodCache.getInstance(mContext));
+        mStatusBarKeyguardViewManager = keyguardViewMediator.registerStatusBar(this,
+                getBouncerContainer(), mScrimController,
+                mFingerprintUnlockController);
+        mKeyguardIndicationController
+                .setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
+        mFingerprintUnlockController.setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
+        mRemoteInputController.addCallback(mStatusBarKeyguardViewManager);
+
+        mRemoteInputController.addCallback(new RemoteInputController.Callback() {
+            @Override
+            public void onRemoteInputSent(Entry entry) {
+                if (FORCE_REMOTE_INPUT_HISTORY && mKeysKeptForRemoteInput.contains(entry.key)) {
+                    removeNotification(entry.key, null);
+                } else if (mRemoteInputEntriesToRemoveOnCollapse.contains(entry)) {
+                    // We're currently holding onto this notification, but from the apps point of
+                    // view it is already canceled, so we'll need to cancel it on the apps behalf
+                    // after sending - unless the app posts an update in the mean time, so wait a
+                    // bit.
+                    mHandler.postDelayed(() -> {
+                        if (mRemoteInputEntriesToRemoveOnCollapse.remove(entry)) {
+                            removeNotification(entry.key, null);
+                        }
+                    }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
+                }
+            }
+        });
+
+        mKeyguardViewMediatorCallback = keyguardViewMediator.getViewMediatorCallback();
+        mLightBarController.setFingerprintUnlockController(mFingerprintUnlockController);
+        Trace.endSection();
+    }
+
+    protected View getStatusBarView() {
+        return mStatusBarView;
+    }
+
+    public StatusBarWindowView getStatusBarWindow() {
+        return mStatusBarWindow;
+    }
+
+    protected ViewGroup getBouncerContainer() {
+        return mStatusBarWindow;
+    }
+
+    public int getStatusBarHeight() {
+        if (mNaturalBarHeight < 0) {
+            final Resources res = mContext.getResources();
+            mNaturalBarHeight =
+                    res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
+        }
+        return mNaturalBarHeight;
+    }
+
+    protected boolean toggleSplitScreenMode(int metricsDockAction, int metricsUndockAction) {
+        if (mRecents == null) {
+            return false;
+        }
+        int dockSide = WindowManagerProxy.getInstance().getDockSide();
+        if (dockSide == WindowManager.DOCKED_INVALID) {
+            return mRecents.dockTopTask(NavigationBarGestureHelper.DRAG_MODE_NONE,
+                    ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT, null, metricsDockAction);
+        } else {
+            Divider divider = getComponent(Divider.class);
+            if (divider != null && divider.isMinimized() && !divider.isHomeStackResizable()) {
+                // Undocking from the minimized state is not supported
+                return false;
+            } else {
+                EventBus.getDefault().send(new UndockingTaskEvent());
+                if (metricsUndockAction != -1) {
+                    mMetricsLogger.action(metricsUndockAction);
+                }
+            }
+        }
+        return true;
+    }
+
+    void awakenDreams() {
+        SystemServicesProxy.getInstance(mContext).awakenDreamsAsync();
+    }
+
+    public UserHandle getCurrentUserHandle() {
+        return new UserHandle(mCurrentUserId);
+    }
+
+    public void addNotification(StatusBarNotification notification, RankingMap ranking)
+            throws InflationException {
+        String key = notification.getKey();
+        if (DEBUG) Log.d(TAG, "addNotification key=" + key);
+
+        mNotificationData.updateRanking(ranking);
+        Entry shadeEntry = createNotificationViews(notification);
+        boolean isHeadsUped = shouldPeek(shadeEntry);
+        if (!isHeadsUped && notification.getNotification().fullScreenIntent != null) {
+            if (shouldSuppressFullScreenIntent(key)) {
+                if (DEBUG) {
+                    Log.d(TAG, "No Fullscreen intent: suppressed by DND: " + key);
+                }
+            } else if (mNotificationData.getImportance(key)
+                    < NotificationManager.IMPORTANCE_HIGH) {
+                if (DEBUG) {
+                    Log.d(TAG, "No Fullscreen intent: not important enough: "
+                            + key);
+                }
+            } else {
+                // Stop screensaver if the notification has a full-screen intent.
+                // (like an incoming phone call)
+                awakenDreams();
+
+                // not immersive & a full-screen alert should be shown
+                if (DEBUG)
+                    Log.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent");
+                try {
+                    EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION,
+                            key);
+                    notification.getNotification().fullScreenIntent.send();
+                    shadeEntry.notifyFullScreenIntentLaunched();
+                    mMetricsLogger.count("note_fullscreen", 1);
+                } catch (PendingIntent.CanceledException e) {
+                }
+            }
+        }
+        abortExistingInflation(key);
+
+        mForegroundServiceController.addNotification(notification,
+                mNotificationData.getImportance(key));
+
+        mPendingNotifications.put(key, shadeEntry);
+    }
+
+    private void abortExistingInflation(String key) {
+        if (mPendingNotifications.containsKey(key)) {
+            Entry entry = mPendingNotifications.get(key);
+            entry.abortTask();
+            mPendingNotifications.remove(key);
+        }
+        Entry addedEntry = mNotificationData.get(key);
+        if (addedEntry != null) {
+            addedEntry.abortTask();
+        }
+    }
+
+    private void addEntry(Entry shadeEntry) {
+        boolean isHeadsUped = shouldPeek(shadeEntry);
+        if (isHeadsUped) {
+            mHeadsUpManager.showNotification(shadeEntry);
+            // Mark as seen immediately
+            setNotificationShown(shadeEntry.notification);
+        }
+        addNotificationViews(shadeEntry);
+        // Recalculate the position of the sliding windows and the titles.
+        setAreThereNotifications();
+    }
+
+    @Override
+    public void handleInflationException(StatusBarNotification notification, Exception e) {
+        handleNotificationError(notification, e.getMessage());
+    }
+
+    @Override
+    public void onAsyncInflationFinished(Entry entry) {
+        mPendingNotifications.remove(entry.key);
+        // If there was an async task started after the removal, we don't want to add it back to
+        // the list, otherwise we might get leaks.
+        boolean isNew = mNotificationData.get(entry.key) == null;
+        if (isNew && !entry.row.isRemoved()) {
+            addEntry(entry);
+        } else if (!isNew && entry.row.hasLowPriorityStateUpdated()) {
+            mVisualStabilityManager.onLowPriorityUpdated(entry);
+            updateNotificationShade();
+        }
+        entry.row.setLowPriorityStateUpdated(false);
+    }
+
+    private boolean shouldSuppressFullScreenIntent(String key) {
+        if (isDeviceInVrMode()) {
+            return true;
+        }
+
+        if (mPowerManager.isInteractive()) {
+            return mNotificationData.shouldSuppressScreenOn(key);
+        } else {
+            return mNotificationData.shouldSuppressScreenOff(key);
+        }
+    }
+
+    protected void updateNotificationRanking(RankingMap ranking) {
+        mNotificationData.updateRanking(ranking);
+        updateNotifications();
+    }
+
+    public void removeNotification(String key, RankingMap ranking) {
+        boolean deferRemoval = false;
+        abortExistingInflation(key);
+        if (mHeadsUpManager.isHeadsUp(key)) {
+            // A cancel() in repsonse to a remote input shouldn't be delayed, as it makes the
+            // sending look longer than it takes.
+            // Also we should not defer the removal if reordering isn't allowed since otherwise
+            // some notifications can't disappear before the panel is closed.
+            boolean ignoreEarliestRemovalTime = mRemoteInputController.isSpinning(key)
+                    && !FORCE_REMOTE_INPUT_HISTORY
+                    || !mVisualStabilityManager.isReorderingAllowed();
+            deferRemoval = !mHeadsUpManager.removeNotification(key,  ignoreEarliestRemovalTime);
+        }
+        if (key.equals(mMediaNotificationKey)) {
+            clearCurrentMediaNotification();
+            updateMediaMetaData(true, true);
+        }
+        if (FORCE_REMOTE_INPUT_HISTORY && mRemoteInputController.isSpinning(key)) {
+            Entry entry = mNotificationData.get(key);
+            StatusBarNotification sbn = entry.notification;
+
+            Notification.Builder b = Notification.Builder
+                    .recoverBuilder(mContext, sbn.getNotification().clone());
+            CharSequence[] oldHistory = sbn.getNotification().extras
+                    .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
+            CharSequence[] newHistory;
+            if (oldHistory == null) {
+                newHistory = new CharSequence[1];
+            } else {
+                newHistory = new CharSequence[oldHistory.length + 1];
+                for (int i = 0; i < oldHistory.length; i++) {
+                    newHistory[i + 1] = oldHistory[i];
+                }
+            }
+            newHistory[0] = String.valueOf(entry.remoteInputText);
+            b.setRemoteInputHistory(newHistory);
+
+            Notification newNotification = b.build();
+
+            // Undo any compatibility view inflation
+            newNotification.contentView = sbn.getNotification().contentView;
+            newNotification.bigContentView = sbn.getNotification().bigContentView;
+            newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
+
+            StatusBarNotification newSbn = new StatusBarNotification(sbn.getPackageName(),
+                    sbn.getOpPkg(),
+                    sbn.getId(), sbn.getTag(), sbn.getUid(), sbn.getInitialPid(),
+                    newNotification, sbn.getUser(), sbn.getOverrideGroupKey(), sbn.getPostTime());
+            boolean updated = false;
+            try {
+                updateNotification(newSbn, null);
+                updated = true;
+            } catch (InflationException e) {
+                deferRemoval = false;
+            }
+            if (updated) {
+                mKeysKeptForRemoteInput.add(entry.key);
+                return;
+            }
+        }
+        if (deferRemoval) {
+            mLatestRankingMap = ranking;
+            mHeadsUpEntriesToRemoveOnSwitch.add(mHeadsUpManager.getEntry(key));
+            return;
+        }
+        Entry entry = mNotificationData.get(key);
+
+        if (entry != null && mRemoteInputController.isRemoteInputActive(entry)
+                && (entry.row != null && !entry.row.isDismissed())) {
+            mLatestRankingMap = ranking;
+            mRemoteInputEntriesToRemoveOnCollapse.add(entry);
+            return;
+        }
+        if (entry != null && mNotificationGutsExposed != null
+                && mNotificationGutsExposed == entry.row.getGuts() && entry.row.getGuts() != null
+                && !entry.row.getGuts().isLeavebehind()) {
+            Log.w(TAG, "Keeping notification because it's showing guts. " + key);
+            mLatestRankingMap = ranking;
+            mKeyToRemoveOnGutsClosed = key;
+            return;
+        }
+
+        if (entry != null) {
+            mForegroundServiceController.removeNotification(entry.notification);
+        }
+
+        if (entry != null && entry.row != null) {
+            entry.row.setRemoved();
+            mStackScroller.cleanUpViewState(entry.row);
+        }
+        // Let's remove the children if this was a summary
+        handleGroupSummaryRemoved(key, ranking);
+        StatusBarNotification old = removeNotificationViews(key, ranking);
+        if (SPEW) Log.d(TAG, "removeNotification key=" + key + " old=" + old);
+
+        if (old != null) {
+            if (CLOSE_PANEL_WHEN_EMPTIED && !hasActiveNotifications()
+                    && !mNotificationPanel.isTracking() && !mNotificationPanel.isQsExpanded()) {
+                if (mState == StatusBarState.SHADE) {
+                    animateCollapsePanels();
+                } else if (mState == StatusBarState.SHADE_LOCKED && !isCollapsing()) {
+                    goToKeyguard();
+                }
+            }
+        }
+        setAreThereNotifications();
+    }
+
+    /**
+     * Ensures that the group children are cancelled immediately when the group summary is cancelled
+     * instead of waiting for the notification manager to send all cancels. Otherwise this could
+     * lead to flickers.
+     *
+     * This also ensures that the animation looks nice and only consists of a single disappear
+     * animation instead of multiple.
+     *
+     * @param key the key of the notification was removed
+     * @param ranking the current ranking
+     */
+    private void handleGroupSummaryRemoved(String key,
+            RankingMap ranking) {
+        Entry entry = mNotificationData.get(key);
+        if (entry != null && entry.row != null
+                && entry.row.isSummaryWithChildren()) {
+            if (entry.notification.getOverrideGroupKey() != null && !entry.row.isDismissed()) {
+                // We don't want to remove children for autobundled notifications as they are not
+                // always cancelled. We only remove them if they were dismissed by the user.
+                return;
+            }
+            List<ExpandableNotificationRow> notificationChildren =
+                    entry.row.getNotificationChildren();
+            ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>();
+            for (int i = 0; i < notificationChildren.size(); i++) {
+                ExpandableNotificationRow row = notificationChildren.get(i);
+                if ((row.getStatusBarNotification().getNotification().flags
+                        & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
+                    // the child is a forground service notification which we can't remove!
+                    continue;
+                }
+                toRemove.add(row);
+                row.setKeepInParent(true);
+                // we need to set this state earlier as otherwise we might generate some weird
+                // animations
+                row.setRemoved();
+            }
+        }
+    }
+
+    protected void performRemoveNotification(StatusBarNotification n) {
+        Entry entry = mNotificationData.get(n.getKey());
+        if (mRemoteInputController.isRemoteInputActive(entry)) {
+            mRemoteInputController.removeRemoteInput(entry, null);
+        }
+        // start old BaseStatusBar.performRemoveNotification.
+        final String pkg = n.getPackageName();
+        final String tag = n.getTag();
+        final int id = n.getId();
+        final int userId = n.getUserId();
+        try {
+            mBarService.onNotificationClear(pkg, tag, id, userId);
+            if (FORCE_REMOTE_INPUT_HISTORY
+                    && mKeysKeptForRemoteInput.contains(n.getKey())) {
+                mKeysKeptForRemoteInput.remove(n.getKey());
+            }
+            removeNotification(n.getKey(), null);
+
+        } catch (RemoteException ex) {
+            // system process is dead if we're here.
+        }
+        if (mStackScroller.hasPulsingNotifications() && mHeadsUpManager.getAllEntries().isEmpty()) {
+            // We were showing a pulse for a notification, but no notifications are pulsing anymore.
+            // Finish the pulse.
+            mDozeScrimController.pulseOutNow();
+        }
+        // end old BaseStatusBar.performRemoveNotification.
+    }
+
+    private void updateNotificationShade() {
+        if (mStackScroller == null) return;
+
+        // Do not modify the notifications during collapse.
+        if (isCollapsing()) {
+            addPostCollapseAction(new Runnable() {
+                @Override
+                public void run() {
+                    updateNotificationShade();
+                }
+            });
+            return;
+        }
+
+        ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+        ArrayList<ExpandableNotificationRow> toShow = new ArrayList<>(activeNotifications.size());
+        final int N = activeNotifications.size();
+        for (int i=0; i<N; i++) {
+            Entry ent = activeNotifications.get(i);
+            if (ent.row.isDismissed() || ent.row.isRemoved()) {
+                // we don't want to update removed notifications because they could
+                // temporarily become children if they were isolated before.
+                continue;
+            }
+            int userId = ent.notification.getUserId();
+
+            // Display public version of the notification if we need to redact.
+            boolean devicePublic = isLockscreenPublicMode(mCurrentUserId);
+            boolean userPublic = devicePublic || isLockscreenPublicMode(userId);
+            boolean needsRedaction = needsRedaction(ent);
+            boolean sensitive = userPublic && needsRedaction;
+            boolean deviceSensitive = devicePublic
+                    && !userAllowsPrivateNotificationsInPublic(mCurrentUserId);
+            ent.row.setSensitive(sensitive, deviceSensitive);
+            ent.row.setNeedsRedaction(needsRedaction);
+            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<ExpandableNotificationRow> toRemove = new ArrayList<>();
+        for (int i=0; i< mStackScroller.getChildCount(); i++) {
+            View child = mStackScroller.getChildAt(i);
+            if (!toShow.contains(child) && child instanceof ExpandableNotificationRow) {
+                toRemove.add((ExpandableNotificationRow) child);
+            }
+        }
+
+        for (ExpandableNotificationRow remove : toRemove) {
+            if (mGroupManager.isChildInGroupWithSummary(remove.getStatusBarNotification())) {
+                // we are only transfering this notification to its parent, don't generate an animation
+                mStackScroller.setChildTransferInProgress(true);
+            }
+            if (remove.isSummaryWithChildren()) {
+                remove.removeAllChildren();
+            }
+            mStackScroller.removeView(remove);
+            mStackScroller.setChildTransferInProgress(false);
+        }
+
+        removeNotificationChildren();
+
+        for (int i=0; i<toShow.size(); i++) {
+            View v = toShow.get(i);
+            if (v.getParent() == null) {
+                mVisualStabilityManager.notifyViewAddition(v);
+                mStackScroller.addView(v);
+            }
+        }
+
+        addNotificationChildrenAndSort();
+
+        // So after all this work notifications still aren't sorted correctly.
+        // Let's do that now by advancing through toShow and mStackScroller in
+        // lock-step, making sure mStackScroller matches what we see in toShow.
+        int j = 0;
+        for (int i = 0; i < mStackScroller.getChildCount(); i++) {
+            View child = mStackScroller.getChildAt(i);
+            if (!(child instanceof ExpandableNotificationRow)) {
+                // We don't care about non-notification views.
+                continue;
+            }
+
+            ExpandableNotificationRow targetChild = toShow.get(j);
+            if (child != targetChild) {
+                // Oops, wrong notification at this position. Put the right one
+                // here and advance both lists.
+                if (mVisualStabilityManager.canReorderNotification(targetChild)) {
+                    mStackScroller.changeViewPosition(targetChild, i);
+                } else {
+                    mVisualStabilityManager.addReorderingAllowedCallback(this);
+                }
+            }
+            j++;
+
+        }
+
+        mVisualStabilityManager.onReorderingFinished();
+        // clear the map again for the next usage
+        mTmpChildOrderMap.clear();
+
+        updateRowStates();
+        updateSpeedBumpIndex();
+        updateClearAll();
+        updateEmptyShadeView();
+
+        updateQsExpansionEnabled();
+
+        // Let's also update the icons
+        mNotificationIconAreaController.updateNotificationIcons(mNotificationData);
+    }
+
+    /** @return true if the entry needs redaction when on the lockscreen. */
+    private boolean needsRedaction(Entry ent) {
+        int userId = ent.notification.getUserId();
+
+        boolean currentUserWantsRedaction = !userAllowsPrivateNotificationsInPublic(mCurrentUserId);
+        boolean notiUserWantsRedaction = !userAllowsPrivateNotificationsInPublic(userId);
+        boolean redactedLockscreen = currentUserWantsRedaction || notiUserWantsRedaction;
+
+        boolean notificationRequestsRedaction =
+                ent.notification.getNotification().visibility == Notification.VISIBILITY_PRIVATE;
+        boolean userForcesRedaction = packageHasVisibilityOverride(ent.notification.getKey());
+
+        return userForcesRedaction || notificationRequestsRedaction && redactedLockscreen;
+    }
+
+    /**
+     * Disable QS if device not provisioned.
+     * If the user switcher is simple then disable QS during setup because
+     * the user intends to use the lock screen user switcher, QS in not needed.
+     */
+    private void updateQsExpansionEnabled() {
+        mNotificationPanel.setQsExpansionEnabled(isDeviceProvisioned()
+                && (mUserSetup || mUserSwitcherController == null
+                        || !mUserSwitcherController.isSimpleUserSwitcher())
+                && ((mDisabled2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) == 0)
+                && !mDozing
+                && !ONLY_CORE_APPS);
+    }
+
+    private void addNotificationChildrenAndSort() {
+        // Let's now add all notification children which are missing
+        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);
+
+            for (int childIndex = 0; orderedChildren != null && childIndex < orderedChildren.size();
+                    childIndex++) {
+                ExpandableNotificationRow childView = orderedChildren.get(childIndex);
+                if (children == null || !children.contains(childView)) {
+                    if (childView.getParent() != null) {
+                        Log.wtf(TAG, "trying to add a notification child that already has " +
+                                "a parent. class:" + childView.getParent().getClass() +
+                                "\n child: " + childView);
+                        // This shouldn't happen. We can recover by removing it though.
+                        ((ViewGroup) childView.getParent()).removeView(childView);
+                    }
+                    mVisualStabilityManager.notifyViewAddition(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, mVisualStabilityManager, this);
+        }
+        if (orderChanged) {
+            mStackScroller.generateChildOrderChangedEvent();
+        }
+    }
+
+    private void removeNotificationChildren() {
+        // First let's remove all children which don't belong in the parents
+        ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>();
+        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);
+
+            if (children != null) {
+                toRemove.clear();
+                for (ExpandableNotificationRow childRow : children) {
+                    if ((orderedChildren == null
+                            || !orderedChildren.contains(childRow))
+                            && !childRow.keepInParent()) {
+                        toRemove.add(childRow);
+                    }
+                }
+                for (ExpandableNotificationRow remove : toRemove) {
+                    parent.removeChildNotification(remove);
+                    if (mNotificationData.get(remove.getStatusBarNotification().getKey()) == null) {
+                        // We only want to add an animation if the view is completely removed
+                        // otherwise it's just a transfer
+                        mStackScroller.notifyGroupChildRemoved(remove,
+                                parent.getChildrenContainer());
+                    }
+                }
+            }
+        }
+    }
+
+    public void addQsTile(ComponentName tile) {
+        mQSPanel.getHost().addTile(tile);
+    }
+
+    public void remQsTile(ComponentName tile) {
+        mQSPanel.getHost().removeTile(tile);
+    }
+
+    public void clickTile(ComponentName tile) {
+        mQSPanel.clickTile(tile);
+    }
+
+    private boolean packageHasVisibilityOverride(String key) {
+        return mNotificationData.getVisibilityOverride(key) == Notification.VISIBILITY_PRIVATE;
+    }
+
+    private void updateClearAll() {
+        if (!mClearAllEnabled) {
+            return;
+        }
+        boolean showDismissView = mState != StatusBarState.KEYGUARD
+                && hasActiveClearableNotifications();
+        mStackScroller.updateDismissView(showDismissView);
+    }
+
+    /**
+     * Return whether there are any clearable notifications
+     */
+    private boolean hasActiveClearableNotifications() {
+        int childCount = mStackScroller.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = mStackScroller.getChildAt(i);
+            if (!(child instanceof ExpandableNotificationRow)) {
+                continue;
+            }
+            if (((ExpandableNotificationRow) child).canViewBeDismissed()) {
+                    return true;
+            }
+        }
+        return false;
+    }
+
+    private void updateEmptyShadeView() {
+        boolean showEmptyShadeView =
+                mState != StatusBarState.KEYGUARD &&
+                        mNotificationData.getActiveNotifications().size() == 0;
+        mNotificationPanel.showEmptyShadeView(showEmptyShadeView);
+    }
+
+    private void updateSpeedBumpIndex() {
+        int speedBumpIndex = 0;
+        int currentIndex = 0;
+        final int N = mStackScroller.getChildCount();
+        for (int i = 0; i < N; i++) {
+            View view = mStackScroller.getChildAt(i);
+            if (view.getVisibility() == View.GONE || !(view instanceof ExpandableNotificationRow)) {
+                continue;
+            }
+            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+            currentIndex++;
+            if (!mNotificationData.isAmbient(row.getStatusBarNotification().getKey())) {
+                speedBumpIndex = currentIndex;
+            }
+        }
+        boolean noAmbient = speedBumpIndex == N;
+        mStackScroller.updateSpeedBumpIndex(speedBumpIndex, noAmbient);
+    }
+
+    public static boolean isTopLevelChild(Entry entry) {
+        return entry.row.getParent() instanceof NotificationStackScrollLayout;
+    }
+
+    protected void updateNotifications() {
+        mNotificationData.filterAndSort();
+
+        updateNotificationShade();
+    }
+
+    public void requestNotificationUpdate() {
+        updateNotifications();
+    }
+
+    protected void setAreThereNotifications() {
+
+        if (SPEW) {
+            final boolean clearable = hasActiveNotifications() &&
+                    hasActiveClearableNotifications();
+            Log.d(TAG, "setAreThereNotifications: N=" +
+                    mNotificationData.getActiveNotifications().size() + " any=" +
+                    hasActiveNotifications() + " clearable=" + clearable);
+        }
+
+        if (mStatusBarView != null) {
+            final View nlo = mStatusBarView.findViewById(R.id.notification_lights_out);
+            final boolean showDot = hasActiveNotifications() && !areLightsOn();
+            if (showDot != (nlo.getAlpha() == 1.0f)) {
+                if (showDot) {
+                    nlo.setAlpha(0f);
+                    nlo.setVisibility(View.VISIBLE);
+                }
+                nlo.animate()
+                        .alpha(showDot ? 1 : 0)
+                        .setDuration(showDot ? 750 : 250)
+                        .setInterpolator(new AccelerateInterpolator(2.0f))
+                        .setListener(showDot ? null : new AnimatorListenerAdapter() {
+                            @Override
+                            public void onAnimationEnd(Animator _a) {
+                                nlo.setVisibility(View.GONE);
+                            }
+                        })
+                        .start();
+            }
+        }
+
+        findAndUpdateMediaNotifications();
+    }
+
+    public void findAndUpdateMediaNotifications() {
+        boolean metaDataChanged = false;
+
+        synchronized (mNotificationData) {
+            ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+            final int N = activeNotifications.size();
+
+            // Promote the media notification with a controller in 'playing' state, if any.
+            Entry mediaNotification = null;
+            MediaController controller = null;
+            for (int i = 0; i < N; i++) {
+                final Entry entry = activeNotifications.get(i);
+                if (isMediaNotification(entry)) {
+                    final MediaSession.Token token =
+                            entry.notification.getNotification().extras
+                            .getParcelable(Notification.EXTRA_MEDIA_SESSION);
+                    if (token != null) {
+                        MediaController aController = new MediaController(mContext, token);
+                        if (PlaybackState.STATE_PLAYING ==
+                                getMediaControllerPlaybackState(aController)) {
+                            if (DEBUG_MEDIA) {
+                                Log.v(TAG, "DEBUG_MEDIA: found mediastyle controller matching "
+                                        + entry.notification.getKey());
+                            }
+                            mediaNotification = entry;
+                            controller = aController;
+                            break;
+                        }
+                    }
+                }
+            }
+            if (mediaNotification == null) {
+                // Still nothing? OK, let's just look for live media sessions and see if they match
+                // one of our notifications. This will catch apps that aren't (yet!) using media
+                // notifications.
+
+                if (mMediaSessionManager != null) {
+                    final List<MediaController> sessions
+                            = mMediaSessionManager.getActiveSessionsForUser(
+                                    null,
+                                    UserHandle.USER_ALL);
+
+                    for (MediaController aController : sessions) {
+                        if (PlaybackState.STATE_PLAYING ==
+                                getMediaControllerPlaybackState(aController)) {
+                            // now to see if we have one like this
+                            final String pkg = aController.getPackageName();
+
+                            for (int i = 0; i < N; i++) {
+                                final Entry entry = activeNotifications.get(i);
+                                if (entry.notification.getPackageName().equals(pkg)) {
+                                    if (DEBUG_MEDIA) {
+                                        Log.v(TAG, "DEBUG_MEDIA: found controller matching "
+                                            + entry.notification.getKey());
+                                    }
+                                    controller = aController;
+                                    mediaNotification = entry;
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (controller != null && !sameSessions(mMediaController, controller)) {
+                // We have a new media session
+                clearCurrentMediaNotification();
+                mMediaController = controller;
+                mMediaController.registerCallback(mMediaListener);
+                mMediaMetadata = mMediaController.getMetadata();
+                if (DEBUG_MEDIA) {
+                    Log.v(TAG, "DEBUG_MEDIA: insert listener, receive metadata: "
+                            + mMediaMetadata);
+                }
+
+                if (mediaNotification != null) {
+                    mMediaNotificationKey = mediaNotification.notification.getKey();
+                    if (DEBUG_MEDIA) {
+                        Log.v(TAG, "DEBUG_MEDIA: Found new media notification: key="
+                                + mMediaNotificationKey + " controller=" + mMediaController);
+                    }
+                }
+                metaDataChanged = true;
+            }
+        }
+
+        if (metaDataChanged) {
+            updateNotifications();
+        }
+        updateMediaMetaData(metaDataChanged, true);
+    }
+
+    private int getMediaControllerPlaybackState(MediaController controller) {
+        if (controller != null) {
+            final PlaybackState playbackState = controller.getPlaybackState();
+            if (playbackState != null) {
+                return playbackState.getState();
+            }
+        }
+        return PlaybackState.STATE_NONE;
+    }
+
+    private boolean isPlaybackActive(int state) {
+        if (state != PlaybackState.STATE_STOPPED
+                && state != PlaybackState.STATE_ERROR
+                && state != PlaybackState.STATE_NONE) {
+            return true;
+        }
+        return false;
+    }
+
+    private void clearCurrentMediaNotification() {
+        mMediaNotificationKey = null;
+        mMediaMetadata = null;
+        if (mMediaController != null) {
+            if (DEBUG_MEDIA) {
+                Log.v(TAG, "DEBUG_MEDIA: Disconnecting from old controller: "
+                        + mMediaController.getPackageName());
+            }
+            mMediaController.unregisterCallback(mMediaListener);
+        }
+        mMediaController = null;
+    }
+
+    private boolean sameSessions(MediaController a, MediaController b) {
+        if (a == b) return true;
+        if (a == null) return false;
+        return a.controlsSameSession(b);
+    }
+
+    /**
+     * Hide the album artwork that is fading out and release its bitmap.
+     */
+    protected Runnable mHideBackdropFront = new Runnable() {
+        @Override
+        public void run() {
+            if (DEBUG_MEDIA) {
+                Log.v(TAG, "DEBUG_MEDIA: removing fade layer");
+            }
+            mBackdropFront.setVisibility(View.INVISIBLE);
+            mBackdropFront.animate().cancel();
+            mBackdropFront.setImageDrawable(null);
+        }
+    };
+
+    /**
+     * Refresh or remove lockscreen artwork from media metadata or the lockscreen wallpaper.
+     */
+    public void updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation) {
+        Trace.beginSection("StatusBar#updateMediaMetaData");
+        if (!SHOW_LOCKSCREEN_MEDIA_ARTWORK) {
+            Trace.endSection();
+            return;
+        }
+
+        if (mBackdrop == null) {
+            Trace.endSection();
+            return; // called too early
+        }
+
+        if (mLaunchTransitionFadingAway) {
+            mBackdrop.setVisibility(View.INVISIBLE);
+            Trace.endSection();
+            return;
+        }
+
+        if (DEBUG_MEDIA) {
+            Log.v(TAG, "DEBUG_MEDIA: updating album art for notification " + mMediaNotificationKey
+                    + " metadata=" + mMediaMetadata
+                    + " metaDataChanged=" + metaDataChanged
+                    + " state=" + mState);
+        }
+
+        Drawable artworkDrawable = null;
+        if (mMediaMetadata != null) {
+            Bitmap artworkBitmap = null;
+            artworkBitmap = mMediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART);
+            if (artworkBitmap == null) {
+                artworkBitmap = mMediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
+                // might still be null
+            }
+            if (artworkBitmap != null) {
+                artworkDrawable = new BitmapDrawable(mBackdropBack.getResources(), artworkBitmap);
+            }
+        }
+        boolean allowWhenShade = false;
+        if (ENABLE_LOCKSCREEN_WALLPAPER && artworkDrawable == null) {
+            Bitmap lockWallpaper = mLockscreenWallpaper.getBitmap();
+            if (lockWallpaper != null) {
+                artworkDrawable = new LockscreenWallpaper.WallpaperDrawable(
+                        mBackdropBack.getResources(), lockWallpaper);
+                // We're in the SHADE mode on the SIM screen - yet we still need to show
+                // the lockscreen wallpaper in that mode.
+                allowWhenShade = mStatusBarKeyguardViewManager != null
+                        && mStatusBarKeyguardViewManager.isShowing();
+            }
+        }
+
+        boolean hideBecauseOccluded = mStatusBarKeyguardViewManager != null
+                && mStatusBarKeyguardViewManager.isOccluded();
+
+        final boolean hasArtwork = artworkDrawable != null;
+
+        if ((hasArtwork || DEBUG_MEDIA_FAKE_ARTWORK)
+                && (mState != StatusBarState.SHADE || allowWhenShade)
+                && mFingerprintUnlockController.getMode()
+                        != FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING
+                && !hideBecauseOccluded) {
+            // time to show some art!
+            if (mBackdrop.getVisibility() != View.VISIBLE) {
+                mBackdrop.setVisibility(View.VISIBLE);
+                if (allowEnterAnimation) {
+                    mBackdrop.setAlpha(SRC_MIN_ALPHA);
+                    mBackdrop.animate().alpha(1f);
+                } else {
+                    mBackdrop.animate().cancel();
+                    mBackdrop.setAlpha(1f);
+                }
+                mStatusBarWindowManager.setBackdropShowing(true);
+                metaDataChanged = true;
+                if (DEBUG_MEDIA) {
+                    Log.v(TAG, "DEBUG_MEDIA: Fading in album artwork");
+                }
+            }
+            if (metaDataChanged) {
+                if (mBackdropBack.getDrawable() != null) {
+                    Drawable drawable =
+                            mBackdropBack.getDrawable().getConstantState()
+                                    .newDrawable(mBackdropFront.getResources()).mutate();
+                    mBackdropFront.setImageDrawable(drawable);
+                    if (mScrimSrcModeEnabled) {
+                        mBackdropFront.getDrawable().mutate().setXfermode(mSrcOverXferMode);
+                    }
+                    mBackdropFront.setAlpha(1f);
+                    mBackdropFront.setVisibility(View.VISIBLE);
+                } else {
+                    mBackdropFront.setVisibility(View.INVISIBLE);
+                }
+
+                if (DEBUG_MEDIA_FAKE_ARTWORK) {
+                    final int c = 0xFF000000 | (int)(Math.random() * 0xFFFFFF);
+                    Log.v(TAG, String.format("DEBUG_MEDIA: setting new color: 0x%08x", c));
+                    mBackdropBack.setBackgroundColor(0xFFFFFFFF);
+                    mBackdropBack.setImageDrawable(new ColorDrawable(c));
+                } else {
+                    mBackdropBack.setImageDrawable(artworkDrawable);
+                }
+                if (mScrimSrcModeEnabled) {
+                    mBackdropBack.getDrawable().mutate().setXfermode(mSrcXferMode);
+                }
+
+                if (mBackdropFront.getVisibility() == View.VISIBLE) {
+                    if (DEBUG_MEDIA) {
+                        Log.v(TAG, "DEBUG_MEDIA: Crossfading album artwork from "
+                                + mBackdropFront.getDrawable()
+                                + " to "
+                                + mBackdropBack.getDrawable());
+                    }
+                    mBackdropFront.animate()
+                            .setDuration(250)
+                            .alpha(0f).withEndAction(mHideBackdropFront);
+                }
+            }
+        } else {
+            // need to hide the album art, either because we are unlocked or because
+            // the metadata isn't there to support it
+            if (mBackdrop.getVisibility() != View.GONE) {
+                if (DEBUG_MEDIA) {
+                    Log.v(TAG, "DEBUG_MEDIA: Fading out album artwork");
+                }
+                if (mFingerprintUnlockController.getMode()
+                        == FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING
+                        || hideBecauseOccluded) {
+
+                    // We are unlocking directly - no animation!
+                    mBackdrop.setVisibility(View.GONE);
+                    mBackdropBack.setImageDrawable(null);
+                    mStatusBarWindowManager.setBackdropShowing(false);
+                } else {
+                    mStatusBarWindowManager.setBackdropShowing(false);
+                    mBackdrop.animate()
+                            .alpha(SRC_MIN_ALPHA)
+                            .setInterpolator(Interpolators.ACCELERATE_DECELERATE)
+                            .setDuration(300)
+                            .setStartDelay(0)
+                            .withEndAction(new Runnable() {
+                                @Override
+                                public void run() {
+                                    mBackdrop.setVisibility(View.GONE);
+                                    mBackdropFront.animate().cancel();
+                                    mBackdropBack.setImageDrawable(null);
+                                    mHandler.post(mHideBackdropFront);
+                                }
+                            });
+                    if (mKeyguardFadingAway) {
+                        mBackdrop.animate()
+                                // Make it disappear faster, as the focus should be on the activity
+                                // behind.
+                                .setDuration(mKeyguardFadingAwayDuration / 2)
+                                .setStartDelay(mKeyguardFadingAwayDelay)
+                                .setInterpolator(Interpolators.LINEAR)
+                                .start();
+                    }
+                }
+            }
+        }
+        Trace.endSection();
+    }
+
+    private void updateReportRejectedTouchVisibility() {
+        if (mReportRejectedTouch == null) {
+            return;
+        }
+        mReportRejectedTouch.setVisibility(mState == StatusBarState.KEYGUARD
+                && mFalsingManager.isReportingEnabled() ? View.VISIBLE : View.INVISIBLE);
+    }
+
+    /**
+     * State is one or more of the DISABLE constants from StatusBarManager.
+     */
+    @Override
+    public void disable(int state1, int state2, boolean animate) {
+        animate &= mStatusBarWindowState != WINDOW_STATE_HIDDEN;
+        mDisabledUnmodified1 = state1;
+        mDisabledUnmodified2 = state2;
+        final int old1 = mDisabled1;
+        final int diff1 = state1 ^ old1;
+        mDisabled1 = state1;
+
+        final int old2 = mDisabled2;
+        final int diff2 = state2 ^ old2;
+        mDisabled2 = state2;
+
+        if (DEBUG) {
+            Log.d(TAG, String.format("disable1: 0x%08x -> 0x%08x (diff1: 0x%08x)",
+                old1, state1, diff1));
+            Log.d(TAG, String.format("disable2: 0x%08x -> 0x%08x (diff2: 0x%08x)",
+                old2, state2, diff2));
+        }
+
+        StringBuilder flagdbg = new StringBuilder();
+        flagdbg.append("disable<");
+        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_EXPAND))                ? 'E' : 'e');
+        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_EXPAND))                ? '!' : ' ');
+        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_NOTIFICATION_ICONS))    ? 'I' : 'i');
+        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_NOTIFICATION_ICONS))    ? '!' : ' ');
+        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS))   ? 'A' : 'a');
+        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_NOTIFICATION_ALERTS))   ? '!' : ' ');
+        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_SYSTEM_INFO))           ? 'S' : 's');
+        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_SYSTEM_INFO))           ? '!' : ' ');
+        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_BACK))                  ? 'B' : 'b');
+        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_BACK))                  ? '!' : ' ');
+        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_HOME))                  ? 'H' : 'h');
+        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_HOME))                  ? '!' : ' ');
+        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_RECENT))                ? 'R' : 'r');
+        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_RECENT))                ? '!' : ' ');
+        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_CLOCK))                 ? 'C' : 'c');
+        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_CLOCK))                 ? '!' : ' ');
+        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_SEARCH))                ? 'S' : 's');
+        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_SEARCH))                ? '!' : ' ');
+        flagdbg.append(0 != ((state2 & StatusBarManager.DISABLE2_QUICK_SETTINGS))       ? 'Q' : 'q');
+        flagdbg.append(0 != ((diff2  & StatusBarManager.DISABLE2_QUICK_SETTINGS))       ? '!' : ' ');
+        flagdbg.append('>');
+        Log.d(TAG, flagdbg.toString());
+
+        if ((diff1 & StatusBarManager.DISABLE_EXPAND) != 0) {
+            if ((state1 & StatusBarManager.DISABLE_EXPAND) != 0) {
+                animateCollapsePanels();
+            }
+        }
+
+        if ((diff1 & StatusBarManager.DISABLE_RECENT) != 0) {
+            if ((state1 & StatusBarManager.DISABLE_RECENT) != 0) {
+                // close recents if it's visible
+                mHandler.removeMessages(MSG_HIDE_RECENT_APPS);
+                mHandler.sendEmptyMessage(MSG_HIDE_RECENT_APPS);
+            }
+        }
+
+        if ((diff1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) {
+            mDisableNotificationAlerts =
+                    (state1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0;
+            mHeadsUpObserver.onChange(true);
+        }
+
+        if ((diff2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) != 0) {
+            updateQsExpansionEnabled();
+        }
+    }
+
+    /**
+     * Reapplies the disable flags as last requested by StatusBarManager.
+     *
+     * This needs to be called if state used by {@link #adjustDisableFlags} changes.
+     */
+    public void recomputeDisableFlags(boolean animate) {
+        mCommandQueue.recomputeDisableFlags(animate);
+    }
+
+    protected H createHandler() {
+        return new StatusBar.H();
+    }
+
+    @Override
+    public void startActivity(Intent intent, boolean dismissShade) {
+        startActivityDismissingKeyguard(intent, false, dismissShade);
+    }
+
+    @Override
+    public void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade) {
+        startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade);
+    }
+
+    @Override
+    public void startActivity(Intent intent, boolean dismissShade, Callback callback) {
+        startActivityDismissingKeyguard(intent, false, dismissShade,
+                false /* disallowEnterPictureInPictureWhileLaunching */, callback);
+    }
+
+    public void setQsExpanded(boolean expanded) {
+        mStatusBarWindowManager.setQsExpanded(expanded);
+        mNotificationPanel.setStatusAccessibilityImportance(expanded
+                ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+    }
+
+    public boolean isGoingToNotificationShade() {
+        return mLeaveOpenOnKeyguardHide;
+    }
+
+    public boolean isWakeUpComingFromTouch() {
+        return mWakeUpComingFromTouch;
+    }
+
+    public boolean isFalsingThresholdNeeded() {
+        return getBarState() == StatusBarState.KEYGUARD;
+    }
+
+    public boolean isDozing() {
+        return mDozing;
+    }
+
+    @Override  // NotificationData.Environment
+    public String getCurrentMediaNotificationKey() {
+        return mMediaNotificationKey;
+    }
+
+    public boolean isScrimSrcModeEnabled() {
+        return mScrimSrcModeEnabled;
+    }
+
+    /**
+     * To be called when there's a state change in StatusBarKeyguardViewManager.
+     */
+    public void onKeyguardViewManagerStatesUpdated() {
+        logStateToEventlog();
+    }
+
+    @Override  // UnlockMethodCache.OnUnlockMethodChangedListener
+    public void onUnlockMethodStateChanged() {
+        logStateToEventlog();
+    }
+
+    @Override
+    public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {
+        if (inPinnedMode) {
+            mStatusBarWindowManager.setHeadsUpShowing(true);
+            mStatusBarWindowManager.setForceStatusBarVisible(true);
+            if (mNotificationPanel.isFullyCollapsed()) {
+                // We need to ensure that the touchable region is updated before the window will be
+                // resized, in order to not catch any touches. A layout will ensure that
+                // onComputeInternalInsets will be called and after that we can resize the layout. Let's
+                // make sure that the window stays small for one frame until the touchableRegion is set.
+                mNotificationPanel.requestLayout();
+                mStatusBarWindowManager.setForceWindowCollapsed(true);
+                mNotificationPanel.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mStatusBarWindowManager.setForceWindowCollapsed(false);
+                    }
+                });
+            }
+        } else {
+            if (!mNotificationPanel.isFullyCollapsed() || mNotificationPanel.isTracking()) {
+                // We are currently tracking or is open and the shade doesn't need to be kept
+                // open artificially.
+                mStatusBarWindowManager.setHeadsUpShowing(false);
+            } else {
+                // we need to keep the panel open artificially, let's wait until the animation
+                // is finished.
+                mHeadsUpManager.setHeadsUpGoingAway(true);
+                mStackScroller.runAfterAnimationFinished(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (!mHeadsUpManager.hasPinnedHeadsUp()) {
+                            mStatusBarWindowManager.setHeadsUpShowing(false);
+                            mHeadsUpManager.setHeadsUpGoingAway(false);
+                        }
+                        removeRemoteInputEntriesKeptUntilCollapsed();
+                    }
+                });
+            }
+        }
+    }
+
+    @Override
+    public void onHeadsUpPinned(ExpandableNotificationRow headsUp) {
+        dismissVolumeDialog();
+    }
+
+    @Override
+    public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {
+    }
+
+    @Override
+    public void onHeadsUpStateChanged(Entry entry, boolean isHeadsUp) {
+        if (!isHeadsUp && mHeadsUpEntriesToRemoveOnSwitch.contains(entry)) {
+            removeNotification(entry.key, mLatestRankingMap);
+            mHeadsUpEntriesToRemoveOnSwitch.remove(entry);
+            if (mHeadsUpEntriesToRemoveOnSwitch.isEmpty()) {
+                mLatestRankingMap = null;
+            }
+        } else {
+            updateNotificationRanking(null);
+            if (isHeadsUp) {
+                mDozeServiceHost.fireNotificationHeadsUp();
+            }
+        }
+
+    }
+
+    protected void updateHeadsUp(String key, Entry entry, boolean shouldPeek,
+            boolean alertAgain) {
+        final boolean wasHeadsUp = isHeadsUp(key);
+        if (wasHeadsUp) {
+            if (!shouldPeek) {
+                // We don't want this to be interrupting anymore, lets remove it
+                mHeadsUpManager.removeNotification(key, false /* ignoreEarliestRemovalTime */);
+            } else {
+                mHeadsUpManager.updateNotification(entry, alertAgain);
+            }
+        } else if (shouldPeek && alertAgain) {
+            // This notification was updated to be a heads-up, show it!
+            mHeadsUpManager.showNotification(entry);
+        }
+    }
+
+    protected void setHeadsUpUser(int newUserId) {
+        if (mHeadsUpManager != null) {
+            mHeadsUpManager.setUser(newUserId);
+        }
+    }
+
+    public boolean isHeadsUp(String key) {
+        return mHeadsUpManager.isHeadsUp(key);
+    }
+
+    protected boolean isSnoozedPackage(StatusBarNotification sbn) {
+        return mHeadsUpManager.isSnoozed(sbn.getPackageName());
+    }
+
+    public boolean isKeyguardCurrentlySecure() {
+        return !mUnlockMethodCache.canSkipBouncer();
+    }
+
+    public void setPanelExpanded(boolean isExpanded) {
+        mPanelExpanded = isExpanded;
+        updateHideIconsForBouncer(false /* animate */);
+        mStatusBarWindowManager.setPanelExpanded(isExpanded);
+        mVisualStabilityManager.setPanelExpanded(isExpanded);
+        if (isExpanded && getBarState() != StatusBarState.KEYGUARD) {
+            if (DEBUG) {
+                Log.v(TAG, "clearing notification effects from setPanelExpanded");
+            }
+            clearNotificationEffects();
+        }
+
+        if (!isExpanded) {
+            removeRemoteInputEntriesKeptUntilCollapsed();
+        }
+    }
+
+    private void removeRemoteInputEntriesKeptUntilCollapsed() {
+        for (int i = 0; i < mRemoteInputEntriesToRemoveOnCollapse.size(); i++) {
+            Entry entry = mRemoteInputEntriesToRemoveOnCollapse.valueAt(i);
+            mRemoteInputController.removeRemoteInput(entry, null);
+            removeNotification(entry.key, mLatestRankingMap);
+        }
+        mRemoteInputEntriesToRemoveOnCollapse.clear();
+    }
+
+    public NotificationStackScrollLayout getNotificationScrollLayout() {
+        return mStackScroller;
+    }
+
+    public boolean isPulsing() {
+        return mDozeScrimController.isPulsing();
+    }
+
+    @Override
+    public void onReorderingAllowed() {
+        updateNotifications();
+    }
+
+    public boolean isLaunchTransitionFadingAway() {
+        return mLaunchTransitionFadingAway;
+    }
+
+    public boolean hideStatusBarIconsWhenExpanded() {
+        return mNotificationPanel.hideStatusBarIconsWhenExpanded();
+    }
+
+    @Override
+    public void onColorsChanged(ColorExtractor extractor, int which) {
+        updateTheme();
+    }
+
+    public boolean isUsingDarkTheme() {
+        OverlayInfo themeInfo = null;
+        try {
+            themeInfo = mOverlayManager.getOverlayInfo("com.android.systemui.theme.dark",
+                    mCurrentUserId);
+        } catch (RemoteException e) {
+            e.printStackTrace();
+        }
+        return themeInfo != null && themeInfo.isEnabled();
+    }
+
+    @Nullable
+    public View getAmbientIndicationContainer() {
+        return mAmbientIndicationContainer;
+    }
+
+    public void setOccluded(boolean occluded) {
+        mIsOccluded = occluded;
+        updateHideIconsForBouncer(false /* animate */);
+    }
+
+    public boolean hideStatusBarIconsForBouncer() {
+        return mHideIconsForBouncer || mWereIconsJustHidden;
+    }
+
+    /**
+     * @param animate should the change of the icons be animated.
+     */
+    private void updateHideIconsForBouncer(boolean animate) {
+        boolean shouldHideIconsForBouncer = !mPanelExpanded && mTopHidesStatusBar && mIsOccluded
+                && (mBouncerShowing || mStatusBarWindowHidden);
+        if (mHideIconsForBouncer != shouldHideIconsForBouncer) {
+            mHideIconsForBouncer = shouldHideIconsForBouncer;
+            if (!shouldHideIconsForBouncer && mBouncerWasShowingWhenHidden) {
+                // We're delaying the showing, since most of the time the fullscreen app will
+                // hide the icons again and we don't want them to fade in and out immediately again.
+                mWereIconsJustHidden = true;
+                mHandler.postDelayed(() -> {
+                    mWereIconsJustHidden = false;
+                    recomputeDisableFlags(true);
+                }, 500);
+            } else {
+                recomputeDisableFlags(animate);
+            }
+        }
+        if (shouldHideIconsForBouncer) {
+            mBouncerWasShowingWhenHidden = mBouncerShowing;
+        }
+    }
+
+    /**
+     * All changes to the status bar and notifications funnel through here and are batched.
+     */
+    protected class H extends Handler {
+        @Override
+        public void handleMessage(Message m) {
+            switch (m.what) {
+                case MSG_TOGGLE_KEYBOARD_SHORTCUTS_MENU:
+                    toggleKeyboardShortcuts(m.arg1);
+                    break;
+                case MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU:
+                    dismissKeyboardShortcuts();
+                    break;
+                // End old BaseStatusBar.H handling.
+                case MSG_OPEN_NOTIFICATION_PANEL:
+                    animateExpandNotificationsPanel();
+                    break;
+                case MSG_OPEN_SETTINGS_PANEL:
+                    animateExpandSettingsPanel((String) m.obj);
+                    break;
+                case MSG_CLOSE_PANELS:
+                    animateCollapsePanels();
+                    break;
+                case MSG_LAUNCH_TRANSITION_TIMEOUT:
+                    onLaunchTransitionTimeout();
+                    break;
+            }
+        }
+    }
+
+    public void maybeEscalateHeadsUp() {
+        Collection<HeadsUpManager.HeadsUpEntry> entries = mHeadsUpManager.getAllEntries();
+        for (HeadsUpManager.HeadsUpEntry entry : entries) {
+            final StatusBarNotification sbn = entry.entry.notification;
+            final Notification notification = sbn.getNotification();
+            if (notification.fullScreenIntent != null) {
+                if (DEBUG) {
+                    Log.d(TAG, "converting a heads up to fullScreen");
+                }
+                try {
+                    EventLog.writeEvent(EventLogTags.SYSUI_HEADS_UP_ESCALATION,
+                            sbn.getKey());
+                    notification.fullScreenIntent.send();
+                    entry.entry.notifyFullScreenIntentLaunched();
+                } catch (PendingIntent.CanceledException e) {
+                }
+            }
+        }
+        mHeadsUpManager.releaseAllImmediately();
+    }
+
+    /**
+     * Called for system navigation gestures. First action opens the panel, second opens
+     * settings. Down action closes the entire panel.
+     */
+    @Override
+    public void handleSystemKey(int key) {
+        if (SPEW) Log.d(TAG, "handleNavigationKey: " + key);
+        if (!panelsEnabled() || !mKeyguardMonitor.isDeviceInteractive()
+                || mKeyguardMonitor.isShowing() && !mKeyguardMonitor.isOccluded()) {
+            return;
+        }
+
+        // Panels are not available in setup
+        if (!mUserSetup) return;
+
+        if (KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP == key) {
+            mMetricsLogger.action(MetricsEvent.ACTION_SYSTEM_NAVIGATION_KEY_UP);
+            mNotificationPanel.collapse(false /* delayed */, 1.0f /* speedUpFactor */);
+        } else if (KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN == key) {
+            mMetricsLogger.action(MetricsEvent.ACTION_SYSTEM_NAVIGATION_KEY_DOWN);
+            if (mNotificationPanel.isFullyCollapsed()) {
+                mNotificationPanel.expand(true /* animate */);
+                mMetricsLogger.count(NotificationPanelView.COUNTER_PANEL_OPEN, 1);
+            } else if (!mNotificationPanel.isInSettings() && !mNotificationPanel.isExpanding()){
+                mNotificationPanel.flingSettings(0 /* velocity */, true /* expand */);
+                mMetricsLogger.count(NotificationPanelView.COUNTER_PANEL_OPEN_QS, 1);
+            }
+        }
+
+    }
+
+    boolean panelsEnabled() {
+        return (mDisabled1 & StatusBarManager.DISABLE_EXPAND) == 0 && !ONLY_CORE_APPS;
+    }
+
+    void makeExpandedVisible(boolean force) {
+        if (SPEW) Log.d(TAG, "Make expanded visible: expanded visible=" + mExpandedVisible);
+        if (!force && (mExpandedVisible || !panelsEnabled())) {
+            return;
+        }
+
+        mExpandedVisible = true;
+
+        // Expand the window to encompass the full screen in anticipation of the drag.
+        // This is only possible to do atomically because the status bar is at the top of the screen!
+        mStatusBarWindowManager.setPanelVisible(true);
+
+        visibilityChanged(true);
+        mWaitingForKeyguardExit = false;
+        recomputeDisableFlags(!force /* animate */);
+        setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true);
+    }
+
+    public void animateCollapsePanels() {
+        animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE);
+    }
+
+    private final Runnable mAnimateCollapsePanels = new Runnable() {
+        @Override
+        public void run() {
+            animateCollapsePanels();
+        }
+    };
+
+    public void postAnimateCollapsePanels() {
+        mHandler.post(mAnimateCollapsePanels);
+    }
+
+    public void postAnimateForceCollapsePanels() {
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */);
+            }
+        });
+    }
+
+    public void postAnimateOpenPanels() {
+        mHandler.sendEmptyMessage(MSG_OPEN_SETTINGS_PANEL);
+    }
+
+    @Override
+    public void togglePanel() {
+        if (mPanelExpanded) {
+            animateCollapsePanels();
+        } else {
+            animateExpandNotificationsPanel();
+        }
+    }
+
+    @Override
+    public void animateCollapsePanels(int flags) {
+        animateCollapsePanels(flags, false /* force */, false /* delayed */,
+                1.0f /* speedUpFactor */);
+    }
+
+    public void animateCollapsePanels(int flags, boolean force) {
+        animateCollapsePanels(flags, force, false /* delayed */, 1.0f /* speedUpFactor */);
+    }
+
+    public void animateCollapsePanels(int flags, boolean force, boolean delayed) {
+        animateCollapsePanels(flags, force, delayed, 1.0f /* speedUpFactor */);
+    }
+
+    public void animateCollapsePanels(int flags, boolean force, boolean delayed,
+            float speedUpFactor) {
+        if (!force && mState != StatusBarState.SHADE) {
+            runPostCollapseRunnables();
+            return;
+        }
+        if (SPEW) {
+            Log.d(TAG, "animateCollapse():"
+                    + " mExpandedVisible=" + mExpandedVisible
+                    + " flags=" + flags);
+        }
+
+        if ((flags & CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL) == 0) {
+            if (!mHandler.hasMessages(MSG_HIDE_RECENT_APPS)) {
+                mHandler.removeMessages(MSG_HIDE_RECENT_APPS);
+                mHandler.sendEmptyMessage(MSG_HIDE_RECENT_APPS);
+            }
+        }
+
+        if (mStatusBarWindow != null && mNotificationPanel.canPanelBeCollapsed()) {
+            // release focus immediately to kick off focus change transition
+            mStatusBarWindowManager.setStatusBarFocusable(false);
+
+            mStatusBarWindow.cancelExpandHelper();
+            mStatusBarView.collapsePanel(true /* animate */, delayed, speedUpFactor);
+        }
+    }
+
+    private void runPostCollapseRunnables() {
+        ArrayList<Runnable> clonedList = new ArrayList<>(mPostCollapseRunnables);
+        mPostCollapseRunnables.clear();
+        int size = clonedList.size();
+        for (int i = 0; i < size; i++) {
+            clonedList.get(i).run();
+        }
+        mStatusBarKeyguardViewManager.readyForKeyguardDone();
+    }
+
+    @Override
+    public void animateExpandNotificationsPanel() {
+        if (SPEW) Log.d(TAG, "animateExpand: mExpandedVisible=" + mExpandedVisible);
+        if (!panelsEnabled()) {
+            return ;
+        }
+
+        mNotificationPanel.expand(true /* animate */);
+
+        if (false) postStartTracing();
+    }
+
+    @Override
+    public void animateExpandSettingsPanel(String subPanel) {
+        if (SPEW) Log.d(TAG, "animateExpand: mExpandedVisible=" + mExpandedVisible);
+        if (!panelsEnabled()) {
+            return;
+        }
+
+        // Settings are not available in setup
+        if (!mUserSetup) return;
+
+
+        if (subPanel != null) {
+            mQSPanel.openDetails(subPanel);
+        }
+        mNotificationPanel.expandWithQs();
+
+        if (false) postStartTracing();
+    }
+
+    public void animateCollapseQuickSettings() {
+        if (mState == StatusBarState.SHADE) {
+            mStatusBarView.collapsePanel(true, false /* delayed */, 1.0f /* speedUpFactor */);
+        }
+    }
+
+    void makeExpandedInvisible() {
+        if (SPEW) Log.d(TAG, "makeExpandedInvisible: mExpandedVisible=" + mExpandedVisible
+                + " mExpandedVisible=" + mExpandedVisible);
+
+        if (!mExpandedVisible || mStatusBarWindow == null) {
+            return;
+        }
+
+        // Ensure the panel is fully collapsed (just in case; bug 6765842, 7260868)
+        mStatusBarView.collapsePanel(/*animate=*/ false, false /* delayed*/,
+                1.0f /* speedUpFactor */);
+
+        mNotificationPanel.closeQs();
+
+        mExpandedVisible = false;
+        visibilityChanged(false);
+
+        // Shrink the window to the size of the status bar only
+        mStatusBarWindowManager.setPanelVisible(false);
+        mStatusBarWindowManager.setForceStatusBarVisible(false);
+
+        // Close any guts that might be visible
+        closeAndSaveGuts(true /* removeLeavebehind */, true /* force */, true /* removeControls */,
+                -1 /* x */, -1 /* y */, true /* resetMenu */);
+
+        runPostCollapseRunnables();
+        setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false);
+        showBouncerIfKeyguard();
+        recomputeDisableFlags(mNotificationPanel.hideStatusBarIconsWhenExpanded() /* animate */);
+
+        // Trimming will happen later if Keyguard is showing - doing it here might cause a jank in
+        // the bouncer appear animation.
+        if (!mStatusBarKeyguardViewManager.isShowing()) {
+            WindowManagerGlobal.getInstance().trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
+        }
+    }
+
+    public boolean interceptTouchEvent(MotionEvent event) {
+        if (DEBUG_GESTURES) {
+            if (event.getActionMasked() != MotionEvent.ACTION_MOVE) {
+                EventLog.writeEvent(EventLogTags.SYSUI_STATUSBAR_TOUCH,
+                        event.getActionMasked(), (int) event.getX(), (int) event.getY(),
+                        mDisabled1, mDisabled2);
+            }
+
+        }
+
+        if (SPEW) {
+            Log.d(TAG, "Touch: rawY=" + event.getRawY() + " event=" + event + " mDisabled1="
+                + mDisabled1 + " mDisabled2=" + mDisabled2 + " mTracking=" + mTracking);
+        } else if (CHATTY) {
+            if (event.getAction() != MotionEvent.ACTION_MOVE) {
+                Log.d(TAG, String.format(
+                            "panel: %s at (%f, %f) mDisabled1=0x%08x mDisabled2=0x%08x",
+                            MotionEvent.actionToString(event.getAction()),
+                            event.getRawX(), event.getRawY(), mDisabled1, mDisabled2));
+            }
+        }
+
+        if (DEBUG_GESTURES) {
+            mGestureRec.add(event);
+        }
+
+        if (mStatusBarWindowState == WINDOW_STATE_SHOWING) {
+            final boolean upOrCancel =
+                    event.getAction() == MotionEvent.ACTION_UP ||
+                    event.getAction() == MotionEvent.ACTION_CANCEL;
+            if (upOrCancel && !mExpandedVisible) {
+                setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false);
+            } else {
+                setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true);
+            }
+        }
+        return false;
+    }
+
+    public GestureRecorder getGestureRecorder() {
+        return mGestureRec;
+    }
+
+    public FingerprintUnlockController getFingerprintUnlockController() {
+        return mFingerprintUnlockController;
+    }
+
+    @Override // CommandQueue
+    public void setWindowState(int window, int state) {
+        boolean showing = state == WINDOW_STATE_SHOWING;
+        if (mStatusBarWindow != null
+                && window == StatusBarManager.WINDOW_STATUS_BAR
+                && mStatusBarWindowState != state) {
+            mStatusBarWindowState = state;
+            if (DEBUG_WINDOW_STATE) Log.d(TAG, "Status bar " + windowStateToString(state));
+            if (!showing && mState == StatusBarState.SHADE) {
+                mStatusBarView.collapsePanel(false /* animate */, false /* delayed */,
+                        1.0f /* speedUpFactor */);
+            }
+            if (mStatusBarView != null) {
+                mStatusBarWindowHidden = state == WINDOW_STATE_HIDDEN;
+                updateHideIconsForBouncer(false /* animate */);
+            }
+        }
+    }
+
+    @Override // CommandQueue
+    public void setSystemUiVisibility(int vis, int fullscreenStackVis, int dockedStackVis,
+            int mask, Rect fullscreenStackBounds, Rect dockedStackBounds) {
+        final int oldVal = mSystemUiVisibility;
+        final int newVal = (oldVal&~mask) | (vis&mask);
+        final int diff = newVal ^ oldVal;
+        if (DEBUG) Log.d(TAG, String.format(
+                "setSystemUiVisibility vis=%s mask=%s oldVal=%s newVal=%s diff=%s",
+                Integer.toHexString(vis), Integer.toHexString(mask),
+                Integer.toHexString(oldVal), Integer.toHexString(newVal),
+                Integer.toHexString(diff)));
+        boolean sbModeChanged = false;
+        if (diff != 0) {
+            mSystemUiVisibility = newVal;
+
+            // update low profile
+            if ((diff & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0) {
+                setAreThereNotifications();
+            }
+
+            // ready to unhide
+            if ((vis & View.STATUS_BAR_UNHIDE) != 0) {
+                mSystemUiVisibility &= ~View.STATUS_BAR_UNHIDE;
+                mNoAnimationOnNextBarModeChange = true;
+            }
+
+            // update status bar mode
+            final int sbMode = computeStatusBarMode(oldVal, newVal);
+
+            sbModeChanged = sbMode != -1;
+            if (sbModeChanged && sbMode != mStatusBarMode) {
+                if (sbMode != mStatusBarMode) {
+                    mStatusBarMode = sbMode;
+                    checkBarModes();
+                }
+                touchAutoHide();
+            }
+
+            if ((vis & View.NAVIGATION_BAR_UNHIDE) != 0) {
+                mSystemUiVisibility &= ~View.NAVIGATION_BAR_UNHIDE;
+            }
+
+            // send updated sysui visibility to window manager
+            notifyUiVisibilityChanged(mSystemUiVisibility);
+        }
+
+        mLightBarController.onSystemUiVisibilityChanged(fullscreenStackVis, dockedStackVis,
+                mask, fullscreenStackBounds, dockedStackBounds, sbModeChanged, mStatusBarMode);
+    }
+
+    void touchAutoHide() {
+        // update transient bar autohide
+        if (mStatusBarMode == MODE_SEMI_TRANSPARENT || (mNavigationBar != null
+                && mNavigationBar.isSemiTransparent())) {
+            scheduleAutohide();
+        } else {
+            cancelAutohide();
+        }
+        touchAutoDim();
+    }
+
+    protected int computeStatusBarMode(int oldVal, int newVal) {
+        return computeBarMode(oldVal, newVal, View.STATUS_BAR_TRANSIENT,
+                View.STATUS_BAR_TRANSLUCENT, View.STATUS_BAR_TRANSPARENT);
+    }
+
+    protected BarTransitions getStatusBarTransitions() {
+        return mStatusBarView.getBarTransitions();
+    }
+
+    protected int computeBarMode(int oldVis, int newVis,
+            int transientFlag, int translucentFlag, int transparentFlag) {
+        final int oldMode = barMode(oldVis, transientFlag, translucentFlag, transparentFlag);
+        final int newMode = barMode(newVis, transientFlag, translucentFlag, transparentFlag);
+        if (oldMode == newMode) {
+            return -1; // no mode change
+        }
+        return newMode;
+    }
+
+    private int barMode(int vis, int transientFlag, int translucentFlag, int transparentFlag) {
+        int lightsOutTransparent = View.SYSTEM_UI_FLAG_LOW_PROFILE | transparentFlag;
+        return (vis & transientFlag) != 0 ? MODE_SEMI_TRANSPARENT
+                : (vis & translucentFlag) != 0 ? MODE_TRANSLUCENT
+                : (vis & lightsOutTransparent) == lightsOutTransparent ? MODE_LIGHTS_OUT_TRANSPARENT
+                : (vis & transparentFlag) != 0 ? MODE_TRANSPARENT
+                : (vis & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0 ? MODE_LIGHTS_OUT
+                : MODE_OPAQUE;
+    }
+
+    void checkBarModes() {
+        if (mDemoMode) return;
+        if (mStatusBarView != null) checkBarMode(mStatusBarMode, mStatusBarWindowState,
+                getStatusBarTransitions());
+        if (mNavigationBar != null) mNavigationBar.checkNavBarModes();
+        mNoAnimationOnNextBarModeChange = false;
+    }
+
+    // Called by NavigationBarFragment
+    void setQsScrimEnabled(boolean scrimEnabled) {
+        mNotificationPanel.setQsScrimEnabled(scrimEnabled);
+    }
+
+    void checkBarMode(int mode, int windowState, BarTransitions transitions) {
+        final boolean powerSave = mBatteryController.isPowerSave();
+        final boolean anim = !mNoAnimationOnNextBarModeChange && mDeviceInteractive
+                && windowState != WINDOW_STATE_HIDDEN && !powerSave;
+        if (powerSave && getBarState() == StatusBarState.SHADE) {
+            mode = MODE_WARNING;
+        }
+        transitions.transitionTo(mode, anim);
+    }
+
+    private void finishBarAnimations() {
+        if (mStatusBarView != null) {
+            mStatusBarView.getBarTransitions().finishAnimations();
+        }
+        if (mNavigationBar != null) {
+            mNavigationBar.finishBarAnimations();
+        }
+    }
+
+    private final Runnable mCheckBarModes = new Runnable() {
+        @Override
+        public void run() {
+            checkBarModes();
+        }
+    };
+
+    public void setInteracting(int barWindow, boolean interacting) {
+        final boolean changing = ((mInteractingWindows & barWindow) != 0) != interacting;
+        mInteractingWindows = interacting
+                ? (mInteractingWindows | barWindow)
+                : (mInteractingWindows & ~barWindow);
+        if (mInteractingWindows != 0) {
+            suspendAutohide();
+        } else {
+            resumeSuspendedAutohide();
+        }
+        // manually dismiss the volume panel when interacting with the nav bar
+        if (changing && interacting && barWindow == StatusBarManager.WINDOW_NAVIGATION_BAR) {
+            dismissVolumeDialog();
+        }
+        checkBarModes();
+        touchAutoDim();
+    }
+
+    private void dismissVolumeDialog() {
+        if (mVolumeComponent != null) {
+            mVolumeComponent.dismissNow();
+        }
+    }
+
+    private void resumeSuspendedAutohide() {
+        if (mAutohideSuspended) {
+            scheduleAutohide();
+            mHandler.postDelayed(mCheckBarModes, 500); // longer than home -> launcher
+        }
+    }
+
+    private void suspendAutohide() {
+        mHandler.removeCallbacks(mAutohide);
+        mHandler.removeCallbacks(mCheckBarModes);
+        mAutohideSuspended = (mSystemUiVisibility & STATUS_OR_NAV_TRANSIENT) != 0;
+    }
+
+    private void cancelAutohide() {
+        mAutohideSuspended = false;
+        mHandler.removeCallbacks(mAutohide);
+    }
+
+    private void scheduleAutohide() {
+        cancelAutohide();
+        mHandler.postDelayed(mAutohide, AUTOHIDE_TIMEOUT_MS);
+    }
+
+    public void touchAutoDim() {
+        if (mNavigationBar != null) {
+            mNavigationBar.getBarTransitions().setAutoDim(false);
+        }
+        mHandler.removeCallbacks(mAutoDim);
+        if (mState != StatusBarState.KEYGUARD && mState != StatusBarState.SHADE_LOCKED) {
+            mHandler.postDelayed(mAutoDim, AUTOHIDE_TIMEOUT_MS);
+        }
+    }
+
+    void checkUserAutohide(View v, MotionEvent event) {
+        if ((mSystemUiVisibility & STATUS_OR_NAV_TRANSIENT) != 0  // a transient bar is revealed
+                && event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
+                && event.getX() == 0 && event.getY() == 0  // a touch outside both bars
+                && !mRemoteInputController.isRemoteInputActive()) { // not due to typing in IME
+            userAutohide();
+        }
+    }
+
+    private void checkRemoteInputOutside(MotionEvent event) {
+        if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
+                && event.getX() == 0 && event.getY() == 0  // a touch outside both bars
+                && mRemoteInputController.isRemoteInputActive()) {
+            mRemoteInputController.closeRemoteInputs();
+        }
+    }
+
+    private void userAutohide() {
+        cancelAutohide();
+        mHandler.postDelayed(mAutohide, 350); // longer than app gesture -> flag clear
+    }
+
+    private boolean areLightsOn() {
+        return 0 == (mSystemUiVisibility & View.SYSTEM_UI_FLAG_LOW_PROFILE);
+    }
+
+    public void setLightsOn(boolean on) {
+        Log.v(TAG, "setLightsOn(" + on + ")");
+        if (on) {
+            setSystemUiVisibility(0, 0, 0, View.SYSTEM_UI_FLAG_LOW_PROFILE,
+                    mLastFullscreenStackBounds, mLastDockedStackBounds);
+        } else {
+            setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE, 0, 0,
+                    View.SYSTEM_UI_FLAG_LOW_PROFILE, mLastFullscreenStackBounds,
+                    mLastDockedStackBounds);
+        }
+    }
+
+    private void notifyUiVisibilityChanged(int vis) {
+        try {
+            if (mLastDispatchedSystemUiVisibility != vis) {
+                mWindowManagerService.statusBarVisibilityChanged(vis);
+                mLastDispatchedSystemUiVisibility = vis;
+            }
+        } catch (RemoteException ex) {
+        }
+    }
+
+    @Override
+    public void topAppWindowChanged(boolean showMenu) {
+        if (SPEW) {
+            Log.d(TAG, (showMenu?"showing":"hiding") + " the MENU button");
+        }
+
+        // See above re: lights-out policy for legacy apps.
+        if (showMenu) setLightsOn(true);
+    }
+
+    public static String viewInfo(View v) {
+        return "[(" + v.getLeft() + "," + v.getTop() + ")(" + v.getRight() + "," + v.getBottom()
+                + ") " + v.getWidth() + "x" + v.getHeight() + "]";
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        synchronized (mQueueLock) {
+            pw.println("Current Status Bar state:");
+            pw.println("  mExpandedVisible=" + mExpandedVisible
+                    + ", mTrackingPosition=" + mTrackingPosition);
+            pw.println("  mTracking=" + mTracking);
+            pw.println("  mDisplayMetrics=" + mDisplayMetrics);
+            pw.println("  mStackScroller: " + viewInfo(mStackScroller));
+            pw.println("  mStackScroller: " + viewInfo(mStackScroller)
+                    + " scroll " + mStackScroller.getScrollX()
+                    + "," + mStackScroller.getScrollY());
+        }
+        pw.print("  mPendingNotifications=");
+        if (mPendingNotifications.size() == 0) {
+            pw.println("null");
+        } else {
+            for (Entry entry : mPendingNotifications.values()) {
+                pw.println(entry.notification);
+            }
+        }
+
+        pw.print("  mInteractingWindows="); pw.println(mInteractingWindows);
+        pw.print("  mStatusBarWindowState=");
+        pw.println(windowStateToString(mStatusBarWindowState));
+        pw.print("  mStatusBarMode=");
+        pw.println(BarTransitions.modeToString(mStatusBarMode));
+        pw.print("  mDozing="); pw.println(mDozing);
+        pw.print("  mZenMode=");
+        pw.println(Settings.Global.zenModeToString(mZenMode));
+        pw.print("  mUseHeadsUp=");
+        pw.println(mUseHeadsUp);
+        pw.print("  mKeyToRemoveOnGutsClosed=");
+        pw.println(mKeyToRemoveOnGutsClosed);
+        if (mStatusBarView != null) {
+            dumpBarTransitions(pw, "mStatusBarView", mStatusBarView.getBarTransitions());
+        }
+
+        pw.print("  mMediaSessionManager=");
+        pw.println(mMediaSessionManager);
+        pw.print("  mMediaNotificationKey=");
+        pw.println(mMediaNotificationKey);
+        pw.print("  mMediaController=");
+        pw.print(mMediaController);
+        if (mMediaController != null) {
+            pw.print(" state=" + mMediaController.getPlaybackState());
+        }
+        pw.println();
+        pw.print("  mMediaMetadata=");
+        pw.print(mMediaMetadata);
+        if (mMediaMetadata != null) {
+            pw.print(" title=" + mMediaMetadata.getText(MediaMetadata.METADATA_KEY_TITLE));
+        }
+        pw.println();
+
+        pw.println("  Panels: ");
+        if (mNotificationPanel != null) {
+            pw.println("    mNotificationPanel=" +
+                mNotificationPanel + " params=" + mNotificationPanel.getLayoutParams().debug(""));
+            pw.print  ("      ");
+            mNotificationPanel.dump(fd, pw, args);
+        }
+        pw.println("  mStackScroller: ");
+        if (mStackScroller != null) {
+            pw.print  ("      ");
+            mStackScroller.dump(fd, pw, args);
+        }
+        pw.println("  Theme:");
+        if (mOverlayManager == null) {
+            pw.println("    overlay manager not initialized!");
+        } else {
+            pw.println("    dark overlay on: " + isUsingDarkTheme());
+        }
+        final boolean lightWpTheme = mContext.getThemeResId() == R.style.Theme_SystemUI_Light;
+        pw.println("    light wallpaper theme: " + lightWpTheme);
+
+        DozeLog.dump(pw);
+
+        if (mFingerprintUnlockController != null) {
+            mFingerprintUnlockController.dump(pw);
+        }
+
+        if (mScrimController != null) {
+            mScrimController.dump(pw);
+        }
+
+        if (DUMPTRUCK) {
+            synchronized (mNotificationData) {
+                mNotificationData.dump(pw, "  ");
+            }
+
+            if (false) {
+                pw.println("see the logcat for a dump of the views we have created.");
+                // must happen on ui thread
+                mHandler.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            mStatusBarView.getLocationOnScreen(mAbsPos);
+                            Log.d(TAG, "mStatusBarView: ----- (" + mAbsPos[0] + "," + mAbsPos[1]
+                                    + ") " + mStatusBarView.getWidth() + "x"
+                                    + getStatusBarHeight());
+                            mStatusBarView.debug();
+                        }
+                    });
+            }
+        }
+
+        if (DEBUG_GESTURES) {
+            pw.print("  status bar gestures: ");
+            mGestureRec.dump(fd, pw, args);
+        }
+
+        if (mHeadsUpManager != null) {
+            mHeadsUpManager.dump(fd, pw, args);
+        } else {
+            pw.println("  mHeadsUpManager: null");
+        }
+        if (mGroupManager != null) {
+            mGroupManager.dump(fd, pw, args);
+        } else {
+            pw.println("  mGroupManager: null");
+        }
+
+        if (mLightBarController != null) {
+            mLightBarController.dump(fd, pw, args);
+        }
+
+        if (KeyguardUpdateMonitor.getInstance(mContext) != null) {
+            KeyguardUpdateMonitor.getInstance(mContext).dump(fd, pw, args);
+        }
+
+        FalsingManager.getInstance(mContext).dump(pw);
+        FalsingLog.dump(pw);
+
+        pw.println("SharedPreferences:");
+        for (Map.Entry<String, ?> entry : Prefs.getAll(mContext).entrySet()) {
+            pw.print("  "); pw.print(entry.getKey()); pw.print("="); pw.println(entry.getValue());
+        }
+    }
+
+    static void dumpBarTransitions(PrintWriter pw, String var, BarTransitions transitions) {
+        pw.print("  "); pw.print(var); pw.print(".BarTransitions.mMode=");
+        pw.println(BarTransitions.modeToString(transitions.getMode()));
+    }
+
+    public void createAndAddWindows() {
+        addStatusBarWindow();
+    }
+
+    private void addStatusBarWindow() {
+        makeStatusBarView();
+        mStatusBarWindowManager = Dependency.get(StatusBarWindowManager.class);
+        mRemoteInputController = new RemoteInputController(mHeadsUpManager);
+        mStatusBarWindowManager.add(mStatusBarWindow, getStatusBarHeight());
+    }
+
+    // called by makeStatusbar and also by PhoneStatusBarView
+    void updateDisplaySize() {
+        mDisplay.getMetrics(mDisplayMetrics);
+        mDisplay.getSize(mCurrentDisplaySize);
+        if (DEBUG_GESTURES) {
+            mGestureRec.tag("display",
+                    String.format("%dx%d", mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels));
+        }
+    }
+
+    float getDisplayDensity() {
+        return mDisplayMetrics.density;
+    }
+
+    public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
+            boolean dismissShade) {
+        startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade,
+                false /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */);
+    }
+
+    public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
+            final boolean dismissShade, final boolean disallowEnterPictureInPictureWhileLaunching,
+            final Callback callback) {
+        if (onlyProvisioned && !isDeviceProvisioned()) return;
+
+        final boolean afterKeyguardGone = PreviewInflater.wouldLaunchResolverActivity(
+                mContext, intent, mCurrentUserId);
+        Runnable runnable = new Runnable() {
+            @Override
+            public void run() {
+                mAssistManager.hideAssist();
+                intent.setFlags(
+                        Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                int result = ActivityManager.START_CANCELED;
+                ActivityOptions options = new ActivityOptions(getActivityOptions());
+                options.setDisallowEnterPictureInPictureWhileLaunching(
+                        disallowEnterPictureInPictureWhileLaunching);
+                if (intent == KeyguardBottomAreaView.INSECURE_CAMERA_INTENT) {
+                    // Normally an activity will set it's requested rotation
+                    // animation on its window. However when launching an activity
+                    // causes the orientation to change this is too late. In these cases
+                    // the default animation is used. This doesn't look good for
+                    // the camera (as it rotates the camera contents out of sync
+                    // with physical reality). So, we ask the WindowManager to
+                    // force the crossfade animation if an orientation change
+                    // happens to occur during the launch.
+                    options.setRotationAnimationHint(
+                            WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS);
+                }
+                try {
+                    result = ActivityManager.getService().startActivityAsUser(
+                            null, mContext.getBasePackageName(),
+                            intent,
+                            intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+                            null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null,
+                            options.toBundle(), UserHandle.CURRENT.getIdentifier());
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Unable to start activity", e);
+                }
+                if (callback != null) {
+                    callback.onActivityStarted(result);
+                }
+            }
+        };
+        Runnable cancelRunnable = new Runnable() {
+            @Override
+            public void run() {
+                if (callback != null) {
+                    callback.onActivityStarted(ActivityManager.START_CANCELED);
+                }
+            }
+        };
+        executeRunnableDismissingKeyguard(runnable, cancelRunnable, dismissShade,
+                afterKeyguardGone, true /* deferred */);
+    }
+
+    public void readyForKeyguardDone() {
+        mStatusBarKeyguardViewManager.readyForKeyguardDone();
+    }
+
+    public void executeRunnableDismissingKeyguard(final Runnable runnable,
+            final Runnable cancelAction,
+            final boolean dismissShade,
+            final boolean afterKeyguardGone,
+            final boolean deferred) {
+        dismissKeyguardThenExecute(() -> {
+            if (runnable != null) {
+                if (mStatusBarKeyguardViewManager.isShowing()
+                        && mStatusBarKeyguardViewManager.isOccluded()) {
+                    mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(runnable);
+                } else {
+                    AsyncTask.execute(runnable);
+                }
+            }
+            if (dismissShade) {
+                if (mExpandedVisible) {
+                    animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */,
+                            true /* delayed*/);
+                } else {
+
+                    // Do it after DismissAction has been processed to conserve the needed ordering.
+                    mHandler.post(this::runPostCollapseRunnables);
+                }
+            } else if (isInLaunchTransition() && mNotificationPanel.isLaunchTransitionFinished()) {
+
+                // We are not dismissing the shade, but the launch transition is already finished,
+                // so nobody will call readyForKeyguardDone anymore. Post it such that
+                // keyguardDonePending gets called first.
+                mHandler.post(mStatusBarKeyguardViewManager::readyForKeyguardDone);
+            }
+            return deferred;
+        }, cancelAction, afterKeyguardGone);
+    }
+
+    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (DEBUG) Log.v(TAG, "onReceive: " + intent);
+            String action = intent.getAction();
+            if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
+                KeyboardShortcuts.dismiss();
+                if (mRemoteInputController != null) {
+                    mRemoteInputController.closeRemoteInputs();
+                }
+                if (isCurrentProfile(getSendingUserId())) {
+                    int flags = CommandQueue.FLAG_EXCLUDE_NONE;
+                    String reason = intent.getStringExtra("reason");
+                    if (reason != null && reason.equals(SYSTEM_DIALOG_REASON_RECENT_APPS)) {
+                        flags |= CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL;
+                    }
+                    animateCollapsePanels(flags);
+                }
+            }
+            else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+                finishBarAnimations();
+                resetUserExpandedStates();
+            }
+            else if (DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG.equals(action)) {
+                mQSPanel.showDeviceMonitoringDialog();
+            }
+        }
+    };
+
+    private BroadcastReceiver mDemoReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (DEBUG) Log.v(TAG, "onReceive: " + intent);
+            String action = intent.getAction();
+            if (ACTION_DEMO.equals(action)) {
+                Bundle bundle = intent.getExtras();
+                if (bundle != null) {
+                    String command = bundle.getString("command", "").trim().toLowerCase();
+                    if (command.length() > 0) {
+                        try {
+                            dispatchDemoCommand(command, bundle);
+                        } catch (Throwable t) {
+                            Log.w(TAG, "Error running demo command, intent=" + intent, t);
+                        }
+                    }
+                }
+            } else if (ACTION_FAKE_ARTWORK.equals(action)) {
+                if (DEBUG_MEDIA_FAKE_ARTWORK) {
+                    updateMediaMetaData(true, true);
+                }
+            }
+        }
+    };
+
+    public void resetUserExpandedStates() {
+        ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+        final int notificationCount = activeNotifications.size();
+        for (int i = 0; i < notificationCount; i++) {
+            NotificationData.Entry entry = activeNotifications.get(i);
+            if (entry.row != null) {
+                entry.row.resetUserExpansion();
+            }
+        }
+    }
+
+    protected void dismissKeyguardThenExecute(OnDismissAction action, boolean afterKeyguardGone) {
+        dismissKeyguardThenExecute(action, null /* cancelRunnable */, afterKeyguardGone);
+    }
+
+    private void dismissKeyguardThenExecute(OnDismissAction action, Runnable cancelAction,
+            boolean afterKeyguardGone) {
+        if (mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP
+                && mUnlockMethodCache.canSkipBouncer()
+                && !mLeaveOpenOnKeyguardHide
+                && isPulsing()) {
+            // Reuse the fingerprint wake-and-unlock transition if we dismiss keyguard from a pulse.
+            // TODO: Factor this transition out of FingerprintUnlockController.
+            mFingerprintUnlockController.startWakeAndUnlock(
+                    FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING);
+        }
+        if (mStatusBarKeyguardViewManager.isShowing()) {
+            mStatusBarKeyguardViewManager.dismissWithAction(action, cancelAction,
+                    afterKeyguardGone);
+        } else {
+            action.onDismiss();
+        }
+    }
+
+    // SystemUIService notifies SystemBars of configuration changes, which then calls down here
+    @Override
+    public void onConfigChanged(Configuration newConfig) {
+        updateResources();
+        updateDisplaySize(); // populates mDisplayMetrics
+
+        if (DEBUG) {
+            Log.v(TAG, "configuration changed: " + mContext.getResources().getConfiguration());
+        }
+
+        updateRowStates();
+        mScreenPinningRequest.onConfigurationChanged();
+    }
+
+    public void userSwitched(int newUserId) {
+        // Begin old BaseStatusBar.userSwitched
+        setHeadsUpUser(newUserId);
+        // End old BaseStatusBar.userSwitched
+        if (MULTIUSER_DEBUG) mNotificationPanelDebugText.setText("USER " + newUserId);
+        animateCollapsePanels();
+        updatePublicMode();
+        mNotificationData.filterAndSort();
+        if (mReinflateNotificationsOnUserSwitched) {
+            updateNotificationsOnDensityOrFontScaleChanged();
+            mReinflateNotificationsOnUserSwitched = false;
+        }
+        updateNotificationShade();
+        clearCurrentMediaNotification();
+        setLockscreenUser(newUserId);
+    }
+
+    protected void setLockscreenUser(int newUserId) {
+        mLockscreenWallpaper.setCurrentUser(newUserId);
+        mScrimController.setCurrentUser(newUserId);
+        updateMediaMetaData(true, false);
+    }
+
+    /**
+     * Reload some of our resources when the configuration changes.
+     *
+     * We don't reload everything when the configuration changes -- we probably
+     * should, but getting that smooth is tough.  Someday we'll fix that.  In the
+     * meantime, just update the things that we know change.
+     */
+    void updateResources() {
+        // Update the quick setting tiles
+        if (mQSPanel != null) {
+            mQSPanel.updateResources();
+        }
+
+        loadDimens();
+
+        if (mNotificationPanel != null) {
+            mNotificationPanel.updateResources();
+        }
+        if (mBrightnessMirrorController != null) {
+            mBrightnessMirrorController.updateResources();
+        }
+    }
+
+    protected void loadDimens() {
+        final Resources res = mContext.getResources();
+
+        int oldBarHeight = mNaturalBarHeight;
+        mNaturalBarHeight = res.getDimensionPixelSize(
+                com.android.internal.R.dimen.status_bar_height);
+        if (mStatusBarWindowManager != null && mNaturalBarHeight != oldBarHeight) {
+            mStatusBarWindowManager.setBarHeight(mNaturalBarHeight);
+        }
+        mMaxAllowedKeyguardNotifications = res.getInteger(
+                R.integer.keyguard_max_notification_count);
+
+        if (DEBUG) Log.v(TAG, "defineSlots");
+    }
+
+    // Visibility reporting
+
+    protected void handleVisibleToUserChanged(boolean visibleToUser) {
+        if (visibleToUser) {
+            handleVisibleToUserChangedImpl(visibleToUser);
+            startNotificationLogging();
+        } else {
+            stopNotificationLogging();
+            handleVisibleToUserChangedImpl(visibleToUser);
+        }
+    }
+
+    void handlePeekToExpandTransistion() {
+        try {
+            // consider the transition from peek to expanded to be a panel open,
+            // but not one that clears notification effects.
+            int notificationLoad = mNotificationData.getActiveNotifications().size();
+            mBarService.onPanelRevealed(false, notificationLoad);
+        } catch (RemoteException ex) {
+            // Won't fail unless the world has ended.
+        }
+    }
+
+    /**
+     * The LEDs are turned off when the notification panel is shown, even just a little bit.
+     * See also StatusBar.setPanelExpanded for another place where we attempt to do this.
+     */
+    // Old BaseStatusBar.handleVisibileToUserChanged
+    private void handleVisibleToUserChangedImpl(boolean visibleToUser) {
+        try {
+            if (visibleToUser) {
+                boolean pinnedHeadsUp = mHeadsUpManager.hasPinnedHeadsUp();
+                boolean clearNotificationEffects =
+                        !isPanelFullyCollapsed() &&
+                        (mState == StatusBarState.SHADE || mState == StatusBarState.SHADE_LOCKED);
+                int notificationLoad = mNotificationData.getActiveNotifications().size();
+                if (pinnedHeadsUp && isPanelFullyCollapsed())  {
+                    notificationLoad = 1;
+                }
+                mBarService.onPanelRevealed(clearNotificationEffects, notificationLoad);
+            } else {
+                mBarService.onPanelHidden();
+            }
+        } catch (RemoteException ex) {
+            // Won't fail unless the world has ended.
+        }
+    }
+
+    private void stopNotificationLogging() {
+        // Report all notifications as invisible and turn down the
+        // reporter.
+        if (!mCurrentlyVisibleNotifications.isEmpty()) {
+            logNotificationVisibilityChanges(Collections.<NotificationVisibility>emptyList(),
+                    mCurrentlyVisibleNotifications);
+            recycleAllVisibilityObjects(mCurrentlyVisibleNotifications);
+        }
+        mHandler.removeCallbacks(mVisibilityReporter);
+        mStackScroller.setChildLocationsChangedListener(null);
+    }
+
+    private void startNotificationLogging() {
+        mStackScroller.setChildLocationsChangedListener(mNotificationLocationsChangedListener);
+        // Some transitions like mVisibleToUser=false -> mVisibleToUser=true don't
+        // cause the scroller to emit child location events. Hence generate
+        // one ourselves to guarantee that we're reporting visible
+        // notifications.
+        // (Note that in cases where the scroller does emit events, this
+        // additional event doesn't break anything.)
+        mNotificationLocationsChangedListener.onChildLocationsChanged(mStackScroller);
+    }
+
+    private void logNotificationVisibilityChanges(
+            Collection<NotificationVisibility> newlyVisible,
+            Collection<NotificationVisibility> noLongerVisible) {
+        if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) {
+            return;
+        }
+        NotificationVisibility[] newlyVisibleAr =
+                newlyVisible.toArray(new NotificationVisibility[newlyVisible.size()]);
+        NotificationVisibility[] noLongerVisibleAr =
+                noLongerVisible.toArray(new NotificationVisibility[noLongerVisible.size()]);
+        try {
+            mBarService.onNotificationVisibilityChanged(newlyVisibleAr, noLongerVisibleAr);
+        } catch (RemoteException e) {
+            // Ignore.
+        }
+
+        final int N = newlyVisible.size();
+        if (N > 0) {
+            String[] newlyVisibleKeyAr = new String[N];
+            for (int i = 0; i < N; i++) {
+                newlyVisibleKeyAr[i] = newlyVisibleAr[i].key;
+            }
+
+            setNotificationsShown(newlyVisibleKeyAr);
+        }
+    }
+
+    // State logging
+
+    private void logStateToEventlog() {
+        boolean isShowing = mStatusBarKeyguardViewManager.isShowing();
+        boolean isOccluded = mStatusBarKeyguardViewManager.isOccluded();
+        boolean isBouncerShowing = mStatusBarKeyguardViewManager.isBouncerShowing();
+        boolean isSecure = mUnlockMethodCache.isMethodSecure();
+        boolean canSkipBouncer = mUnlockMethodCache.canSkipBouncer();
+        int stateFingerprint = getLoggingFingerprint(mState,
+                isShowing,
+                isOccluded,
+                isBouncerShowing,
+                isSecure,
+                canSkipBouncer);
+        if (stateFingerprint != mLastLoggedStateFingerprint) {
+            if (mStatusBarStateLog == null) {
+                mStatusBarStateLog = new LogMaker(MetricsEvent.VIEW_UNKNOWN);
+            }
+            mMetricsLogger.write(mStatusBarStateLog
+                    .setCategory(isBouncerShowing ? MetricsEvent.BOUNCER : MetricsEvent.LOCKSCREEN)
+                    .setType(isShowing ? MetricsEvent.TYPE_OPEN : MetricsEvent.TYPE_CLOSE)
+                    .setSubtype(isSecure ? 1 : 0));
+            EventLogTags.writeSysuiStatusBarState(mState,
+                    isShowing ? 1 : 0,
+                    isOccluded ? 1 : 0,
+                    isBouncerShowing ? 1 : 0,
+                    isSecure ? 1 : 0,
+                    canSkipBouncer ? 1 : 0);
+            mLastLoggedStateFingerprint = stateFingerprint;
+        }
+    }
+
+    /**
+     * Returns a fingerprint of fields logged to eventlog
+     */
+    private static int getLoggingFingerprint(int statusBarState, boolean keyguardShowing,
+            boolean keyguardOccluded, boolean bouncerShowing, boolean secure,
+            boolean currentlyInsecure) {
+        // Reserve 8 bits for statusBarState. We'll never go higher than
+        // that, right? Riiiight.
+        return (statusBarState & 0xFF)
+                | ((keyguardShowing   ? 1 : 0) <<  8)
+                | ((keyguardOccluded  ? 1 : 0) <<  9)
+                | ((bouncerShowing    ? 1 : 0) << 10)
+                | ((secure            ? 1 : 0) << 11)
+                | ((currentlyInsecure ? 1 : 0) << 12);
+    }
+
+    //
+    // tracing
+    //
+
+    void postStartTracing() {
+        mHandler.postDelayed(mStartTracing, 3000);
+    }
+
+    void vibrate() {
+        android.os.Vibrator vib = (android.os.Vibrator)mContext.getSystemService(
+                Context.VIBRATOR_SERVICE);
+        vib.vibrate(250, VIBRATION_ATTRIBUTES);
+    }
+
+    Runnable mStartTracing = new Runnable() {
+        @Override
+        public void run() {
+            vibrate();
+            SystemClock.sleep(250);
+            Log.d(TAG, "startTracing");
+            android.os.Debug.startMethodTracing("/data/statusbar-traces/trace");
+            mHandler.postDelayed(mStopTracing, 10000);
+        }
+    };
+
+    Runnable mStopTracing = new Runnable() {
+        @Override
+        public void run() {
+            android.os.Debug.stopMethodTracing();
+            Log.d(TAG, "stopTracing");
+            vibrate();
+        }
+    };
+
+    @Override
+    public void postQSRunnableDismissingKeyguard(final Runnable runnable) {
+        mHandler.post(() -> {
+            mLeaveOpenOnKeyguardHide = true;
+            executeRunnableDismissingKeyguard(() -> mHandler.post(runnable), null, false, false,
+                    false);
+        });
+    }
+
+    @Override
+    public void postStartActivityDismissingKeyguard(final PendingIntent intent) {
+        mHandler.post(() -> startPendingIntentDismissingKeyguard(intent));
+    }
+
+    @Override
+    public void postStartActivityDismissingKeyguard(final Intent intent, int delay) {
+        mHandler.postDelayed(() ->
+                handleStartActivityDismissingKeyguard(intent, true /*onlyProvisioned*/), delay);
+    }
+
+    private void handleStartActivityDismissingKeyguard(Intent intent, boolean onlyProvisioned) {
+        startActivityDismissingKeyguard(intent, onlyProvisioned, true /* dismissShade */);
+    }
+
+    private static class FastColorDrawable extends Drawable {
+        private final int mColor;
+
+        public FastColorDrawable(int color) {
+            mColor = 0xff000000 | color;
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            canvas.drawColor(mColor, PorterDuff.Mode.SRC);
+        }
+
+        @Override
+        public void setAlpha(int alpha) {
+        }
+
+        @Override
+        public void setColorFilter(ColorFilter colorFilter) {
+        }
+
+        @Override
+        public int getOpacity() {
+            return PixelFormat.OPAQUE;
+        }
+
+        @Override
+        public void setBounds(int left, int top, int right, int bottom) {
+        }
+
+        @Override
+        public void setBounds(Rect bounds) {
+        }
+    }
+
+    public void destroy() {
+        // Begin old BaseStatusBar.destroy().
+        mContext.unregisterReceiver(mBaseBroadcastReceiver);
+        try {
+            mNotificationListener.unregisterAsSystemService();
+        } catch (RemoteException e) {
+            // Ignore.
+        }
+        mDeviceProvisionedController.removeCallback(mDeviceProvisionedListener);
+        // End old BaseStatusBar.destroy().
+        if (mStatusBarWindow != null) {
+            mWindowManager.removeViewImmediate(mStatusBarWindow);
+            mStatusBarWindow = null;
+        }
+        if (mNavigationBarView != null) {
+            mWindowManager.removeViewImmediate(mNavigationBarView);
+            mNavigationBarView = null;
+        }
+        mContext.unregisterReceiver(mBroadcastReceiver);
+        mContext.unregisterReceiver(mDemoReceiver);
+        mAssistManager.destroy();
+
+        if (mQSPanel != null && mQSPanel.getHost() != null) {
+            mQSPanel.getHost().destroy();
+        }
+        Dependency.get(ActivityStarterDelegate.class).setActivityStarterImpl(null);
+        mDeviceProvisionedController.removeCallback(mUserSetupObserver);
+        Dependency.get(ConfigurationController.class).removeCallback(this);
+    }
+
+    private boolean mDemoModeAllowed;
+    private boolean mDemoMode;
+
+    @Override
+    public void dispatchDemoCommand(String command, Bundle args) {
+        if (!mDemoModeAllowed) {
+            mDemoModeAllowed = Settings.Global.getInt(mContext.getContentResolver(),
+                    DEMO_MODE_ALLOWED, 0) != 0;
+        }
+        if (!mDemoModeAllowed) return;
+        if (command.equals(COMMAND_ENTER)) {
+            mDemoMode = true;
+        } else if (command.equals(COMMAND_EXIT)) {
+            mDemoMode = false;
+            checkBarModes();
+        } else if (!mDemoMode) {
+            // automatically enter demo mode on first demo command
+            dispatchDemoCommand(COMMAND_ENTER, new Bundle());
+        }
+        boolean modeChange = command.equals(COMMAND_ENTER) || command.equals(COMMAND_EXIT);
+        if ((modeChange || command.equals(COMMAND_VOLUME)) && mVolumeComponent != null) {
+            mVolumeComponent.dispatchDemoCommand(command, args);
+        }
+        if (modeChange || command.equals(COMMAND_CLOCK)) {
+            dispatchDemoCommandToView(command, args, R.id.clock);
+        }
+        if (modeChange || command.equals(COMMAND_BATTERY)) {
+            mBatteryController.dispatchDemoCommand(command, args);
+        }
+        if (modeChange || command.equals(COMMAND_STATUS)) {
+            ((StatusBarIconControllerImpl) mIconController).dispatchDemoCommand(command, args);
+        }
+        if (mNetworkController != null && (modeChange || command.equals(COMMAND_NETWORK))) {
+            mNetworkController.dispatchDemoCommand(command, args);
+        }
+        if (modeChange || command.equals(COMMAND_NOTIFICATIONS)) {
+            View notifications = mStatusBarView == null ? null
+                    : mStatusBarView.findViewById(R.id.notification_icon_area);
+            if (notifications != null) {
+                String visible = args.getString("visible");
+                int vis = mDemoMode && "false".equals(visible) ? View.INVISIBLE : View.VISIBLE;
+                notifications.setVisibility(vis);
+            }
+        }
+        if (command.equals(COMMAND_BARS)) {
+            String mode = args.getString("mode");
+            int barMode = "opaque".equals(mode) ? MODE_OPAQUE :
+                    "translucent".equals(mode) ? MODE_TRANSLUCENT :
+                    "semi-transparent".equals(mode) ? MODE_SEMI_TRANSPARENT :
+                    "transparent".equals(mode) ? MODE_TRANSPARENT :
+                    "warning".equals(mode) ? MODE_WARNING :
+                    -1;
+            if (barMode != -1) {
+                boolean animate = true;
+                if (mStatusBarView != null) {
+                    mStatusBarView.getBarTransitions().transitionTo(barMode, animate);
+                }
+                if (mNavigationBar != null) {
+                    mNavigationBar.getBarTransitions().transitionTo(barMode, animate);
+                }
+            }
+        }
+    }
+
+    private void dispatchDemoCommandToView(String command, Bundle args, int id) {
+        if (mStatusBarView == null) return;
+        View v = mStatusBarView.findViewById(id);
+        if (v instanceof DemoMode) {
+            ((DemoMode)v).dispatchDemoCommand(command, args);
+        }
+    }
+
+    /**
+     * @return The {@link StatusBarState} the status bar is in.
+     */
+    public int getBarState() {
+        return mState;
+    }
+
+    public boolean isPanelFullyCollapsed() {
+        return mNotificationPanel.isFullyCollapsed();
+    }
+
+    public void showKeyguard() {
+        mKeyguardRequested = true;
+        mLeaveOpenOnKeyguardHide = false;
+        mPendingRemoteInputView = null;
+        updateIsKeyguard();
+        mAssistManager.onLockscreenShown();
+    }
+
+    public boolean hideKeyguard() {
+        mKeyguardRequested = false;
+        return updateIsKeyguard();
+    }
+
+    private boolean updateIsKeyguard() {
+        boolean wakeAndUnlocking = mFingerprintUnlockController.getMode()
+                == FingerprintUnlockController.MODE_WAKE_AND_UNLOCK;
+
+        // For dozing, keyguard needs to be shown whenever the device is non-interactive. Otherwise
+        // there's no surface we can show to the user. Note that the device goes fully interactive
+        // late in the transition, so we also allow the device to start dozing once the screen has
+        // turned off fully.
+        boolean keyguardForDozing = mDozingRequested &&
+                (!mDeviceInteractive || isGoingToSleep() && (isScreenFullyOff() || mIsKeyguard));
+        boolean shouldBeKeyguard = (mKeyguardRequested || keyguardForDozing) && !wakeAndUnlocking;
+        if (keyguardForDozing) {
+            updatePanelExpansionForKeyguard();
+        }
+        if (shouldBeKeyguard) {
+            if (isGoingToSleep()
+                    && mScreenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_TURNING_OFF) {
+                // Delay showing the keyguard until screen turned off.
+            } else {
+                showKeyguardImpl();
+            }
+        } else {
+            return hideKeyguardImpl();
+        }
+        return false;
+    }
+
+    public void showKeyguardImpl() {
+        mIsKeyguard = true;
+        if (mLaunchTransitionFadingAway) {
+            mNotificationPanel.animate().cancel();
+            onLaunchTransitionFadingEnded();
+        }
+        mHandler.removeMessages(MSG_LAUNCH_TRANSITION_TIMEOUT);
+        if (mUserSwitcherController != null && mUserSwitcherController.useFullscreenUserSwitcher()) {
+            setBarState(StatusBarState.FULLSCREEN_USER_SWITCHER);
+        } else {
+            setBarState(StatusBarState.KEYGUARD);
+        }
+        updateKeyguardState(false /* goingToFullShade */, false /* fromShadeLocked */);
+        updatePanelExpansionForKeyguard();
+        if (mDraggedDownRow != null) {
+            mDraggedDownRow.setUserLocked(false);
+            mDraggedDownRow.notifyHeightChanged(false  /* needsAnimation */);
+            mDraggedDownRow = null;
+        }
+    }
+
+    private void updatePanelExpansionForKeyguard() {
+        if (mState == StatusBarState.KEYGUARD && mFingerprintUnlockController.getMode()
+                != FingerprintUnlockController.MODE_WAKE_AND_UNLOCK) {
+            instantExpandNotificationsPanel();
+        } else if (mState == StatusBarState.FULLSCREEN_USER_SWITCHER) {
+            instantCollapseNotificationPanel();
+        }
+    }
+
+    private void onLaunchTransitionFadingEnded() {
+        mNotificationPanel.setAlpha(1.0f);
+        mNotificationPanel.onAffordanceLaunchEnded();
+        releaseGestureWakeLock();
+        runLaunchTransitionEndRunnable();
+        mLaunchTransitionFadingAway = false;
+        mScrimController.forceHideScrims(false /* hide */, false /* animated */);
+        updateMediaMetaData(true /* metaDataChanged */, true);
+    }
+
+    public boolean isCollapsing() {
+        return mNotificationPanel.isCollapsing();
+    }
+
+    public void addPostCollapseAction(Runnable r) {
+        mPostCollapseRunnables.add(r);
+    }
+
+    public boolean isInLaunchTransition() {
+        return mNotificationPanel.isLaunchTransitionRunning()
+                || mNotificationPanel.isLaunchTransitionFinished();
+    }
+
+    /**
+     * Fades the content of the keyguard away after the launch transition is done.
+     *
+     * @param beforeFading the runnable to be run when the circle is fully expanded and the fading
+     *                     starts
+     * @param endRunnable the runnable to be run when the transition is done
+     */
+    public void fadeKeyguardAfterLaunchTransition(final Runnable beforeFading,
+            Runnable endRunnable) {
+        mHandler.removeMessages(MSG_LAUNCH_TRANSITION_TIMEOUT);
+        mLaunchTransitionEndRunnable = endRunnable;
+        Runnable hideRunnable = new Runnable() {
+            @Override
+            public void run() {
+                mLaunchTransitionFadingAway = true;
+                if (beforeFading != null) {
+                    beforeFading.run();
+                }
+                mScrimController.forceHideScrims(true /* hide */, false /* animated */);
+                updateMediaMetaData(false, true);
+                mNotificationPanel.setAlpha(1);
+                mStackScroller.setParentNotFullyVisible(true);
+                mNotificationPanel.animate()
+                        .alpha(0)
+                        .setStartDelay(FADE_KEYGUARD_START_DELAY)
+                        .setDuration(FADE_KEYGUARD_DURATION)
+                        .withLayer()
+                        .withEndAction(new Runnable() {
+                            @Override
+                            public void run() {
+                                onLaunchTransitionFadingEnded();
+                            }
+                        });
+                mCommandQueue.appTransitionStarting(SystemClock.uptimeMillis(),
+                        LightBarTransitionsController.DEFAULT_TINT_ANIMATION_DURATION, true);
+            }
+        };
+        if (mNotificationPanel.isLaunchTransitionRunning()) {
+            mNotificationPanel.setLaunchTransitionEndRunnable(hideRunnable);
+        } else {
+            hideRunnable.run();
+        }
+    }
+
+    /**
+     * Fades the content of the Keyguard while we are dozing and makes it invisible when finished
+     * fading.
+     */
+    public void fadeKeyguardWhilePulsing() {
+        mNotificationPanel.notifyStartFading();
+        mNotificationPanel.animate()
+                .alpha(0f)
+                .setStartDelay(0)
+                .setDuration(FADE_KEYGUARD_DURATION_PULSING)
+                .setInterpolator(ScrimController.KEYGUARD_FADE_OUT_INTERPOLATOR)
+                .start();
+    }
+
+    /**
+     * Plays the animation when an activity that was occluding Keyguard goes away.
+     */
+    public void animateKeyguardUnoccluding() {
+        mScrimController.animateKeyguardUnoccluding(500);
+        mNotificationPanel.setExpandedFraction(0f);
+        animateExpandNotificationsPanel();
+    }
+
+    /**
+     * Starts the timeout when we try to start the affordances on Keyguard. We usually rely that
+     * Keyguard goes away via fadeKeyguardAfterLaunchTransition, however, that might not happen
+     * because the launched app crashed or something else went wrong.
+     */
+    public void startLaunchTransitionTimeout() {
+        mHandler.sendEmptyMessageDelayed(MSG_LAUNCH_TRANSITION_TIMEOUT,
+                LAUNCH_TRANSITION_TIMEOUT_MS);
+    }
+
+    private void onLaunchTransitionTimeout() {
+        Log.w(TAG, "Launch transition: Timeout!");
+        mNotificationPanel.onAffordanceLaunchEnded();
+        releaseGestureWakeLock();
+        mNotificationPanel.resetViews();
+    }
+
+    private void runLaunchTransitionEndRunnable() {
+        if (mLaunchTransitionEndRunnable != null) {
+            Runnable r = mLaunchTransitionEndRunnable;
+
+            // mLaunchTransitionEndRunnable might call showKeyguard, which would execute it again,
+            // which would lead to infinite recursion. Protect against it.
+            mLaunchTransitionEndRunnable = null;
+            r.run();
+        }
+    }
+
+    /**
+     * @return true if we would like to stay in the shade, false if it should go away entirely
+     */
+    public boolean hideKeyguardImpl() {
+        mIsKeyguard = false;
+        Trace.beginSection("StatusBar#hideKeyguard");
+        boolean staying = mLeaveOpenOnKeyguardHide;
+        setBarState(StatusBarState.SHADE);
+        View viewToClick = null;
+        if (mLeaveOpenOnKeyguardHide) {
+            if (!mKeyguardRequested) {
+                mLeaveOpenOnKeyguardHide = false;
+            }
+            long delay = calculateGoingToFullShadeDelay();
+            mNotificationPanel.animateToFullShade(delay);
+            if (mDraggedDownRow != null) {
+                mDraggedDownRow.setUserLocked(false);
+                mDraggedDownRow = null;
+            }
+            if (!mKeyguardRequested) {
+                viewToClick = mPendingRemoteInputView;
+                mPendingRemoteInputView = null;
+            }
+
+            // Disable layout transitions in navbar for this transition because the load is just
+            // too heavy for the CPU and GPU on any device.
+            if (mNavigationBar != null) {
+                mNavigationBar.disableAnimationsDuringHide(delay);
+            }
+        } else if (!mNotificationPanel.isCollapsing()) {
+            instantCollapseNotificationPanel();
+        }
+        updateKeyguardState(staying, false /* fromShadeLocked */);
+
+        if (viewToClick != null && viewToClick.isAttachedToWindow()) {
+            viewToClick.callOnClick();
+        }
+
+        // Keyguard state has changed, but QS is not listening anymore. Make sure to update the tile
+        // visibilities so next time we open the panel we know the correct height already.
+        if (mQSPanel != null) {
+            mQSPanel.refreshAllTiles();
+        }
+        mHandler.removeMessages(MSG_LAUNCH_TRANSITION_TIMEOUT);
+        releaseGestureWakeLock();
+        mNotificationPanel.onAffordanceLaunchEnded();
+        mNotificationPanel.animate().cancel();
+        mNotificationPanel.setAlpha(1f);
+        Trace.endSection();
+        return staying;
+    }
+
+    private void releaseGestureWakeLock() {
+        if (mGestureWakeLock.isHeld()) {
+            mGestureWakeLock.release();
+        }
+    }
+
+    public long calculateGoingToFullShadeDelay() {
+        return mKeyguardFadingAwayDelay + mKeyguardFadingAwayDuration;
+    }
+
+    /**
+     * Notifies the status bar that Keyguard is going away very soon.
+     */
+    public void keyguardGoingAway() {
+
+        // Treat Keyguard exit animation as an app transition to achieve nice transition for status
+        // bar.
+        mKeyguardGoingAway = true;
+        mKeyguardMonitor.notifyKeyguardGoingAway(true);
+        mCommandQueue.appTransitionPending(true);
+    }
+
+    /**
+     * Notifies the status bar the Keyguard is fading away with the specified timings.
+     *
+     * @param startTime the start time of the animations in uptime millis
+     * @param delay the precalculated animation delay in miliseconds
+     * @param fadeoutDuration the duration of the exit animation, in milliseconds
+     */
+    public void setKeyguardFadingAway(long startTime, long delay, long fadeoutDuration) {
+        mKeyguardFadingAway = true;
+        mKeyguardFadingAwayDelay = delay;
+        mKeyguardFadingAwayDuration = fadeoutDuration;
+        mWaitingForKeyguardExit = false;
+        mCommandQueue.appTransitionStarting(startTime + fadeoutDuration
+                        - LightBarTransitionsController.DEFAULT_TINT_ANIMATION_DURATION,
+                LightBarTransitionsController.DEFAULT_TINT_ANIMATION_DURATION, true);
+        recomputeDisableFlags(fadeoutDuration > 0 /* animate */);
+        mCommandQueue.appTransitionStarting(
+                    startTime - LightBarTransitionsController.DEFAULT_TINT_ANIMATION_DURATION,
+                    LightBarTransitionsController.DEFAULT_TINT_ANIMATION_DURATION, true);
+        mKeyguardMonitor.notifyKeyguardFadingAway(delay, fadeoutDuration);
+    }
+
+    public boolean isKeyguardFadingAway() {
+        return mKeyguardFadingAway;
+    }
+
+    /**
+     * Notifies that the Keyguard fading away animation is done.
+     */
+    public void finishKeyguardFadingAway() {
+        mKeyguardFadingAway = false;
+        mKeyguardGoingAway = false;
+        mKeyguardMonitor.notifyKeyguardDoneFading();
+    }
+
+    public void stopWaitingForKeyguardExit() {
+        mWaitingForKeyguardExit = false;
+    }
+
+    private void updatePublicMode() {
+        final boolean showingKeyguard = mStatusBarKeyguardViewManager.isShowing();
+        final boolean devicePublic = showingKeyguard
+                && mStatusBarKeyguardViewManager.isSecure(mCurrentUserId);
+
+        // Look for public mode users. Users are considered public in either case of:
+        //   - device keyguard is shown in secure mode;
+        //   - profile is locked with a work challenge.
+        for (int i = mCurrentProfiles.size() - 1; i >= 0; i--) {
+            final int userId = mCurrentProfiles.valueAt(i).id;
+            boolean isProfilePublic = devicePublic;
+            if (!devicePublic && userId != mCurrentUserId) {
+                // We can't rely on KeyguardManager#isDeviceLocked() for unified profile challenge
+                // due to a race condition where this code could be called before
+                // TrustManagerService updates its internal records, resulting in an incorrect
+                // state being cached in mLockscreenPublicMode. (b/35951989)
+                if (mLockPatternUtils.isSeparateProfileChallengeEnabled(userId)
+                        && mStatusBarKeyguardViewManager.isSecure(userId)) {
+                    isProfilePublic = mKeyguardManager.isDeviceLocked(userId);
+                }
+            }
+            setLockscreenPublicMode(isProfilePublic, userId);
+        }
+    }
+
+    protected void updateKeyguardState(boolean goingToFullShade, boolean fromShadeLocked) {
+        Trace.beginSection("StatusBar#updateKeyguardState");
+        if (mState == StatusBarState.KEYGUARD) {
+            mKeyguardIndicationController.setVisible(true);
+            mNotificationPanel.resetViews();
+            if (mKeyguardUserSwitcher != null) {
+                mKeyguardUserSwitcher.setKeyguard(true, fromShadeLocked);
+            }
+            if (mStatusBarView != null) mStatusBarView.removePendingHideExpandedRunnables();
+            if (mAmbientIndicationContainer != null) {
+                mAmbientIndicationContainer.setVisibility(View.VISIBLE);
+            }
+        } else {
+            mKeyguardIndicationController.setVisible(false);
+            if (mKeyguardUserSwitcher != null) {
+                mKeyguardUserSwitcher.setKeyguard(false,
+                        goingToFullShade ||
+                        mState == StatusBarState.SHADE_LOCKED ||
+                        fromShadeLocked);
+            }
+            if (mAmbientIndicationContainer != null) {
+                mAmbientIndicationContainer.setVisibility(View.INVISIBLE);
+            }
+        }
+        if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) {
+            mScrimController.setKeyguardShowing(true);
+        } else {
+            mScrimController.setKeyguardShowing(false);
+        }
+        mNotificationPanel.setBarState(mState, mKeyguardFadingAway, goingToFullShade);
+        updateTheme();
+        updateDozingState();
+        updatePublicMode();
+        updateStackScrollerState(goingToFullShade, fromShadeLocked);
+        updateNotifications();
+        checkBarModes();
+        updateMediaMetaData(false, mState != StatusBarState.KEYGUARD);
+        mKeyguardMonitor.notifyKeyguardState(mStatusBarKeyguardViewManager.isShowing(),
+                mUnlockMethodCache.isMethodSecure(),
+                mStatusBarKeyguardViewManager.isOccluded());
+        Trace.endSection();
+    }
+
+    /**
+     * Switches theme from light to dark and vice-versa.
+     */
+    protected void updateTheme() {
+        final boolean inflated = mStackScroller != null;
+
+        // The system wallpaper defines if QS should be light or dark.
+        WallpaperColors systemColors = mColorExtractor
+                .getWallpaperColors(WallpaperManager.FLAG_SYSTEM);
+        final boolean useDarkTheme = systemColors != null
+                && (systemColors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_THEME) != 0;
+        if (isUsingDarkTheme() != useDarkTheme) {
+            try {
+                mOverlayManager.setEnabled("com.android.systemui.theme.dark",
+                        useDarkTheme, mCurrentUserId);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Can't change theme", e);
+            }
+        }
+
+        // Lock wallpaper defines the color of the majority of the views, hence we'll use it
+        // to set our default theme.
+        final boolean lockDarkText = mColorExtractor.getColors(WallpaperManager.FLAG_LOCK, true
+                /* ignoreVisibility */).supportsDarkText();
+        final int themeResId = lockDarkText ? R.style.Theme_SystemUI_Light : R.style.Theme_SystemUI;
+        if (mContext.getThemeResId() != themeResId) {
+            mContext.setTheme(themeResId);
+            if (inflated) {
+                reinflateViews();
+            }
+        }
+
+        if (inflated) {
+            int which;
+            if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) {
+                which = WallpaperManager.FLAG_LOCK;
+            } else {
+                which = WallpaperManager.FLAG_SYSTEM;
+            }
+            final boolean useDarkText = mColorExtractor.getColors(which,
+                    true /* ignoreVisibility */).supportsDarkText();
+            mStackScroller.updateDecorViews(useDarkText);
+
+            // Make sure we have the correct navbar/statusbar colors.
+            mStatusBarWindowManager.setKeyguardDark(useDarkText);
+        }
+    }
+
+    private void updateDozingState() {
+        Trace.traceCounter(Trace.TRACE_TAG_APP, "dozing", mDozing ? 1 : 0);
+        Trace.beginSection("StatusBar#updateDozingState");
+        boolean animate = !mDozing && mDozeServiceHost.shouldAnimateWakeup();
+        mNotificationPanel.setDozing(mDozing, animate);
+        mStackScroller.setDark(mDozing, animate, mWakeUpTouchLocation);
+        mScrimController.setDozing(mDozing);
+        mKeyguardIndicationController.setDozing(mDozing);
+        mNotificationPanel.setDark(mDozing, animate);
+        updateQsExpansionEnabled();
+        mDozeScrimController.setDozing(mDozing, animate);
+        updateRowStates();
+        Trace.endSection();
+    }
+
+    public void updateStackScrollerState(boolean goingToFullShade, boolean fromShadeLocked) {
+        if (mStackScroller == null) return;
+        boolean onKeyguard = mState == StatusBarState.KEYGUARD;
+        boolean publicMode = isAnyProfilePublicMode();
+        mStackScroller.setHideSensitive(publicMode, goingToFullShade);
+        mStackScroller.setDimmed(onKeyguard, fromShadeLocked /* animate */);
+        mStackScroller.setExpandingEnabled(!onKeyguard);
+        ActivatableNotificationView activatedChild = mStackScroller.getActivatedChild();
+        mStackScroller.setActivatedChild(null);
+        if (activatedChild != null) {
+            activatedChild.makeInactive(false /* animate */);
+        }
+    }
+
+    public void userActivity() {
+        if (mState == StatusBarState.KEYGUARD) {
+            mKeyguardViewMediatorCallback.userActivity();
+        }
+    }
+
+    public boolean interceptMediaKey(KeyEvent event) {
+        return mState == StatusBarState.KEYGUARD
+                && mStatusBarKeyguardViewManager.interceptMediaKey(event);
+    }
+
+    protected boolean shouldUnlockOnMenuPressed() {
+        return mDeviceInteractive && mState != StatusBarState.SHADE
+            && mStatusBarKeyguardViewManager.shouldDismissOnMenuPressed();
+    }
+
+    public boolean onMenuPressed() {
+        if (shouldUnlockOnMenuPressed()) {
+            animateCollapsePanels(
+                    CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL /* flags */, true /* force */);
+            return true;
+        }
+        return false;
+    }
+
+    public void endAffordanceLaunch() {
+        releaseGestureWakeLock();
+        mNotificationPanel.onAffordanceLaunchEnded();
+    }
+
+    public boolean onBackPressed() {
+        if (mStatusBarKeyguardViewManager.onBackPressed()) {
+            return true;
+        }
+        if (mNotificationPanel.isQsExpanded()) {
+            if (mNotificationPanel.isQsDetailShowing()) {
+                mNotificationPanel.closeQsDetail();
+            } else {
+                mNotificationPanel.animateCloseQs();
+            }
+            return true;
+        }
+        if (mState != StatusBarState.KEYGUARD && mState != StatusBarState.SHADE_LOCKED) {
+            animateCollapsePanels();
+            return true;
+        }
+        if (mKeyguardUserSwitcher != null && mKeyguardUserSwitcher.hideIfNotSimple(true)) {
+            return true;
+        }
+        return false;
+    }
+
+    public boolean onSpacePressed() {
+        if (mDeviceInteractive && mState != StatusBarState.SHADE) {
+            animateCollapsePanels(
+                    CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL /* flags */, true /* force */);
+            return true;
+        }
+        return false;
+    }
+
+    private void showBouncerIfKeyguard() {
+        if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) {
+            showBouncer();
+        }
+    }
+
+    protected void showBouncer() {
+        mWaitingForKeyguardExit = mStatusBarKeyguardViewManager.isShowing();
+        mStatusBarKeyguardViewManager.dismiss();
+    }
+
+    private void instantExpandNotificationsPanel() {
+        // Make our window larger and the panel expanded.
+        makeExpandedVisible(true);
+        mNotificationPanel.expand(false /* animate */);
+        recomputeDisableFlags(false /* animate */);
+    }
+
+    private void instantCollapseNotificationPanel() {
+        mNotificationPanel.instantCollapse();
+    }
+
+    @Override
+    public void onActivated(ActivatableNotificationView view) {
+        onActivated((View)view);
+        mStackScroller.setActivatedChild(view);
+    }
+
+    public void onActivated(View view) {
+        mLockscreenGestureLogger.write(
+                MetricsEvent.ACTION_LS_NOTE,
+                0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */);
+        mKeyguardIndicationController.showTransientIndication(R.string.notification_tap_again);
+        ActivatableNotificationView previousView = mStackScroller.getActivatedChild();
+        if (previousView != null) {
+            previousView.makeInactive(true /* animate */);
+        }
+    }
+
+    /**
+     * @param state The {@link StatusBarState} to set.
+     */
+    public void setBarState(int state) {
+        // If we're visible and switched to SHADE_LOCKED (the user dragged
+        // down on the lockscreen), clear notification LED, vibration,
+        // ringing.
+        // Other transitions are covered in handleVisibleToUserChanged().
+        if (state != mState && mVisible && (state == StatusBarState.SHADE_LOCKED
+                || (state == StatusBarState.SHADE && isGoingToNotificationShade()))) {
+            clearNotificationEffects();
+        }
+        if (state == StatusBarState.KEYGUARD) {
+            removeRemoteInputEntriesKeptUntilCollapsed();
+            maybeEscalateHeadsUp();
+        }
+        mState = state;
+        mGroupManager.setStatusBarState(state);
+        mHeadsUpManager.setStatusBarState(state);
+        mFalsingManager.setStatusBarState(state);
+        mStatusBarWindowManager.setStatusBarState(state);
+        mStackScroller.setStatusBarState(state);
+        updateReportRejectedTouchVisibility();
+        updateDozing();
+        updateTheme();
+        touchAutoDim();
+        mNotificationShelf.setStatusBarState(state);
+    }
+
+    @Override
+    public void onActivationReset(ActivatableNotificationView view) {
+        if (view == mStackScroller.getActivatedChild()) {
+            mStackScroller.setActivatedChild(null);
+            onActivationReset((View)view);
+        }
+    }
+
+    public void onActivationReset(View view) {
+        mKeyguardIndicationController.hideTransientIndication();
+    }
+
+    public void onTrackingStarted() {
+        runPostCollapseRunnables();
+    }
+
+    public void onClosingFinished() {
+        runPostCollapseRunnables();
+        if (!isPanelFullyCollapsed()) {
+            // if we set it not to be focusable when collapsing, we have to undo it when we aborted
+            // the closing
+            mStatusBarWindowManager.setStatusBarFocusable(true);
+        }
+    }
+
+    public void onUnlockHintStarted() {
+        mFalsingManager.onUnlockHintStarted();
+        mKeyguardIndicationController.showTransientIndication(R.string.keyguard_unlock);
+    }
+
+    public void onHintFinished() {
+        // Delay the reset a bit so the user can read the text.
+        mKeyguardIndicationController.hideTransientIndicationDelayed(HINT_RESET_DELAY_MS);
+    }
+
+    public void onCameraHintStarted() {
+        mFalsingManager.onCameraHintStarted();
+        mKeyguardIndicationController.showTransientIndication(R.string.camera_hint);
+    }
+
+    public void onVoiceAssistHintStarted() {
+        mFalsingManager.onLeftAffordanceHintStarted();
+        mKeyguardIndicationController.showTransientIndication(R.string.voice_hint);
+    }
+
+    public void onPhoneHintStarted() {
+        mFalsingManager.onLeftAffordanceHintStarted();
+        mKeyguardIndicationController.showTransientIndication(R.string.phone_hint);
+    }
+
+    public void onTrackingStopped(boolean expand) {
+        if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) {
+            if (!expand && !mUnlockMethodCache.canSkipBouncer()) {
+                showBouncerIfKeyguard();
+            }
+        }
+    }
+
+    protected int getMaxKeyguardNotifications(boolean recompute) {
+        if (recompute) {
+            mMaxKeyguardNotifications = Math.max(1,
+                    mNotificationPanel.computeMaxKeyguardNotifications(
+                            mMaxAllowedKeyguardNotifications));
+            return mMaxKeyguardNotifications;
+        }
+        return mMaxKeyguardNotifications;
+    }
+
+    public int getMaxKeyguardNotifications() {
+        return getMaxKeyguardNotifications(false /* recompute */);
+    }
+
+    // TODO: Figure out way to remove these.
+    public NavigationBarView getNavigationBarView() {
+        return (mNavigationBar != null ? (NavigationBarView) mNavigationBar.getView() : null);
+    }
+
+    public View getNavigationBarWindow() {
+        return mNavigationBarView;
+    }
+
+    /**
+     * TODO: Remove this method. Views should not be passed forward. Will cause theme issues.
+     * @return bottom area view
+     */
+    public KeyguardBottomAreaView getKeyguardBottomAreaView() {
+        return mNotificationPanel.getKeyguardBottomAreaView();
+    }
+
+    // ---------------------- DragDownHelper.OnDragDownListener ------------------------------------
+
+
+    /* Only ever called as a consequence of a lockscreen expansion gesture. */
+    @Override
+    public boolean onDraggedDown(View startingChild, int dragLengthY) {
+        if (mState == StatusBarState.KEYGUARD
+                && hasActiveNotifications() && (!isDozing() || isPulsing())) {
+            mLockscreenGestureLogger.write(
+                    MetricsEvent.ACTION_LS_SHADE,
+                    (int) (dragLengthY / mDisplayMetrics.density),
+                    0 /* velocityDp - N/A */);
+
+            // We have notifications, go to locked shade.
+            goToLockedShade(startingChild);
+            if (startingChild instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) startingChild;
+                row.onExpandedByGesture(true /* drag down is always an open */);
+            }
+            return true;
+        } else {
+            // abort gesture.
+            return false;
+        }
+    }
+
+    @Override
+    public void onDragDownReset() {
+        mStackScroller.setDimmed(true /* dimmed */, true /* animated */);
+        mStackScroller.resetScrollPosition();
+        mStackScroller.resetCheckSnoozeLeavebehind();
+    }
+
+    @Override
+    public void onCrossedThreshold(boolean above) {
+        mStackScroller.setDimmed(!above /* dimmed */, true /* animate */);
+    }
+
+    @Override
+    public void onTouchSlopExceeded() {
+        mStackScroller.removeLongPressCallback();
+        mStackScroller.checkSnoozeLeavebehind();
+    }
+
+    @Override
+    public void setEmptyDragAmount(float amount) {
+        mNotificationPanel.setEmptyDragAmount(amount);
+    }
+
+    @Override
+    public boolean isFalsingCheckNeeded() {
+        return mState == StatusBarState.KEYGUARD;
+    }
+
+    /**
+     * If secure with redaction: Show bouncer, go to unlocked shade.
+     *
+     * <p>If secure without redaction or no security: Go to {@link StatusBarState#SHADE_LOCKED}.</p>
+     *
+     * @param expandView The view to expand after going to the shade.
+     */
+    public void goToLockedShade(View expandView) {
+        int userId = mCurrentUserId;
+        ExpandableNotificationRow row = null;
+        if (expandView instanceof ExpandableNotificationRow) {
+            row = (ExpandableNotificationRow) expandView;
+            row.setUserExpanded(true /* userExpanded */, true /* allowChildExpansion */);
+            // Indicate that the group expansion is changing at this time -- this way the group
+            // and children backgrounds / divider animations will look correct.
+            row.setGroupExpansionChanging(true);
+            if (row.getStatusBarNotification() != null) {
+                userId = row.getStatusBarNotification().getUserId();
+            }
+        }
+        boolean fullShadeNeedsBouncer = !userAllowsPrivateNotificationsInPublic(mCurrentUserId)
+                || !mShowLockscreenNotifications || mFalsingManager.shouldEnforceBouncer();
+        if (isLockscreenPublicMode(userId) && fullShadeNeedsBouncer) {
+            mLeaveOpenOnKeyguardHide = true;
+            showBouncerIfKeyguard();
+            mDraggedDownRow = row;
+            mPendingRemoteInputView = null;
+        } else {
+            mNotificationPanel.animateToFullShade(0 /* delay */);
+            setBarState(StatusBarState.SHADE_LOCKED);
+            updateKeyguardState(false /* goingToFullShade */, false /* fromShadeLocked */);
+        }
+    }
+
+    public void onLockedNotificationImportanceChange(OnDismissAction dismissAction) {
+        mLeaveOpenOnKeyguardHide = true;
+        dismissKeyguardThenExecute(dismissAction, true /* afterKeyguardGone */);
+    }
+
+    protected void onLockedRemoteInput(ExpandableNotificationRow row, View clicked) {
+        mLeaveOpenOnKeyguardHide = true;
+        showBouncer();
+        mPendingRemoteInputView = clicked;
+    }
+
+    protected void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row,
+            View clickedView) {
+        if (isKeyguardShowing()) {
+            onLockedRemoteInput(row, clickedView);
+        } else {
+            row.setUserExpanded(true);
+            row.getPrivateLayout().setOnExpandedVisibleListener(clickedView::performClick);
+        }
+    }
+
+    protected boolean startWorkChallengeIfNecessary(int userId, IntentSender intendSender,
+            String notificationKey) {
+        // Clear pending remote view, as we do not want to trigger pending remote input view when
+        // it's called by other code
+        mPendingWorkRemoteInputView = null;
+        // Begin old BaseStatusBar.startWorkChallengeIfNecessary.
+        final Intent newIntent = mKeyguardManager.createConfirmDeviceCredentialIntent(null,
+                null, userId);
+        if (newIntent == null) {
+            return false;
+        }
+        final Intent callBackIntent = new Intent(NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION);
+        callBackIntent.putExtra(Intent.EXTRA_INTENT, intendSender);
+        callBackIntent.putExtra(Intent.EXTRA_INDEX, notificationKey);
+        callBackIntent.setPackage(mContext.getPackageName());
+
+        PendingIntent callBackPendingIntent = PendingIntent.getBroadcast(
+                mContext,
+                0,
+                callBackIntent,
+                PendingIntent.FLAG_CANCEL_CURRENT |
+                        PendingIntent.FLAG_ONE_SHOT |
+                        PendingIntent.FLAG_IMMUTABLE);
+        newIntent.putExtra(
+                Intent.EXTRA_INTENT,
+                callBackPendingIntent.getIntentSender());
+        try {
+            ActivityManager.getService().startConfirmDeviceCredentialIntent(newIntent,
+                    null /*options*/);
+        } catch (RemoteException ex) {
+            // ignore
+        }
+        return true;
+        // End old BaseStatusBar.startWorkChallengeIfNecessary.
+    }
+
+    protected void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row,
+            View clicked) {
+        // Collapse notification and show work challenge
+        animateCollapsePanels();
+        startWorkChallengeIfNecessary(userId, null, null);
+        // Add pending remote input view after starting work challenge, as starting work challenge
+        // will clear all previous pending review view
+        mPendingWorkRemoteInputView = clicked;
+    }
+
+    private boolean isAnyProfilePublicMode() {
+        for (int i = mCurrentProfiles.size() - 1; i >= 0; i--) {
+            if (isLockscreenPublicMode(mCurrentProfiles.valueAt(i).id)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    protected void onWorkChallengeChanged() {
+        updatePublicMode();
+        updateNotifications();
+        if (mPendingWorkRemoteInputView != null && !isAnyProfilePublicMode()) {
+            // Expand notification panel and the notification row, then click on remote input view
+            final Runnable clickPendingViewRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    final View pendingWorkRemoteInputView = mPendingWorkRemoteInputView;
+                    if (pendingWorkRemoteInputView == null) {
+                        return;
+                    }
+
+                    // Climb up the hierarchy until we get to the container for this row.
+                    ViewParent p = pendingWorkRemoteInputView.getParent();
+                    while (!(p instanceof ExpandableNotificationRow)) {
+                        if (p == null) {
+                            return;
+                        }
+                        p = p.getParent();
+                    }
+
+                    final ExpandableNotificationRow row = (ExpandableNotificationRow) p;
+                    ViewParent viewParent = row.getParent();
+                    if (viewParent instanceof NotificationStackScrollLayout) {
+                        final NotificationStackScrollLayout scrollLayout =
+                                (NotificationStackScrollLayout) viewParent;
+                        row.makeActionsVisibile();
+                        row.post(new Runnable() {
+                            @Override
+                            public void run() {
+                                final Runnable finishScrollingCallback = new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        mPendingWorkRemoteInputView.callOnClick();
+                                        mPendingWorkRemoteInputView = null;
+                                        scrollLayout.setFinishScrollingCallback(null);
+                                    }
+                                };
+                                if (scrollLayout.scrollTo(row)) {
+                                    // It scrolls! So call it when it's finished.
+                                    scrollLayout.setFinishScrollingCallback(
+                                            finishScrollingCallback);
+                                } else {
+                                    // It does not scroll, so call it now!
+                                    finishScrollingCallback.run();
+                                }
+                            }
+                        });
+                    }
+                }
+            };
+            mNotificationPanel.getViewTreeObserver().addOnGlobalLayoutListener(
+                    new ViewTreeObserver.OnGlobalLayoutListener() {
+                        @Override
+                        public void onGlobalLayout() {
+                            if (mNotificationPanel.mStatusBar.getStatusBarWindow()
+                                    .getHeight() != mNotificationPanel.mStatusBar
+                                            .getStatusBarHeight()) {
+                                mNotificationPanel.getViewTreeObserver()
+                                        .removeOnGlobalLayoutListener(this);
+                                mNotificationPanel.post(clickPendingViewRunnable);
+                            }
+                        }
+                    });
+            instantExpandNotificationsPanel();
+        }
+    }
+
+    @Override
+    public void onExpandClicked(Entry clickedEntry, boolean nowExpanded) {
+        mHeadsUpManager.setExpanded(clickedEntry, nowExpanded);
+        if (mState == StatusBarState.KEYGUARD && nowExpanded) {
+            goToLockedShade(clickedEntry.row);
+        }
+    }
+
+    /**
+     * Goes back to the keyguard after hanging around in {@link StatusBarState#SHADE_LOCKED}.
+     */
+    public void goToKeyguard() {
+        if (mState == StatusBarState.SHADE_LOCKED) {
+            mStackScroller.onGoToKeyguard();
+            setBarState(StatusBarState.KEYGUARD);
+            updateKeyguardState(false /* goingToFullShade */, true /* fromShadeLocked*/);
+        }
+    }
+
+    public long getKeyguardFadingAwayDelay() {
+        return mKeyguardFadingAwayDelay;
+    }
+
+    public long getKeyguardFadingAwayDuration() {
+        return mKeyguardFadingAwayDuration;
+    }
+
+    public void setBouncerShowing(boolean bouncerShowing) {
+        mBouncerShowing = bouncerShowing;
+        if (mStatusBarView != null) mStatusBarView.setBouncerShowing(bouncerShowing);
+        updateHideIconsForBouncer(true /* animate */);
+        recomputeDisableFlags(true /* animate */);
+    }
+
+    public void cancelCurrentTouch() {
+        if (mNotificationPanel.isTracking()) {
+            mStatusBarWindow.cancelCurrentTouch();
+            if (mState == StatusBarState.SHADE) {
+                animateCollapsePanels();
+            }
+        }
+    }
+
+    WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() {
+        @Override
+        public void onFinishedGoingToSleep() {
+            mNotificationPanel.onAffordanceLaunchEnded();
+            releaseGestureWakeLock();
+            mLaunchCameraOnScreenTurningOn = false;
+            mDeviceInteractive = false;
+            mWakeUpComingFromTouch = false;
+            mWakeUpTouchLocation = null;
+            mStackScroller.setAnimationsEnabled(false);
+            mVisualStabilityManager.setScreenOn(false);
+            updateVisibleToUser();
+
+            // We need to disable touch events because these might
+            // collapse the panel after we expanded it, and thus we would end up with a blank
+            // Keyguard.
+            mNotificationPanel.setTouchDisabled(true);
+            mStatusBarWindow.cancelCurrentTouch();
+            if (mLaunchCameraOnFinishedGoingToSleep) {
+                mLaunchCameraOnFinishedGoingToSleep = false;
+
+                // This gets executed before we will show Keyguard, so post it in order that the state
+                // is correct.
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        onCameraLaunchGestureDetected(mLastCameraLaunchSource);
+                    }
+                });
+            }
+            updateIsKeyguard();
+        }
+
+        @Override
+        public void onStartedGoingToSleep() {
+            notifyHeadsUpGoingToSleep();
+            dismissVolumeDialog();
+        }
+
+        @Override
+        public void onStartedWakingUp() {
+            mDeviceInteractive = true;
+            mStackScroller.setAnimationsEnabled(true);
+            mVisualStabilityManager.setScreenOn(true);
+            mNotificationPanel.setTouchDisabled(false);
+
+            maybePrepareWakeUpFromAod();
+
+            mDozeServiceHost.stopDozing();
+            updateVisibleToUser();
+            updateIsKeyguard();
+        }
+    };
+
+    ScreenLifecycle.Observer mScreenObserver = new ScreenLifecycle.Observer() {
+        @Override
+        public void onScreenTurningOn() {
+            mFalsingManager.onScreenTurningOn();
+            mNotificationPanel.onScreenTurningOn();
+
+            maybePrepareWakeUpFromAod();
+
+            if (mLaunchCameraOnScreenTurningOn) {
+                mNotificationPanel.launchCamera(false, mLastCameraLaunchSource);
+                mLaunchCameraOnScreenTurningOn = false;
+            }
+        }
+
+        @Override
+        public void onScreenTurnedOn() {
+            mScrimController.wakeUpFromAod();
+            mDozeScrimController.onScreenTurnedOn();
+        }
+
+        @Override
+        public void onScreenTurnedOff() {
+            mFalsingManager.onScreenOff();
+            // If we pulse in from AOD, we turn the screen off first. However, updatingIsKeyguard
+            // in that case destroys the HeadsUpManager state, so don't do it in that case.
+            if (!isPulsing()) {
+                updateIsKeyguard();
+            }
+        }
+    };
+
+    public int getWakefulnessState() {
+        return mWakefulnessLifecycle.getWakefulness();
+    }
+
+    private void maybePrepareWakeUpFromAod() {
+        int wakefulness = mWakefulnessLifecycle.getWakefulness();
+        if (mDozing && wakefulness == WAKEFULNESS_WAKING && !isPulsing()) {
+            mScrimController.prepareWakeUpFromAod();
+        }
+    }
+
+    private void vibrateForCameraGesture() {
+        // Make sure to pass -1 for repeat so VibratorService doesn't stop us when going to sleep.
+        mVibrator.vibrate(mCameraLaunchGestureVibePattern, -1 /* repeat */);
+    }
+
+    /**
+     * @return true if the screen is currently fully off, i.e. has finished turning off and has
+     *         since not started turning on.
+     */
+    public boolean isScreenFullyOff() {
+        return mScreenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_OFF;
+    }
+
+    @Override
+    public void showScreenPinningRequest(int taskId) {
+        if (mKeyguardMonitor.isShowing()) {
+            // Don't allow apps to trigger this from keyguard.
+            return;
+        }
+        // Show screen pinning request, since this comes from an app, show 'no thanks', button.
+        showScreenPinningRequest(taskId, true);
+    }
+
+    public void showScreenPinningRequest(int taskId, boolean allowCancel) {
+        mScreenPinningRequest.showPrompt(taskId, allowCancel);
+    }
+
+    public boolean hasActiveNotifications() {
+        return !mNotificationData.getActiveNotifications().isEmpty();
+    }
+
+    public void wakeUpIfDozing(long time, View where) {
+        if (mDozing) {
+            PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+            pm.wakeUp(time, "com.android.systemui:NODOZE");
+            mWakeUpComingFromTouch = true;
+            where.getLocationInWindow(mTmpInt2);
+            mWakeUpTouchLocation = new PointF(mTmpInt2[0] + where.getWidth() / 2,
+                    mTmpInt2[1] + where.getHeight() / 2);
+            mStatusBarKeyguardViewManager.notifyDeviceWakeUpRequested();
+            mFalsingManager.onScreenOnFromTouch();
+        }
+    }
+
+    @Override
+    public void appTransitionCancelled() {
+        EventBus.getDefault().send(new AppTransitionFinishedEvent());
+    }
+
+    @Override
+    public void appTransitionFinished() {
+        EventBus.getDefault().send(new AppTransitionFinishedEvent());
+    }
+
+    @Override
+    public void onCameraLaunchGestureDetected(int source) {
+        mLastCameraLaunchSource = source;
+        if (isGoingToSleep()) {
+            if (DEBUG_CAMERA_LIFT) Slog.d(TAG, "Finish going to sleep before launching camera");
+            mLaunchCameraOnFinishedGoingToSleep = true;
+            return;
+        }
+        if (!mNotificationPanel.canCameraGestureBeLaunched(
+                mStatusBarKeyguardViewManager.isShowing() && mExpandedVisible)) {
+            if (DEBUG_CAMERA_LIFT) Slog.d(TAG, "Can't launch camera right now, mExpandedVisible: " +
+                    mExpandedVisible);
+            return;
+        }
+        if (!mDeviceInteractive) {
+            PowerManager pm = mContext.getSystemService(PowerManager.class);
+            pm.wakeUp(SystemClock.uptimeMillis(), "com.android.systemui:CAMERA_GESTURE");
+            mStatusBarKeyguardViewManager.notifyDeviceWakeUpRequested();
+        }
+        vibrateForCameraGesture();
+        if (!mStatusBarKeyguardViewManager.isShowing()) {
+            startActivityDismissingKeyguard(KeyguardBottomAreaView.INSECURE_CAMERA_INTENT,
+                    false /* onlyProvisioned */, true /* dismissShade */,
+                    true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */);
+        } else {
+            if (!mDeviceInteractive) {
+                // Avoid flickering of the scrim when we instant launch the camera and the bouncer
+                // comes on.
+                mScrimController.dontAnimateBouncerChangesUntilNextFrame();
+                mGestureWakeLock.acquire(LAUNCH_TRANSITION_TIMEOUT_MS + 1000L);
+            }
+            if (isScreenTurningOnOrOn()) {
+                if (DEBUG_CAMERA_LIFT) Slog.d(TAG, "Launching camera");
+                mNotificationPanel.launchCamera(mDeviceInteractive /* animate */, source);
+            } else {
+                // We need to defer the camera launch until the screen comes on, since otherwise
+                // we will dismiss us too early since we are waiting on an activity to be drawn and
+                // incorrectly get notified because of the screen on event (which resumes and pauses
+                // some activities)
+                if (DEBUG_CAMERA_LIFT) Slog.d(TAG, "Deferring until screen turns on");
+                mLaunchCameraOnScreenTurningOn = true;
+            }
+        }
+    }
+
+    boolean isCameraAllowedByAdmin() {
+        if (mDevicePolicyManager.getCameraDisabled(null, mCurrentUserId)) {
+            return false;
+        } else if (isKeyguardShowing() && isKeyguardSecure()) {
+            // Check if the admin has disabled the camera specifically for the keyguard
+            return (mDevicePolicyManager.getKeyguardDisabledFeatures(null, mCurrentUserId)
+                    & DevicePolicyManager.KEYGUARD_DISABLE_SECURE_CAMERA) == 0;
+        }
+
+        return true;
+    }
+
+    private boolean isGoingToSleep() {
+        return mWakefulnessLifecycle.getWakefulness()
+                == WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP;
+    }
+
+    private boolean isScreenTurningOnOrOn() {
+        return mScreenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_TURNING_ON
+                || mScreenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_ON;
+    }
+
+    public void notifyFpAuthModeChanged() {
+        updateDozing();
+    }
+
+    private void updateDozing() {
+        Trace.beginSection("StatusBar#updateDozing");
+        // When in wake-and-unlock while pulsing, keep dozing state until fully unlocked.
+        mDozing = mDozingRequested && mState == StatusBarState.KEYGUARD
+                || mFingerprintUnlockController.getMode()
+                        == FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING;
+        // When in wake-and-unlock we may not have received a change to mState
+        // but we still should not be dozing, manually set to false.
+        if (mFingerprintUnlockController.getMode() ==
+                FingerprintUnlockController.MODE_WAKE_AND_UNLOCK) {
+            mDozing = false;
+        }
+        mStatusBarWindowManager.setDozing(mDozing);
+        mStatusBarKeyguardViewManager.setDozing(mDozing);
+        if (mAmbientIndicationContainer instanceof DozeReceiver) {
+            ((DozeReceiver) mAmbientIndicationContainer).setDozing(mDozing);
+        }
+        updateDozingState();
+        Trace.endSection();
+    }
+
+    public boolean isKeyguardShowing() {
+        if (mStatusBarKeyguardViewManager == null) {
+            Slog.i(TAG, "isKeyguardShowing() called before startKeyguard(), returning true");
+            return true;
+        }
+        return mStatusBarKeyguardViewManager.isShowing();
+    }
+
+    private final class DozeServiceHost implements DozeHost {
+        private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
+        private boolean mAnimateWakeup;
+        private boolean mIgnoreTouchWhilePulsing;
+
+        @Override
+        public String toString() {
+            return "PSB.DozeServiceHost[mCallbacks=" + mCallbacks.size() + "]";
+        }
+
+        public void firePowerSaveChanged(boolean active) {
+            for (Callback callback : mCallbacks) {
+                callback.onPowerSaveChanged(active);
+            }
+        }
+
+        public void fireNotificationHeadsUp() {
+            for (Callback callback : mCallbacks) {
+                callback.onNotificationHeadsUp();
+            }
+        }
+
+        @Override
+        public void addCallback(@NonNull Callback callback) {
+            mCallbacks.add(callback);
+        }
+
+        @Override
+        public void removeCallback(@NonNull Callback callback) {
+            mCallbacks.remove(callback);
+        }
+
+        @Override
+        public void startDozing() {
+            if (!mDozingRequested) {
+                mDozingRequested = true;
+                DozeLog.traceDozing(mContext, mDozing);
+                updateDozing();
+                updateIsKeyguard();
+            }
+        }
+
+        @Override
+        public void pulseWhileDozing(@NonNull PulseCallback callback, int reason) {
+            if (reason == DozeLog.PULSE_REASON_SENSOR_LONG_PRESS) {
+                mPowerManager.wakeUp(SystemClock.uptimeMillis(), "com.android.systemui:NODOZE");
+                startAssist(new Bundle());
+                return;
+            }
+
+            mDozeScrimController.pulse(new PulseCallback() {
+
+                @Override
+                public void onPulseStarted() {
+                    callback.onPulseStarted();
+                    Collection<HeadsUpManager.HeadsUpEntry> pulsingEntries =
+                            mHeadsUpManager.getAllEntries();
+                    if (!pulsingEntries.isEmpty()) {
+                        // Only pulse the stack scroller if there's actually something to show.
+                        // Otherwise just show the always-on screen.
+                        setPulsing(pulsingEntries);
+                    }
+                }
+
+                @Override
+                public void onPulseFinished() {
+                    callback.onPulseFinished();
+                    setPulsing(null);
+                }
+
+                private void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> pulsing) {
+                    mStackScroller.setPulsing(pulsing);
+                    mNotificationPanel.setPulsing(pulsing != null);
+                    mVisualStabilityManager.setPulsing(pulsing != null);
+                    mIgnoreTouchWhilePulsing = false;
+                }
+            }, reason);
+        }
+
+        @Override
+        public void stopDozing() {
+            if (mDozingRequested) {
+                mDozingRequested = false;
+                DozeLog.traceDozing(mContext, mDozing);
+                updateDozing();
+            }
+        }
+
+        @Override
+        public void onIgnoreTouchWhilePulsing(boolean ignore) {
+            if (ignore != mIgnoreTouchWhilePulsing) {
+                DozeLog.tracePulseTouchDisabledByProx(mContext, ignore);
+            }
+            mIgnoreTouchWhilePulsing = ignore;
+            if (isDozing() && ignore) {
+                mStatusBarWindow.cancelCurrentTouch();
+            }
+        }
+
+        @Override
+        public void dozeTimeTick() {
+            mNotificationPanel.refreshTime();
+        }
+
+        @Override
+        public boolean isPowerSaveActive() {
+            return mBatteryController.isPowerSave();
+        }
+
+        @Override
+        public boolean isPulsingBlocked() {
+            return mFingerprintUnlockController.getMode()
+                    == FingerprintUnlockController.MODE_WAKE_AND_UNLOCK;
+        }
+
+        @Override
+        public boolean isProvisioned() {
+            return mDeviceProvisionedController.isDeviceProvisioned()
+                    && mDeviceProvisionedController.isCurrentUserSetup();
+        }
+
+        @Override
+        public boolean isBlockingDoze() {
+            if (mFingerprintUnlockController.hasPendingAuthentication()) {
+                Log.i(TAG, "Blocking AOD because fingerprint has authenticated");
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void startPendingIntentDismissingKeyguard(PendingIntent intent) {
+            StatusBar.this.startPendingIntentDismissingKeyguard(intent);
+        }
+
+        @Override
+        public void abortPulsing() {
+            mDozeScrimController.abortPulsing();
+        }
+
+        @Override
+        public void extendPulse() {
+            mDozeScrimController.extendPulse();
+        }
+
+        @Override
+        public void setAnimateWakeup(boolean animateWakeup) {
+            if (mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_AWAKE
+                    || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_WAKING) {
+                // Too late to change the wakeup animation.
+                return;
+            }
+            mAnimateWakeup = animateWakeup;
+        }
+
+        @Override
+        public void onDoubleTap(float screenX, float screenY) {
+            if (screenX > 0 && screenY > 0 && mAmbientIndicationContainer != null 
+                && mAmbientIndicationContainer.getVisibility() == View.VISIBLE) {
+                mAmbientIndicationContainer.getLocationOnScreen(mTmpInt2);
+                float viewX = screenX - mTmpInt2[0];
+                float viewY = screenY - mTmpInt2[1];
+                if (0 <= viewX && viewX <= mAmbientIndicationContainer.getWidth()
+                        && 0 <= viewY && viewY <= mAmbientIndicationContainer.getHeight()) {
+                    dispatchDoubleTap(viewX, viewY);
+                }
+            }
+        }
+
+        @Override
+        public void setDozeScreenBrightness(int value) {
+            mStatusBarWindowManager.setDozeScreenBrightness(value);
+        }
+
+        @Override
+        public void setAodDimmingScrim(float scrimOpacity) {
+            mDozeScrimController.setAodDimmingScrim(scrimOpacity);
+        }
+
+        public void dispatchDoubleTap(float viewX, float viewY) {
+            dispatchTap(mAmbientIndicationContainer, viewX, viewY);
+            dispatchTap(mAmbientIndicationContainer, viewX, viewY);
+        }
+
+        private void dispatchTap(View view, float x, float y) {
+            long now = SystemClock.elapsedRealtime();
+            dispatchTouchEvent(view, x, y, now, MotionEvent.ACTION_DOWN);
+            dispatchTouchEvent(view, x, y, now, MotionEvent.ACTION_UP);
+        }
+
+        private void dispatchTouchEvent(View view, float x, float y, long now, int action) {
+            MotionEvent ev = MotionEvent.obtain(now, now, action, x, y, 0 /* meta */);
+            view.dispatchTouchEvent(ev);
+            ev.recycle();
+        }
+
+        private boolean shouldAnimateWakeup() {
+            return mAnimateWakeup;
+        }
+    }
+
+    public boolean shouldIgnoreTouch() {
+        return isDozing() && mDozeServiceHost.mIgnoreTouchWhilePulsing;
+    }
+
+    // Begin Extra BaseStatusBar methods.
+
+    protected CommandQueue mCommandQueue;
+    protected IStatusBarService mBarService;
+
+    // all notifications
+    protected NotificationData mNotificationData;
+    protected NotificationStackScrollLayout mStackScroller;
+
+    protected NotificationGroupManager mGroupManager = new NotificationGroupManager();
+
+    protected RemoteInputController mRemoteInputController;
+
+    // for heads up notifications
+    protected HeadsUpManager mHeadsUpManager;
+
+    private AboveShelfObserver mAboveShelfObserver;
+
+    // handling reordering
+    protected VisualStabilityManager mVisualStabilityManager = new VisualStabilityManager();
+
+    protected int mCurrentUserId = 0;
+    final protected SparseArray<UserInfo> mCurrentProfiles = new SparseArray<UserInfo>();
+
+    protected int mLayoutDirection = -1; // invalid
+    protected AccessibilityManager mAccessibilityManager;
+
+    protected boolean mDeviceInteractive;
+
+    protected boolean mVisible;
+    protected ArraySet<Entry> mHeadsUpEntriesToRemoveOnSwitch = new ArraySet<>();
+    protected ArraySet<Entry> mRemoteInputEntriesToRemoveOnCollapse = new ArraySet<>();
+
+    /**
+     * Notifications with keys in this set are not actually around anymore. We kept them around
+     * when they were canceled in response to a remote input interaction. This allows us to show
+     * what you replied and allows you to continue typing into it.
+     */
+    protected ArraySet<String> mKeysKeptForRemoteInput = new ArraySet<>();
+
+    // mScreenOnFromKeyguard && mVisible.
+    private boolean mVisibleToUser;
+
+    private Locale mLocale;
+
+    protected boolean mUseHeadsUp = false;
+    protected boolean mHeadsUpTicker = false;
+    protected boolean mDisableNotificationAlerts = false;
+
+    protected DevicePolicyManager mDevicePolicyManager;
+    protected PowerManager mPowerManager;
+    protected StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+
+    // public mode, private notifications, etc
+    private final SparseBooleanArray mLockscreenPublicMode = new SparseBooleanArray();
+    private final SparseBooleanArray mUsersAllowingPrivateNotifications = new SparseBooleanArray();
+    private final SparseBooleanArray mUsersAllowingNotifications = new SparseBooleanArray();
+
+    private UserManager mUserManager;
+
+    protected KeyguardManager mKeyguardManager;
+    private LockPatternUtils mLockPatternUtils;
+    private DeviceProvisionedController mDeviceProvisionedController
+            = Dependency.get(DeviceProvisionedController.class);
+    protected SystemServicesProxy mSystemServicesProxy;
+
+    // UI-specific methods
+
+    protected WindowManager mWindowManager;
+    protected IWindowManager mWindowManagerService;
+
+    protected Display mDisplay;
+
+    protected RecentsComponent mRecents;
+
+    protected int mZenMode;
+
+    // which notification is currently being longpress-examined by the user
+    private NotificationGuts mNotificationGutsExposed;
+    private MenuItem mGutsMenuItem;
+
+    private KeyboardShortcuts mKeyboardShortcuts;
+
+    protected NotificationShelf mNotificationShelf;
+    protected DismissView mDismissView;
+    protected EmptyShadeView mEmptyShadeView;
+
+    private NotificationClicker mNotificationClicker = new NotificationClicker();
+
+    protected AssistManager mAssistManager;
+
+    protected boolean mVrMode;
+
+    private Set<String> mNonBlockablePkgs;
+
+    public boolean isDeviceInteractive() {
+        return mDeviceInteractive;
+    }
+
+    @Override  // NotificationData.Environment
+    public boolean isDeviceProvisioned() {
+        return mDeviceProvisionedController.isDeviceProvisioned();
+    }
+
+    private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
+        @Override
+        public void onVrStateChanged(boolean enabled) {
+            mVrMode = enabled;
+        }
+    };
+
+    public boolean isDeviceInVrMode() {
+        return mVrMode;
+    }
+
+    private final DeviceProvisionedListener mDeviceProvisionedListener =
+            new DeviceProvisionedListener() {
+        @Override
+        public void onDeviceProvisionedChanged() {
+            updateNotifications();
+        }
+    };
+
+    protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) {
+        @Override
+        public void onChange(boolean selfChange) {
+            final int mode = Settings.Global.getInt(mContext.getContentResolver(),
+                    Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_OFF);
+            setZenMode(mode);
+
+            updateLockscreenNotificationSetting();
+        }
+    };
+
+    private final ContentObserver mLockscreenSettingsObserver = new ContentObserver(mHandler) {
+        @Override
+        public void onChange(boolean selfChange) {
+            // We don't know which user changed LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS or
+            // LOCK_SCREEN_SHOW_NOTIFICATIONS, so we just dump our cache ...
+            mUsersAllowingPrivateNotifications.clear();
+            mUsersAllowingNotifications.clear();
+            // ... and refresh all the notifications
+            updateLockscreenNotificationSetting();
+            updateNotifications();
+        }
+    };
+
+    private RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() {
+
+        @Override
+        public boolean onClickHandler(
+                final View view, final PendingIntent pendingIntent, final Intent fillInIntent) {
+            wakeUpIfDozing(SystemClock.uptimeMillis(), view);
+
+
+            if (handleRemoteInput(view, pendingIntent, fillInIntent)) {
+                return true;
+            }
+
+            if (DEBUG) {
+                Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
+            }
+            logActionClick(view);
+            // The intent we are sending is for the application, which
+            // won't have permission to immediately start an activity after
+            // the user switches to home.  We know it is safe to do at this
+            // point, so make sure new activity switches are now allowed.
+            try {
+                ActivityManager.getService().resumeAppSwitches();
+            } catch (RemoteException e) {
+            }
+            final boolean isActivity = pendingIntent.isActivity();
+            if (isActivity) {
+                final boolean keyguardShowing = mStatusBarKeyguardViewManager.isShowing();
+                final boolean afterKeyguardGone = PreviewInflater.wouldLaunchResolverActivity(
+                        mContext, pendingIntent.getIntent(), mCurrentUserId);
+                dismissKeyguardThenExecute(new OnDismissAction() {
+                    @Override
+                    public boolean onDismiss() {
+                        try {
+                            ActivityManager.getService().resumeAppSwitches();
+                        } catch (RemoteException e) {
+                        }
+
+                        boolean handled = superOnClickHandler(view, pendingIntent, fillInIntent);
+
+                        // close the shade if it was open
+                        if (handled && !mNotificationPanel.isFullyCollapsed()) {
+                            animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL,
+                                    true /* force */);
+                            visibilityChanged(false);
+                            mAssistManager.hideAssist();
+
+                            // Wait for activity start.
+                            return true;
+                        } else {
+                            return false;
+                        }
+
+                    }
+                }, afterKeyguardGone);
+                return true;
+            } else {
+                return superOnClickHandler(view, pendingIntent, fillInIntent);
+            }
+        }
+
+        private void logActionClick(View view) {
+            ViewParent parent = view.getParent();
+            String key = getNotificationKeyForParent(parent);
+            if (key == null) {
+                Log.w(TAG, "Couldn't determine notification for click.");
+                return;
+            }
+            int index = -1;
+            // If this is a default template, determine the index of the button.
+            if (view.getId() == com.android.internal.R.id.action0 &&
+                    parent != null && parent instanceof ViewGroup) {
+                ViewGroup actionGroup = (ViewGroup) parent;
+                index = actionGroup.indexOfChild(view);
+            }
+            try {
+                mBarService.onNotificationActionClick(key, index);
+            } catch (RemoteException e) {
+                // Ignore
+            }
+        }
+
+        private String getNotificationKeyForParent(ViewParent parent) {
+            while (parent != null) {
+                if (parent instanceof ExpandableNotificationRow) {
+                    return ((ExpandableNotificationRow) parent).getStatusBarNotification().getKey();
+                }
+                parent = parent.getParent();
+            }
+            return null;
+        }
+
+        private boolean superOnClickHandler(View view, PendingIntent pendingIntent,
+                Intent fillInIntent) {
+            return super.onClickHandler(view, pendingIntent, fillInIntent,
+                    StackId.FULLSCREEN_WORKSPACE_STACK_ID);
+        }
+
+        private boolean handleRemoteInput(View view, PendingIntent pendingIntent, Intent fillInIntent) {
+            Object tag = view.getTag(com.android.internal.R.id.remote_input_tag);
+            RemoteInput[] inputs = null;
+            if (tag instanceof RemoteInput[]) {
+                inputs = (RemoteInput[]) tag;
+            }
+
+            if (inputs == null) {
+                return false;
+            }
+
+            RemoteInput input = null;
+
+            for (RemoteInput i : inputs) {
+                if (i.getAllowFreeFormInput()) {
+                    input = i;
+                }
+            }
+
+            if (input == null) {
+                return false;
+            }
+
+            ViewParent p = view.getParent();
+            RemoteInputView riv = null;
+            while (p != null) {
+                if (p instanceof View) {
+                    View pv = (View) p;
+                    if (pv.isRootNamespace()) {
+                        riv = findRemoteInputView(pv);
+                        break;
+                    }
+                }
+                p = p.getParent();
+            }
+            ExpandableNotificationRow row = null;
+            while (p != null) {
+                if (p instanceof ExpandableNotificationRow) {
+                    row = (ExpandableNotificationRow) p;
+                    break;
+                }
+                p = p.getParent();
+            }
+
+            if (row == null) {
+                return false;
+            }
+
+            row.setUserExpanded(true);
+
+            if (!mAllowLockscreenRemoteInput) {
+                final int userId = pendingIntent.getCreatorUserHandle().getIdentifier();
+                if (isLockscreenPublicMode(userId)) {
+                    onLockedRemoteInput(row, view);
+                    return true;
+                }
+                if (mUserManager.getUserInfo(userId).isManagedProfile()
+                        && mKeyguardManager.isDeviceLocked(userId)) {
+                    onLockedWorkRemoteInput(userId, row, view);
+                    return true;
+                }
+            }
+
+            if (riv == null) {
+                riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild());
+                if (riv == null) {
+                    return false;
+                }
+                if (!row.getPrivateLayout().getExpandedChild().isShown()) {
+                    onMakeExpandedVisibleForRemoteInput(row, view);
+                    return true;
+                }
+            }
+
+            int width = view.getWidth();
+            if (view instanceof TextView) {
+                // Center the reveal on the text which might be off-center from the TextView
+                TextView tv = (TextView) view;
+                if (tv.getLayout() != null) {
+                    int innerWidth = (int) tv.getLayout().getLineWidth(0);
+                    innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight();
+                    width = Math.min(width, innerWidth);
+                }
+            }
+            int cx = view.getLeft() + width / 2;
+            int cy = view.getTop() + view.getHeight() / 2;
+            int w = riv.getWidth();
+            int h = riv.getHeight();
+            int r = Math.max(
+                    Math.max(cx + cy, cx + (h - cy)),
+                    Math.max((w - cx) + cy, (w - cx) + (h - cy)));
+
+            riv.setRevealParameters(cx, cy, r);
+            riv.setPendingIntent(pendingIntent);
+            riv.setRemoteInput(inputs, input);
+            riv.focusAnimated();
+
+            return true;
+        }
+
+        private RemoteInputView findRemoteInputView(View v) {
+            if (v == null) {
+                return null;
+            }
+            return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG);
+        }
+    };
+
+    private final BroadcastReceiver mBaseBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (Intent.ACTION_USER_SWITCHED.equals(action)) {
+                mCurrentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+                updateCurrentProfilesCache();
+                if (true) Log.v(TAG, "userId " + mCurrentUserId + " is in the house");
+
+                updateLockscreenNotificationSetting();
+
+                userSwitched(mCurrentUserId);
+            } else if (Intent.ACTION_USER_ADDED.equals(action)) {
+                updateCurrentProfilesCache();
+            } else if (Intent.ACTION_USER_PRESENT.equals(action)) {
+                List<ActivityManager.RecentTaskInfo> recentTask = null;
+                try {
+                    recentTask = ActivityManager.getService().getRecentTasks(1,
+                            ActivityManager.RECENT_WITH_EXCLUDED
+                            | ActivityManager.RECENT_INCLUDE_PROFILES,
+                            mCurrentUserId).getList();
+                } catch (RemoteException e) {
+                    // Abandon hope activity manager not running.
+                }
+                if (recentTask != null && recentTask.size() > 0) {
+                    UserInfo user = mUserManager.getUserInfo(recentTask.get(0).userId);
+                    if (user != null && user.isManagedProfile()) {
+                        Toast toast = Toast.makeText(mContext,
+                                R.string.managed_profile_foreground_toast,
+                                Toast.LENGTH_SHORT);
+                        TextView text = (TextView) toast.getView().findViewById(
+                                android.R.id.message);
+                        text.setCompoundDrawablesRelativeWithIntrinsicBounds(
+                                R.drawable.stat_sys_managed_profile_status, 0, 0, 0);
+                        int paddingPx = mContext.getResources().getDimensionPixelSize(
+                                R.dimen.managed_profile_toast_padding);
+                        text.setCompoundDrawablePadding(paddingPx);
+                        toast.show();
+                    }
+                }
+            } else if (BANNER_ACTION_CANCEL.equals(action) || BANNER_ACTION_SETUP.equals(action)) {
+                NotificationManager noMan = (NotificationManager)
+                        mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+                noMan.cancel(SystemMessage.NOTE_HIDDEN_NOTIFICATIONS);
+
+                Settings.Secure.putInt(mContext.getContentResolver(),
+                        Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, 0);
+                if (BANNER_ACTION_SETUP.equals(action)) {
+                    animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL,
+                            true /* force */);
+                    mContext.startActivity(new Intent(Settings.ACTION_APP_NOTIFICATION_REDACTION)
+                            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+                    );
+                }
+            } else if (NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION.equals(action)) {
+                final IntentSender intentSender = intent.getParcelableExtra(Intent.EXTRA_INTENT);
+                final String notificationKey = intent.getStringExtra(Intent.EXTRA_INDEX);
+                if (intentSender != null) {
+                    try {
+                        mContext.startIntentSender(intentSender, null, 0, 0, 0);
+                    } catch (IntentSender.SendIntentException e) {
+                        /* ignore */
+                    }
+                }
+                if (notificationKey != null) {
+                    try {
+                        mBarService.onNotificationClick(notificationKey);
+                    } catch (RemoteException e) {
+                        /* ignore */
+                    }
+                }
+            }
+        }
+    };
+
+    private final BroadcastReceiver mAllUsersReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+
+            if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED.equals(action) &&
+                    isCurrentProfile(getSendingUserId())) {
+                mUsersAllowingPrivateNotifications.clear();
+                updateLockscreenNotificationSetting();
+                updateNotifications();
+            } else if (Intent.ACTION_DEVICE_LOCKED_CHANGED.equals(action)) {
+                if (userId != mCurrentUserId && isCurrentProfile(userId)) {
+                    onWorkChallengeChanged();
+                }
+            }
+        }
+    };
+
+    private final NotificationListenerWithPlugins mNotificationListener =
+            new NotificationListenerWithPlugins() {
+        @Override
+        public void onListenerConnected() {
+            if (DEBUG) Log.d(TAG, "onListenerConnected");
+            onPluginConnected();
+            final StatusBarNotification[] notifications = getActiveNotifications();
+            if (notifications == null) {
+                Log.w(TAG, "onListenerConnected unable to get active notifications.");
+                return;
+            }
+            final RankingMap currentRanking = getCurrentRanking();
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    for (StatusBarNotification sbn : notifications) {
+                        try {
+                            addNotification(sbn, currentRanking);
+                        } catch (InflationException e) {
+                            handleInflationException(sbn, e);
+                        }
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void onNotificationPosted(final StatusBarNotification sbn,
+                final RankingMap rankingMap) {
+            if (DEBUG) Log.d(TAG, "onNotificationPosted: " + sbn);
+            if (sbn != null && !onPluginNotificationPosted(sbn, rankingMap)) {
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        processForRemoteInput(sbn.getNotification());
+                        String key = sbn.getKey();
+                        mKeysKeptForRemoteInput.remove(key);
+                        boolean isUpdate = mNotificationData.get(key) != null;
+                        // 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 (!ENABLE_CHILD_NOTIFICATIONS
+                            && mGroupManager.isChildInGroupWithSummary(sbn)) {
+                            if (DEBUG) {
+                                Log.d(TAG, "Ignoring group child due to existing summary: " + sbn);
+                            }
+
+                            // Remove existing notification to avoid stale data.
+                            if (isUpdate) {
+                                removeNotification(key, rankingMap);
+                            } else {
+                                mNotificationData.updateRanking(rankingMap);
+                            }
+                            return;
+                        }
+                        try {
+                            if (isUpdate) {
+                                updateNotification(sbn, rankingMap);
+                            } else {
+                                addNotification(sbn, rankingMap);
+                            }
+                        } catch (InflationException e) {
+                            handleInflationException(sbn, e);
+                        }
+                    }
+                });
+            }
+        }
+
+        @Override
+        public void onNotificationRemoved(StatusBarNotification sbn,
+                final RankingMap rankingMap) {
+            if (DEBUG) Log.d(TAG, "onNotificationRemoved: " + sbn);
+            if (sbn != null && !onPluginNotificationRemoved(sbn, rankingMap)) {
+                final String key = sbn.getKey();
+                mHandler.post(() -> removeNotification(key, rankingMap));
+            }
+        }
+
+        @Override
+        public void onNotificationRankingUpdate(final RankingMap rankingMap) {
+            if (DEBUG) Log.d(TAG, "onRankingUpdate");
+            if (rankingMap != null) {
+                RankingMap r = onPluginRankingUpdate(rankingMap);
+                mHandler.post(() -> updateNotificationRanking(r));
+            }
+        }
+
+    };
+
+    private void updateCurrentProfilesCache() {
+        synchronized (mCurrentProfiles) {
+            mCurrentProfiles.clear();
+            if (mUserManager != null) {
+                for (UserInfo user : mUserManager.getProfiles(mCurrentUserId)) {
+                    mCurrentProfiles.put(user.id, user);
+                }
+            }
+        }
+    }
+
+    protected void notifyUserAboutHiddenNotifications() {
+        if (0 != Settings.Secure.getInt(mContext.getContentResolver(),
+                Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, 1)) {
+            Log.d(TAG, "user hasn't seen notification about hidden notifications");
+            if (!mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser())) {
+                Log.d(TAG, "insecure lockscreen, skipping notification");
+                Settings.Secure.putInt(mContext.getContentResolver(),
+                        Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, 0);
+                return;
+            }
+            Log.d(TAG, "disabling lockecreen notifications and alerting the user");
+            // disable lockscreen notifications until user acts on the banner.
+            Settings.Secure.putInt(mContext.getContentResolver(),
+                    Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 0);
+            Settings.Secure.putInt(mContext.getContentResolver(),
+                    Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0);
+
+            final String packageName = mContext.getPackageName();
+            PendingIntent cancelIntent = PendingIntent.getBroadcast(mContext, 0,
+                    new Intent(BANNER_ACTION_CANCEL).setPackage(packageName),
+                    PendingIntent.FLAG_CANCEL_CURRENT);
+            PendingIntent setupIntent = PendingIntent.getBroadcast(mContext, 0,
+                    new Intent(BANNER_ACTION_SETUP).setPackage(packageName),
+                    PendingIntent.FLAG_CANCEL_CURRENT);
+
+            final int colorRes = com.android.internal.R.color.system_notification_accent_color;
+            Notification.Builder note =
+                    new Notification.Builder(mContext, NotificationChannels.GENERAL)
+                            .setSmallIcon(R.drawable.ic_android)
+                            .setContentTitle(mContext.getString(
+                                    R.string.hidden_notifications_title))
+                            .setContentText(mContext.getString(R.string.hidden_notifications_text))
+                            .setOngoing(true)
+                            .setColor(mContext.getColor(colorRes))
+                            .setContentIntent(setupIntent)
+                            .addAction(R.drawable.ic_close,
+                                    mContext.getString(R.string.hidden_notifications_cancel),
+                                    cancelIntent)
+                            .addAction(R.drawable.ic_settings,
+                                    mContext.getString(R.string.hidden_notifications_setup),
+                                    setupIntent);
+            overrideNotificationAppName(mContext, note);
+
+            NotificationManager noMan =
+                    (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+            noMan.notify(SystemMessage.NOTE_HIDDEN_NOTIFICATIONS, note.build());
+        }
+    }
+
+    @Override  // NotificationData.Environment
+    public boolean isNotificationForCurrentProfiles(StatusBarNotification n) {
+        final int thisUserId = mCurrentUserId;
+        final int notificationUserId = n.getUserId();
+        if (DEBUG && MULTIUSER_DEBUG) {
+            Log.v(TAG, String.format("%s: current userid: %d, notification userid: %d",
+                    n, thisUserId, notificationUserId));
+        }
+        return isCurrentProfile(notificationUserId);
+    }
+
+    protected void setNotificationShown(StatusBarNotification n) {
+        setNotificationsShown(new String[]{n.getKey()});
+    }
+
+    protected void setNotificationsShown(String[] keys) {
+        try {
+            mNotificationListener.setNotificationsShown(keys);
+        } catch (RuntimeException e) {
+            Log.d(TAG, "failed setNotificationsShown: ", e);
+        }
+    }
+
+    protected boolean isCurrentProfile(int userId) {
+        synchronized (mCurrentProfiles) {
+            return userId == UserHandle.USER_ALL || mCurrentProfiles.get(userId) != null;
+        }
+    }
+
+    @Override
+    public NotificationGroupManager getGroupManager() {
+        return mGroupManager;
+    }
+
+    public boolean isMediaNotification(NotificationData.Entry entry) {
+        // TODO: confirm that there's a valid media key
+        return entry.getExpandedContentView() != null &&
+               entry.getExpandedContentView()
+                       .findViewById(com.android.internal.R.id.media_actions) != null;
+    }
+
+    // The button in the guts that links to the system notification settings for that app
+    private void startAppNotificationSettingsActivity(String packageName, final int appUid,
+            final NotificationChannel channel) {
+        final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
+        intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName);
+        intent.putExtra(Settings.EXTRA_APP_UID, appUid);
+        if (channel != null) {
+            intent.putExtra(EXTRA_FRAGMENT_ARG_KEY, channel.getId());
+        }
+        startNotificationGutsIntent(intent, appUid);
+    }
+
+    private void startNotificationGutsIntent(final Intent intent, final int appUid) {
+        dismissKeyguardThenExecute(new OnDismissAction() {
+            @Override
+            public boolean onDismiss() {
+                AsyncTask.execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        TaskStackBuilder.create(mContext)
+                                .addNextIntentWithParentStack(intent)
+                                .startActivities(getActivityOptions(),
+                                        new UserHandle(UserHandle.getUserId(appUid)));
+                    }
+                });
+                animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */);
+                return true;
+            }
+        }, false /* afterKeyguardGone */);
+    }
+
+    public void setNotificationSnoozed(StatusBarNotification sbn, SnoozeOption snoozeOption) {
+        if (snoozeOption.getSnoozeCriterion() != null) {
+            mNotificationListener.snoozeNotification(sbn.getKey(),
+                    snoozeOption.getSnoozeCriterion().getId());
+        } else {
+            mNotificationListener.snoozeNotification(sbn.getKey(),
+                    snoozeOption.getMinutesToSnoozeFor() * 60 * 1000);
+        }
+    }
+
+    private void bindGuts(final ExpandableNotificationRow row, MenuItem item) {
+        row.inflateGuts();
+        row.setGutsView(item);
+        final StatusBarNotification sbn = row.getStatusBarNotification();
+        row.setTag(sbn.getPackageName());
+        final NotificationGuts guts = row.getGuts();
+        guts.setClosedListener((NotificationGuts g) -> {
+            if (!g.willBeRemoved() && !row.isRemoved()) {
+                mStackScroller.onHeightChanged(row, !isPanelFullyCollapsed() /* needsAnimation */);
+            }
+            if (mNotificationGutsExposed == g) {
+                mNotificationGutsExposed = null;
+                mGutsMenuItem = null;
+            }
+            String key = sbn.getKey();
+            if (key.equals(mKeyToRemoveOnGutsClosed)) {
+                mKeyToRemoveOnGutsClosed = null;
+                removeNotification(key, mLatestRankingMap);
+            }
+        });
+
+        View gutsView = item.getGutsView();
+        if (gutsView instanceof NotificationSnooze) {
+            NotificationSnooze snoozeGuts = (NotificationSnooze) gutsView;
+            snoozeGuts.setSnoozeListener(mStackScroller.getSwipeActionHelper());
+            snoozeGuts.setStatusBarNotification(sbn);
+            snoozeGuts.setSnoozeOptions(row.getEntry().snoozeCriteria);
+            guts.setHeightChangedListener((NotificationGuts g) -> {
+                mStackScroller.onHeightChanged(row, row.isShown() /* needsAnimation */);
+            });
+        }
+
+        if (gutsView instanceof NotificationInfo) {
+            final UserHandle userHandle = sbn.getUser();
+            PackageManager pmUser = getPackageManagerForUser(mContext,
+                    userHandle.getIdentifier());
+            final INotificationManager iNotificationManager = INotificationManager.Stub.asInterface(
+                    ServiceManager.getService(Context.NOTIFICATION_SERVICE));
+            final String pkg = sbn.getPackageName();
+            NotificationInfo info = (NotificationInfo) gutsView;
+            // Settings link is only valid for notifications that specify a user, unless this is the
+            // system user.
+            NotificationInfo.OnSettingsClickListener onSettingsClick = null;
+            if (!userHandle.equals(UserHandle.ALL) || mCurrentUserId == UserHandle.USER_SYSTEM) {
+                onSettingsClick = (View v, NotificationChannel channel, int appUid) -> {
+                    mMetricsLogger.action(MetricsEvent.ACTION_NOTE_INFO);
+                    guts.resetFalsingCheck();
+                    startAppNotificationSettingsActivity(pkg, appUid, channel);
+                };
+            }
+            final NotificationInfo.OnAppSettingsClickListener onAppSettingsClick = (View v,
+                    Intent intent) -> {
+                mMetricsLogger.action(MetricsEvent.ACTION_APP_NOTE_SETTINGS);
+                guts.resetFalsingCheck();
+                startNotificationGutsIntent(intent, sbn.getUid());
+            };
+            final View.OnClickListener onDoneClick = (View v) -> {
+                saveAndCloseNotificationMenu(info, row, guts, v);
+            };
+            final NotificationInfo.CheckSaveListener checkSaveListener =
+                    (Runnable saveImportance) -> {
+                // If the user has security enabled, show challenge if the setting is changed.
+                if (isLockscreenPublicMode(userHandle.getIdentifier())
+                        && (mState == StatusBarState.KEYGUARD
+                                || mState == StatusBarState.SHADE_LOCKED)) {
+                    onLockedNotificationImportanceChange(() -> {
+                        saveImportance.run();
+                        return true;
+                    });
+                } else {
+                    saveImportance.run();
+                }
+            };
+
+            ArraySet<NotificationChannel> channels = new ArraySet<NotificationChannel>();
+            channels.add(row.getEntry().channel);
+            if (row.isSummaryWithChildren()) {
+                // If this is a summary, then add in the children notification channels for the
+                // same user and pkg.
+                final List<ExpandableNotificationRow> childrenRows = row.getNotificationChildren();
+                final int numChildren = childrenRows.size();
+                for (int i = 0; i < numChildren; i++) {
+                    final ExpandableNotificationRow childRow = childrenRows.get(i);
+                    final NotificationChannel childChannel = childRow.getEntry().channel;
+                    final StatusBarNotification childSbn = childRow.getStatusBarNotification();
+                    if (childSbn.getUser().equals(userHandle) &&
+                            childSbn.getPackageName().equals(pkg)) {
+                        channels.add(childChannel);
+                    }
+                }
+            }
+            try {
+                info.bindNotification(pmUser, iNotificationManager, pkg, new ArrayList(channels),
+                        row.getEntry().channel.getImportance(), sbn, onSettingsClick,
+                        onAppSettingsClick, onDoneClick, checkSaveListener,
+                        mNonBlockablePkgs);
+            } catch (RemoteException e) {
+                Log.e(TAG, e.toString());
+            }
+        }
+    }
+
+    private void saveAndCloseNotificationMenu(NotificationInfo info,
+            ExpandableNotificationRow row, NotificationGuts guts, View done) {
+        guts.resetFalsingCheck();
+        int[] rowLocation = new int[2];
+        int[] doneLocation = new int[2];
+        row.getLocationOnScreen(rowLocation);
+        done.getLocationOnScreen(doneLocation);
+
+        final int centerX = done.getWidth() / 2;
+        final int centerY = done.getHeight() / 2;
+        final int x = doneLocation[0] - rowLocation[0] + centerX;
+        final int y = doneLocation[1] - rowLocation[1] + centerY;
+        closeAndSaveGuts(false /* removeLeavebehind */, false /* force */,
+                true /* removeControls */, x, y, true /* resetMenu */);
+    }
+
+    protected SwipeHelper.LongPressListener getNotificationLongClicker() {
+        return new SwipeHelper.LongPressListener() {
+            @Override
+            public boolean onLongPress(View v, final int x, final int y,
+                    MenuItem item) {
+                if (!(v instanceof ExpandableNotificationRow)) {
+                    return false;
+                }
+                if (v.getWindowToken() == null) {
+                    Log.e(TAG, "Trying to show notification guts, but not attached to window");
+                    return false;
+                }
+
+                final ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+                if (row.isDark()) {
+                    return false;
+                }
+                v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+                if (row.areGutsExposed()) {
+                    closeAndSaveGuts(false /* removeLeavebehind */, false /* force */,
+                            true /* removeControls */, -1 /* x */, -1 /* y */,
+                            true /* resetMenu */);
+                    return false;
+                }
+                bindGuts(row, item);
+                NotificationGuts guts = row.getGuts();
+
+                // Assume we are a status_bar_notification_row
+                if (guts == null) {
+                    // This view has no guts. Examples are the more card or the dismiss all view
+                    return false;
+                }
+
+                mMetricsLogger.action(MetricsEvent.ACTION_NOTE_CONTROLS);
+
+                // ensure that it's laid but not visible until actually laid out
+                guts.setVisibility(View.INVISIBLE);
+                // Post to ensure the the guts are properly laid out.
+                guts.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (row.getWindowToken() == null) {
+                            Log.e(TAG, "Trying to show notification guts, but not attached to "
+                                    + "window");
+                            return;
+                        }
+                        closeAndSaveGuts(true /* removeLeavebehind */, true /* force */,
+                                true /* removeControls */, -1 /* x */, -1 /* y */,
+                                false /* resetMenu */);
+                        guts.setVisibility(View.VISIBLE);
+                        final double horz = Math.max(guts.getWidth() - x, x);
+                        final double vert = Math.max(guts.getHeight() - y, y);
+                        final float r = (float) Math.hypot(horz, vert);
+                        final Animator a
+                                = ViewAnimationUtils.createCircularReveal(guts, x, y, 0, r);
+                        a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+                        a.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+                        a.addListener(new AnimatorListenerAdapter() {
+                            @Override
+                            public void onAnimationEnd(Animator animation) {
+                                super.onAnimationEnd(animation);
+                                // Move the notification view back over the menu
+                                row.resetTranslation();
+                            }
+                        });
+                        a.start();
+                        final boolean needsFalsingProtection =
+                                (mState == StatusBarState.KEYGUARD &&
+                                !mAccessibilityManager.isTouchExplorationEnabled());
+                        guts.setExposed(true /* exposed */, needsFalsingProtection);
+                        row.closeRemoteInput();
+                        mStackScroller.onHeightChanged(row, true /* needsAnimation */);
+                        mNotificationGutsExposed = guts;
+                        mGutsMenuItem = item;
+                    }
+                });
+                return true;
+            }
+        };
+    }
+
+    /**
+     * Returns the exposed NotificationGuts or null if none are exposed.
+     */
+    public NotificationGuts getExposedGuts() {
+        return mNotificationGutsExposed;
+    }
+
+    /**
+     * Closes guts or notification menus that might be visible and saves any changes.
+     *
+     * @param removeLeavebehinds true if leavebehinds (e.g. snooze) should be closed.
+     * @param force true if guts should be closed regardless of state (used for snooze only).
+     * @param removeControls true if controls (e.g. info) should be closed.
+     * @param x if closed based on touch location, this is the x touch location.
+     * @param y if closed based on touch location, this is the y touch location.
+     * @param resetMenu if any notification menus that might be revealed should be closed.
+     */
+    public void closeAndSaveGuts(boolean removeLeavebehinds, boolean force, boolean removeControls,
+            int x, int y, boolean resetMenu) {
+        if (mNotificationGutsExposed != null) {
+            mNotificationGutsExposed.closeControls(removeLeavebehinds, removeControls, x, y, force);
+        }
+        if (resetMenu) {
+            mStackScroller.resetExposedMenuView(false /* animate */, true /* force */);
+        }
+    }
+
+    @Override
+    public void toggleSplitScreen() {
+        toggleSplitScreenMode(-1 /* metricsDockAction */, -1 /* metricsUndockAction */);
+    }
+
+    @Override
+    public void preloadRecentApps() {
+        int msg = MSG_PRELOAD_RECENT_APPS;
+        mHandler.removeMessages(msg);
+        mHandler.sendEmptyMessage(msg);
+    }
+
+    @Override
+    public void cancelPreloadRecentApps() {
+        int msg = MSG_CANCEL_PRELOAD_RECENT_APPS;
+        mHandler.removeMessages(msg);
+        mHandler.sendEmptyMessage(msg);
+    }
+
+    @Override
+    public void dismissKeyboardShortcutsMenu() {
+        int msg = MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU;
+        mHandler.removeMessages(msg);
+        mHandler.sendEmptyMessage(msg);
+    }
+
+    @Override
+    public void toggleKeyboardShortcutsMenu(int deviceId) {
+        int msg = MSG_TOGGLE_KEYBOARD_SHORTCUTS_MENU;
+        mHandler.removeMessages(msg);
+        mHandler.obtainMessage(msg, deviceId, 0).sendToTarget();
+    }
+
+    @Override
+    public void setTopAppHidesStatusBar(boolean topAppHidesStatusBar) {
+        mTopHidesStatusBar = topAppHidesStatusBar;
+        if (!topAppHidesStatusBar && mWereIconsJustHidden) {
+            // Immediately update the icon hidden state, since that should only apply if we're
+            // staying fullscreen.
+            mWereIconsJustHidden = false;
+            recomputeDisableFlags(true);
+        }
+        updateHideIconsForBouncer(true /* animate */);
+    }
+
+    protected void sendCloseSystemWindows(String reason) {
+        try {
+            ActivityManager.getService().closeSystemDialogs(reason);
+        } catch (RemoteException e) {
+        }
+    }
+
+    protected void toggleKeyboardShortcuts(int deviceId) {
+        KeyboardShortcuts.toggle(mContext, deviceId);
+    }
+
+    protected void dismissKeyboardShortcuts() {
+        KeyboardShortcuts.dismiss();
+    }
+
+    /**
+     * Save the current "public" (locked and secure) state of the lockscreen.
+     */
+    public void setLockscreenPublicMode(boolean publicMode, int userId) {
+        mLockscreenPublicMode.put(userId, publicMode);
+    }
+
+    public boolean isLockscreenPublicMode(int userId) {
+        if (userId == UserHandle.USER_ALL) {
+            return mLockscreenPublicMode.get(mCurrentUserId, false);
+        }
+        return mLockscreenPublicMode.get(userId, false);
+    }
+
+    /**
+     * Has the given user chosen to allow notifications to be shown even when the lockscreen is in
+     * "public" (secure & locked) mode?
+     */
+    public boolean userAllowsNotificationsInPublic(int userHandle) {
+        if (userHandle == UserHandle.USER_ALL) {
+            return true;
+        }
+
+        if (mUsersAllowingNotifications.indexOfKey(userHandle) < 0) {
+            final boolean allowed = 0 != Settings.Secure.getIntForUser(
+                    mContext.getContentResolver(),
+                    Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 0, userHandle);
+            mUsersAllowingNotifications.append(userHandle, allowed);
+            return allowed;
+        }
+
+        return mUsersAllowingNotifications.get(userHandle);
+    }
+
+    /**
+     * Has the given user chosen to allow their private (full) notifications to be shown even
+     * when the lockscreen is in "public" (secure & locked) mode?
+     */
+    public boolean userAllowsPrivateNotificationsInPublic(int userHandle) {
+        if (userHandle == UserHandle.USER_ALL) {
+            return true;
+        }
+
+        if (mUsersAllowingPrivateNotifications.indexOfKey(userHandle) < 0) {
+            final boolean allowedByUser = 0 != Settings.Secure.getIntForUser(
+                    mContext.getContentResolver(),
+                    Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0, userHandle);
+            final boolean allowedByDpm = adminAllowsUnredactedNotifications(userHandle);
+            final boolean allowed = allowedByUser && allowedByDpm;
+            mUsersAllowingPrivateNotifications.append(userHandle, allowed);
+            return allowed;
+        }
+
+        return mUsersAllowingPrivateNotifications.get(userHandle);
+    }
+
+    private boolean adminAllowsUnredactedNotifications(int userHandle) {
+        if (userHandle == UserHandle.USER_ALL) {
+            return true;
+        }
+        final int dpmFlags = mDevicePolicyManager.getKeyguardDisabledFeatures(null /* admin */,
+                    userHandle);
+        return (dpmFlags & DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS) == 0;
+    }
+
+    /**
+     * Returns true if we're on a secure lockscreen and the user wants to hide notification data.
+     * If so, notifications should be hidden.
+     */
+    @Override  // NotificationData.Environment
+    public boolean shouldHideNotifications(int userId) {
+        return isLockscreenPublicMode(userId) && !userAllowsNotificationsInPublic(userId)
+                || (userId != mCurrentUserId && shouldHideNotifications(mCurrentUserId));
+    }
+
+    /**
+     * Returns true if we're on a secure lockscreen and the user wants to hide notifications via
+     * package-specific override.
+     */
+    @Override // NotificationDate.Environment
+    public boolean shouldHideNotifications(String key) {
+        return isLockscreenPublicMode(mCurrentUserId)
+                && mNotificationData.getVisibilityOverride(key) == Notification.VISIBILITY_SECRET;
+    }
+
+    /**
+     * Returns true if we're on a secure lockscreen.
+     */
+    @Override  // NotificationData.Environment
+    public boolean isSecurelyLocked(int userId) {
+        return isLockscreenPublicMode(userId);
+    }
+
+    public void onNotificationClear(StatusBarNotification notification) {
+        try {
+            mBarService.onNotificationClear(
+                    notification.getPackageName(),
+                    notification.getTag(),
+                    notification.getId(),
+                    notification.getUserId());
+        } catch (android.os.RemoteException ex) {
+            // oh well
+        }
+    }
+
+    /**
+     * Called when the notification panel layouts
+     */
+    public void onPanelLaidOut() {
+        updateKeyguardMaxNotifications();
+    }
+
+    public void updateKeyguardMaxNotifications() {
+        if (mState == StatusBarState.KEYGUARD) {
+            // Since the number of notifications is determined based on the height of the view, we
+            // need to update them.
+            int maxBefore = getMaxKeyguardNotifications(false /* recompute */);
+            int maxNotifications = getMaxKeyguardNotifications(true /* recompute */);
+            if (maxBefore != maxNotifications) {
+                updateRowStates();
+            }
+        }
+    }
+
+    protected void inflateViews(Entry entry, ViewGroup parent) {
+        PackageManager pmUser = getPackageManagerForUser(mContext,
+                entry.notification.getUser().getIdentifier());
+
+        final StatusBarNotification sbn = entry.notification;
+        if (entry.row != null) {
+            entry.reset();
+            updateNotification(entry, pmUser, sbn, entry.row);
+        } else {
+            new RowInflaterTask().inflate(mContext, parent, entry,
+                    row -> {
+                        bindRow(entry, pmUser, sbn, row);
+                        updateNotification(entry, pmUser, sbn, row);
+                    });
+        }
+
+    }
+
+    private void bindRow(Entry entry, PackageManager pmUser,
+            StatusBarNotification sbn, ExpandableNotificationRow row) {
+        row.setExpansionLogger(this, entry.notification.getKey());
+        row.setGroupManager(mGroupManager);
+        row.setHeadsUpManager(mHeadsUpManager);
+        row.setAboveShelfChangedListener(mAboveShelfObserver);
+        row.setRemoteInputController(mRemoteInputController);
+        row.setOnExpandClickListener(this);
+        row.setRemoteViewClickHandler(mOnClickHandler);
+        row.setInflationCallback(this);
+        row.setSecureStateProvider(this::isKeyguardCurrentlySecure);
+
+        // Get the app name.
+        // Note that Notification.Builder#bindHeaderAppName has similar logic
+        // but since this field is used in the guts, it must be accurate.
+        // Therefore we will only show the application label, or, failing that, the
+        // package name. No substitutions.
+        final String pkg = sbn.getPackageName();
+        String appname = pkg;
+        try {
+            final ApplicationInfo info = pmUser.getApplicationInfo(pkg,
+                    PackageManager.MATCH_UNINSTALLED_PACKAGES
+                            | PackageManager.MATCH_DISABLED_COMPONENTS);
+            if (info != null) {
+                appname = String.valueOf(pmUser.getApplicationLabel(info));
+            }
+        } catch (NameNotFoundException e) {
+            // Do nothing
+        }
+        row.setAppName(appname);
+        row.setOnDismissRunnable(() ->
+                performRemoveNotification(row.getStatusBarNotification()));
+        row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+        if (ENABLE_REMOTE_INPUT) {
+            row.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
+        }
+    }
+
+    private void updateNotification(Entry entry, PackageManager pmUser,
+            StatusBarNotification sbn, ExpandableNotificationRow row) {
+        row.setNeedsRedaction(needsRedaction(entry));
+        boolean isLowPriority = mNotificationData.isAmbient(sbn.getKey());
+        boolean isUpdate = mNotificationData.get(entry.key) != null;
+        boolean wasLowPriority = row.isLowPriority();
+        row.setIsLowPriority(isLowPriority);
+        row.setLowPriorityStateUpdated(isUpdate && (wasLowPriority != isLowPriority));
+        // bind the click event to the content area
+        mNotificationClicker.register(row, sbn);
+
+        // Extract target SDK version.
+        try {
+            ApplicationInfo info = pmUser.getApplicationInfo(sbn.getPackageName(), 0);
+            entry.targetSdk = info.targetSdkVersion;
+        } catch (NameNotFoundException ex) {
+            Log.e(TAG, "Failed looking up ApplicationInfo for " + sbn.getPackageName(), ex);
+        }
+        row.setLegacy(entry.targetSdk >= Build.VERSION_CODES.GINGERBREAD
+                && entry.targetSdk < Build.VERSION_CODES.LOLLIPOP);
+        entry.setIconTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP);
+        entry.autoRedacted = entry.notification.getNotification().publicVersion == null;
+
+        entry.row = row;
+        entry.row.setOnActivatedListener(this);
+
+        boolean useIncreasedCollapsedHeight = mMessagingUtil.isImportantMessaging(sbn,
+                mNotificationData.getImportance(sbn.getKey()));
+        boolean useIncreasedHeadsUp = useIncreasedCollapsedHeight && mPanelExpanded;
+        row.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
+        row.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp);
+        row.updateNotification(entry);
+    }
+
+    /**
+     * Adds RemoteInput actions from the WearableExtender; to be removed once more apps support this
+     * via first-class API.
+     *
+     * TODO: Remove once enough apps specify remote inputs on their own.
+     */
+    private void processForRemoteInput(Notification n) {
+        if (!ENABLE_REMOTE_INPUT) return;
+
+        if (n.extras != null && n.extras.containsKey("android.wearable.EXTENSIONS") &&
+                (n.actions == null || n.actions.length == 0)) {
+            Notification.Action viableAction = null;
+            Notification.WearableExtender we = new Notification.WearableExtender(n);
+
+            List<Notification.Action> actions = we.getActions();
+            final int numActions = actions.size();
+
+            for (int i = 0; i < numActions; i++) {
+                Notification.Action action = actions.get(i);
+                if (action == null) {
+                    continue;
+                }
+                RemoteInput[] remoteInputs = action.getRemoteInputs();
+                if (remoteInputs == null) {
+                    continue;
+                }
+                for (RemoteInput ri : remoteInputs) {
+                    if (ri.getAllowFreeFormInput()) {
+                        viableAction = action;
+                        break;
+                    }
+                }
+                if (viableAction != null) {
+                    break;
+                }
+            }
+
+            if (viableAction != null) {
+                Notification.Builder rebuilder = Notification.Builder.recoverBuilder(mContext, n);
+                rebuilder.setActions(viableAction);
+                rebuilder.build(); // will rewrite n
+            }
+        }
+    }
+
+    public void startPendingIntentDismissingKeyguard(final PendingIntent intent) {
+        if (!isDeviceProvisioned()) return;
+
+        final boolean keyguardShowing = mStatusBarKeyguardViewManager.isShowing();
+        final boolean afterKeyguardGone = intent.isActivity()
+                && PreviewInflater.wouldLaunchResolverActivity(mContext, intent.getIntent(),
+                mCurrentUserId);
+        dismissKeyguardThenExecute(new OnDismissAction() {
+            @Override
+            public boolean onDismiss() {
+                new Thread() {
+                    @Override
+                    public void run() {
+                        try {
+                            // The intent we are sending is for the application, which
+                            // won't have permission to immediately start an activity after
+                            // the user switches to home.  We know it is safe to do at this
+                            // point, so make sure new activity switches are now allowed.
+                            ActivityManager.getService().resumeAppSwitches();
+                        } catch (RemoteException e) {
+                        }
+                        try {
+                            intent.send(null, 0, null, null, null, null, getActivityOptions());
+                        } catch (PendingIntent.CanceledException e) {
+                            // the stack trace isn't very helpful here.
+                            // Just log the exception message.
+                            Log.w(TAG, "Sending intent failed: " + e);
+
+                            // TODO: Dismiss Keyguard.
+                        }
+                        if (intent.isActivity()) {
+                            mAssistManager.hideAssist();
+                        }
+                    }
+                }.start();
+
+                if (!mNotificationPanel.isFullyCollapsed()) {
+                    // close the shade if it was open
+                    animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL,
+                            true /* force */, true /* delayed */);
+                    visibilityChanged(false);
+
+                    return true;
+                } else {
+                    return false;
+                }
+            }
+        }, afterKeyguardGone);
+    }
+
+
+    private final class NotificationClicker implements View.OnClickListener {
+
+        @Override
+        public void onClick(final View v) {
+            if (!(v instanceof ExpandableNotificationRow)) {
+                Log.e(TAG, "NotificationClicker called on a view that is not a notification row.");
+                return;
+            }
+
+            wakeUpIfDozing(SystemClock.uptimeMillis(), v);
+
+            final ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+            final StatusBarNotification sbn = row.getStatusBarNotification();
+            if (sbn == null) {
+                Log.e(TAG, "NotificationClicker called on an unclickable notification,");
+                return;
+            }
+
+            // Check if the notification is displaying the menu, if so slide notification back
+            if (row.getProvider() != null && row.getProvider().isMenuVisible()) {
+                row.animateTranslateNotification(0);
+                return;
+            }
+
+            Notification notification = sbn.getNotification();
+            final PendingIntent intent = notification.contentIntent != null
+                    ? notification.contentIntent
+                    : notification.fullScreenIntent;
+            final String notificationKey = sbn.getKey();
+
+            // Mark notification for one frame.
+            row.setJustClicked(true);
+            DejankUtils.postAfterTraversal(new Runnable() {
+                @Override
+                public void run() {
+                    row.setJustClicked(false);
+                }
+            });
+
+            final boolean afterKeyguardGone = intent.isActivity()
+                    && PreviewInflater.wouldLaunchResolverActivity(mContext, intent.getIntent(),
+                            mCurrentUserId);
+            dismissKeyguardThenExecute(new OnDismissAction() {
+                @Override
+                public boolean onDismiss() {
+                    if (mHeadsUpManager != null && mHeadsUpManager.isHeadsUp(notificationKey)) {
+                        // Release the HUN notification to the shade.
+
+                        if (isPanelFullyCollapsed()) {
+                            HeadsUpManager.setIsClickedNotification(row, true);
+                        }
+                        //
+                        // In most cases, when FLAG_AUTO_CANCEL is set, the notification will
+                        // become canceled shortly by NoMan, but we can't assume that.
+                        mHeadsUpManager.releaseImmediately(notificationKey);
+                    }
+                    StatusBarNotification parentToCancel = null;
+                    if (shouldAutoCancel(sbn) && mGroupManager.isOnlyChildInGroup(sbn)) {
+                        StatusBarNotification summarySbn = mGroupManager.getLogicalGroupSummary(sbn)
+                                        .getStatusBarNotification();
+                        if (shouldAutoCancel(summarySbn)) {
+                            parentToCancel = summarySbn;
+                        }
+                    }
+                    final StatusBarNotification parentToCancelFinal = parentToCancel;
+                    final Runnable runnable = new Runnable() {
+                        @Override
+                        public void run() {
+                            try {
+                                // The intent we are sending is for the application, which
+                                // won't have permission to immediately start an activity after
+                                // the user switches to home.  We know it is safe to do at this
+                                // point, so make sure new activity switches are now allowed.
+                                ActivityManager.getService().resumeAppSwitches();
+                            } catch (RemoteException e) {
+                            }
+                            if (intent != null) {
+                                // If we are launching a work activity and require to launch
+                                // separate work challenge, we defer the activity action and cancel
+                                // notification until work challenge is unlocked.
+                                if (intent.isActivity()) {
+                                    final int userId = intent.getCreatorUserHandle()
+                                            .getIdentifier();
+                                    if (mLockPatternUtils.isSeparateProfileChallengeEnabled(userId)
+                                            && mKeyguardManager.isDeviceLocked(userId)) {
+                                        // TODO(b/28935539): should allow certain activities to
+                                        // bypass work challenge
+                                        if (startWorkChallengeIfNecessary(userId,
+                                                intent.getIntentSender(), notificationKey)) {
+                                            // Show work challenge, do not run PendingIntent and
+                                            // remove notification
+                                            return;
+                                        }
+                                    }
+                                }
+                                try {
+                                    intent.send(null, 0, null, null, null, null,
+                                            getActivityOptions());
+                                } catch (PendingIntent.CanceledException e) {
+                                    // the stack trace isn't very helpful here.
+                                    // Just log the exception message.
+                                    Log.w(TAG, "Sending contentIntent failed: " + e);
+
+                                    // TODO: Dismiss Keyguard.
+                                }
+                                if (intent.isActivity()) {
+                                    mAssistManager.hideAssist();
+                                }
+                            }
+
+                            try {
+                                mBarService.onNotificationClick(notificationKey);
+                            } catch (RemoteException ex) {
+                                // system process is dead if we're here.
+                            }
+                            if (parentToCancelFinal != null) {
+                                // We have to post it to the UI thread for synchronization
+                                mHandler.post(new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        Runnable removeRunnable = new Runnable() {
+                                            @Override
+                                            public void run() {
+                                                performRemoveNotification(parentToCancelFinal);
+                                            }
+                                        };
+                                        if (isCollapsing()) {
+                                            // To avoid lags we're only performing the remove
+                                            // after the shade was collapsed
+                                            addPostCollapseAction(removeRunnable);
+                                        } else {
+                                            removeRunnable.run();
+                                        }
+                                    }
+                                });
+                            }
+                        }
+                    };
+
+                    if (mStatusBarKeyguardViewManager.isShowing()
+                            && mStatusBarKeyguardViewManager.isOccluded()) {
+                        mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(runnable);
+                    } else {
+                        new Thread(runnable).start();
+                    }
+
+                    if (!mNotificationPanel.isFullyCollapsed()) {
+                        // close the shade if it was open
+                        animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL,
+                                true /* force */, true /* delayed */);
+                        visibilityChanged(false);
+
+                        return true;
+                    } else {
+                        return false;
+                    }
+                }
+            }, afterKeyguardGone);
+        }
+
+        private boolean shouldAutoCancel(StatusBarNotification sbn) {
+            int flags = sbn.getNotification().flags;
+            if ((flags & Notification.FLAG_AUTO_CANCEL) != Notification.FLAG_AUTO_CANCEL) {
+                return false;
+            }
+            if ((flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
+                return false;
+            }
+            return true;
+        }
+
+        public void register(ExpandableNotificationRow row, StatusBarNotification sbn) {
+            Notification notification = sbn.getNotification();
+            if (notification.contentIntent != null || notification.fullScreenIntent != null) {
+                row.setOnClickListener(this);
+            } else {
+                row.setOnClickListener(null);
+            }
+        }
+    }
+
+    protected Bundle getActivityOptions() {
+        // Anything launched from the notification shade should always go into the
+        // fullscreen stack.
+        ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchStackId(StackId.FULLSCREEN_WORKSPACE_STACK_ID);
+        return options.toBundle();
+    }
+
+    protected void visibilityChanged(boolean visible) {
+        if (mVisible != visible) {
+            mVisible = visible;
+            if (!visible) {
+                closeAndSaveGuts(true /* removeLeavebehind */, true /* force */,
+                        true /* removeControls */, -1 /* x */, -1 /* y */, true /* resetMenu */);
+            }
+        }
+        updateVisibleToUser();
+    }
+
+    protected void updateVisibleToUser() {
+        boolean oldVisibleToUser = mVisibleToUser;
+        mVisibleToUser = mVisible && mDeviceInteractive;
+
+        if (oldVisibleToUser != mVisibleToUser) {
+            handleVisibleToUserChanged(mVisibleToUser);
+        }
+    }
+
+    /**
+     * Clear Buzz/Beep/Blink.
+     */
+    public void clearNotificationEffects() {
+        try {
+            mBarService.clearNotificationEffects();
+        } catch (RemoteException e) {
+            // Won't fail unless the world has ended.
+        }
+    }
+
+    /**
+     * Cancel this notification and tell the StatusBarManagerService / NotificationManagerService
+     * about the failure.
+     *
+     * WARNING: this will call back into us.  Don't hold any locks.
+     */
+    void handleNotificationError(StatusBarNotification n, String message) {
+        removeNotification(n.getKey(), null);
+        try {
+            mBarService.onNotificationError(n.getPackageName(), n.getTag(), n.getId(), n.getUid(),
+                    n.getInitialPid(), message, n.getUserId());
+        } catch (RemoteException ex) {
+            // The end is nigh.
+        }
+    }
+
+    protected StatusBarNotification removeNotificationViews(String key, RankingMap ranking) {
+        NotificationData.Entry entry = mNotificationData.remove(key, ranking);
+        if (entry == null) {
+            Log.w(TAG, "removeNotification for unknown key: " + key);
+            return null;
+        }
+        updateNotifications();
+        Dependency.get(LeakDetector.class).trackGarbage(entry);
+        return entry.notification;
+    }
+
+    protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn)
+            throws InflationException {
+        if (DEBUG) {
+            Log.d(TAG, "createNotificationViews(notification=" + sbn);
+        }
+        NotificationData.Entry entry = new NotificationData.Entry(sbn);
+        Dependency.get(LeakDetector.class).trackInstance(entry);
+        entry.createIcons(mContext, sbn);
+        // Construct the expanded view.
+        inflateViews(entry, mStackScroller);
+        return entry;
+    }
+
+    protected void addNotificationViews(Entry entry) {
+        if (entry == null) {
+            return;
+        }
+        // Add the expanded view and icon.
+        mNotificationData.add(entry);
+        updateNotifications();
+    }
+
+    /**
+     * Updates expanded, dimmed and locked states of notification rows.
+     */
+    protected void updateRowStates() {
+        final int N = mStackScroller.getChildCount();
+
+        int visibleNotifications = 0;
+        boolean onKeyguard = mState == StatusBarState.KEYGUARD;
+        int maxNotifications = -1;
+        if (onKeyguard) {
+            maxNotifications = getMaxKeyguardNotifications(true /* recompute */);
+        }
+        mStackScroller.setMaxDisplayedNotifications(maxNotifications);
+        Stack<ExpandableNotificationRow> stack = new Stack<>();
+        for (int i = N - 1; i >= 0; i--) {
+            View child = mStackScroller.getChildAt(i);
+            if (!(child instanceof ExpandableNotificationRow)) {
+                continue;
+            }
+            stack.push((ExpandableNotificationRow) child);
+        }
+        while(!stack.isEmpty()) {
+            ExpandableNotificationRow row = stack.pop();
+            NotificationData.Entry entry = row.getEntry();
+            boolean isChildNotification =
+                    mGroupManager.isChildInGroupWithSummary(entry.notification);
+
+            row.setOnKeyguard(onKeyguard);
+
+            if (!onKeyguard) {
+                // If mAlwaysExpandNonGroupedNotification is false, then only expand the
+                // very first notification and if it's not a child of grouped notifications.
+                row.setSystemExpanded(mAlwaysExpandNonGroupedNotification
+                        || (visibleNotifications == 0 && !isChildNotification
+                        && !row.isLowPriority()));
+            }
+
+            entry.row.setShowAmbient(isDozing());
+            int userId = entry.notification.getUserId();
+            boolean suppressedSummary = mGroupManager.isSummaryOfSuppressedGroup(
+                    entry.notification) && !entry.row.isRemoved();
+            boolean showOnKeyguard = shouldShowOnKeyguard(entry.notification);
+            if (suppressedSummary
+                    || (isLockscreenPublicMode(userId) && !mShowLockscreenNotifications)
+                    || (onKeyguard && !showOnKeyguard)) {
+                entry.row.setVisibility(View.GONE);
+            } else {
+                boolean wasGone = entry.row.getVisibility() == View.GONE;
+                if (wasGone) {
+                    entry.row.setVisibility(View.VISIBLE);
+                }
+                if (!isChildNotification && !entry.row.isRemoved()) {
+                    if (wasGone) {
+                        // notify the scroller of a child addition
+                        mStackScroller.generateAddAnimation(entry.row,
+                                !showOnKeyguard /* fromMoreCard */);
+                    }
+                    visibleNotifications++;
+                }
+            }
+            if (row.isSummaryWithChildren()) {
+                List<ExpandableNotificationRow> notificationChildren =
+                        row.getNotificationChildren();
+                int size = notificationChildren.size();
+                for (int i = size - 1; i >= 0; i--) {
+                    stack.push(notificationChildren.get(i));
+                }
+            }
+        }
+        mNotificationPanel.setNoVisibleNotifications(visibleNotifications == 0);
+
+        // The following views will be moved to the end of mStackScroller. This counter represents
+        // the offset from the last child. Initialized to 1 for the very last position. It is post-
+        // incremented in the following "changeViewPosition" calls so that its value is correct for
+        // subsequent calls.
+        int offsetFromEnd = 1;
+        if (mDismissView != null) {
+            mStackScroller.changeViewPosition(mDismissView,
+                    mStackScroller.getChildCount() - offsetFromEnd++);
+        }
+
+        mStackScroller.changeViewPosition(mEmptyShadeView,
+                mStackScroller.getChildCount() - offsetFromEnd++);
+
+        // No post-increment for this call because it is the last one. Make sure to add one if
+        // another "changeViewPosition" call is ever added.
+        mStackScroller.changeViewPosition(mNotificationShelf,
+                mStackScroller.getChildCount() - offsetFromEnd);
+
+        // Scrim opacity varies based on notification count
+        mScrimController.setNotificationCount(mStackScroller.getNotGoneChildCount());
+    }
+
+    public boolean shouldShowOnKeyguard(StatusBarNotification sbn) {
+        return mShowLockscreenNotifications && !mNotificationData.isAmbient(sbn.getKey());
+    }
+
+    // extended in StatusBar
+    protected void setShowLockscreenNotifications(boolean show) {
+        mShowLockscreenNotifications = show;
+    }
+
+    protected void setLockScreenAllowRemoteInput(boolean allowLockscreenRemoteInput) {
+        mAllowLockscreenRemoteInput = allowLockscreenRemoteInput;
+    }
+
+    private void updateLockscreenNotificationSetting() {
+        final boolean show = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+                Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
+                1,
+                mCurrentUserId) != 0;
+        final int dpmFlags = mDevicePolicyManager.getKeyguardDisabledFeatures(
+                null /* admin */, mCurrentUserId);
+        final boolean allowedByDpm = (dpmFlags
+                & DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS) == 0;
+
+        setShowLockscreenNotifications(show && allowedByDpm);
+
+        if (ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT) {
+            final boolean remoteInput = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+                    Settings.Secure.LOCK_SCREEN_ALLOW_REMOTE_INPUT,
+                    0,
+                    mCurrentUserId) != 0;
+            final boolean remoteInputDpm =
+                    (dpmFlags & DevicePolicyManager.KEYGUARD_DISABLE_REMOTE_INPUT) == 0;
+
+            setLockScreenAllowRemoteInput(remoteInput && remoteInputDpm);
+        } else {
+            setLockScreenAllowRemoteInput(false);
+        }
+    }
+
+    public void updateNotification(StatusBarNotification notification, RankingMap ranking)
+            throws InflationException {
+        if (DEBUG) Log.d(TAG, "updateNotification(" + notification + ")");
+
+        final String key = notification.getKey();
+        abortExistingInflation(key);
+        Entry entry = mNotificationData.get(key);
+        if (entry == null) {
+            return;
+        }
+        mHeadsUpEntriesToRemoveOnSwitch.remove(entry);
+        mRemoteInputEntriesToRemoveOnCollapse.remove(entry);
+        if (key.equals(mKeyToRemoveOnGutsClosed)) {
+            mKeyToRemoveOnGutsClosed = null;
+            Log.w(TAG, "Notification that was kept for guts was updated. " + key);
+        }
+
+        Notification n = notification.getNotification();
+        mNotificationData.updateRanking(ranking);
+
+        final StatusBarNotification oldNotification = entry.notification;
+        entry.notification = notification;
+        mGroupManager.onEntryUpdated(entry, oldNotification);
+
+        entry.updateIcons(mContext, notification);
+        inflateViews(entry, mStackScroller);
+
+        mForegroundServiceController.updateNotification(notification,
+                mNotificationData.getImportance(key));
+
+        boolean shouldPeek = shouldPeek(entry, notification);
+        boolean alertAgain = alertAgain(entry, n);
+
+        updateHeadsUp(key, entry, shouldPeek, alertAgain);
+        updateNotifications();
+
+        if (!notification.isClearable()) {
+            // The user may have performed a dismiss action on the notification, since it's
+            // not clearable we should snap it back.
+            mStackScroller.snapViewIfNeeded(entry.row);
+        }
+
+        if (DEBUG) {
+            // Is this for you?
+            boolean isForCurrentUser = isNotificationForCurrentProfiles(notification);
+            Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you");
+        }
+
+        setAreThereNotifications();
+    }
+
+    protected void notifyHeadsUpGoingToSleep() {
+        maybeEscalateHeadsUp();
+    }
+
+    private boolean alertAgain(Entry oldEntry, Notification newNotification) {
+        return oldEntry == null || !oldEntry.hasInterrupted()
+                || (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0;
+    }
+
+    protected boolean shouldPeek(Entry entry) {
+        return shouldPeek(entry, entry.notification);
+    }
+
+    protected boolean shouldPeek(Entry entry, StatusBarNotification sbn) {
+        if (!mUseHeadsUp || isDeviceInVrMode()) {
+            if (DEBUG) Log.d(TAG, "No peeking: no huns or vr mode");
+            return false;
+        }
+
+        if (mNotificationData.shouldFilterOut(sbn)) {
+            if (DEBUG) Log.d(TAG, "No peeking: filtered notification: " + sbn.getKey());
+            return false;
+        }
+
+        boolean inUse = mPowerManager.isScreenOn() && !mSystemServicesProxy.isDreaming();
+
+        if (!inUse && !isDozing()) {
+            if (DEBUG) {
+                Log.d(TAG, "No peeking: not in use: " + sbn.getKey());
+            }
+            return false;
+        }
+
+        if (!isDozing() && mNotificationData.shouldSuppressScreenOn(sbn.getKey())) {
+            if (DEBUG) Log.d(TAG, "No peeking: suppressed by DND: " + sbn.getKey());
+            return false;
+        }
+
+        if (isDozing() && mNotificationData.shouldSuppressScreenOff(sbn.getKey())) {
+            if (DEBUG) Log.d(TAG, "No peeking: suppressed by DND: " + sbn.getKey());
+            return false;
+        }
+
+        if (entry.hasJustLaunchedFullScreenIntent()) {
+            if (DEBUG) Log.d(TAG, "No peeking: recent fullscreen: " + sbn.getKey());
+            return false;
+        }
+
+        if (isSnoozedPackage(sbn)) {
+            if (DEBUG) Log.d(TAG, "No peeking: snoozed package: " + sbn.getKey());
+            return false;
+        }
+
+        // Allow peeking for DEFAULT notifications only if we're on Ambient Display.
+        int importanceLevel = isDozing() ? NotificationManager.IMPORTANCE_DEFAULT
+                : NotificationManager.IMPORTANCE_HIGH;
+        if (mNotificationData.getImportance(sbn.getKey()) < importanceLevel) {
+            if (DEBUG) Log.d(TAG, "No peeking: unimportant notification: " + sbn.getKey());
+            return false;
+        }
+
+        if (sbn.getNotification().fullScreenIntent != null) {
+            if (mAccessibilityManager.isTouchExplorationEnabled()) {
+                if (DEBUG) Log.d(TAG, "No peeking: accessible fullscreen: " + sbn.getKey());
+                return false;
+            } else if (mDozing) {
+                // We never want heads up when we are dozing.
+                return false;
+            } else {
+                // we only allow head-up on the lockscreen if it doesn't have a fullscreen intent
+                return !mStatusBarKeyguardViewManager.isShowing()
+                        || mStatusBarKeyguardViewManager.isOccluded();
+            }
+        }
+
+        // Don't peek notifications that are suppressed due to group alert behavior
+        if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) {
+            if (DEBUG) Log.d(TAG, "No peeking: suppressed due to group alert behavior");
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * @return Whether the security bouncer from Keyguard is showing.
+     */
+    public boolean isBouncerShowing() {
+        return mBouncerShowing;
+    }
+
+    /**
+     * @return a PackageManger for userId or if userId is < 0 (USER_ALL etc) then
+     *         return PackageManager for mContext
+     */
+    public static PackageManager getPackageManagerForUser(Context context, int userId) {
+        Context contextForUser = context;
+        // UserHandle defines special userId as negative values, e.g. USER_ALL
+        if (userId >= 0) {
+            try {
+                // Create a context for the correct user so if a package isn't installed
+                // for user 0 we can still load information about the package.
+                contextForUser =
+                        context.createPackageContextAsUser(context.getPackageName(),
+                        Context.CONTEXT_RESTRICTED,
+                        new UserHandle(userId));
+            } catch (NameNotFoundException e) {
+                // Shouldn't fail to find the package name for system ui.
+            }
+        }
+        return contextForUser.getPackageManager();
+    }
+
+    @Override
+    public void logNotificationExpansion(String key, boolean userAction, boolean expanded) {
+        mUiOffloadThread.submit(() -> {
+            try {
+                mBarService.onNotificationExpansionChanged(key, userAction, expanded);
+            } catch (RemoteException e) {
+                // Ignore.
+            }
+        });
+    }
+
+    public boolean isKeyguardSecure() {
+        if (mStatusBarKeyguardViewManager == null) {
+            // startKeyguard() hasn't been called yet, so we don't know.
+            // Make sure anything that needs to know isKeyguardSecure() checks and re-checks this
+            // value onVisibilityChanged().
+            Slog.w(TAG, "isKeyguardSecure() called before startKeyguard(), returning false",
+                    new Throwable());
+            return false;
+        }
+        return mStatusBarKeyguardViewManager.isSecure();
+    }
+
+    @Override
+    public void showAssistDisclosure() {
+        if (mAssistManager != null) {
+            mAssistManager.showDisclosure();
+        }
+    }
+
+    public NotificationPanelView getPanel() {
+        return mNotificationPanel;
+    }
+
+    @Override
+    public void startAssist(Bundle args) {
+        if (mAssistManager != null) {
+            mAssistManager.startAssist(args);
+        }
+    }
+    // End Extra BaseStatusBarMethods.
+
+    private final Runnable mAutoDim = () -> {
+        if (mNavigationBar != null) {
+            mNavigationBar.getBarTransitions().setAutoDim(true);
+        }
+    };
+}
diff --git a/com/android/systemui/statusbar/phone/StatusBarIconController.java b/com/android/systemui/statusbar/phone/StatusBarIconController.java
new file mode 100644
index 0000000..c240765
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2017 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.phone;
+
+import android.annotation.ColorInt;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.settingslib.Utils;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+
+public interface StatusBarIconController {
+
+    public void addIconGroup(IconManager iconManager);
+    public void removeIconGroup(IconManager iconManager);
+    public void setExternalIcon(String slot);
+    public void setIcon(String slot, int resourceId, CharSequence contentDescription);
+    public void setIcon(String slot, StatusBarIcon icon);
+    public void setIconVisibility(String slotTty, boolean b);
+    public void removeIcon(String slot);
+
+    public static final String ICON_BLACKLIST = "icon_blacklist";
+
+    public static ArraySet<String> getIconBlacklist(String blackListStr) {
+        ArraySet<String> ret = new ArraySet<>();
+        if (blackListStr == null) {
+            blackListStr = "rotate,headset";
+        }
+        String[] blacklist = blackListStr.split(",");
+        for (String slot : blacklist) {
+            if (!TextUtils.isEmpty(slot)) {
+                ret.add(slot);
+            }
+        }
+        return ret;
+    }
+
+    /**
+     * Version of ViewGroup that observers state from the DarkIconDispatcher.
+     */
+    public static class DarkIconManager extends IconManager {
+        private final DarkIconDispatcher mDarkIconDispatcher;
+        private int mIconHPadding;
+
+        public DarkIconManager(LinearLayout linearLayout) {
+            super(linearLayout);
+            mIconHPadding = mContext.getResources().getDimensionPixelSize(
+                    R.dimen.status_bar_icon_padding);
+            mDarkIconDispatcher = Dependency.get(DarkIconDispatcher.class);
+        }
+
+        @Override
+        protected void onIconAdded(int index, String slot, boolean blocked,
+                StatusBarIcon icon) {
+            StatusBarIconView v = addIcon(index, slot, blocked, icon);
+            mDarkIconDispatcher.addDarkReceiver(v);
+        }
+
+        @Override
+        protected LayoutParams onCreateLayoutParams() {
+            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+                    ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize);
+            lp.setMargins(mIconHPadding, 0, mIconHPadding, 0);
+            return lp;
+        }
+
+        @Override
+        protected void destroy() {
+            for (int i = 0; i < mGroup.getChildCount(); i++) {
+                mDarkIconDispatcher.removeDarkReceiver((ImageView) mGroup.getChildAt(i));
+            }
+            mGroup.removeAllViews();
+        }
+
+        @Override
+        protected void onRemoveIcon(int viewIndex) {
+            mDarkIconDispatcher.removeDarkReceiver((ImageView) mGroup.getChildAt(viewIndex));
+            super.onRemoveIcon(viewIndex);
+        }
+
+        @Override
+        public void onSetIcon(int viewIndex, StatusBarIcon icon) {
+            super.onSetIcon(viewIndex, icon);
+            mDarkIconDispatcher.applyDark((ImageView) mGroup.getChildAt(viewIndex));
+        }
+    }
+
+    public static class TintedIconManager extends IconManager {
+        private int mColor;
+
+        public TintedIconManager(ViewGroup group) {
+            super(group);
+        }
+
+        @Override
+        protected void onIconAdded(int index, String slot, boolean blocked, StatusBarIcon icon) {
+            StatusBarIconView v = addIcon(index, slot, blocked, icon);
+            v.setStaticDrawableColor(mColor);
+        }
+
+        public void setTint(int color) {
+            mColor = color;
+            for (int i = 0; i < mGroup.getChildCount(); i++) {
+                View child = mGroup.getChildAt(i);
+                if (child instanceof StatusBarIconView) {
+                    StatusBarIconView icon = (StatusBarIconView) child;
+                    icon.setStaticDrawableColor(mColor);
+                }
+            }
+        }
+    }
+
+    /**
+     * Turns info from StatusBarIconController into ImageViews in a ViewGroup.
+     */
+    public static class IconManager {
+        protected final ViewGroup mGroup;
+        protected final Context mContext;
+        protected final int mIconSize;
+
+        public IconManager(ViewGroup group) {
+            mGroup = group;
+            mContext = group.getContext();
+            mIconSize = mContext.getResources().getDimensionPixelSize(
+                    com.android.internal.R.dimen.status_bar_icon_size);
+        }
+
+        protected void onIconAdded(int index, String slot, boolean blocked,
+                StatusBarIcon icon) {
+            addIcon(index, slot, blocked, icon);
+        }
+
+        protected StatusBarIconView addIcon(int index, String slot, boolean blocked,
+                StatusBarIcon icon) {
+            StatusBarIconView view = onCreateStatusBarIconView(slot, blocked);
+            view.set(icon);
+            mGroup.addView(view, index, onCreateLayoutParams());
+            return view;
+        }
+
+        @VisibleForTesting
+        protected StatusBarIconView onCreateStatusBarIconView(String slot, boolean blocked) {
+            return new StatusBarIconView(mContext, slot, null, blocked);
+        }
+
+        protected LinearLayout.LayoutParams onCreateLayoutParams() {
+            return new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize);
+        }
+
+        protected void destroy() {
+            mGroup.removeAllViews();
+        }
+
+        protected void onIconExternal(int viewIndex, int height) {
+            ImageView imageView = (ImageView) mGroup.getChildAt(viewIndex);
+            imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
+            imageView.setAdjustViewBounds(true);
+            setHeightAndCenter(imageView, height);
+        }
+
+        protected void onDensityOrFontScaleChanged() {
+            for (int i = 0; i < mGroup.getChildCount(); i++) {
+                View child = mGroup.getChildAt(i);
+                LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+                        ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize);
+                child.setLayoutParams(lp);
+            }
+        }
+
+        private void setHeightAndCenter(ImageView imageView, int height) {
+            ViewGroup.LayoutParams params = imageView.getLayoutParams();
+            params.height = height;
+            if (params instanceof LinearLayout.LayoutParams) {
+                ((LinearLayout.LayoutParams) params).gravity = Gravity.CENTER_VERTICAL;
+            }
+            imageView.setLayoutParams(params);
+        }
+
+        protected void onRemoveIcon(int viewIndex) {
+            mGroup.removeViewAt(viewIndex);
+        }
+
+        public void onSetIcon(int viewIndex, StatusBarIcon icon) {
+            StatusBarIconView view = (StatusBarIconView) mGroup.getChildAt(viewIndex);
+            view.set(icon);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
new file mode 100644
index 0000000..68f8e06
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2017 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.phone;
+
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.Dependency;
+import com.android.systemui.Dumpable;
+import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+import com.android.systemui.statusbar.policy.IconLogger;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.tuner.TunerService.Tunable;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Receives the callbacks from CommandQueue related to icons and tracks the state of
+ * all the icons. Dispatches this state to any IconManagers that are currently
+ * registered with it.
+ */
+public class StatusBarIconControllerImpl extends StatusBarIconList implements Tunable,
+        ConfigurationListener, Dumpable, CommandQueue.Callbacks, StatusBarIconController {
+
+    private final DarkIconDispatcher mDarkIconDispatcher;
+
+    private Context mContext;
+    private DemoStatusIcons mDemoStatusIcons;
+
+    private final ArrayList<IconManager> mIconGroups = new ArrayList<>();
+
+    private final ArraySet<String> mIconBlacklist = new ArraySet<>();
+    private final IconLogger mIconLogger = Dependency.get(IconLogger.class);
+
+    public StatusBarIconControllerImpl(Context context) {
+        super(context.getResources().getStringArray(
+                com.android.internal.R.array.config_statusBarIcons));
+        Dependency.get(ConfigurationController.class).addCallback(this);
+        mDarkIconDispatcher = Dependency.get(DarkIconDispatcher.class);
+        mContext = context;
+
+        loadDimens();
+
+        SysUiServiceProvider.getComponent(context, CommandQueue.class)
+                .addCallbacks(this);
+        Dependency.get(TunerService.class).addTunable(this, ICON_BLACKLIST);
+    }
+
+    @Override
+    public void addIconGroup(IconManager group) {
+        mIconGroups.add(group);
+        for (int i = 0; i < mIcons.size(); i++) {
+            StatusBarIcon icon = mIcons.get(i);
+            if (icon != null) {
+                String slot = mSlots.get(i);
+                boolean blocked = mIconBlacklist.contains(slot);
+                group.onIconAdded(getViewIndex(getSlotIndex(slot)), slot, blocked, icon);
+            }
+        }
+    }
+
+    @Override
+    public void removeIconGroup(IconManager group) {
+        group.destroy();
+        mIconGroups.remove(group);
+    }
+
+    @Override
+    public void onTuningChanged(String key, String newValue) {
+        if (!ICON_BLACKLIST.equals(key)) {
+            return;
+        }
+        mIconBlacklist.clear();
+        mIconBlacklist.addAll(StatusBarIconController.getIconBlacklist(newValue));
+        ArrayList<StatusBarIcon> current = new ArrayList<>(mIcons);
+        ArrayList<String> currentSlots = new ArrayList<>(mSlots);
+        // Remove all the icons.
+        for (int i = current.size() - 1; i >= 0; i--) {
+            removeIcon(currentSlots.get(i));
+        }
+        // Add them all back
+        for (int i = 0; i < current.size(); i++) {
+            setIcon(currentSlots.get(i), current.get(i));
+        }
+    }
+
+    private void loadDimens() {
+    }
+
+    private void addSystemIcon(int index, StatusBarIcon icon) {
+        String slot = getSlot(index);
+        int viewIndex = getViewIndex(index);
+        boolean blocked = mIconBlacklist.contains(slot);
+
+        mIconLogger.onIconVisibility(getSlot(index), icon.visible);
+        mIconGroups.forEach(l -> l.onIconAdded(viewIndex, slot, blocked, icon));
+    }
+
+    @Override
+    public void setIcon(String slot, int resourceId, CharSequence contentDescription) {
+        int index = getSlotIndex(slot);
+        StatusBarIcon icon = getIcon(index);
+        if (icon == null) {
+            icon = new StatusBarIcon(UserHandle.SYSTEM, mContext.getPackageName(),
+                    Icon.createWithResource(mContext, resourceId), 0, 0, contentDescription);
+            setIcon(slot, icon);
+        } else {
+            icon.icon = Icon.createWithResource(mContext, resourceId);
+            icon.contentDescription = contentDescription;
+            handleSet(index, icon);
+        }
+    }
+
+    @Override
+    public void setExternalIcon(String slot) {
+        int viewIndex = getViewIndex(getSlotIndex(slot));
+        int height = mContext.getResources().getDimensionPixelSize(
+                R.dimen.status_bar_icon_drawing_size);
+        mIconGroups.forEach(l -> l.onIconExternal(viewIndex, height));
+    }
+
+    @Override
+    public void setIcon(String slot, StatusBarIcon icon) {
+        setIcon(getSlotIndex(slot), icon);
+    }
+
+    @Override
+    public void removeIcon(String slot) {
+        int index = getSlotIndex(slot);
+        removeIcon(index);
+    }
+
+    public void setIconVisibility(String slot, boolean visibility) {
+        int index = getSlotIndex(slot);
+        StatusBarIcon icon = getIcon(index);
+        if (icon == null || icon.visible == visibility) {
+            return;
+        }
+        icon.visible = visibility;
+        handleSet(index, icon);
+    }
+
+    @Override
+    public void removeIcon(int index) {
+        if (getIcon(index) == null) {
+            return;
+        }
+        mIconLogger.onIconHidden(getSlot(index));
+        super.removeIcon(index);
+        int viewIndex = getViewIndex(index);
+        mIconGroups.forEach(l -> l.onRemoveIcon(viewIndex));
+    }
+
+    @Override
+    public void setIcon(int index, StatusBarIcon icon) {
+        if (icon == null) {
+            removeIcon(index);
+            return;
+        }
+        boolean isNew = getIcon(index) == null;
+        super.setIcon(index, icon);
+        if (isNew) {
+            addSystemIcon(index, icon);
+        } else {
+            handleSet(index, icon);
+        }
+    }
+
+    private void handleSet(int index, StatusBarIcon icon) {
+        int viewIndex = getViewIndex(index);
+        mIconLogger.onIconVisibility(getSlot(index), icon.visible);
+        mIconGroups.forEach(l -> l.onSetIcon(viewIndex, icon));
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        // TODO: Dump info about all icon groups?
+        ViewGroup statusIcons = mIconGroups.get(0).mGroup;
+        int N = statusIcons.getChildCount();
+        pw.println("  icon views: " + N);
+        for (int i = 0; i < N; i++) {
+            StatusBarIconView ic = (StatusBarIconView) statusIcons.getChildAt(i);
+            pw.println("    [" + i + "] icon=" + ic);
+        }
+        super.dump(pw);
+    }
+
+    public void dispatchDemoCommand(String command, Bundle args) {
+        if (mDemoStatusIcons == null) {
+            // TODO: Rework how we handle demo mode.
+            int iconSize = mContext.getResources().getDimensionPixelSize(
+                    com.android.internal.R.dimen.status_bar_icon_size);
+            mDemoStatusIcons = new DemoStatusIcons((LinearLayout) mIconGroups.get(0).mGroup,
+                    iconSize);
+        }
+        mDemoStatusIcons.dispatchDemoCommand(command, args);
+    }
+
+    @Override
+    public void onDensityOrFontScaleChanged() {
+        loadDimens();
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/StatusBarIconList.java b/com/android/systemui/statusbar/phone/StatusBarIconList.java
new file mode 100644
index 0000000..f600908
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/StatusBarIconList.java
@@ -0,0 +1,86 @@
+/*
+ * 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.phone;
+
+import com.android.internal.statusbar.StatusBarIcon;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+public class StatusBarIconList {
+    protected ArrayList<String> mSlots = new ArrayList<>();
+    protected ArrayList<StatusBarIcon> mIcons = new ArrayList<>();
+
+    public StatusBarIconList(String[] slots) {
+        final int N = slots.length;
+        for (int i=0; i < N; i++) {
+            mSlots.add(slots[i]);
+            mIcons.add(null);
+        }
+    }
+
+    public int getSlotIndex(String slot) {
+        final int N = mSlots.size();
+        for (int i=0; i<N; i++) {
+            if (slot.equals(mSlots.get(i))) {
+                return i;
+            }
+        }
+        // Auto insert new items at the beginning.
+        mSlots.add(0, slot);
+        mIcons.add(0, null);
+        return 0;
+    }
+
+    public int size() {
+        return mSlots.size();
+    }
+
+    public void setIcon(int index, StatusBarIcon icon) {
+        mIcons.set(index, icon);
+    }
+
+    public void removeIcon(int index) {
+        mIcons.set(index, null);
+    }
+
+    public String getSlot(int index) {
+        return mSlots.get(index);
+    }
+
+    public StatusBarIcon getIcon(int index) {
+        return mIcons.get(index);
+    }
+
+    public int getViewIndex(int index) {
+        int count = 0;
+        for (int i = 0; i < index; i++) {
+            if (mIcons.get(i) != null) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    public void dump(PrintWriter pw) {
+        final int N = mSlots.size();
+        pw.println("  icon slots: " + N);
+        for (int i=0; i<N; i++) {
+            pw.printf("    %2d: (%s) %s\n", i, mSlots.get(i), mIcons.get(i));
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
new file mode 100644
index 0000000..bbce751
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -0,0 +1,723 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import static com.android.keyguard.KeyguardHostView.OnDismissAction;
+import static com.android.systemui.statusbar.phone.FingerprintUnlockController.MODE_WAKE_AND_UNLOCK;
+import static com.android.systemui.statusbar.phone.FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING;
+
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewRootImpl;
+import android.view.WindowManagerGlobal;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.LatencyTracker;
+import com.android.keyguard.ViewMediatorCallback;
+import com.android.systemui.DejankUtils;
+import com.android.systemui.Dependency;
+import com.android.systemui.SystemUIFactory;
+import com.android.systemui.keyguard.DismissCallbackRegistry;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.RemoteInputController;
+
+import java.util.ArrayList;
+
+/**
+ * Manages creating, showing, hiding and resetting the keyguard within the status bar. Calls back
+ * via {@link ViewMediatorCallback} to poke the wake lock and report that the keyguard is done,
+ * which is in turn, reported to this class by the current
+ * {@link com.android.keyguard.KeyguardViewBase}.
+ */
+public class StatusBarKeyguardViewManager implements RemoteInputController.Callback {
+
+    // When hiding the Keyguard with timing supplied from WindowManager, better be early than late.
+    private static final long HIDE_TIMING_CORRECTION_MS = - 16 * 3;
+
+    // Delay for showing the navigation bar when the bouncer appears. This should be kept in sync
+    // with the appear animations of the PIN/pattern/password views.
+    private static final long NAV_BAR_SHOW_DELAY_BOUNCER = 320;
+
+    private static final long WAKE_AND_UNLOCK_SCRIM_FADEOUT_DURATION_MS = 200;
+
+    // Duration of the Keyguard dismissal animation in case the user is currently locked. This is to
+    // make everything a bit slower to bridge a gap until the user is unlocked and home screen has
+    // dranw its first frame.
+    private static final long KEYGUARD_DISMISS_DURATION_LOCKED = 2000;
+
+    private static String TAG = "StatusBarKeyguardViewManager";
+
+    protected final Context mContext;
+    private final StatusBarWindowManager mStatusBarWindowManager;
+    private final boolean mDisplayBlanksAfterDoze;
+
+    protected LockPatternUtils mLockPatternUtils;
+    protected ViewMediatorCallback mViewMediatorCallback;
+    protected StatusBar mStatusBar;
+    private ScrimController mScrimController;
+    private FingerprintUnlockController mFingerprintUnlockController;
+
+    private ViewGroup mContainer;
+
+    private boolean mScreenTurnedOn;
+    protected KeyguardBouncer mBouncer;
+    protected boolean mShowing;
+    protected boolean mOccluded;
+    protected boolean mRemoteInputActive;
+    private boolean mDozing;
+
+    protected boolean mFirstUpdate = true;
+    protected boolean mLastShowing;
+    protected boolean mLastOccluded;
+    private boolean mLastBouncerShowing;
+    private boolean mLastBouncerDismissible;
+    protected boolean mLastRemoteInputActive;
+    private boolean mLastDozing;
+    private boolean mLastDeferScrimFadeOut;
+    private int mLastFpMode;
+
+    private OnDismissAction mAfterKeyguardGoneAction;
+    private final ArrayList<Runnable> mAfterKeyguardGoneRunnables = new ArrayList<>();
+    private boolean mDeferScrimFadeOut;
+
+    // Dismiss action to be launched when we stop dozing or the keyguard is gone.
+    private DismissWithActionRequest mPendingWakeupAction;
+
+    private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback =
+            new KeyguardUpdateMonitorCallback() {
+        @Override
+        public void onEmergencyCallAction() {
+
+            // Since we won't get a setOccluded call we have to reset the view manually such that
+            // the bouncer goes away.
+            if (mOccluded) {
+                reset(true /* hideBouncerWhenShowing */);
+            }
+        }
+    };
+
+    public StatusBarKeyguardViewManager(Context context, ViewMediatorCallback callback,
+            LockPatternUtils lockPatternUtils) {
+        mContext = context;
+        mViewMediatorCallback = callback;
+        mLockPatternUtils = lockPatternUtils;
+        mStatusBarWindowManager = Dependency.get(StatusBarWindowManager.class);
+        KeyguardUpdateMonitor.getInstance(context).registerCallback(mUpdateMonitorCallback);
+        mDisplayBlanksAfterDoze = context.getResources().getBoolean(
+                com.android.internal.R.bool.config_displayBlanksAfterDoze);
+    }
+
+    public void registerStatusBar(StatusBar statusBar,
+            ViewGroup container,
+            ScrimController scrimController,
+            FingerprintUnlockController fingerprintUnlockController,
+            DismissCallbackRegistry dismissCallbackRegistry) {
+        mStatusBar = statusBar;
+        mContainer = container;
+        mScrimController = scrimController;
+        mFingerprintUnlockController = fingerprintUnlockController;
+        mBouncer = SystemUIFactory.getInstance().createKeyguardBouncer(mContext,
+                mViewMediatorCallback, mLockPatternUtils, container, dismissCallbackRegistry);
+    }
+
+    /**
+     * Show the keyguard.  Will handle creating and attaching to the view manager
+     * lazily.
+     */
+    public void show(Bundle options) {
+        mShowing = true;
+        mStatusBarWindowManager.setKeyguardShowing(true);
+        mScrimController.abortKeyguardFadingOut();
+        reset(true /* hideBouncerWhenShowing */);
+    }
+
+    /**
+     * Shows the notification keyguard or the bouncer depending on
+     * {@link KeyguardBouncer#needsFullscreenBouncer()}.
+     */
+    protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing) {
+        if (mBouncer.needsFullscreenBouncer() && !mDozing) {
+            // The keyguard might be showing (already). So we need to hide it.
+            mStatusBar.hideKeyguard();
+            mBouncer.show(true /* resetSecuritySelection */);
+        } else {
+            mStatusBar.showKeyguard();
+            if (hideBouncerWhenShowing) {
+                hideBouncer(false /* destroyView */);
+                mBouncer.prepare();
+            }
+        }
+        updateStates();
+    }
+
+    private void hideBouncer(boolean destroyView) {
+        mBouncer.hide(destroyView);
+        cancelPendingWakeupAction();
+    }
+
+    private void showBouncer() {
+        if (mShowing) {
+            mBouncer.show(false /* resetSecuritySelection */);
+        }
+        updateStates();
+    }
+
+    public void dismissWithAction(OnDismissAction r, Runnable cancelAction,
+            boolean afterKeyguardGone) {
+        if (mShowing) {
+            cancelPendingWakeupAction();
+            // If we're dozing, this needs to be delayed until after we wake up - unless we're
+            // wake-and-unlocking, because there dozing will last until the end of the transition.
+            if (mDozing && !isWakeAndUnlocking()) {
+                mPendingWakeupAction = new DismissWithActionRequest(
+                        r, cancelAction, afterKeyguardGone);
+                return;
+            }
+
+            if (!afterKeyguardGone) {
+                mBouncer.showWithDismissAction(r, cancelAction);
+            } else {
+                mAfterKeyguardGoneAction = r;
+                mBouncer.show(false /* resetSecuritySelection */);
+            }
+        }
+        updateStates();
+    }
+
+    private boolean isWakeAndUnlocking() {
+        int mode = mFingerprintUnlockController.getMode();
+        return mode == MODE_WAKE_AND_UNLOCK || mode == MODE_WAKE_AND_UNLOCK_PULSING;
+    }
+
+    /**
+     * Adds a {@param runnable} to be executed after Keyguard is gone.
+     */
+    public void addAfterKeyguardGoneRunnable(Runnable runnable) {
+        mAfterKeyguardGoneRunnables.add(runnable);
+    }
+
+    /**
+     * Reset the state of the view.
+     */
+    public void reset(boolean hideBouncerWhenShowing) {
+        if (mShowing) {
+            if (mOccluded && !mDozing) {
+                mStatusBar.hideKeyguard();
+                mStatusBar.stopWaitingForKeyguardExit();
+                if (hideBouncerWhenShowing || mBouncer.needsFullscreenBouncer()) {
+                    hideBouncer(false /* destroyView */);
+                }
+            } else {
+                showBouncerOrKeyguard(hideBouncerWhenShowing);
+            }
+            KeyguardUpdateMonitor.getInstance(mContext).sendKeyguardReset();
+            updateStates();
+        }
+    }
+
+    public void onStartedGoingToSleep() {
+        // TODO: remove
+    }
+
+    public void onFinishedGoingToSleep() {
+        mBouncer.onScreenTurnedOff();
+    }
+
+    public void onStartedWakingUp() {
+        // TODO: remove
+    }
+
+    public void onScreenTurningOn() {
+        // TODO: remove
+    }
+
+    public void onScreenTurnedOn() {
+        Trace.beginSection("StatusBarKeyguardViewManager#onScreenTurnedOn");
+        mScreenTurnedOn = true;
+        if (mDeferScrimFadeOut) {
+            mDeferScrimFadeOut = false;
+            animateScrimControllerKeyguardFadingOut(0, WAKE_AND_UNLOCK_SCRIM_FADEOUT_DURATION_MS,
+                    true /* skipFirstFrame */);
+            updateStates();
+        }
+        Trace.endSection();
+    }
+
+    @Override
+    public void onRemoteInputActive(boolean active) {
+        mRemoteInputActive = active;
+        updateStates();
+    }
+
+    public void setDozing(boolean dozing) {
+        if (mDozing != dozing) {
+            mDozing = dozing;
+            if (dozing || mBouncer.needsFullscreenBouncer() || mOccluded) {
+                reset(dozing /* hideBouncerWhenShowing */);
+            }
+            updateStates();
+
+            if (!dozing) {
+                launchPendingWakeupAction();
+            }
+        }
+    }
+
+    public void onScreenTurnedOff() {
+        mScreenTurnedOn = false;
+    }
+
+    public void notifyDeviceWakeUpRequested() {
+        // TODO: remove
+    }
+
+    public void setNeedsInput(boolean needsInput) {
+        mStatusBarWindowManager.setKeyguardNeedsInput(needsInput);
+    }
+
+    public boolean isUnlockWithWallpaper() {
+        return mStatusBarWindowManager.isShowingWallpaper();
+    }
+
+    public void setOccluded(boolean occluded, boolean animate) {
+        mStatusBar.setOccluded(occluded);
+        if (occluded && !mOccluded && mShowing) {
+            if (mStatusBar.isInLaunchTransition()) {
+                mOccluded = true;
+                mStatusBar.fadeKeyguardAfterLaunchTransition(null /* beforeFading */,
+                        new Runnable() {
+                            @Override
+                            public void run() {
+                                mStatusBarWindowManager.setKeyguardOccluded(mOccluded);
+                                reset(true /* hideBouncerWhenShowing */);
+                            }
+                        });
+                return;
+            }
+        }
+        boolean isOccluding = !mOccluded && occluded;
+        mOccluded = occluded;
+        if (mShowing) {
+            mStatusBar.updateMediaMetaData(false, animate && !occluded);
+        }
+        mStatusBarWindowManager.setKeyguardOccluded(occluded);
+
+        // setDozing(false) will call reset once we stop dozing.
+        if (!mDozing) {
+            // If Keyguard is reshown, don't hide the bouncer as it might just have been requested
+            // by a FLAG_DISMISS_KEYGUARD_ACTIVITY.
+            reset(isOccluding /* hideBouncerWhenShowing*/);
+        }
+        if (animate && !occluded && mShowing) {
+            mStatusBar.animateKeyguardUnoccluding();
+        }
+    }
+
+    public boolean isOccluded() {
+        return mOccluded;
+    }
+
+    /**
+     * Starts the animation before we dismiss Keyguard, i.e. an disappearing animation on the
+     * security view of the bouncer.
+     *
+     * @param finishRunnable the runnable to be run after the animation finished, or {@code null} if
+     *                       no action should be run
+     */
+    public void startPreHideAnimation(Runnable finishRunnable) {
+        if (mBouncer.isShowing()) {
+            mBouncer.startPreHideAnimation(finishRunnable);
+        } else if (finishRunnable != null) {
+            finishRunnable.run();
+        }
+    }
+
+    /**
+     * Hides the keyguard view
+     */
+    public void hide(long startTime, long fadeoutDuration) {
+        mShowing = false;
+        launchPendingWakeupAction();
+
+        if (KeyguardUpdateMonitor.getInstance(mContext).needsSlowUnlockTransition()) {
+            fadeoutDuration = KEYGUARD_DISMISS_DURATION_LOCKED;
+        }
+        long uptimeMillis = SystemClock.uptimeMillis();
+        long delay = Math.max(0, startTime + HIDE_TIMING_CORRECTION_MS - uptimeMillis);
+
+        if (mStatusBar.isInLaunchTransition() ) {
+            mStatusBar.fadeKeyguardAfterLaunchTransition(new Runnable() {
+                @Override
+                public void run() {
+                    mStatusBarWindowManager.setKeyguardShowing(false);
+                    mStatusBarWindowManager.setKeyguardFadingAway(true);
+                    hideBouncer(true /* destroyView */);
+                    updateStates();
+                    mScrimController.animateKeyguardFadingOut(
+                            StatusBar.FADE_KEYGUARD_START_DELAY,
+                            StatusBar.FADE_KEYGUARD_DURATION, null,
+                            false /* skipFirstFrame */);
+                }
+            }, new Runnable() {
+                @Override
+                public void run() {
+                    mStatusBar.hideKeyguard();
+                    mStatusBarWindowManager.setKeyguardFadingAway(false);
+                    mViewMediatorCallback.keyguardGone();
+                    executeAfterKeyguardGoneAction();
+                }
+            });
+        } else {
+            executeAfterKeyguardGoneAction();
+            boolean wakeUnlockPulsing =
+                    mFingerprintUnlockController.getMode() == MODE_WAKE_AND_UNLOCK_PULSING;
+            if (wakeUnlockPulsing) {
+                delay = 0;
+                fadeoutDuration = 240;
+            }
+            mStatusBar.setKeyguardFadingAway(startTime, delay, fadeoutDuration);
+            mFingerprintUnlockController.startKeyguardFadingAway();
+            hideBouncer(true /* destroyView */);
+            if (wakeUnlockPulsing) {
+                mStatusBarWindowManager.setKeyguardFadingAway(true);
+                mStatusBar.fadeKeyguardWhilePulsing();
+                animateScrimControllerKeyguardFadingOut(delay, fadeoutDuration,
+                        mStatusBar::hideKeyguard, false /* skipFirstFrame */);
+            } else {
+                mFingerprintUnlockController.startKeyguardFadingAway();
+                mStatusBar.setKeyguardFadingAway(startTime, delay, fadeoutDuration);
+                boolean staying = mStatusBar.hideKeyguard();
+                if (!staying) {
+                    mStatusBarWindowManager.setKeyguardFadingAway(true);
+                    if (mFingerprintUnlockController.getMode() == MODE_WAKE_AND_UNLOCK) {
+                        boolean turnedOnSinceAuth =
+                                mFingerprintUnlockController.hasScreenTurnedOnSinceAuthenticating();
+                        if (!mScreenTurnedOn || mDisplayBlanksAfterDoze && !turnedOnSinceAuth) {
+                            // Not ready to animate yet; either because the screen is not on yet,
+                            // or it is on but will turn off before waking out of doze.
+                            mDeferScrimFadeOut = true;
+                        } else {
+
+                            // Screen is already on, don't defer with fading out.
+                            animateScrimControllerKeyguardFadingOut(0,
+                                    WAKE_AND_UNLOCK_SCRIM_FADEOUT_DURATION_MS,
+                                    true /* skipFirstFrame */);
+                        }
+                    } else {
+                        animateScrimControllerKeyguardFadingOut(delay, fadeoutDuration,
+                                false /* skipFirstFrame */);
+                    }
+                } else {
+                    mScrimController.animateGoingToFullShade(delay, fadeoutDuration);
+                    mStatusBar.finishKeyguardFadingAway();
+                    mFingerprintUnlockController.finishKeyguardFadingAway();
+                }
+            }
+            updateStates();
+            mStatusBarWindowManager.setKeyguardShowing(false);
+            mViewMediatorCallback.keyguardGone();
+        }
+    }
+
+    public void onDensityOrFontScaleChanged() {
+        hideBouncer(true /* destroyView */);
+    }
+
+    public void onOverlayChanged() {
+        hideBouncer(true /* destroyView */);
+        mBouncer.prepare();
+    }
+
+    private void animateScrimControllerKeyguardFadingOut(long delay, long duration,
+            boolean skipFirstFrame) {
+        animateScrimControllerKeyguardFadingOut(delay, duration, null /* endRunnable */,
+                skipFirstFrame);
+    }
+
+    private void animateScrimControllerKeyguardFadingOut(long delay, long duration,
+            final Runnable endRunnable, boolean skipFirstFrame) {
+        Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "Fading out", 0);
+        mScrimController.animateKeyguardFadingOut(delay, duration, new Runnable() {
+            @Override
+            public void run() {
+                if (endRunnable != null) {
+                    endRunnable.run();
+                }
+                mContainer.postDelayed(() -> mStatusBarWindowManager.setKeyguardFadingAway(false),
+                        100);
+                mStatusBar.finishKeyguardFadingAway();
+                mFingerprintUnlockController.finishKeyguardFadingAway();
+                WindowManagerGlobal.getInstance().trimMemory(
+                        ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
+                Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, "Fading out", 0);
+            }
+        }, skipFirstFrame);
+        if (mFingerprintUnlockController.getMode() == MODE_WAKE_AND_UNLOCK
+                && LatencyTracker.isEnabled(mContext)) {
+            DejankUtils.postAfterTraversal(() ->
+                    LatencyTracker.getInstance(mContext).onActionEnd(
+                            LatencyTracker.ACTION_FINGERPRINT_WAKE_AND_UNLOCK));
+        }
+    }
+
+    private void executeAfterKeyguardGoneAction() {
+        if (mAfterKeyguardGoneAction != null) {
+            mAfterKeyguardGoneAction.onDismiss();
+            mAfterKeyguardGoneAction = null;
+        }
+        for (int i = 0; i < mAfterKeyguardGoneRunnables.size(); i++) {
+            mAfterKeyguardGoneRunnables.get(i).run();
+        }
+        mAfterKeyguardGoneRunnables.clear();
+    }
+
+    /**
+     * Dismisses the keyguard by going to the next screen or making it gone.
+     */
+    public void dismissAndCollapse() {
+        mStatusBar.executeRunnableDismissingKeyguard(null, null, true, false, true);
+    }
+
+    public void dismiss() {
+        showBouncer();
+    }
+
+    /**
+     * WARNING: This method might cause Binder calls.
+     */
+    public boolean isSecure() {
+        return mBouncer.isSecure();
+    }
+
+    /**
+     * @return Whether the keyguard is showing
+     */
+    public boolean isShowing() {
+        return mShowing;
+    }
+
+    /**
+     * Notifies this manager that the back button has been pressed.
+     *
+     * @return whether the back press has been handled
+     */
+    public boolean onBackPressed() {
+        if (mBouncer.isShowing()) {
+            mStatusBar.endAffordanceLaunch();
+            reset(true /* hideBouncerWhenShowing */);
+            return true;
+        }
+        return false;
+    }
+
+    public boolean isBouncerShowing() {
+        return mBouncer.isShowing();
+    }
+
+    private long getNavBarShowDelay() {
+        if (mStatusBar.isKeyguardFadingAway()) {
+            return mStatusBar.getKeyguardFadingAwayDelay();
+        } else if (mBouncer.isShowing()) {
+            return NAV_BAR_SHOW_DELAY_BOUNCER;
+        } else {
+            // No longer dozing, or remote input is active. No delay.
+            return 0;
+        }
+    }
+
+    private Runnable mMakeNavigationBarVisibleRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mStatusBar.getNavigationBarView().getRootView().setVisibility(View.VISIBLE);
+        }
+    };
+
+    protected void updateStates() {
+        int vis = mContainer.getSystemUiVisibility();
+        boolean showing = mShowing;
+        boolean occluded = mOccluded;
+        boolean bouncerShowing = mBouncer.isShowing();
+        boolean bouncerDismissible = !mBouncer.isFullscreenBouncer();
+        boolean remoteInputActive = mRemoteInputActive;
+
+        if ((bouncerDismissible || !showing || remoteInputActive) !=
+                (mLastBouncerDismissible || !mLastShowing || mLastRemoteInputActive)
+                || mFirstUpdate) {
+            if (bouncerDismissible || !showing || remoteInputActive) {
+                mContainer.setSystemUiVisibility(vis & ~View.STATUS_BAR_DISABLE_BACK);
+            } else {
+                mContainer.setSystemUiVisibility(vis | View.STATUS_BAR_DISABLE_BACK);
+            }
+        }
+
+        boolean navBarVisible = isNavBarVisible();
+        boolean lastNavBarVisible = getLastNavBarVisible();
+        if (navBarVisible != lastNavBarVisible || mFirstUpdate) {
+            if (mStatusBar.getNavigationBarView() != null) {
+                if (navBarVisible) {
+                    long delay = getNavBarShowDelay();
+                    if (delay == 0) {
+                        mMakeNavigationBarVisibleRunnable.run();
+                    } else {
+                        mContainer.postOnAnimationDelayed(mMakeNavigationBarVisibleRunnable,
+                                delay);
+                    }
+                } else {
+                    mContainer.removeCallbacks(mMakeNavigationBarVisibleRunnable);
+                    mStatusBar.getNavigationBarView().getRootView().setVisibility(View.GONE);
+                }
+            }
+        }
+
+        if (bouncerShowing != mLastBouncerShowing || mFirstUpdate) {
+            mStatusBarWindowManager.setBouncerShowing(bouncerShowing);
+            mStatusBar.setBouncerShowing(bouncerShowing);
+            mScrimController.setBouncerShowing(bouncerShowing);
+        }
+
+        KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+        if ((showing && !occluded) != (mLastShowing && !mLastOccluded) || mFirstUpdate) {
+            updateMonitor.onKeyguardVisibilityChanged(showing && !occluded);
+        }
+        if (bouncerShowing != mLastBouncerShowing || mFirstUpdate) {
+            updateMonitor.sendKeyguardBouncerChanged(bouncerShowing);
+        }
+
+        mFirstUpdate = false;
+        mLastShowing = showing;
+        mLastOccluded = occluded;
+        mLastBouncerShowing = bouncerShowing;
+        mLastBouncerDismissible = bouncerDismissible;
+        mLastRemoteInputActive = remoteInputActive;
+        mLastDozing = mDozing;
+        mLastDeferScrimFadeOut = mDeferScrimFadeOut;
+        mLastFpMode = mFingerprintUnlockController.getMode();
+        mStatusBar.onKeyguardViewManagerStatesUpdated();
+    }
+
+    /**
+     * @return Whether the navigation bar should be made visible based on the current state.
+     */
+    protected boolean isNavBarVisible() {
+        int fpMode = mFingerprintUnlockController.getMode();
+        boolean keyguardShowing = mShowing && !mOccluded;
+        boolean hideWhileDozing = mDozing && fpMode != MODE_WAKE_AND_UNLOCK_PULSING;
+        return (!keyguardShowing && !hideWhileDozing || mBouncer.isShowing()
+                || mRemoteInputActive) && !mDeferScrimFadeOut;
+    }
+
+    /**
+     * @return Whether the navigation bar was made visible based on the last known state.
+     */
+    protected boolean getLastNavBarVisible() {
+        boolean keyguardShowing = mLastShowing && !mLastOccluded;
+        boolean hideWhileDozing = mLastDozing && mLastFpMode != MODE_WAKE_AND_UNLOCK_PULSING;
+        return (!keyguardShowing && !hideWhileDozing || mLastBouncerShowing
+                || mLastRemoteInputActive) && !mLastDeferScrimFadeOut;
+    }
+
+    public boolean shouldDismissOnMenuPressed() {
+        return mBouncer.shouldDismissOnMenuPressed();
+    }
+
+    public boolean interceptMediaKey(KeyEvent event) {
+        return mBouncer.interceptMediaKey(event);
+    }
+
+    public void readyForKeyguardDone() {
+        mViewMediatorCallback.readyForKeyguardDone();
+    }
+
+    public boolean shouldDisableWindowAnimationsForUnlock() {
+        return mStatusBar.isInLaunchTransition();
+    }
+
+    public boolean isGoingToNotificationShade() {
+        return mStatusBar.isGoingToNotificationShade();
+    }
+
+    public boolean isSecure(int userId) {
+        return mBouncer.isSecure() || mLockPatternUtils.isSecure(userId);
+    }
+
+    public void keyguardGoingAway() {
+        mStatusBar.keyguardGoingAway();
+    }
+
+    public void animateCollapsePanels(float speedUpFactor) {
+        mStatusBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */,
+                false /* delayed */, speedUpFactor);
+    }
+
+    /**
+     * Notifies that the user has authenticated by other means than using the bouncer, for example,
+     * fingerprint.
+     */
+    public void notifyKeyguardAuthenticated(boolean strongAuth) {
+        mBouncer.notifyKeyguardAuthenticated(strongAuth);
+    }
+
+    public void showBouncerMessage(String message, int color) {
+        mBouncer.showMessage(message, color);
+    }
+
+    public ViewRootImpl getViewRootImpl() {
+        return mStatusBar.getStatusBarView().getViewRootImpl();
+    }
+
+    public void launchPendingWakeupAction() {
+        DismissWithActionRequest request = mPendingWakeupAction;
+        mPendingWakeupAction = null;
+        if (request != null) {
+            if (mShowing) {
+                dismissWithAction(request.dismissAction, request.cancelAction,
+                        request.afterKeyguardGone);
+            } else if (request.dismissAction != null) {
+                request.dismissAction.onDismiss();
+            }
+        }
+    }
+
+    public void cancelPendingWakeupAction() {
+        DismissWithActionRequest request = mPendingWakeupAction;
+        mPendingWakeupAction = null;
+        if (request != null && request.cancelAction != null) {
+            request.cancelAction.run();
+        }
+    }
+
+    private static class DismissWithActionRequest {
+        final OnDismissAction dismissAction;
+        final Runnable cancelAction;
+        final boolean afterKeyguardGone;
+
+        DismissWithActionRequest(OnDismissAction dismissAction, Runnable cancelAction,
+                boolean afterKeyguardGone) {
+            this.dismissAction = dismissAction;
+            this.cancelAction = cancelAction;
+            this.afterKeyguardGone = afterKeyguardGone;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/StatusBarWindowManager.java b/com/android/systemui/statusbar/phone/StatusBarWindowManager.java
new file mode 100644
index 0000000..ed96b41
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/StatusBarWindowManager.java
@@ -0,0 +1,484 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.app.ActivityManager;
+import android.app.IActivityManager;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+
+import com.android.keyguard.R;
+import com.android.systemui.Dumpable;
+import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.statusbar.RemoteInputController;
+import com.android.systemui.statusbar.StatusBarState;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.reflect.Field;
+
+/**
+ * Encapsulates all logic for the status bar window state management.
+ */
+public class StatusBarWindowManager implements RemoteInputController.Callback, Dumpable {
+
+    private static final String TAG = "StatusBarWindowManager";
+
+    private final Context mContext;
+    private final WindowManager mWindowManager;
+    private final IActivityManager mActivityManager;
+    private View mStatusBarView;
+    private WindowManager.LayoutParams mLp;
+    private WindowManager.LayoutParams mLpChanged;
+    private boolean mHasTopUi;
+    private boolean mHasTopUiChanged;
+    private int mBarHeight;
+    private final boolean mKeyguardScreenRotation;
+    private float mScreenBrightnessDoze;
+    private final State mCurrentState = new State();
+    private OtherwisedCollapsedListener mListener;
+
+    public StatusBarWindowManager(Context context) {
+        mContext = context;
+        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+        mActivityManager = ActivityManager.getService();
+        mKeyguardScreenRotation = shouldEnableKeyguardScreenRotation();
+        mScreenBrightnessDoze = mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_screenBrightnessDoze) / 255f;
+    }
+
+    private boolean shouldEnableKeyguardScreenRotation() {
+        Resources res = mContext.getResources();
+        return SystemProperties.getBoolean("lockscreen.rot_override", false)
+                || res.getBoolean(R.bool.config_enableLockScreenRotation);
+    }
+
+    /**
+     * Adds the status bar view to the window manager.
+     *
+     * @param statusBarView The view to add.
+     * @param barHeight The height of the status bar in collapsed state.
+     */
+    public void add(View statusBarView, int barHeight) {
+
+        // Now that the status bar window encompasses the sliding panel and its
+        // translucent backdrop, the entire thing is made TRANSLUCENT and is
+        // hardware-accelerated.
+        mLp = new WindowManager.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                barHeight,
+                WindowManager.LayoutParams.TYPE_STATUS_BAR,
+                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                        | WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
+                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+                        | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+                        | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
+                PixelFormat.TRANSLUCENT);
+        mLp.token = new Binder();
+        mLp.gravity = Gravity.TOP;
+        mLp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+        mLp.setTitle("StatusBar");
+        mLp.packageName = mContext.getPackageName();
+        mStatusBarView = statusBarView;
+        mBarHeight = barHeight;
+        mWindowManager.addView(mStatusBarView, mLp);
+        mLpChanged = new WindowManager.LayoutParams();
+        mLpChanged.copyFrom(mLp);
+    }
+
+    public void setDozeScreenBrightness(int value) {
+        mScreenBrightnessDoze = value / 255f;
+    }
+
+    public void setKeyguardDark(boolean dark) {
+        int vis = mStatusBarView.getSystemUiVisibility();
+        if (dark) {
+            vis = vis | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+            vis = vis | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+        } else {
+            vis = vis & ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+            vis = vis & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+        }
+        mStatusBarView.setSystemUiVisibility(vis);
+    }
+
+    private void applyKeyguardFlags(State state) {
+        if (state.keyguardShowing) {
+            mLpChanged.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD;
+        } else {
+            mLpChanged.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD;
+        }
+
+        if (state.keyguardShowing && !state.backdropShowing && !state.dozing) {
+            mLpChanged.flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
+        } else {
+            mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
+        }
+    }
+
+    private void adjustScreenOrientation(State state) {
+        if (state.isKeyguardShowingAndNotOccluded() || state.dozing) {
+            if (mKeyguardScreenRotation) {
+                mLpChanged.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_USER;
+            } else {
+                mLpChanged.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+            }
+        } else {
+            mLpChanged.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+        }
+    }
+
+    private void applyFocusableFlag(State state) {
+        boolean panelFocusable = state.statusBarFocusable && state.panelExpanded;
+        if (state.bouncerShowing && (state.keyguardOccluded || state.keyguardNeedsInput)
+                || StatusBar.ENABLE_REMOTE_INPUT && state.remoteInputActive) {
+            mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+            mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+        } else if (state.isKeyguardShowingAndNotOccluded() || panelFocusable) {
+            mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+            mLpChanged.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+        } else {
+            mLpChanged.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+            mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+        }
+
+        mLpChanged.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+    }
+
+    private void applyHeight(State state) {
+        boolean expanded = isExpanded(state);
+        if (state.forcePluginOpen) {
+            mListener.setWouldOtherwiseCollapse(expanded);
+            expanded = true;
+        }
+        if (expanded) {
+            mLpChanged.height = ViewGroup.LayoutParams.MATCH_PARENT;
+        } else {
+            mLpChanged.height = mBarHeight;
+        }
+    }
+
+    private boolean isExpanded(State state) {
+        return !state.forceCollapsed && (state.isKeyguardShowingAndNotOccluded()
+                || state.panelVisible || state.keyguardFadingAway || state.bouncerShowing
+                || state.headsUpShowing || state.scrimsVisible);
+    }
+
+    private void applyFitsSystemWindows(State state) {
+        boolean fitsSystemWindows = !state.isKeyguardShowingAndNotOccluded();
+        if (mStatusBarView.getFitsSystemWindows() != fitsSystemWindows) {
+            mStatusBarView.setFitsSystemWindows(fitsSystemWindows);
+            mStatusBarView.requestApplyInsets();
+        }
+    }
+
+    private void applyUserActivityTimeout(State state) {
+        if (state.isKeyguardShowingAndNotOccluded()
+                && state.statusBarState == StatusBarState.KEYGUARD
+                && !state.qsExpanded) {
+            mLpChanged.userActivityTimeout = KeyguardViewMediator.AWAKE_INTERVAL_DEFAULT_MS;
+        } else {
+            mLpChanged.userActivityTimeout = -1;
+        }
+    }
+
+    private void applyInputFeatures(State state) {
+        if (state.isKeyguardShowingAndNotOccluded()
+                && state.statusBarState == StatusBarState.KEYGUARD
+                && !state.qsExpanded && !state.forceUserActivity) {
+            mLpChanged.inputFeatures |=
+                    WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY;
+        } else {
+            mLpChanged.inputFeatures &=
+                    ~WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY;
+        }
+    }
+
+    private void apply(State state) {
+        applyKeyguardFlags(state);
+        applyForceStatusBarVisibleFlag(state);
+        applyFocusableFlag(state);
+        adjustScreenOrientation(state);
+        applyHeight(state);
+        applyUserActivityTimeout(state);
+        applyInputFeatures(state);
+        applyFitsSystemWindows(state);
+        applyModalFlag(state);
+        applyBrightness(state);
+        applyHasTopUi(state);
+        applySleepToken(state);
+        if (mLp.copyFrom(mLpChanged) != 0) {
+            mWindowManager.updateViewLayout(mStatusBarView, mLp);
+        }
+        if (mHasTopUi != mHasTopUiChanged) {
+            try {
+                mActivityManager.setHasTopUi(mHasTopUiChanged);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to call setHasTopUi", e);
+            }
+            mHasTopUi = mHasTopUiChanged;
+        }
+    }
+
+    private void applyForceStatusBarVisibleFlag(State state) {
+        if (state.forceStatusBarVisible) {
+            mLpChanged.privateFlags |= WindowManager
+                    .LayoutParams.PRIVATE_FLAG_FORCE_STATUS_BAR_VISIBLE_TRANSPARENT;
+        } else {
+            mLpChanged.privateFlags &= ~WindowManager
+                    .LayoutParams.PRIVATE_FLAG_FORCE_STATUS_BAR_VISIBLE_TRANSPARENT;
+        }
+    }
+
+    private void applyModalFlag(State state) {
+        if (state.headsUpShowing) {
+            mLpChanged.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+        } else {
+            mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+        }
+    }
+
+    private void applyBrightness(State state) {
+        if (state.forceDozeBrightness) {
+            mLpChanged.screenBrightness = mScreenBrightnessDoze;
+        } else {
+            mLpChanged.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
+        }
+    }
+
+    private void applyHasTopUi(State state) {
+        mHasTopUiChanged = isExpanded(state);
+    }
+
+    private void applySleepToken(State state) {
+        if (state.dozing) {
+            mLpChanged.privateFlags |= LayoutParams.PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN;
+        } else {
+            mLpChanged.privateFlags &= ~LayoutParams.PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN;
+        }
+    }
+
+    public void setKeyguardShowing(boolean showing) {
+        mCurrentState.keyguardShowing = showing;
+        apply(mCurrentState);
+    }
+
+    public void setKeyguardOccluded(boolean occluded) {
+        mCurrentState.keyguardOccluded = occluded;
+        apply(mCurrentState);
+    }
+
+    public void setKeyguardNeedsInput(boolean needsInput) {
+        mCurrentState.keyguardNeedsInput = needsInput;
+        apply(mCurrentState);
+    }
+
+    public void setPanelVisible(boolean visible) {
+        mCurrentState.panelVisible = visible;
+        mCurrentState.statusBarFocusable = visible;
+        apply(mCurrentState);
+    }
+
+    public void setStatusBarFocusable(boolean focusable) {
+        mCurrentState.statusBarFocusable = focusable;
+        apply(mCurrentState);
+    }
+
+    public void setBouncerShowing(boolean showing) {
+        mCurrentState.bouncerShowing = showing;
+        apply(mCurrentState);
+    }
+
+    public void setBackdropShowing(boolean showing) {
+        mCurrentState.backdropShowing = showing;
+        apply(mCurrentState);
+    }
+
+    public void setKeyguardFadingAway(boolean keyguardFadingAway) {
+        mCurrentState.keyguardFadingAway = keyguardFadingAway;
+        apply(mCurrentState);
+    }
+
+    public void setQsExpanded(boolean expanded) {
+        mCurrentState.qsExpanded = expanded;
+        apply(mCurrentState);
+    }
+
+    public void setForceUserActivity(boolean forceUserActivity) {
+        mCurrentState.forceUserActivity = forceUserActivity;
+        apply(mCurrentState);
+    }
+
+    public void setScrimsVisible(boolean scrimsVisible) {
+        mCurrentState.scrimsVisible = scrimsVisible;
+        apply(mCurrentState);
+    }
+
+    public void setHeadsUpShowing(boolean showing) {
+        mCurrentState.headsUpShowing = showing;
+        apply(mCurrentState);
+    }
+
+    /**
+     * @param state The {@link StatusBarState} of the status bar.
+     */
+    public void setStatusBarState(int state) {
+        mCurrentState.statusBarState = state;
+        apply(mCurrentState);
+    }
+
+    public void setForceStatusBarVisible(boolean forceStatusBarVisible) {
+        mCurrentState.forceStatusBarVisible = forceStatusBarVisible;
+        apply(mCurrentState);
+    }
+
+    /**
+     * Force the window to be collapsed, even if it should theoretically be expanded.
+     * Used for when a heads-up comes in but we still need to wait for the touchable regions to
+     * be computed.
+     */
+    public void setForceWindowCollapsed(boolean force) {
+        mCurrentState.forceCollapsed = force;
+        apply(mCurrentState);
+    }
+
+    public void setPanelExpanded(boolean isExpanded) {
+        mCurrentState.panelExpanded = isExpanded;
+        apply(mCurrentState);
+    }
+
+    @Override
+    public void onRemoteInputActive(boolean remoteInputActive) {
+        mCurrentState.remoteInputActive = remoteInputActive;
+        apply(mCurrentState);
+    }
+
+    /**
+     * Set whether the screen brightness is forced to the value we use for doze mode by the status
+     * bar window.
+     */
+    public void setForceDozeBrightness(boolean forceDozeBrightness) {
+        mCurrentState.forceDozeBrightness = forceDozeBrightness;
+        apply(mCurrentState);
+    }
+
+    public void setDozing(boolean dozing) {
+        mCurrentState.dozing = dozing;
+        apply(mCurrentState);
+    }
+
+    public void setBarHeight(int barHeight) {
+        mBarHeight = barHeight;
+        apply(mCurrentState);
+    }
+
+    public void setForcePluginOpen(boolean forcePluginOpen) {
+        mCurrentState.forcePluginOpen = forcePluginOpen;
+        apply(mCurrentState);
+    }
+
+    public void setStateListener(OtherwisedCollapsedListener listener) {
+        mListener = listener;
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("StatusBarWindowManager state:");
+        pw.println(mCurrentState);
+    }
+
+    public boolean isShowingWallpaper() {
+        return !mCurrentState.backdropShowing;
+    }
+
+    private static class State {
+        boolean keyguardShowing;
+        boolean keyguardOccluded;
+        boolean keyguardNeedsInput;
+        boolean panelVisible;
+        boolean panelExpanded;
+        boolean statusBarFocusable;
+        boolean bouncerShowing;
+        boolean keyguardFadingAway;
+        boolean qsExpanded;
+        boolean headsUpShowing;
+        boolean forceStatusBarVisible;
+        boolean forceCollapsed;
+        boolean forceDozeBrightness;
+        boolean forceUserActivity;
+        boolean backdropShowing;
+
+        /**
+         * The {@link StatusBar} state from the status bar.
+         */
+        int statusBarState;
+
+        boolean remoteInputActive;
+        boolean forcePluginOpen;
+        boolean dozing;
+        boolean scrimsVisible;
+
+        private boolean isKeyguardShowingAndNotOccluded() {
+            return keyguardShowing && !keyguardOccluded;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder result = new StringBuilder();
+            String newLine = "\n";
+            result.append("Window State {");
+            result.append(newLine);
+
+            Field[] fields = this.getClass().getDeclaredFields();
+
+            // Print field names paired with their values
+            for (Field field : fields) {
+                result.append("  ");
+                try {
+                    result.append(field.getName());
+                    result.append(": ");
+                    //requires access to private field:
+                    result.append(field.get(this));
+                } catch (IllegalAccessException ex) {
+                }
+                result.append(newLine);
+            }
+            result.append("}");
+
+            return result.toString();
+        }
+    }
+
+    /**
+     * Custom listener to pipe data back to plugins about whether or not the status bar would be
+     * collapsed if not for the plugin.
+     * TODO: Find cleaner way to do this.
+     */
+    public interface OtherwisedCollapsedListener {
+        void setWouldOtherwiseCollapse(boolean otherwiseCollapse);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/StatusBarWindowView.java b/com/android/systemui/statusbar/phone/StatusBarWindowView.java
new file mode 100644
index 0000000..d7f11f7
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/StatusBarWindowView.java
@@ -0,0 +1,749 @@
+/*
+ * Copyright (C) 2012 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.phone;
+
+import android.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.LayoutRes;
+import android.app.StatusBarManager;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.media.session.MediaSessionLegacyHelper;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.ActionMode;
+import android.view.InputDevice;
+import android.view.InputQueue;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+import android.widget.FrameLayout;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.view.FloatingActionMode;
+import com.android.internal.widget.FloatingToolbar;
+import com.android.systemui.R;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.statusbar.DragDownHelper;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+
+public class StatusBarWindowView extends FrameLayout {
+    public static final String TAG = "StatusBarWindowView";
+    public static final boolean DEBUG = StatusBar.DEBUG;
+
+    private DragDownHelper mDragDownHelper;
+    private DoubleTapHelper mDoubleTapHelper;
+    private NotificationStackScrollLayout mStackScrollLayout;
+    private NotificationPanelView mNotificationPanel;
+    private View mBrightnessMirror;
+
+    private int mRightInset = 0;
+    private int mLeftInset = 0;
+
+    private StatusBar mService;
+    private final Paint mTransparentSrcPaint = new Paint();
+    private FalsingManager mFalsingManager;
+
+    // Implements the floating action mode for TextView's Cut/Copy/Past menu. Normally provided by
+    // DecorView, but since this is a special window we have to roll our own.
+    private View mFloatingActionModeOriginatingView;
+    private ActionMode mFloatingActionMode;
+    private FloatingToolbar mFloatingToolbar;
+    private ViewTreeObserver.OnPreDrawListener mFloatingToolbarPreDrawListener;
+    private boolean mTouchCancelled;
+    private boolean mTouchActive;
+
+    public StatusBarWindowView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setMotionEventSplittingEnabled(false);
+        mTransparentSrcPaint.setColor(0);
+        mTransparentSrcPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+        mFalsingManager = FalsingManager.getInstance(context);
+        mDoubleTapHelper = new DoubleTapHelper(this, active -> {}, () -> {
+            mService.wakeUpIfDozing(SystemClock.uptimeMillis(), this);
+            return true;
+        }, null, null);
+    }
+
+    @Override
+    protected boolean fitSystemWindows(Rect insets) {
+        if (getFitsSystemWindows()) {
+            boolean paddingChanged = insets.top != getPaddingTop()
+                    || insets.bottom != getPaddingBottom();
+
+            // Super-special right inset handling, because scrims and backdrop need to ignore it.
+            if (insets.right != mRightInset || insets.left != mLeftInset) {
+                mRightInset = insets.right;
+                mLeftInset = insets.left;
+                applyMargins();
+            }
+            // Drop top inset, and pass through bottom inset.
+            if (paddingChanged) {
+                setPadding(0, 0, 0, 0);
+            }
+            insets.left = 0;
+            insets.top = 0;
+            insets.right = 0;
+        } else {
+            if (mRightInset != 0 || mLeftInset != 0) {
+                mRightInset = 0;
+                mLeftInset = 0;
+                applyMargins();
+            }
+            boolean changed = getPaddingLeft() != 0
+                    || getPaddingRight() != 0
+                    || getPaddingTop() != 0
+                    || getPaddingBottom() != 0;
+            if (changed) {
+                setPadding(0, 0, 0, 0);
+            }
+            insets.top = 0;
+        }
+        return false;
+    }
+
+    private void applyMargins() {
+        final int N = getChildCount();
+        for (int i = 0; i < N; i++) {
+            View child = getChildAt(i);
+            if (child.getLayoutParams() instanceof LayoutParams) {
+                LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (!lp.ignoreRightInset
+                        && (lp.rightMargin != mRightInset || lp.leftMargin != mLeftInset)) {
+                    lp.rightMargin = mRightInset;
+                    lp.leftMargin = mLeftInset;
+                    child.requestLayout();
+                }
+            }
+        }
+    }
+
+    @Override
+    public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new LayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    protected FrameLayout.LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mStackScrollLayout = (NotificationStackScrollLayout) findViewById(
+                R.id.notification_stack_scroller);
+        mNotificationPanel = (NotificationPanelView) findViewById(R.id.notification_panel);
+        mBrightnessMirror = findViewById(R.id.brightness_mirror);
+    }
+
+    @Override
+    public void onViewAdded(View child) {
+        super.onViewAdded(child);
+        if (child.getId() == R.id.brightness_mirror) {
+            mBrightnessMirror = child;
+        }
+    }
+
+    public void setService(StatusBar service) {
+        mService = service;
+        setDragDownHelper(new DragDownHelper(getContext(), this, mStackScrollLayout, mService));
+    }
+
+    @VisibleForTesting
+    void setDragDownHelper(DragDownHelper dragDownHelper) {
+        mDragDownHelper = dragDownHelper;
+    }
+
+    @Override
+    protected void onAttachedToWindow () {
+        super.onAttachedToWindow();
+
+        // We need to ensure that our window doesn't suffer from overdraw which would normally
+        // occur if our window is translucent. Since we are drawing the whole window anyway with
+        // the scrim, we don't need the window to be cleared in the beginning.
+        if (mService.isScrimSrcModeEnabled()) {
+            IBinder windowToken = getWindowToken();
+            WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams();
+            lp.token = windowToken;
+            setLayoutParams(lp);
+            WindowManagerGlobal.getInstance().changeCanvasOpacity(windowToken, true);
+            setWillNotDraw(false);
+        } else {
+            setWillNotDraw(!DEBUG);
+        }
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        if (mService.interceptMediaKey(event)) {
+            return true;
+        }
+        if (super.dispatchKeyEvent(event)) {
+            return true;
+        }
+        boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
+        switch (event.getKeyCode()) {
+            case KeyEvent.KEYCODE_BACK:
+                if (!down) {
+                    mService.onBackPressed();
+                }
+                return true;
+            case KeyEvent.KEYCODE_MENU:
+                if (!down) {
+                    return mService.onMenuPressed();
+                }
+            case KeyEvent.KEYCODE_SPACE:
+                if (!down) {
+                    return mService.onSpacePressed();
+                }
+                break;
+            case KeyEvent.KEYCODE_VOLUME_DOWN:
+            case KeyEvent.KEYCODE_VOLUME_UP:
+                if (mService.isDozing()) {
+                    MediaSessionLegacyHelper.getHelper(mContext).sendVolumeKeyEvent(
+                            event, AudioManager.USE_DEFAULT_STREAM_TYPE, true);
+                    return true;
+                }
+                break;
+        }
+        return false;
+    }
+
+    public void setTouchActive(boolean touchActive) {
+        mTouchActive = touchActive;
+        mStackScrollLayout.setTouchActive(touchActive);
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        boolean isDown = ev.getActionMasked() == MotionEvent.ACTION_DOWN;
+        boolean isCancel = ev.getActionMasked() == MotionEvent.ACTION_CANCEL;
+        if (!isCancel && mService.shouldIgnoreTouch()) {
+            return false;
+        }
+        if (isDown && mNotificationPanel.isFullyCollapsed()) {
+            mNotificationPanel.startExpandLatencyTracking();
+        }
+        if (isDown) {
+            setTouchActive(true);
+            mTouchCancelled = false;
+        } else if (ev.getActionMasked() == MotionEvent.ACTION_UP
+                || ev.getActionMasked() == MotionEvent.ACTION_CANCEL) {
+            setTouchActive(false);
+        }
+        if (mTouchCancelled) {
+            return false;
+        }
+        mFalsingManager.onTouchEvent(ev, getWidth(), getHeight());
+        if (mBrightnessMirror != null && mBrightnessMirror.getVisibility() == VISIBLE) {
+            // Disallow new pointers while the brightness mirror is visible. This is so that you
+            // can't touch anything other than the brightness slider while the mirror is showing
+            // and the rest of the panel is transparent.
+            if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
+                return false;
+            }
+        }
+        if (isDown) {
+            mStackScrollLayout.closeControlsIfOutsideTouch(ev);
+        }
+        if (mService.isDozing()) {
+            mService.mDozeScrimController.extendPulse();
+        }
+
+        return super.dispatchTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (mService.isDozing() && !mStackScrollLayout.hasPulsingNotifications()) {
+            // Capture all touch events in always-on.
+            return true;
+        }
+        boolean intercept = false;
+        if (mNotificationPanel.isFullyExpanded()
+                && mStackScrollLayout.getVisibility() == View.VISIBLE
+                && mService.getBarState() == StatusBarState.KEYGUARD
+                && !mService.isBouncerShowing()
+                && !mService.isDozing()) {
+            intercept = mDragDownHelper.onInterceptTouchEvent(ev);
+        }
+        if (!intercept) {
+            super.onInterceptTouchEvent(ev);
+        }
+        if (intercept) {
+            MotionEvent cancellation = MotionEvent.obtain(ev);
+            cancellation.setAction(MotionEvent.ACTION_CANCEL);
+            mStackScrollLayout.onInterceptTouchEvent(cancellation);
+            mNotificationPanel.onInterceptTouchEvent(cancellation);
+            cancellation.recycle();
+        }
+        return intercept;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        boolean handled = false;
+        if (mService.isDozing()) {
+            mDoubleTapHelper.onTouchEvent(ev);
+            handled = true;
+        }
+        if ((mService.getBarState() == StatusBarState.KEYGUARD && !handled)
+                || mDragDownHelper.isDraggingDown()) {
+            // we still want to finish our drag down gesture when locking the screen
+            handled = mDragDownHelper.onTouchEvent(ev);
+        }
+        if (!handled) {
+            handled = super.onTouchEvent(ev);
+        }
+        final int action = ev.getAction();
+        if (!handled && (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL)) {
+            mService.setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false);
+        }
+        return handled;
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        if (mService.isScrimSrcModeEnabled()) {
+            // We need to ensure that our window is always drawn fully even when we have paddings,
+            // since we simulate it to be opaque.
+            int paddedBottom = getHeight() - getPaddingBottom();
+            int paddedRight = getWidth() - getPaddingRight();
+            if (getPaddingTop() != 0) {
+                canvas.drawRect(0, 0, getWidth(), getPaddingTop(), mTransparentSrcPaint);
+            }
+            if (getPaddingBottom() != 0) {
+                canvas.drawRect(0, paddedBottom, getWidth(), getHeight(), mTransparentSrcPaint);
+            }
+            if (getPaddingLeft() != 0) {
+                canvas.drawRect(0, getPaddingTop(), getPaddingLeft(), paddedBottom,
+                        mTransparentSrcPaint);
+            }
+            if (getPaddingRight() != 0) {
+                canvas.drawRect(paddedRight, getPaddingTop(), getWidth(), paddedBottom,
+                        mTransparentSrcPaint);
+            }
+        }
+        if (DEBUG) {
+            Paint pt = new Paint();
+            pt.setColor(0x80FFFF00);
+            pt.setStrokeWidth(12.0f);
+            pt.setStyle(Paint.Style.STROKE);
+            canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), pt);
+        }
+    }
+
+    public void cancelExpandHelper() {
+        if (mStackScrollLayout != null) {
+            mStackScrollLayout.cancelExpandHelper();
+        }
+    }
+
+    public void cancelCurrentTouch() {
+        if (mTouchActive) {
+            final long now = SystemClock.uptimeMillis();
+            MotionEvent event = MotionEvent.obtain(now, now,
+                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
+            event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+            dispatchTouchEvent(event);
+            event.recycle();
+            mTouchCancelled = true;
+        }
+    }
+
+    public class LayoutParams extends FrameLayout.LayoutParams {
+
+        public boolean ignoreRightInset;
+
+        public LayoutParams(int width, int height) {
+            super(width, height);
+        }
+
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+
+            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.StatusBarWindowView_Layout);
+            ignoreRightInset = a.getBoolean(
+                    R.styleable.StatusBarWindowView_Layout_ignoreRightInset, false);
+            a.recycle();
+        }
+    }
+
+    @Override
+    public ActionMode startActionModeForChild(View originalView, ActionMode.Callback callback,
+            int type) {
+        if (type == ActionMode.TYPE_FLOATING) {
+            return startActionMode(originalView, callback, type);
+        }
+        return super.startActionModeForChild(originalView, callback, type);
+    }
+
+    private ActionMode createFloatingActionMode(
+            View originatingView, ActionMode.Callback2 callback) {
+        if (mFloatingActionMode != null) {
+            mFloatingActionMode.finish();
+        }
+        cleanupFloatingActionModeViews();
+        mFloatingToolbar = new FloatingToolbar(mFakeWindow);
+        final FloatingActionMode mode =
+                new FloatingActionMode(mContext, callback, originatingView, mFloatingToolbar);
+        mFloatingActionModeOriginatingView = originatingView;
+        mFloatingToolbarPreDrawListener =
+                new ViewTreeObserver.OnPreDrawListener() {
+                    @Override
+                    public boolean onPreDraw() {
+                        mode.updateViewLocationInWindow();
+                        return true;
+                    }
+                };
+        return mode;
+    }
+
+    private void setHandledFloatingActionMode(ActionMode mode) {
+        mFloatingActionMode = mode;
+        mFloatingActionMode.invalidate();  // Will show the floating toolbar if necessary.
+        mFloatingActionModeOriginatingView.getViewTreeObserver()
+                .addOnPreDrawListener(mFloatingToolbarPreDrawListener);
+    }
+
+    private void cleanupFloatingActionModeViews() {
+        if (mFloatingToolbar != null) {
+            mFloatingToolbar.dismiss();
+            mFloatingToolbar = null;
+        }
+        if (mFloatingActionModeOriginatingView != null) {
+            if (mFloatingToolbarPreDrawListener != null) {
+                mFloatingActionModeOriginatingView.getViewTreeObserver()
+                        .removeOnPreDrawListener(mFloatingToolbarPreDrawListener);
+                mFloatingToolbarPreDrawListener = null;
+            }
+            mFloatingActionModeOriginatingView = null;
+        }
+    }
+
+    private ActionMode startActionMode(
+            View originatingView, ActionMode.Callback callback, int type) {
+        ActionMode.Callback2 wrappedCallback = new ActionModeCallback2Wrapper(callback);
+        ActionMode mode = createFloatingActionMode(originatingView, wrappedCallback);
+        if (mode != null && wrappedCallback.onCreateActionMode(mode, mode.getMenu())) {
+            setHandledFloatingActionMode(mode);
+        } else {
+            mode = null;
+        }
+        return mode;
+    }
+
+    private class ActionModeCallback2Wrapper extends ActionMode.Callback2 {
+        private final ActionMode.Callback mWrapped;
+
+        public ActionModeCallback2Wrapper(ActionMode.Callback wrapped) {
+            mWrapped = wrapped;
+        }
+
+        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+            return mWrapped.onCreateActionMode(mode, menu);
+        }
+
+        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+            requestFitSystemWindows();
+            return mWrapped.onPrepareActionMode(mode, menu);
+        }
+
+        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+            return mWrapped.onActionItemClicked(mode, item);
+        }
+
+        public void onDestroyActionMode(ActionMode mode) {
+            mWrapped.onDestroyActionMode(mode);
+            if (mode == mFloatingActionMode) {
+                cleanupFloatingActionModeViews();
+                mFloatingActionMode = null;
+            }
+            requestFitSystemWindows();
+        }
+
+        @Override
+        public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
+            if (mWrapped instanceof ActionMode.Callback2) {
+                ((ActionMode.Callback2) mWrapped).onGetContentRect(mode, view, outRect);
+            } else {
+                super.onGetContentRect(mode, view, outRect);
+            }
+        }
+    }
+
+    /**
+     * Minimal window to satisfy FloatingToolbar.
+     */
+    private Window mFakeWindow = new Window(mContext) {
+        @Override
+        public void takeSurface(SurfaceHolder.Callback2 callback) {
+        }
+
+        @Override
+        public void takeInputQueue(InputQueue.Callback callback) {
+        }
+
+        @Override
+        public boolean isFloating() {
+            return false;
+        }
+
+        @Override
+        public void alwaysReadCloseOnTouchAttr() {
+        }
+
+        @Override
+        public void setContentView(@LayoutRes int layoutResID) {
+        }
+
+        @Override
+        public void setContentView(View view) {
+        }
+
+        @Override
+        public void setContentView(View view, ViewGroup.LayoutParams params) {
+        }
+
+        @Override
+        public void addContentView(View view, ViewGroup.LayoutParams params) {
+        }
+
+        @Override
+        public void clearContentView() {
+        }
+
+        @Override
+        public View getCurrentFocus() {
+            return null;
+        }
+
+        @Override
+        public LayoutInflater getLayoutInflater() {
+            return null;
+        }
+
+        @Override
+        public void setTitle(CharSequence title) {
+        }
+
+        @Override
+        public void setTitleColor(@ColorInt int textColor) {
+        }
+
+        @Override
+        public void openPanel(int featureId, KeyEvent event) {
+        }
+
+        @Override
+        public void closePanel(int featureId) {
+        }
+
+        @Override
+        public void togglePanel(int featureId, KeyEvent event) {
+        }
+
+        @Override
+        public void invalidatePanelMenu(int featureId) {
+        }
+
+        @Override
+        public boolean performPanelShortcut(int featureId, int keyCode, KeyEvent event, int flags) {
+            return false;
+        }
+
+        @Override
+        public boolean performPanelIdentifierAction(int featureId, int id, int flags) {
+            return false;
+        }
+
+        @Override
+        public void closeAllPanels() {
+        }
+
+        @Override
+        public boolean performContextMenuIdentifierAction(int id, int flags) {
+            return false;
+        }
+
+        @Override
+        public void onConfigurationChanged(Configuration newConfig) {
+        }
+
+        @Override
+        public void setBackgroundDrawable(Drawable drawable) {
+        }
+
+        @Override
+        public void setFeatureDrawableResource(int featureId, @DrawableRes int resId) {
+        }
+
+        @Override
+        public void setFeatureDrawableUri(int featureId, Uri uri) {
+        }
+
+        @Override
+        public void setFeatureDrawable(int featureId, Drawable drawable) {
+        }
+
+        @Override
+        public void setFeatureDrawableAlpha(int featureId, int alpha) {
+        }
+
+        @Override
+        public void setFeatureInt(int featureId, int value) {
+        }
+
+        @Override
+        public void takeKeyEvents(boolean get) {
+        }
+
+        @Override
+        public boolean superDispatchKeyEvent(KeyEvent event) {
+            return false;
+        }
+
+        @Override
+        public boolean superDispatchKeyShortcutEvent(KeyEvent event) {
+            return false;
+        }
+
+        @Override
+        public boolean superDispatchTouchEvent(MotionEvent event) {
+            return false;
+        }
+
+        @Override
+        public boolean superDispatchTrackballEvent(MotionEvent event) {
+            return false;
+        }
+
+        @Override
+        public boolean superDispatchGenericMotionEvent(MotionEvent event) {
+            return false;
+        }
+
+        @Override
+        public View getDecorView() {
+            return StatusBarWindowView.this;
+        }
+
+        @Override
+        public View peekDecorView() {
+            return null;
+        }
+
+        @Override
+        public Bundle saveHierarchyState() {
+            return null;
+        }
+
+        @Override
+        public void restoreHierarchyState(Bundle savedInstanceState) {
+        }
+
+        @Override
+        protected void onActive() {
+        }
+
+        @Override
+        public void setChildDrawable(int featureId, Drawable drawable) {
+        }
+
+        @Override
+        public void setChildInt(int featureId, int value) {
+        }
+
+        @Override
+        public boolean isShortcutKey(int keyCode, KeyEvent event) {
+            return false;
+        }
+
+        @Override
+        public void setVolumeControlStream(int streamType) {
+        }
+
+        @Override
+        public int getVolumeControlStream() {
+            return 0;
+        }
+
+        @Override
+        public int getStatusBarColor() {
+            return 0;
+        }
+
+        @Override
+        public void setStatusBarColor(@ColorInt int color) {
+        }
+
+        @Override
+        public int getNavigationBarColor() {
+            return 0;
+        }
+
+        @Override
+        public void setNavigationBarColor(@ColorInt int color) {
+        }
+
+        @Override
+        public void setDecorCaptionShade(int decorCaptionShade) {
+        }
+
+        @Override
+        public void setResizingCaptionDrawable(Drawable drawable) {
+        }
+
+        @Override
+        public void onMultiWindowModeChanged() {
+        }
+
+        @Override
+        public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
+        }
+
+        @Override
+        public void reportActivityRelaunched() {
+        }
+    };
+
+}
+
diff --git a/com/android/systemui/statusbar/phone/SystemUIDialog.java b/com/android/systemui/statusbar/phone/SystemUIDialog.java
new file mode 100644
index 0000000..378dad7
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.UserHandle;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.KeyguardMonitor;
+
+/**
+ * Base class for dialogs that should appear over panels and keyguard.
+ */
+public class SystemUIDialog extends AlertDialog {
+
+    private final Context mContext;
+
+    public SystemUIDialog(Context context) {
+        this(context, R.style.Theme_SystemUI_Dialog);
+    }
+
+    public SystemUIDialog(Context context, int theme) {
+        super(context, theme);
+        mContext = context;
+
+        applyFlags(this);
+        WindowManager.LayoutParams attrs = getWindow().getAttributes();
+        attrs.setTitle(getClass().getSimpleName());
+        getWindow().setAttributes(attrs);
+
+        registerDismissListener(this);
+    }
+
+    public void setShowForAllUsers(boolean show) {
+        setShowForAllUsers(this, show);
+    }
+
+    public void setMessage(int resId) {
+        setMessage(mContext.getString(resId));
+    }
+
+    public void setPositiveButton(int resId, OnClickListener onClick) {
+        setButton(BUTTON_POSITIVE, mContext.getString(resId), onClick);
+    }
+
+    public void setNegativeButton(int resId, OnClickListener onClick) {
+        setButton(BUTTON_NEGATIVE, mContext.getString(resId), onClick);
+    }
+
+    public static void setShowForAllUsers(Dialog dialog, boolean show) {
+        if (show) {
+            dialog.getWindow().getAttributes().privateFlags |=
+                    WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
+        } else {
+            dialog.getWindow().getAttributes().privateFlags &=
+                    ~WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
+        }
+    }
+
+    public static void setWindowOnTop(Dialog dialog) {
+        if (Dependency.get(KeyguardMonitor.class).isShowing()) {
+            dialog.getWindow().setType(LayoutParams.TYPE_STATUS_BAR_PANEL);
+        } else {
+            dialog.getWindow().setType(LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
+        }
+    }
+
+    public static AlertDialog applyFlags(AlertDialog dialog) {
+        dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL);
+        dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
+                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+        return dialog;
+    }
+
+    public static void registerDismissListener(Dialog dialog) {
+        boolean[] registered = new boolean[1];
+        Context context = dialog.getContext();
+        final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (dialog != null) {
+                    dialog.dismiss();
+                }
+            }
+        };
+        context.registerReceiverAsUser(mReceiver, UserHandle.CURRENT,
+                new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), null, null);
+        registered[0] = true;
+        dialog.setOnDismissListener(d -> {
+            if (registered[0]) {
+                context.unregisterReceiver(mReceiver);
+                registered[0] = false;
+            }
+        });
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/TrustDrawable.java b/com/android/systemui/statusbar/phone/TrustDrawable.java
new file mode 100644
index 0000000..028da86
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/TrustDrawable.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.animation.Interpolator;
+
+import com.android.settingslib.Utils;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+
+public class TrustDrawable extends Drawable {
+
+    private static final long ENTERING_FROM_UNSET_START_DELAY = 200;
+    private static final long VISIBLE_DURATION = 1000;
+    private static final long EXIT_DURATION = 500;
+    private static final long ENTER_DURATION = 500;
+
+    private static final int ALPHA_VISIBLE_MIN = 0x26;
+    private static final int ALPHA_VISIBLE_MAX = 0x4c;
+
+    private static final int STATE_UNSET = -1;
+    private static final int STATE_GONE = 0;
+    private static final int STATE_ENTERING = 1;
+    private static final int STATE_VISIBLE = 2;
+    private static final int STATE_EXITING = 3;
+
+    private int mAlpha;
+    private boolean mAnimating;
+
+    private int mCurAlpha;
+    private float mCurInnerRadius;
+    private Animator mCurAnimator;
+    private int mState = STATE_UNSET;
+    private Paint mPaint;
+    private boolean mTrustManaged;
+
+    private final float mInnerRadiusVisibleMin;
+    private final float mInnerRadiusVisibleMax;
+    private final float mInnerRadiusExit;
+    private final float mInnerRadiusEnter;
+    private final float mThickness;
+
+    private final Animator mVisibleAnimator;
+
+    public TrustDrawable(Context context) {
+        Resources r = context.getResources();
+        mInnerRadiusVisibleMin = r.getDimension(R.dimen.trust_circle_inner_radius_visible_min);
+        mInnerRadiusVisibleMax = r.getDimension(R.dimen.trust_circle_inner_radius_visible_max);
+        mInnerRadiusExit = r.getDimension(R.dimen.trust_circle_inner_radius_exit);
+        mInnerRadiusEnter = r.getDimension(R.dimen.trust_circle_inner_radius_enter);
+        mThickness = r.getDimension(R.dimen.trust_circle_thickness);
+
+        mCurInnerRadius = mInnerRadiusEnter;
+
+        mVisibleAnimator = makeVisibleAnimator();
+
+        mPaint = new Paint();
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setColor(Utils.getColorAttr(context, R.attr.wallpaperTextColor));
+        mPaint.setAntiAlias(true);
+        mPaint.setStrokeWidth(mThickness);
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        int newAlpha = (mCurAlpha * mAlpha) / 256;
+        if (newAlpha == 0) {
+            return;
+        }
+        final Rect r = getBounds();
+        mPaint.setAlpha(newAlpha);
+        canvas.drawCircle(r.exactCenterX(), r.exactCenterY(), mCurInnerRadius, mPaint);
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        mAlpha = alpha;
+    }
+
+    @Override
+    public int getAlpha() {
+        return mAlpha;
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter colorFilter) {
+        throw new UnsupportedOperationException("not implemented");
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+
+    public void start() {
+        if (!mAnimating) {
+            mAnimating = true;
+            updateState(true);
+            invalidateSelf();
+        }
+    }
+
+    public void stop() {
+        if (mAnimating) {
+            mAnimating = false;
+            if (mCurAnimator != null) {
+                mCurAnimator.cancel();
+                mCurAnimator = null;
+            }
+            mState = STATE_UNSET;
+            mCurAlpha = 0;
+            mCurInnerRadius = mInnerRadiusEnter;
+            invalidateSelf();
+        }
+    }
+
+    public void setTrustManaged(boolean trustManaged) {
+        if (trustManaged == mTrustManaged && mState != STATE_UNSET) return;
+        mTrustManaged = trustManaged;
+        updateState(true);
+    }
+
+    private void updateState(boolean allowTransientState) {
+        if (!mAnimating) {
+            return;
+        }
+
+        int nextState = mState;
+        if (mState == STATE_UNSET) {
+            nextState = mTrustManaged ? STATE_ENTERING : STATE_GONE;
+        } else if (mState == STATE_GONE) {
+            if (mTrustManaged) nextState = STATE_ENTERING;
+        } else if (mState == STATE_ENTERING) {
+            if (!mTrustManaged) nextState = STATE_EXITING;
+        } else if (mState == STATE_VISIBLE) {
+            if (!mTrustManaged) nextState = STATE_EXITING;
+        } else if (mState == STATE_EXITING) {
+            if (mTrustManaged) nextState = STATE_ENTERING;
+        }
+        if (!allowTransientState) {
+            if (nextState == STATE_ENTERING) nextState = STATE_VISIBLE;
+            if (nextState == STATE_EXITING) nextState = STATE_GONE;
+        }
+
+        if (nextState != mState) {
+            if (mCurAnimator != null) {
+                mCurAnimator.cancel();
+                mCurAnimator = null;
+            }
+
+            if (nextState == STATE_GONE) {
+                mCurAlpha = 0;
+                mCurInnerRadius = mInnerRadiusEnter;
+            } else if (nextState == STATE_ENTERING) {
+                mCurAnimator = makeEnterAnimator(mCurInnerRadius, mCurAlpha);
+                if (mState == STATE_UNSET) {
+                    mCurAnimator.setStartDelay(ENTERING_FROM_UNSET_START_DELAY);
+                }
+            } else if (nextState == STATE_VISIBLE) {
+                mCurAlpha = ALPHA_VISIBLE_MAX;
+                mCurInnerRadius = mInnerRadiusVisibleMax;
+                mCurAnimator = mVisibleAnimator;
+            } else if (nextState == STATE_EXITING) {
+                mCurAnimator = makeExitAnimator(mCurInnerRadius, mCurAlpha);
+            }
+
+            mState = nextState;
+            if (mCurAnimator != null) {
+                mCurAnimator.start();
+            }
+            invalidateSelf();
+        }
+    }
+
+    private Animator makeVisibleAnimator() {
+        return makeAnimators(mInnerRadiusVisibleMax, mInnerRadiusVisibleMin,
+                ALPHA_VISIBLE_MAX, ALPHA_VISIBLE_MIN, VISIBLE_DURATION,
+                Interpolators.ACCELERATE_DECELERATE,
+                true /* repeating */, false /* stateUpdateListener */);
+    }
+
+    private Animator makeEnterAnimator(float radius, int alpha) {
+        return makeAnimators(radius, mInnerRadiusVisibleMax,
+                alpha, ALPHA_VISIBLE_MAX, ENTER_DURATION, Interpolators.LINEAR_OUT_SLOW_IN,
+                false /* repeating */, true /* stateUpdateListener */);
+    }
+
+    private Animator makeExitAnimator(float radius, int alpha) {
+        return makeAnimators(radius, mInnerRadiusExit,
+                alpha, 0, EXIT_DURATION, Interpolators.FAST_OUT_SLOW_IN,
+                false /* repeating */, true /* stateUpdateListener */);
+    }
+
+    private Animator makeAnimators(float startRadius, float endRadius,
+            int startAlpha, int endAlpha, long duration, Interpolator interpolator,
+            boolean repeating, boolean stateUpdateListener) {
+        ValueAnimator alphaAnimator = configureAnimator(
+                ValueAnimator.ofInt(startAlpha, endAlpha),
+                duration, mAlphaUpdateListener, interpolator, repeating);
+        ValueAnimator sizeAnimator = configureAnimator(
+                ValueAnimator.ofFloat(startRadius, endRadius),
+                duration, mRadiusUpdateListener, interpolator, repeating);
+
+        AnimatorSet set = new AnimatorSet();
+        set.playTogether(alphaAnimator, sizeAnimator);
+        if (stateUpdateListener) {
+            set.addListener(new StateUpdateAnimatorListener());
+        }
+        return set;
+    }
+
+    private ValueAnimator configureAnimator(ValueAnimator animator, long duration,
+            ValueAnimator.AnimatorUpdateListener updateListener, Interpolator interpolator,
+            boolean repeating) {
+        animator.setDuration(duration);
+        animator.addUpdateListener(updateListener);
+        animator.setInterpolator(interpolator);
+        if (repeating) {
+            animator.setRepeatCount(ValueAnimator.INFINITE);
+            animator.setRepeatMode(ValueAnimator.REVERSE);
+        }
+        return animator;
+    }
+
+    private final ValueAnimator.AnimatorUpdateListener mAlphaUpdateListener =
+            new ValueAnimator.AnimatorUpdateListener() {
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            mCurAlpha = (int) animation.getAnimatedValue();
+            invalidateSelf();
+        }
+    };
+
+    private final ValueAnimator.AnimatorUpdateListener mRadiusUpdateListener =
+            new ValueAnimator.AnimatorUpdateListener() {
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            mCurInnerRadius = (float) animation.getAnimatedValue();
+            invalidateSelf();
+        }
+    };
+
+    private class StateUpdateAnimatorListener extends AnimatorListenerAdapter {
+        boolean mCancelled;
+
+        @Override
+        public void onAnimationStart(Animator animation) {
+            mCancelled = false;
+        }
+
+        @Override
+        public void onAnimationCancel(Animator animation) {
+            mCancelled = true;
+        }
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            if (!mCancelled) {
+                updateState(false);
+            }
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/UnlockMethodCache.java b/com/android/systemui/statusbar/phone/UnlockMethodCache.java
new file mode 100644
index 0000000..f9c2130
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/UnlockMethodCache.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.content.Context;
+import android.os.Trace;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+
+import java.util.ArrayList;
+
+/**
+ * Caches whether the current unlock method is insecure, taking trust into account. This information
+ * might be a little bit out of date and should not be used for actual security decisions; it should
+ * be only used for visual indications.
+ */
+public class UnlockMethodCache {
+
+    private static UnlockMethodCache sInstance;
+
+    private final LockPatternUtils mLockPatternUtils;
+    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private final ArrayList<OnUnlockMethodChangedListener> mListeners = new ArrayList<>();
+    /** Whether the user configured a secure unlock method (PIN, password, etc.) */
+    private boolean mSecure;
+    /** Whether the unlock method is currently insecure (insecure method or trusted environment) */
+    private boolean mCanSkipBouncer;
+    private boolean mTrustManaged;
+    private boolean mFaceUnlockRunning;
+    private boolean mTrusted;
+
+    private UnlockMethodCache(Context ctx) {
+        mLockPatternUtils = new LockPatternUtils(ctx);
+        mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(ctx);
+        KeyguardUpdateMonitor.getInstance(ctx).registerCallback(mCallback);
+        update(true /* updateAlways */);
+    }
+
+    public static UnlockMethodCache getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new UnlockMethodCache(context);
+        }
+        return sInstance;
+    }
+
+    /**
+     * @return whether the user configured a secure unlock method like PIN, password, etc.
+     */
+    public boolean isMethodSecure() {
+        return mSecure;
+    }
+
+    public boolean isTrusted() {
+        return mTrusted;
+    }
+
+    /**
+     * @return whether the lockscreen is currently insecure, and the bouncer won't be shown
+     */
+    public boolean canSkipBouncer() {
+        return mCanSkipBouncer;
+    }
+
+    public void addListener(OnUnlockMethodChangedListener listener) {
+        mListeners.add(listener);
+    }
+
+    public void removeListener(OnUnlockMethodChangedListener listener) {
+        mListeners.remove(listener);
+    }
+
+    private void update(boolean updateAlways) {
+        Trace.beginSection("UnlockMethodCache#update");
+        int user = KeyguardUpdateMonitor.getCurrentUser();
+        boolean secure = mLockPatternUtils.isSecure(user);
+        boolean canSkipBouncer = !secure ||  mKeyguardUpdateMonitor.getUserCanSkipBouncer(user);
+        boolean trustManaged = mKeyguardUpdateMonitor.getUserTrustIsManaged(user);
+        boolean trusted = mKeyguardUpdateMonitor.getUserHasTrust(user);
+        boolean faceUnlockRunning = mKeyguardUpdateMonitor.isFaceUnlockRunning(user)
+                && trustManaged;
+        boolean changed = secure != mSecure || canSkipBouncer != mCanSkipBouncer ||
+                trustManaged != mTrustManaged  || faceUnlockRunning != mFaceUnlockRunning;
+        if (changed || updateAlways) {
+            mSecure = secure;
+            mCanSkipBouncer = canSkipBouncer;
+            mTrusted = trusted;
+            mTrustManaged = trustManaged;
+            mFaceUnlockRunning = faceUnlockRunning;
+            notifyListeners();
+        }
+        Trace.endSection();
+    }
+
+    private void notifyListeners() {
+        for (OnUnlockMethodChangedListener listener : mListeners) {
+            listener.onUnlockMethodStateChanged();
+        }
+    }
+
+    private final KeyguardUpdateMonitorCallback mCallback = new KeyguardUpdateMonitorCallback() {
+        @Override
+        public void onUserSwitchComplete(int userId) {
+            update(false /* updateAlways */);
+        }
+
+        @Override
+        public void onTrustChanged(int userId) {
+            update(false /* updateAlways */);
+        }
+
+        @Override
+        public void onTrustManagedChanged(int userId) {
+            update(false /* updateAlways */);
+        }
+
+        @Override
+        public void onStartedWakingUp() {
+            update(false /* updateAlways */);
+        }
+
+        @Override
+        public void onFingerprintAuthenticated(int userId) {
+            Trace.beginSection("KeyguardUpdateMonitorCallback#onFingerprintAuthenticated");
+            if (!mKeyguardUpdateMonitor.isUnlockingWithFingerprintAllowed()) {
+                Trace.endSection();
+                return;
+            }
+            update(false /* updateAlways */);
+            Trace.endSection();
+        }
+
+        @Override
+        public void onFaceUnlockStateChanged(boolean running, int userId) {
+            update(false /* updateAlways */);
+        }
+
+        @Override
+        public void onStrongAuthStateChanged(int userId) {
+            update(false /* updateAlways */);
+        }
+
+        @Override
+        public void onScreenTurnedOff() {
+            update(false /* updateAlways */);
+        }
+
+        @Override
+        public void onKeyguardVisibilityChanged(boolean showing) {
+            update(false /* updateAlways */);
+        }
+    };
+
+    public boolean isTrustManaged() {
+        return mTrustManaged;
+    }
+
+    public boolean isFaceUnlockRunning() {
+        return mFaceUnlockRunning;
+    }
+
+    public static interface OnUnlockMethodChangedListener {
+        void onUnlockMethodStateChanged();
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/UserAvatarView.java b/com/android/systemui/statusbar/phone/UserAvatarView.java
new file mode 100644
index 0000000..dc1b35d
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/UserAvatarView.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.settingslib.drawable.UserIconDrawable;
+import com.android.systemui.R;
+
+/**
+ * A view that displays a user image cropped to a circle with an optional frame.
+ */
+public class UserAvatarView extends View {
+
+    private final UserIconDrawable mDrawable = new UserIconDrawable();
+
+    public UserAvatarView(Context context, AttributeSet attrs,
+            int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.UserAvatarView, defStyleAttr, defStyleRes);
+        final int N = a.getIndexCount();
+        for (int i = 0; i < N; i++) {
+            int attr = a.getIndex(i);
+            switch (attr) {
+                case R.styleable.UserAvatarView_avatarPadding:
+                    setAvatarPadding(a.getDimension(attr, 0));
+                    break;
+                case R.styleable.UserAvatarView_frameWidth:
+                    setFrameWidth(a.getDimension(attr, 0));
+                    break;
+                case R.styleable.UserAvatarView_framePadding:
+                    setFramePadding(a.getDimension(attr, 0));
+                    break;
+                case R.styleable.UserAvatarView_frameColor:
+                    setFrameColor(a.getColorStateList(attr));
+                    break;
+                case R.styleable.UserAvatarView_badgeDiameter:
+                    setBadgeDiameter(a.getDimension(attr, 0));
+                    break;
+                case R.styleable.UserAvatarView_badgeMargin:
+                    setBadgeMargin(a.getDimension(attr, 0));
+                    break;
+            }
+        }
+        a.recycle();
+        setBackground(mDrawable);
+    }
+
+    public UserAvatarView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public UserAvatarView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public UserAvatarView(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * @deprecated use {@link #setAvatar(Bitmap)} instead.
+     */
+    @Deprecated
+    public void setBitmap(Bitmap bitmap) {
+        setAvatar(bitmap);
+    }
+
+    public void setFrameColor(ColorStateList color) {
+        mDrawable.setFrameColor(color);
+    }
+
+    public void setFrameWidth(float frameWidth) {
+        mDrawable.setFrameWidth(frameWidth);
+    }
+
+    public void setFramePadding(float framePadding) {
+        mDrawable.setFramePadding(framePadding);
+    }
+
+    public void setAvatarPadding(float avatarPadding) {
+        mDrawable.setPadding(avatarPadding);
+    }
+
+    public void setBadgeDiameter(float diameter) {
+        mDrawable.setBadgeRadius(diameter * 0.5f);
+    }
+
+    public void setBadgeMargin(float margin) {
+        mDrawable.setBadgeMargin(margin);
+    }
+
+    public void setAvatar(Bitmap avatar) {
+        mDrawable.setIcon(avatar);
+        mDrawable.setBadge(null);
+    }
+
+    public void setAvatarWithBadge(Bitmap avatar, int userId) {
+        mDrawable.setIcon(avatar);
+        mDrawable.setBadgeIfManagedUser(getContext(), userId);
+    }
+
+    public void setDrawable(Drawable d) {
+        if (d instanceof UserIconDrawable) {
+            throw new RuntimeException("Recursively adding UserIconDrawable");
+        }
+        mDrawable.setIconDrawable(d);
+        mDrawable.setBadge(null);
+    }
+
+    public void setDrawableWithBadge(Drawable d, int userId) {
+        if (d instanceof UserIconDrawable) {
+            throw new RuntimeException("Recursively adding UserIconDrawable");
+        }
+        mDrawable.setIconDrawable(d);
+        mDrawable.setBadgeIfManagedUser(getContext(), userId);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/VelocityTrackerFactory.java b/com/android/systemui/statusbar/phone/VelocityTrackerFactory.java
new file mode 100644
index 0000000..e153b85
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/VelocityTrackerFactory.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.content.Context;
+
+import com.android.systemui.R;
+
+/**
+ * A class to generate {@link VelocityTrackerInterface}, depending on the configuration.
+ */
+public class VelocityTrackerFactory {
+
+    public static final String PLATFORM_IMPL = "platform";
+    public static final String NOISY_IMPL = "noisy";
+
+    public static VelocityTrackerInterface obtain(Context ctx) {
+        String tracker = ctx.getResources().getString(R.string.velocity_tracker_impl);
+        switch (tracker) {
+            case NOISY_IMPL:
+                return NoisyVelocityTracker.obtain();
+            case PLATFORM_IMPL:
+                return PlatformVelocityTracker.obtain();
+            default:
+                throw new IllegalStateException("Invalid tracker: " + tracker);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/VelocityTrackerInterface.java b/com/android/systemui/statusbar/phone/VelocityTrackerInterface.java
new file mode 100644
index 0000000..a54b054
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/VelocityTrackerInterface.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2014 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.phone;
+
+import android.view.MotionEvent;
+
+/**
+ * An interface for a velocity tracker to delegate. To be implemented by different velocity tracking
+ * algorithms.
+ */
+public interface VelocityTrackerInterface {
+    public void addMovement(MotionEvent event);
+    public void computeCurrentVelocity(int units);
+    public float getXVelocity();
+    public float getYVelocity();
+    public void recycle();
+}
diff --git a/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java b/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java
new file mode 100644
index 0000000..c0a6837
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.net.wifi.WifiManager.ActionListener;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.settingslib.wifi.AccessPoint;
+import com.android.settingslib.wifi.WifiTracker;
+import com.android.settingslib.wifi.WifiTracker.WifiListener;
+import com.android.systemui.R;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+public class AccessPointControllerImpl
+        implements NetworkController.AccessPointController, WifiListener {
+    private static final String TAG = "AccessPointController";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    // This string extra specifies a network to open the connect dialog on, so the user can enter
+    // network credentials.  This is used by quick settings for secured networks.
+    private static final String EXTRA_START_CONNECT_SSID = "wifi_start_connect_ssid";
+
+    private static final int[] ICONS = {
+        R.drawable.ic_qs_wifi_full_0,
+        R.drawable.ic_qs_wifi_full_1,
+        R.drawable.ic_qs_wifi_full_2,
+        R.drawable.ic_qs_wifi_full_3,
+        R.drawable.ic_qs_wifi_full_4,
+    };
+
+    private final Context mContext;
+    private final ArrayList<AccessPointCallback> mCallbacks = new ArrayList<AccessPointCallback>();
+    private final WifiTracker mWifiTracker;
+    private final UserManager mUserManager;
+
+    private int mCurrentUser;
+
+    public AccessPointControllerImpl(Context context, Looper bgLooper) {
+        mContext = context;
+        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+        mWifiTracker = new WifiTracker(context, this, bgLooper, false, true);
+        mCurrentUser = ActivityManager.getCurrentUser();
+    }
+
+    public boolean canConfigWifi() {
+        return !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_WIFI,
+                new UserHandle(mCurrentUser));
+    }
+
+    public void onUserSwitched(int newUserId) {
+        mCurrentUser = newUserId;
+    }
+
+    @Override
+    public void addAccessPointCallback(AccessPointCallback callback) {
+        if (callback == null || mCallbacks.contains(callback)) return;
+        if (DEBUG) Log.d(TAG, "addCallback " + callback);
+        mCallbacks.add(callback);
+        if (mCallbacks.size() == 1) {
+            mWifiTracker.startTracking();
+        }
+    }
+
+    @Override
+    public void removeAccessPointCallback(AccessPointCallback callback) {
+        if (callback == null) return;
+        if (DEBUG) Log.d(TAG, "removeCallback " + callback);
+        mCallbacks.remove(callback);
+        if (mCallbacks.isEmpty()) {
+            mWifiTracker.stopTracking();
+        }
+    }
+
+    @Override
+    public void scanForAccessPoints() {
+        if (DEBUG) Log.d(TAG, "force update APs!");
+        mWifiTracker.forceUpdate();
+        fireAcccessPointsCallback(mWifiTracker.getAccessPoints());
+    }
+
+    @Override
+    public int getIcon(AccessPoint ap) {
+        int level = ap.getLevel();
+        return ICONS[level >= 0 ? level : 0];
+    }
+
+    public boolean connect(AccessPoint ap) {
+        if (ap == null) return false;
+        if (DEBUG) Log.d(TAG, "connect networkId=" + ap.getConfig().networkId);
+        if (ap.isSaved()) {
+            mWifiTracker.getManager().connect(ap.getConfig().networkId, mConnectListener);
+        } else {
+            // Unknown network, need to add it.
+            if (ap.getSecurity() != AccessPoint.SECURITY_NONE) {
+                Intent intent = new Intent(Settings.ACTION_WIFI_SETTINGS);
+                intent.putExtra(EXTRA_START_CONNECT_SSID, ap.getSsidStr());
+                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                fireSettingsIntentCallback(intent);
+                return true;
+            } else {
+                ap.generateOpenNetworkConfig();
+                mWifiTracker.getManager().connect(ap.getConfig(), mConnectListener);
+            }
+        }
+        return false;
+    }
+
+    private void fireSettingsIntentCallback(Intent intent) {
+        for (AccessPointCallback callback : mCallbacks) {
+            callback.onSettingsActivityTriggered(intent);
+        }
+    }
+
+    private void fireAcccessPointsCallback(List<AccessPoint> aps) {
+        for (AccessPointCallback callback : mCallbacks) {
+            callback.onAccessPointsChanged(aps);
+        }
+    }
+
+    public void dump(PrintWriter pw) {
+        mWifiTracker.dump(pw);
+    }
+
+    @Override
+    public void onWifiStateChanged(int state) {
+    }
+
+    @Override
+    public void onConnectedChanged() {
+        fireAcccessPointsCallback(mWifiTracker.getAccessPoints());
+    }
+
+    @Override
+    public void onAccessPointsChanged() {
+        fireAcccessPointsCallback(mWifiTracker.getAccessPoints());
+    }
+
+    private final ActionListener mConnectListener = new ActionListener() {
+        @Override
+        public void onSuccess() {
+            if (DEBUG) Log.d(TAG, "connect success");
+        }
+
+        @Override
+        public void onFailure(int reason) {
+            if (DEBUG) Log.d(TAG, "connect failure reason=" + reason);
+        }
+    };
+}
diff --git a/com/android/systemui/statusbar/policy/AccessibilityContentDescriptions.java b/com/android/systemui/statusbar/policy/AccessibilityContentDescriptions.java
new file mode 100644
index 0000000..8f86e2d
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/AccessibilityContentDescriptions.java
@@ -0,0 +1,43 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+package com.android.systemui.statusbar.policy;
+
+import com.android.systemui.R;
+
+/**
+ * Content descriptions for accessibility support.
+ */
+public class AccessibilityContentDescriptions {
+
+    private AccessibilityContentDescriptions() {}
+    static final int[] PHONE_SIGNAL_STRENGTH = {
+        R.string.accessibility_no_phone,
+        R.string.accessibility_phone_one_bar,
+        R.string.accessibility_phone_two_bars,
+        R.string.accessibility_phone_three_bars,
+        R.string.accessibility_phone_signal_full
+    };
+
+    static final int[] DATA_CONNECTION_STRENGTH = {
+        R.string.accessibility_no_data,
+        R.string.accessibility_data_one_bar,
+        R.string.accessibility_data_two_bars,
+        R.string.accessibility_data_three_bars,
+        R.string.accessibility_data_signal_full
+    };
+
+    static final int[] WIFI_CONNECTION_STRENGTH = {
+        R.string.accessibility_no_wifi,
+        R.string.accessibility_wifi_one_bar,
+        R.string.accessibility_wifi_two_bars,
+        R.string.accessibility_wifi_three_bars,
+        R.string.accessibility_wifi_signal_full
+    };
+
+    static final int WIFI_NO_CONNECTION = R.string.accessibility_no_wifi;
+
+    static final int[] ETHERNET_CONNECTION_VALUES = {
+        R.string.accessibility_ethernet_disconnected,
+        R.string.accessibility_ethernet_connected,
+    };
+}
diff --git a/com/android/systemui/statusbar/policy/AccessibilityController.java b/com/android/systemui/statusbar/policy/AccessibilityController.java
new file mode 100644
index 0000000..cc431dd
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/AccessibilityController.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.content.Context;
+import android.view.accessibility.AccessibilityManager;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+public class AccessibilityController implements
+        AccessibilityManager.AccessibilityStateChangeListener,
+        AccessibilityManager.TouchExplorationStateChangeListener {
+
+    private final ArrayList<AccessibilityStateChangedCallback> mChangeCallbacks = new ArrayList<>();
+
+    private boolean mAccessibilityEnabled;
+    private boolean mTouchExplorationEnabled;
+
+    public AccessibilityController(Context context) {
+        AccessibilityManager am =
+                (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+        am.addTouchExplorationStateChangeListener(this);
+        am.addAccessibilityStateChangeListener(this);
+        mAccessibilityEnabled = am.isEnabled();
+        mTouchExplorationEnabled = am.isTouchExplorationEnabled();
+    }
+
+    public boolean isAccessibilityEnabled() {
+        return mAccessibilityEnabled;
+    }
+
+    public boolean isTouchExplorationEnabled() {
+        return mTouchExplorationEnabled;
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("AccessibilityController state:");
+        pw.print("  mAccessibilityEnabled="); pw.println(mAccessibilityEnabled);
+        pw.print("  mTouchExplorationEnabled="); pw.println(mTouchExplorationEnabled);
+    }
+
+    public void addStateChangedCallback(AccessibilityStateChangedCallback cb) {
+        mChangeCallbacks.add(cb);
+        cb.onStateChanged(mAccessibilityEnabled, mTouchExplorationEnabled);
+    }
+
+    public void removeStateChangedCallback(AccessibilityStateChangedCallback cb) {
+        mChangeCallbacks.remove(cb);
+    }
+
+    private void fireChanged() {
+        final int N = mChangeCallbacks.size();
+        for (int i = 0; i < N; i++) {
+            mChangeCallbacks.get(i).onStateChanged(mAccessibilityEnabled, mTouchExplorationEnabled);
+        }
+    }
+
+    @Override
+    public void onAccessibilityStateChanged(boolean enabled) {
+        mAccessibilityEnabled = enabled;
+        fireChanged();
+    }
+
+    @Override
+    public void onTouchExplorationStateChanged(boolean enabled) {
+        mTouchExplorationEnabled = enabled;
+        fireChanged();
+    }
+
+    public interface AccessibilityStateChangedCallback {
+        void onStateChanged(boolean accessibilityEnabled, boolean touchExplorationEnabled);
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java b/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java
new file mode 100644
index 0000000..6a573f5
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2017 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.policy;
+
+import android.content.Context;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener;
+
+/**
+ * For mocking because AccessibilityManager is final for some reason...
+ */
+public class AccessibilityManagerWrapper implements
+        CallbackController<AccessibilityServicesStateChangeListener> {
+
+    private final AccessibilityManager mAccessibilityManager;
+
+    public AccessibilityManagerWrapper(Context context) {
+        mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+    }
+
+    @Override
+    public void addCallback(AccessibilityServicesStateChangeListener listener) {
+        mAccessibilityManager.addAccessibilityServicesStateChangeListener(listener, null);
+    }
+
+    @Override
+    public void removeCallback(AccessibilityServicesStateChangeListener listener) {
+        mAccessibilityManager.removeAccessibilityServicesStateChangeListener(listener);
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/BatteryController.java b/com/android/systemui/statusbar/policy/BatteryController.java
new file mode 100644
index 0000000..641fe69
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/BatteryController.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2010 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.policy;
+
+import com.android.systemui.DemoMode;
+import com.android.systemui.Dumpable;
+import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+public interface BatteryController extends DemoMode, Dumpable,
+        CallbackController<BatteryStateChangeCallback> {
+    /**
+     * Prints the current state of the {@link BatteryController} to the given {@link PrintWriter}.
+     */
+    void dump(FileDescriptor fd, PrintWriter pw, String[] args);
+
+    /**
+     * Sets if the current device is in power save mode.
+     */
+    void setPowerSaveMode(boolean powerSave);
+
+    /**
+     * Returns {@code true} if the device is currently in power save mode.
+     */
+    boolean isPowerSave();
+
+    /**
+     * A listener that will be notified whenever a change in battery level or power save mode
+     * has occurred.
+     */
+    interface BatteryStateChangeCallback {
+        default void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {}
+        default void onPowerSaveChanged(boolean isPowerSave) {}
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
new file mode 100644
index 0000000..e8d5af6
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2016 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.policy;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.util.Log;
+import com.android.systemui.DemoMode;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Default implementation of a {@link BatteryController}. This controller monitors for battery
+ * level change events that are broadcasted by the system.
+ */
+public class BatteryControllerImpl extends BroadcastReceiver implements BatteryController {
+    private static final String TAG = "BatteryController";
+
+    public static final String ACTION_LEVEL_TEST = "com.android.systemui.BATTERY_LEVEL_TEST";
+
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private final ArrayList<BatteryController.BatteryStateChangeCallback> mChangeCallbacks = new ArrayList<>();
+    private final PowerManager mPowerManager;
+    private final Handler mHandler;
+    private final Context mContext;
+
+    protected int mLevel;
+    protected boolean mPluggedIn;
+    protected boolean mCharging;
+    protected boolean mCharged;
+    protected boolean mPowerSave;
+    private boolean mTestmode = false;
+    private boolean mHasReceivedBattery = false;
+
+    public BatteryControllerImpl(Context context) {
+        mContext = context;
+        mHandler = new Handler();
+        mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+
+        registerReceiver();
+        updatePowerSave();
+    }
+
+    private void registerReceiver() {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_BATTERY_CHANGED);
+        filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
+        filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGING);
+        filter.addAction(ACTION_LEVEL_TEST);
+        mContext.registerReceiver(this, filter);
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("BatteryController state:");
+        pw.print("  mLevel="); pw.println(mLevel);
+        pw.print("  mPluggedIn="); pw.println(mPluggedIn);
+        pw.print("  mCharging="); pw.println(mCharging);
+        pw.print("  mCharged="); pw.println(mCharged);
+        pw.print("  mPowerSave="); pw.println(mPowerSave);
+    }
+
+    @Override
+    public void setPowerSaveMode(boolean powerSave) {
+        mPowerManager.setPowerSaveMode(powerSave);
+    }
+
+    @Override
+    public void addCallback(BatteryController.BatteryStateChangeCallback cb) {
+        synchronized (mChangeCallbacks) {
+            mChangeCallbacks.add(cb);
+        }
+        if (!mHasReceivedBattery) return;
+        cb.onBatteryLevelChanged(mLevel, mPluggedIn, mCharging);
+        cb.onPowerSaveChanged(mPowerSave);
+    }
+
+    @Override
+    public void removeCallback(BatteryController.BatteryStateChangeCallback cb) {
+        synchronized (mChangeCallbacks) {
+            mChangeCallbacks.remove(cb);
+        }
+    }
+
+    @Override
+    public void onReceive(final Context context, Intent intent) {
+        final String action = intent.getAction();
+        if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
+            if (mTestmode && !intent.getBooleanExtra("testmode", false)) return;
+            mHasReceivedBattery = true;
+            mLevel = (int)(100f
+                    * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)
+                    / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100));
+            mPluggedIn = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0;
+
+            final int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS,
+                    BatteryManager.BATTERY_STATUS_UNKNOWN);
+            mCharged = status == BatteryManager.BATTERY_STATUS_FULL;
+            mCharging = mCharged || status == BatteryManager.BATTERY_STATUS_CHARGING;
+
+            fireBatteryLevelChanged();
+        } else if (action.equals(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)) {
+            updatePowerSave();
+        } else if (action.equals(PowerManager.ACTION_POWER_SAVE_MODE_CHANGING)) {
+            setPowerSave(intent.getBooleanExtra(PowerManager.EXTRA_POWER_SAVE_MODE, false));
+        } else if (action.equals(ACTION_LEVEL_TEST)) {
+            mTestmode = true;
+            mHandler.post(new Runnable() {
+                int curLevel = 0;
+                int incr = 1;
+                int saveLevel = mLevel;
+                boolean savePlugged = mPluggedIn;
+                Intent dummy = new Intent(Intent.ACTION_BATTERY_CHANGED);
+                @Override
+                public void run() {
+                    if (curLevel < 0) {
+                        mTestmode = false;
+                        dummy.putExtra("level", saveLevel);
+                        dummy.putExtra("plugged", savePlugged);
+                        dummy.putExtra("testmode", false);
+                    } else {
+                        dummy.putExtra("level", curLevel);
+                        dummy.putExtra("plugged", incr > 0 ? BatteryManager.BATTERY_PLUGGED_AC
+                                : 0);
+                        dummy.putExtra("testmode", true);
+                    }
+                    context.sendBroadcast(dummy);
+
+                    if (!mTestmode) return;
+
+                    curLevel += incr;
+                    if (curLevel == 100) {
+                        incr *= -1;
+                    }
+                    mHandler.postDelayed(this, 200);
+                }
+            });
+        }
+    }
+
+    @Override
+    public boolean isPowerSave() {
+        return mPowerSave;
+    }
+
+    private void updatePowerSave() {
+        setPowerSave(mPowerManager.isPowerSaveMode());
+    }
+
+    private void setPowerSave(boolean powerSave) {
+        if (powerSave == mPowerSave) return;
+        mPowerSave = powerSave;
+        if (DEBUG) Log.d(TAG, "Power save is " + (mPowerSave ? "on" : "off"));
+        firePowerSaveChanged();
+    }
+
+    protected void fireBatteryLevelChanged() {
+        synchronized (mChangeCallbacks) {
+            final int N = mChangeCallbacks.size();
+            for (int i = 0; i < N; i++) {
+                mChangeCallbacks.get(i).onBatteryLevelChanged(mLevel, mPluggedIn, mCharging);
+            }
+        }
+    }
+
+    private void firePowerSaveChanged() {
+        synchronized (mChangeCallbacks) {
+            final int N = mChangeCallbacks.size();
+            for (int i = 0; i < N; i++) {
+                mChangeCallbacks.get(i).onPowerSaveChanged(mPowerSave);
+            }
+        }
+    }
+
+    private boolean mDemoMode;
+
+    @Override
+    public void dispatchDemoCommand(String command, Bundle args) {
+        if (!mDemoMode && command.equals(COMMAND_ENTER)) {
+            mDemoMode = true;
+            mContext.unregisterReceiver(this);
+        } else if (mDemoMode && command.equals(COMMAND_EXIT)) {
+            mDemoMode = false;
+            registerReceiver();
+            updatePowerSave();
+        } else if (mDemoMode && command.equals(COMMAND_BATTERY)) {
+            String level = args.getString("level");
+            String plugged = args.getString("plugged");
+            String powerSave = args.getString("powersave");
+            if (level != null) {
+                mLevel = Math.min(Math.max(Integer.parseInt(level), 0), 100);
+            }
+            if (plugged != null) {
+                mPluggedIn = Boolean.parseBoolean(plugged);
+            }
+            if (powerSave != null) {
+                mPowerSave = powerSave.equals("true");
+                firePowerSaveChanged();
+            }
+            fireBatteryLevelChanged();
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/BluetoothController.java b/com/android/systemui/statusbar/policy/BluetoothController.java
new file mode 100644
index 0000000..b693ebb
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/BluetoothController.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.systemui.Dumpable;
+import com.android.systemui.statusbar.policy.BluetoothController.Callback;
+
+import java.util.Collection;
+
+public interface BluetoothController extends CallbackController<Callback>, Dumpable {
+    boolean isBluetoothSupported();
+    boolean isBluetoothEnabled();
+
+    int getBluetoothState();
+
+    boolean isBluetoothConnected();
+    boolean isBluetoothConnecting();
+    String getLastDeviceName();
+    void setBluetoothEnabled(boolean enabled);
+    Collection<CachedBluetoothDevice> getDevices();
+    void connect(CachedBluetoothDevice device);
+    void disconnect(CachedBluetoothDevice device);
+    boolean canConfigBluetooth();
+
+    int getMaxConnectionState(CachedBluetoothDevice device);
+    int getBondState(CachedBluetoothDevice device);
+    CachedBluetoothDevice getLastDevice();
+
+    public interface Callback {
+        void onBluetoothStateChange(boolean enabled);
+        void onBluetoothDevicesChanged();
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
new file mode 100644
index 0000000..3b15c2b
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2008 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.policy;
+
+import android.app.ActivityManager;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+
+import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.systemui.Dependency;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.WeakHashMap;
+
+public class BluetoothControllerImpl implements BluetoothController, BluetoothCallback,
+        CachedBluetoothDevice.Callback {
+    private static final String TAG = "BluetoothController";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private final LocalBluetoothManager mLocalBluetoothManager;
+    private final UserManager mUserManager;
+    private final int mCurrentUser;
+    private final WeakHashMap<CachedBluetoothDevice, ActuallyCachedState> mCachedState =
+            new WeakHashMap<>();
+    private final Handler mBgHandler;
+
+    private boolean mEnabled;
+    private int mConnectionState = BluetoothAdapter.STATE_DISCONNECTED;
+    private CachedBluetoothDevice mLastDevice;
+
+    private final H mHandler = new H(Looper.getMainLooper());
+    private int mState;
+
+    public BluetoothControllerImpl(Context context, Looper bgLooper) {
+        mLocalBluetoothManager = Dependency.get(LocalBluetoothManager.class);
+        mBgHandler = new Handler(bgLooper);
+        if (mLocalBluetoothManager != null) {
+            mLocalBluetoothManager.getEventManager().setReceiverHandler(mBgHandler);
+            mLocalBluetoothManager.getEventManager().registerCallback(this);
+            onBluetoothStateChanged(
+                    mLocalBluetoothManager.getBluetoothAdapter().getBluetoothState());
+        }
+        mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+        mCurrentUser = ActivityManager.getCurrentUser();
+    }
+
+    @Override
+    public boolean canConfigBluetooth() {
+        return !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_BLUETOOTH,
+                UserHandle.of(mCurrentUser))
+            && !mUserManager.hasUserRestriction(UserManager.DISALLOW_BLUETOOTH,
+                UserHandle.of(mCurrentUser));
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("BluetoothController state:");
+        pw.print("  mLocalBluetoothManager="); pw.println(mLocalBluetoothManager);
+        if (mLocalBluetoothManager == null) {
+            return;
+        }
+        pw.print("  mEnabled="); pw.println(mEnabled);
+        pw.print("  mConnectionState="); pw.println(stateToString(mConnectionState));
+        pw.print("  mLastDevice="); pw.println(mLastDevice);
+        pw.print("  mCallbacks.size="); pw.println(mHandler.mCallbacks.size());
+        pw.println("  Bluetooth Devices:");
+        for (CachedBluetoothDevice device :
+                mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy()) {
+            pw.println("    " + getDeviceString(device));
+        }
+    }
+
+    private static String stateToString(int state) {
+        switch (state) {
+            case BluetoothAdapter.STATE_CONNECTED:
+                return "CONNECTED";
+            case BluetoothAdapter.STATE_CONNECTING:
+                return "CONNECTING";
+            case BluetoothAdapter.STATE_DISCONNECTED:
+                return "DISCONNECTED";
+            case BluetoothAdapter.STATE_DISCONNECTING:
+                return "DISCONNECTING";
+        }
+        return "UNKNOWN(" + state + ")";
+    }
+
+    private String getDeviceString(CachedBluetoothDevice device) {
+        return device.getName() + " " + device.getBondState() + " " + device.isConnected();
+    }
+
+    @Override
+    public int getBondState(CachedBluetoothDevice device) {
+        return getCachedState(device).mBondState;
+    }
+
+    @Override
+    public CachedBluetoothDevice getLastDevice() {
+        return mLastDevice;
+    }
+
+    @Override
+    public int getMaxConnectionState(CachedBluetoothDevice device) {
+        return getCachedState(device).mMaxConnectionState;
+    }
+
+    @Override
+    public void addCallback(Callback cb) {
+        mHandler.obtainMessage(H.MSG_ADD_CALLBACK, cb).sendToTarget();
+        mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
+    }
+
+    @Override
+    public void removeCallback(Callback cb) {
+        mHandler.obtainMessage(H.MSG_REMOVE_CALLBACK, cb).sendToTarget();
+    }
+
+    @Override
+    public boolean isBluetoothEnabled() {
+        return mEnabled;
+    }
+
+    @Override
+    public int getBluetoothState() {
+        return mState;
+    }
+
+    @Override
+    public boolean isBluetoothConnected() {
+        return mConnectionState == BluetoothAdapter.STATE_CONNECTED;
+    }
+
+    @Override
+    public boolean isBluetoothConnecting() {
+        return mConnectionState == BluetoothAdapter.STATE_CONNECTING;
+    }
+
+    @Override
+    public void setBluetoothEnabled(boolean enabled) {
+        if (mLocalBluetoothManager != null) {
+            mLocalBluetoothManager.getBluetoothAdapter().setBluetoothEnabled(enabled);
+        }
+    }
+
+    @Override
+    public boolean isBluetoothSupported() {
+        return mLocalBluetoothManager != null;
+    }
+
+    @Override
+    public void connect(final CachedBluetoothDevice device) {
+        if (mLocalBluetoothManager == null || device == null) return;
+        device.connect(true);
+    }
+
+    @Override
+    public void disconnect(CachedBluetoothDevice device) {
+        if (mLocalBluetoothManager == null || device == null) return;
+        device.disconnect();
+    }
+
+    @Override
+    public String getLastDeviceName() {
+        return mLastDevice != null ? mLastDevice.getName() : null;
+    }
+
+    @Override
+    public Collection<CachedBluetoothDevice> getDevices() {
+        return mLocalBluetoothManager != null
+                ? mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy()
+                : null;
+    }
+
+    private void updateConnected() {
+        // Make sure our connection state is up to date.
+        int state = mLocalBluetoothManager.getBluetoothAdapter().getConnectionState();
+        if (mLastDevice != null && !mLastDevice.isConnected()) {
+            // Clear out last device if no longer connected.
+            mLastDevice = null;
+        }
+        // If any of the devices are in a higher state than the adapter, move the adapter into
+        // that state.
+        for (CachedBluetoothDevice device : getDevices()) {
+            int maxDeviceState = device.getMaxConnectionState();
+            if (maxDeviceState > state) {
+                state = maxDeviceState;
+            }
+            if (mLastDevice == null && device.isConnected()) {
+                // Set as last connected device only if we don't have one.
+                mLastDevice = device;
+            }
+        }
+
+        if (mLastDevice == null && state == BluetoothAdapter.STATE_CONNECTED) {
+            // If somehow we think we are connected, but have no connected devices, we aren't
+            // connected.
+            state = BluetoothAdapter.STATE_DISCONNECTED;
+        }
+        if (state != mConnectionState) {
+            mConnectionState = state;
+            mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
+        }
+    }
+
+    @Override
+    public void onBluetoothStateChanged(int bluetoothState) {
+        mEnabled = bluetoothState == BluetoothAdapter.STATE_ON
+                || bluetoothState == BluetoothAdapter.STATE_TURNING_ON;
+        mState = bluetoothState;
+        mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
+    }
+
+    @Override
+    public void onScanningStateChanged(boolean started) {
+        // Don't care.
+    }
+
+    @Override
+    public void onDeviceAdded(CachedBluetoothDevice cachedDevice) {
+        cachedDevice.registerCallback(this);
+        updateConnected();
+        mHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED);
+    }
+
+    @Override
+    public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) {
+        mCachedState.remove(cachedDevice);
+        updateConnected();
+        mHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED);
+    }
+
+    @Override
+    public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
+        mCachedState.remove(cachedDevice);
+        updateConnected();
+        mHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED);
+    }
+
+    @Override
+    public void onDeviceAttributesChanged() {
+        updateConnected();
+        mHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED);
+    }
+
+    @Override
+    public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) {
+        mCachedState.remove(cachedDevice);
+        mLastDevice = cachedDevice;
+        updateConnected();
+        mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
+    }
+
+    private ActuallyCachedState getCachedState(CachedBluetoothDevice device) {
+        ActuallyCachedState state = mCachedState.get(device);
+        if (state == null) {
+            state = new ActuallyCachedState(device, mHandler);
+            mBgHandler.post(state);
+            mCachedState.put(device, state);
+            return state;
+        }
+        return state;
+    }
+
+    private static class ActuallyCachedState implements Runnable {
+
+        private final WeakReference<CachedBluetoothDevice> mDevice;
+        private final Handler mUiHandler;
+        private int mBondState = BluetoothDevice.BOND_NONE;
+        private int mMaxConnectionState = BluetoothProfile.STATE_DISCONNECTED;
+
+        private ActuallyCachedState(CachedBluetoothDevice device, Handler uiHandler) {
+            mDevice = new WeakReference<>(device);
+            mUiHandler = uiHandler;
+        }
+
+        @Override
+        public void run() {
+            CachedBluetoothDevice device = mDevice.get();
+            if (device != null) {
+                mBondState = device.getBondState();
+                mMaxConnectionState = device.getMaxConnectionState();
+                mUiHandler.removeMessages(H.MSG_PAIRED_DEVICES_CHANGED);
+                mUiHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED);
+            }
+        }
+    }
+
+    private final class H extends Handler {
+        private final ArrayList<BluetoothController.Callback> mCallbacks = new ArrayList<>();
+
+        private static final int MSG_PAIRED_DEVICES_CHANGED = 1;
+        private static final int MSG_STATE_CHANGED = 2;
+        private static final int MSG_ADD_CALLBACK = 3;
+        private static final int MSG_REMOVE_CALLBACK = 4;
+
+        public H(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_PAIRED_DEVICES_CHANGED:
+                    firePairedDevicesChanged();
+                    break;
+                case MSG_STATE_CHANGED:
+                    fireStateChange();
+                    break;
+                case MSG_ADD_CALLBACK:
+                    mCallbacks.add((BluetoothController.Callback) msg.obj);
+                    break;
+                case MSG_REMOVE_CALLBACK:
+                    mCallbacks.remove((BluetoothController.Callback) msg.obj);
+                    break;
+            }
+        }
+
+        private void firePairedDevicesChanged() {
+            for (BluetoothController.Callback cb : mCallbacks) {
+                cb.onBluetoothDevicesChanged();
+            }
+        }
+
+        private void fireStateChange() {
+            for (BluetoothController.Callback cb : mCallbacks) {
+                fireStateChange(cb);
+            }
+        }
+
+        private void fireStateChange(BluetoothController.Callback cb) {
+            cb.onBluetoothStateChange(mEnabled);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/BrightnessMirrorController.java b/com/android/systemui/statusbar/policy/BrightnessMirrorController.java
new file mode 100644
index 0000000..42ce4c5
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/BrightnessMirrorController.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.util.ArraySet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+import android.widget.FrameLayout;
+
+import com.android.internal.util.Preconditions;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.ScrimController;
+import com.android.systemui.statusbar.phone.StatusBarWindowView;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+/**
+ * Controls showing and hiding of the brightness mirror.
+ */
+public class BrightnessMirrorController
+        implements CallbackController<BrightnessMirrorController.BrightnessMirrorListener> {
+
+    private final NotificationStackScrollLayout mStackScroller;
+    public long TRANSITION_DURATION_OUT = 150;
+    public long TRANSITION_DURATION_IN = 200;
+
+    private final StatusBarWindowView mStatusBarWindow;
+    private final ScrimController mScrimController;
+    private final View mNotificationPanel;
+    private final ArraySet<BrightnessMirrorListener> mBrightnessMirrorListeners = new ArraySet<>();
+    private final int[] mInt2Cache = new int[2];
+    private View mBrightnessMirror;
+
+    public BrightnessMirrorController(StatusBarWindowView statusBarWindow,
+            ScrimController scrimController) {
+        mStatusBarWindow = statusBarWindow;
+        mBrightnessMirror = statusBarWindow.findViewById(R.id.brightness_mirror);
+        mNotificationPanel = statusBarWindow.findViewById(R.id.notification_panel);
+        mStackScroller = (NotificationStackScrollLayout) statusBarWindow.findViewById(
+                R.id.notification_stack_scroller);
+        mScrimController = scrimController;
+    }
+
+    public void showMirror() {
+        mBrightnessMirror.setVisibility(View.VISIBLE);
+        mStackScroller.setFadingOut(true);
+        mScrimController.forceHideScrims(true /* hide */, true /* animated */);
+        outAnimation(mNotificationPanel.animate())
+                .withLayer();
+    }
+
+    public void hideMirror() {
+        mScrimController.forceHideScrims(false /* hide */, true /* animated */);
+        inAnimation(mNotificationPanel.animate())
+                .withLayer()
+                .withEndAction(new Runnable() {
+                    @Override
+                    public void run() {
+                        mBrightnessMirror.setVisibility(View.INVISIBLE);
+                        mStackScroller.setFadingOut(false);
+                    }
+                });
+    }
+
+    private ViewPropertyAnimator outAnimation(ViewPropertyAnimator a) {
+        return a.alpha(0.0f)
+                .setDuration(TRANSITION_DURATION_OUT)
+                .setInterpolator(Interpolators.ALPHA_OUT)
+                .withEndAction(null);
+    }
+    private ViewPropertyAnimator inAnimation(ViewPropertyAnimator a) {
+        return a.alpha(1.0f)
+                .setDuration(TRANSITION_DURATION_IN)
+                .setInterpolator(Interpolators.ALPHA_IN);
+    }
+
+    public void setLocation(View original) {
+        original.getLocationInWindow(mInt2Cache);
+
+        // Original is slightly larger than the mirror, so make sure to use the center for the
+        // positioning.
+        int originalX = mInt2Cache[0] + original.getWidth() / 2;
+        int originalY = mInt2Cache[1] + original.getHeight() / 2;
+        mBrightnessMirror.setTranslationX(0);
+        mBrightnessMirror.setTranslationY(0);
+        mBrightnessMirror.getLocationInWindow(mInt2Cache);
+        int mirrorX = mInt2Cache[0] + mBrightnessMirror.getWidth() / 2;
+        int mirrorY = mInt2Cache[1] + mBrightnessMirror.getHeight() / 2;
+        mBrightnessMirror.setTranslationX(originalX - mirrorX);
+        mBrightnessMirror.setTranslationY(originalY - mirrorY);
+    }
+
+    public View getMirror() {
+        return mBrightnessMirror;
+    }
+
+    public void updateResources() {
+        FrameLayout.LayoutParams lp =
+                (FrameLayout.LayoutParams) mBrightnessMirror.getLayoutParams();
+        lp.width = mBrightnessMirror.getResources().getDimensionPixelSize(
+                R.dimen.qs_panel_width);
+        lp.gravity = mBrightnessMirror.getResources().getInteger(
+                R.integer.notification_panel_layout_gravity);
+        mBrightnessMirror.setLayoutParams(lp);
+    }
+
+    public void onOverlayChanged() {
+        reinflate();
+    }
+
+    public void onDensityOrFontScaleChanged() {
+        reinflate();
+    }
+
+    private void reinflate() {
+        int index = mStatusBarWindow.indexOfChild(mBrightnessMirror);
+        mStatusBarWindow.removeView(mBrightnessMirror);
+        mBrightnessMirror = LayoutInflater.from(mBrightnessMirror.getContext()).inflate(
+                R.layout.brightness_mirror, mStatusBarWindow, false);
+        mStatusBarWindow.addView(mBrightnessMirror, index);
+
+        for (int i = 0; i < mBrightnessMirrorListeners.size(); i++) {
+            mBrightnessMirrorListeners.valueAt(i).onBrightnessMirrorReinflated(mBrightnessMirror);
+        }
+    }
+
+    @Override
+    public void addCallback(BrightnessMirrorListener listener) {
+        Preconditions.checkNotNull(listener);
+        mBrightnessMirrorListeners.add(listener);
+    }
+
+    @Override
+    public void removeCallback(BrightnessMirrorListener listener) {
+        mBrightnessMirrorListeners.remove(listener);
+    }
+
+    public interface BrightnessMirrorListener {
+        void onBrightnessMirrorReinflated(View brightnessMirror);
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/CallbackController.java b/com/android/systemui/statusbar/policy/CallbackController.java
new file mode 100644
index 0000000..9042ca6
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/CallbackController.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2016 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.policy;
+
+public interface CallbackController<T> {
+    void addCallback(T listener);
+    void removeCallback(T listener);
+}
diff --git a/com/android/systemui/statusbar/policy/CallbackHandler.java b/com/android/systemui/statusbar/policy/CallbackHandler.java
new file mode 100644
index 0000000..a456786
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/CallbackHandler.java
@@ -0,0 +1,179 @@
+/*
+ * 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.policy;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.telephony.SubscriptionInfo;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.statusbar.policy.NetworkController.EmergencyListener;
+import com.android.systemui.statusbar.policy.NetworkController.IconState;
+import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Implements network listeners and forwards the calls along onto other listeners but on
+ * the current or specified Looper.
+ */
+public class CallbackHandler extends Handler implements EmergencyListener, SignalCallback {
+    private static final int MSG_EMERGENCE_CHANGED           = 0;
+    private static final int MSG_SUBS_CHANGED                = 1;
+    private static final int MSG_NO_SIM_VISIBLE_CHANGED      = 2;
+    private static final int MSG_ETHERNET_CHANGED            = 3;
+    private static final int MSG_AIRPLANE_MODE_CHANGED       = 4;
+    private static final int MSG_MOBILE_DATA_ENABLED_CHANGED = 5;
+    private static final int MSG_ADD_REMOVE_EMERGENCY        = 6;
+    private static final int MSG_ADD_REMOVE_SIGNAL           = 7;
+
+    // All the callbacks.
+    private final ArrayList<EmergencyListener> mEmergencyListeners = new ArrayList<>();
+    private final ArrayList<SignalCallback> mSignalCallbacks = new ArrayList<>();
+
+    public CallbackHandler() {
+        super(Looper.getMainLooper());
+    }
+
+    @VisibleForTesting
+    CallbackHandler(Looper looper) {
+        super(looper);
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+            case MSG_EMERGENCE_CHANGED:
+                for (EmergencyListener listener : mEmergencyListeners) {
+                    listener.setEmergencyCallsOnly(msg.arg1 != 0);
+                }
+                break;
+            case MSG_SUBS_CHANGED:
+                for (SignalCallback signalCluster : mSignalCallbacks) {
+                    signalCluster.setSubs((List<SubscriptionInfo>) msg.obj);
+                }
+                break;
+            case MSG_NO_SIM_VISIBLE_CHANGED:
+                for (SignalCallback signalCluster : mSignalCallbacks) {
+                    signalCluster.setNoSims(msg.arg1 != 0);
+                }
+                break;
+            case MSG_ETHERNET_CHANGED:
+                for (SignalCallback signalCluster : mSignalCallbacks) {
+                    signalCluster.setEthernetIndicators((IconState) msg.obj);
+                }
+                break;
+            case MSG_AIRPLANE_MODE_CHANGED:
+                for (SignalCallback signalCluster : mSignalCallbacks) {
+                    signalCluster.setIsAirplaneMode((IconState) msg.obj);
+                }
+                break;
+            case MSG_MOBILE_DATA_ENABLED_CHANGED:
+                for (SignalCallback signalCluster : mSignalCallbacks) {
+                    signalCluster.setMobileDataEnabled(msg.arg1 != 0);
+                }
+                break;
+            case MSG_ADD_REMOVE_EMERGENCY:
+                if (msg.arg1 != 0) {
+                    mEmergencyListeners.add((EmergencyListener) msg.obj);
+                } else {
+                    mEmergencyListeners.remove((EmergencyListener) msg.obj);
+                }
+                break;
+            case MSG_ADD_REMOVE_SIGNAL:
+                if (msg.arg1 != 0) {
+                    mSignalCallbacks.add((SignalCallback) msg.obj);
+                } else {
+                    mSignalCallbacks.remove((SignalCallback) msg.obj);
+                }
+                break;
+        }
+    }
+
+    @Override
+    public void setWifiIndicators(final boolean enabled, final IconState statusIcon,
+            final IconState qsIcon, final boolean activityIn, final boolean activityOut,
+            final String description, boolean isTransient) {
+        post(new Runnable() {
+            @Override
+            public void run() {
+                for (SignalCallback callback : mSignalCallbacks) {
+                    callback.setWifiIndicators(enabled, statusIcon, qsIcon, activityIn, activityOut,
+                            description, isTransient);
+                }
+            }
+        });
+    }
+
+    @Override
+    public void setMobileDataIndicators(final IconState statusIcon, final IconState qsIcon,
+            final int statusType, final int qsType,final boolean activityIn,
+            final boolean activityOut, final String typeContentDescription,
+            final String description, final boolean isWide, final int subId, boolean roaming) {
+        post(new Runnable() {
+            @Override
+            public void run() {
+                for (SignalCallback signalCluster : mSignalCallbacks) {
+                    signalCluster.setMobileDataIndicators(statusIcon, qsIcon, statusType, qsType,
+                            activityIn, activityOut, typeContentDescription, description, isWide,
+                            subId, roaming);
+                }
+            }
+        });
+    }
+
+    @Override
+    public void setSubs(List<SubscriptionInfo> subs) {
+        obtainMessage(MSG_SUBS_CHANGED, subs).sendToTarget();
+    }
+
+    @Override
+    public void setNoSims(boolean show) {
+        obtainMessage(MSG_NO_SIM_VISIBLE_CHANGED, show ? 1 : 0, 0).sendToTarget();
+    }
+
+    @Override
+    public void setMobileDataEnabled(boolean enabled) {
+        obtainMessage(MSG_MOBILE_DATA_ENABLED_CHANGED, enabled ? 1 : 0, 0).sendToTarget();
+    }
+
+    @Override
+    public void setEmergencyCallsOnly(boolean emergencyOnly) {
+        obtainMessage(MSG_EMERGENCE_CHANGED, emergencyOnly ? 1 : 0, 0).sendToTarget();
+    }
+
+    @Override
+    public void setEthernetIndicators(IconState icon) {
+        obtainMessage(MSG_ETHERNET_CHANGED, icon).sendToTarget();;
+    }
+
+    @Override
+    public void setIsAirplaneMode(IconState icon) {
+        obtainMessage(MSG_AIRPLANE_MODE_CHANGED, icon).sendToTarget();;
+    }
+
+    public void setListening(EmergencyListener listener, boolean listening) {
+        obtainMessage(MSG_ADD_REMOVE_EMERGENCY, listening ? 1 : 0, 0, listener).sendToTarget();
+    }
+
+    public void setListening(SignalCallback listener, boolean listening) {
+        obtainMessage(MSG_ADD_REMOVE_SIGNAL, listening ? 1 : 0, 0, listener).sendToTarget();
+    }
+
+}
diff --git a/com/android/systemui/statusbar/policy/CastController.java b/com/android/systemui/statusbar/policy/CastController.java
new file mode 100644
index 0000000..97be6ed
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/CastController.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import com.android.systemui.Dumpable;
+import com.android.systemui.statusbar.policy.CastController.Callback;
+
+import java.util.Set;
+
+public interface CastController extends CallbackController<Callback>, Dumpable {
+    void setDiscovering(boolean request);
+    void setCurrentUserId(int currentUserId);
+    Set<CastDevice> getCastDevices();
+    void startCasting(CastDevice device);
+    void stopCasting(CastDevice device);
+
+    public interface Callback {
+        void onCastDevicesChanged();
+    }
+
+    public static final class CastDevice {
+        public static final int STATE_DISCONNECTED = 0;
+        public static final int STATE_CONNECTING = 1;
+        public static final int STATE_CONNECTED = 2;
+
+        public String id;
+        public String name;
+        public String description;
+        public int state = STATE_DISCONNECTED;
+        public Object tag;
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/CastControllerImpl.java b/com/android/systemui/statusbar/policy/CastControllerImpl.java
new file mode 100644
index 0000000..2bf62bb
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/CastControllerImpl.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.media.MediaRouter;
+import android.media.MediaRouter.RouteInfo;
+import android.media.projection.MediaProjectionInfo;
+import android.media.projection.MediaProjectionManager;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.systemui.R;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+
+import static android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
+
+/** Platform implementation of the cast controller. **/
+public class CastControllerImpl implements CastController {
+    private static final String TAG = "CastController";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private final Context mContext;
+    private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
+    private final MediaRouter mMediaRouter;
+    private final ArrayMap<String, RouteInfo> mRoutes = new ArrayMap<>();
+    private final Object mDiscoveringLock = new Object();
+    private final MediaProjectionManager mProjectionManager;
+    private final Object mProjectionLock = new Object();
+
+    private boolean mDiscovering;
+    private boolean mCallbackRegistered;
+    private MediaProjectionInfo mProjection;
+
+    public CastControllerImpl(Context context) {
+        mContext = context;
+        mMediaRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
+        mProjectionManager = (MediaProjectionManager)
+                context.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
+        mProjection = mProjectionManager.getActiveProjectionInfo();
+        mProjectionManager.addCallback(mProjectionCallback, new Handler());
+        if (DEBUG) Log.d(TAG, "new CastController()");
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("CastController state:");
+        pw.print("  mDiscovering="); pw.println(mDiscovering);
+        pw.print("  mCallbackRegistered="); pw.println(mCallbackRegistered);
+        pw.print("  mCallbacks.size="); pw.println(mCallbacks.size());
+        pw.print("  mRoutes.size="); pw.println(mRoutes.size());
+        for (int i = 0; i < mRoutes.size(); i++) {
+            final RouteInfo route = mRoutes.valueAt(i);
+            pw.print("    "); pw.println(routeToString(route));
+        }
+        pw.print("  mProjection="); pw.println(mProjection);
+    }
+
+    @Override
+    public void addCallback(Callback callback) {
+        mCallbacks.add(callback);
+        fireOnCastDevicesChanged(callback);
+        synchronized (mDiscoveringLock) {
+            handleDiscoveryChangeLocked();
+        }
+    }
+
+    @Override
+    public void removeCallback(Callback callback) {
+        mCallbacks.remove(callback);
+        synchronized (mDiscoveringLock) {
+            handleDiscoveryChangeLocked();
+        }
+    }
+
+    @Override
+    public void setDiscovering(boolean request) {
+        synchronized (mDiscoveringLock) {
+            if (mDiscovering == request) return;
+            mDiscovering = request;
+            if (DEBUG) Log.d(TAG, "setDiscovering: " + request);
+            handleDiscoveryChangeLocked();
+        }
+    }
+
+    private void handleDiscoveryChangeLocked() {
+        if (mCallbackRegistered) {
+            mMediaRouter.removeCallback(mMediaCallback);
+            mCallbackRegistered = false;
+        }
+        if (mDiscovering) {
+            mMediaRouter.addCallback(ROUTE_TYPE_REMOTE_DISPLAY, mMediaCallback,
+                    MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
+            mCallbackRegistered = true;
+        } else if (mCallbacks.size() != 0) {
+            mMediaRouter.addCallback(ROUTE_TYPE_REMOTE_DISPLAY, mMediaCallback,
+                    MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
+            mCallbackRegistered = true;
+        }
+    }
+
+    @Override
+    public void setCurrentUserId(int currentUserId) {
+        mMediaRouter.rebindAsUser(currentUserId);
+    }
+
+    @Override
+    public Set<CastDevice> getCastDevices() {
+        final ArraySet<CastDevice> devices = new ArraySet<CastDevice>();
+        synchronized (mProjectionLock) {
+            if (mProjection != null) {
+                final CastDevice device = new CastDevice();
+                device.id = mProjection.getPackageName();
+                device.name = getAppName(mProjection.getPackageName());
+                device.description = mContext.getString(R.string.quick_settings_casting);
+                device.state = CastDevice.STATE_CONNECTED;
+                device.tag = mProjection;
+                devices.add(device);
+                return devices;
+            }
+        }
+        synchronized(mRoutes) {
+            for (RouteInfo route : mRoutes.values()) {
+                final CastDevice device = new CastDevice();
+                device.id = route.getTag().toString();
+                final CharSequence name = route.getName(mContext);
+                device.name = name != null ? name.toString() : null;
+                final CharSequence description = route.getDescription();
+                device.description = description != null ? description.toString() : null;
+                device.state = route.isConnecting() ? CastDevice.STATE_CONNECTING
+                        : route.isSelected() ? CastDevice.STATE_CONNECTED
+                        : CastDevice.STATE_DISCONNECTED;
+                device.tag = route;
+                devices.add(device);
+            }
+        }
+        return devices;
+    }
+
+    @Override
+    public void startCasting(CastDevice device) {
+        if (device == null || device.tag == null) return;
+        final RouteInfo route = (RouteInfo) device.tag;
+        if (DEBUG) Log.d(TAG, "startCasting: " + routeToString(route));
+        mMediaRouter.selectRoute(ROUTE_TYPE_REMOTE_DISPLAY, route);
+    }
+
+    @Override
+    public void stopCasting(CastDevice device) {
+        final boolean isProjection = device.tag instanceof MediaProjectionInfo;
+        if (DEBUG) Log.d(TAG, "stopCasting isProjection=" + isProjection);
+        if (isProjection) {
+            final MediaProjectionInfo projection = (MediaProjectionInfo) device.tag;
+            if (Objects.equals(mProjectionManager.getActiveProjectionInfo(), projection)) {
+                mProjectionManager.stopActiveProjection();
+            } else {
+                Log.w(TAG, "Projection is no longer active: " + projection);
+            }
+        } else {
+            mMediaRouter.getFallbackRoute().select();
+        }
+    }
+
+    private void setProjection(MediaProjectionInfo projection, boolean started) {
+        boolean changed = false;
+        final MediaProjectionInfo oldProjection = mProjection;
+        synchronized (mProjectionLock) {
+            final boolean isCurrent = Objects.equals(projection, mProjection);
+            if (started && !isCurrent) {
+                mProjection = projection;
+                changed = true;
+            } else if (!started && isCurrent) {
+                mProjection = null;
+                changed = true;
+            }
+        }
+        if (changed) {
+            if (DEBUG) Log.d(TAG, "setProjection: " + oldProjection + " -> " + mProjection);
+            fireOnCastDevicesChanged();
+        }
+    }
+
+    private String getAppName(String packageName) {
+        final PackageManager pm = mContext.getPackageManager();
+        try {
+            final ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0);
+            if (appInfo != null) {
+                final CharSequence label = appInfo.loadLabel(pm);
+                if (!TextUtils.isEmpty(label)) {
+                    return label.toString();
+                }
+            }
+            Log.w(TAG, "No label found for package: " + packageName);
+        } catch (NameNotFoundException e) {
+            Log.w(TAG, "Error getting appName for package: " + packageName, e);
+        }
+        return packageName;
+    }
+
+    private void updateRemoteDisplays() {
+        synchronized(mRoutes) {
+            mRoutes.clear();
+            final int n = mMediaRouter.getRouteCount();
+            for (int i = 0; i < n; i++) {
+                final RouteInfo route = mMediaRouter.getRouteAt(i);
+                if (!route.isEnabled()) continue;
+                if (!route.matchesTypes(ROUTE_TYPE_REMOTE_DISPLAY)) continue;
+                ensureTagExists(route);
+                mRoutes.put(route.getTag().toString(), route);
+            }
+            final RouteInfo selected = mMediaRouter.getSelectedRoute(ROUTE_TYPE_REMOTE_DISPLAY);
+            if (selected != null && !selected.isDefault()) {
+                ensureTagExists(selected);
+                mRoutes.put(selected.getTag().toString(), selected);
+            }
+        }
+        fireOnCastDevicesChanged();
+    }
+
+    private void ensureTagExists(RouteInfo route) {
+        if (route.getTag() == null) {
+            route.setTag(UUID.randomUUID().toString());
+        }
+    }
+
+    private void fireOnCastDevicesChanged() {
+        for (Callback callback : mCallbacks) {
+            fireOnCastDevicesChanged(callback);
+        }
+    }
+
+    private void fireOnCastDevicesChanged(Callback callback) {
+        callback.onCastDevicesChanged();
+    }
+
+    private static String routeToString(RouteInfo route) {
+        if (route == null) return null;
+        final StringBuilder sb = new StringBuilder().append(route.getName()).append('/')
+                .append(route.getDescription()).append('@').append(route.getDeviceAddress())
+                .append(",status=").append(route.getStatus());
+        if (route.isDefault()) sb.append(",default");
+        if (route.isEnabled()) sb.append(",enabled");
+        if (route.isConnecting()) sb.append(",connecting");
+        if (route.isSelected()) sb.append(",selected");
+        return sb.append(",id=").append(route.getTag()).toString();
+    }
+
+    private final MediaRouter.SimpleCallback mMediaCallback = new MediaRouter.SimpleCallback() {
+        @Override
+        public void onRouteAdded(MediaRouter router, RouteInfo route) {
+            if (DEBUG) Log.d(TAG, "onRouteAdded: " + routeToString(route));
+            updateRemoteDisplays();
+        }
+        @Override
+        public void onRouteChanged(MediaRouter router, RouteInfo route) {
+            if (DEBUG) Log.d(TAG, "onRouteChanged: " + routeToString(route));
+            updateRemoteDisplays();
+        }
+        @Override
+        public void onRouteRemoved(MediaRouter router, RouteInfo route) {
+            if (DEBUG) Log.d(TAG, "onRouteRemoved: " + routeToString(route));
+            updateRemoteDisplays();
+        }
+        @Override
+        public void onRouteSelected(MediaRouter router, int type, RouteInfo route) {
+            if (DEBUG) Log.d(TAG, "onRouteSelected(" + type + "): " + routeToString(route));
+            updateRemoteDisplays();
+        }
+        @Override
+        public void onRouteUnselected(MediaRouter router, int type, RouteInfo route) {
+            if (DEBUG) Log.d(TAG, "onRouteUnselected(" + type + "): " + routeToString(route));
+            updateRemoteDisplays();
+        }
+    };
+
+    private final MediaProjectionManager.Callback mProjectionCallback
+            = new MediaProjectionManager.Callback() {
+        @Override
+        public void onStart(MediaProjectionInfo info) {
+            setProjection(info, true);
+        }
+
+        @Override
+        public void onStop(MediaProjectionInfo info) {
+            setProjection(info, false);
+        }
+    };
+}
diff --git a/com/android/systemui/statusbar/policy/Clock.java b/com/android/systemui/statusbar/policy/Clock.java
new file mode 100644
index 0000000..4c92d01
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/Clock.java
@@ -0,0 +1,401 @@
+/*
+ * Copyright (C) 2006 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.policy;
+
+import libcore.icu.LocaleData;
+
+import android.app.ActivityManager;
+import android.app.StatusBarManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.format.DateFormat;
+import android.text.style.CharacterStyle;
+import android.text.style.RelativeSizeSpan;
+import android.util.AttributeSet;
+import android.view.Display;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.systemui.DemoMode;
+import com.android.systemui.Dependency;
+import com.android.systemui.FontSizeUtils;
+import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.phone.StatusBarIconController;
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.tuner.TunerService.Tunable;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Digital clock for the status bar.
+ */
+public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.Callbacks,
+        DarkReceiver, ConfigurationListener {
+
+    public static final String CLOCK_SECONDS = "clock_seconds";
+
+    private boolean mClockVisibleByPolicy = true;
+    private boolean mClockVisibleByUser = true;
+
+    private boolean mAttached;
+    private Calendar mCalendar;
+    private String mClockFormatString;
+    private SimpleDateFormat mClockFormat;
+    private SimpleDateFormat mContentDescriptionFormat;
+    private Locale mLocale;
+
+    private static final int AM_PM_STYLE_NORMAL  = 0;
+    private static final int AM_PM_STYLE_SMALL   = 1;
+    private static final int AM_PM_STYLE_GONE    = 2;
+
+    private final int mAmPmStyle;
+    private final boolean mShowDark;
+    private boolean mShowSeconds;
+    private Handler mSecondsHandler;
+
+    public Clock(Context context) {
+        this(context, null);
+    }
+
+    public Clock(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public Clock(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        TypedArray a = context.getTheme().obtainStyledAttributes(
+                attrs,
+                R.styleable.Clock,
+                0, 0);
+        try {
+            mAmPmStyle = a.getInt(R.styleable.Clock_amPmStyle, AM_PM_STYLE_GONE);
+            mShowDark = a.getBoolean(R.styleable.Clock_showDark, true);
+        } finally {
+            a.recycle();
+        }
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        if (!mAttached) {
+            mAttached = true;
+            IntentFilter filter = new IntentFilter();
+
+            filter.addAction(Intent.ACTION_TIME_TICK);
+            filter.addAction(Intent.ACTION_TIME_CHANGED);
+            filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+            filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+            filter.addAction(Intent.ACTION_USER_SWITCHED);
+
+            getContext().registerReceiverAsUser(mIntentReceiver, UserHandle.ALL, filter,
+                    null, Dependency.get(Dependency.TIME_TICK_HANDLER));
+            Dependency.get(TunerService.class).addTunable(this, CLOCK_SECONDS,
+                    StatusBarIconController.ICON_BLACKLIST);
+            SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).addCallbacks(this);
+            if (mShowDark) {
+                Dependency.get(DarkIconDispatcher.class).addDarkReceiver(this);
+            }
+        }
+
+        // NOTE: It's safe to do these after registering the receiver since the receiver always runs
+        // in the main thread, therefore the receiver can't run before this method returns.
+
+        // The time zone may have changed while the receiver wasn't registered, so update the Time
+        mCalendar = Calendar.getInstance(TimeZone.getDefault());
+
+        // Make sure we update to the current time
+        updateClock();
+        updateShowSeconds();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (mAttached) {
+            getContext().unregisterReceiver(mIntentReceiver);
+            mAttached = false;
+            Dependency.get(TunerService.class).removeTunable(this);
+            SysUiServiceProvider.getComponent(getContext(), CommandQueue.class)
+                    .removeCallbacks(this);
+            if (mShowDark) {
+                Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(this);
+            }
+        }
+    }
+
+    private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(Intent.ACTION_TIMEZONE_CHANGED)) {
+                String tz = intent.getStringExtra("time-zone");
+                getHandler().post(() -> {
+                    mCalendar = Calendar.getInstance(TimeZone.getTimeZone(tz));
+                    if (mClockFormat != null) {
+                        mClockFormat.setTimeZone(mCalendar.getTimeZone());
+                    }
+                });
+            } else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
+                final Locale newLocale = getResources().getConfiguration().locale;
+                getHandler().post(() -> {
+                    if (!newLocale.equals(mLocale)) {
+                        mLocale = newLocale;
+                        mClockFormatString = ""; // force refresh
+                    }
+                });
+            }
+            getHandler().post(() -> updateClock());
+        }
+    };
+
+    public void setClockVisibleByUser(boolean visible) {
+        mClockVisibleByUser = visible;
+        updateClockVisibility();
+    }
+
+    public void setClockVisibilityByPolicy(boolean visible) {
+        mClockVisibleByPolicy = visible;
+        updateClockVisibility();
+    }
+
+    private void updateClockVisibility() {
+        boolean visible = mClockVisibleByPolicy && mClockVisibleByUser;
+        Dependency.get(IconLogger.class).onIconVisibility("clock", visible);
+        int visibility = visible ? View.VISIBLE : View.GONE;
+        setVisibility(visibility);
+    }
+
+    final void updateClock() {
+        if (mDemoMode) return;
+        mCalendar.setTimeInMillis(System.currentTimeMillis());
+        setText(getSmallTime());
+        setContentDescription(mContentDescriptionFormat.format(mCalendar.getTime()));
+    }
+
+    @Override
+    public void onTuningChanged(String key, String newValue) {
+        if (CLOCK_SECONDS.equals(key)) {
+            mShowSeconds = newValue != null && Integer.parseInt(newValue) != 0;
+            updateShowSeconds();
+        } else {
+            setClockVisibleByUser(!StatusBarIconController.getIconBlacklist(newValue)
+                    .contains("clock"));
+            updateClockVisibility();
+        }
+    }
+
+    @Override
+    public void disable(int state1, int state2, boolean animate) {
+        boolean clockVisibleByPolicy = (state1 & StatusBarManager.DISABLE_CLOCK) == 0;
+        if (clockVisibleByPolicy != mClockVisibleByPolicy) {
+            setClockVisibilityByPolicy(clockVisibleByPolicy);
+        }
+    }
+
+    @Override
+    public void onDarkChanged(Rect area, float darkIntensity, int tint) {
+        setTextColor(DarkIconDispatcher.getTint(area, this, tint));
+    }
+
+    @Override
+    public void onDensityOrFontScaleChanged() {
+        FontSizeUtils.updateFontSize(this, R.dimen.status_bar_clock_size);
+        setPaddingRelative(
+                mContext.getResources().getDimensionPixelSize(
+                        R.dimen.status_bar_clock_starting_padding),
+                0,
+                mContext.getResources().getDimensionPixelSize(
+                        R.dimen.status_bar_clock_end_padding),
+                0);
+    }
+
+    private void updateShowSeconds() {
+        if (mShowSeconds) {
+            // Wait until we have a display to start trying to show seconds.
+            if (mSecondsHandler == null && getDisplay() != null) {
+                mSecondsHandler = new Handler();
+                if (getDisplay().getState() == Display.STATE_ON) {
+                    mSecondsHandler.postAtTime(mSecondTick,
+                            SystemClock.uptimeMillis() / 1000 * 1000 + 1000);
+                }
+                IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
+                filter.addAction(Intent.ACTION_SCREEN_ON);
+                mContext.registerReceiver(mScreenReceiver, filter);
+            }
+        } else {
+            if (mSecondsHandler != null) {
+                mContext.unregisterReceiver(mScreenReceiver);
+                mSecondsHandler.removeCallbacks(mSecondTick);
+                mSecondsHandler = null;
+                updateClock();
+            }
+        }
+    }
+
+    private final CharSequence getSmallTime() {
+        Context context = getContext();
+        boolean is24 = DateFormat.is24HourFormat(context, ActivityManager.getCurrentUser());
+        LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale);
+
+        final char MAGIC1 = '\uEF00';
+        final char MAGIC2 = '\uEF01';
+
+        SimpleDateFormat sdf;
+        String format = mShowSeconds
+                ? is24 ? d.timeFormat_Hms : d.timeFormat_hms
+                : is24 ? d.timeFormat_Hm : d.timeFormat_hm;
+        if (!format.equals(mClockFormatString)) {
+            mContentDescriptionFormat = new SimpleDateFormat(format);
+            /*
+             * Search for an unquoted "a" in the format string, so we can
+             * add dummy characters around it to let us find it again after
+             * formatting and change its size.
+             */
+            if (mAmPmStyle != AM_PM_STYLE_NORMAL) {
+                int a = -1;
+                boolean quoted = false;
+                for (int i = 0; i < format.length(); i++) {
+                    char c = format.charAt(i);
+
+                    if (c == '\'') {
+                        quoted = !quoted;
+                    }
+                    if (!quoted && c == 'a') {
+                        a = i;
+                        break;
+                    }
+                }
+
+                if (a >= 0) {
+                    // Move a back so any whitespace before AM/PM is also in the alternate size.
+                    final int b = a;
+                    while (a > 0 && Character.isWhitespace(format.charAt(a-1))) {
+                        a--;
+                    }
+                    format = format.substring(0, a) + MAGIC1 + format.substring(a, b)
+                        + "a" + MAGIC2 + format.substring(b + 1);
+                }
+            }
+            mClockFormat = sdf = new SimpleDateFormat(format);
+            mClockFormatString = format;
+        } else {
+            sdf = mClockFormat;
+        }
+        String result = sdf.format(mCalendar.getTime());
+
+        if (mAmPmStyle != AM_PM_STYLE_NORMAL) {
+            int magic1 = result.indexOf(MAGIC1);
+            int magic2 = result.indexOf(MAGIC2);
+            if (magic1 >= 0 && magic2 > magic1) {
+                SpannableStringBuilder formatted = new SpannableStringBuilder(result);
+                if (mAmPmStyle == AM_PM_STYLE_GONE) {
+                    formatted.delete(magic1, magic2+1);
+                } else {
+                    if (mAmPmStyle == AM_PM_STYLE_SMALL) {
+                        CharacterStyle style = new RelativeSizeSpan(0.7f);
+                        formatted.setSpan(style, magic1, magic2,
+                                          Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
+                    }
+                    formatted.delete(magic2, magic2 + 1);
+                    formatted.delete(magic1, magic1 + 1);
+                }
+                return formatted;
+            }
+        }
+
+        return result;
+
+    }
+
+    private boolean mDemoMode;
+
+    @Override
+    public void dispatchDemoCommand(String command, Bundle args) {
+        if (!mDemoMode && command.equals(COMMAND_ENTER)) {
+            mDemoMode = true;
+        } else if (mDemoMode && command.equals(COMMAND_EXIT)) {
+            mDemoMode = false;
+            updateClock();
+        } else if (mDemoMode && command.equals(COMMAND_CLOCK)) {
+            String millis = args.getString("millis");
+            String hhmm = args.getString("hhmm");
+            if (millis != null) {
+                mCalendar.setTimeInMillis(Long.parseLong(millis));
+            } else if (hhmm != null && hhmm.length() == 4) {
+                int hh = Integer.parseInt(hhmm.substring(0, 2));
+                int mm = Integer.parseInt(hhmm.substring(2));
+                boolean is24 = DateFormat.is24HourFormat(
+                        getContext(), ActivityManager.getCurrentUser());
+                if (is24) {
+                    mCalendar.set(Calendar.HOUR_OF_DAY, hh);
+                } else {
+                    mCalendar.set(Calendar.HOUR, hh);
+                }
+                mCalendar.set(Calendar.MINUTE, mm);
+            }
+            setText(getSmallTime());
+            setContentDescription(mContentDescriptionFormat.format(mCalendar.getTime()));
+        }
+    }
+
+    private final BroadcastReceiver mScreenReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+                if (mSecondsHandler != null) {
+                    mSecondsHandler.removeCallbacks(mSecondTick);
+                }
+            } else if (Intent.ACTION_SCREEN_ON.equals(action)) {
+                if (mSecondsHandler != null) {
+                    mSecondsHandler.postAtTime(mSecondTick,
+                            SystemClock.uptimeMillis() / 1000 * 1000 + 1000);
+                }
+            }
+        }
+    };
+
+    private final Runnable mSecondTick = new Runnable() {
+        @Override
+        public void run() {
+            if (mCalendar != null) {
+                updateClock();
+            }
+            mSecondsHandler.postAtTime(this, SystemClock.uptimeMillis() / 1000 * 1000 + 1000);
+        }
+    };
+}
+
diff --git a/com/android/systemui/statusbar/policy/ConfigurationController.java b/com/android/systemui/statusbar/policy/ConfigurationController.java
new file mode 100644
index 0000000..3dca371
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/ConfigurationController.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 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.policy;
+
+import android.content.res.Configuration;
+
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
+
+/**
+ * Common listener for configuration or subsets of configuration changes (like density or
+ * font scaling), providing easy static dependence on these events.
+ */
+public interface ConfigurationController extends CallbackController<ConfigurationListener> {
+
+    interface ConfigurationListener {
+        default void onConfigChanged(Configuration newConfig) {}
+        default void onDensityOrFontScaleChanged() {}
+        default void onOverlayChanged() {}
+        default void onLocaleListChanged() {}
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/DarkIconDispatcher.java b/com/android/systemui/statusbar/policy/DarkIconDispatcher.java
new file mode 100644
index 0000000..58944c6
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/DarkIconDispatcher.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2017 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.policy;
+
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.systemui.statusbar.phone.LightBarTransitionsController;
+
+public interface DarkIconDispatcher {
+
+    void setIconsDarkArea(Rect r);
+    LightBarTransitionsController getTransitionsController();
+
+    void addDarkReceiver(DarkReceiver receiver);
+    void addDarkReceiver(ImageView imageView);
+
+    // Must have been previously been added through one of the addDarkReceive methods above.
+    void removeDarkReceiver(DarkReceiver object);
+    void removeDarkReceiver(ImageView object);
+
+    // Used to reapply darkness on an object, must have previously been added through
+    // addDarkReceiver.
+    void applyDark(ImageView object);
+
+    int DEFAULT_ICON_TINT = Color.WHITE;
+    Rect sTmpRect = new Rect();
+    int[] sTmpInt2 = new int[2];
+
+    /**
+     * @return the tint to apply to {@param view} depending on the desired tint {@param color} and
+     *         the screen {@param tintArea} in which to apply that tint
+     */
+    static int getTint(Rect tintArea, View view, int color) {
+        if (isInArea(tintArea, view)) {
+            return color;
+        } else {
+            return DEFAULT_ICON_TINT;
+        }
+    }
+
+    /**
+     * @return the dark intensity to apply to {@param view} depending on the desired dark
+     *         {@param intensity} and the screen {@param tintArea} in which to apply that intensity
+     */
+    static float getDarkIntensity(Rect tintArea, View view, float intensity) {
+        if (isInArea(tintArea, view)) {
+            return intensity;
+        } else {
+            return 0f;
+        }
+    }
+
+    /**
+     * @return true if more than half of the {@param view} area are in {@param area}, false
+     *         otherwise
+     */
+    static boolean isInArea(Rect area, View view) {
+        if (area.isEmpty()) {
+            return true;
+        }
+        sTmpRect.set(area);
+        view.getLocationOnScreen(sTmpInt2);
+        int left = sTmpInt2[0];
+
+        int intersectStart = Math.max(left, area.left);
+        int intersectEnd = Math.min(left + view.getWidth(), area.right);
+        int intersectAmount = Math.max(0, intersectEnd - intersectStart);
+
+        boolean coversFullStatusBar = area.top <= 0;
+        boolean majorityOfWidth = 2 * intersectAmount > view.getWidth();
+        return majorityOfWidth && coversFullStatusBar;
+    }
+
+    interface DarkReceiver {
+        void onDarkChanged(Rect area, float darkIntensity, int tint);
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/DataSaverController.java b/com/android/systemui/statusbar/policy/DataSaverController.java
new file mode 100644
index 0000000..0df7859
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/DataSaverController.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 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.policy;
+
+import com.android.systemui.statusbar.policy.DataSaverController.Listener;
+
+public interface DataSaverController extends CallbackController<Listener> {
+
+    boolean isDataSaverEnabled();
+    void setDataSaverEnabled(boolean enabled);
+
+    public interface Listener {
+        void onDataSaverChanged(boolean isDataSaving);
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java b/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java
new file mode 100644
index 0000000..2951943
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2016 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.policy;
+
+import android.content.Context;
+import android.net.INetworkPolicyListener;
+import android.net.NetworkPolicyManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+
+import com.android.systemui.statusbar.policy.DataSaverController.Listener;
+
+import java.util.ArrayList;
+
+public class DataSaverControllerImpl implements DataSaverController {
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private final ArrayList<Listener> mListeners = new ArrayList<>();
+    private final NetworkPolicyManager mPolicyManager;
+
+    public DataSaverControllerImpl(Context context) {
+        mPolicyManager = NetworkPolicyManager.from(context);
+    }
+
+    private void handleRestrictBackgroundChanged(boolean isDataSaving) {
+        synchronized (mListeners) {
+            for (int i = 0; i < mListeners.size(); i++) {
+                mListeners.get(i).onDataSaverChanged(isDataSaving);
+            }
+        }
+    }
+
+    public void addCallback(Listener listener) {
+        synchronized (mListeners) {
+            mListeners.add(listener);
+            if (mListeners.size() == 1) {
+                mPolicyManager.registerListener(mPolicyListener);
+            }
+        }
+        listener.onDataSaverChanged(isDataSaverEnabled());
+    }
+
+    public void removeCallback(Listener listener) {
+        synchronized (mListeners) {
+            mListeners.remove(listener);
+            if (mListeners.size() == 0) {
+                mPolicyManager.unregisterListener(mPolicyListener);
+            }
+        }
+    }
+
+    public boolean isDataSaverEnabled() {
+        return mPolicyManager.getRestrictBackground();
+    }
+
+    public void setDataSaverEnabled(boolean enabled) {
+        mPolicyManager.setRestrictBackground(enabled);
+        try {
+            mPolicyListener.onRestrictBackgroundChanged(enabled);
+        } catch (RemoteException e) {
+        }
+    }
+
+    private final INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
+        @Override
+        public void onUidRulesChanged(int uid, int uidRules) throws RemoteException {
+        }
+
+        @Override
+        public void onMeteredIfacesChanged(String[] strings) throws RemoteException {
+        }
+
+        @Override
+        public void onRestrictBackgroundChanged(final boolean isDataSaving) throws RemoteException {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    handleRestrictBackgroundChanged(isDataSaving);
+                }
+            });
+        }
+
+        @Override
+        public void onUidPoliciesChanged(int uid, int uidPolicies) throws RemoteException {
+        }
+    };
+
+}
diff --git a/com/android/systemui/statusbar/policy/DateView.java b/com/android/systemui/statusbar/policy/DateView.java
new file mode 100644
index 0000000..74a30fa
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/DateView.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2008 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.policy;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.TypedArray;
+import android.icu.text.DateFormat;
+import android.icu.text.DisplayContext;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+
+import java.util.Date;
+import java.util.Locale;
+
+public class DateView extends TextView {
+    private static final String TAG = "DateView";
+
+    private final Date mCurrentTime = new Date();
+
+    private DateFormat mDateFormat;
+    private String mLastText;
+    private String mDatePattern;
+
+    private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (Intent.ACTION_TIME_TICK.equals(action)
+                    || Intent.ACTION_TIME_CHANGED.equals(action)
+                    || Intent.ACTION_TIMEZONE_CHANGED.equals(action)
+                    || Intent.ACTION_LOCALE_CHANGED.equals(action)) {
+                if (Intent.ACTION_LOCALE_CHANGED.equals(action)
+                        || Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {
+                    // need to get a fresh date format
+                    getHandler().post(() -> mDateFormat = null);
+                }
+                getHandler().post(() -> updateClock());
+            }
+        }
+    };
+
+    public DateView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        TypedArray a = context.getTheme().obtainStyledAttributes(
+                attrs,
+                R.styleable.DateView,
+                0, 0);
+
+        try {
+            mDatePattern = a.getString(R.styleable.DateView_datePattern);
+        } finally {
+            a.recycle();
+        }
+        if (mDatePattern == null) {
+            mDatePattern = getContext().getString(R.string.system_ui_date_pattern);
+        }
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_TIME_TICK);
+        filter.addAction(Intent.ACTION_TIME_CHANGED);
+        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+        filter.addAction(Intent.ACTION_LOCALE_CHANGED);
+        getContext().registerReceiver(mIntentReceiver, filter, null,
+                Dependency.get(Dependency.TIME_TICK_HANDLER));
+
+        updateClock();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        mDateFormat = null; // reload the locale next time
+        getContext().unregisterReceiver(mIntentReceiver);
+    }
+
+    protected void updateClock() {
+        if (mDateFormat == null) {
+            final Locale l = Locale.getDefault();
+            DateFormat format = DateFormat.getInstanceForSkeleton(mDatePattern, l);
+            format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
+            mDateFormat = format;
+        }
+
+        mCurrentTime.setTime(System.currentTimeMillis());
+
+        final String text = mDateFormat.format(mCurrentTime);
+        if (!text.equals(mLastText)) {
+            setText(text);
+            mLastText = text;
+        }
+    }
+
+    public void setDatePattern(String pattern) {
+        if (TextUtils.equals(pattern, mDatePattern)) {
+            return;
+        }
+        mDatePattern = pattern;
+        mDateFormat = null;
+        if (isAttachedToWindow()) {
+            updateClock();
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/DeadZone.java b/com/android/systemui/statusbar/policy/DeadZone.java
new file mode 100644
index 0000000..06040e2
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/DeadZone.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2012 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.policy;
+
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.Slog;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.View;
+
+import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.statusbar.phone.StatusBar;
+
+/**
+ * The "dead zone" consumes unintentional taps along the top edge of the navigation bar.
+ * When users are typing quickly on an IME they may attempt to hit the space bar, overshoot, and
+ * accidentally hit the home button. The DeadZone expands temporarily after each tap in the UI
+ * outside the navigation bar (since this is when accidental taps are more likely), then contracts
+ * back over time (since a later tap might be intended for the top of the bar).
+ */
+public class DeadZone extends View {
+    public static final String TAG = "DeadZone";
+
+    public static final boolean DEBUG = false;
+    public static final int HORIZONTAL = 0;  // Consume taps along the top edge.
+    public static final int VERTICAL = 1;  // Consume taps along the left edge.
+
+    private static final boolean CHATTY = true; // print to logcat when we eat a click
+    private final StatusBar mStatusBar;
+
+    private boolean mShouldFlash;
+    private float mFlashFrac = 0f;
+
+    private int mSizeMax;
+    private int mSizeMin;
+    // Upon activity elsewhere in the UI, the dead zone will hold steady for
+    // mHold ms, then move back over the course of mDecay ms
+    private int mHold, mDecay;
+    private boolean mVertical;
+    private long mLastPokeTime;
+    private int mDisplayRotation;
+
+    private final Runnable mDebugFlash = new Runnable() {
+        @Override
+        public void run() {
+            ObjectAnimator.ofFloat(DeadZone.this, "flash", 1f, 0f).setDuration(150).start();
+        }
+    };
+
+    public DeadZone(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public DeadZone(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DeadZone,
+                defStyle, 0);
+
+        mHold = a.getInteger(R.styleable.DeadZone_holdTime, 0);
+        mDecay = a.getInteger(R.styleable.DeadZone_decayTime, 0);
+
+        mSizeMin = a.getDimensionPixelSize(R.styleable.DeadZone_minSize, 0);
+        mSizeMax = a.getDimensionPixelSize(R.styleable.DeadZone_maxSize, 0);
+
+        int index = a.getInt(R.styleable.DeadZone_orientation, -1);
+        mVertical = (index == VERTICAL);
+
+        if (DEBUG)
+            Slog.v(TAG, this + " size=[" + mSizeMin + "-" + mSizeMax + "] hold=" + mHold
+                    + (mVertical ? " vertical" : " horizontal"));
+
+        setFlashOnTouchCapture(context.getResources().getBoolean(R.bool.config_dead_zone_flash));
+        mStatusBar = SysUiServiceProvider.getComponent(context, StatusBar.class);
+    }
+
+    static float lerp(float a, float b, float f) {
+        return (b - a) * f + a;
+    }
+
+    private float getSize(long now) {
+        if (mSizeMax == 0)
+            return 0;
+        long dt = (now - mLastPokeTime);
+        if (dt > mHold + mDecay)
+            return mSizeMin;
+        if (dt < mHold)
+            return mSizeMax;
+        return (int) lerp(mSizeMax, mSizeMin, (float) (dt - mHold) / mDecay);
+    }
+
+    public void setFlashOnTouchCapture(boolean dbg) {
+        mShouldFlash = dbg;
+        mFlashFrac = 0f;
+        postInvalidate();
+    }
+
+    // I made you a touch event...
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (DEBUG) {
+            Slog.v(TAG, this + " onTouch: " + MotionEvent.actionToString(event.getAction()));
+        }
+
+        // Don't consume events for high precision pointing devices. For this purpose a stylus is
+        // considered low precision (like a finger), so its events may be consumed.
+        if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
+            return false;
+        }
+
+        final int action = event.getAction();
+        if (action == MotionEvent.ACTION_OUTSIDE) {
+            poke(event);
+            return true;
+        } else if (action == MotionEvent.ACTION_DOWN) {
+            if (DEBUG) {
+                Slog.v(TAG, this + " ACTION_DOWN: " + event.getX() + "," + event.getY());
+            }
+            if (mStatusBar != null) mStatusBar.touchAutoDim();
+            int size = (int) getSize(event.getEventTime());
+            // In the vertical orientation consume taps along the left edge.
+            // In horizontal orientation consume taps along the top edge.
+            final boolean consumeEvent;
+            if (mVertical) {
+                if (mDisplayRotation == Surface.ROTATION_270) {
+                    consumeEvent = event.getX() > getWidth() - size;
+                } else {
+                    consumeEvent = event.getX() < size;
+                }
+            } else {
+                consumeEvent = event.getY() < size;
+            }
+            if (consumeEvent) {
+                if (CHATTY) {
+                    Slog.v(TAG, "consuming errant click: (" + event.getX() + "," + event.getY() + ")");
+                }
+                if (mShouldFlash) {
+                    post(mDebugFlash);
+                    postInvalidate();
+                }
+                return true; // ...but I eated it
+            }
+        }
+        return false;
+    }
+
+    private void poke(MotionEvent event) {
+        mLastPokeTime = event.getEventTime();
+        if (DEBUG)
+            Slog.v(TAG, "poked! size=" + getSize(mLastPokeTime));
+        if (mShouldFlash) postInvalidate();
+    }
+
+    public void setFlash(float f) {
+        mFlashFrac = f;
+        postInvalidate();
+    }
+
+    public float getFlash() {
+        return mFlashFrac;
+    }
+
+    @Override
+    public void onDraw(Canvas can) {
+        if (!mShouldFlash || mFlashFrac <= 0f) {
+            return;
+        }
+
+        final int size = (int) getSize(SystemClock.uptimeMillis());
+        if (mVertical) {
+            if (mDisplayRotation == Surface.ROTATION_270) {
+                can.clipRect(can.getWidth() - size, 0, can.getWidth(), can.getHeight());
+            } else {
+                can.clipRect(0, 0, size, can.getHeight());
+            }
+        } else {
+            can.clipRect(0, 0, can.getWidth(), size);
+        }
+
+        final float frac = DEBUG ? (mFlashFrac - 0.5f) + 0.5f : mFlashFrac;
+        can.drawARGB((int) (frac * 0xFF), 0xDD, 0xEE, 0xAA);
+
+        if (DEBUG && size > mSizeMin)
+            // crazy aggressive redrawing here, for debugging only
+            postInvalidateDelayed(100);
+    }
+
+    public void setDisplayRotation(int rotation) {
+        mDisplayRotation = rotation;
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/DeviceProvisionedController.java b/com/android/systemui/statusbar/policy/DeviceProvisionedController.java
new file mode 100644
index 0000000..cae76b4
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/DeviceProvisionedController.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 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.policy;
+
+import android.content.Context;
+
+import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
+
+public interface DeviceProvisionedController extends CallbackController<DeviceProvisionedListener> {
+
+    boolean isDeviceProvisioned();
+    boolean isUserSetup(int currentUser);
+    int getCurrentUser();
+
+    default boolean isCurrentUserSetup() {
+        return isUserSetup(getCurrentUser());
+    }
+
+    interface DeviceProvisionedListener {
+        default void onDeviceProvisionedChanged() { }
+        default void onUserSwitched() {
+            onUserSetupChanged();
+        }
+        default void onUserSetupChanged() { }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.java b/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.java
new file mode 100644
index 0000000..f2283a5
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017 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.policy;
+
+import android.app.ActivityManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.provider.Settings.Global;
+import android.provider.Settings.Secure;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.settings.CurrentUserTracker;
+
+import java.util.ArrayList;
+
+public class DeviceProvisionedControllerImpl extends CurrentUserTracker implements
+        DeviceProvisionedController {
+
+    private final ArrayList<DeviceProvisionedListener> mListeners = new ArrayList<>();
+    private final ContentResolver mContentResolver;
+    private final Context mContext;
+    private final Uri mDeviceProvisionedUri;
+    private final Uri mUserSetupUri;
+
+    public DeviceProvisionedControllerImpl(Context context) {
+        super(context);
+        mContext = context;
+        mContentResolver = context.getContentResolver();
+        mDeviceProvisionedUri = Global.getUriFor(Global.DEVICE_PROVISIONED);
+        mUserSetupUri = Secure.getUriFor(Secure.USER_SETUP_COMPLETE);
+    }
+
+    @Override
+    public boolean isDeviceProvisioned() {
+        return Global.getInt(mContentResolver, Global.DEVICE_PROVISIONED, 0) != 0;
+    }
+
+    @Override
+    public boolean isUserSetup(int currentUser) {
+        return Secure.getIntForUser(mContentResolver, Secure.USER_SETUP_COMPLETE, 0, currentUser)
+                != 0;
+    }
+
+    @Override
+    public int getCurrentUser() {
+        return ActivityManager.getCurrentUser();
+    }
+
+    @Override
+    public void addCallback(DeviceProvisionedListener listener) {
+        mListeners.add(listener);
+        if (mListeners.size() == 1) {
+            startListening(getCurrentUser());
+        }
+        listener.onUserSetupChanged();
+        listener.onDeviceProvisionedChanged();
+    }
+
+    @Override
+    public void removeCallback(DeviceProvisionedListener listener) {
+        mListeners.remove(listener);
+        if (mListeners.size() == 0) {
+            stopListening();
+        }
+    }
+
+    private void startListening(int user) {
+        mContentResolver.registerContentObserver(mDeviceProvisionedUri, true,
+                mSettingsObserver, 0);
+        mContentResolver.registerContentObserver(mUserSetupUri, true,
+                mSettingsObserver, user);
+        startTracking();
+    }
+
+    private void stopListening() {
+        stopTracking();
+        mContentResolver.unregisterContentObserver(mSettingsObserver);
+    }
+
+    @Override
+    public void onUserSwitched(int newUserId) {
+        mContentResolver.unregisterContentObserver(mSettingsObserver);
+        mContentResolver.registerContentObserver(mDeviceProvisionedUri, true,
+                mSettingsObserver, 0);
+        mContentResolver.registerContentObserver(mUserSetupUri, true,
+                mSettingsObserver, newUserId);
+        notifyUserChanged();
+    }
+
+    private void notifyUserChanged() {
+        for (int i = mListeners.size() - 1; i >= 0; --i) {
+            mListeners.get(i).onUserSwitched();
+        }
+    }
+
+    private void notifySetupChanged() {
+        for (int i = mListeners.size() - 1; i >= 0; --i) {
+            mListeners.get(i).onUserSetupChanged();
+        }
+    }
+
+    private void notifyProvisionedChanged() {
+        for (int i = mListeners.size() - 1; i >= 0; --i) {
+            mListeners.get(i).onDeviceProvisionedChanged();
+        }
+    }
+
+    protected final ContentObserver mSettingsObserver = new ContentObserver(Dependency.get(
+            Dependency.MAIN_HANDLER)) {
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri, int userId) {
+            if (mUserSetupUri.equals(uri)) {
+                notifySetupChanged();
+            } else {
+                notifyProvisionedChanged();
+            }
+        }
+    };
+}
diff --git a/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java b/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java
new file mode 100644
index 0000000..c726189
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2016 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.policy;
+
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.provider.Settings;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionInfo;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.TextView;
+
+import com.android.internal.telephony.IccCardConstants;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+
+import java.util.List;
+
+public class EmergencyCryptkeeperText extends TextView {
+
+    private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private final KeyguardUpdateMonitorCallback mCallback = new KeyguardUpdateMonitorCallback() {
+        @Override
+        public void onPhoneStateChanged(int phoneState) {
+            update();
+        }
+
+        @Override
+        public void onRefreshCarrierInfo() {
+            update();
+        }
+    };
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(intent.getAction())) {
+                update();
+            }
+        }
+    };
+
+    public EmergencyCryptkeeperText(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        setVisibility(GONE);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+        mKeyguardUpdateMonitor.registerCallback(mCallback);
+        getContext().registerReceiver(mReceiver,
+                new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
+        update();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (mKeyguardUpdateMonitor != null) {
+            mKeyguardUpdateMonitor.removeCallback(mCallback);
+        }
+        getContext().unregisterReceiver(mReceiver);
+    }
+
+    public void update() {
+        boolean hasMobile = ConnectivityManager.from(mContext)
+                .isNetworkSupported(ConnectivityManager.TYPE_MOBILE);
+        boolean airplaneMode = (Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.AIRPLANE_MODE_ON, 0) == 1);
+
+        if (!hasMobile || airplaneMode) {
+            setText(null);
+            setVisibility(GONE);
+            return;
+        }
+
+        boolean allSimsMissing = true;
+        CharSequence displayText = null;
+
+        List<SubscriptionInfo> subs = mKeyguardUpdateMonitor.getSubscriptionInfo(false);
+        final int N = subs.size();
+        for (int i = 0; i < N; i++) {
+            int subId = subs.get(i).getSubscriptionId();
+            IccCardConstants.State simState = mKeyguardUpdateMonitor.getSimState(subId);
+            CharSequence carrierName = subs.get(i).getCarrierName();
+            if (simState.iccCardExist() && !TextUtils.isEmpty(carrierName)) {
+                allSimsMissing = false;
+                displayText = carrierName;
+            }
+        }
+        if (allSimsMissing) {
+            if (N != 0) {
+                // Shows "Emergency calls only" on devices that are voice-capable.
+                // This depends on mPlmn containing the text "Emergency calls only" when the radio
+                // has some connectivity. Otherwise it should show "No service"
+                // Grab the first subscription, because they all should contain the emergency text,
+                // described above.
+                displayText = subs.get(0).getCarrierName();
+            } else {
+                // We don't have a SubscriptionInfo to get the emergency calls only from.
+                // Grab it from the old sticky broadcast if possible instead. We can use it
+                // here because no subscriptions are active, so we don't have
+                // to worry about MSIM clashing.
+                displayText = getContext().getText(
+                        com.android.internal.R.string.emergency_calls_only);
+                Intent i = getContext().registerReceiver(null,
+                        new IntentFilter(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION));
+                if (i != null) {
+                    displayText = i.getStringExtra(TelephonyIntents.EXTRA_PLMN);
+                }
+            }
+        }
+
+        setText(displayText);
+        setVisibility(TextUtils.isEmpty(displayText) ? GONE : VISIBLE);
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/EncryptionHelper.java b/com/android/systemui/statusbar/policy/EncryptionHelper.java
new file mode 100644
index 0000000..639e50c
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/EncryptionHelper.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 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.policy;
+
+import android.os.SystemProperties;
+
+/**
+ * Helper for determining whether the phone is decrypted yet.
+ */
+public class EncryptionHelper {
+
+    public static final boolean IS_DATA_ENCRYPTED = isDataEncrypted();
+
+    private static boolean isDataEncrypted() {
+        String voldState = SystemProperties.get("vold.decrypt");
+        return "1".equals(voldState) || "trigger_restart_min_framework".equals(voldState);
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/EthernetIcons.java b/com/android/systemui/statusbar/policy/EthernetIcons.java
new file mode 100644
index 0000000..b391bd9
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/EthernetIcons.java
@@ -0,0 +1,26 @@
+/*
+ * 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.policy;
+
+import com.android.systemui.R;
+
+class EthernetIcons {
+    static final int[][] ETHERNET_ICONS = {
+            { R.drawable.stat_sys_ethernet },
+            { R.drawable.stat_sys_ethernet_fully },
+    };
+}
diff --git a/com/android/systemui/statusbar/policy/EthernetSignalController.java b/com/android/systemui/statusbar/policy/EthernetSignalController.java
new file mode 100644
index 0000000..159bd41
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/EthernetSignalController.java
@@ -0,0 +1,63 @@
+/*
+ * 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.policy;
+
+import android.content.Context;
+import android.net.NetworkCapabilities;
+
+import com.android.systemui.statusbar.policy.NetworkController.IconState;
+import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+
+import java.util.BitSet;
+
+
+public class EthernetSignalController extends
+        SignalController<SignalController.State, SignalController.IconGroup> {
+
+    public EthernetSignalController(Context context,
+            CallbackHandler callbackHandler, NetworkControllerImpl networkController) {
+        super("EthernetSignalController", context, NetworkCapabilities.TRANSPORT_ETHERNET,
+                callbackHandler, networkController);
+        mCurrentState.iconGroup = mLastState.iconGroup = new IconGroup(
+                "Ethernet Icons",
+                EthernetIcons.ETHERNET_ICONS,
+                null,
+                AccessibilityContentDescriptions.ETHERNET_CONNECTION_VALUES,
+                0, 0, 0, 0,
+                AccessibilityContentDescriptions.ETHERNET_CONNECTION_VALUES[0]);
+    }
+
+    @Override
+    public void updateConnectivity(BitSet connectedTransports, BitSet validatedTransports) {
+        mCurrentState.connected = connectedTransports.get(mTransportType);
+        super.updateConnectivity(connectedTransports, validatedTransports);
+    }
+
+    @Override
+    public void notifyListeners(SignalCallback callback) {
+        boolean ethernetVisible = mCurrentState.connected;
+        String contentDescription = getStringIfExists(getContentDescription());
+
+        // TODO: wire up data transfer using WifiSignalPoller.
+        callback.setEthernetIndicators(new IconState(ethernetVisible, getCurrentIconId(),
+                contentDescription));
+    }
+
+    @Override
+    public SignalController.State cleanState() {
+        return new SignalController.State();
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/ExtensionController.java b/com/android/systemui/statusbar/policy/ExtensionController.java
new file mode 100644
index 0000000..cade5dc
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/ExtensionController.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2017 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.policy;
+
+import android.content.Context;
+
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * Utility class used to select between a plugin, tuner settings, and a default implementation
+ * of an interface.
+ */
+public interface ExtensionController {
+
+    <T> ExtensionBuilder<T> newExtension(Class<T> cls);
+
+    interface Extension<T> {
+        T get();
+        Context getContext();
+        void destroy();
+        void addCallback(Consumer<T> callback);
+        /**
+         * Triggers the extension to cycle through each of the sources again because something
+         * (like configuration) may have changed.
+         */
+        T reload();
+
+        /**
+         * Null out the cached item for the purpose of memory saving, should only be done
+         * when any other references are already gotten.
+         * @param isDestroyed
+         */
+        void clearItem(boolean isDestroyed);
+    }
+
+    interface ExtensionBuilder<T> {
+        ExtensionBuilder<T> withTunerFactory(TunerFactory<T> factory);
+        <P extends T> ExtensionBuilder<T> withPlugin(Class<P> cls);
+        <P extends T> ExtensionBuilder<T> withPlugin(Class<P> cls, String action);
+        <P> ExtensionBuilder<T> withPlugin(Class<P> cls, String action,
+                PluginConverter<T, P> converter);
+        ExtensionBuilder<T> withDefault(Supplier<T> def);
+        ExtensionBuilder<T> withCallback(Consumer<T> callback);
+        ExtensionBuilder<T> withUiMode(int mode, Supplier<T> def);
+        ExtensionBuilder<T> withFeature(String feature, Supplier<T> def);
+        Extension build();
+    }
+
+    public interface PluginConverter<T, P> {
+        T getInterfaceFromPlugin(P plugin);
+    }
+
+    public interface TunerFactory<T> {
+        String[] keys();
+        T create(Map<String, String> settings);
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/ExtensionControllerImpl.java b/com/android/systemui/statusbar/policy/ExtensionControllerImpl.java
new file mode 100644
index 0000000..6d75cfc
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/ExtensionControllerImpl.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2017 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.policy;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Handler;
+import android.util.ArrayMap;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.PluginManager;
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.tuner.TunerService.Tunable;
+import com.android.systemui.util.leak.LeakDetector;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class ExtensionControllerImpl implements ExtensionController {
+
+    public static final int SORT_ORDER_PLUGIN  = 0;
+    public static final int SORT_ORDER_TUNER   = 1;
+    public static final int SORT_ORDER_FEATURE = 2;
+    public static final int SORT_ORDER_UI_MODE = 3;
+    public static final int SORT_ORDER_DEFAULT = 4;
+
+    private final Context mDefaultContext;
+
+    public ExtensionControllerImpl(Context context) {
+        mDefaultContext = context;
+    }
+
+    @Override
+    public <T> ExtensionBuilder<T> newExtension(Class<T> cls) {
+        return new ExtensionBuilder<>();
+    }
+
+    private interface Producer<T> {
+        T get();
+
+        void destroy();
+    }
+
+    private class ExtensionBuilder<T> implements ExtensionController.ExtensionBuilder<T> {
+
+        private ExtensionImpl<T> mExtension = new ExtensionImpl<>();
+
+        @Override
+        public ExtensionController.ExtensionBuilder<T> withTunerFactory(TunerFactory<T> factory) {
+            mExtension.addTunerFactory(factory, factory.keys());
+            return this;
+        }
+
+        @Override
+        public <P extends T> ExtensionController.ExtensionBuilder<T> withPlugin(Class<P> cls) {
+            return withPlugin(cls, PluginManager.getAction(cls));
+        }
+
+        @Override
+        public <P extends T> ExtensionController.ExtensionBuilder<T> withPlugin(Class<P> cls,
+                String action) {
+            return withPlugin(cls, action, null);
+        }
+
+        @Override
+        public <P> ExtensionController.ExtensionBuilder<T> withPlugin(Class<P> cls,
+                String action, PluginConverter<T, P> converter) {
+            mExtension.addPlugin(action, cls, converter);
+            return this;
+        }
+
+        @Override
+        public ExtensionController.ExtensionBuilder<T> withDefault(Supplier<T> def) {
+            mExtension.addDefault(def);
+            return this;
+        }
+
+        @Override
+        public ExtensionController.ExtensionBuilder<T> withUiMode(int uiMode,
+                Supplier<T> supplier) {
+            mExtension.addUiMode(uiMode, supplier);
+            return this;
+        }
+
+        @Override
+        public ExtensionController.ExtensionBuilder<T> withFeature(String feature,
+                Supplier<T> supplier) {
+            mExtension.addFeature(feature, supplier);
+            return this;
+        }
+
+        @Override
+        public ExtensionController.ExtensionBuilder<T> withCallback(
+                Consumer<T> callback) {
+            mExtension.mCallbacks.add(callback);
+            return this;
+        }
+
+        @Override
+        public ExtensionController.Extension build() {
+            // Sort items in ascending order
+            Collections.sort(mExtension.mProducers, Comparator.comparingInt(Item::sortOrder));
+            mExtension.notifyChanged();
+            return mExtension;
+        }
+    }
+
+    private class ExtensionImpl<T> implements ExtensionController.Extension<T> {
+        private final ArrayList<Item<T>> mProducers = new ArrayList<>();
+        private final ArrayList<Consumer<T>> mCallbacks = new ArrayList<>();
+        private T mItem;
+        private Context mPluginContext;
+
+        public void addCallback(Consumer<T> callback) {
+            mCallbacks.add(callback);
+        }
+
+        @Override
+        public T get() {
+            return mItem;
+        }
+
+        @Override
+        public Context getContext() {
+            return mPluginContext != null ? mPluginContext : mDefaultContext;
+        }
+
+        @Override
+        public void destroy() {
+            for (int i = 0; i < mProducers.size(); i++) {
+                mProducers.get(i).destroy();
+            }
+        }
+
+        @Override
+        public T reload() {
+            notifyChanged();
+            return get();
+        }
+
+        @Override
+        public void clearItem(boolean isDestroyed) {
+            if (isDestroyed && mItem != null) {
+                Dependency.get(LeakDetector.class).trackGarbage(mItem);
+            }
+            mItem = null;
+        }
+
+        private void notifyChanged() {
+            if (mItem != null) {
+                Dependency.get(LeakDetector.class).trackGarbage(mItem);
+            }
+            mItem = null;
+            for (int i = 0; i < mProducers.size(); i++) {
+                final T item = mProducers.get(i).get();
+                if (item != null) {
+                    mItem = item;
+                    break;
+                }
+            }
+            for (int i = 0; i < mCallbacks.size(); i++) {
+                mCallbacks.get(i).accept(mItem);
+            }
+        }
+
+        public void addDefault(Supplier<T> def) {
+            mProducers.add(new Default(def));
+        }
+
+        public <P> void addPlugin(String action, Class<P> cls, PluginConverter<T, P> converter) {
+            mProducers.add(new PluginItem(action, cls, converter));
+        }
+
+        public void addTunerFactory(TunerFactory<T> factory, String[] keys) {
+            mProducers.add(new TunerItem(factory, keys));
+        }
+
+        public void addUiMode(int uiMode, Supplier<T> mode) {
+            mProducers.add(new UiModeItem(uiMode, mode));
+        }
+
+        public void addFeature(String feature, Supplier<T> mode) {
+            mProducers.add(new FeatureItem<>(feature, mode));
+        }
+
+        private class PluginItem<P extends Plugin> implements Item<T>, PluginListener<P> {
+            private final PluginConverter<T, P> mConverter;
+            private T mItem;
+
+            public PluginItem(String action, Class<P> cls, PluginConverter<T, P> converter) {
+                mConverter = converter;
+                Dependency.get(PluginManager.class).addPluginListener(action, this, cls);
+            }
+
+            @Override
+            public void onPluginConnected(P plugin, Context pluginContext) {
+                mPluginContext = pluginContext;
+                if (mConverter != null) {
+                    mItem = mConverter.getInterfaceFromPlugin(plugin);
+                } else {
+                    mItem = (T) plugin;
+                }
+                notifyChanged();
+            }
+
+            @Override
+            public void onPluginDisconnected(P plugin) {
+                mPluginContext = null;
+                mItem = null;
+                notifyChanged();
+            }
+
+            @Override
+            public T get() {
+                return mItem;
+            }
+
+            @Override
+            public void destroy() {
+                Dependency.get(PluginManager.class).removePluginListener(this);
+            }
+
+            @Override
+            public int sortOrder() {
+                return SORT_ORDER_PLUGIN;
+            }
+        }
+
+        private class TunerItem<T> implements Item<T>, Tunable {
+            private final TunerFactory<T> mFactory;
+            private final ArrayMap<String, String> mSettings = new ArrayMap<>();
+            private T mItem;
+
+            public TunerItem(TunerFactory<T> factory, String... setting) {
+                mFactory = factory;
+                Dependency.get(TunerService.class).addTunable(this, setting);
+            }
+
+            @Override
+            public T get() {
+                return mItem;
+            }
+
+            @Override
+            public void destroy() {
+                Dependency.get(TunerService.class).removeTunable(this);
+            }
+
+            @Override
+            public void onTuningChanged(String key, String newValue) {
+                mSettings.put(key, newValue);
+                mItem = mFactory.create(mSettings);
+                notifyChanged();
+            }
+
+            @Override
+            public int sortOrder() {
+                return SORT_ORDER_TUNER;
+            }
+        }
+
+        private class UiModeItem<T> implements Item<T>, ConfigurationListener {
+
+            private final int mDesiredUiMode;
+            private final Supplier<T> mSupplier;
+            private int mUiMode;
+            private Handler mHandler = new Handler();
+
+            public UiModeItem(int uiMode, Supplier<T> supplier) {
+                mDesiredUiMode = uiMode;
+                mSupplier = supplier;
+                mUiMode = mDefaultContext.getResources().getConfiguration().uiMode
+                        & Configuration.UI_MODE_TYPE_MASK;
+                Dependency.get(ConfigurationController.class).addCallback(this);
+            }
+
+            @Override
+            public void onConfigChanged(Configuration newConfig) {
+                int newMode = newConfig.uiMode & Configuration.UI_MODE_TYPE_MASK;
+                if (newMode != mUiMode) {
+                    mUiMode = newMode;
+                    // Post to make sure we don't have concurrent modifications.
+                    mHandler.post(ExtensionImpl.this::notifyChanged);
+                }
+            }
+
+            @Override
+            public T get() {
+                return (mUiMode == mDesiredUiMode) ? mSupplier.get() : null;
+            }
+
+            @Override
+            public void destroy() {
+                Dependency.get(ConfigurationController.class).removeCallback(this);
+            }
+
+            @Override
+            public int sortOrder() {
+                return SORT_ORDER_UI_MODE;
+            }
+        }
+
+        private class FeatureItem<T> implements Item<T> {
+            private final String mFeature;
+            private final Supplier<T> mSupplier;
+
+            public FeatureItem(String feature, Supplier<T> supplier) {
+                mSupplier = supplier;
+                mFeature = feature;
+            }
+
+            @Override
+            public T get() {
+                return mDefaultContext.getPackageManager().hasSystemFeature(mFeature)
+                        ? mSupplier.get() : null;
+            }
+
+            @Override
+            public void destroy() {
+
+            }
+
+            @Override
+            public int sortOrder() {
+                return SORT_ORDER_FEATURE;
+            }
+        }
+
+        private class Default<T> implements Item<T> {
+            private final Supplier<T> mSupplier;
+
+            public Default(Supplier<T> supplier) {
+                mSupplier = supplier;
+            }
+
+            @Override
+            public T get() {
+                return mSupplier.get();
+            }
+
+            @Override
+            public void destroy() {
+
+            }
+
+            @Override
+            public int sortOrder() {
+                return SORT_ORDER_DEFAULT;
+            }
+        }
+    }
+
+    private interface Item<T> extends Producer<T> {
+        int sortOrder();
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/FlashlightController.java b/com/android/systemui/statusbar/policy/FlashlightController.java
new file mode 100644
index 0000000..e576f36
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/FlashlightController.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 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.policy;
+
+import com.android.systemui.Dumpable;
+import com.android.systemui.statusbar.policy.FlashlightController.FlashlightListener;
+
+public interface FlashlightController extends CallbackController<FlashlightListener>, Dumpable {
+
+    boolean hasFlashlight();
+    void setFlashlight(boolean newState);
+    boolean isAvailable();
+    boolean isEnabled();
+
+    public interface FlashlightListener {
+
+        /**
+         * Called when the flashlight was turned off or on.
+         * @param enabled true if the flashlight is currently turned on.
+         */
+        void onFlashlightChanged(boolean enabled);
+
+
+        /**
+         * Called when there is an error that turns the flashlight off.
+         */
+        void onFlashlightError();
+
+        /**
+         * Called when there is a change in availability of the flashlight functionality
+         * @param available true if the flashlight is currently available.
+         */
+        void onFlashlightAvailabilityChanged(boolean available);
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java b/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
new file mode 100644
index 0000000..f0cfa2c
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.systemui.statusbar.policy.FlashlightController.FlashlightListener;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * Manages the flashlight.
+ */
+public class FlashlightControllerImpl implements FlashlightController {
+
+    private static final String TAG = "FlashlightController";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final int DISPATCH_ERROR = 0;
+    private static final int DISPATCH_CHANGED = 1;
+    private static final int DISPATCH_AVAILABILITY_CHANGED = 2;
+
+    private final CameraManager mCameraManager;
+    private final Context mContext;
+    /** Call {@link #ensureHandler()} before using */
+    private Handler mHandler;
+
+    /** Lock on mListeners when accessing */
+    private final ArrayList<WeakReference<FlashlightListener>> mListeners = new ArrayList<>(1);
+
+    /** Lock on {@code this} when accessing */
+    private boolean mFlashlightEnabled;
+
+    private String mCameraId;
+    private boolean mTorchAvailable;
+
+    public FlashlightControllerImpl(Context context) {
+        mContext = context;
+        mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
+
+        tryInitCamera();
+    }
+
+    private void tryInitCamera() {
+        try {
+            mCameraId = getCameraId();
+        } catch (Throwable e) {
+            Log.e(TAG, "Couldn't initialize.", e);
+            return;
+        }
+
+        if (mCameraId != null) {
+            ensureHandler();
+            mCameraManager.registerTorchCallback(mTorchCallback, mHandler);
+        }
+    }
+
+    public void setFlashlight(boolean enabled) {
+        boolean pendingError = false;
+        synchronized (this) {
+            if (mCameraId == null) return;
+            if (mFlashlightEnabled != enabled) {
+                mFlashlightEnabled = enabled;
+                try {
+                    mCameraManager.setTorchMode(mCameraId, enabled);
+                } catch (CameraAccessException e) {
+                    Log.e(TAG, "Couldn't set torch mode", e);
+                    mFlashlightEnabled = false;
+                    pendingError = true;
+                }
+            }
+        }
+        dispatchModeChanged(mFlashlightEnabled);
+        if (pendingError) {
+            dispatchError();
+        }
+    }
+
+    public boolean hasFlashlight() {
+        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH);
+    }
+
+    public synchronized boolean isEnabled() {
+        return mFlashlightEnabled;
+    }
+
+    public synchronized boolean isAvailable() {
+        return mTorchAvailable;
+    }
+
+    public void addCallback(FlashlightListener l) {
+        synchronized (mListeners) {
+            if (mCameraId == null) {
+                tryInitCamera();
+            }
+            cleanUpListenersLocked(l);
+            mListeners.add(new WeakReference<>(l));
+            l.onFlashlightAvailabilityChanged(mTorchAvailable);
+            l.onFlashlightChanged(mFlashlightEnabled);
+        }
+    }
+
+    public void removeCallback(FlashlightListener l) {
+        synchronized (mListeners) {
+            cleanUpListenersLocked(l);
+        }
+    }
+
+    private synchronized void ensureHandler() {
+        if (mHandler == null) {
+            HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
+            thread.start();
+            mHandler = new Handler(thread.getLooper());
+        }
+    }
+
+    private String getCameraId() throws CameraAccessException {
+        String[] ids = mCameraManager.getCameraIdList();
+        for (String id : ids) {
+            CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id);
+            Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
+            Integer lensFacing = c.get(CameraCharacteristics.LENS_FACING);
+            if (flashAvailable != null && flashAvailable
+                    && lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_BACK) {
+                return id;
+            }
+        }
+        return null;
+    }
+
+    private void dispatchModeChanged(boolean enabled) {
+        dispatchListeners(DISPATCH_CHANGED, enabled);
+    }
+
+    private void dispatchError() {
+        dispatchListeners(DISPATCH_CHANGED, false /* argument (ignored) */);
+    }
+
+    private void dispatchAvailabilityChanged(boolean available) {
+        dispatchListeners(DISPATCH_AVAILABILITY_CHANGED, available);
+    }
+
+    private void dispatchListeners(int message, boolean argument) {
+        synchronized (mListeners) {
+            final int N = mListeners.size();
+            boolean cleanup = false;
+            for (int i = 0; i < N; i++) {
+                FlashlightListener l = mListeners.get(i).get();
+                if (l != null) {
+                    if (message == DISPATCH_ERROR) {
+                        l.onFlashlightError();
+                    } else if (message == DISPATCH_CHANGED) {
+                        l.onFlashlightChanged(argument);
+                    } else if (message == DISPATCH_AVAILABILITY_CHANGED) {
+                        l.onFlashlightAvailabilityChanged(argument);
+                    }
+                } else {
+                    cleanup = true;
+                }
+            }
+            if (cleanup) {
+                cleanUpListenersLocked(null);
+            }
+        }
+    }
+
+    private void cleanUpListenersLocked(FlashlightListener listener) {
+        for (int i = mListeners.size() - 1; i >= 0; i--) {
+            FlashlightListener found = mListeners.get(i).get();
+            if (found == null || found == listener) {
+                mListeners.remove(i);
+            }
+        }
+    }
+
+    private final CameraManager.TorchCallback mTorchCallback =
+            new CameraManager.TorchCallback() {
+
+        @Override
+        public void onTorchModeUnavailable(String cameraId) {
+            if (TextUtils.equals(cameraId, mCameraId)) {
+                setCameraAvailable(false);
+            }
+        }
+
+        @Override
+        public void onTorchModeChanged(String cameraId, boolean enabled) {
+            if (TextUtils.equals(cameraId, mCameraId)) {
+                setCameraAvailable(true);
+                setTorchMode(enabled);
+            }
+        }
+
+        private void setCameraAvailable(boolean available) {
+            boolean changed;
+            synchronized (FlashlightControllerImpl.this) {
+                changed = mTorchAvailable != available;
+                mTorchAvailable = available;
+            }
+            if (changed) {
+                if (DEBUG) Log.d(TAG, "dispatchAvailabilityChanged(" + available + ")");
+                dispatchAvailabilityChanged(available);
+            }
+        }
+
+        private void setTorchMode(boolean enabled) {
+            boolean changed;
+            synchronized (FlashlightControllerImpl.this) {
+                changed = mFlashlightEnabled != enabled;
+                mFlashlightEnabled = enabled;
+            }
+            if (changed) {
+                if (DEBUG) Log.d(TAG, "dispatchModeChanged(" + enabled + ")");
+                dispatchModeChanged(enabled);
+            }
+        }
+    };
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("FlashlightController state:");
+
+        pw.print("  mCameraId=");
+        pw.println(mCameraId);
+        pw.print("  mFlashlightEnabled=");
+        pw.println(mFlashlightEnabled);
+        pw.print("  mTorchAvailable=");
+        pw.println(mTorchAvailable);
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/HeadsUpManager.java b/com/android/systemui/statusbar/policy/HeadsUpManager.java
new file mode 100644
index 0000000..53dfb24
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/HeadsUpManager.java
@@ -0,0 +1,766 @@
+/*
+ * 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.policy;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.support.v4.util.ArraySet;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pools;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.phone.StatusBar;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Stack;
+
+/**
+ * A manager which handles heads up notifications which is a special mode where
+ * they simply peek from the top of the screen.
+ */
+public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsListener,
+        VisualStabilityManager.Callback {
+    private static final String TAG = "HeadsUpManager";
+    private static final boolean DEBUG = false;
+    private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
+    private static final int TAG_CLICKED_NOTIFICATION = R.id.is_clicked_heads_up_tag;
+
+    private final int mHeadsUpNotificationDecay;
+    private final int mMinimumDisplayTime;
+
+    private final int mTouchAcceptanceDelay;
+    private final ArrayMap<String, Long> mSnoozedPackages;
+    private final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>();
+    private final int mDefaultSnoozeLengthMs;
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private final Pools.Pool<HeadsUpEntry> mEntryPool = new Pools.Pool<HeadsUpEntry>() {
+
+        private Stack<HeadsUpEntry> mPoolObjects = new Stack<>();
+
+        @Override
+        public HeadsUpEntry acquire() {
+            if (!mPoolObjects.isEmpty()) {
+                return mPoolObjects.pop();
+            }
+            return new HeadsUpEntry();
+        }
+
+        @Override
+        public boolean release(HeadsUpEntry instance) {
+            instance.reset();
+            mPoolObjects.push(instance);
+            return true;
+        }
+    };
+
+    private final View mStatusBarWindowView;
+    private final int mStatusBarHeight;
+    private final Context mContext;
+    private final NotificationGroupManager mGroupManager;
+    private StatusBar mBar;
+    private int mSnoozeLengthMs;
+    private ContentObserver mSettingsObserver;
+    private HashMap<String, HeadsUpEntry> mHeadsUpEntries = new HashMap<>();
+    private HashSet<String> mSwipedOutKeys = new HashSet<>();
+    private int mUser;
+    private Clock mClock;
+    private boolean mReleaseOnExpandFinish;
+    private boolean mTrackingHeadsUp;
+    private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>();
+    private ArraySet<NotificationData.Entry> mEntriesToRemoveWhenReorderingAllowed
+            = new ArraySet<>();
+    private boolean mIsExpanded;
+    private boolean mHasPinnedNotification;
+    private int[] mTmpTwoArray = new int[2];
+    private boolean mHeadsUpGoingAway;
+    private boolean mWaitingOnCollapseWhenGoingAway;
+    private boolean mIsObserving;
+    private boolean mRemoteInputActive;
+    private float mExpandedHeight;
+    private VisualStabilityManager mVisualStabilityManager;
+    private int mStatusBarState;
+
+    public HeadsUpManager(final Context context, View statusBarWindowView,
+                          NotificationGroupManager groupManager) {
+        mContext = context;
+        Resources resources = mContext.getResources();
+        mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay);
+        mSnoozedPackages = new ArrayMap<>();
+        mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms);
+        mSnoozeLengthMs = mDefaultSnoozeLengthMs;
+        mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time);
+        mHeadsUpNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay);
+        mClock = new Clock();
+
+        mSnoozeLengthMs = Settings.Global.getInt(context.getContentResolver(),
+                SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs);
+        mSettingsObserver = new ContentObserver(mHandler) {
+            @Override
+            public void onChange(boolean selfChange) {
+                final int packageSnoozeLengthMs = Settings.Global.getInt(
+                        context.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1);
+                if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) {
+                    mSnoozeLengthMs = packageSnoozeLengthMs;
+                    if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
+                }
+            }
+        };
+        context.getContentResolver().registerContentObserver(
+                Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false,
+                mSettingsObserver);
+        mStatusBarWindowView = statusBarWindowView;
+        mGroupManager = groupManager;
+        mStatusBarHeight = resources.getDimensionPixelSize(
+                com.android.internal.R.dimen.status_bar_height);
+    }
+
+    private void updateTouchableRegionListener() {
+        boolean shouldObserve = mHasPinnedNotification || mHeadsUpGoingAway
+                || mWaitingOnCollapseWhenGoingAway;
+        if (shouldObserve == mIsObserving) {
+            return;
+        }
+        if (shouldObserve) {
+            mStatusBarWindowView.getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+            mStatusBarWindowView.requestLayout();
+        } else {
+            mStatusBarWindowView.getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+        }
+        mIsObserving = shouldObserve;
+    }
+
+    public void setBar(StatusBar bar) {
+        mBar = bar;
+    }
+
+    public void addListener(OnHeadsUpChangedListener listener) {
+        mListeners.add(listener);
+    }
+
+    public void removeListener(OnHeadsUpChangedListener listener) {
+        mListeners.remove(listener);
+    }
+
+    public StatusBar getBar() {
+        return mBar;
+    }
+
+    /**
+     * Called when posting a new notification to the heads up.
+     */
+    public void showNotification(NotificationData.Entry headsUp) {
+        if (DEBUG) Log.v(TAG, "showNotification");
+        addHeadsUpEntry(headsUp);
+        updateNotification(headsUp, true);
+        headsUp.setInterruption();
+    }
+
+    /**
+     * Called when updating or posting a notification to the heads up.
+     */
+    public void updateNotification(NotificationData.Entry headsUp, boolean alert) {
+        if (DEBUG) Log.v(TAG, "updateNotification");
+
+        headsUp.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+
+        if (alert) {
+            HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(headsUp.key);
+            if (headsUpEntry == null) {
+                // the entry was released before this update (i.e by a listener) This can happen
+                // with the groupmanager
+                return;
+            }
+            headsUpEntry.updateEntry();
+            setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUp));
+        }
+    }
+
+    private void addHeadsUpEntry(NotificationData.Entry entry) {
+        HeadsUpEntry headsUpEntry = mEntryPool.acquire();
+
+        // This will also add the entry to the sortedList
+        headsUpEntry.setEntry(entry);
+        mHeadsUpEntries.put(entry.key, headsUpEntry);
+        entry.row.setHeadsUp(true);
+        setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(entry));
+        for (OnHeadsUpChangedListener listener : mListeners) {
+            listener.onHeadsUpStateChanged(entry, true);
+        }
+        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+    }
+
+    private boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) {
+        return mStatusBarState != StatusBarState.KEYGUARD
+                && !mIsExpanded || hasFullScreenIntent(entry);
+    }
+
+    private boolean hasFullScreenIntent(NotificationData.Entry entry) {
+        return entry.notification.getNotification().fullScreenIntent != null;
+    }
+
+    private void setEntryPinned(HeadsUpEntry headsUpEntry, boolean isPinned) {
+        ExpandableNotificationRow row = headsUpEntry.entry.row;
+        if (row.isPinned() != isPinned) {
+            row.setPinned(isPinned);
+            updatePinnedMode();
+            for (OnHeadsUpChangedListener listener : mListeners) {
+                if (isPinned) {
+                    listener.onHeadsUpPinned(row);
+                } else {
+                    listener.onHeadsUpUnPinned(row);
+                }
+            }
+        }
+    }
+
+    private void removeHeadsUpEntry(NotificationData.Entry entry) {
+        HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key);
+        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+        entry.row.setHeadsUp(false);
+        setEntryPinned(remove, false /* isPinned */);
+        for (OnHeadsUpChangedListener listener : mListeners) {
+            listener.onHeadsUpStateChanged(entry, false);
+        }
+        mEntryPool.release(remove);
+    }
+
+    public void removeAllHeadsUpEntries() {
+        for (String key : mHeadsUpEntries.keySet()) {
+            removeHeadsUpEntry(mHeadsUpEntries.get(key).entry);
+        }
+    }
+
+    private void updatePinnedMode() {
+        boolean hasPinnedNotification = hasPinnedNotificationInternal();
+        if (hasPinnedNotification == mHasPinnedNotification) {
+            return;
+        }
+        mHasPinnedNotification = hasPinnedNotification;
+        if (mHasPinnedNotification) {
+            MetricsLogger.count(mContext, "note_peek", 1);
+        }
+        updateTouchableRegionListener();
+        for (OnHeadsUpChangedListener listener : mListeners) {
+            listener.onHeadsUpPinnedModeChanged(hasPinnedNotification);
+        }
+    }
+
+    /**
+     * React to the removal of the notification in the heads up.
+     *
+     * @return true if the notification was removed and false if it still needs to be kept around
+     * for a bit since it wasn't shown long enough
+     */
+    public boolean removeNotification(String key, boolean ignoreEarliestRemovalTime) {
+        if (DEBUG) Log.v(TAG, "remove");
+        if (wasShownLongEnough(key) || ignoreEarliestRemovalTime) {
+            releaseImmediately(key);
+            return true;
+        } else {
+            getHeadsUpEntry(key).removeAsSoonAsPossible();
+            return false;
+        }
+    }
+
+    private boolean wasShownLongEnough(String key) {
+        HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
+        HeadsUpEntry topEntry = getTopEntry();
+        if (mSwipedOutKeys.contains(key)) {
+            // We always instantly dismiss views being manually swiped out.
+            mSwipedOutKeys.remove(key);
+            return true;
+        }
+        if (headsUpEntry != topEntry) {
+            return true;
+        }
+        return headsUpEntry.wasShownLongEnough();
+    }
+
+    public boolean isHeadsUp(String key) {
+        return mHeadsUpEntries.containsKey(key);
+    }
+
+    /**
+     * Push any current Heads Up notification down into the shade.
+     */
+    public void releaseAllImmediately() {
+        if (DEBUG) Log.v(TAG, "releaseAllImmediately");
+        ArrayList<String> keys = new ArrayList<>(mHeadsUpEntries.keySet());
+        for (String key : keys) {
+            releaseImmediately(key);
+        }
+    }
+
+    public void releaseImmediately(String key) {
+        HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
+        if (headsUpEntry == null) {
+            return;
+        }
+        NotificationData.Entry shadeEntry = headsUpEntry.entry;
+        removeHeadsUpEntry(shadeEntry);
+    }
+
+    public boolean isSnoozed(String packageName) {
+        final String key = snoozeKey(packageName, mUser);
+        Long snoozedUntil = mSnoozedPackages.get(key);
+        if (snoozedUntil != null) {
+            if (snoozedUntil > SystemClock.elapsedRealtime()) {
+                if (DEBUG) Log.v(TAG, key + " snoozed");
+                return true;
+            }
+            mSnoozedPackages.remove(packageName);
+        }
+        return false;
+    }
+
+    public void snooze() {
+        for (String key : mHeadsUpEntries.keySet()) {
+            HeadsUpEntry entry = mHeadsUpEntries.get(key);
+            String packageName = entry.entry.notification.getPackageName();
+            mSnoozedPackages.put(snoozeKey(packageName, mUser),
+                    SystemClock.elapsedRealtime() + mSnoozeLengthMs);
+        }
+        mReleaseOnExpandFinish = true;
+    }
+
+    private static String snoozeKey(String packageName, int user) {
+        return user + "," + packageName;
+    }
+
+    private HeadsUpEntry getHeadsUpEntry(String key) {
+        return mHeadsUpEntries.get(key);
+    }
+
+    public NotificationData.Entry getEntry(String key) {
+        return mHeadsUpEntries.get(key).entry;
+    }
+
+    public Collection<HeadsUpEntry> getAllEntries() {
+        return mHeadsUpEntries.values();
+    }
+
+    public HeadsUpEntry getTopEntry() {
+        if (mHeadsUpEntries.isEmpty()) {
+            return null;
+        }
+        HeadsUpEntry topEntry = null;
+        for (HeadsUpEntry entry: mHeadsUpEntries.values()) {
+            if (topEntry == null || entry.compareTo(topEntry) == -1) {
+                topEntry = entry;
+            }
+        }
+        return topEntry;
+    }
+
+    /**
+     * Decides whether a click is invalid for a notification, i.e it has not been shown long enough
+     * that a user might have consciously clicked on it.
+     *
+     * @param key the key of the touched notification
+     * @return whether the touch is invalid and should be discarded
+     */
+    public boolean shouldSwallowClick(String key) {
+        HeadsUpEntry entry = mHeadsUpEntries.get(key);
+        if (entry != null && mClock.currentTimeMillis() < entry.postTime) {
+            return true;
+        }
+        return false;
+    }
+
+    public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
+        if (mIsExpanded || mBar.isBouncerShowing()) {
+            // The touchable region is always the full area when expanded
+            return;
+        }
+        if (mHasPinnedNotification) {
+            ExpandableNotificationRow topEntry = getTopEntry().entry.row;
+            if (topEntry.isChildInGroup()) {
+                final ExpandableNotificationRow groupSummary
+                        = mGroupManager.getGroupSummary(topEntry.getStatusBarNotification());
+                if (groupSummary != null) {
+                    topEntry = groupSummary;
+                }
+            }
+            topEntry.getLocationOnScreen(mTmpTwoArray);
+            int minX = mTmpTwoArray[0];
+            int maxX = mTmpTwoArray[0] + topEntry.getWidth();
+            int maxY = topEntry.getIntrinsicHeight();
+
+            info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+            info.touchableRegion.set(minX, 0, maxX, maxY);
+        } else if (mHeadsUpGoingAway || mWaitingOnCollapseWhenGoingAway) {
+            info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+            info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
+        }
+    }
+
+    public void setUser(int user) {
+        mUser = user;
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("HeadsUpManager state:");
+        pw.print("  mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay);
+        pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
+        pw.print("  now="); pw.println(SystemClock.elapsedRealtime());
+        pw.print("  mUser="); pw.println(mUser);
+        for (HeadsUpEntry entry: mHeadsUpEntries.values()) {
+            pw.print("  HeadsUpEntry="); pw.println(entry.entry);
+        }
+        int N = mSnoozedPackages.size();
+        pw.println("  snoozed packages: " + N);
+        for (int i = 0; i < N; i++) {
+            pw.print("    "); pw.print(mSnoozedPackages.valueAt(i));
+            pw.print(", "); pw.println(mSnoozedPackages.keyAt(i));
+        }
+    }
+
+    public boolean hasPinnedHeadsUp() {
+        return mHasPinnedNotification;
+    }
+
+    private boolean hasPinnedNotificationInternal() {
+        for (String key : mHeadsUpEntries.keySet()) {
+            HeadsUpEntry entry = mHeadsUpEntries.get(key);
+            if (entry.entry.row.isPinned()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Notifies that a notification was swiped out and will be removed.
+     *
+     * @param key the notification key
+     */
+    public void addSwipedOutNotification(String key) {
+        mSwipedOutKeys.add(key);
+    }
+
+    public void unpinAll() {
+        for (String key : mHeadsUpEntries.keySet()) {
+            HeadsUpEntry entry = mHeadsUpEntries.get(key);
+            setEntryPinned(entry, false /* isPinned */);
+            // maybe it got un sticky
+            entry.updateEntry(false /* updatePostTime */);
+        }
+    }
+
+    public void onExpandingFinished() {
+        if (mReleaseOnExpandFinish) {
+            releaseAllImmediately();
+            mReleaseOnExpandFinish = false;
+        } else {
+            for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
+                if (isHeadsUp(entry.key)) {
+                    // Maybe the heads-up was removed already
+                    removeHeadsUpEntry(entry);
+                }
+            }
+        }
+        mEntriesToRemoveAfterExpand.clear();
+    }
+
+    public void setTrackingHeadsUp(boolean trackingHeadsUp) {
+        mTrackingHeadsUp = trackingHeadsUp;
+    }
+
+    public boolean isTrackingHeadsUp() {
+        return mTrackingHeadsUp;
+    }
+
+    public void setIsExpanded(boolean isExpanded) {
+        if (isExpanded != mIsExpanded) {
+            mIsExpanded = isExpanded;
+            if (isExpanded) {
+                // make sure our state is sane
+                mWaitingOnCollapseWhenGoingAway = false;
+                mHeadsUpGoingAway = false;
+                updateTouchableRegionListener();
+            }
+        }
+    }
+
+    /**
+     * @return the height of the top heads up notification when pinned. This is different from the
+     *         intrinsic height, which also includes whether the notification is system expanded and
+     *         is mainly used when dragging down from a heads up notification.
+     */
+    public int getTopHeadsUpPinnedHeight() {
+        HeadsUpEntry topEntry = getTopEntry();
+        if (topEntry == null || topEntry.entry == null) {
+            return 0;
+        }
+        ExpandableNotificationRow row = topEntry.entry.row;
+        if (row.isChildInGroup()) {
+            final ExpandableNotificationRow groupSummary
+                    = mGroupManager.getGroupSummary(row.getStatusBarNotification());
+            if (groupSummary != null) {
+                row = groupSummary;
+            }
+        }
+        return row.getPinnedHeadsUpHeight();
+    }
+
+    /**
+     * Compare two entries and decide how they should be ranked.
+     *
+     * @return -1 if the first argument should be ranked higher than the second, 1 if the second
+     * one should be ranked higher and 0 if they are equal.
+     */
+    public int compare(NotificationData.Entry a, NotificationData.Entry b) {
+        HeadsUpEntry aEntry = getHeadsUpEntry(a.key);
+        HeadsUpEntry bEntry = getHeadsUpEntry(b.key);
+        if (aEntry == null || bEntry == null) {
+            return aEntry == null ? 1 : -1;
+        }
+        return aEntry.compareTo(bEntry);
+    }
+
+    /**
+     * Set that we are exiting the headsUp pinned mode, but some notifications might still be
+     * animating out. This is used to keep the touchable regions in a sane state.
+     */
+    public void setHeadsUpGoingAway(boolean headsUpGoingAway) {
+        if (headsUpGoingAway != mHeadsUpGoingAway) {
+            mHeadsUpGoingAway = headsUpGoingAway;
+            if (!headsUpGoingAway) {
+                waitForStatusBarLayout();
+            }
+            updateTouchableRegionListener();
+        }
+    }
+
+    /**
+     * We need to wait on the whole panel to collapse, before we can remove the touchable region
+     * listener.
+     */
+    private void waitForStatusBarLayout() {
+        mWaitingOnCollapseWhenGoingAway = true;
+        mStatusBarWindowView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+            @Override
+            public void onLayoutChange(View v, int left, int top, int right, int bottom,
+                    int oldLeft,
+                    int oldTop, int oldRight, int oldBottom) {
+                if (mStatusBarWindowView.getHeight() <= mStatusBarHeight) {
+                    mStatusBarWindowView.removeOnLayoutChangeListener(this);
+                    mWaitingOnCollapseWhenGoingAway = false;
+                    updateTouchableRegionListener();
+                }
+            }
+        });
+    }
+
+    public static void setIsClickedNotification(View child, boolean clicked) {
+        child.setTag(TAG_CLICKED_NOTIFICATION, clicked ? true : null);
+    }
+
+    public static boolean isClickedHeadsUpNotification(View child) {
+        Boolean clicked = (Boolean) child.getTag(TAG_CLICKED_NOTIFICATION);
+        return clicked != null && clicked;
+    }
+
+    public void setRemoteInputActive(NotificationData.Entry entry, boolean remoteInputActive) {
+        HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key);
+        if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) {
+            headsUpEntry.remoteInputActive = remoteInputActive;
+            if (remoteInputActive) {
+                headsUpEntry.removeAutoRemovalCallbacks();
+            } else {
+                headsUpEntry.updateEntry(false /* updatePostTime */);
+            }
+        }
+    }
+
+    /**
+     * Set an entry to be expanded and therefore stick in the heads up area if it's pinned
+     * until it's collapsed again.
+     */
+    public void setExpanded(NotificationData.Entry entry, boolean expanded) {
+        HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key);
+        if (headsUpEntry != null && headsUpEntry.expanded != expanded && entry.row.isPinned()) {
+            headsUpEntry.expanded = expanded;
+            if (expanded) {
+                headsUpEntry.removeAutoRemovalCallbacks();
+            } else {
+                headsUpEntry.updateEntry(false /* updatePostTime */);
+            }
+        }
+    }
+
+    @Override
+    public void onReorderingAllowed() {
+        mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(false);
+        for (NotificationData.Entry entry : mEntriesToRemoveWhenReorderingAllowed) {
+            if (isHeadsUp(entry.key)) {
+                // Maybe the heads-up was removed already
+                removeHeadsUpEntry(entry);
+            }
+        }
+        mEntriesToRemoveWhenReorderingAllowed.clear();
+        mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(true);
+    }
+
+    public void setVisualStabilityManager(VisualStabilityManager visualStabilityManager) {
+        mVisualStabilityManager = visualStabilityManager;
+    }
+
+    public void setStatusBarState(int statusBarState) {
+        mStatusBarState = statusBarState;
+    }
+
+    /**
+     * This represents a notification and how long it is in a heads up mode. It also manages its
+     * lifecycle automatically when created.
+     */
+    public class HeadsUpEntry implements Comparable<HeadsUpEntry> {
+        public NotificationData.Entry entry;
+        public long postTime;
+        public long earliestRemovaltime;
+        private Runnable mRemoveHeadsUpRunnable;
+        public boolean remoteInputActive;
+        public boolean expanded;
+
+        public void setEntry(final NotificationData.Entry entry) {
+            this.entry = entry;
+
+            // The actual post time will be just after the heads-up really slided in
+            postTime = mClock.currentTimeMillis() + mTouchAcceptanceDelay;
+            mRemoveHeadsUpRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    if (!mVisualStabilityManager.isReorderingAllowed()) {
+                        mEntriesToRemoveWhenReorderingAllowed.add(entry);
+                        mVisualStabilityManager.addReorderingAllowedCallback(HeadsUpManager.this);
+                    } else if (!mTrackingHeadsUp) {
+                        removeHeadsUpEntry(entry);
+                    } else {
+                        mEntriesToRemoveAfterExpand.add(entry);
+                    }
+                }
+            };
+            updateEntry();
+        }
+
+        public void updateEntry() {
+            updateEntry(true);
+        }
+
+        public void updateEntry(boolean updatePostTime) {
+            long currentTime = mClock.currentTimeMillis();
+            earliestRemovaltime = currentTime + mMinimumDisplayTime;
+            if (updatePostTime) {
+                postTime = Math.max(postTime, currentTime);
+            }
+            removeAutoRemovalCallbacks();
+            if (mEntriesToRemoveAfterExpand.contains(entry)) {
+                mEntriesToRemoveAfterExpand.remove(entry);
+            }
+            if (mEntriesToRemoveWhenReorderingAllowed.contains(entry)) {
+                mEntriesToRemoveWhenReorderingAllowed.remove(entry);
+            }
+            if (!isSticky()) {
+                long finishTime = postTime + mHeadsUpNotificationDecay;
+                long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
+                mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay);
+            }
+        }
+
+        private boolean isSticky() {
+            return (entry.row.isPinned() && expanded)
+                    || remoteInputActive || hasFullScreenIntent(entry);
+        }
+
+        @Override
+        public int compareTo(HeadsUpEntry o) {
+            boolean isPinned = entry.row.isPinned();
+            boolean otherPinned = o.entry.row.isPinned();
+            if (isPinned && !otherPinned) {
+                return -1;
+            } else if (!isPinned && otherPinned) {
+                return 1;
+            }
+            boolean selfFullscreen = hasFullScreenIntent(entry);
+            boolean otherFullscreen = hasFullScreenIntent(o.entry);
+            if (selfFullscreen && !otherFullscreen) {
+                return -1;
+            } else if (!selfFullscreen && otherFullscreen) {
+                return 1;
+            }
+
+            if (remoteInputActive && !o.remoteInputActive) {
+                return -1;
+            } else if (!remoteInputActive && o.remoteInputActive) {
+                return 1;
+            }
+
+            return postTime < o.postTime ? 1
+                    : postTime == o.postTime ? entry.key.compareTo(o.entry.key)
+                            : -1;
+        }
+
+        public void removeAutoRemovalCallbacks() {
+            mHandler.removeCallbacks(mRemoveHeadsUpRunnable);
+        }
+
+        public boolean wasShownLongEnough() {
+            return earliestRemovaltime < mClock.currentTimeMillis();
+        }
+
+        public void removeAsSoonAsPossible() {
+            removeAutoRemovalCallbacks();
+            mHandler.postDelayed(mRemoveHeadsUpRunnable,
+                    earliestRemovaltime - mClock.currentTimeMillis());
+        }
+
+        public void reset() {
+            removeAutoRemovalCallbacks();
+            entry = null;
+            mRemoveHeadsUpRunnable = null;
+            expanded = false;
+            remoteInputActive = false;
+        }
+    }
+
+    public static class Clock {
+        public long currentTimeMillis() {
+            return SystemClock.elapsedRealtime();
+        }
+    }
+
+}
diff --git a/com/android/systemui/statusbar/policy/HotspotController.java b/com/android/systemui/statusbar/policy/HotspotController.java
new file mode 100644
index 0000000..6457209
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/HotspotController.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import com.android.systemui.Dumpable;
+import com.android.systemui.statusbar.policy.HotspotController.Callback;
+
+public interface HotspotController extends CallbackController<Callback>, Dumpable {
+    boolean isHotspotEnabled();
+    boolean isHotspotTransient();
+
+    void setHotspotEnabled(boolean enabled);
+    boolean isHotspotSupported();
+
+    public interface Callback {
+        void onHotspotChanged(boolean enabled);
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/HotspotControllerImpl.java b/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
new file mode 100644
index 0000000..1ebb986
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.wifi.WifiManager;
+import android.os.Handler;
+import android.os.UserManager;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+public class HotspotControllerImpl implements HotspotController {
+
+    private static final String TAG = "HotspotController";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
+    private final Receiver mReceiver = new Receiver();
+    private final ConnectivityManager mConnectivityManager;
+    private final Context mContext;
+
+    private int mHotspotState;
+    private boolean mWaitingForCallback;
+
+    public HotspotControllerImpl(Context context) {
+        mContext = context;
+        mConnectivityManager = (ConnectivityManager) context.getSystemService(
+                Context.CONNECTIVITY_SERVICE);
+    }
+
+    @Override
+    public boolean isHotspotSupported() {
+        return mConnectivityManager.isTetheringSupported()
+                && mConnectivityManager.getTetherableWifiRegexs().length != 0
+                && UserManager.get(mContext).isUserAdmin(ActivityManager.getCurrentUser());
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("HotspotController state:");
+        pw.print("  mHotspotEnabled="); pw.println(stateToString(mHotspotState));
+    }
+
+    private static String stateToString(int hotspotState) {
+        switch (hotspotState) {
+            case WifiManager.WIFI_AP_STATE_DISABLED:
+                return "DISABLED";
+            case WifiManager.WIFI_AP_STATE_DISABLING:
+                return "DISABLING";
+            case WifiManager.WIFI_AP_STATE_ENABLED:
+                return "ENABLED";
+            case WifiManager.WIFI_AP_STATE_ENABLING:
+                return "ENABLING";
+            case WifiManager.WIFI_AP_STATE_FAILED:
+                return "FAILED";
+        }
+        return null;
+    }
+
+    @Override
+    public void addCallback(Callback callback) {
+        synchronized (mCallbacks) {
+            if (callback == null || mCallbacks.contains(callback)) return;
+            if (DEBUG) Log.d(TAG, "addCallback " + callback);
+            mCallbacks.add(callback);
+            mReceiver.setListening(!mCallbacks.isEmpty());
+        }
+    }
+
+    @Override
+    public void removeCallback(Callback callback) {
+        if (callback == null) return;
+        if (DEBUG) Log.d(TAG, "removeCallback " + callback);
+        synchronized (mCallbacks) {
+            mCallbacks.remove(callback);
+            mReceiver.setListening(!mCallbacks.isEmpty());
+        }
+    }
+
+    @Override
+    public boolean isHotspotEnabled() {
+        return mHotspotState == WifiManager.WIFI_AP_STATE_ENABLED;
+    }
+
+    @Override
+    public boolean isHotspotTransient() {
+        return mWaitingForCallback || (mHotspotState == WifiManager.WIFI_AP_STATE_ENABLING);
+    }
+
+    @Override
+    public void setHotspotEnabled(boolean enabled) {
+        if (enabled) {
+            OnStartTetheringCallback callback = new OnStartTetheringCallback();
+            mWaitingForCallback = true;
+            if (DEBUG) Log.d(TAG, "Starting tethering");
+            mConnectivityManager.startTethering(
+                    ConnectivityManager.TETHERING_WIFI, false, callback);
+            fireCallback(isHotspotEnabled());
+        } else {
+            mConnectivityManager.stopTethering(ConnectivityManager.TETHERING_WIFI);
+        }
+    }
+
+    private void fireCallback(boolean isEnabled) {
+        synchronized (mCallbacks) {
+            for (Callback callback : mCallbacks) {
+                callback.onHotspotChanged(isEnabled);
+            }
+        }
+    }
+
+    private final class OnStartTetheringCallback extends
+            ConnectivityManager.OnStartTetheringCallback {
+        @Override
+        public void onTetheringStarted() {
+            if (DEBUG) Log.d(TAG, "onTetheringStarted");
+            mWaitingForCallback = false;
+            // Don't fire a callback here, instead wait for the next update from wifi.
+        }
+
+        @Override
+        public void onTetheringFailed() {
+            if (DEBUG) Log.d(TAG, "onTetheringFailed");
+            mWaitingForCallback = false;
+            fireCallback(isHotspotEnabled());
+          // TODO: Show error.
+        }
+    }
+
+    private final class Receiver extends BroadcastReceiver {
+        private boolean mRegistered;
+
+        public void setListening(boolean listening) {
+            if (listening && !mRegistered) {
+                if (DEBUG) Log.d(TAG, "Registering receiver");
+                final IntentFilter filter = new IntentFilter();
+                filter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
+                mContext.registerReceiver(this, filter);
+                mRegistered = true;
+            } else if (!listening && mRegistered) {
+                if (DEBUG) Log.d(TAG, "Unregistering receiver");
+                mContext.unregisterReceiver(this);
+                mRegistered = false;
+            }
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            int state = intent.getIntExtra(
+                    WifiManager.EXTRA_WIFI_AP_STATE, WifiManager.WIFI_AP_STATE_FAILED);
+            if (DEBUG) Log.d(TAG, "onReceive " + state);
+            mHotspotState = state;
+            fireCallback(mHotspotState == WifiManager.WIFI_AP_STATE_ENABLED);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/IconLogger.java b/com/android/systemui/statusbar/policy/IconLogger.java
new file mode 100644
index 0000000..710e1df
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/IconLogger.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 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.policy;
+
+public interface IconLogger {
+
+    void onIconShown(String tag);
+    void onIconHidden(String tag);
+
+    default void onIconVisibility(String tag, boolean visible) {
+        if (visible) {
+            onIconShown(tag);
+        } else {
+            onIconHidden(tag);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/IconLoggerImpl.java b/com/android/systemui/statusbar/policy/IconLoggerImpl.java
new file mode 100644
index 0000000..0c201c3
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/IconLoggerImpl.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2017 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.policy;
+
+import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_NUM_STATUS_ICONS;
+import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_STATUS_ICONS;
+import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.STATUS_BAR_ICONS_CHANGED;
+import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_ACTION;
+
+import android.content.Context;
+import android.metrics.LogMaker;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArraySet;
+
+import com.android.internal.logging.MetricsLogger;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class IconLoggerImpl implements IconLogger {
+
+    // Minimum ms between log statements.
+    // NonFinalForTesting
+    @VisibleForTesting
+    protected static long MIN_LOG_INTERVAL = 1000;
+
+    private final Context mContext;
+    private final Handler mHandler;
+    private final MetricsLogger mLogger;
+    private final ArraySet<String> mIcons = new ArraySet<>();
+    private final List<String> mIconIndex;
+    private long mLastLog = System.currentTimeMillis();
+
+    public IconLoggerImpl(Context context, Looper bgLooper, MetricsLogger logger) {
+        mContext = context;
+        mHandler = new Handler(bgLooper);
+        mLogger = logger;
+        String[] icons = mContext.getResources().getStringArray(
+                com.android.internal.R.array.config_statusBarIcons);
+        mIconIndex = Arrays.asList(icons);
+        doLog();
+    }
+
+    @Override
+    public void onIconShown(String tag) {
+        synchronized (mIcons) {
+            if (mIcons.contains(tag)) return;
+            mIcons.add(tag);
+        }
+        if (!mHandler.hasCallbacks(mLog)) {
+            mHandler.postDelayed(mLog, MIN_LOG_INTERVAL);
+        }
+    }
+
+    @Override
+    public void onIconHidden(String tag) {
+        synchronized (mIcons) {
+            if (!mIcons.contains(tag)) return;
+            mIcons.remove(tag);
+        }
+        if (!mHandler.hasCallbacks(mLog)) {
+            mHandler.postDelayed(mLog, MIN_LOG_INTERVAL);
+        }
+    }
+
+    private void doLog() {
+        long time = System.currentTimeMillis();
+        long timeSinceLastLog = time - mLastLog;
+        mLastLog = time;
+
+        ArraySet<String> icons;
+        synchronized (mIcons) {
+            icons = new ArraySet<>(mIcons);
+        }
+        mLogger.write(new LogMaker(STATUS_BAR_ICONS_CHANGED)
+                .setType(TYPE_ACTION)
+                .setLatency(timeSinceLastLog)
+                .addTaggedData(FIELD_NUM_STATUS_ICONS, icons.size())
+                .addTaggedData(FIELD_STATUS_ICONS, getBitField(icons)));
+    }
+
+    private int getBitField(ArraySet<String> icons) {
+        int iconsVisible = 0;
+        for (String icon : icons) {
+            int index = mIconIndex.indexOf(icon);
+            if (index >= 0) {
+                iconsVisible |= (1 << index);
+            }
+        }
+        return iconsVisible;
+    }
+
+    private final Runnable mLog = this::doLog;
+}
diff --git a/com/android/systemui/statusbar/policy/KeyButtonDrawable.java b/com/android/systemui/statusbar/policy/KeyButtonDrawable.java
new file mode 100644
index 0000000..21a96e2
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/KeyButtonDrawable.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 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.policy;
+
+import android.annotation.Nullable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.view.Gravity;
+
+/**
+ * Drawable for {@link KeyButtonView}s which contains an asset for both normal mode and light
+ * navigation bar mode.
+ */
+public class KeyButtonDrawable extends LayerDrawable {
+
+    private final boolean mHasDarkDrawable;
+
+    public static KeyButtonDrawable create(Drawable lightDrawable,
+            @Nullable Drawable darkDrawable) {
+        if (darkDrawable != null) {
+            return new KeyButtonDrawable(
+                    new Drawable[] { lightDrawable.mutate(), darkDrawable.mutate() });
+        } else {
+            return new KeyButtonDrawable(new Drawable[] { lightDrawable.mutate() });
+        }
+    }
+
+    private KeyButtonDrawable(Drawable[] drawables) {
+        super(drawables);
+        for (int i = 0; i < drawables.length; i++) {
+            setLayerGravity(i, Gravity.CENTER);
+        }
+        mutate();
+        mHasDarkDrawable = drawables.length > 1;
+        setDarkIntensity(0f);
+    }
+
+    public void setDarkIntensity(float intensity) {
+        if (!mHasDarkDrawable) {
+            return;
+        }
+        getDrawable(0).setAlpha((int) ((1 - intensity) * 255f));
+        getDrawable(1).setAlpha((int) (intensity * 255f));
+        invalidateSelf();
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/KeyButtonRipple.java b/com/android/systemui/statusbar/policy/KeyButtonRipple.java
new file mode 100644
index 0000000..cc7943b
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/KeyButtonRipple.java
@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.CanvasProperty;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.view.DisplayListCanvas;
+import android.view.RenderNodeAnimator;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+public class KeyButtonRipple extends Drawable {
+
+    private static final float GLOW_MAX_SCALE_FACTOR = 1.35f;
+    private static final float GLOW_MAX_ALPHA = 0.2f;
+    private static final float GLOW_MAX_ALPHA_DARK = 0.1f;
+    private static final int ANIMATION_DURATION_SCALE = 350;
+    private static final int ANIMATION_DURATION_FADE = 450;
+
+    private Paint mRipplePaint;
+    private CanvasProperty<Float> mLeftProp;
+    private CanvasProperty<Float> mTopProp;
+    private CanvasProperty<Float> mRightProp;
+    private CanvasProperty<Float> mBottomProp;
+    private CanvasProperty<Float> mRxProp;
+    private CanvasProperty<Float> mRyProp;
+    private CanvasProperty<Paint> mPaintProp;
+    private float mGlowAlpha = 0f;
+    private float mGlowScale = 1f;
+    private boolean mPressed;
+    private boolean mDrawingHardwareGlow;
+    private int mMaxWidth;
+    private boolean mLastDark;
+    private boolean mDark;
+
+    private final Interpolator mInterpolator = new LogInterpolator();
+    private boolean mSupportHardware;
+    private final View mTargetView;
+
+    private final HashSet<Animator> mRunningAnimations = new HashSet<>();
+    private final ArrayList<Animator> mTmpArray = new ArrayList<>();
+
+    public KeyButtonRipple(Context ctx, View targetView) {
+        mMaxWidth =  ctx.getResources().getDimensionPixelSize(R.dimen.key_button_ripple_max_width);
+        mTargetView = targetView;
+    }
+
+    public void setDarkIntensity(float darkIntensity) {
+        mDark = darkIntensity >= 0.5f;
+    }
+
+    private Paint getRipplePaint() {
+        if (mRipplePaint == null) {
+            mRipplePaint = new Paint();
+            mRipplePaint.setAntiAlias(true);
+            mRipplePaint.setColor(mLastDark ? 0xff000000 : 0xffffffff);
+        }
+        return mRipplePaint;
+    }
+
+    private void drawSoftware(Canvas canvas) {
+        if (mGlowAlpha > 0f) {
+            final Paint p = getRipplePaint();
+            p.setAlpha((int)(mGlowAlpha * 255f));
+
+            final float w = getBounds().width();
+            final float h = getBounds().height();
+            final boolean horizontal = w > h;
+            final float diameter = getRippleSize() * mGlowScale;
+            final float radius = diameter * .5f;
+            final float cx = w * .5f;
+            final float cy = h * .5f;
+            final float rx = horizontal ? radius : cx;
+            final float ry = horizontal ? cy : radius;
+            final float corner = horizontal ? cy : cx;
+
+            canvas.drawRoundRect(cx - rx, cy - ry,
+                    cx + rx, cy + ry,
+                    corner, corner, p);
+        }
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        mSupportHardware = canvas.isHardwareAccelerated();
+        if (mSupportHardware) {
+            drawHardware((DisplayListCanvas) canvas);
+        } else {
+            drawSoftware(canvas);
+        }
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        // Not supported.
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter colorFilter) {
+        // Not supported.
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+
+    private boolean isHorizontal() {
+        return getBounds().width() > getBounds().height();
+    }
+
+    private void drawHardware(DisplayListCanvas c) {
+        if (mDrawingHardwareGlow) {
+            c.drawRoundRect(mLeftProp, mTopProp, mRightProp, mBottomProp, mRxProp, mRyProp,
+                    mPaintProp);
+        }
+    }
+
+    public float getGlowAlpha() {
+        return mGlowAlpha;
+    }
+
+    public void setGlowAlpha(float x) {
+        mGlowAlpha = x;
+        invalidateSelf();
+    }
+
+    public float getGlowScale() {
+        return mGlowScale;
+    }
+
+    public void setGlowScale(float x) {
+        mGlowScale = x;
+        invalidateSelf();
+    }
+
+    private float getMaxGlowAlpha() {
+        return mLastDark ? GLOW_MAX_ALPHA_DARK : GLOW_MAX_ALPHA;
+    }
+
+    @Override
+    protected boolean onStateChange(int[] state) {
+        boolean pressed = false;
+        for (int i = 0; i < state.length; i++) {
+            if (state[i] == android.R.attr.state_pressed) {
+                pressed = true;
+                break;
+            }
+        }
+        if (pressed != mPressed) {
+            setPressed(pressed);
+            mPressed = pressed;
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public void jumpToCurrentState() {
+        cancelAnimations();
+    }
+
+    @Override
+    public boolean isStateful() {
+        return true;
+    }
+
+    @Override
+    public boolean hasFocusStateSpecified() {
+        return true;
+    }
+
+    public void setPressed(boolean pressed) {
+        if (mDark != mLastDark && pressed) {
+            mRipplePaint = null;
+            mLastDark = mDark;
+        }
+        if (mSupportHardware) {
+            setPressedHardware(pressed);
+        } else {
+            setPressedSoftware(pressed);
+        }
+    }
+
+    private void cancelAnimations() {
+        mTmpArray.addAll(mRunningAnimations);
+        int size = mTmpArray.size();
+        for (int i = 0; i < size; i++) {
+            Animator a = mTmpArray.get(i);
+            a.cancel();
+        }
+        mTmpArray.clear();
+        mRunningAnimations.clear();
+    }
+
+    private void setPressedSoftware(boolean pressed) {
+        if (pressed) {
+            enterSoftware();
+        } else {
+            exitSoftware();
+        }
+    }
+
+    private void enterSoftware() {
+        cancelAnimations();
+        mGlowAlpha = getMaxGlowAlpha();
+        ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(this, "glowScale",
+                0f, GLOW_MAX_SCALE_FACTOR);
+        scaleAnimator.setInterpolator(mInterpolator);
+        scaleAnimator.setDuration(ANIMATION_DURATION_SCALE);
+        scaleAnimator.addListener(mAnimatorListener);
+        scaleAnimator.start();
+        mRunningAnimations.add(scaleAnimator);
+    }
+
+    private void exitSoftware() {
+        ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "glowAlpha", mGlowAlpha, 0f);
+        alphaAnimator.setInterpolator(Interpolators.ALPHA_OUT);
+        alphaAnimator.setDuration(ANIMATION_DURATION_FADE);
+        alphaAnimator.addListener(mAnimatorListener);
+        alphaAnimator.start();
+        mRunningAnimations.add(alphaAnimator);
+    }
+
+    private void setPressedHardware(boolean pressed) {
+        if (pressed) {
+            enterHardware();
+        } else {
+            exitHardware();
+        }
+    }
+
+    /**
+     * Sets the left/top property for the round rect to {@code prop} depending on whether we are
+     * horizontal or vertical mode.
+     */
+    private void setExtendStart(CanvasProperty<Float> prop) {
+        if (isHorizontal()) {
+            mLeftProp = prop;
+        } else {
+            mTopProp = prop;
+        }
+    }
+
+    private CanvasProperty<Float> getExtendStart() {
+        return isHorizontal() ? mLeftProp : mTopProp;
+    }
+
+    /**
+     * Sets the right/bottom property for the round rect to {@code prop} depending on whether we are
+     * horizontal or vertical mode.
+     */
+    private void setExtendEnd(CanvasProperty<Float> prop) {
+        if (isHorizontal()) {
+            mRightProp = prop;
+        } else {
+            mBottomProp = prop;
+        }
+    }
+
+    private CanvasProperty<Float> getExtendEnd() {
+        return isHorizontal() ? mRightProp : mBottomProp;
+    }
+
+    private int getExtendSize() {
+        return isHorizontal() ? getBounds().width() : getBounds().height();
+    }
+
+    private int getRippleSize() {
+        int size = isHorizontal() ? getBounds().width() : getBounds().height();
+        return Math.min(size, mMaxWidth);
+    }
+
+    private void enterHardware() {
+        cancelAnimations();
+        mDrawingHardwareGlow = true;
+        setExtendStart(CanvasProperty.createFloat(getExtendSize() / 2));
+        final RenderNodeAnimator startAnim = new RenderNodeAnimator(getExtendStart(),
+                getExtendSize()/2 - GLOW_MAX_SCALE_FACTOR * getRippleSize()/2);
+        startAnim.setDuration(ANIMATION_DURATION_SCALE);
+        startAnim.setInterpolator(mInterpolator);
+        startAnim.addListener(mAnimatorListener);
+        startAnim.setTarget(mTargetView);
+
+        setExtendEnd(CanvasProperty.createFloat(getExtendSize() / 2));
+        final RenderNodeAnimator endAnim = new RenderNodeAnimator(getExtendEnd(),
+                getExtendSize()/2 + GLOW_MAX_SCALE_FACTOR * getRippleSize()/2);
+        endAnim.setDuration(ANIMATION_DURATION_SCALE);
+        endAnim.setInterpolator(mInterpolator);
+        endAnim.addListener(mAnimatorListener);
+        endAnim.setTarget(mTargetView);
+
+        if (isHorizontal()) {
+            mTopProp = CanvasProperty.createFloat(0f);
+            mBottomProp = CanvasProperty.createFloat(getBounds().height());
+            mRxProp = CanvasProperty.createFloat(getBounds().height()/2);
+            mRyProp = CanvasProperty.createFloat(getBounds().height()/2);
+        } else {
+            mLeftProp = CanvasProperty.createFloat(0f);
+            mRightProp = CanvasProperty.createFloat(getBounds().width());
+            mRxProp = CanvasProperty.createFloat(getBounds().width()/2);
+            mRyProp = CanvasProperty.createFloat(getBounds().width()/2);
+        }
+
+        mGlowScale = GLOW_MAX_SCALE_FACTOR;
+        mGlowAlpha = getMaxGlowAlpha();
+        mRipplePaint = getRipplePaint();
+        mRipplePaint.setAlpha((int) (mGlowAlpha * 255));
+        mPaintProp = CanvasProperty.createPaint(mRipplePaint);
+
+        startAnim.start();
+        endAnim.start();
+        mRunningAnimations.add(startAnim);
+        mRunningAnimations.add(endAnim);
+
+        invalidateSelf();
+    }
+
+    private void exitHardware() {
+        mPaintProp = CanvasProperty.createPaint(getRipplePaint());
+        final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPaintProp,
+                RenderNodeAnimator.PAINT_ALPHA, 0);
+        opacityAnim.setDuration(ANIMATION_DURATION_FADE);
+        opacityAnim.setInterpolator(Interpolators.ALPHA_OUT);
+        opacityAnim.addListener(mAnimatorListener);
+        opacityAnim.setTarget(mTargetView);
+
+        opacityAnim.start();
+        mRunningAnimations.add(opacityAnim);
+
+        invalidateSelf();
+    }
+
+    private final AnimatorListenerAdapter mAnimatorListener =
+            new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mRunningAnimations.remove(animation);
+            if (mRunningAnimations.isEmpty() && !mPressed) {
+                mDrawingHardwareGlow = false;
+                invalidateSelf();
+            }
+        }
+    };
+
+    /**
+     * Interpolator with a smooth log deceleration
+     */
+    private static final class LogInterpolator implements Interpolator {
+        @Override
+        public float getInterpolation(float input) {
+            return 1 - (float) Math.pow(400, -input * 1.4);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/KeyButtonView.java b/com/android/systemui/statusbar/policy/KeyButtonView.java
new file mode 100644
index 0000000..0501771
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/KeyButtonView.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2008 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.policy;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.hardware.input.InputManager;
+import android.media.AudioManager;
+import android.metrics.LogMaker;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.ImageView;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider.ButtonInterface;
+
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
+
+public class KeyButtonView extends ImageView implements ButtonInterface {
+
+    private final boolean mPlaySounds;
+    private int mContentDescriptionRes;
+    private long mDownTime;
+    private int mCode;
+    private int mTouchSlop;
+    private boolean mSupportsLongpress = true;
+    private AudioManager mAudioManager;
+    private boolean mGestureAborted;
+    private boolean mLongClicked;
+    private OnClickListener mOnClickListener;
+    private final KeyButtonRipple mRipple;
+    private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
+
+    private final Runnable mCheckLongPress = new Runnable() {
+        public void run() {
+            if (isPressed()) {
+                // Log.d("KeyButtonView", "longpressed: " + this);
+                if (isLongClickable()) {
+                    // Just an old-fashioned ImageView
+                    performLongClick();
+                    mLongClicked = true;
+                } else if (mSupportsLongpress) {
+                    sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);
+                    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
+                    mLongClicked = true;
+                }
+            }
+        }
+    };
+
+    public KeyButtonView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public KeyButtonView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.KeyButtonView,
+                defStyle, 0);
+
+        mCode = a.getInteger(R.styleable.KeyButtonView_keyCode, 0);
+
+        mSupportsLongpress = a.getBoolean(R.styleable.KeyButtonView_keyRepeat, true);
+        mPlaySounds = a.getBoolean(R.styleable.KeyButtonView_playSound, true);
+
+        TypedValue value = new TypedValue();
+        if (a.getValue(R.styleable.KeyButtonView_android_contentDescription, value)) {
+            mContentDescriptionRes = value.resourceId;
+        }
+
+        a.recycle();
+
+        setClickable(true);
+        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+
+        mRipple = new KeyButtonRipple(context, this);
+        setBackground(mRipple);
+    }
+
+    @Override
+    public boolean isClickable() {
+        return mCode != 0 || super.isClickable();
+    }
+
+    public void setCode(int code) {
+        mCode = code;
+    }
+
+    @Override
+    public void setOnClickListener(OnClickListener onClickListener) {
+        super.setOnClickListener(onClickListener);
+        mOnClickListener = onClickListener;
+    }
+
+    public void loadAsync(Icon icon) {
+        new AsyncTask<Icon, Void, Drawable>() {
+            @Override
+            protected Drawable doInBackground(Icon... params) {
+                return params[0].loadDrawable(mContext);
+            }
+
+            @Override
+            protected void onPostExecute(Drawable drawable) {
+                setImageDrawable(drawable);
+            }
+        }.execute(icon);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+
+        if (mContentDescriptionRes != 0) {
+            setContentDescription(mContext.getString(mContentDescriptionRes));
+        }
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        if (mCode != 0) {
+            info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, null));
+            if (mSupportsLongpress || isLongClickable()) {
+                info.addAction(
+                        new AccessibilityNodeInfo.AccessibilityAction(ACTION_LONG_CLICK, null));
+            }
+        }
+    }
+
+    @Override
+    protected void onWindowVisibilityChanged(int visibility) {
+        super.onWindowVisibilityChanged(visibility);
+        if (visibility != View.VISIBLE) {
+            jumpDrawablesToCurrentState();
+        }
+    }
+
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (action == ACTION_CLICK && mCode != 0) {
+            sendEvent(KeyEvent.ACTION_DOWN, 0, SystemClock.uptimeMillis());
+            sendEvent(KeyEvent.ACTION_UP, 0);
+            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
+            playSoundEffect(SoundEffectConstants.CLICK);
+            return true;
+        } else if (action == ACTION_LONG_CLICK && mCode != 0) {
+            sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);
+            sendEvent(KeyEvent.ACTION_UP, 0);
+            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
+            return true;
+        }
+        return super.performAccessibilityActionInternal(action, arguments);
+    }
+
+    public boolean onTouchEvent(MotionEvent ev) {
+        final int action = ev.getAction();
+        int x, y;
+        if (action == MotionEvent.ACTION_DOWN) {
+            mGestureAborted = false;
+        }
+        if (mGestureAborted) {
+            return false;
+        }
+
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                mDownTime = SystemClock.uptimeMillis();
+                mLongClicked = false;
+                setPressed(true);
+                if (mCode != 0) {
+                    sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);
+                } else {
+                    // Provide the same haptic feedback that the system offers for virtual keys.
+                    performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+                }
+                playSoundEffect(SoundEffectConstants.CLICK);
+                removeCallbacks(mCheckLongPress);
+                postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
+                break;
+            case MotionEvent.ACTION_MOVE:
+                x = (int)ev.getX();
+                y = (int)ev.getY();
+                setPressed(x >= -mTouchSlop
+                        && x < getWidth() + mTouchSlop
+                        && y >= -mTouchSlop
+                        && y < getHeight() + mTouchSlop);
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                setPressed(false);
+                if (mCode != 0) {
+                    sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
+                }
+                removeCallbacks(mCheckLongPress);
+                break;
+            case MotionEvent.ACTION_UP:
+                final boolean doIt = isPressed() && !mLongClicked;
+                setPressed(false);
+                // Always send a release ourselves because it doesn't seem to be sent elsewhere
+                // and it feels weird to sometimes get a release haptic and other times not.
+                if ((SystemClock.uptimeMillis() - mDownTime) > 150 && !mLongClicked) {
+                    performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE);
+                }
+                if (mCode != 0) {
+                    if (doIt) {
+                        sendEvent(KeyEvent.ACTION_UP, 0);
+                        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
+                    } else {
+                        sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
+                    }
+                } else {
+                    // no key code, just a regular ImageView
+                    if (doIt && mOnClickListener != null) {
+                        mOnClickListener.onClick(this);
+                        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
+                    }
+                }
+                removeCallbacks(mCheckLongPress);
+                break;
+        }
+
+        return true;
+    }
+
+    public void playSoundEffect(int soundConstant) {
+        if (!mPlaySounds) return;
+        mAudioManager.playSoundEffect(soundConstant, ActivityManager.getCurrentUser());
+    }
+
+    public void sendEvent(int action, int flags) {
+        sendEvent(action, flags, SystemClock.uptimeMillis());
+    }
+
+    void sendEvent(int action, int flags, long when) {
+        mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_NAV_BUTTON_EVENT)
+                .setType(MetricsEvent.TYPE_ACTION)
+                .setSubtype(mCode)
+                .addTaggedData(MetricsEvent.FIELD_NAV_ACTION, action)
+                .addTaggedData(MetricsEvent.FIELD_FLAGS, flags));
+        final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
+        final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount,
+                0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+                flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
+                InputDevice.SOURCE_KEYBOARD);
+        InputManager.getInstance().injectInputEvent(ev,
+                InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
+    }
+
+    @Override
+    public void abortCurrentGesture() {
+        setPressed(false);
+        mGestureAborted = true;
+    }
+
+    @Override
+    public void setDarkIntensity(float darkIntensity) {
+        Drawable drawable = getDrawable();
+        if (drawable != null) {
+            ((KeyButtonDrawable) getDrawable()).setDarkIntensity(darkIntensity);
+
+            // Since we reuse the same drawable for multiple views, we need to invalidate the view
+            // manually.
+            invalidate();
+        }
+        mRipple.setDarkIntensity(darkIntensity);
+    }
+
+    @Override
+    public void setVertical(boolean vertical) {
+        //no op
+    }
+}
+
+
diff --git a/com/android/systemui/statusbar/policy/KeyguardMonitor.java b/com/android/systemui/statusbar/policy/KeyguardMonitor.java
new file mode 100644
index 0000000..ccfbb26
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/KeyguardMonitor.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 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.policy;
+
+import com.android.systemui.statusbar.policy.KeyguardMonitor.Callback;
+
+public interface KeyguardMonitor extends CallbackController<Callback> {
+
+    boolean isSecure();
+    boolean canSkipBouncer();
+    boolean isShowing();
+    boolean isOccluded();
+    boolean isKeyguardFadingAway();
+    boolean isKeyguardGoingAway();
+    long getKeyguardFadingAwayDuration();
+    long getKeyguardFadingAwayDelay();
+
+    interface Callback {
+        void onKeyguardShowingChanged();
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/KeyguardMonitorImpl.java b/com/android/systemui/statusbar/policy/KeyguardMonitorImpl.java
new file mode 100644
index 0000000..5ead02f
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/KeyguardMonitorImpl.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.app.ActivityManager;
+import android.content.Context;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.systemui.settings.CurrentUserTracker;
+
+import java.util.ArrayList;
+
+public class KeyguardMonitorImpl extends KeyguardUpdateMonitorCallback
+        implements KeyguardMonitor {
+
+    private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
+
+    private final Context mContext;
+    private final CurrentUserTracker mUserTracker;
+    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+
+    private int mCurrentUser;
+    private boolean mShowing;
+    private boolean mSecure;
+    private boolean mOccluded;
+    private boolean mCanSkipBouncer;
+
+    private boolean mListening;
+    private boolean mKeyguardFadingAway;
+    private long mKeyguardFadingAwayDelay;
+    private long mKeyguardFadingAwayDuration;
+    private boolean mKeyguardGoingAway;
+
+    public KeyguardMonitorImpl(Context context) {
+        mContext = context;
+        mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
+        mUserTracker = new CurrentUserTracker(mContext) {
+            @Override
+            public void onUserSwitched(int newUserId) {
+                mCurrentUser = newUserId;
+                updateCanSkipBouncerState();
+            }
+        };
+    }
+
+    @Override
+    public void addCallback(Callback callback) {
+        mCallbacks.add(callback);
+        if (mCallbacks.size() != 0 && !mListening) {
+            mListening = true;
+            mCurrentUser = ActivityManager.getCurrentUser();
+            updateCanSkipBouncerState();
+            mKeyguardUpdateMonitor.registerCallback(this);
+            mUserTracker.startTracking();
+        }
+    }
+
+    @Override
+    public void removeCallback(Callback callback) {
+        if (mCallbacks.remove(callback) && mCallbacks.size() == 0 && mListening) {
+            mListening = false;
+            mKeyguardUpdateMonitor.removeCallback(this);
+            mUserTracker.stopTracking();
+        }
+    }
+
+    @Override
+    public boolean isShowing() {
+        return mShowing;
+    }
+
+    @Override
+    public boolean isSecure() {
+        return mSecure;
+    }
+
+    @Override
+    public boolean isOccluded() {
+        return mOccluded;
+    }
+
+    @Override
+    public boolean canSkipBouncer() {
+        return mCanSkipBouncer;
+    }
+
+    public void notifyKeyguardState(boolean showing, boolean secure, boolean occluded) {
+        if (mShowing == showing && mSecure == secure && mOccluded == occluded) return;
+        mShowing = showing;
+        mSecure = secure;
+        mOccluded = occluded;
+        notifyKeyguardChanged();
+    }
+
+    @Override
+    public void onTrustChanged(int userId) {
+        updateCanSkipBouncerState();
+        notifyKeyguardChanged();
+    }
+
+    public boolean isDeviceInteractive() {
+        return mKeyguardUpdateMonitor.isDeviceInteractive();
+    }
+
+    private void updateCanSkipBouncerState() {
+        mCanSkipBouncer = mKeyguardUpdateMonitor.getUserCanSkipBouncer(mCurrentUser);
+    }
+
+    private void notifyKeyguardChanged() {
+        // Copy the list to allow removal during callback.
+        new ArrayList<Callback>(mCallbacks).forEach(Callback::onKeyguardShowingChanged);
+    }
+
+    public void notifyKeyguardFadingAway(long delay, long fadeoutDuration) {
+        mKeyguardFadingAway = true;
+        mKeyguardFadingAwayDelay = delay;
+        mKeyguardFadingAwayDuration = fadeoutDuration;
+    }
+
+    public void notifyKeyguardDoneFading() {
+        mKeyguardFadingAway = false;
+        mKeyguardGoingAway = false;
+    }
+
+    @Override
+    public boolean isKeyguardFadingAway() {
+        return mKeyguardFadingAway;
+    }
+
+    @Override
+    public boolean isKeyguardGoingAway() {
+        return mKeyguardGoingAway;
+    }
+
+    @Override
+    public long getKeyguardFadingAwayDelay() {
+        return mKeyguardFadingAwayDelay;
+    }
+
+    @Override
+    public long getKeyguardFadingAwayDuration() {
+        return mKeyguardFadingAwayDuration;
+    }
+
+    public void notifyKeyguardGoingAway(boolean keyguardGoingAway) {
+        mKeyguardGoingAway = keyguardGoingAway;
+    }
+}
\ No newline at end of file
diff --git a/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java b/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java
new file mode 100644
index 0000000..4b283fe
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.widget.FrameLayout;
+
+import com.android.settingslib.animation.AppearAnimationUtils;
+import com.android.systemui.Dependency;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.qs.tiles.UserDetailItemView;
+import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
+import com.android.systemui.statusbar.phone.NotificationPanelView;
+
+/**
+ * Manages the user switcher on the Keyguard.
+ */
+public class KeyguardUserSwitcher {
+
+    private static final String TAG = "KeyguardUserSwitcher";
+    private static final boolean ALWAYS_ON = false;
+
+    private final Container mUserSwitcherContainer;
+    private final KeyguardStatusBarView mStatusBarView;
+    private final Adapter mAdapter;
+    private final AppearAnimationUtils mAppearAnimationUtils;
+    private final KeyguardUserSwitcherScrim mBackground;
+
+    private ViewGroup mUserSwitcher;
+    private ObjectAnimator mBgAnimator;
+    private UserSwitcherController mUserSwitcherController;
+    private boolean mAnimating;
+
+    public KeyguardUserSwitcher(Context context, ViewStub userSwitcher,
+            KeyguardStatusBarView statusBarView, NotificationPanelView panelView) {
+        boolean keyguardUserSwitcherEnabled =
+                context.getResources().getBoolean(R.bool.config_keyguardUserSwitcher) || ALWAYS_ON;
+        UserSwitcherController userSwitcherController = Dependency.get(UserSwitcherController.class);
+        if (userSwitcherController != null && keyguardUserSwitcherEnabled) {
+            mUserSwitcherContainer = (Container) userSwitcher.inflate();
+            mBackground = new KeyguardUserSwitcherScrim(context);
+            reinflateViews();
+            mStatusBarView = statusBarView;
+            mStatusBarView.setKeyguardUserSwitcher(this);
+            panelView.setKeyguardUserSwitcher(this);
+            mAdapter = new Adapter(context, userSwitcherController, this);
+            mAdapter.registerDataSetObserver(mDataSetObserver);
+            mUserSwitcherController = userSwitcherController;
+            mAppearAnimationUtils = new AppearAnimationUtils(context, 400, -0.5f, 0.5f,
+                    Interpolators.FAST_OUT_SLOW_IN);
+            mUserSwitcherContainer.setKeyguardUserSwitcher(this);
+        } else {
+            mUserSwitcherContainer = null;
+            mStatusBarView = null;
+            mAdapter = null;
+            mAppearAnimationUtils = null;
+            mBackground = null;
+        }
+    }
+
+    private void reinflateViews() {
+        if (mUserSwitcher != null) {
+            mUserSwitcher.setBackground(null);
+            mUserSwitcher.removeOnLayoutChangeListener(mBackground);
+        }
+        mUserSwitcherContainer.removeAllViews();
+
+        LayoutInflater.from(mUserSwitcherContainer.getContext())
+                .inflate(R.layout.keyguard_user_switcher_inner, mUserSwitcherContainer);
+
+        mUserSwitcher = (ViewGroup) mUserSwitcherContainer.findViewById(
+                R.id.keyguard_user_switcher_inner);
+        mUserSwitcher.addOnLayoutChangeListener(mBackground);
+        mUserSwitcher.setBackground(mBackground);
+    }
+
+    public void setKeyguard(boolean keyguard, boolean animate) {
+        if (mUserSwitcher != null) {
+            if (keyguard && shouldExpandByDefault()) {
+                show(animate);
+            } else {
+                hide(animate);
+            }
+        }
+    }
+
+    /**
+     * @return true if the user switcher should be expanded by default on the lock screen.
+     * @see android.os.UserManager#isUserSwitcherEnabled()
+     */
+    private boolean shouldExpandByDefault() {
+        return (mUserSwitcherController != null) && mUserSwitcherController.isSimpleUserSwitcher();
+    }
+
+    public void show(boolean animate) {
+        if (mUserSwitcher != null && mUserSwitcherContainer.getVisibility() != View.VISIBLE) {
+            cancelAnimations();
+            mAdapter.refresh();
+            mUserSwitcherContainer.setVisibility(View.VISIBLE);
+            mStatusBarView.setKeyguardUserSwitcherShowing(true, animate);
+            if (animate) {
+                startAppearAnimation();
+            }
+        }
+    }
+
+    private boolean hide(boolean animate) {
+        if (mUserSwitcher != null && mUserSwitcherContainer.getVisibility() == View.VISIBLE) {
+            cancelAnimations();
+            if (animate) {
+                startDisappearAnimation();
+            } else {
+                mUserSwitcherContainer.setVisibility(View.GONE);
+            }
+            mStatusBarView.setKeyguardUserSwitcherShowing(false, animate);
+            return true;
+        }
+        return false;
+    }
+
+    private void cancelAnimations() {
+        int count = mUserSwitcher.getChildCount();
+        for (int i = 0; i < count; i++) {
+            mUserSwitcher.getChildAt(i).animate().cancel();
+        }
+        if (mBgAnimator != null) {
+            mBgAnimator.cancel();
+        }
+        mUserSwitcher.animate().cancel();
+        mAnimating = false;
+    }
+
+    private void startAppearAnimation() {
+        int count = mUserSwitcher.getChildCount();
+        View[] objects = new View[count];
+        for (int i = 0; i < count; i++) {
+            objects[i] = mUserSwitcher.getChildAt(i);
+        }
+        mUserSwitcher.setClipChildren(false);
+        mUserSwitcher.setClipToPadding(false);
+        mAppearAnimationUtils.startAnimation(objects, new Runnable() {
+            @Override
+            public void run() {
+                mUserSwitcher.setClipChildren(true);
+                mUserSwitcher.setClipToPadding(true);
+            }
+        });
+        mAnimating = true;
+        mBgAnimator = ObjectAnimator.ofInt(mBackground, "alpha", 0, 255);
+        mBgAnimator.setDuration(400);
+        mBgAnimator.setInterpolator(Interpolators.ALPHA_IN);
+        mBgAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mBgAnimator = null;
+                mAnimating = false;
+            }
+        });
+        mBgAnimator.start();
+    }
+
+    private void startDisappearAnimation() {
+        mAnimating = true;
+        mUserSwitcher.animate()
+                .alpha(0f)
+                .setDuration(300)
+                .setInterpolator(Interpolators.ALPHA_OUT)
+                .withEndAction(new Runnable() {
+                    @Override
+                    public void run() {
+                        mUserSwitcherContainer.setVisibility(View.GONE);
+                        mUserSwitcher.setAlpha(1f);
+                        mAnimating = false;
+                    }
+                });
+    }
+
+    private void refresh() {
+        final int childCount = mUserSwitcher.getChildCount();
+        final int adapterCount = mAdapter.getCount();
+        final int N = Math.max(childCount, adapterCount);
+        for (int i = 0; i < N; i++) {
+            if (i < adapterCount) {
+                View oldView = null;
+                if (i < childCount) {
+                    oldView = mUserSwitcher.getChildAt(i);
+                }
+                View newView = mAdapter.getView(i, oldView, mUserSwitcher);
+                if (oldView == null) {
+                    // We ran out of existing views. Add it at the end.
+                    mUserSwitcher.addView(newView);
+                } else if (oldView != newView) {
+                    // We couldn't rebind the view. Replace it.
+                    mUserSwitcher.removeViewAt(i);
+                    mUserSwitcher.addView(newView, i);
+                }
+            } else {
+                int lastIndex = mUserSwitcher.getChildCount() - 1;
+                mUserSwitcher.removeViewAt(lastIndex);
+            }
+        }
+    }
+
+    public boolean hideIfNotSimple(boolean animate) {
+        if (mUserSwitcherContainer != null && !mUserSwitcherController.isSimpleUserSwitcher()) {
+            return hide(animate);
+        }
+        return false;
+    }
+
+    boolean isAnimating() {
+        return mAnimating;
+    }
+
+    public final DataSetObserver mDataSetObserver = new DataSetObserver() {
+        @Override
+        public void onChanged() {
+            refresh();
+        }
+    };
+
+    public void onDensityOrFontScaleChanged() {
+        if (mUserSwitcherContainer != null) {
+            reinflateViews();
+            refresh();
+        }
+    }
+
+    public static class Adapter extends UserSwitcherController.BaseUserAdapter implements
+            View.OnClickListener {
+
+        private Context mContext;
+        private KeyguardUserSwitcher mKeyguardUserSwitcher;
+
+        public Adapter(Context context, UserSwitcherController controller,
+                KeyguardUserSwitcher kgu) {
+            super(controller);
+            mContext = context;
+            mKeyguardUserSwitcher = kgu;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            UserSwitcherController.UserRecord item = getItem(position);
+            if (!(convertView instanceof UserDetailItemView)
+                    || !(convertView.getTag() instanceof UserSwitcherController.UserRecord)) {
+                convertView = LayoutInflater.from(mContext).inflate(
+                        R.layout.keyguard_user_switcher_item, parent, false);
+                convertView.setOnClickListener(this);
+            }
+            UserDetailItemView v = (UserDetailItemView) convertView;
+
+            String name = getName(mContext, item);
+            if (item.picture == null) {
+                v.bind(name, getDrawable(mContext, item).mutate(), item.resolveId());
+            } else {
+                v.bind(name, item.picture, item.info.id);
+            }
+            // Disable the icon if switching is disabled
+            v.setAvatarEnabled(item.isSwitchToEnabled);
+            convertView.setActivated(item.isCurrent);
+            convertView.setTag(item);
+            return convertView;
+        }
+
+        @Override
+        public void onClick(View v) {
+            UserSwitcherController.UserRecord user = (UserSwitcherController.UserRecord) v.getTag();
+            if (user.isCurrent && !user.isGuest) {
+                // Close the switcher if tapping the current user. Guest is excluded because
+                // tapping the guest user while it's current clears the session.
+                mKeyguardUserSwitcher.hideIfNotSimple(true /* animate */);
+            } else if (user.isSwitchToEnabled) {
+                switchTo(user);
+            }
+        }
+    }
+
+    public static class Container extends FrameLayout {
+
+        private KeyguardUserSwitcher mKeyguardUserSwitcher;
+
+        public Container(Context context, AttributeSet attrs) {
+            super(context, attrs);
+            setClipChildren(false);
+        }
+
+        public void setKeyguardUserSwitcher(KeyguardUserSwitcher keyguardUserSwitcher) {
+            mKeyguardUserSwitcher = keyguardUserSwitcher;
+        }
+
+        @Override
+        public boolean onTouchEvent(MotionEvent ev) {
+            // Hide switcher if it didn't handle the touch event (and let the event go through).
+            if (mKeyguardUserSwitcher != null && !mKeyguardUserSwitcher.isAnimating()) {
+                mKeyguardUserSwitcher.hideIfNotSimple(true /* animate */);
+            }
+            return false;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/KeyguardUserSwitcherScrim.java b/com/android/systemui/statusbar/policy/KeyguardUserSwitcherScrim.java
new file mode 100644
index 0000000..49f5bcd
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/KeyguardUserSwitcherScrim.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.RadialGradient;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.graphics.drawable.Drawable;
+import android.util.LayoutDirection;
+import android.view.View;
+
+import com.android.systemui.R;
+
+/**
+ * Gradient background for the user switcher on Keyguard.
+ */
+public class KeyguardUserSwitcherScrim extends Drawable
+        implements View.OnLayoutChangeListener {
+
+    private static final float OUTER_EXTENT = 2.5f;
+    private static final float INNER_EXTENT = 0.75f;
+
+    private int mDarkColor;
+    private int mTop;
+    private int mAlpha = 255;
+    private Paint mRadialGradientPaint = new Paint();
+    private int mLayoutWidth;
+
+    public KeyguardUserSwitcherScrim(Context context) {
+        mDarkColor = context.getColor(
+                R.color.keyguard_user_switcher_background_gradient_color);
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        boolean isLtr = getLayoutDirection() == LayoutDirection.LTR;
+        Rect bounds = getBounds();
+        float width = bounds.width() * OUTER_EXTENT;
+        float height = (mTop + bounds.height()) * OUTER_EXTENT;
+        canvas.translate(0, -mTop);
+        canvas.scale(1, height / width);
+        canvas.drawRect(isLtr ? bounds.right - width : 0, 0,
+                isLtr ? bounds.right : bounds.left + width, width, mRadialGradientPaint);
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        mAlpha = alpha;
+        updatePaint();
+        invalidateSelf();
+    }
+
+    @Override
+    public int getAlpha() {
+        return mAlpha;
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter colorFilter) {
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+
+    @Override
+    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+            int oldTop, int oldRight, int oldBottom) {
+        if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
+            mLayoutWidth = right - left;
+            mTop = top;
+            updatePaint();
+        }
+    }
+
+    private void updatePaint() {
+        if (mLayoutWidth == 0) {
+            return;
+        }
+        float radius = mLayoutWidth * OUTER_EXTENT;
+        boolean isLtr = getLayoutDirection() == LayoutDirection.LTR;
+        mRadialGradientPaint.setShader(
+                new RadialGradient(isLtr ? mLayoutWidth : 0, 0, radius,
+                        new int[] { Color.argb(
+                                        (int) (Color.alpha(mDarkColor) * mAlpha / 255f), 0, 0, 0),
+                                Color.TRANSPARENT },
+                        new float[] { Math.max(0f, mLayoutWidth * INNER_EXTENT / radius), 1f },
+                        Shader.TileMode.CLAMP));
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/Listenable.java b/com/android/systemui/statusbar/policy/Listenable.java
new file mode 100644
index 0000000..4fa59fd
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/Listenable.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+/** Common interface for components with an active listening state. **/
+public interface Listenable {
+    void setListening(boolean listening);
+}
diff --git a/com/android/systemui/statusbar/policy/LocationController.java b/com/android/systemui/statusbar/policy/LocationController.java
new file mode 100644
index 0000000..8e8a285
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/LocationController.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import com.android.systemui.statusbar.policy.LocationController.LocationChangeCallback;
+
+public interface LocationController extends CallbackController<LocationChangeCallback> {
+    boolean isLocationActive();
+    boolean isLocationEnabled();
+    boolean setLocationEnabled(boolean enabled);
+
+    /**
+     * A callback for change in location settings (the user has enabled/disabled location).
+     */
+    public interface LocationChangeCallback {
+        /**
+         * Called whenever location's state changes.
+         * @param active
+         */
+        default void onLocationActiveChanged(boolean active) {}
+
+        /**
+         * Called whenever location settings change.
+         *
+         * @param locationEnabled A value of true indicates that at least one type of location
+         *                        is enabled in settings.
+         */
+        default void onLocationSettingsChanged(boolean locationEnabled) {}
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/LocationControllerImpl.java b/com/android/systemui/statusbar/policy/LocationControllerImpl.java
new file mode 100644
index 0000000..874f0d9
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/LocationControllerImpl.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2008 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.policy;
+
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.app.StatusBarManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.location.LocationManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.systemui.R;
+import com.android.systemui.util.Utils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A controller to manage changes of location related states and update the views accordingly.
+ */
+public class LocationControllerImpl extends BroadcastReceiver implements LocationController {
+
+    private static final int[] mHighPowerRequestAppOpArray
+        = new int[] {AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION};
+
+    private Context mContext;
+
+    private AppOpsManager mAppOpsManager;
+    private StatusBarManager mStatusBarManager;
+
+    private boolean mAreActiveLocationRequests;
+
+    private ArrayList<LocationChangeCallback> mSettingsChangeCallbacks =
+            new ArrayList<LocationChangeCallback>();
+    private final H mHandler = new H();
+
+    public LocationControllerImpl(Context context, Looper bgLooper) {
+        mContext = context;
+
+        // Register to listen for changes in location settings.
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(LocationManager.HIGH_POWER_REQUEST_CHANGE_ACTION);
+        filter.addAction(LocationManager.MODE_CHANGED_ACTION);
+        context.registerReceiverAsUser(this, UserHandle.ALL, filter, null, new Handler(bgLooper));
+
+        mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+        mStatusBarManager
+                = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE);
+
+        // Examine the current location state and initialize the status view.
+        updateActiveLocationRequests();
+    }
+
+    /**
+     * Add a callback to listen for changes in location settings.
+     */
+    public void addCallback(LocationChangeCallback cb) {
+        mSettingsChangeCallbacks.add(cb);
+        mHandler.sendEmptyMessage(H.MSG_LOCATION_SETTINGS_CHANGED);
+    }
+
+    public void removeCallback(LocationChangeCallback cb) {
+        mSettingsChangeCallbacks.remove(cb);
+    }
+
+    /**
+     * Enable or disable location in settings.
+     *
+     * <p>This will attempt to enable/disable every type of location setting
+     * (e.g. high and balanced power).
+     *
+     * <p>If enabling, a user consent dialog will pop up prompting the user to accept.
+     * If the user doesn't accept, network location won't be enabled.
+     *
+     * @return true if attempt to change setting was successful.
+     */
+    public boolean setLocationEnabled(boolean enabled) {
+        int currentUserId = ActivityManager.getCurrentUser();
+        if (isUserLocationRestricted(currentUserId)) {
+            return false;
+        }
+        final ContentResolver cr = mContext.getContentResolver();
+        // When enabling location, a user consent dialog will pop up, and the
+        // setting won't be fully enabled until the user accepts the agreement.
+        int mode = enabled
+                ? Settings.Secure.LOCATION_MODE_PREVIOUS : Settings.Secure.LOCATION_MODE_OFF;
+        // QuickSettings always runs as the owner, so specifically set the settings
+        // for the current foreground user.
+        return Settings.Secure
+                .putIntForUser(cr, Settings.Secure.LOCATION_MODE, mode, currentUserId);
+    }
+
+    /**
+     * Returns true if location isn't disabled in settings.
+     */
+    public boolean isLocationEnabled() {
+        ContentResolver resolver = mContext.getContentResolver();
+        // QuickSettings always runs as the owner, so specifically retrieve the settings
+        // for the current foreground user.
+        int mode = Settings.Secure.getIntForUser(resolver, Settings.Secure.LOCATION_MODE,
+                Settings.Secure.LOCATION_MODE_OFF, ActivityManager.getCurrentUser());
+        return mode != Settings.Secure.LOCATION_MODE_OFF;
+    }
+
+    @Override
+    public boolean isLocationActive() {
+        return mAreActiveLocationRequests;
+    }
+
+    /**
+     * Returns true if the current user is restricted from using location.
+     */
+    private boolean isUserLocationRestricted(int userId) {
+        final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+        return um.hasUserRestriction(UserManager.DISALLOW_SHARE_LOCATION,
+                UserHandle.of(userId));
+    }
+
+    /**
+     * Returns true if there currently exist active high power location requests.
+     */
+    @VisibleForTesting
+    protected boolean areActiveHighPowerLocationRequests() {
+        List<AppOpsManager.PackageOps> packages
+            = mAppOpsManager.getPackagesForOps(mHighPowerRequestAppOpArray);
+        // AppOpsManager can return null when there is no requested data.
+        if (packages != null) {
+            final int numPackages = packages.size();
+            for (int packageInd = 0; packageInd < numPackages; packageInd++) {
+                AppOpsManager.PackageOps packageOp = packages.get(packageInd);
+                List<AppOpsManager.OpEntry> opEntries = packageOp.getOps();
+                if (opEntries != null) {
+                    final int numOps = opEntries.size();
+                    for (int opInd = 0; opInd < numOps; opInd++) {
+                        AppOpsManager.OpEntry opEntry = opEntries.get(opInd);
+                        // AppOpsManager should only return OP_MONITOR_HIGH_POWER_LOCATION because
+                        // of the mHighPowerRequestAppOpArray filter, but checking defensively.
+                        if (opEntry.getOp() == AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION) {
+                            if (opEntry.isRunning()) {
+                                return true;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    // Reads the active location requests and updates the status view if necessary.
+    private void updateActiveLocationRequests() {
+        boolean hadActiveLocationRequests = mAreActiveLocationRequests;
+        mAreActiveLocationRequests = areActiveHighPowerLocationRequests();
+        if (mAreActiveLocationRequests != hadActiveLocationRequests) {
+            mHandler.sendEmptyMessage(H.MSG_LOCATION_ACTIVE_CHANGED);
+        }
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        final String action = intent.getAction();
+        if (LocationManager.HIGH_POWER_REQUEST_CHANGE_ACTION.equals(action)) {
+            updateActiveLocationRequests();
+        } else if (LocationManager.MODE_CHANGED_ACTION.equals(action)) {
+            mHandler.sendEmptyMessage(H.MSG_LOCATION_SETTINGS_CHANGED);
+        }
+    }
+
+    private final class H extends Handler {
+        private static final int MSG_LOCATION_SETTINGS_CHANGED = 1;
+        private static final int MSG_LOCATION_ACTIVE_CHANGED = 2;
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_LOCATION_SETTINGS_CHANGED:
+                    locationSettingsChanged();
+                    break;
+                case MSG_LOCATION_ACTIVE_CHANGED:
+                    locationActiveChanged();
+                    break;
+            }
+        }
+
+        private void locationActiveChanged() {
+            Utils.safeForeach(mSettingsChangeCallbacks,
+                    cb -> cb.onLocationActiveChanged(mAreActiveLocationRequests));
+        }
+
+        private void locationSettingsChanged() {
+            boolean isEnabled = isLocationEnabled();
+            Utils.safeForeach(mSettingsChangeCallbacks,
+                    cb -> cb.onLocationSettingsChanged(isEnabled));
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/MobileSignalController.java b/com/android/systemui/statusbar/policy/MobileSignalController.java
new file mode 100644
index 0000000..652f8bb
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/MobileSignalController.java
@@ -0,0 +1,647 @@
+/*
+ * 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.policy;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.net.NetworkCapabilities;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings.Global;
+import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
+import android.telephony.SignalStrength;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.telephony.cdma.EriInfo;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.SignalDrawable;
+import com.android.systemui.statusbar.policy.NetworkController.IconState;
+import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+import com.android.systemui.statusbar.policy.NetworkControllerImpl.Config;
+import com.android.systemui.statusbar.policy.NetworkControllerImpl.SubscriptionDefaults;
+
+import java.io.PrintWriter;
+import java.util.BitSet;
+import java.util.Objects;
+
+
+public class MobileSignalController extends SignalController<
+        MobileSignalController.MobileState, MobileSignalController.MobileIconGroup> {
+    private final TelephonyManager mPhone;
+    private final SubscriptionDefaults mDefaults;
+    private final String mNetworkNameDefault;
+    private final String mNetworkNameSeparator;
+    private final ContentObserver mObserver;
+    @VisibleForTesting
+    final PhoneStateListener mPhoneStateListener;
+    // Save entire info for logging, we only use the id.
+    final SubscriptionInfo mSubscriptionInfo;
+
+    // @VisibleForDemoMode
+    final SparseArray<MobileIconGroup> mNetworkToIconLookup;
+
+    // Since some pieces of the phone state are interdependent we store it locally,
+    // this could potentially become part of MobileState for simplification/complication
+    // of code.
+    private int mDataNetType = TelephonyManager.NETWORK_TYPE_UNKNOWN;
+    private int mDataState = TelephonyManager.DATA_DISCONNECTED;
+    private ServiceState mServiceState;
+    private SignalStrength mSignalStrength;
+    private MobileIconGroup mDefaultIcons;
+    private Config mConfig;
+
+    // TODO: Reduce number of vars passed in, if we have the NetworkController, probably don't
+    // need listener lists anymore.
+    public MobileSignalController(Context context, Config config, boolean hasMobileData,
+            TelephonyManager phone, CallbackHandler callbackHandler,
+            NetworkControllerImpl networkController, SubscriptionInfo info,
+            SubscriptionDefaults defaults, Looper receiverLooper) {
+        super("MobileSignalController(" + info.getSubscriptionId() + ")", context,
+                NetworkCapabilities.TRANSPORT_CELLULAR, callbackHandler,
+                networkController);
+        mNetworkToIconLookup = new SparseArray<>();
+        mConfig = config;
+        mPhone = phone;
+        mDefaults = defaults;
+        mSubscriptionInfo = info;
+        mPhoneStateListener = new MobilePhoneStateListener(info.getSubscriptionId(),
+                receiverLooper);
+        mNetworkNameSeparator = getStringIfExists(R.string.status_bar_network_name_separator);
+        mNetworkNameDefault = getStringIfExists(
+                com.android.internal.R.string.lockscreen_carrier_default);
+
+        mapIconSets();
+
+        String networkName = info.getCarrierName() != null ? info.getCarrierName().toString()
+                : mNetworkNameDefault;
+        mLastState.networkName = mCurrentState.networkName = networkName;
+        mLastState.networkNameData = mCurrentState.networkNameData = networkName;
+        mLastState.enabled = mCurrentState.enabled = hasMobileData;
+        mLastState.iconGroup = mCurrentState.iconGroup = mDefaultIcons;
+        // Get initial data sim state.
+        updateDataSim();
+        mObserver = new ContentObserver(new Handler(receiverLooper)) {
+            @Override
+            public void onChange(boolean selfChange) {
+                updateTelephony();
+            }
+        };
+    }
+
+    public void setConfiguration(Config config) {
+        mConfig = config;
+        mapIconSets();
+        updateTelephony();
+    }
+
+    public int getDataContentDescription() {
+        return getIcons().mDataContentDescription;
+    }
+
+    public void setAirplaneMode(boolean airplaneMode) {
+        mCurrentState.airplaneMode = airplaneMode;
+        notifyListenersIfNecessary();
+    }
+
+    public void setUserSetupComplete(boolean userSetup) {
+        mCurrentState.userSetup = userSetup;
+        notifyListenersIfNecessary();
+    }
+
+    @Override
+    public void updateConnectivity(BitSet connectedTransports, BitSet validatedTransports) {
+        boolean isValidated = validatedTransports.get(mTransportType);
+        mCurrentState.isDefault = connectedTransports.get(mTransportType);
+        // Only show this as not having connectivity if we are default.
+        mCurrentState.inetCondition = (isValidated || !mCurrentState.isDefault) ? 1 : 0;
+        notifyListenersIfNecessary();
+    }
+
+    public void setCarrierNetworkChangeMode(boolean carrierNetworkChangeMode) {
+        mCurrentState.carrierNetworkChangeMode = carrierNetworkChangeMode;
+        updateTelephony();
+    }
+
+    /**
+     * Start listening for phone state changes.
+     */
+    public void registerListener() {
+        mPhone.listen(mPhoneStateListener,
+                PhoneStateListener.LISTEN_SERVICE_STATE
+                        | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS
+                        | PhoneStateListener.LISTEN_CALL_STATE
+                        | PhoneStateListener.LISTEN_DATA_CONNECTION_STATE
+                        | PhoneStateListener.LISTEN_DATA_ACTIVITY
+                        | PhoneStateListener.LISTEN_CARRIER_NETWORK_CHANGE);
+        mContext.getContentResolver().registerContentObserver(Global.getUriFor(Global.MOBILE_DATA),
+                true, mObserver);
+        mContext.getContentResolver().registerContentObserver(Global.getUriFor(
+                Global.MOBILE_DATA + mSubscriptionInfo.getSubscriptionId()),
+                true, mObserver);
+    }
+
+    /**
+     * Stop listening for phone state changes.
+     */
+    public void unregisterListener() {
+        mPhone.listen(mPhoneStateListener, 0);
+        mContext.getContentResolver().unregisterContentObserver(mObserver);
+    }
+
+    /**
+     * Produce a mapping of data network types to icon groups for simple and quick use in
+     * updateTelephony.
+     */
+    private void mapIconSets() {
+        mNetworkToIconLookup.clear();
+
+        mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EVDO_0, TelephonyIcons.THREE_G);
+        mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EVDO_A, TelephonyIcons.THREE_G);
+        mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EVDO_B, TelephonyIcons.THREE_G);
+        mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EHRPD, TelephonyIcons.THREE_G);
+        mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UMTS, TelephonyIcons.THREE_G);
+        mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_TD_SCDMA, TelephonyIcons.THREE_G);
+
+        if (!mConfig.showAtLeast3G) {
+            mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UNKNOWN,
+                    TelephonyIcons.UNKNOWN);
+            mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EDGE, TelephonyIcons.E);
+            mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_CDMA, TelephonyIcons.ONE_X);
+            mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_1xRTT, TelephonyIcons.ONE_X);
+
+            mDefaultIcons = TelephonyIcons.G;
+        } else {
+            mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UNKNOWN,
+                    TelephonyIcons.THREE_G);
+            mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EDGE,
+                    TelephonyIcons.THREE_G);
+            mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_CDMA,
+                    TelephonyIcons.THREE_G);
+            mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_1xRTT,
+                    TelephonyIcons.THREE_G);
+            mDefaultIcons = TelephonyIcons.THREE_G;
+        }
+
+        MobileIconGroup hGroup = TelephonyIcons.THREE_G;
+        if (mConfig.hspaDataDistinguishable) {
+            hGroup = TelephonyIcons.H;
+        }
+        mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSDPA, hGroup);
+        mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSUPA, hGroup);
+        mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSPA, hGroup);
+        mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSPAP, hGroup);
+
+        if (mConfig.show4gForLte) {
+            mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE, TelephonyIcons.FOUR_G);
+            if (mConfig.hideLtePlus) {
+                mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE_CA,
+                        TelephonyIcons.FOUR_G);
+            } else {
+                mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE_CA,
+                        TelephonyIcons.FOUR_G_PLUS);
+            }
+        } else {
+            mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE, TelephonyIcons.LTE);
+            if (mConfig.hideLtePlus) {
+                mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE_CA,
+                        TelephonyIcons.LTE);
+            } else {
+                mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE_CA,
+                        TelephonyIcons.LTE_PLUS);
+            }
+        }
+        mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_IWLAN, TelephonyIcons.WFC);
+    }
+
+    private int getNumLevels() {
+        if (mConfig.inflateSignalStrengths) {
+            return SignalStrength.NUM_SIGNAL_STRENGTH_BINS + 1;
+        }
+        return SignalStrength.NUM_SIGNAL_STRENGTH_BINS;
+    }
+
+    @Override
+    public int getCurrentIconId() {
+        if (mCurrentState.iconGroup == TelephonyIcons.CARRIER_NETWORK_CHANGE) {
+            return SignalDrawable.getCarrierChangeState(getNumLevels());
+        } else if (mCurrentState.connected) {
+            int level = mCurrentState.level;
+            if (mConfig.inflateSignalStrengths) {
+                level++;
+            }
+            return SignalDrawable.getState(level, getNumLevels(),
+                    mCurrentState.inetCondition == 0);
+        } else if (mCurrentState.enabled) {
+            return SignalDrawable.getEmptyState(getNumLevels());
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public int getQsCurrentIconId() {
+        if (mCurrentState.airplaneMode) {
+            return SignalDrawable.getAirplaneModeState(getNumLevels());
+        }
+
+        return getCurrentIconId();
+    }
+
+    @Override
+    public void notifyListeners(SignalCallback callback) {
+        MobileIconGroup icons = getIcons();
+
+        String contentDescription = getStringIfExists(getContentDescription());
+        String dataContentDescription = getStringIfExists(icons.mDataContentDescription);
+        final boolean dataDisabled = mCurrentState.iconGroup == TelephonyIcons.DATA_DISABLED
+                && mCurrentState.userSetup;
+
+        // Show icon in QS when we are connected or data is disabled.
+        boolean showDataIcon = mCurrentState.dataConnected || dataDisabled;
+        IconState statusIcon = new IconState(mCurrentState.enabled && !mCurrentState.airplaneMode,
+                getCurrentIconId(), contentDescription);
+
+        int qsTypeIcon = 0;
+        IconState qsIcon = null;
+        String description = null;
+        // Only send data sim callbacks to QS.
+        if (mCurrentState.dataSim) {
+            qsTypeIcon = showDataIcon ? icons.mQsDataType : 0;
+            qsIcon = new IconState(mCurrentState.enabled
+                    && !mCurrentState.isEmergency, getQsCurrentIconId(), contentDescription);
+            description = mCurrentState.isEmergency ? null : mCurrentState.networkName;
+        }
+        boolean activityIn = mCurrentState.dataConnected
+                && !mCurrentState.carrierNetworkChangeMode
+                && mCurrentState.activityIn;
+        boolean activityOut = mCurrentState.dataConnected
+                && !mCurrentState.carrierNetworkChangeMode
+                && mCurrentState.activityOut;
+        showDataIcon &= mCurrentState.isDefault || dataDisabled;
+        int typeIcon = showDataIcon ? icons.mDataType : 0;
+        callback.setMobileDataIndicators(statusIcon, qsIcon, typeIcon, qsTypeIcon,
+                activityIn, activityOut, dataContentDescription, description, icons.mIsWide,
+                mSubscriptionInfo.getSubscriptionId(), mCurrentState.roaming);
+    }
+
+    @Override
+    protected MobileState cleanState() {
+        return new MobileState();
+    }
+
+    private boolean hasService() {
+        if (mServiceState != null) {
+            // Consider the device to be in service if either voice or data
+            // service is available. Some SIM cards are marketed as data-only
+            // and do not support voice service, and on these SIM cards, we
+            // want to show signal bars for data service as well as the "no
+            // service" or "emergency calls only" text that indicates that voice
+            // is not available.
+            switch (mServiceState.getVoiceRegState()) {
+                case ServiceState.STATE_POWER_OFF:
+                    return false;
+                case ServiceState.STATE_OUT_OF_SERVICE:
+                case ServiceState.STATE_EMERGENCY_ONLY:
+                    return mServiceState.getDataRegState() == ServiceState.STATE_IN_SERVICE;
+                default:
+                    return true;
+            }
+        } else {
+            return false;
+        }
+    }
+
+    private boolean isCdma() {
+        return (mSignalStrength != null) && !mSignalStrength.isGsm();
+    }
+
+    public boolean isEmergencyOnly() {
+        return (mServiceState != null && mServiceState.isEmergencyOnly());
+    }
+
+    private boolean isRoaming() {
+        // During a carrier change, roaming indications need to be supressed.
+        if (isCarrierNetworkChangeActive()) {
+            return false;
+        }
+        if (isCdma() && mServiceState != null) {
+            final int iconMode = mServiceState.getCdmaEriIconMode();
+            return mServiceState.getCdmaEriIconIndex() != EriInfo.ROAMING_INDICATOR_OFF
+                    && (iconMode == EriInfo.ROAMING_ICON_MODE_NORMAL
+                    || iconMode == EriInfo.ROAMING_ICON_MODE_FLASH);
+        } else {
+            return mServiceState != null && mServiceState.getRoaming();
+        }
+    }
+
+    private boolean isCarrierNetworkChangeActive() {
+        return mCurrentState.carrierNetworkChangeMode;
+    }
+
+    public void handleBroadcast(Intent intent) {
+        String action = intent.getAction();
+        if (action.equals(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION)) {
+            updateNetworkName(intent.getBooleanExtra(TelephonyIntents.EXTRA_SHOW_SPN, false),
+                    intent.getStringExtra(TelephonyIntents.EXTRA_SPN),
+                    intent.getStringExtra(TelephonyIntents.EXTRA_DATA_SPN),
+                    intent.getBooleanExtra(TelephonyIntents.EXTRA_SHOW_PLMN, false),
+                    intent.getStringExtra(TelephonyIntents.EXTRA_PLMN));
+            notifyListenersIfNecessary();
+        } else if (action.equals(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)) {
+            updateDataSim();
+            notifyListenersIfNecessary();
+        }
+    }
+
+    private void updateDataSim() {
+        int defaultDataSub = mDefaults.getDefaultDataSubId();
+        if (SubscriptionManager.isValidSubscriptionId(defaultDataSub)) {
+            mCurrentState.dataSim = defaultDataSub == mSubscriptionInfo.getSubscriptionId();
+        } else {
+            // There doesn't seem to be a data sim selected, however if
+            // there isn't a MobileSignalController with dataSim set, then
+            // QS won't get any callbacks and will be blank.  Instead
+            // lets just assume we are the data sim (which will basically
+            // show one at random) in QS until one is selected.  The user
+            // should pick one soon after, so we shouldn't be in this state
+            // for long.
+            mCurrentState.dataSim = true;
+        }
+    }
+
+    /**
+     * Updates the network's name based on incoming spn and plmn.
+     */
+    void updateNetworkName(boolean showSpn, String spn, String dataSpn,
+            boolean showPlmn, String plmn) {
+        if (CHATTY) {
+            Log.d("CarrierLabel", "updateNetworkName showSpn=" + showSpn
+                    + " spn=" + spn + " dataSpn=" + dataSpn
+                    + " showPlmn=" + showPlmn + " plmn=" + plmn);
+        }
+        StringBuilder str = new StringBuilder();
+        StringBuilder strData = new StringBuilder();
+        if (showPlmn && plmn != null) {
+            str.append(plmn);
+            strData.append(plmn);
+        }
+        if (showSpn && spn != null) {
+            if (str.length() != 0) {
+                str.append(mNetworkNameSeparator);
+            }
+            str.append(spn);
+        }
+        if (str.length() != 0) {
+            mCurrentState.networkName = str.toString();
+        } else {
+            mCurrentState.networkName = mNetworkNameDefault;
+        }
+        if (showSpn && dataSpn != null) {
+            if (strData.length() != 0) {
+                strData.append(mNetworkNameSeparator);
+            }
+            strData.append(dataSpn);
+        }
+        if (strData.length() != 0) {
+            mCurrentState.networkNameData = strData.toString();
+        } else {
+            mCurrentState.networkNameData = mNetworkNameDefault;
+        }
+    }
+
+    /**
+     * Updates the current state based on mServiceState, mSignalStrength, mDataNetType,
+     * mDataState, and mSimState.  It should be called any time one of these is updated.
+     * This will call listeners if necessary.
+     */
+    private final void updateTelephony() {
+        if (DEBUG) {
+            Log.d(mTag, "updateTelephonySignalStrength: hasService=" + hasService()
+                    + " ss=" + mSignalStrength);
+        }
+        mCurrentState.connected = hasService() && mSignalStrength != null;
+        if (mCurrentState.connected) {
+            if (!mSignalStrength.isGsm() && mConfig.alwaysShowCdmaRssi) {
+                mCurrentState.level = mSignalStrength.getCdmaLevel();
+            } else {
+                mCurrentState.level = mSignalStrength.getLevel();
+            }
+        }
+        if (mNetworkToIconLookup.indexOfKey(mDataNetType) >= 0) {
+            mCurrentState.iconGroup = mNetworkToIconLookup.get(mDataNetType);
+        } else {
+            mCurrentState.iconGroup = mDefaultIcons;
+        }
+        mCurrentState.dataConnected = mCurrentState.connected
+                && mDataState == TelephonyManager.DATA_CONNECTED;
+
+        mCurrentState.roaming = isRoaming();
+        if (isCarrierNetworkChangeActive()) {
+            mCurrentState.iconGroup = TelephonyIcons.CARRIER_NETWORK_CHANGE;
+        } else if (isDataDisabled()) {
+            mCurrentState.iconGroup = TelephonyIcons.DATA_DISABLED;
+        }
+        if (isEmergencyOnly() != mCurrentState.isEmergency) {
+            mCurrentState.isEmergency = isEmergencyOnly();
+            mNetworkController.recalculateEmergency();
+        }
+        // Fill in the network name if we think we have it.
+        if (mCurrentState.networkName == mNetworkNameDefault && mServiceState != null
+                && !TextUtils.isEmpty(mServiceState.getOperatorAlphaShort())) {
+            mCurrentState.networkName = mServiceState.getOperatorAlphaShort();
+        }
+
+        notifyListenersIfNecessary();
+    }
+
+    private boolean isDataDisabled() {
+        return !mPhone.getDataEnabled(mSubscriptionInfo.getSubscriptionId());
+    }
+
+    @VisibleForTesting
+    void setActivity(int activity) {
+        mCurrentState.activityIn = activity == TelephonyManager.DATA_ACTIVITY_INOUT
+                || activity == TelephonyManager.DATA_ACTIVITY_IN;
+        mCurrentState.activityOut = activity == TelephonyManager.DATA_ACTIVITY_INOUT
+                || activity == TelephonyManager.DATA_ACTIVITY_OUT;
+        notifyListenersIfNecessary();
+    }
+
+    @Override
+    public void dump(PrintWriter pw) {
+        super.dump(pw);
+        pw.println("  mSubscription=" + mSubscriptionInfo + ",");
+        pw.println("  mServiceState=" + mServiceState + ",");
+        pw.println("  mSignalStrength=" + mSignalStrength + ",");
+        pw.println("  mDataState=" + mDataState + ",");
+        pw.println("  mDataNetType=" + mDataNetType + ",");
+    }
+
+    class MobilePhoneStateListener extends PhoneStateListener {
+        public MobilePhoneStateListener(int subId, Looper looper) {
+            super(subId, looper);
+        }
+
+        @Override
+        public void onSignalStrengthsChanged(SignalStrength signalStrength) {
+            if (DEBUG) {
+                Log.d(mTag, "onSignalStrengthsChanged signalStrength=" + signalStrength +
+                        ((signalStrength == null) ? "" : (" level=" + signalStrength.getLevel())));
+            }
+            mSignalStrength = signalStrength;
+            updateTelephony();
+        }
+
+        @Override
+        public void onServiceStateChanged(ServiceState state) {
+            if (DEBUG) {
+                Log.d(mTag, "onServiceStateChanged voiceState=" + state.getVoiceRegState()
+                        + " dataState=" + state.getDataRegState());
+            }
+            mServiceState = state;
+            if (state != null) {
+                mDataNetType = state.getDataNetworkType();
+                if (mDataNetType == TelephonyManager.NETWORK_TYPE_LTE && mServiceState != null &&
+                        mServiceState.isUsingCarrierAggregation()) {
+                    mDataNetType = TelephonyManager.NETWORK_TYPE_LTE_CA;
+                }
+            }
+            updateTelephony();
+        }
+
+        @Override
+        public void onDataConnectionStateChanged(int state, int networkType) {
+            if (DEBUG) {
+                Log.d(mTag, "onDataConnectionStateChanged: state=" + state
+                        + " type=" + networkType);
+            }
+            mDataState = state;
+            mDataNetType = networkType;
+            if (mDataNetType == TelephonyManager.NETWORK_TYPE_LTE && mServiceState != null &&
+                    mServiceState.isUsingCarrierAggregation()) {
+                mDataNetType = TelephonyManager.NETWORK_TYPE_LTE_CA;
+            }
+            updateTelephony();
+        }
+
+        @Override
+        public void onDataActivity(int direction) {
+            if (DEBUG) {
+                Log.d(mTag, "onDataActivity: direction=" + direction);
+            }
+            setActivity(direction);
+        }
+
+        @Override
+        public void onCarrierNetworkChange(boolean active) {
+            if (DEBUG) {
+                Log.d(mTag, "onCarrierNetworkChange: active=" + active);
+            }
+            mCurrentState.carrierNetworkChangeMode = active;
+
+            updateTelephony();
+        }
+    };
+
+    static class MobileIconGroup extends SignalController.IconGroup {
+        final int mDataContentDescription; // mContentDescriptionDataType
+        final int mDataType;
+        final boolean mIsWide;
+        final int mQsDataType;
+
+        public MobileIconGroup(String name, int[][] sbIcons, int[][] qsIcons, int[] contentDesc,
+                int sbNullState, int qsNullState, int sbDiscState, int qsDiscState,
+                int discContentDesc, int dataContentDesc, int dataType, boolean isWide,
+                int qsDataType) {
+            super(name, sbIcons, qsIcons, contentDesc, sbNullState, qsNullState, sbDiscState,
+                    qsDiscState, discContentDesc);
+            mDataContentDescription = dataContentDesc;
+            mDataType = dataType;
+            mIsWide = isWide;
+            mQsDataType = qsDataType;
+        }
+    }
+
+    static class MobileState extends SignalController.State {
+        String networkName;
+        String networkNameData;
+        boolean dataSim;
+        boolean dataConnected;
+        boolean isEmergency;
+        boolean airplaneMode;
+        boolean carrierNetworkChangeMode;
+        boolean isDefault;
+        boolean userSetup;
+        boolean roaming;
+
+        @Override
+        public void copyFrom(State s) {
+            super.copyFrom(s);
+            MobileState state = (MobileState) s;
+            dataSim = state.dataSim;
+            networkName = state.networkName;
+            networkNameData = state.networkNameData;
+            dataConnected = state.dataConnected;
+            isDefault = state.isDefault;
+            isEmergency = state.isEmergency;
+            airplaneMode = state.airplaneMode;
+            carrierNetworkChangeMode = state.carrierNetworkChangeMode;
+            userSetup = state.userSetup;
+            roaming = state.roaming;
+        }
+
+        @Override
+        protected void toString(StringBuilder builder) {
+            super.toString(builder);
+            builder.append(',');
+            builder.append("dataSim=").append(dataSim).append(',');
+            builder.append("networkName=").append(networkName).append(',');
+            builder.append("networkNameData=").append(networkNameData).append(',');
+            builder.append("dataConnected=").append(dataConnected).append(',');
+            builder.append("roaming=").append(roaming).append(',');
+            builder.append("isDefault=").append(isDefault).append(',');
+            builder.append("isEmergency=").append(isEmergency).append(',');
+            builder.append("airplaneMode=").append(airplaneMode).append(',');
+            builder.append("carrierNetworkChangeMode=").append(carrierNetworkChangeMode)
+                    .append(',');
+            builder.append("userSetup=").append(userSetup);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            return super.equals(o)
+                    && Objects.equals(((MobileState) o).networkName, networkName)
+                    && Objects.equals(((MobileState) o).networkNameData, networkNameData)
+                    && ((MobileState) o).dataSim == dataSim
+                    && ((MobileState) o).dataConnected == dataConnected
+                    && ((MobileState) o).isEmergency == isEmergency
+                    && ((MobileState) o).airplaneMode == airplaneMode
+                    && ((MobileState) o).carrierNetworkChangeMode == carrierNetworkChangeMode
+                    && ((MobileState) o).userSetup == userSetup
+                    && ((MobileState) o).isDefault == isDefault
+                    && ((MobileState) o).roaming == roaming;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/NetworkController.java b/com/android/systemui/statusbar/policy/NetworkController.java
new file mode 100644
index 0000000..2771011
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/NetworkController.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.content.Context;
+import android.content.Intent;
+import android.telephony.SubscriptionInfo;
+
+import com.android.settingslib.net.DataUsageController;
+import com.android.settingslib.wifi.AccessPoint;
+import com.android.systemui.DemoMode;
+import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+
+import java.util.List;
+
+public interface NetworkController extends CallbackController<SignalCallback>, DemoMode {
+
+    boolean hasMobileDataFeature();
+    void addCallback(SignalCallback cb);
+    void removeCallback(SignalCallback cb);
+    void setWifiEnabled(boolean enabled);
+    AccessPointController getAccessPointController();
+    DataUsageController getMobileDataController();
+    DataSaverController getDataSaverController();
+
+    boolean hasVoiceCallingFeature();
+
+    void addEmergencyListener(EmergencyListener listener);
+    void removeEmergencyListener(EmergencyListener listener);
+    boolean hasEmergencyCryptKeeperText();
+    boolean isRadioOn();
+
+    public interface SignalCallback {
+        default void setWifiIndicators(boolean enabled, IconState statusIcon, IconState qsIcon,
+                boolean activityIn, boolean activityOut, String description, boolean isTransient) {}
+
+        default void setMobileDataIndicators(IconState statusIcon, IconState qsIcon, int statusType,
+                int qsType, boolean activityIn, boolean activityOut, String typeContentDescription,
+                String description, boolean isWide, int subId, boolean roaming) {}
+        default void setSubs(List<SubscriptionInfo> subs) {}
+        default void setNoSims(boolean show) {}
+
+        default void setEthernetIndicators(IconState icon) {}
+
+        default void setIsAirplaneMode(IconState icon) {}
+
+        default void setMobileDataEnabled(boolean enabled) {}
+    }
+
+    public interface EmergencyListener {
+        void setEmergencyCallsOnly(boolean emergencyOnly);
+    }
+
+    public static class IconState {
+        public final boolean visible;
+        public final int icon;
+        public final String contentDescription;
+
+        public IconState(boolean visible, int icon, String contentDescription) {
+            this.visible = visible;
+            this.icon = icon;
+            this.contentDescription = contentDescription;
+        }
+
+        public IconState(boolean visible, int icon, int contentDescription,
+                Context context) {
+            this(visible, icon, context.getString(contentDescription));
+        }
+    }
+
+    /**
+     * Tracks changes in access points.  Allows listening for changes, scanning for new APs,
+     * and connecting to new ones.
+     */
+    public interface AccessPointController {
+        void addAccessPointCallback(AccessPointCallback callback);
+        void removeAccessPointCallback(AccessPointCallback callback);
+        void scanForAccessPoints();
+        int getIcon(AccessPoint ap);
+        boolean connect(AccessPoint ap);
+        boolean canConfigWifi();
+
+        public interface AccessPointCallback {
+            void onAccessPointsChanged(List<AccessPoint> accessPoints);
+            void onSettingsActivityTriggered(Intent settingsIntent);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
new file mode 100644
index 0000000..c217bda
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
@@ -0,0 +1,978 @@
+/*
+ * Copyright (C) 2010 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.policy;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.NetworkCapabilities;
+import android.net.wifi.WifiManager;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.telephony.ServiceState;
+import android.telephony.SignalStrength;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.MathUtils;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.settingslib.net.DataUsageController;
+import com.android.systemui.ConfigurationChangedReceiver;
+import com.android.systemui.DemoMode;
+import com.android.systemui.Dumpable;
+import com.android.systemui.R;
+import com.android.systemui.settings.CurrentUserTracker;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+
+/** Platform implementation of the network controller. **/
+public class NetworkControllerImpl extends BroadcastReceiver
+        implements NetworkController, DemoMode, DataUsageController.NetworkNameProvider,
+        ConfigurationChangedReceiver, Dumpable {
+    // debug
+    static final String TAG = "NetworkController";
+    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    // additional diagnostics, but not logspew
+    static final boolean CHATTY =  Log.isLoggable(TAG + "Chat", Log.DEBUG);
+
+    private static final int EMERGENCY_NO_CONTROLLERS = 0;
+    private static final int EMERGENCY_FIRST_CONTROLLER = 100;
+    private static final int EMERGENCY_VOICE_CONTROLLER = 200;
+    private static final int EMERGENCY_NO_SUB = 300;
+    private static final int EMERGENCY_ASSUMED_VOICE_CONTROLLER = 400;
+
+    private final Context mContext;
+    private final TelephonyManager mPhone;
+    private final WifiManager mWifiManager;
+    private final ConnectivityManager mConnectivityManager;
+    private final SubscriptionManager mSubscriptionManager;
+    private final boolean mHasMobileDataFeature;
+    private final SubscriptionDefaults mSubDefaults;
+    private final DataSaverController mDataSaverController;
+    private final CurrentUserTracker mUserTracker;
+    private Config mConfig;
+
+    // Subcontrollers.
+    @VisibleForTesting
+    final WifiSignalController mWifiSignalController;
+
+    @VisibleForTesting
+    final EthernetSignalController mEthernetSignalController;
+
+    @VisibleForTesting
+    final SparseArray<MobileSignalController> mMobileSignalControllers = new SparseArray<>();
+    // When no SIMs are around at setup, and one is added later, it seems to default to the first
+    // SIM for most actions.  This may be null if there aren't any SIMs around.
+    private MobileSignalController mDefaultSignalController;
+    private final AccessPointControllerImpl mAccessPoints;
+    private final DataUsageController mDataUsageController;
+
+    private boolean mInetCondition; // Used for Logging and demo.
+
+    // BitSets indicating which network transport types (e.g., TRANSPORT_WIFI, TRANSPORT_MOBILE) are
+    // connected and validated, respectively.
+    private final BitSet mConnectedTransports = new BitSet();
+    private final BitSet mValidatedTransports = new BitSet();
+
+    // States that don't belong to a subcontroller.
+    private boolean mAirplaneMode = false;
+    private boolean mHasNoSims;
+    private Locale mLocale = null;
+    // This list holds our ordering.
+    private List<SubscriptionInfo> mCurrentSubscriptions = new ArrayList<>();
+
+    @VisibleForTesting
+    boolean mListening;
+
+    // The current user ID.
+    private int mCurrentUserId;
+
+    private OnSubscriptionsChangedListener mSubscriptionListener;
+
+    // Handler that all broadcasts are received on.
+    private final Handler mReceiverHandler;
+    // Handler that all callbacks are made on.
+    private final CallbackHandler mCallbackHandler;
+
+    private int mEmergencySource;
+    private boolean mIsEmergency;
+
+    @VisibleForTesting
+    ServiceState mLastServiceState;
+    private boolean mUserSetup;
+
+    /**
+     * Construct this controller object and register for updates.
+     */
+    public NetworkControllerImpl(Context context, Looper bgLooper,
+            DeviceProvisionedController deviceProvisionedController) {
+        this(context, (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE),
+                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE),
+                (WifiManager) context.getSystemService(Context.WIFI_SERVICE),
+                SubscriptionManager.from(context), Config.readConfig(context), bgLooper,
+                new CallbackHandler(),
+                new AccessPointControllerImpl(context, bgLooper),
+                new DataUsageController(context),
+                new SubscriptionDefaults(),
+                deviceProvisionedController);
+        mReceiverHandler.post(mRegisterListeners);
+    }
+
+    @VisibleForTesting
+    NetworkControllerImpl(Context context, ConnectivityManager connectivityManager,
+            TelephonyManager telephonyManager, WifiManager wifiManager,
+            SubscriptionManager subManager, Config config, Looper bgLooper,
+            CallbackHandler callbackHandler,
+            AccessPointControllerImpl accessPointController,
+            DataUsageController dataUsageController,
+            SubscriptionDefaults defaultsHandler,
+            DeviceProvisionedController deviceProvisionedController) {
+        mContext = context;
+        mConfig = config;
+        mReceiverHandler = new Handler(bgLooper);
+        mCallbackHandler = callbackHandler;
+        mDataSaverController = new DataSaverControllerImpl(context);
+
+        mSubscriptionManager = subManager;
+        mSubDefaults = defaultsHandler;
+        mConnectivityManager = connectivityManager;
+        mHasMobileDataFeature =
+                mConnectivityManager.isNetworkSupported(ConnectivityManager.TYPE_MOBILE);
+
+        // telephony
+        mPhone = telephonyManager;
+
+        // wifi
+        mWifiManager = wifiManager;
+
+        mLocale = mContext.getResources().getConfiguration().locale;
+        mAccessPoints = accessPointController;
+        mDataUsageController = dataUsageController;
+        mDataUsageController.setNetworkController(this);
+        // TODO: Find a way to move this into DataUsageController.
+        mDataUsageController.setCallback(new DataUsageController.Callback() {
+            @Override
+            public void onMobileDataEnabled(boolean enabled) {
+                mCallbackHandler.setMobileDataEnabled(enabled);
+            }
+        });
+        mWifiSignalController = new WifiSignalController(mContext, mHasMobileDataFeature,
+                mCallbackHandler, this);
+
+        mEthernetSignalController = new EthernetSignalController(mContext, mCallbackHandler, this);
+
+        // AIRPLANE_MODE_CHANGED is sent at boot; we've probably already missed it
+        updateAirplaneMode(true /* force callback */);
+        mUserTracker = new CurrentUserTracker(mContext) {
+            @Override
+            public void onUserSwitched(int newUserId) {
+                NetworkControllerImpl.this.onUserSwitched(newUserId);
+            }
+        };
+        mUserTracker.startTracking();
+        deviceProvisionedController.addCallback(new DeviceProvisionedListener() {
+            @Override
+            public void onUserSetupChanged() {
+                setUserSetupComplete(deviceProvisionedController.isUserSetup(
+                        deviceProvisionedController.getCurrentUser()));
+            }
+        });
+    }
+
+    public DataSaverController getDataSaverController() {
+        return mDataSaverController;
+    }
+
+    private void registerListeners() {
+        for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+            MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i);
+            mobileSignalController.registerListener();
+        }
+        if (mSubscriptionListener == null) {
+            mSubscriptionListener = new SubListener();
+        }
+        mSubscriptionManager.addOnSubscriptionsChangedListener(mSubscriptionListener);
+
+        // broadcasts
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(WifiManager.RSSI_CHANGED_ACTION);
+        filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
+        filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
+        filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
+        filter.addAction(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED);
+        filter.addAction(TelephonyIntents.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED);
+        filter.addAction(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED);
+        filter.addAction(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION);
+        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+        filter.addAction(ConnectivityManager.INET_CONDITION_ACTION);
+        filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        mContext.registerReceiver(this, filter, null, mReceiverHandler);
+        mListening = true;
+
+        updateMobileControllers();
+    }
+
+    private void unregisterListeners() {
+        mListening = false;
+        for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+            MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i);
+            mobileSignalController.unregisterListener();
+        }
+        mSubscriptionManager.removeOnSubscriptionsChangedListener(mSubscriptionListener);
+        mContext.unregisterReceiver(this);
+    }
+
+    public int getConnectedWifiLevel() {
+        return mWifiSignalController.getState().level;
+    }
+
+    @Override
+    public AccessPointController getAccessPointController() {
+        return mAccessPoints;
+    }
+
+    @Override
+    public DataUsageController getMobileDataController() {
+        return mDataUsageController;
+    }
+
+    public void addEmergencyListener(EmergencyListener listener) {
+        mCallbackHandler.setListening(listener, true);
+        mCallbackHandler.setEmergencyCallsOnly(isEmergencyOnly());
+    }
+
+    public void removeEmergencyListener(EmergencyListener listener) {
+        mCallbackHandler.setListening(listener, false);
+    }
+
+    public boolean hasMobileDataFeature() {
+        return mHasMobileDataFeature;
+    }
+
+    public boolean hasVoiceCallingFeature() {
+        return mPhone.getPhoneType() != TelephonyManager.PHONE_TYPE_NONE;
+    }
+
+    private MobileSignalController getDataController() {
+        int dataSubId = mSubDefaults.getDefaultDataSubId();
+        if (!SubscriptionManager.isValidSubscriptionId(dataSubId)) {
+            if (DEBUG) Log.e(TAG, "No data sim selected");
+            return mDefaultSignalController;
+        }
+        if (mMobileSignalControllers.indexOfKey(dataSubId) >= 0) {
+            return mMobileSignalControllers.get(dataSubId);
+        }
+        if (DEBUG) Log.e(TAG, "Cannot find controller for data sub: " + dataSubId);
+        return mDefaultSignalController;
+    }
+
+    public String getMobileDataNetworkName() {
+        MobileSignalController controller = getDataController();
+        return controller != null ? controller.getState().networkNameData : "";
+    }
+
+    public boolean isEmergencyOnly() {
+        if (mMobileSignalControllers.size() == 0) {
+            // When there are no active subscriptions, determine emengency state from last
+            // broadcast.
+            mEmergencySource = EMERGENCY_NO_CONTROLLERS;
+            return mLastServiceState != null && mLastServiceState.isEmergencyOnly();
+        }
+        int voiceSubId = mSubDefaults.getDefaultVoiceSubId();
+        if (!SubscriptionManager.isValidSubscriptionId(voiceSubId)) {
+            for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+                MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i);
+                if (!mobileSignalController.getState().isEmergency) {
+                    mEmergencySource = EMERGENCY_FIRST_CONTROLLER
+                            + mobileSignalController.mSubscriptionInfo.getSubscriptionId();
+                    if (DEBUG) Log.d(TAG, "Found emergency " + mobileSignalController.mTag);
+                    return false;
+                }
+            }
+        }
+        if (mMobileSignalControllers.indexOfKey(voiceSubId) >= 0) {
+            mEmergencySource = EMERGENCY_VOICE_CONTROLLER + voiceSubId;
+            if (DEBUG) Log.d(TAG, "Getting emergency from " + voiceSubId);
+            return mMobileSignalControllers.get(voiceSubId).getState().isEmergency;
+        }
+        // If we have the wrong subId but there is only one sim anyway, assume it should be the
+        // default.
+        if (mMobileSignalControllers.size() == 1) {
+            mEmergencySource = EMERGENCY_ASSUMED_VOICE_CONTROLLER
+                    + mMobileSignalControllers.keyAt(0);
+            if (DEBUG) Log.d(TAG, "Getting assumed emergency from "
+                    + mMobileSignalControllers.keyAt(0));
+            return mMobileSignalControllers.valueAt(0).getState().isEmergency;
+        }
+        if (DEBUG) Log.e(TAG, "Cannot find controller for voice sub: " + voiceSubId);
+        mEmergencySource = EMERGENCY_NO_SUB + voiceSubId;
+        // Something is wrong, better assume we can't make calls...
+        return true;
+    }
+
+    /**
+     * Emergency status may have changed (triggered by MobileSignalController),
+     * so we should recheck and send out the state to listeners.
+     */
+    void recalculateEmergency() {
+        mIsEmergency = isEmergencyOnly();
+        mCallbackHandler.setEmergencyCallsOnly(mIsEmergency);
+    }
+
+    public void addCallback(SignalCallback cb) {
+        cb.setSubs(mCurrentSubscriptions);
+        cb.setIsAirplaneMode(new IconState(mAirplaneMode,
+                TelephonyIcons.FLIGHT_MODE_ICON, R.string.accessibility_airplane_mode, mContext));
+        cb.setNoSims(mHasNoSims);
+        mWifiSignalController.notifyListeners(cb);
+        mEthernetSignalController.notifyListeners(cb);
+        for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+            MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i);
+            mobileSignalController.notifyListeners(cb);
+        }
+        mCallbackHandler.setListening(cb, true);
+    }
+
+    @Override
+    public void removeCallback(SignalCallback cb) {
+        mCallbackHandler.setListening(cb, false);
+    }
+
+    @Override
+    public void setWifiEnabled(final boolean enabled) {
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... args) {
+                // Disable tethering if enabling Wifi
+                final int wifiApState = mWifiManager.getWifiApState();
+                if (enabled && ((wifiApState == WifiManager.WIFI_AP_STATE_ENABLING) ||
+                        (wifiApState == WifiManager.WIFI_AP_STATE_ENABLED))) {
+                    mWifiManager.setWifiApEnabled(null, false);
+                }
+
+                mWifiManager.setWifiEnabled(enabled);
+                return null;
+            }
+        }.execute();
+    }
+
+    private void onUserSwitched(int newUserId) {
+        mCurrentUserId = newUserId;
+        mAccessPoints.onUserSwitched(newUserId);
+        updateConnectivity();
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (CHATTY) {
+            Log.d(TAG, "onReceive: intent=" + intent);
+        }
+        final String action = intent.getAction();
+        if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION) ||
+                action.equals(ConnectivityManager.INET_CONDITION_ACTION)) {
+            updateConnectivity();
+        } else if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) {
+            refreshLocale();
+            updateAirplaneMode(false);
+        } else if (action.equals(TelephonyIntents.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED)) {
+            // We are using different subs now, we might be able to make calls.
+            recalculateEmergency();
+        } else if (action.equals(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)) {
+            // Notify every MobileSignalController so they can know whether they are the
+            // data sim or not.
+            for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+                MobileSignalController controller = mMobileSignalControllers.valueAt(i);
+                controller.handleBroadcast(intent);
+            }
+        } else if (action.equals(TelephonyIntents.ACTION_SIM_STATE_CHANGED)) {
+            // Might have different subscriptions now.
+            updateMobileControllers();
+        } else if (action.equals(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED)) {
+            mLastServiceState = ServiceState.newFromBundle(intent.getExtras());
+            if (mMobileSignalControllers.size() == 0) {
+                // If none of the subscriptions are active, we might need to recalculate
+                // emergency state.
+                recalculateEmergency();
+            }
+        } else {
+            int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY,
+                    SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+            if (SubscriptionManager.isValidSubscriptionId(subId)) {
+                if (mMobileSignalControllers.indexOfKey(subId) >= 0) {
+                    mMobileSignalControllers.get(subId).handleBroadcast(intent);
+                } else {
+                    // Can't find this subscription...  We must be out of date.
+                    updateMobileControllers();
+                }
+            } else {
+                // No sub id, must be for the wifi.
+                mWifiSignalController.handleBroadcast(intent);
+            }
+        }
+    }
+
+    public void onConfigurationChanged(Configuration newConfig) {
+        mConfig = Config.readConfig(mContext);
+        mReceiverHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                handleConfigurationChanged();
+            }
+        });
+    }
+
+    @VisibleForTesting
+    void handleConfigurationChanged() {
+        for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+            MobileSignalController controller = mMobileSignalControllers.valueAt(i);
+            controller.setConfiguration(mConfig);
+        }
+        refreshLocale();
+    }
+
+    private void updateMobileControllers() {
+        if (!mListening) {
+            return;
+        }
+        doUpdateMobileControllers();
+    }
+
+    @VisibleForTesting
+    void doUpdateMobileControllers() {
+        List<SubscriptionInfo> subscriptions = mSubscriptionManager.getActiveSubscriptionInfoList();
+        if (subscriptions == null) {
+            subscriptions = Collections.emptyList();
+        }
+        // If there have been no relevant changes to any of the subscriptions, we can leave as is.
+        if (hasCorrectMobileControllers(subscriptions)) {
+            // Even if the controllers are correct, make sure we have the right no sims state.
+            // Such as on boot, don't need any controllers, because there are no sims,
+            // but we still need to update the no sim state.
+            updateNoSims();
+            return;
+        }
+        setCurrentSubscriptions(subscriptions);
+        updateNoSims();
+        recalculateEmergency();
+    }
+
+    @VisibleForTesting
+    protected void updateNoSims() {
+        boolean hasNoSims = mHasMobileDataFeature && mMobileSignalControllers.size() == 0;
+        if (hasNoSims != mHasNoSims) {
+            mHasNoSims = hasNoSims;
+            mCallbackHandler.setNoSims(mHasNoSims);
+        }
+    }
+
+    @VisibleForTesting
+    void setCurrentSubscriptions(List<SubscriptionInfo> subscriptions) {
+        Collections.sort(subscriptions, new Comparator<SubscriptionInfo>() {
+            @Override
+            public int compare(SubscriptionInfo lhs, SubscriptionInfo rhs) {
+                return lhs.getSimSlotIndex() == rhs.getSimSlotIndex()
+                        ? lhs.getSubscriptionId() - rhs.getSubscriptionId()
+                        : lhs.getSimSlotIndex() - rhs.getSimSlotIndex();
+            }
+        });
+        mCurrentSubscriptions = subscriptions;
+
+        SparseArray<MobileSignalController> cachedControllers =
+                new SparseArray<MobileSignalController>();
+        for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+            cachedControllers.put(mMobileSignalControllers.keyAt(i),
+                    mMobileSignalControllers.valueAt(i));
+        }
+        mMobileSignalControllers.clear();
+        final int num = subscriptions.size();
+        for (int i = 0; i < num; i++) {
+            int subId = subscriptions.get(i).getSubscriptionId();
+            // If we have a copy of this controller already reuse it, otherwise make a new one.
+            if (cachedControllers.indexOfKey(subId) >= 0) {
+                mMobileSignalControllers.put(subId, cachedControllers.get(subId));
+                cachedControllers.remove(subId);
+            } else {
+                MobileSignalController controller = new MobileSignalController(mContext, mConfig,
+                        mHasMobileDataFeature, mPhone, mCallbackHandler,
+                        this, subscriptions.get(i), mSubDefaults, mReceiverHandler.getLooper());
+                controller.setUserSetupComplete(mUserSetup);
+                mMobileSignalControllers.put(subId, controller);
+                if (subscriptions.get(i).getSimSlotIndex() == 0) {
+                    mDefaultSignalController = controller;
+                }
+                if (mListening) {
+                    controller.registerListener();
+                }
+            }
+        }
+        if (mListening) {
+            for (int i = 0; i < cachedControllers.size(); i++) {
+                int key = cachedControllers.keyAt(i);
+                if (cachedControllers.get(key) == mDefaultSignalController) {
+                    mDefaultSignalController = null;
+                }
+                cachedControllers.get(key).unregisterListener();
+            }
+        }
+        mCallbackHandler.setSubs(subscriptions);
+        notifyAllListeners();
+
+        // There may be new MobileSignalControllers around, make sure they get the current
+        // inet condition and airplane mode.
+        pushConnectivityToSignals();
+        updateAirplaneMode(true /* force */);
+    }
+
+    private void setUserSetupComplete(final boolean userSetup) {
+        mReceiverHandler.post(() -> handleSetUserSetupComplete(userSetup));
+    }
+
+    private void handleSetUserSetupComplete(boolean userSetup) {
+        mUserSetup = userSetup;
+        for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+            MobileSignalController controller = mMobileSignalControllers.valueAt(i);
+            controller.setUserSetupComplete(mUserSetup);
+        }
+    }
+
+    @VisibleForTesting
+    boolean hasCorrectMobileControllers(List<SubscriptionInfo> allSubscriptions) {
+        if (allSubscriptions.size() != mMobileSignalControllers.size()) {
+            return false;
+        }
+        for (SubscriptionInfo info : allSubscriptions) {
+            if (mMobileSignalControllers.indexOfKey(info.getSubscriptionId()) < 0) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private void updateAirplaneMode(boolean force) {
+        boolean airplaneMode = (Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.AIRPLANE_MODE_ON, 0) == 1);
+        if (airplaneMode != mAirplaneMode || force) {
+            mAirplaneMode = airplaneMode;
+            for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+                MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i);
+                mobileSignalController.setAirplaneMode(mAirplaneMode);
+            }
+            notifyListeners();
+        }
+    }
+
+    private void refreshLocale() {
+        Locale current = mContext.getResources().getConfiguration().locale;
+        if (!current.equals(mLocale)) {
+            mLocale = current;
+            notifyAllListeners();
+        }
+    }
+
+    /**
+     * Forces update of all callbacks on both SignalClusters and
+     * NetworkSignalChangedCallbacks.
+     */
+    private void notifyAllListeners() {
+        notifyListeners();
+        for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+            MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i);
+            mobileSignalController.notifyListeners();
+        }
+        mWifiSignalController.notifyListeners();
+        mEthernetSignalController.notifyListeners();
+    }
+
+    /**
+     * Notifies listeners of changes in state of to the NetworkController, but
+     * does not notify for any info on SignalControllers, for that call
+     * notifyAllListeners.
+     */
+    private void notifyListeners() {
+        mCallbackHandler.setIsAirplaneMode(new IconState(mAirplaneMode,
+                TelephonyIcons.FLIGHT_MODE_ICON, R.string.accessibility_airplane_mode, mContext));
+        mCallbackHandler.setNoSims(mHasNoSims);
+    }
+
+    /**
+     * Update the Inet conditions and what network we are connected to.
+     */
+    private void updateConnectivity() {
+        mConnectedTransports.clear();
+        mValidatedTransports.clear();
+        for (NetworkCapabilities nc :
+                mConnectivityManager.getDefaultNetworkCapabilitiesForUser(mCurrentUserId)) {
+            for (int transportType : nc.getTransportTypes()) {
+                mConnectedTransports.set(transportType);
+                if (nc.hasCapability(NET_CAPABILITY_VALIDATED)) {
+                    mValidatedTransports.set(transportType);
+                }
+            }
+        }
+
+        if (CHATTY) {
+            Log.d(TAG, "updateConnectivity: mConnectedTransports=" + mConnectedTransports);
+            Log.d(TAG, "updateConnectivity: mValidatedTransports=" + mValidatedTransports);
+        }
+
+        mInetCondition = !mValidatedTransports.isEmpty();
+
+        pushConnectivityToSignals();
+    }
+
+    /**
+     * Pushes the current connectivity state to all SignalControllers.
+     */
+    private void pushConnectivityToSignals() {
+        // We want to update all the icons, all at once, for any condition change
+        for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+            MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i);
+            mobileSignalController.updateConnectivity(mConnectedTransports, mValidatedTransports);
+        }
+        mWifiSignalController.updateConnectivity(mConnectedTransports, mValidatedTransports);
+        mEthernetSignalController.updateConnectivity(mConnectedTransports, mValidatedTransports);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("NetworkController state:");
+
+        pw.println("  - telephony ------");
+        pw.print("  hasVoiceCallingFeature()=");
+        pw.println(hasVoiceCallingFeature());
+
+        pw.println("  - connectivity ------");
+        pw.print("  mConnectedTransports=");
+        pw.println(mConnectedTransports);
+        pw.print("  mValidatedTransports=");
+        pw.println(mValidatedTransports);
+        pw.print("  mInetCondition=");
+        pw.println(mInetCondition);
+        pw.print("  mAirplaneMode=");
+        pw.println(mAirplaneMode);
+        pw.print("  mLocale=");
+        pw.println(mLocale);
+        pw.print("  mLastServiceState=");
+        pw.println(mLastServiceState);
+        pw.print("  mIsEmergency=");
+        pw.println(mIsEmergency);
+        pw.print("  mEmergencySource=");
+        pw.println(emergencyToString(mEmergencySource));
+
+        for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+            MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i);
+            mobileSignalController.dump(pw);
+        }
+        mWifiSignalController.dump(pw);
+
+        mEthernetSignalController.dump(pw);
+
+        mAccessPoints.dump(pw);
+    }
+
+    private static final String emergencyToString(int emergencySource) {
+        if (emergencySource > EMERGENCY_NO_SUB) {
+            return "ASSUMED_VOICE_CONTROLLER(" + (emergencySource - EMERGENCY_VOICE_CONTROLLER)
+                    + ")";
+        } else if (emergencySource > EMERGENCY_NO_SUB) {
+            return "NO_SUB(" + (emergencySource - EMERGENCY_NO_SUB) + ")";
+        } else if (emergencySource > EMERGENCY_VOICE_CONTROLLER) {
+            return "VOICE_CONTROLLER(" + (emergencySource - EMERGENCY_VOICE_CONTROLLER) + ")";
+        } else if (emergencySource > EMERGENCY_FIRST_CONTROLLER) {
+            return "FIRST_CONTROLLER(" + (emergencySource - EMERGENCY_FIRST_CONTROLLER) + ")";
+        } else if (emergencySource == EMERGENCY_NO_CONTROLLERS) {
+            return "NO_CONTROLLERS";
+        }
+        return "UNKNOWN_SOURCE";
+    }
+
+    private boolean mDemoMode;
+    private boolean mDemoInetCondition;
+    private WifiSignalController.WifiState mDemoWifiState;
+
+    @Override
+    public void dispatchDemoCommand(String command, Bundle args) {
+        if (!mDemoMode && command.equals(COMMAND_ENTER)) {
+            if (DEBUG) Log.d(TAG, "Entering demo mode");
+            unregisterListeners();
+            mDemoMode = true;
+            mDemoInetCondition = mInetCondition;
+            mDemoWifiState = mWifiSignalController.getState();
+            mDemoWifiState.ssid = "DemoMode";
+        } else if (mDemoMode && command.equals(COMMAND_EXIT)) {
+            if (DEBUG) Log.d(TAG, "Exiting demo mode");
+            mDemoMode = false;
+            // Update what MobileSignalControllers, because they may change
+            // to set the number of sim slots.
+            updateMobileControllers();
+            for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+                MobileSignalController controller = mMobileSignalControllers.valueAt(i);
+                controller.resetLastState();
+            }
+            mWifiSignalController.resetLastState();
+            mReceiverHandler.post(mRegisterListeners);
+            notifyAllListeners();
+        } else if (mDemoMode && command.equals(COMMAND_NETWORK)) {
+            String airplane = args.getString("airplane");
+            if (airplane != null) {
+                boolean show = airplane.equals("show");
+                mCallbackHandler.setIsAirplaneMode(new IconState(show,
+                        TelephonyIcons.FLIGHT_MODE_ICON, R.string.accessibility_airplane_mode,
+                        mContext));
+            }
+            String fully = args.getString("fully");
+            if (fully != null) {
+                mDemoInetCondition = Boolean.parseBoolean(fully);
+                BitSet connected = new BitSet();
+
+                if (mDemoInetCondition) {
+                    connected.set(mWifiSignalController.mTransportType);
+                }
+                mWifiSignalController.updateConnectivity(connected, connected);
+                for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+                    MobileSignalController controller = mMobileSignalControllers.valueAt(i);
+                    if (mDemoInetCondition) {
+                        connected.set(controller.mTransportType);
+                    }
+                    controller.updateConnectivity(connected, connected);
+                }
+            }
+            String wifi = args.getString("wifi");
+            if (wifi != null) {
+                boolean show = wifi.equals("show");
+                String level = args.getString("level");
+                if (level != null) {
+                    mDemoWifiState.level = level.equals("null") ? -1
+                            : Math.min(Integer.parseInt(level), WifiIcons.WIFI_LEVEL_COUNT - 1);
+                    mDemoWifiState.connected = mDemoWifiState.level >= 0;
+                }
+                String activity = args.getString("activity");
+                if (activity != null) {
+                    switch (activity) {
+                        case "inout":
+                            mWifiSignalController.setActivity(WifiManager.DATA_ACTIVITY_INOUT);
+                            break;
+                        case "in":
+                            mWifiSignalController.setActivity(WifiManager.DATA_ACTIVITY_IN);
+                            break;
+                        case "out":
+                            mWifiSignalController.setActivity(WifiManager.DATA_ACTIVITY_OUT);
+                            break;
+                        default:
+                            mWifiSignalController.setActivity(WifiManager.DATA_ACTIVITY_NONE);
+                            break;
+                    }
+                } else {
+                    mWifiSignalController.setActivity(WifiManager.DATA_ACTIVITY_NONE);
+                }
+                mDemoWifiState.enabled = show;
+                mWifiSignalController.notifyListeners();
+            }
+            String sims = args.getString("sims");
+            if (sims != null) {
+                int num = MathUtils.constrain(Integer.parseInt(sims), 1, 8);
+                List<SubscriptionInfo> subs = new ArrayList<>();
+                if (num != mMobileSignalControllers.size()) {
+                    mMobileSignalControllers.clear();
+                    int start = mSubscriptionManager.getActiveSubscriptionInfoCountMax();
+                    for (int i = start /* get out of normal index range */; i < start + num; i++) {
+                        subs.add(addSignalController(i, i));
+                    }
+                    mCallbackHandler.setSubs(subs);
+                }
+            }
+            String nosim = args.getString("nosim");
+            if (nosim != null) {
+                mHasNoSims = nosim.equals("show");
+                mCallbackHandler.setNoSims(mHasNoSims);
+            }
+            String mobile = args.getString("mobile");
+            if (mobile != null) {
+                boolean show = mobile.equals("show");
+                String datatype = args.getString("datatype");
+                String slotString = args.getString("slot");
+                int slot = TextUtils.isEmpty(slotString) ? 0 : Integer.parseInt(slotString);
+                slot = MathUtils.constrain(slot, 0, 8);
+                // Ensure we have enough sim slots
+                List<SubscriptionInfo> subs = new ArrayList<>();
+                while (mMobileSignalControllers.size() <= slot) {
+                    int nextSlot = mMobileSignalControllers.size();
+                    subs.add(addSignalController(nextSlot, nextSlot));
+                }
+                if (!subs.isEmpty()) {
+                    mCallbackHandler.setSubs(subs);
+                }
+                // Hack to index linearly for easy use.
+                MobileSignalController controller = mMobileSignalControllers.valueAt(slot);
+                controller.getState().dataSim = datatype != null;
+                controller.getState().isDefault = datatype != null;
+                controller.getState().dataConnected = datatype != null;
+                if (datatype != null) {
+                    controller.getState().iconGroup =
+                            datatype.equals("1x") ? TelephonyIcons.ONE_X :
+                            datatype.equals("3g") ? TelephonyIcons.THREE_G :
+                            datatype.equals("4g") ? TelephonyIcons.FOUR_G :
+                            datatype.equals("4g+") ? TelephonyIcons.FOUR_G_PLUS :
+                            datatype.equals("e") ? TelephonyIcons.E :
+                            datatype.equals("g") ? TelephonyIcons.G :
+                            datatype.equals("h") ? TelephonyIcons.H :
+                            datatype.equals("lte") ? TelephonyIcons.LTE :
+                            datatype.equals("lte+") ? TelephonyIcons.LTE_PLUS :
+                            datatype.equals("dis") ? TelephonyIcons.DATA_DISABLED :
+                            TelephonyIcons.UNKNOWN;
+                }
+                if (args.containsKey("roam")) {
+                    controller.getState().roaming = "show".equals(args.getString("roam"));
+                }
+                String level = args.getString("level");
+                if (level != null) {
+                    controller.getState().level = level.equals("null") ? -1
+                            : Math.min(Integer.parseInt(level),
+                                    SignalStrength.NUM_SIGNAL_STRENGTH_BINS);
+                    controller.getState().connected = controller.getState().level >= 0;
+                }
+                String activity = args.getString("activity");
+                if (activity != null) {
+                    controller.getState().dataConnected = true;
+                    switch (activity) {
+                        case "inout":
+                            controller.setActivity(TelephonyManager.DATA_ACTIVITY_INOUT);
+                            break;
+                        case "in":
+                            controller.setActivity(TelephonyManager.DATA_ACTIVITY_IN);
+                            break;
+                        case "out":
+                            controller.setActivity(TelephonyManager.DATA_ACTIVITY_OUT);
+                            break;
+                        default:
+                            controller.setActivity(TelephonyManager.DATA_ACTIVITY_NONE);
+                            break;
+                    }
+                } else {
+                    controller.setActivity(TelephonyManager.DATA_ACTIVITY_NONE);
+                }
+                controller.getState().enabled = show;
+                controller.notifyListeners();
+            }
+            String carrierNetworkChange = args.getString("carriernetworkchange");
+            if (carrierNetworkChange != null) {
+                boolean show = carrierNetworkChange.equals("show");
+                for (int i = 0; i < mMobileSignalControllers.size(); i++) {
+                    MobileSignalController controller = mMobileSignalControllers.valueAt(i);
+                    controller.setCarrierNetworkChangeMode(show);
+                }
+            }
+        }
+    }
+
+    private SubscriptionInfo addSignalController(int id, int simSlotIndex) {
+        SubscriptionInfo info = new SubscriptionInfo(id, "", simSlotIndex, "", "", 0, 0, "", 0,
+                null, 0, 0, "");
+        MobileSignalController controller = new MobileSignalController(mContext,
+                mConfig, mHasMobileDataFeature, mPhone, mCallbackHandler, this, info,
+                mSubDefaults, mReceiverHandler.getLooper());
+        mMobileSignalControllers.put(id, controller);
+        controller.getState().userSetup = true;
+        return info;
+    }
+
+    public boolean hasEmergencyCryptKeeperText() {
+        return EncryptionHelper.IS_DATA_ENCRYPTED;
+    }
+
+    public boolean isRadioOn() {
+        return !mAirplaneMode;
+    }
+
+    private class SubListener extends OnSubscriptionsChangedListener {
+        @Override
+        public void onSubscriptionsChanged() {
+            updateMobileControllers();
+        }
+    }
+
+    /**
+     * Used to register listeners from the BG Looper, this way the PhoneStateListeners that
+     * get created will also run on the BG Looper.
+     */
+    private final Runnable mRegisterListeners = new Runnable() {
+        @Override
+        public void run() {
+            registerListeners();
+        }
+    };
+
+    public static class SubscriptionDefaults {
+        public int getDefaultVoiceSubId() {
+            return SubscriptionManager.getDefaultVoiceSubscriptionId();
+        }
+
+        public int getDefaultDataSubId() {
+            return SubscriptionManager.getDefaultDataSubscriptionId();
+        }
+    }
+
+    @VisibleForTesting
+    static class Config {
+        boolean showAtLeast3G = false;
+        boolean alwaysShowCdmaRssi = false;
+        boolean show4gForLte = false;
+        boolean hideLtePlus = false;
+        boolean hspaDataDistinguishable;
+        boolean inflateSignalStrengths = false;
+
+        static Config readConfig(Context context) {
+            Config config = new Config();
+            Resources res = context.getResources();
+
+            config.showAtLeast3G = res.getBoolean(R.bool.config_showMin3G);
+            config.alwaysShowCdmaRssi =
+                    res.getBoolean(com.android.internal.R.bool.config_alwaysUseCdmaRssi);
+            config.show4gForLte = res.getBoolean(R.bool.config_show4GForLTE);
+            config.hspaDataDistinguishable =
+                    res.getBoolean(R.bool.config_hspa_data_distinguishable);
+            config.hideLtePlus = res.getBoolean(R.bool.config_hideLtePlus);
+            config.inflateSignalStrengths = res.getBoolean(R.bool.config_inflateSignalStrength);
+            return config;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/NextAlarmController.java b/com/android/systemui/statusbar/policy/NextAlarmController.java
new file mode 100644
index 0000000..366a752
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/NextAlarmController.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 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.policy;
+
+import android.app.AlarmManager;
+
+import com.android.systemui.Dumpable;
+import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback;
+
+public interface NextAlarmController extends CallbackController<NextAlarmChangeCallback>, Dumpable {
+
+    public interface NextAlarmChangeCallback {
+        void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm);
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java b/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
new file mode 100644
index 0000000..dfdeae1
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.app.AlarmManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.UserHandle;
+
+import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+public class NextAlarmControllerImpl extends BroadcastReceiver
+        implements NextAlarmController {
+
+    private final ArrayList<NextAlarmChangeCallback> mChangeCallbacks = new ArrayList<>();
+
+    private AlarmManager mAlarmManager;
+    private AlarmManager.AlarmClockInfo mNextAlarm;
+
+    public NextAlarmControllerImpl(Context context) {
+        mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_USER_SWITCHED);
+        filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
+        context.registerReceiverAsUser(this, UserHandle.ALL, filter, null, null);
+        updateNextAlarm();
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("NextAlarmController state:");
+        pw.print("  mNextAlarm="); pw.println(mNextAlarm);
+    }
+
+    public void addCallback(NextAlarmChangeCallback cb) {
+        mChangeCallbacks.add(cb);
+        cb.onNextAlarmChanged(mNextAlarm);
+    }
+
+    public void removeCallback(NextAlarmChangeCallback cb) {
+        mChangeCallbacks.remove(cb);
+    }
+
+    public void onReceive(Context context, Intent intent) {
+        final String action = intent.getAction();
+        if (action.equals(Intent.ACTION_USER_SWITCHED)
+                || action.equals(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)) {
+            updateNextAlarm();
+        }
+    }
+
+    private void updateNextAlarm() {
+        mNextAlarm = mAlarmManager.getNextAlarmClock(UserHandle.USER_CURRENT);
+        fireNextAlarmChanged();
+    }
+
+    private void fireNextAlarmChanged() {
+        int n = mChangeCallbacks.size();
+        for (int i = 0; i < n; i++) {
+            mChangeCallbacks.get(i).onNextAlarmChanged(mNextAlarm);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java b/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
new file mode 100644
index 0000000..5444f06
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 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.policy;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+
+/**
+ * A listener to heads up changes
+ */
+public interface OnHeadsUpChangedListener {
+    /**
+     * The state whether there exist pinned heads-ups or not changed.
+     *
+     * @param inPinnedMode whether there are any pinned heads-ups
+     */
+    default void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {}
+
+    /**
+     * A notification was just pinned to the top.
+     */
+    default void onHeadsUpPinned(ExpandableNotificationRow headsUp) {}
+
+    /**
+     * A notification was just unpinned from the top.
+     */
+    default void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {}
+
+    /**
+     * A notification just became a heads up or turned back to its normal state.
+     *
+     * @param entry     the entry of the changed notification
+     * @param isHeadsUp whether the notification is now a headsUp notification
+     */
+    default void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {}
+}
diff --git a/com/android/systemui/statusbar/policy/PreviewInflater.java b/com/android/systemui/statusbar/policy/PreviewInflater.java
new file mode 100644
index 0000000..687b83a
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/PreviewInflater.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.statusbar.phone.KeyguardPreviewContainer;
+
+import java.util.List;
+
+/**
+ * Utility class to inflate previews for phone and camera affordance.
+ */
+public class PreviewInflater {
+
+    private static final String TAG = "PreviewInflater";
+
+    private static final String META_DATA_KEYGUARD_LAYOUT = "com.android.keyguard.layout";
+
+    private Context mContext;
+    private LockPatternUtils mLockPatternUtils;
+
+    public PreviewInflater(Context context, LockPatternUtils lockPatternUtils) {
+        mContext = context;
+        mLockPatternUtils = lockPatternUtils;
+    }
+
+    public View inflatePreview(Intent intent) {
+        WidgetInfo info = getWidgetInfo(intent);
+        return inflatePreview(info);
+    }
+
+    public View inflatePreviewFromService(ComponentName componentName) {
+        WidgetInfo info = getWidgetInfoFromService(componentName);
+        return inflatePreview(info);
+    }
+
+    private KeyguardPreviewContainer inflatePreview(WidgetInfo info) {
+        if (info == null) {
+            return null;
+        }
+        View v = inflateWidgetView(info);
+        if (v == null) {
+            return null;
+        }
+        KeyguardPreviewContainer container = new KeyguardPreviewContainer(mContext, null);
+        container.addView(v);
+        return container;
+    }
+
+    private View inflateWidgetView(WidgetInfo widgetInfo) {
+        View widgetView = null;
+        try {
+            Context appContext = mContext.createPackageContext(
+                    widgetInfo.contextPackage, Context.CONTEXT_RESTRICTED);
+            LayoutInflater appInflater = (LayoutInflater)
+                    appContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+            appInflater = appInflater.cloneInContext(appContext);
+            widgetView = appInflater.inflate(widgetInfo.layoutId, null, false);
+        } catch (PackageManager.NameNotFoundException|RuntimeException e) {
+            Log.w(TAG, "Error creating widget view", e);
+        }
+        return widgetView;
+    }
+
+    private WidgetInfo getWidgetInfoFromService(ComponentName componentName) {
+        PackageManager packageManager = mContext.getPackageManager();
+        // Look for the preview specified in the service meta-data
+        try {
+            Bundle metaData = packageManager.getServiceInfo(
+                    componentName, PackageManager.GET_META_DATA).metaData;
+            return getWidgetInfoFromMetaData(componentName.getPackageName(), metaData);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Failed to load preview; " + componentName.flattenToShortString()
+                    + " not found", e);
+        }
+        return null;
+    }
+
+    private WidgetInfo getWidgetInfoFromMetaData(String contextPackage,
+            Bundle metaData) {
+        if (metaData == null) {
+            return null;
+        }
+        int layoutId = metaData.getInt(META_DATA_KEYGUARD_LAYOUT);
+        if (layoutId == 0) {
+            return null;
+        }
+        WidgetInfo info = new WidgetInfo();
+        info.contextPackage = contextPackage;
+        info.layoutId = layoutId;
+        return info;
+    }
+
+    private WidgetInfo getWidgetInfo(Intent intent) {
+        PackageManager packageManager = mContext.getPackageManager();
+        int flags = PackageManager.MATCH_DEFAULT_ONLY
+                | PackageManager.MATCH_DIRECT_BOOT_AWARE
+                | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+        final List<ResolveInfo> appList = packageManager.queryIntentActivitiesAsUser(
+                intent, flags, KeyguardUpdateMonitor.getCurrentUser());
+        if (appList.size() == 0) {
+            return null;
+        }
+        ResolveInfo resolved = packageManager.resolveActivityAsUser(intent,
+                flags | PackageManager.GET_META_DATA,
+                KeyguardUpdateMonitor.getCurrentUser());
+        if (wouldLaunchResolverActivity(resolved, appList)) {
+            return null;
+        }
+        if (resolved == null || resolved.activityInfo == null) {
+            return null;
+        }
+        return getWidgetInfoFromMetaData(resolved.activityInfo.packageName,
+                resolved.activityInfo.metaData);
+    }
+
+    public static boolean wouldLaunchResolverActivity(Context ctx, Intent intent,
+            int currentUserId) {
+        return getTargetActivityInfo(ctx, intent, currentUserId, false /* onlyDirectBootAware */)
+                == null;
+    }
+
+    /**
+     * @param onlyDirectBootAware a boolean indicating whether the matched activity packages must
+     *                            be direct boot aware when in direct boot mode if false, all
+     *                            packages are considered a match even if they are not aware.
+     * @return the target activity info of the intent it resolves to a specific package or
+     *         {@code null} if it resolved to the resolver activity
+     */
+    public static ActivityInfo getTargetActivityInfo(Context ctx, Intent intent,
+            int currentUserId, boolean onlyDirectBootAware) {
+        PackageManager packageManager = ctx.getPackageManager();
+        int flags = PackageManager.MATCH_DEFAULT_ONLY;
+        if (!onlyDirectBootAware) {
+            flags |=  PackageManager.MATCH_DIRECT_BOOT_AWARE
+                    | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+        }
+        final List<ResolveInfo> appList = packageManager.queryIntentActivitiesAsUser(
+                intent, flags, currentUserId);
+        if (appList.size() == 0) {
+            return null;
+        }
+        ResolveInfo resolved = packageManager.resolveActivityAsUser(intent,
+                flags | PackageManager.GET_META_DATA, currentUserId);
+        if (resolved == null || wouldLaunchResolverActivity(resolved, appList)) {
+            return null;
+        } else {
+            return resolved.activityInfo;
+        }
+    }
+
+    private static boolean wouldLaunchResolverActivity(
+            ResolveInfo resolved, List<ResolveInfo> appList) {
+        // If the list contains the above resolved activity, then it can't be
+        // ResolverActivity itself.
+        for (int i = 0; i < appList.size(); i++) {
+            ResolveInfo tmp = appList.get(i);
+            if (tmp.activityInfo.name.equals(resolved.activityInfo.name)
+                    && tmp.activityInfo.packageName.equals(resolved.activityInfo.packageName)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static class WidgetInfo {
+        String contextPackage;
+        int layoutId;
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/RemoteInputView.java b/com/android/systemui/statusbar/policy/RemoteInputView.java
new file mode 100644
index 0000000..37b0de4
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -0,0 +1,610 @@
+/*
+ * 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.policy;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutManager;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.RemoteInputController;
+import com.android.systemui.statusbar.notification.NotificationViewWrapper;
+import com.android.systemui.statusbar.stack.ScrollContainer;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+/**
+ * Host for the remote input.
+ */
+public class RemoteInputView extends LinearLayout implements View.OnClickListener, TextWatcher {
+
+    private static final String TAG = "RemoteInput";
+
+    // A marker object that let's us easily find views of this class.
+    public static final Object VIEW_TAG = new Object();
+
+    public final Object mToken = new Object();
+
+    private RemoteEditText mEditText;
+    private ImageButton mSendButton;
+    private ProgressBar mProgressBar;
+    private PendingIntent mPendingIntent;
+    private RemoteInput[] mRemoteInputs;
+    private RemoteInput mRemoteInput;
+    private RemoteInputController mController;
+
+    private NotificationData.Entry mEntry;
+
+    private ScrollContainer mScrollContainer;
+    private View mScrollContainerChild;
+    private boolean mRemoved;
+
+    private int mRevealCx;
+    private int mRevealCy;
+    private int mRevealR;
+
+    private boolean mResetting;
+    private NotificationViewWrapper mWrapper;
+
+    public RemoteInputView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mProgressBar = findViewById(R.id.remote_input_progress);
+
+        mSendButton = findViewById(R.id.remote_input_send);
+        mSendButton.setOnClickListener(this);
+
+        mEditText = (RemoteEditText) getChildAt(0);
+        mEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+            @Override
+            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+                final boolean isSoftImeEvent = event == null
+                        && (actionId == EditorInfo.IME_ACTION_DONE
+                        || actionId == EditorInfo.IME_ACTION_NEXT
+                        || actionId == EditorInfo.IME_ACTION_SEND);
+                final boolean isKeyboardEnterKey = event != null
+                        && KeyEvent.isConfirmKey(event.getKeyCode())
+                        && event.getAction() == KeyEvent.ACTION_DOWN;
+
+                if (isSoftImeEvent || isKeyboardEnterKey) {
+                    if (mEditText.length() > 0) {
+                        sendRemoteInput();
+                    }
+                    // Consume action to prevent IME from closing.
+                    return true;
+                }
+                return false;
+            }
+        });
+        mEditText.addTextChangedListener(this);
+        mEditText.setInnerFocusable(false);
+        mEditText.mRemoteInputView = this;
+    }
+
+    private void sendRemoteInput() {
+        Bundle results = new Bundle();
+        results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString());
+        Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent,
+                results);
+
+        mEditText.setEnabled(false);
+        mSendButton.setVisibility(INVISIBLE);
+        mProgressBar.setVisibility(VISIBLE);
+        mEntry.remoteInputText = mEditText.getText();
+        mController.addSpinning(mEntry.key, mToken);
+        mController.removeRemoteInput(mEntry, mToken);
+        mEditText.mShowImeOnInputConnection = false;
+        mController.remoteInputSent(mEntry);
+
+        // Tell ShortcutManager that this package has been "activated".  ShortcutManager
+        // will reset the throttling for this package.
+        // Strictly speaking, the intent receiver may be different from the notification publisher,
+        // but that's an edge case, and also because we can't always know which package will receive
+        // an intent, so we just reset for the publisher.
+        getContext().getSystemService(ShortcutManager.class).onApplicationActive(
+                mEntry.notification.getPackageName(),
+                mEntry.notification.getUser().getIdentifier());
+
+        MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_SEND,
+                mEntry.notification.getPackageName());
+        try {
+            mPendingIntent.send(mContext, 0, fillInIntent);
+        } catch (PendingIntent.CanceledException e) {
+            Log.i(TAG, "Unable to send remote input result", e);
+            MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_FAIL,
+                    mEntry.notification.getPackageName());
+        }
+    }
+
+    public static RemoteInputView inflate(Context context, ViewGroup root,
+            NotificationData.Entry entry,
+            RemoteInputController controller) {
+        RemoteInputView v = (RemoteInputView)
+                LayoutInflater.from(context).inflate(R.layout.remote_input, root, false);
+        v.mController = controller;
+        v.mEntry = entry;
+        v.setTag(VIEW_TAG);
+
+        return v;
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (v == mSendButton) {
+            sendRemoteInput();
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        super.onTouchEvent(event);
+
+        // We never want for a touch to escape to an outer view or one we covered.
+        return true;
+    }
+
+    private void onDefocus(boolean animate) {
+        mController.removeRemoteInput(mEntry, mToken);
+        mEntry.remoteInputText = mEditText.getText();
+
+        // During removal, we get reattached and lose focus. Not hiding in that
+        // case to prevent flicker.
+        if (!mRemoved) {
+            if (animate && mRevealR > 0) {
+                Animator reveal = ViewAnimationUtils.createCircularReveal(
+                        this, mRevealCx, mRevealCy, mRevealR, 0);
+                reveal.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+                reveal.setDuration(StackStateAnimator.ANIMATION_DURATION_CLOSE_REMOTE_INPUT);
+                reveal.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        setVisibility(INVISIBLE);
+                        if (mWrapper != null) {
+                            mWrapper.setRemoteInputVisible(false);
+                        }
+                    }
+                });
+                reveal.start();
+            } else {
+                setVisibility(INVISIBLE);
+                if (mWrapper != null) {
+                    mWrapper.setRemoteInputVisible(false);
+                }
+            }
+        }
+        MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_CLOSE,
+                mEntry.notification.getPackageName());
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        if (mEntry.row.isChangingPosition()) {
+            if (getVisibility() == VISIBLE && mEditText.isFocusable()) {
+                mEditText.requestFocus();
+            }
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (mEntry.row.isChangingPosition() || isTemporarilyDetached()) {
+            return;
+        }
+        mController.removeRemoteInput(mEntry, mToken);
+        mController.removeSpinning(mEntry.key, mToken);
+    }
+
+    public void setPendingIntent(PendingIntent pendingIntent) {
+        mPendingIntent = pendingIntent;
+    }
+
+    public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput) {
+        mRemoteInputs = remoteInputs;
+        mRemoteInput = remoteInput;
+        mEditText.setHint(mRemoteInput.getLabel());
+    }
+
+    public void focusAnimated() {
+        if (getVisibility() != VISIBLE) {
+            Animator animator = ViewAnimationUtils.createCircularReveal(
+                    this, mRevealCx, mRevealCy, 0, mRevealR);
+            animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+            animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+            animator.start();
+        }
+        focus();
+    }
+
+    public void focus() {
+        MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_OPEN,
+                mEntry.notification.getPackageName());
+
+        setVisibility(VISIBLE);
+        if (mWrapper != null) {
+            mWrapper.setRemoteInputVisible(true);
+        }
+        mController.addRemoteInput(mEntry, mToken);
+        mEditText.setInnerFocusable(true);
+        mEditText.mShowImeOnInputConnection = true;
+        mEditText.setText(mEntry.remoteInputText);
+        mEditText.setSelection(mEditText.getText().length());
+        mEditText.requestFocus();
+        updateSendButton();
+    }
+
+    public void onNotificationUpdateOrReset() {
+        boolean sending = mProgressBar.getVisibility() == VISIBLE;
+
+        if (sending) {
+            // Update came in after we sent the reply, time to reset.
+            reset();
+        }
+
+        if (isActive() && mWrapper != null) {
+            mWrapper.setRemoteInputVisible(true);
+        }
+    }
+
+    private void reset() {
+        mResetting = true;
+
+        mEditText.getText().clear();
+        mEditText.setEnabled(true);
+        mSendButton.setVisibility(VISIBLE);
+        mProgressBar.setVisibility(INVISIBLE);
+        mController.removeSpinning(mEntry.key, mToken);
+        updateSendButton();
+        onDefocus(false /* animate */);
+
+        mResetting = false;
+    }
+
+    @Override
+    public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
+        if (mResetting && child == mEditText) {
+            // Suppress text events if it happens during resetting. Ideally this would be
+            // suppressed by the text view not being shown, but that doesn't work here because it
+            // needs to stay visible for the animation.
+            return false;
+        }
+        return super.onRequestSendAccessibilityEvent(child, event);
+    }
+
+    private void updateSendButton() {
+        mSendButton.setEnabled(mEditText.getText().length() != 0);
+    }
+
+    @Override
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+    @Override
+    public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+    @Override
+    public void afterTextChanged(Editable s) {
+        updateSendButton();
+    }
+
+    public void close() {
+        mEditText.defocusIfNeeded(false /* animated */);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            findScrollContainer();
+            if (mScrollContainer != null) {
+                mScrollContainer.requestDisallowLongPress();
+                mScrollContainer.requestDisallowDismiss();
+            }
+        }
+        return super.onInterceptTouchEvent(ev);
+    }
+
+    public boolean requestScrollTo() {
+        findScrollContainer();
+        mScrollContainer.lockScrollTo(mScrollContainerChild);
+        return true;
+    }
+
+    private void findScrollContainer() {
+        if (mScrollContainer == null) {
+            mScrollContainerChild = null;
+            ViewParent p = this;
+            while (p != null) {
+                if (mScrollContainerChild == null && p instanceof ExpandableView) {
+                    mScrollContainerChild = (View) p;
+                }
+                if (p.getParent() instanceof ScrollContainer) {
+                    mScrollContainer = (ScrollContainer) p.getParent();
+                    if (mScrollContainerChild == null) {
+                        mScrollContainerChild = (View) p;
+                    }
+                    break;
+                }
+                p = p.getParent();
+            }
+        }
+    }
+
+    public boolean isActive() {
+        return mEditText.isFocused() && mEditText.isEnabled();
+    }
+
+    public void stealFocusFrom(RemoteInputView other) {
+        other.close();
+        setPendingIntent(other.mPendingIntent);
+        setRemoteInput(other.mRemoteInputs, other.mRemoteInput);
+        setRevealParameters(other.mRevealCx, other.mRevealCy, other.mRevealR);
+        focus();
+    }
+
+    /**
+     * Tries to find an action in {@param actions} that matches the current pending intent
+     * of this view and updates its state to that of the found action
+     *
+     * @return true if a matching action was found, false otherwise
+     */
+    public boolean updatePendingIntentFromActions(Notification.Action[] actions) {
+        if (mPendingIntent == null || actions == null) {
+            return false;
+        }
+        Intent current = mPendingIntent.getIntent();
+        if (current == null) {
+            return false;
+        }
+
+        for (Notification.Action a : actions) {
+            RemoteInput[] inputs = a.getRemoteInputs();
+            if (a.actionIntent == null || inputs == null) {
+                continue;
+            }
+            Intent candidate = a.actionIntent.getIntent();
+            if (!current.filterEquals(candidate)) {
+                continue;
+            }
+
+            RemoteInput input = null;
+            for (RemoteInput i : inputs) {
+                if (i.getAllowFreeFormInput()) {
+                    input = i;
+                }
+            }
+            if (input == null) {
+                continue;
+            }
+            setPendingIntent(a.actionIntent);
+            setRemoteInput(inputs, input);
+            return true;
+        }
+        return false;
+    }
+
+    public PendingIntent getPendingIntent() {
+        return mPendingIntent;
+    }
+
+    public void setRemoved() {
+        mRemoved = true;
+    }
+
+    public void setRevealParameters(int cx, int cy, int r) {
+        mRevealCx = cx;
+        mRevealCy = cy;
+        mRevealR = r;
+    }
+
+    @Override
+    public void dispatchStartTemporaryDetach() {
+        super.dispatchStartTemporaryDetach();
+        // Detach the EditText temporarily such that it doesn't get onDetachedFromWindow and
+        // won't lose IME focus.
+        detachViewFromParent(mEditText);
+    }
+
+    @Override
+    public void dispatchFinishTemporaryDetach() {
+        if (isAttachedToWindow()) {
+            attachViewToParent(mEditText, 0, mEditText.getLayoutParams());
+        } else {
+            removeDetachedView(mEditText, false /* animate */);
+        }
+        super.dispatchFinishTemporaryDetach();
+    }
+
+    public void setWrapper(NotificationViewWrapper wrapper) {
+        mWrapper = wrapper;
+    }
+
+    /**
+     * An EditText that changes appearance based on whether it's focusable and becomes
+     * un-focusable whenever the user navigates away from it or it becomes invisible.
+     */
+    public static class RemoteEditText extends EditText {
+
+        private final Drawable mBackground;
+        private RemoteInputView mRemoteInputView;
+        boolean mShowImeOnInputConnection;
+
+        public RemoteEditText(Context context, AttributeSet attrs) {
+            super(context, attrs);
+            mBackground = getBackground();
+        }
+
+        private void defocusIfNeeded(boolean animate) {
+            if (mRemoteInputView != null && mRemoteInputView.mEntry.row.isChangingPosition()
+                    || isTemporarilyDetached()) {
+                if (isTemporarilyDetached()) {
+                    // We might get reattached but then the other one of HUN / expanded might steal
+                    // our focus, so we'll need to save our text here.
+                    if (mRemoteInputView != null) {
+                        mRemoteInputView.mEntry.remoteInputText = getText();
+                    }
+                }
+                return;
+            }
+            if (isFocusable() && isEnabled()) {
+                setInnerFocusable(false);
+                if (mRemoteInputView != null) {
+                    mRemoteInputView.onDefocus(animate);
+                }
+                mShowImeOnInputConnection = false;
+            }
+        }
+
+        @Override
+        protected void onVisibilityChanged(View changedView, int visibility) {
+            super.onVisibilityChanged(changedView, visibility);
+
+            if (!isShown()) {
+                defocusIfNeeded(false /* animate */);
+            }
+        }
+
+        @Override
+        protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+            super.onFocusChanged(focused, direction, previouslyFocusedRect);
+            if (!focused) {
+                defocusIfNeeded(true /* animate */);
+            }
+        }
+
+        @Override
+        public void getFocusedRect(Rect r) {
+            super.getFocusedRect(r);
+            r.top = mScrollY;
+            r.bottom = mScrollY + (mBottom - mTop);
+        }
+
+        @Override
+        public boolean requestRectangleOnScreen(Rect rectangle) {
+            return mRemoteInputView.requestScrollTo();
+        }
+
+        @Override
+        public boolean onKeyDown(int keyCode, KeyEvent event) {
+            if (keyCode == KeyEvent.KEYCODE_BACK) {
+                // Eat the DOWN event here to prevent any default behavior.
+                return true;
+            }
+            return super.onKeyDown(keyCode, event);
+        }
+
+        @Override
+        public boolean onKeyUp(int keyCode, KeyEvent event) {
+            if (keyCode == KeyEvent.KEYCODE_BACK) {
+                defocusIfNeeded(true /* animate */);
+                return true;
+            }
+            return super.onKeyUp(keyCode, event);
+        }
+
+        @Override
+        public boolean onCheckIsTextEditor() {
+            // Stop being editable while we're being removed. During removal, we get reattached,
+            // and editable views get their spellchecking state re-evaluated which is too costly
+            // during the removal animation.
+            boolean flyingOut = mRemoteInputView != null && mRemoteInputView.mRemoved;
+            return !flyingOut && super.onCheckIsTextEditor();
+        }
+
+        @Override
+        public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+            final InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
+
+            if (mShowImeOnInputConnection && inputConnection != null) {
+                final InputMethodManager imm = InputMethodManager.getInstance();
+                if (imm != null) {
+                    // onCreateInputConnection is called by InputMethodManager in the middle of
+                    // setting up the connection to the IME; wait with requesting the IME until that
+                    // work has completed.
+                    post(new Runnable() {
+                        @Override
+                        public void run() {
+                            imm.viewClicked(RemoteEditText.this);
+                            imm.showSoftInput(RemoteEditText.this, 0);
+                        }
+                    });
+                }
+            }
+
+            return inputConnection;
+        }
+
+        @Override
+        public void onCommitCompletion(CompletionInfo text) {
+            clearComposingText();
+            setText(text.getText());
+            setSelection(getText().length());
+        }
+
+        void setInnerFocusable(boolean focusable) {
+            setFocusableInTouchMode(focusable);
+            setFocusable(focusable);
+            setCursorVisible(focusable);
+
+            if (focusable) {
+                requestFocus();
+                setBackground(mBackground);
+            } else {
+                setBackground(null);
+            }
+
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/RotationLockController.java b/com/android/systemui/statusbar/policy/RotationLockController.java
new file mode 100644
index 0000000..722874b
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/RotationLockController.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import com.android.systemui.statusbar.policy.RotationLockController.RotationLockControllerCallback;
+
+public interface RotationLockController extends Listenable,
+        CallbackController<RotationLockControllerCallback> {
+    int getRotationLockOrientation();
+    boolean isRotationLockAffordanceVisible();
+    boolean isRotationLocked();
+    void setRotationLocked(boolean locked);
+
+    public interface RotationLockControllerCallback {
+        void onRotationLockStateChanged(boolean rotationLocked, boolean affordanceVisible);
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java b/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
new file mode 100644
index 0000000..4f96496
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2010 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.policy;
+
+import android.content.Context;
+import android.os.UserHandle;
+
+import com.android.internal.view.RotationPolicy;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/** Platform implementation of the rotation lock controller. **/
+public final class RotationLockControllerImpl implements RotationLockController {
+    private final Context mContext;
+    private final CopyOnWriteArrayList<RotationLockControllerCallback> mCallbacks =
+            new CopyOnWriteArrayList<RotationLockControllerCallback>();
+
+    private final RotationPolicy.RotationPolicyListener mRotationPolicyListener =
+            new RotationPolicy.RotationPolicyListener() {
+        @Override
+        public void onChange() {
+            notifyChanged();
+        }
+    };
+
+    public RotationLockControllerImpl(Context context) {
+        mContext = context;
+        setListening(true);
+    }
+
+    public void addCallback(RotationLockControllerCallback callback) {
+        mCallbacks.add(callback);
+        notifyChanged(callback);
+    }
+
+    public void removeCallback(RotationLockControllerCallback callback) {
+        mCallbacks.remove(callback);
+    }
+
+    public int getRotationLockOrientation() {
+        return RotationPolicy.getRotationLockOrientation(mContext);
+    }
+
+    public boolean isRotationLocked() {
+        return RotationPolicy.isRotationLocked(mContext);
+    }
+
+    public void setRotationLocked(boolean locked) {
+        RotationPolicy.setRotationLock(mContext, locked);
+    }
+
+    public boolean isRotationLockAffordanceVisible() {
+        return RotationPolicy.isRotationLockToggleVisible(mContext);
+    }
+
+    @Override
+    public void setListening(boolean listening) {
+        if (listening) {
+            RotationPolicy.registerRotationPolicyListener(mContext, mRotationPolicyListener,
+                    UserHandle.USER_ALL);
+        } else {
+            RotationPolicy.unregisterRotationPolicyListener(mContext, mRotationPolicyListener);
+        }
+    }
+
+    private void notifyChanged() {
+        for (RotationLockControllerCallback callback : mCallbacks) {
+            notifyChanged(callback);
+        }
+    }
+
+    private void notifyChanged(RotationLockControllerCallback callback) {
+        callback.onRotationLockStateChanged(RotationPolicy.isRotationLocked(mContext),
+                RotationPolicy.isRotationLockToggleVisible(mContext));
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/ScrollAdapter.java b/com/android/systemui/statusbar/policy/ScrollAdapter.java
new file mode 100644
index 0000000..f35e22d
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/ScrollAdapter.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.view.View;
+
+/**
+ * A scroll adapter which can be queried for meta information about the scroll state
+ */
+public interface ScrollAdapter {
+
+    /**
+     * @return Whether the view returned by {@link #getHostView()} is scrolled to the top
+     */
+    public boolean isScrolledToTop();
+
+    /**
+     * @return Whether the view returned by {@link #getHostView()} is scrolled to the bottom
+     */
+    public boolean isScrolledToBottom();
+
+    /**
+     * @return The view in which the scrolling is performed
+     */
+    public View getHostView();
+}
diff --git a/com/android/systemui/statusbar/policy/SecurityController.java b/com/android/systemui/statusbar/policy/SecurityController.java
new file mode 100644
index 0000000..1fb9b69
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/SecurityController.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import com.android.systemui.Dumpable;
+import com.android.systemui.statusbar.policy.SecurityController.SecurityControllerCallback;
+
+public interface SecurityController extends CallbackController<SecurityControllerCallback>,
+        Dumpable {
+    /** Whether the device has device owner, even if not on this user. */
+    boolean isDeviceManaged();
+    boolean hasProfileOwner();
+    boolean hasWorkProfile();
+    String getDeviceOwnerName();
+    String getProfileOwnerName();
+    CharSequence getDeviceOwnerOrganizationName();
+    CharSequence getWorkProfileOrganizationName();
+    boolean isNetworkLoggingEnabled();
+    boolean isVpnEnabled();
+    boolean isVpnRestricted();
+    /** Whether the VPN app should use branded VPN iconography.  */
+    boolean isVpnBranded();
+    String getPrimaryVpnName();
+    String getWorkProfileVpnName();
+    boolean hasCACertInCurrentUser();
+    boolean hasCACertInWorkProfile();
+    void onUserSwitched(int newUserId);
+
+    public interface SecurityControllerCallback {
+        void onStateChanged();
+    }
+
+}
diff --git a/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
new file mode 100644
index 0000000..b22ce18
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.app.ActivityManager;
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserInfo;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.IConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.security.KeyChain;
+import android.security.KeyChain.KeyChainConnection;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.net.LegacyVpnInfo;
+import com.android.internal.net.VpnConfig;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.settings.CurrentUserTracker;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+public class SecurityControllerImpl extends CurrentUserTracker implements SecurityController {
+
+    private static final String TAG = "SecurityController";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final NetworkRequest REQUEST = new NetworkRequest.Builder()
+            .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+            .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+            .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+            .build();
+    private static final int NO_NETWORK = -1;
+
+    private static final String VPN_BRANDED_META_DATA = "com.android.systemui.IS_BRANDED";
+
+    private static final int CA_CERT_LOADING_RETRY_TIME_IN_MS = 30_000;
+
+    private final Context mContext;
+    private final ConnectivityManager mConnectivityManager;
+    private final IConnectivityManager mConnectivityManagerService;
+    private final DevicePolicyManager mDevicePolicyManager;
+    private final PackageManager mPackageManager;
+    private final UserManager mUserManager;
+
+    @GuardedBy("mCallbacks")
+    private final ArrayList<SecurityControllerCallback> mCallbacks = new ArrayList<>();
+
+    private SparseArray<VpnConfig> mCurrentVpns = new SparseArray<>();
+    private int mCurrentUserId;
+    private int mVpnUserId;
+
+    // Key: userId, Value: whether the user has CACerts installed
+    // Needs to be cached here since the query has to be asynchronous
+    private ArrayMap<Integer, Boolean> mHasCACerts = new ArrayMap<Integer, Boolean>();
+
+    public SecurityControllerImpl(Context context) {
+        this(context, null);
+    }
+
+    public SecurityControllerImpl(Context context, SecurityControllerCallback callback) {
+        super(context);
+        mContext = context;
+        mDevicePolicyManager = (DevicePolicyManager)
+                context.getSystemService(Context.DEVICE_POLICY_SERVICE);
+        mConnectivityManager = (ConnectivityManager)
+                context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        mConnectivityManagerService = IConnectivityManager.Stub.asInterface(
+                ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
+        mPackageManager = context.getPackageManager();
+        mUserManager = (UserManager)
+                context.getSystemService(Context.USER_SERVICE);
+
+        addCallback(callback);
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(KeyChain.ACTION_TRUST_STORE_CHANGED);
+        context.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null,
+                new Handler(Dependency.get(Dependency.BG_LOOPER)));
+
+        // TODO: re-register network callback on user change.
+        mConnectivityManager.registerNetworkCallback(REQUEST, mNetworkCallback);
+        onUserSwitched(ActivityManager.getCurrentUser());
+        startTracking();
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("SecurityController state:");
+        pw.print("  mCurrentVpns={");
+        for (int i = 0 ; i < mCurrentVpns.size(); i++) {
+            if (i > 0) {
+                pw.print(", ");
+            }
+            pw.print(mCurrentVpns.keyAt(i));
+            pw.print('=');
+            pw.print(mCurrentVpns.valueAt(i).user);
+        }
+        pw.println("}");
+    }
+
+    @Override
+    public boolean isDeviceManaged() {
+        return mDevicePolicyManager.isDeviceManaged();
+    }
+
+    @Override
+    public String getDeviceOwnerName() {
+        return mDevicePolicyManager.getDeviceOwnerNameOnAnyUser();
+    }
+
+    @Override
+    public boolean hasProfileOwner() {
+        return mDevicePolicyManager.getProfileOwnerAsUser(mCurrentUserId) != null;
+    }
+
+    @Override
+    public String getProfileOwnerName() {
+        for (int profileId : mUserManager.getProfileIdsWithDisabled(mCurrentUserId)) {
+            String name = mDevicePolicyManager.getProfileOwnerNameAsUser(profileId);
+            if (name != null) {
+                return name;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public CharSequence getDeviceOwnerOrganizationName() {
+        return mDevicePolicyManager.getDeviceOwnerOrganizationName();
+    }
+
+    @Override
+    public CharSequence getWorkProfileOrganizationName() {
+        final int profileId = getWorkProfileUserId(mCurrentUserId);
+        if (profileId == UserHandle.USER_NULL) return null;
+        return mDevicePolicyManager.getOrganizationNameForUser(profileId);
+    }
+
+    @Override
+    public String getPrimaryVpnName() {
+        VpnConfig cfg = mCurrentVpns.get(mVpnUserId);
+        if (cfg != null) {
+            return getNameForVpnConfig(cfg, new UserHandle(mVpnUserId));
+        } else {
+            return null;
+        }
+    }
+
+    private int getWorkProfileUserId(int userId) {
+        for (final UserInfo userInfo : mUserManager.getProfiles(userId)) {
+            if (userInfo.isManagedProfile()) {
+                return userInfo.id;
+            }
+        }
+        return UserHandle.USER_NULL;
+    }
+
+    @Override
+    public boolean hasWorkProfile() {
+        return getWorkProfileUserId(mCurrentUserId) != UserHandle.USER_NULL;
+    }
+
+    @Override
+    public String getWorkProfileVpnName() {
+        final int profileId = getWorkProfileUserId(mVpnUserId);
+        if (profileId == UserHandle.USER_NULL) return null;
+        VpnConfig cfg = mCurrentVpns.get(profileId);
+        if (cfg != null) {
+            return getNameForVpnConfig(cfg, UserHandle.of(profileId));
+        }
+        return null;
+    }
+
+    @Override
+    public boolean isNetworkLoggingEnabled() {
+        return mDevicePolicyManager.isNetworkLoggingEnabled(null);
+    }
+
+    @Override
+    public boolean isVpnEnabled() {
+        for (int profileId : mUserManager.getProfileIdsWithDisabled(mVpnUserId)) {
+            if (mCurrentVpns.get(profileId) != null) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isVpnRestricted() {
+        UserHandle currentUser = new UserHandle(mCurrentUserId);
+        return mUserManager.getUserInfo(mCurrentUserId).isRestricted()
+                || mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN, currentUser);
+    }
+
+    @Override
+    public boolean isVpnBranded() {
+        VpnConfig cfg = mCurrentVpns.get(mVpnUserId);
+        if (cfg == null) {
+            return false;
+        }
+
+        String packageName = getPackageNameForVpnConfig(cfg);
+        if (packageName == null) {
+            return false;
+        }
+
+        return isVpnPackageBranded(packageName);
+    }
+
+    @Override
+    public boolean hasCACertInCurrentUser() {
+        Boolean hasCACerts = mHasCACerts.get(mCurrentUserId);
+        return hasCACerts != null && hasCACerts.booleanValue();
+    }
+
+    @Override
+    public boolean hasCACertInWorkProfile() {
+        int userId = getWorkProfileUserId(mCurrentUserId);
+        if (userId == UserHandle.USER_NULL) return false;
+        Boolean hasCACerts = mHasCACerts.get(userId);
+        return hasCACerts != null && hasCACerts.booleanValue();
+    }
+
+    @Override
+    public void removeCallback(SecurityControllerCallback callback) {
+        synchronized (mCallbacks) {
+            if (callback == null) return;
+            if (DEBUG) Log.d(TAG, "removeCallback " + callback);
+            mCallbacks.remove(callback);
+        }
+    }
+
+    @Override
+    public void addCallback(SecurityControllerCallback callback) {
+        synchronized (mCallbacks) {
+            if (callback == null || mCallbacks.contains(callback)) return;
+            if (DEBUG) Log.d(TAG, "addCallback " + callback);
+            mCallbacks.add(callback);
+        }
+    }
+
+    @Override
+    public void onUserSwitched(int newUserId) {
+        mCurrentUserId = newUserId;
+        final UserInfo newUserInfo = mUserManager.getUserInfo(newUserId);
+        if (newUserInfo.isRestricted()) {
+            // VPN for a restricted profile is routed through its owner user
+            mVpnUserId = newUserInfo.restrictedProfileParentId;
+        } else {
+            mVpnUserId = mCurrentUserId;
+        }
+        refreshCACerts();
+        fireCallbacks();
+    }
+
+    private void refreshCACerts() {
+        new CACertLoader().execute(mCurrentUserId);
+        int workProfileId = getWorkProfileUserId(mCurrentUserId);
+        if (workProfileId != UserHandle.USER_NULL) new CACertLoader().execute(workProfileId);
+    }
+
+    private String getNameForVpnConfig(VpnConfig cfg, UserHandle user) {
+        if (cfg.legacy) {
+            return mContext.getString(R.string.legacy_vpn_name);
+        }
+        // The package name for an active VPN is stored in the 'user' field of its VpnConfig
+        final String vpnPackage = cfg.user;
+        try {
+            Context userContext = mContext.createPackageContextAsUser(mContext.getPackageName(),
+                    0 /* flags */, user);
+            return VpnConfig.getVpnLabel(userContext, vpnPackage).toString();
+        } catch (NameNotFoundException nnfe) {
+            Log.e(TAG, "Package " + vpnPackage + " is not present", nnfe);
+            return null;
+        }
+    }
+
+    private void fireCallbacks() {
+        synchronized (mCallbacks) {
+            for (SecurityControllerCallback callback : mCallbacks) {
+                callback.onStateChanged();
+            }
+        }
+    }
+
+    private void updateState() {
+        // Find all users with an active VPN
+        SparseArray<VpnConfig> vpns = new SparseArray<>();
+        try {
+            for (UserInfo user : mUserManager.getUsers()) {
+                VpnConfig cfg = mConnectivityManagerService.getVpnConfig(user.id);
+                if (cfg == null) {
+                    continue;
+                } else if (cfg.legacy) {
+                    // Legacy VPNs should do nothing if the network is disconnected. Third-party
+                    // VPN warnings need to continue as traffic can still go to the app.
+                    LegacyVpnInfo legacyVpn = mConnectivityManagerService.getLegacyVpnInfo(user.id);
+                    if (legacyVpn == null || legacyVpn.state != LegacyVpnInfo.STATE_CONNECTED) {
+                        continue;
+                    }
+                }
+                vpns.put(user.id, cfg);
+            }
+        } catch (RemoteException rme) {
+            // Roll back to previous state
+            Log.e(TAG, "Unable to list active VPNs", rme);
+            return;
+        }
+        mCurrentVpns = vpns;
+    }
+
+    private String getPackageNameForVpnConfig(VpnConfig cfg) {
+        if (cfg.legacy) {
+            return null;
+        }
+        return cfg.user;
+    }
+
+    private boolean isVpnPackageBranded(String packageName) {
+        boolean isBranded;
+        try {
+            ApplicationInfo info = mPackageManager.getApplicationInfo(packageName,
+                PackageManager.GET_META_DATA);
+            if (info == null || info.metaData == null || !info.isSystemApp()) {
+                return false;
+            }
+            isBranded = info.metaData.getBoolean(VPN_BRANDED_META_DATA, false);
+        } catch (NameNotFoundException e) {
+            return false;
+        }
+        return isBranded;
+    }
+
+    private final NetworkCallback mNetworkCallback = new NetworkCallback() {
+        @Override
+        public void onAvailable(Network network) {
+            if (DEBUG) Log.d(TAG, "onAvailable " + network.netId);
+            updateState();
+            fireCallbacks();
+        };
+
+        // TODO Find another way to receive VPN lost.  This may be delayed depending on
+        // how long the VPN connection is held on to.
+        @Override
+        public void onLost(Network network) {
+            if (DEBUG) Log.d(TAG, "onLost " + network.netId);
+            updateState();
+            fireCallbacks();
+        };
+    };
+
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override public void onReceive(Context context, Intent intent) {
+            if (KeyChain.ACTION_TRUST_STORE_CHANGED.equals(intent.getAction())) {
+                refreshCACerts();
+            }
+        }
+    };
+
+    protected class CACertLoader extends AsyncTask<Integer, Void, Pair<Integer, Boolean> > {
+
+        @Override
+        protected Pair<Integer, Boolean> doInBackground(Integer... userId) {
+            try (KeyChainConnection conn = KeyChain.bindAsUser(mContext,
+                                                               UserHandle.of(userId[0]))) {
+                boolean hasCACerts = !(conn.getService().getUserCaAliases().getList().isEmpty());
+                return new Pair<Integer, Boolean>(userId[0], hasCACerts);
+            } catch (RemoteException | InterruptedException | AssertionError e) {
+                Log.i(TAG, e.getMessage());
+                new Handler(Dependency.get(Dependency.BG_LOOPER)).postDelayed(
+                        () -> new CACertLoader().execute(userId[0]),
+                        CA_CERT_LOADING_RETRY_TIME_IN_MS);
+                return new Pair<Integer, Boolean>(userId[0], null);
+            }
+        }
+
+        @Override
+        protected void onPostExecute(Pair<Integer, Boolean> result) {
+            if (DEBUG) Log.d(TAG, "onPostExecute " + result);
+            if (result.second != null) {
+                mHasCACerts.put(result.first, result.second);
+                fireCallbacks();
+            }
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/SignalController.java b/com/android/systemui/statusbar/policy/SignalController.java
new file mode 100644
index 0000000..91c208d
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/SignalController.java
@@ -0,0 +1,319 @@
+/*
+ * 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.policy;
+
+import android.content.Context;
+import android.text.format.DateFormat;
+import android.util.Log;
+import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+
+import java.io.PrintWriter;
+import java.util.BitSet;
+
+import static com.android.systemui.statusbar.policy.NetworkControllerImpl.TAG;
+
+
+/**
+ * Common base class for handling signal for both wifi and mobile data.
+ */
+public abstract class SignalController<T extends SignalController.State,
+        I extends SignalController.IconGroup> {
+    // Save the previous SignalController.States of all SignalControllers for dumps.
+    static final boolean RECORD_HISTORY = true;
+    // If RECORD_HISTORY how many to save, must be a power of 2.
+    static final int HISTORY_SIZE = 64;
+
+    protected static final boolean DEBUG = NetworkControllerImpl.DEBUG;
+    protected static final boolean CHATTY = NetworkControllerImpl.CHATTY;
+
+    protected final String mTag;
+    protected final T mCurrentState;
+    protected final T mLastState;
+    protected final int mTransportType;
+    protected final Context mContext;
+    // The owner of the SignalController (i.e. NetworkController will maintain the following
+    // lists and call notifyListeners whenever the list has changed to ensure everyone
+    // is aware of current state.
+    protected final NetworkControllerImpl mNetworkController;
+
+    private final CallbackHandler mCallbackHandler;
+
+    // Save the previous HISTORY_SIZE states for logging.
+    private final State[] mHistory;
+    // Where to copy the next state into.
+    private int mHistoryIndex;
+
+    public SignalController(String tag, Context context, int type, CallbackHandler callbackHandler,
+            NetworkControllerImpl networkController) {
+        mTag = TAG + "." + tag;
+        mNetworkController = networkController;
+        mTransportType = type;
+        mContext = context;
+        mCallbackHandler = callbackHandler;
+        mCurrentState = cleanState();
+        mLastState = cleanState();
+        if (RECORD_HISTORY) {
+            mHistory = new State[HISTORY_SIZE];
+            for (int i = 0; i < HISTORY_SIZE; i++) {
+                mHistory[i] = cleanState();
+            }
+        }
+    }
+
+    public T getState() {
+        return mCurrentState;
+    }
+
+    public void updateConnectivity(BitSet connectedTransports, BitSet validatedTransports) {
+        mCurrentState.inetCondition = validatedTransports.get(mTransportType) ? 1 : 0;
+        notifyListenersIfNecessary();
+    }
+
+    /**
+     * Used at the end of demo mode to clear out any ugly state that it has created.
+     * Since we haven't had any callbacks, then isDirty will not have been triggered,
+     * so we can just take the last good state directly from there.
+     *
+     * Used for demo mode.
+     */
+    public void resetLastState() {
+        mCurrentState.copyFrom(mLastState);
+    }
+
+    /**
+     * Determines if the state of this signal controller has changed and
+     * needs to trigger callbacks related to it.
+     */
+    public boolean isDirty() {
+        if (!mLastState.equals(mCurrentState)) {
+            if (DEBUG) {
+                Log.d(mTag, "Change in state from: " + mLastState + "\n"
+                        + "\tto: " + mCurrentState);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    public void saveLastState() {
+        if (RECORD_HISTORY) {
+            recordLastState();
+        }
+        // Updates the current time.
+        mCurrentState.time = System.currentTimeMillis();
+        mLastState.copyFrom(mCurrentState);
+    }
+
+    /**
+     * Gets the signal icon for QS based on current state of connected, enabled, and level.
+     */
+    public int getQsCurrentIconId() {
+        if (mCurrentState.connected) {
+            return getIcons().mQsIcons[mCurrentState.inetCondition][mCurrentState.level];
+        } else if (mCurrentState.enabled) {
+            return getIcons().mQsDiscState;
+        } else {
+            return getIcons().mQsNullState;
+        }
+    }
+
+    /**
+     * Gets the signal icon for SB based on current state of connected, enabled, and level.
+     */
+    public int getCurrentIconId() {
+        if (mCurrentState.connected) {
+            return getIcons().mSbIcons[mCurrentState.inetCondition][mCurrentState.level];
+        } else if (mCurrentState.enabled) {
+            return getIcons().mSbDiscState;
+        } else {
+            return getIcons().mSbNullState;
+        }
+    }
+
+    /**
+     * Gets the content description id for the signal based on current state of connected and
+     * level.
+     */
+    public int getContentDescription() {
+        if (mCurrentState.connected) {
+            return getIcons().mContentDesc[mCurrentState.level];
+        } else {
+            return getIcons().mDiscContentDesc;
+        }
+    }
+
+    public void notifyListenersIfNecessary() {
+        if (isDirty()) {
+            saveLastState();
+            notifyListeners();
+        }
+    }
+
+    /**
+     * Returns the resource if resId is not 0, and an empty string otherwise.
+     */
+    protected String getStringIfExists(int resId) {
+        return resId != 0 ? mContext.getString(resId) : "";
+    }
+
+    protected I getIcons() {
+        return (I) mCurrentState.iconGroup;
+    }
+
+    /**
+     * Saves the last state of any changes, so we can log the current
+     * and last value of any state data.
+     */
+    protected void recordLastState() {
+        mHistory[mHistoryIndex++ & (HISTORY_SIZE - 1)].copyFrom(mLastState);
+    }
+
+    public void dump(PrintWriter pw) {
+        pw.println("  - " + mTag + " -----");
+        pw.println("  Current State: " + mCurrentState);
+        if (RECORD_HISTORY) {
+            // Count up the states that actually contain time stamps, and only display those.
+            int size = 0;
+            for (int i = 0; i < HISTORY_SIZE; i++) {
+                if (mHistory[i].time != 0) size++;
+            }
+            // Print out the previous states in ordered number.
+            for (int i = mHistoryIndex + HISTORY_SIZE - 1;
+                    i >= mHistoryIndex + HISTORY_SIZE - size; i--) {
+                pw.println("  Previous State(" + (mHistoryIndex + HISTORY_SIZE - i) + "): "
+                        + mHistory[i & (HISTORY_SIZE - 1)]);
+            }
+        }
+    }
+
+    public final void notifyListeners() {
+        notifyListeners(mCallbackHandler);
+    }
+
+    /**
+     * Trigger callbacks based on current state.  The callbacks should be completely
+     * based on current state, and only need to be called in the scenario where
+     * mCurrentState != mLastState.
+     */
+    public abstract void notifyListeners(SignalCallback callback);
+
+    /**
+     * Generate a blank T.
+     */
+    protected abstract T cleanState();
+
+    /*
+     * Holds icons for a given state. Arrays are generally indexed as inet
+     * state (full connectivity or not) first, and second dimension as
+     * signal strength.
+     */
+    static class IconGroup {
+        final int[][] mSbIcons;
+        final int[][] mQsIcons;
+        final int[] mContentDesc;
+        final int mSbNullState;
+        final int mQsNullState;
+        final int mSbDiscState;
+        final int mQsDiscState;
+        final int mDiscContentDesc;
+        // For logging.
+        final String mName;
+
+        public IconGroup(String name, int[][] sbIcons, int[][] qsIcons, int[] contentDesc,
+                int sbNullState, int qsNullState, int sbDiscState, int qsDiscState,
+                int discContentDesc) {
+            mName = name;
+            mSbIcons = sbIcons;
+            mQsIcons = qsIcons;
+            mContentDesc = contentDesc;
+            mSbNullState = sbNullState;
+            mQsNullState = qsNullState;
+            mSbDiscState = sbDiscState;
+            mQsDiscState = qsDiscState;
+            mDiscContentDesc = discContentDesc;
+        }
+
+        @Override
+        public String toString() {
+            return "IconGroup(" + mName + ")";
+        }
+    }
+
+    static class State {
+        boolean connected;
+        boolean enabled;
+        boolean activityIn;
+        boolean activityOut;
+        int level;
+        IconGroup iconGroup;
+        int inetCondition;
+        int rssi; // Only for logging.
+
+        // Not used for comparison, just used for logging.
+        long time;
+
+        public void copyFrom(State state) {
+            connected = state.connected;
+            enabled = state.enabled;
+            level = state.level;
+            iconGroup = state.iconGroup;
+            inetCondition = state.inetCondition;
+            activityIn = state.activityIn;
+            activityOut = state.activityOut;
+            rssi = state.rssi;
+            time = state.time;
+        }
+
+        @Override
+        public String toString() {
+            if (time != 0) {
+                StringBuilder builder = new StringBuilder();
+                toString(builder);
+                return builder.toString();
+            } else {
+                return "Empty " + getClass().getSimpleName();
+            }
+        }
+
+        protected void toString(StringBuilder builder) {
+            builder.append("connected=").append(connected).append(',')
+                    .append("enabled=").append(enabled).append(',')
+                    .append("level=").append(level).append(',')
+                    .append("inetCondition=").append(inetCondition).append(',')
+                    .append("iconGroup=").append(iconGroup).append(',')
+                    .append("activityIn=").append(activityIn).append(',')
+                    .append("activityOut=").append(activityOut).append(',')
+                    .append("rssi=").append(rssi).append(',')
+                    .append("lastModified=").append(DateFormat.format("MM-dd HH:mm:ss", time));
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!o.getClass().equals(getClass())) {
+                return false;
+            }
+            State other = (State) o;
+            return other.connected == connected
+                    && other.enabled == enabled
+                    && other.level == level
+                    && other.inetCondition == inetCondition
+                    && other.iconGroup == iconGroup
+                    && other.activityIn == activityIn
+                    && other.activityOut == activityOut
+                    && other.rssi == rssi;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/SplitClockView.java b/com/android/systemui/statusbar/policy/SplitClockView.java
new file mode 100644
index 0000000..9f61574
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/SplitClockView.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.UserHandle;
+import android.text.format.DateFormat;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+import android.widget.TextClock;
+
+import com.android.systemui.R;
+
+/**
+ * Container for a clock which has two separate views for the clock itself and AM/PM indicator. This
+ * is used to scale the clock independently of AM/PM.
+ */
+public class SplitClockView extends LinearLayout {
+
+    private TextClock mTimeView;
+    private TextClock mAmPmView;
+
+    private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (Intent.ACTION_TIME_CHANGED.equals(action)
+                    || Intent.ACTION_TIMEZONE_CHANGED.equals(action)
+                    || Intent.ACTION_LOCALE_CHANGED.equals(action)
+                    || Intent.ACTION_CONFIGURATION_CHANGED.equals(action)
+                    || Intent.ACTION_USER_SWITCHED.equals(action)) {
+                updatePatterns();
+            }
+        }
+    };
+
+    public SplitClockView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mTimeView = findViewById(R.id.time_view);
+        mAmPmView = findViewById(R.id.am_pm_view);
+        mTimeView.setShowCurrentUserTime(true);
+        mAmPmView.setShowCurrentUserTime(true);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_TIME_CHANGED);
+        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+        filter.addAction(Intent.ACTION_LOCALE_CHANGED);
+        filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+        filter.addAction(Intent.ACTION_USER_SWITCHED);
+        getContext().registerReceiverAsUser(mIntentReceiver, UserHandle.ALL, filter, null, null);
+
+        updatePatterns();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        getContext().unregisterReceiver(mIntentReceiver);
+    }
+
+    private void updatePatterns() {
+        String formatString = DateFormat.getTimeFormatString(getContext(),
+                ActivityManager.getCurrentUser());
+        int index = getAmPmPartEndIndex(formatString);
+        String timeString;
+        String amPmString;
+        if (index == -1) {
+            timeString = formatString;
+            amPmString = "";
+        } else {
+            timeString = formatString.substring(0, index);
+            amPmString = formatString.substring(index);
+        }
+        mTimeView.setFormat12Hour(timeString);
+        mTimeView.setFormat24Hour(timeString);
+        mTimeView.setContentDescriptionFormat12Hour(formatString);
+        mTimeView.setContentDescriptionFormat24Hour(formatString);
+        mAmPmView.setFormat12Hour(amPmString);
+        mAmPmView.setFormat24Hour(amPmString);
+    }
+
+    /**
+     * @return the index where the AM/PM part starts at the end in {@code formatString} including
+     *         leading white spaces or {@code -1} if no AM/PM part is found or {@code formatString}
+     *         doesn't end with AM/PM part
+     */
+    private static int getAmPmPartEndIndex(String formatString) {
+        boolean hasAmPm = false;
+        int length = formatString.length();
+        for (int i = length - 1; i >= 0; i--) {
+            char c = formatString.charAt(i);
+            boolean isAmPm = c == 'a';
+            boolean isWhitespace = Character.isWhitespace(c);
+            if (isAmPm) {
+                hasAmPm = true;
+            }
+            if (isAmPm || isWhitespace) {
+                continue;
+            }
+            if (i == length - 1) {
+
+                // First character was not AM/PM and not whitespace, so it's not ending with AM/PM.
+                return -1;
+            } else {
+
+                // If we have AM/PM at all, return last index, or -1 to indicate that it's not
+                // ending with AM/PM.
+                return hasAmPm ? i + 1 : -1;
+            }
+        }
+
+        // Only AM/PM and whitespaces? The whole string is AM/PM. Else: Only whitespaces in the
+        // string.
+        return hasAmPm ? 0 : -1;
+    }
+
+}
diff --git a/com/android/systemui/statusbar/policy/TelephonyIcons.java b/com/android/systemui/statusbar/policy/TelephonyIcons.java
new file mode 100644
index 0000000..aaa0568
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/TelephonyIcons.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2008 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.policy;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.MobileSignalController.MobileIconGroup;
+
+class TelephonyIcons {
+    //***** Data connection icons
+
+    static final int QS_DATA_G = R.drawable.ic_qs_signal_g;
+    static final int QS_DATA_3G = R.drawable.ic_qs_signal_3g;
+    static final int QS_DATA_E = R.drawable.ic_qs_signal_e;
+    static final int QS_DATA_H = R.drawable.ic_qs_signal_h;
+    static final int QS_DATA_1X = R.drawable.ic_qs_signal_1x;
+    static final int QS_DATA_4G = R.drawable.ic_qs_signal_4g;
+    static final int QS_DATA_4G_PLUS = R.drawable.ic_qs_signal_4g_plus;
+    static final int QS_DATA_LTE = R.drawable.ic_qs_signal_lte;
+    static final int QS_DATA_LTE_PLUS = R.drawable.ic_qs_signal_lte_plus;
+
+    static final int FLIGHT_MODE_ICON = R.drawable.stat_sys_airplane_mode;
+
+    static final int ICON_LTE = R.drawable.stat_sys_data_fully_connected_lte;
+    static final int ICON_LTE_PLUS = R.drawable.stat_sys_data_fully_connected_lte_plus;
+    static final int ICON_G = R.drawable.stat_sys_data_fully_connected_g;
+    static final int ICON_E = R.drawable.stat_sys_data_fully_connected_e;
+    static final int ICON_H = R.drawable.stat_sys_data_fully_connected_h;
+    static final int ICON_3G = R.drawable.stat_sys_data_fully_connected_3g;
+    static final int ICON_4G = R.drawable.stat_sys_data_fully_connected_4g;
+    static final int ICON_4G_PLUS = R.drawable.stat_sys_data_fully_connected_4g_plus;
+    static final int ICON_1X = R.drawable.stat_sys_data_fully_connected_1x;
+
+    static final int ICON_DATA_DISABLED = R.drawable.stat_sys_data_disabled;
+
+    static final int QS_ICON_DATA_DISABLED = R.drawable.ic_qs_data_disabled;
+
+    static final MobileIconGroup CARRIER_NETWORK_CHANGE = new MobileIconGroup(
+            "CARRIER_NETWORK_CHANGE",
+            null,
+            null,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+            0, 0,
+            0,
+            0,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+            R.string.accessibility_carrier_network_change_mode,
+            0,
+            false,
+            0
+            );
+
+    static final MobileIconGroup THREE_G = new MobileIconGroup(
+            "3G",
+            null,
+            null,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+            0, 0,
+            0,
+            0,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+            R.string.accessibility_data_connection_3g,
+            TelephonyIcons.ICON_3G,
+            true,
+            TelephonyIcons.QS_DATA_3G
+            );
+
+    static final MobileIconGroup WFC = new MobileIconGroup(
+            "WFC",
+            null,
+            null,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+            0, 0,
+            0,
+            0,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+            0, 0, false, 0
+            );
+
+    static final MobileIconGroup UNKNOWN = new MobileIconGroup(
+            "Unknown",
+            null,
+            null,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+            0, 0,
+            0,
+            0,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+            0, 0, false, 0
+            );
+
+    static final MobileIconGroup E = new MobileIconGroup(
+            "E",
+            null,
+            null,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+            0, 0,
+            0,
+            0,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+            R.string.accessibility_data_connection_edge,
+            TelephonyIcons.ICON_E,
+            false,
+            TelephonyIcons.QS_DATA_E
+            );
+
+    static final MobileIconGroup ONE_X = new MobileIconGroup(
+            "1X",
+            null,
+            null,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+            0, 0,
+            0,
+            0,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+            R.string.accessibility_data_connection_cdma,
+            TelephonyIcons.ICON_1X,
+            true,
+            TelephonyIcons.QS_DATA_1X
+            );
+
+    static final MobileIconGroup G = new MobileIconGroup(
+            "G",
+            null,
+            null,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+            0, 0,
+            0,
+            0,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+            R.string.accessibility_data_connection_gprs,
+            TelephonyIcons.ICON_G,
+            false,
+            TelephonyIcons.QS_DATA_G
+            );
+
+    static final MobileIconGroup H = new MobileIconGroup(
+            "H",
+            null,
+            null,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+            0, 0,
+            0,
+            0,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+            R.string.accessibility_data_connection_3_5g,
+            TelephonyIcons.ICON_H,
+            false,
+            TelephonyIcons.QS_DATA_H
+            );
+
+    static final MobileIconGroup FOUR_G = new MobileIconGroup(
+            "4G",
+            null,
+            null,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+            0, 0,
+            0,
+            0,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+            R.string.accessibility_data_connection_4g,
+            TelephonyIcons.ICON_4G,
+            true,
+            TelephonyIcons.QS_DATA_4G
+            );
+
+    static final MobileIconGroup FOUR_G_PLUS = new MobileIconGroup(
+            "4G+",
+            null,
+            null,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+            0,0,
+            0,
+            0,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+            R.string.accessibility_data_connection_4g_plus,
+            TelephonyIcons.ICON_4G_PLUS,
+            true,
+            TelephonyIcons.QS_DATA_4G_PLUS
+            );
+
+    static final MobileIconGroup LTE = new MobileIconGroup(
+            "LTE",
+            null,
+            null,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+            0, 0,
+            0,
+            0,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+            R.string.accessibility_data_connection_lte,
+            TelephonyIcons.ICON_LTE,
+            true,
+            TelephonyIcons.QS_DATA_LTE
+            );
+
+    static final MobileIconGroup LTE_PLUS = new MobileIconGroup(
+            "LTE+",
+            null,
+            null,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+            0, 0,
+            0,
+            0,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+            R.string.accessibility_data_connection_lte_plus,
+            TelephonyIcons.ICON_LTE_PLUS,
+            true,
+            TelephonyIcons.QS_DATA_LTE_PLUS
+            );
+
+    static final MobileIconGroup DATA_DISABLED = new MobileIconGroup(
+            "DataDisabled",
+            null,
+            null,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
+            0, 0,
+            0,
+            0,
+            AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
+            R.string.accessibility_cell_data_off,
+            TelephonyIcons.ICON_DATA_DISABLED,
+            false,
+            TelephonyIcons.QS_ICON_DATA_DISABLED
+            );
+}
+
diff --git a/com/android/systemui/statusbar/policy/UserInfoController.java b/com/android/systemui/statusbar/policy/UserInfoController.java
new file mode 100644
index 0000000..1e23a20
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/UserInfoController.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016 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.policy;
+
+import android.graphics.drawable.Drawable;
+
+import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener;
+
+public interface UserInfoController extends CallbackController<OnUserInfoChangedListener> {
+
+    void reloadUserInfo();
+
+    public interface OnUserInfoChangedListener {
+        public void onUserInfoChanged(String name, Drawable picture, String userAccount);
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java b/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
new file mode 100644
index 0000000..527addf
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import com.android.internal.util.UserIcons;
+import com.android.settingslib.drawable.UserIconDrawable;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener;
+
+import java.util.ArrayList;
+
+public class UserInfoControllerImpl implements UserInfoController {
+
+    private static final String TAG = "UserInfoController";
+
+    private final Context mContext;
+    private final ArrayList<OnUserInfoChangedListener> mCallbacks =
+            new ArrayList<OnUserInfoChangedListener>();
+    private AsyncTask<Void, Void, UserInfoQueryResult> mUserInfoTask;
+
+    private String mUserName;
+    private Drawable mUserDrawable;
+    private String mUserAccount;
+
+    public UserInfoControllerImpl(Context context) {
+        mContext = context;
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_USER_SWITCHED);
+        mContext.registerReceiver(mReceiver, filter);
+
+        IntentFilter profileFilter = new IntentFilter();
+        profileFilter.addAction(ContactsContract.Intents.ACTION_PROFILE_CHANGED);
+        profileFilter.addAction(Intent.ACTION_USER_INFO_CHANGED);
+        mContext.registerReceiverAsUser(mProfileReceiver, UserHandle.ALL, profileFilter,
+                null, null);
+    }
+
+    public void addCallback(OnUserInfoChangedListener callback) {
+        mCallbacks.add(callback);
+        callback.onUserInfoChanged(mUserName, mUserDrawable, mUserAccount);
+    }
+
+    public void removeCallback(OnUserInfoChangedListener callback) {
+        mCallbacks.remove(callback);
+    }
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (Intent.ACTION_USER_SWITCHED.equals(action)) {
+                reloadUserInfo();
+            }
+        }
+    };
+
+    private final BroadcastReceiver mProfileReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (ContactsContract.Intents.ACTION_PROFILE_CHANGED.equals(action) ||
+                    Intent.ACTION_USER_INFO_CHANGED.equals(action)) {
+                try {
+                    final int currentUser = ActivityManager.getService().getCurrentUser().id;
+                    final int changedUser =
+                            intent.getIntExtra(Intent.EXTRA_USER_HANDLE, getSendingUserId());
+                    if (changedUser == currentUser) {
+                        reloadUserInfo();
+                    }
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Couldn't get current user id for profile change", e);
+                }
+            }
+        }
+    };
+
+    public void reloadUserInfo() {
+        if (mUserInfoTask != null) {
+            mUserInfoTask.cancel(false);
+            mUserInfoTask = null;
+        }
+        queryForUserInformation();
+    }
+
+    private void queryForUserInformation() {
+        Context currentUserContext;
+        UserInfo userInfo;
+        try {
+            userInfo = ActivityManager.getService().getCurrentUser();
+            currentUserContext = mContext.createPackageContextAsUser("android", 0,
+                    new UserHandle(userInfo.id));
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "Couldn't create user context", e);
+            throw new RuntimeException(e);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Couldn't get user info", e);
+            throw new RuntimeException(e);
+        }
+        final int userId = userInfo.id;
+        final boolean isGuest = userInfo.isGuest();
+        final String userName = userInfo.name;
+        final boolean lightIcon = mContext.getThemeResId() != R.style.Theme_SystemUI_Light;
+
+        final Resources res = mContext.getResources();
+        final int avatarSize = Math.max(
+                res.getDimensionPixelSize(R.dimen.multi_user_avatar_expanded_size),
+                res.getDimensionPixelSize(R.dimen.multi_user_avatar_keyguard_size));
+
+        final Context context = currentUserContext;
+        mUserInfoTask = new AsyncTask<Void, Void, UserInfoQueryResult>() {
+
+            @Override
+            protected UserInfoQueryResult doInBackground(Void... params) {
+                final UserManager um = UserManager.get(mContext);
+
+                // Fall back to the UserManager nickname if we can't read the name from the local
+                // profile below.
+                String name = userName;
+                Drawable avatar = null;
+                Bitmap rawAvatar = um.getUserIcon(userId);
+                if (rawAvatar != null) {
+                    avatar = new UserIconDrawable(avatarSize)
+                            .setIcon(rawAvatar).setBadgeIfManagedUser(mContext, userId).bake();
+                } else {
+                    avatar = UserIcons.getDefaultUserIcon(isGuest? UserHandle.USER_NULL : userId,
+                            lightIcon);
+                }
+
+                // If it's a single-user device, get the profile name, since the nickname is not
+                // usually valid
+                if (um.getUsers().size() <= 1) {
+                    // Try and read the display name from the local profile
+                    final Cursor cursor = context.getContentResolver().query(
+                            ContactsContract.Profile.CONTENT_URI, new String[] {
+                                    ContactsContract.CommonDataKinds.Phone._ID,
+                                    ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME
+                            }, null, null, null);
+                    if (cursor != null) {
+                        try {
+                            if (cursor.moveToFirst()) {
+                                name = cursor.getString(cursor.getColumnIndex(
+                                        ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
+                            }
+                        } finally {
+                            cursor.close();
+                        }
+                    }
+                }
+                String userAccount = um.getUserAccount(userId);
+                return new UserInfoQueryResult(name, avatar, userAccount);
+            }
+
+            @Override
+            protected void onPostExecute(UserInfoQueryResult result) {
+                mUserName = result.getName();
+                mUserDrawable = result.getAvatar();
+                mUserAccount = result.getUserAccount();
+                mUserInfoTask = null;
+                notifyChanged();
+            }
+        };
+        mUserInfoTask.execute();
+    }
+
+    private void notifyChanged() {
+        for (OnUserInfoChangedListener listener : mCallbacks) {
+            listener.onUserInfoChanged(mUserName, mUserDrawable, mUserAccount);
+        }
+    }
+
+    public void onDensityOrFontScaleChanged() {
+        reloadUserInfo();
+    }
+
+    private static class UserInfoQueryResult {
+        private String mName;
+        private Drawable mAvatar;
+        private String mUserAccount;
+
+        public UserInfoQueryResult(String name, Drawable avatar, String userAccount) {
+            mName = name;
+            mAvatar = avatar;
+            mUserAccount = userAccount;
+        }
+
+        public String getName() {
+            return mName;
+        }
+
+        public Drawable getAvatar() {
+            return mAvatar;
+        }
+
+        public String getUserAccount() {
+            return mUserAccount;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/UserSwitcherController.java b/com/android/systemui/statusbar/policy/UserSwitcherController.java
new file mode 100644
index 0000000..700c01a
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -0,0 +1,966 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
+
+import android.R.attr;
+import android.app.ActivityManager;
+import android.app.Dialog;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.UserInfo;
+import android.database.ContentObserver;
+import android.graphics.Bitmap;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.internal.util.UserIcons;
+import com.android.settingslib.RestrictedLockUtils;
+import com.android.settingslib.Utils;
+import com.android.systemui.Dependency;
+import com.android.systemui.GuestResumeSessionReceiver;
+import com.android.systemui.Prefs;
+import com.android.systemui.Prefs.Key;
+import com.android.systemui.R;
+import com.android.systemui.SystemUI;
+import com.android.systemui.SystemUISecondaryUserService;
+import com.android.systemui.plugins.qs.DetailAdapter;
+import com.android.systemui.qs.tiles.UserDetailView;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+import com.android.systemui.util.NotificationChannels;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Keeps a list of all users on the device for user switching.
+ */
+public class UserSwitcherController {
+
+    private static final String TAG = "UserSwitcherController";
+    private static final boolean DEBUG = false;
+    private static final String SIMPLE_USER_SWITCHER_GLOBAL_SETTING =
+            "lockscreenSimpleUserSwitcher";
+    private static final String ACTION_REMOVE_GUEST = "com.android.systemui.REMOVE_GUEST";
+    private static final String ACTION_LOGOUT_USER = "com.android.systemui.LOGOUT_USER";
+    private static final int PAUSE_REFRESH_USERS_TIMEOUT_MS = 3000;
+
+    private static final String TAG_REMOVE_GUEST = "remove_guest";
+    private static final String TAG_LOGOUT_USER = "logout_user";
+
+    private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
+
+    protected final Context mContext;
+    protected final UserManager mUserManager;
+    private final ArrayList<WeakReference<BaseUserAdapter>> mAdapters = new ArrayList<>();
+    private final GuestResumeSessionReceiver mGuestResumeSessionReceiver
+            = new GuestResumeSessionReceiver();
+    private final KeyguardMonitor mKeyguardMonitor;
+    protected final Handler mHandler;
+    private final ActivityStarter mActivityStarter;
+
+    private ArrayList<UserRecord> mUsers = new ArrayList<>();
+    private Dialog mExitGuestDialog;
+    private Dialog mAddUserDialog;
+    private int mLastNonGuestUser = UserHandle.USER_SYSTEM;
+    private boolean mResumeUserOnGuestLogout = true;
+    private boolean mSimpleUserSwitcher;
+    private boolean mAddUsersWhenLocked;
+    private boolean mPauseRefreshUsers;
+    private int mSecondaryUser = UserHandle.USER_NULL;
+    private Intent mSecondaryUserServiceIntent;
+    private SparseBooleanArray mForcePictureLoadForUserId = new SparseBooleanArray(2);
+
+    public UserSwitcherController(Context context, KeyguardMonitor keyguardMonitor,
+            Handler handler, ActivityStarter activityStarter) {
+        mContext = context;
+        mGuestResumeSessionReceiver.register(context);
+        mKeyguardMonitor = keyguardMonitor;
+        mHandler = handler;
+        mActivityStarter = activityStarter;
+        mUserManager = UserManager.get(context);
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_USER_ADDED);
+        filter.addAction(Intent.ACTION_USER_REMOVED);
+        filter.addAction(Intent.ACTION_USER_INFO_CHANGED);
+        filter.addAction(Intent.ACTION_USER_SWITCHED);
+        filter.addAction(Intent.ACTION_USER_STOPPED);
+        filter.addAction(Intent.ACTION_USER_UNLOCKED);
+        mContext.registerReceiverAsUser(mReceiver, UserHandle.SYSTEM, filter,
+                null /* permission */, null /* scheduler */);
+
+        mSecondaryUserServiceIntent = new Intent(context, SystemUISecondaryUserService.class);
+
+        filter = new IntentFilter();
+        filter.addAction(ACTION_REMOVE_GUEST);
+        filter.addAction(ACTION_LOGOUT_USER);
+        mContext.registerReceiverAsUser(mReceiver, UserHandle.SYSTEM, filter,
+                PERMISSION_SELF, null /* scheduler */);
+
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Global.getUriFor(SIMPLE_USER_SWITCHER_GLOBAL_SETTING), true,
+                mSettingsObserver);
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Global.getUriFor(Settings.Global.ADD_USERS_WHEN_LOCKED), true,
+                mSettingsObserver);
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Global.getUriFor(
+                        Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED),
+                true, mSettingsObserver);
+        // Fetch initial values.
+        mSettingsObserver.onChange(false);
+
+        keyguardMonitor.addCallback(mCallback);
+        listenForCallState();
+
+        refreshUsers(UserHandle.USER_NULL);
+    }
+
+    /**
+     * Refreshes users from UserManager.
+     *
+     * The pictures are only loaded if they have not been loaded yet.
+     *
+     * @param forcePictureLoadForId forces the picture of the given user to be reloaded.
+     */
+    @SuppressWarnings("unchecked")
+    private void refreshUsers(int forcePictureLoadForId) {
+        if (DEBUG) Log.d(TAG, "refreshUsers(forcePictureLoadForId=" + forcePictureLoadForId+")");
+        if (forcePictureLoadForId != UserHandle.USER_NULL) {
+            mForcePictureLoadForUserId.put(forcePictureLoadForId, true);
+        }
+
+        if (mPauseRefreshUsers) {
+            return;
+        }
+
+        boolean forceAllUsers = mForcePictureLoadForUserId.get(UserHandle.USER_ALL);
+        SparseArray<Bitmap> bitmaps = new SparseArray<>(mUsers.size());
+        final int N = mUsers.size();
+        for (int i = 0; i < N; i++) {
+            UserRecord r = mUsers.get(i);
+            if (r == null || r.picture == null || r.info == null || forceAllUsers
+                    || mForcePictureLoadForUserId.get(r.info.id)) {
+                continue;
+            }
+            bitmaps.put(r.info.id, r.picture);
+        }
+        mForcePictureLoadForUserId.clear();
+
+        final boolean addUsersWhenLocked = mAddUsersWhenLocked;
+        new AsyncTask<SparseArray<Bitmap>, Void, ArrayList<UserRecord>>() {
+            @SuppressWarnings("unchecked")
+            @Override
+            protected ArrayList<UserRecord> doInBackground(SparseArray<Bitmap>... params) {
+                final SparseArray<Bitmap> bitmaps = params[0];
+                List<UserInfo> infos = mUserManager.getUsers(true);
+                if (infos == null) {
+                    return null;
+                }
+                ArrayList<UserRecord> records = new ArrayList<>(infos.size());
+                int currentId = ActivityManager.getCurrentUser();
+                boolean canSwitchUsers = mUserManager.canSwitchUsers();
+                UserInfo currentUserInfo = null;
+                UserRecord guestRecord = null;
+
+                for (UserInfo info : infos) {
+                    boolean isCurrent = currentId == info.id;
+                    if (isCurrent) {
+                        currentUserInfo = info;
+                    }
+                    boolean switchToEnabled = canSwitchUsers || isCurrent;
+                    if (info.isEnabled()) {
+                        if (info.isGuest()) {
+                            // Tapping guest icon triggers remove and a user switch therefore
+                            // the icon shouldn't be enabled even if the user is current
+                            guestRecord = new UserRecord(info, null /* picture */,
+                                    true /* isGuest */, isCurrent, false /* isAddUser */,
+                                    false /* isRestricted */, canSwitchUsers);
+                        } else if (info.supportsSwitchToByUser()) {
+                            Bitmap picture = bitmaps.get(info.id);
+                            if (picture == null) {
+                                picture = mUserManager.getUserIcon(info.id);
+
+                                if (picture != null) {
+                                    int avatarSize = mContext.getResources()
+                                            .getDimensionPixelSize(R.dimen.max_avatar_size);
+                                    picture = Bitmap.createScaledBitmap(
+                                            picture, avatarSize, avatarSize, true);
+                                }
+                            }
+                            int index = isCurrent ? 0 : records.size();
+                            records.add(index, new UserRecord(info, picture, false /* isGuest */,
+                                    isCurrent, false /* isAddUser */, false /* isRestricted */,
+                                    switchToEnabled));
+                        }
+                    }
+                }
+                if (records.size() > 1 || guestRecord != null) {
+                    Prefs.putBoolean(mContext, Key.SEEN_MULTI_USER, true);
+                }
+
+                boolean systemCanCreateUsers = !mUserManager.hasBaseUserRestriction(
+                                UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM);
+                boolean currentUserCanCreateUsers = currentUserInfo != null
+                        && (currentUserInfo.isAdmin()
+                                || currentUserInfo.id == UserHandle.USER_SYSTEM)
+                        && systemCanCreateUsers;
+                boolean anyoneCanCreateUsers = systemCanCreateUsers && addUsersWhenLocked;
+                boolean canCreateGuest = (currentUserCanCreateUsers || anyoneCanCreateUsers)
+                        && guestRecord == null;
+                boolean canCreateUser = (currentUserCanCreateUsers || anyoneCanCreateUsers)
+                        && mUserManager.canAddMoreUsers();
+                boolean createIsRestricted = !addUsersWhenLocked;
+
+                if (!mSimpleUserSwitcher) {
+                    if (guestRecord == null) {
+                        if (canCreateGuest) {
+                            guestRecord = new UserRecord(null /* info */, null /* picture */,
+                                    true /* isGuest */, false /* isCurrent */,
+                                    false /* isAddUser */, createIsRestricted, canSwitchUsers);
+                            checkIfAddUserDisallowedByAdminOnly(guestRecord);
+                            records.add(guestRecord);
+                        }
+                    } else {
+                        int index = guestRecord.isCurrent ? 0 : records.size();
+                        records.add(index, guestRecord);
+                    }
+                }
+
+                if (!mSimpleUserSwitcher && canCreateUser) {
+                    UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */,
+                            false /* isGuest */, false /* isCurrent */, true /* isAddUser */,
+                            createIsRestricted, canSwitchUsers);
+                    checkIfAddUserDisallowedByAdminOnly(addUserRecord);
+                    records.add(addUserRecord);
+                }
+
+                return records;
+            }
+
+            @Override
+            protected void onPostExecute(ArrayList<UserRecord> userRecords) {
+                if (userRecords != null) {
+                    mUsers = userRecords;
+                    notifyAdapters();
+                }
+            }
+        }.execute((SparseArray) bitmaps);
+    }
+
+    private void pauseRefreshUsers() {
+        if (!mPauseRefreshUsers) {
+            mHandler.postDelayed(mUnpauseRefreshUsers, PAUSE_REFRESH_USERS_TIMEOUT_MS);
+            mPauseRefreshUsers = true;
+        }
+    }
+
+    private void notifyAdapters() {
+        for (int i = mAdapters.size() - 1; i >= 0; i--) {
+            BaseUserAdapter adapter = mAdapters.get(i).get();
+            if (adapter != null) {
+                adapter.notifyDataSetChanged();
+            } else {
+                mAdapters.remove(i);
+            }
+        }
+    }
+
+    public boolean isSimpleUserSwitcher() {
+        return mSimpleUserSwitcher;
+    }
+
+    public boolean useFullscreenUserSwitcher() {
+        // Use adb to override:
+        // adb shell settings put system enable_fullscreen_user_switcher 0  # Turn it off.
+        // adb shell settings put system enable_fullscreen_user_switcher 1  # Turn it on.
+        // Restart SystemUI or adb reboot.
+        final int DEFAULT = -1;
+        final int overrideUseFullscreenUserSwitcher =
+                Settings.System.getInt(mContext.getContentResolver(),
+                        "enable_fullscreen_user_switcher", DEFAULT);
+        if (overrideUseFullscreenUserSwitcher != DEFAULT) {
+            return overrideUseFullscreenUserSwitcher != 0;
+        }
+        // Otherwise default to the build setting.
+        return mContext.getResources().getBoolean(R.bool.config_enableFullscreenUserSwitcher);
+    }
+
+    public void setResumeUserOnGuestLogout(boolean resume) {
+        mResumeUserOnGuestLogout = resume;
+    }
+
+    public void logoutCurrentUser() {
+        int currentUser = ActivityManager.getCurrentUser();
+        if (currentUser != UserHandle.USER_SYSTEM) {
+            pauseRefreshUsers();
+            ActivityManager.logoutCurrentUser();
+        }
+    }
+
+    public void removeUserId(int userId) {
+        if (userId == UserHandle.USER_SYSTEM) {
+            Log.w(TAG, "User " + userId + " could not removed.");
+            return;
+        }
+        if (ActivityManager.getCurrentUser() == userId) {
+            switchToUserId(UserHandle.USER_SYSTEM);
+        }
+        if (mUserManager.removeUser(userId)) {
+            refreshUsers(UserHandle.USER_NULL);
+        }
+    }
+
+    public void switchTo(UserRecord record) {
+        int id;
+        if (record.isGuest && record.info == null) {
+            // No guest user. Create one.
+            UserInfo guest = mUserManager.createGuest(
+                    mContext, mContext.getString(R.string.guest_nickname));
+            if (guest == null) {
+                // Couldn't create guest, most likely because there already exists one, we just
+                // haven't reloaded the user list yet.
+                return;
+            }
+            id = guest.id;
+        } else if (record.isAddUser) {
+            showAddUserDialog();
+            return;
+        } else {
+            id = record.info.id;
+        }
+
+        int currUserId = ActivityManager.getCurrentUser();
+        if (currUserId == id) {
+            if (record.isGuest) {
+                showExitGuestDialog(id);
+            }
+            return;
+        }
+
+        if (UserManager.isGuestUserEphemeral()) {
+            // If switching from guest, we want to bring up the guest exit dialog instead of switching
+            UserInfo currUserInfo = mUserManager.getUserInfo(currUserId);
+            if (currUserInfo != null && currUserInfo.isGuest()) {
+                showExitGuestDialog(currUserId, record.resolveId());
+                return;
+            }
+        }
+
+        switchToUserId(id);
+    }
+
+    public void switchTo(int userId) {
+        final int count = mUsers.size();
+        for (int i = 0; i < count; ++i) {
+            UserRecord record = mUsers.get(i);
+            if (record.info != null && record.info.id == userId) {
+                switchTo(record);
+                return;
+            }
+        }
+
+        Log.e(TAG, "Couldn't switch to user, id=" + userId);
+    }
+
+    public int getSwitchableUserCount() {
+        int count = 0;
+        final int N = mUsers.size();
+        for (int i = 0; i < N; ++i) {
+            UserRecord record = mUsers.get(i);
+            if (record.info != null && record.info.supportsSwitchTo()) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    protected void switchToUserId(int id) {
+        try {
+            pauseRefreshUsers();
+            ActivityManager.getService().switchUser(id);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Couldn't switch user.", e);
+        }
+    }
+
+    private void showExitGuestDialog(int id) {
+        int newId = UserHandle.USER_SYSTEM;
+        if (mResumeUserOnGuestLogout && mLastNonGuestUser != UserHandle.USER_SYSTEM) {
+            UserInfo info = mUserManager.getUserInfo(mLastNonGuestUser);
+            if (info != null && info.isEnabled() && info.supportsSwitchToByUser()) {
+                newId = info.id;
+            }
+        }
+        showExitGuestDialog(id, newId);
+    }
+
+    protected void showExitGuestDialog(int id, int targetId) {
+        if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) {
+            mExitGuestDialog.cancel();
+        }
+        mExitGuestDialog = new ExitGuestDialog(mContext, id, targetId);
+        mExitGuestDialog.show();
+    }
+
+    public void showAddUserDialog() {
+        if (mAddUserDialog != null && mAddUserDialog.isShowing()) {
+            mAddUserDialog.cancel();
+        }
+        mAddUserDialog = new AddUserDialog(mContext);
+        mAddUserDialog.show();
+    }
+
+    protected void exitGuest(int id, int targetId) {
+        switchToUserId(targetId);
+        mUserManager.removeUser(id);
+    }
+
+    private void listenForCallState() {
+        TelephonyManager.from(mContext).listen(mPhoneStateListener,
+                PhoneStateListener.LISTEN_CALL_STATE);
+    }
+
+    private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
+        private int mCallState;
+
+        @Override
+        public void onCallStateChanged(int state, String incomingNumber) {
+            if (mCallState == state) return;
+            if (DEBUG) Log.v(TAG, "Call state changed: " + state);
+            mCallState = state;
+            int currentUserId = ActivityManager.getCurrentUser();
+            UserInfo userInfo = mUserManager.getUserInfo(currentUserId);
+            if (userInfo != null && userInfo.isGuest()) {
+                showGuestNotification(currentUserId);
+            }
+            refreshUsers(UserHandle.USER_NULL);
+        }
+    };
+
+    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (DEBUG) {
+                Log.v(TAG, "Broadcast: a=" + intent.getAction()
+                       + " user=" + intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1));
+            }
+
+            boolean unpauseRefreshUsers = false;
+            int forcePictureLoadForId = UserHandle.USER_NULL;
+
+            if (ACTION_REMOVE_GUEST.equals(intent.getAction())) {
+                int currentUser = ActivityManager.getCurrentUser();
+                UserInfo userInfo = mUserManager.getUserInfo(currentUser);
+                if (userInfo != null && userInfo.isGuest()) {
+                    showExitGuestDialog(currentUser);
+                }
+                return;
+            } else if (ACTION_LOGOUT_USER.equals(intent.getAction())) {
+                logoutCurrentUser();
+            } else if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) {
+                if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) {
+                    mExitGuestDialog.cancel();
+                    mExitGuestDialog = null;
+                }
+
+                final int currentId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+                final UserInfo userInfo = mUserManager.getUserInfo(currentId);
+                final int N = mUsers.size();
+                for (int i = 0; i < N; i++) {
+                    UserRecord record = mUsers.get(i);
+                    if (record.info == null) continue;
+                    boolean shouldBeCurrent = record.info.id == currentId;
+                    if (record.isCurrent != shouldBeCurrent) {
+                        mUsers.set(i, record.copyWithIsCurrent(shouldBeCurrent));
+                    }
+                    if (shouldBeCurrent && !record.isGuest) {
+                        mLastNonGuestUser = record.info.id;
+                    }
+                    if ((userInfo == null || !userInfo.isAdmin()) && record.isRestricted) {
+                        // Immediately remove restricted records in case the AsyncTask is too slow.
+                        mUsers.remove(i);
+                        i--;
+                    }
+                }
+                notifyAdapters();
+
+                // Disconnect from the old secondary user's service
+                if (mSecondaryUser != UserHandle.USER_NULL) {
+                    context.stopServiceAsUser(mSecondaryUserServiceIntent,
+                            UserHandle.of(mSecondaryUser));
+                    mSecondaryUser = UserHandle.USER_NULL;
+                }
+                // Connect to the new secondary user's service (purely to ensure that a persistent
+                // SystemUI application is created for that user)
+                if (userInfo != null && userInfo.id != UserHandle.USER_SYSTEM) {
+                    context.startServiceAsUser(mSecondaryUserServiceIntent,
+                            UserHandle.of(userInfo.id));
+                    mSecondaryUser = userInfo.id;
+                }
+
+                if (UserManager.isSplitSystemUser() && userInfo != null && !userInfo.isGuest()
+                        && userInfo.id != UserHandle.USER_SYSTEM) {
+                    showLogoutNotification(currentId);
+                }
+                if (userInfo != null && userInfo.isGuest()) {
+                    showGuestNotification(currentId);
+                }
+                unpauseRefreshUsers = true;
+            } else if (Intent.ACTION_USER_INFO_CHANGED.equals(intent.getAction())) {
+                forcePictureLoadForId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE,
+                        UserHandle.USER_NULL);
+            } else if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
+                // Unlocking the system user may require a refresh
+                int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+                if (userId != UserHandle.USER_SYSTEM) {
+                    return;
+                }
+            }
+            refreshUsers(forcePictureLoadForId);
+            if (unpauseRefreshUsers) {
+                mUnpauseRefreshUsers.run();
+            }
+        }
+
+        private void showLogoutNotification(int userId) {
+            PendingIntent logoutPI = PendingIntent.getBroadcastAsUser(mContext,
+                    0, new Intent(ACTION_LOGOUT_USER), 0, UserHandle.SYSTEM);
+            Notification.Builder builder =
+                    new Notification.Builder(mContext, NotificationChannels.GENERAL)
+                            .setVisibility(Notification.VISIBILITY_SECRET)
+                            .setSmallIcon(R.drawable.ic_person)
+                            .setContentTitle(mContext.getString(
+                                    R.string.user_logout_notification_title))
+                            .setContentText(mContext.getString(
+                                    R.string.user_logout_notification_text))
+                            .setContentIntent(logoutPI)
+                            .setOngoing(true)
+                            .setShowWhen(false)
+                            .addAction(R.drawable.ic_delete,
+                                    mContext.getString(R.string.user_logout_notification_action),
+                                    logoutPI);
+            SystemUI.overrideNotificationAppName(mContext, builder);
+            NotificationManager.from(mContext).notifyAsUser(TAG_LOGOUT_USER,
+                    SystemMessage.NOTE_LOGOUT_USER, builder.build(), new UserHandle(userId));
+        }
+    };
+
+    private void showGuestNotification(int guestUserId) {
+        boolean canSwitchUsers = mUserManager.canSwitchUsers();
+        // Disable 'Remove guest' action if cannot switch users right now
+        PendingIntent removeGuestPI = canSwitchUsers ? PendingIntent.getBroadcastAsUser(mContext,
+                0, new Intent(ACTION_REMOVE_GUEST), 0, UserHandle.SYSTEM) : null;
+
+        Notification.Builder builder =
+                new Notification.Builder(mContext, NotificationChannels.GENERAL)
+                        .setVisibility(Notification.VISIBILITY_SECRET)
+                        .setSmallIcon(R.drawable.ic_person)
+                        .setContentTitle(mContext.getString(R.string.guest_notification_title))
+                        .setContentText(mContext.getString(R.string.guest_notification_text))
+                        .setContentIntent(removeGuestPI)
+                        .setShowWhen(false)
+                        .addAction(R.drawable.ic_delete,
+                                mContext.getString(R.string.guest_notification_remove_action),
+                                removeGuestPI);
+        SystemUI.overrideNotificationAppName(mContext, builder);
+        NotificationManager.from(mContext).notifyAsUser(TAG_REMOVE_GUEST,
+                SystemMessage.NOTE_REMOVE_GUEST, builder.build(), new UserHandle(guestUserId));
+    }
+
+    private final Runnable mUnpauseRefreshUsers = new Runnable() {
+        @Override
+        public void run() {
+            mHandler.removeCallbacks(this);
+            mPauseRefreshUsers = false;
+            refreshUsers(UserHandle.USER_NULL);
+        }
+    };
+
+    private final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) {
+        public void onChange(boolean selfChange) {
+            mSimpleUserSwitcher = Settings.Global.getInt(mContext.getContentResolver(),
+                    SIMPLE_USER_SWITCHER_GLOBAL_SETTING, 0) != 0;
+            mAddUsersWhenLocked = Settings.Global.getInt(mContext.getContentResolver(),
+                    Settings.Global.ADD_USERS_WHEN_LOCKED, 0) != 0;
+            refreshUsers(UserHandle.USER_NULL);
+        };
+    };
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("UserSwitcherController state:");
+        pw.println("  mLastNonGuestUser=" + mLastNonGuestUser);
+        pw.print("  mUsers.size="); pw.println(mUsers.size());
+        for (int i = 0; i < mUsers.size(); i++) {
+            final UserRecord u = mUsers.get(i);
+            pw.print("    "); pw.println(u.toString());
+        }
+    }
+
+    public String getCurrentUserName(Context context) {
+        if (mUsers.isEmpty()) return null;
+        UserRecord item = mUsers.get(0);
+        if (item == null || item.info == null) return null;
+        if (item.isGuest) return context.getString(R.string.guest_nickname);
+        return item.info.name;
+    }
+
+    public void onDensityOrFontScaleChanged() {
+        refreshUsers(UserHandle.USER_ALL);
+    }
+
+    @VisibleForTesting
+    public void addAdapter(WeakReference<BaseUserAdapter> adapter) {
+        mAdapters.add(adapter);
+    }
+
+    @VisibleForTesting
+    public ArrayList<UserRecord> getUsers() {
+        return mUsers;
+    }
+
+    public static abstract class BaseUserAdapter extends BaseAdapter {
+
+        final UserSwitcherController mController;
+        private final KeyguardMonitor mKeyguardMonitor;
+
+        protected BaseUserAdapter(UserSwitcherController controller) {
+            mController = controller;
+            mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
+            controller.addAdapter(new WeakReference<>(this));
+        }
+
+        public int getUserCount() {
+            boolean secureKeyguardShowing = mKeyguardMonitor.isShowing()
+                    && mKeyguardMonitor.isSecure()
+                    && !mKeyguardMonitor.canSkipBouncer();
+            if (!secureKeyguardShowing) {
+                return mController.getUsers().size();
+            }
+            // The lock screen is secure and showing. Filter out restricted records.
+            final int N = mController.getUsers().size();
+            int count = 0;
+            for (int i = 0; i < N; i++) {
+                if (mController.getUsers().get(i).isGuest) continue;
+                if (mController.getUsers().get(i).isRestricted) {
+                    break;
+                } else {
+                    count++;
+                }
+            }
+            return count;
+        }
+
+        @Override
+        public int getCount() {
+            boolean secureKeyguardShowing = mKeyguardMonitor.isShowing()
+                    && mKeyguardMonitor.isSecure()
+                    && !mKeyguardMonitor.canSkipBouncer();
+            if (!secureKeyguardShowing) {
+                return mController.getUsers().size();
+            }
+            // The lock screen is secure and showing. Filter out restricted records.
+            final int N = mController.getUsers().size();
+            int count = 0;
+            for (int i = 0; i < N; i++) {
+                if (mController.getUsers().get(i).isRestricted) {
+                    break;
+                } else {
+                    count++;
+                }
+            }
+            return count;
+        }
+
+        @Override
+        public UserRecord getItem(int position) {
+            return mController.getUsers().get(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        public void switchTo(UserRecord record) {
+            mController.switchTo(record);
+        }
+
+        public String getName(Context context, UserRecord item) {
+            if (item.isGuest) {
+                if (item.isCurrent) {
+                    return context.getString(R.string.guest_exit_guest);
+                } else {
+                    return context.getString(
+                            item.info == null ? R.string.guest_new_guest : R.string.guest_nickname);
+                }
+            } else if (item.isAddUser) {
+                return context.getString(R.string.user_add_user);
+            } else {
+                return item.info.name;
+            }
+        }
+
+        public Drawable getDrawable(Context context, UserRecord item) {
+            if (item.isAddUser) {
+                return context.getDrawable(R.drawable.ic_add_circle_qs);
+            }
+            Drawable icon = UserIcons.getDefaultUserIcon(item.resolveId(), /* light= */ false);
+            if (item.isGuest) {
+                icon.setColorFilter(Utils.getColorAttr(context, android.R.attr.colorForeground),
+                        Mode.SRC_IN);
+            }
+            return icon;
+        }
+
+        public void refresh() {
+            mController.refreshUsers(UserHandle.USER_NULL);
+        }
+    }
+
+    private void checkIfAddUserDisallowedByAdminOnly(UserRecord record) {
+        EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(mContext,
+                UserManager.DISALLOW_ADD_USER, ActivityManager.getCurrentUser());
+        if (admin != null && !RestrictedLockUtils.hasBaseUserRestriction(mContext,
+                UserManager.DISALLOW_ADD_USER, ActivityManager.getCurrentUser())) {
+            record.isDisabledByAdmin = true;
+            record.enforcedAdmin = admin;
+        } else {
+            record.isDisabledByAdmin = false;
+            record.enforcedAdmin = null;
+        }
+    }
+
+    public void startActivity(Intent intent) {
+        mActivityStarter.startActivity(intent, true);
+    }
+
+    public static final class UserRecord {
+        public final UserInfo info;
+        public final Bitmap picture;
+        public final boolean isGuest;
+        public final boolean isCurrent;
+        public final boolean isAddUser;
+        /** If true, the record is only visible to the owner and only when unlocked. */
+        public final boolean isRestricted;
+        public boolean isDisabledByAdmin;
+        public EnforcedAdmin enforcedAdmin;
+        public boolean isSwitchToEnabled;
+
+        public UserRecord(UserInfo info, Bitmap picture, boolean isGuest, boolean isCurrent,
+                boolean isAddUser, boolean isRestricted, boolean isSwitchToEnabled) {
+            this.info = info;
+            this.picture = picture;
+            this.isGuest = isGuest;
+            this.isCurrent = isCurrent;
+            this.isAddUser = isAddUser;
+            this.isRestricted = isRestricted;
+            this.isSwitchToEnabled = isSwitchToEnabled;
+        }
+
+        public UserRecord copyWithIsCurrent(boolean _isCurrent) {
+            return new UserRecord(info, picture, isGuest, _isCurrent, isAddUser, isRestricted,
+                    isSwitchToEnabled);
+        }
+
+        public int resolveId() {
+            if (isGuest || info == null) {
+                return UserHandle.USER_NULL;
+            }
+            return info.id;
+        }
+
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("UserRecord(");
+            if (info != null) {
+                sb.append("name=\"").append(info.name).append("\" id=").append(info.id);
+            } else {
+                if (isGuest) {
+                    sb.append("<add guest placeholder>");
+                } else if (isAddUser) {
+                    sb.append("<add user placeholder>");
+                }
+            }
+            if (isGuest) sb.append(" <isGuest>");
+            if (isAddUser) sb.append(" <isAddUser>");
+            if (isCurrent) sb.append(" <isCurrent>");
+            if (picture != null) sb.append(" <hasPicture>");
+            if (isRestricted) sb.append(" <isRestricted>");
+            if (isDisabledByAdmin) {
+                sb.append(" <isDisabledByAdmin>");
+                sb.append(" enforcedAdmin=").append(enforcedAdmin);
+            }
+            if (isSwitchToEnabled) {
+                sb.append(" <isSwitchToEnabled>");
+            }
+            sb.append(')');
+            return sb.toString();
+        }
+    }
+
+    public final DetailAdapter userDetailAdapter = new DetailAdapter() {
+        private final Intent USER_SETTINGS_INTENT = new Intent(Settings.ACTION_USER_SETTINGS);
+
+        @Override
+        public CharSequence getTitle() {
+            return mContext.getString(R.string.quick_settings_user_title);
+        }
+
+        @Override
+        public View createDetailView(Context context, View convertView, ViewGroup parent) {
+            UserDetailView v;
+            if (!(convertView instanceof UserDetailView)) {
+                v = UserDetailView.inflate(context, parent, false);
+                v.createAndSetAdapter(UserSwitcherController.this);
+            } else {
+                v = (UserDetailView) convertView;
+            }
+            v.refreshAdapter();
+            return v;
+        }
+
+        @Override
+        public Intent getSettingsIntent() {
+            return USER_SETTINGS_INTENT;
+        }
+
+        @Override
+        public Boolean getToggleState() {
+            return null;
+        }
+
+        @Override
+        public void setToggleState(boolean state) {
+        }
+
+        @Override
+        public int getMetricsCategory() {
+            return MetricsEvent.QS_USERDETAIL;
+        }
+    };
+
+    private final KeyguardMonitor.Callback mCallback = new KeyguardMonitor.Callback() {
+        @Override
+        public void onKeyguardShowingChanged() {
+
+            // When Keyguard is going away, we don't need to update our items immediately which
+            // helps making the transition faster.
+            if (!mKeyguardMonitor.isShowing()) {
+                mHandler.post(UserSwitcherController.this::notifyAdapters);
+            } else {
+                notifyAdapters();
+            }
+        }
+    };
+
+    private final class ExitGuestDialog extends SystemUIDialog implements
+            DialogInterface.OnClickListener {
+
+        private final int mGuestId;
+        private final int mTargetId;
+
+        public ExitGuestDialog(Context context, int guestId, int targetId) {
+            super(context);
+            setTitle(R.string.guest_exit_guest_dialog_title);
+            setMessage(context.getString(R.string.guest_exit_guest_dialog_message));
+            setButton(DialogInterface.BUTTON_NEGATIVE,
+                    context.getString(android.R.string.cancel), this);
+            setButton(DialogInterface.BUTTON_POSITIVE,
+                    context.getString(R.string.guest_exit_guest_dialog_remove), this);
+            setCanceledOnTouchOutside(false);
+            mGuestId = guestId;
+            mTargetId = targetId;
+        }
+
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            if (which == BUTTON_NEGATIVE) {
+                cancel();
+            } else {
+                dismiss();
+                exitGuest(mGuestId, mTargetId);
+            }
+        }
+    }
+
+    private final class AddUserDialog extends SystemUIDialog implements
+            DialogInterface.OnClickListener {
+
+        public AddUserDialog(Context context) {
+            super(context);
+            setTitle(R.string.user_add_user_title);
+            setMessage(context.getString(R.string.user_add_user_message_short));
+            setButton(DialogInterface.BUTTON_NEGATIVE,
+                    context.getString(android.R.string.cancel), this);
+            setButton(DialogInterface.BUTTON_POSITIVE,
+                    context.getString(android.R.string.ok), this);
+        }
+
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            if (which == BUTTON_NEGATIVE) {
+                cancel();
+            } else {
+                dismiss();
+                if (ActivityManager.isUserAMonkey()) {
+                    return;
+                }
+                UserInfo user = mUserManager.createUser(
+                        mContext.getString(R.string.user_new_user_name), 0 /* flags */);
+                if (user == null) {
+                    // Couldn't create user, most likely because there are too many, but we haven't
+                    // been able to reload the list yet.
+                    return;
+                }
+                int id = user.id;
+                Bitmap icon = UserIcons.convertToBitmap(UserIcons.getDefaultUserIcon(
+                        id, /* light= */ false));
+                mUserManager.setUserIcon(id, icon);
+                switchToUserId(id);
+            }
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/WifiIcons.java b/com/android/systemui/statusbar/policy/WifiIcons.java
new file mode 100644
index 0000000..374408d
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/WifiIcons.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2008 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.policy;
+
+import com.android.systemui.R;
+
+public class WifiIcons {
+    static final int[][] WIFI_SIGNAL_STRENGTH = {
+            { R.drawable.stat_sys_wifi_signal_0,
+              R.drawable.stat_sys_wifi_signal_1,
+              R.drawable.stat_sys_wifi_signal_2,
+              R.drawable.stat_sys_wifi_signal_3,
+              R.drawable.stat_sys_wifi_signal_4 },
+            { R.drawable.stat_sys_wifi_signal_0_fully,
+              R.drawable.stat_sys_wifi_signal_1_fully,
+              R.drawable.stat_sys_wifi_signal_2_fully,
+              R.drawable.stat_sys_wifi_signal_3_fully,
+              R.drawable.stat_sys_wifi_signal_4_fully }
+        };
+
+    public static final int[][] QS_WIFI_SIGNAL_STRENGTH = {
+            { R.drawable.ic_qs_wifi_0,
+              R.drawable.ic_qs_wifi_1,
+              R.drawable.ic_qs_wifi_2,
+              R.drawable.ic_qs_wifi_3,
+              R.drawable.ic_qs_wifi_4 },
+            { R.drawable.ic_qs_wifi_full_0,
+              R.drawable.ic_qs_wifi_full_1,
+              R.drawable.ic_qs_wifi_full_2,
+              R.drawable.ic_qs_wifi_full_3,
+              R.drawable.ic_qs_wifi_full_4 }
+        };
+
+    static final int QS_WIFI_NO_NETWORK = R.drawable.ic_qs_wifi_no_network;
+    static final int WIFI_NO_NETWORK = R.drawable.stat_sys_wifi_signal_null;
+
+    static final int WIFI_LEVEL_COUNT = WIFI_SIGNAL_STRENGTH[0].length;
+}
diff --git a/com/android/systemui/statusbar/policy/WifiSignalController.java b/com/android/systemui/statusbar/policy/WifiSignalController.java
new file mode 100644
index 0000000..2819624
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/WifiSignalController.java
@@ -0,0 +1,176 @@
+/*
+ * 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.policy;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.NetworkCapabilities;
+import android.net.wifi.WifiManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.AsyncChannel;
+import com.android.settingslib.wifi.WifiStatusTracker;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.NetworkController.IconState;
+import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+
+import java.util.Objects;
+
+
+public class WifiSignalController extends
+        SignalController<WifiSignalController.WifiState, SignalController.IconGroup> {
+    private final WifiManager mWifiManager;
+    private final AsyncChannel mWifiChannel;
+    private final boolean mHasMobileData;
+    private final WifiStatusTracker mWifiTracker;
+
+    public WifiSignalController(Context context, boolean hasMobileData,
+            CallbackHandler callbackHandler, NetworkControllerImpl networkController) {
+        super("WifiSignalController", context, NetworkCapabilities.TRANSPORT_WIFI,
+                callbackHandler, networkController);
+        mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+        mWifiTracker = new WifiStatusTracker(mWifiManager);
+        mHasMobileData = hasMobileData;
+        Handler handler = new WifiHandler(Looper.getMainLooper());
+        mWifiChannel = new AsyncChannel();
+        Messenger wifiMessenger = mWifiManager.getWifiServiceMessenger();
+        if (wifiMessenger != null) {
+            mWifiChannel.connect(context, handler, wifiMessenger);
+        }
+        // WiFi only has one state.
+        mCurrentState.iconGroup = mLastState.iconGroup = new IconGroup(
+                "Wi-Fi Icons",
+                WifiIcons.WIFI_SIGNAL_STRENGTH,
+                WifiIcons.QS_WIFI_SIGNAL_STRENGTH,
+                AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH,
+                WifiIcons.WIFI_NO_NETWORK,
+                WifiIcons.QS_WIFI_NO_NETWORK,
+                WifiIcons.WIFI_NO_NETWORK,
+                WifiIcons.QS_WIFI_NO_NETWORK,
+                AccessibilityContentDescriptions.WIFI_NO_CONNECTION
+                );
+
+    }
+
+    @Override
+    protected WifiState cleanState() {
+        return new WifiState();
+    }
+
+    @Override
+    public void notifyListeners(SignalCallback callback) {
+        // only show wifi in the cluster if connected or if wifi-only
+        boolean wifiVisible = mCurrentState.enabled
+                && (mCurrentState.connected || !mHasMobileData);
+        String wifiDesc = wifiVisible ? mCurrentState.ssid : null;
+        boolean ssidPresent = wifiVisible && mCurrentState.ssid != null;
+        String contentDescription = getStringIfExists(getContentDescription());
+        if (mCurrentState.inetCondition == 0) {
+            contentDescription +=
+                    ("," + mContext.getString(R.string.accessibility_quick_settings_no_internet));
+        }
+
+        IconState statusIcon = new IconState(wifiVisible, getCurrentIconId(), contentDescription);
+        IconState qsIcon = new IconState(mCurrentState.connected, getQsCurrentIconId(),
+                contentDescription);
+        callback.setWifiIndicators(mCurrentState.enabled, statusIcon, qsIcon,
+                ssidPresent && mCurrentState.activityIn, ssidPresent && mCurrentState.activityOut,
+                wifiDesc, mCurrentState.isTransient);
+    }
+
+    /**
+     * Extract wifi state directly from broadcasts about changes in wifi state.
+     */
+    public void handleBroadcast(Intent intent) {
+        mWifiTracker.handleBroadcast(intent);
+        mCurrentState.enabled = mWifiTracker.enabled;
+        mCurrentState.connected = mWifiTracker.connected;
+        mCurrentState.ssid = mWifiTracker.ssid;
+        mCurrentState.rssi = mWifiTracker.rssi;
+        mCurrentState.level = mWifiTracker.level;
+        notifyListenersIfNecessary();
+    }
+
+    @VisibleForTesting
+    void setActivity(int wifiActivity) {
+        mCurrentState.activityIn = wifiActivity == WifiManager.DATA_ACTIVITY_INOUT
+                || wifiActivity == WifiManager.DATA_ACTIVITY_IN;
+        mCurrentState.activityOut = wifiActivity == WifiManager.DATA_ACTIVITY_INOUT
+                || wifiActivity == WifiManager.DATA_ACTIVITY_OUT;
+        notifyListenersIfNecessary();
+    }
+
+    /**
+     * Handler to receive the data activity on wifi.
+     */
+    private class WifiHandler extends Handler {
+        WifiHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED:
+                    if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) {
+                        mWifiChannel.sendMessage(Message.obtain(this,
+                                AsyncChannel.CMD_CHANNEL_FULL_CONNECTION));
+                    } else {
+                        Log.e(mTag, "Failed to connect to wifi");
+                    }
+                    break;
+                case WifiManager.DATA_ACTIVITY_NOTIFICATION:
+                    setActivity(msg.arg1);
+                    break;
+                default:
+                    // Ignore
+                    break;
+            }
+        }
+    }
+
+    static class WifiState extends SignalController.State {
+        String ssid;
+        boolean isTransient;
+
+        @Override
+        public void copyFrom(State s) {
+            super.copyFrom(s);
+            WifiState state = (WifiState) s;
+            ssid = state.ssid;
+            isTransient = state.isTransient;
+        }
+
+        @Override
+        protected void toString(StringBuilder builder) {
+            super.toString(builder);
+            builder.append(',').append("ssid=").append(ssid);
+            builder.append(',').append("isTransient=").append(isTransient);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            return super.equals(o)
+                    && Objects.equals(((WifiState) o).ssid, ssid)
+                    && (((WifiState) o).isTransient == isTransient);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/policy/ZenModeController.java b/com/android/systemui/statusbar/policy/ZenModeController.java
new file mode 100644
index 0000000..8777aa6
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/ZenModeController.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.content.ComponentName;
+import android.net.Uri;
+import android.service.notification.Condition;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.ZenRule;
+
+import com.android.systemui.statusbar.policy.ZenModeController.Callback;
+
+public interface ZenModeController extends CallbackController<Callback> {
+    void setZen(int zen, Uri conditionId, String reason);
+    int getZen();
+    ZenRule getManualRule();
+    ZenModeConfig getConfig();
+    long getNextAlarm();
+    boolean isZenAvailable();
+    ComponentName getEffectsSuppressor();
+    boolean isCountdownConditionSupported();
+    int getCurrentUser();
+    boolean isVolumeRestricted();
+
+    public static interface Callback {
+        default void onZenChanged(int zen) {}
+        default void onConditionsChanged(Condition[] conditions) {}
+        default void onNextAlarmChanged() {}
+        default void onZenAvailableChanged(boolean available) {}
+        default void onEffectsSupressorChanged() {}
+        default void onManualRuleChanged(ZenRule rule) {}
+        default void onConfigChanged(ZenModeConfig config) {}
+    }
+
+}
\ No newline at end of file
diff --git a/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
new file mode 100644
index 0000000..0fd2445
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2014 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.policy;
+
+import android.app.ActivityManager;
+import android.app.AlarmManager;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings.Global;
+import android.provider.Settings.Secure;
+import android.service.notification.Condition;
+import android.service.notification.IConditionListener;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.ZenRule;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.systemui.qs.GlobalSetting;
+import com.android.systemui.settings.CurrentUserTracker;
+import com.android.systemui.util.Utils;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.Objects;
+
+/** Platform implementation of the zen mode controller. **/
+public class ZenModeControllerImpl extends CurrentUserTracker implements ZenModeController {
+    private static final String TAG = "ZenModeController";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
+    private final Context mContext;
+    private final GlobalSetting mModeSetting;
+    private final GlobalSetting mConfigSetting;
+    private final NotificationManager mNoMan;
+    private final LinkedHashMap<Uri, Condition> mConditions = new LinkedHashMap<Uri, Condition>();
+    private final AlarmManager mAlarmManager;
+    private final SetupObserver mSetupObserver;
+    private final UserManager mUserManager;
+
+    private int mUserId;
+    private boolean mRequesting;
+    private boolean mRegistered;
+    private ZenModeConfig mConfig;
+
+    public ZenModeControllerImpl(Context context, Handler handler) {
+        super(context);
+        mContext = context;
+        mModeSetting = new GlobalSetting(mContext, handler, Global.ZEN_MODE) {
+            @Override
+            protected void handleValueChanged(int value) {
+                fireZenChanged(value);
+            }
+        };
+        mConfigSetting = new GlobalSetting(mContext, handler, Global.ZEN_MODE_CONFIG_ETAG) {
+            @Override
+            protected void handleValueChanged(int value) {
+                updateZenModeConfig();
+            }
+        };
+        mNoMan = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+        mConfig = mNoMan.getZenModeConfig();
+        mModeSetting.setListening(true);
+        mConfigSetting.setListening(true);
+        mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+        mSetupObserver = new SetupObserver(handler);
+        mSetupObserver.register();
+        mUserManager = context.getSystemService(UserManager.class);
+        startTracking();
+    }
+
+    @Override
+    public boolean isVolumeRestricted() {
+        return mUserManager.hasUserRestriction(UserManager.DISALLOW_ADJUST_VOLUME,
+                new UserHandle(mUserId));
+    }
+
+    @Override
+    public void addCallback(Callback callback) {
+        mCallbacks.add(callback);
+    }
+
+    @Override
+    public void removeCallback(Callback callback) {
+        mCallbacks.remove(callback);
+    }
+
+    @Override
+    public int getZen() {
+        return mModeSetting.getValue();
+    }
+
+    @Override
+    public void setZen(int zen, Uri conditionId, String reason) {
+        mNoMan.setZenMode(zen, conditionId, reason);
+    }
+
+    @Override
+    public boolean isZenAvailable() {
+        return mSetupObserver.isDeviceProvisioned() && mSetupObserver.isUserSetup();
+    }
+
+    @Override
+    public ZenRule getManualRule() {
+        return mConfig == null ? null : mConfig.manualRule;
+    }
+
+    @Override
+    public ZenModeConfig getConfig() {
+        return mConfig;
+    }
+
+    @Override
+    public long getNextAlarm() {
+        final AlarmManager.AlarmClockInfo info = mAlarmManager.getNextAlarmClock(mUserId);
+        return info != null ? info.getTriggerTime() : 0;
+    }
+
+    @Override
+    public void onUserSwitched(int userId) {
+        mUserId = userId;
+        if (mRegistered) {
+            mContext.unregisterReceiver(mReceiver);
+        }
+        final IntentFilter filter = new IntentFilter(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
+        filter.addAction(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED);
+        mContext.registerReceiverAsUser(mReceiver, new UserHandle(mUserId), filter, null, null);
+        mRegistered = true;
+        mSetupObserver.register();
+    }
+
+    @Override
+    public ComponentName getEffectsSuppressor() {
+        return NotificationManager.from(mContext).getEffectsSuppressor();
+    }
+
+    @Override
+    public boolean isCountdownConditionSupported() {
+        return NotificationManager.from(mContext)
+                .isSystemConditionProviderEnabled(ZenModeConfig.COUNTDOWN_PATH);
+    }
+
+    @Override
+    public int getCurrentUser() {
+        return ActivityManager.getCurrentUser();
+    }
+
+    private void fireNextAlarmChanged() {
+        Utils.safeForeach(mCallbacks, c -> c.onNextAlarmChanged());
+    }
+
+    private void fireEffectsSuppressorChanged() {
+        Utils.safeForeach(mCallbacks, c -> c.onEffectsSupressorChanged());
+    }
+
+    private void fireZenChanged(int zen) {
+        Utils.safeForeach(mCallbacks, c -> c.onZenChanged(zen));
+    }
+
+    private void fireZenAvailableChanged(boolean available) {
+        Utils.safeForeach(mCallbacks, c -> c.onZenAvailableChanged(available));
+    }
+
+    private void fireConditionsChanged(Condition[] conditions) {
+        Utils.safeForeach(mCallbacks, c -> c.onConditionsChanged(conditions));
+    }
+
+    private void fireManualRuleChanged(ZenRule rule) {
+        Utils.safeForeach(mCallbacks, c -> c.onManualRuleChanged(rule));
+    }
+
+    @VisibleForTesting
+    protected void fireConfigChanged(ZenModeConfig config) {
+        Utils.safeForeach(mCallbacks, c -> c.onConfigChanged(config));
+    }
+
+    private void updateConditions(Condition[] conditions) {
+        if (conditions == null || conditions.length == 0) return;
+        for (Condition c : conditions) {
+            if ((c.flags & Condition.FLAG_RELEVANT_NOW) == 0) continue;
+            mConditions.put(c.id, c);
+        }
+        fireConditionsChanged(
+                mConditions.values().toArray(new Condition[mConditions.values().size()]));
+    }
+
+    private void updateZenModeConfig() {
+        final ZenModeConfig config = mNoMan.getZenModeConfig();
+        if (Objects.equals(config, mConfig)) return;
+        final ZenRule oldRule = mConfig != null ? mConfig.manualRule : null;
+        mConfig = config;
+        fireConfigChanged(config);
+        final ZenRule newRule = config != null ? config.manualRule : null;
+        if (Objects.equals(oldRule, newRule)) return;
+        fireManualRuleChanged(newRule);
+    }
+
+    private final IConditionListener mListener = new IConditionListener.Stub() {
+        @Override
+        public void onConditionsReceived(Condition[] conditions) {
+            if (DEBUG) Slog.d(TAG, "onConditionsReceived "
+                    + (conditions == null ? 0 : conditions.length) + " mRequesting=" + mRequesting);
+            if (!mRequesting) return;
+            updateConditions(conditions);
+        }
+    };
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED.equals(intent.getAction())) {
+                fireNextAlarmChanged();
+            }
+            if (NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED.equals(intent.getAction())) {
+                fireEffectsSuppressorChanged();
+            }
+        }
+    };
+
+    private final class SetupObserver extends ContentObserver {
+        private final ContentResolver mResolver;
+
+        private boolean mRegistered;
+
+        public SetupObserver(Handler handler) {
+            super(handler);
+            mResolver = mContext.getContentResolver();
+        }
+
+        public boolean isUserSetup() {
+            return Secure.getIntForUser(mResolver, Secure.USER_SETUP_COMPLETE, 0, mUserId) != 0;
+        }
+
+        public boolean isDeviceProvisioned() {
+            return Global.getInt(mResolver, Global.DEVICE_PROVISIONED, 0) != 0;
+        }
+
+        public void register() {
+            if (mRegistered) {
+                mResolver.unregisterContentObserver(this);
+            }
+            mResolver.registerContentObserver(
+                    Global.getUriFor(Global.DEVICE_PROVISIONED), false, this);
+            mResolver.registerContentObserver(
+                    Secure.getUriFor(Secure.USER_SETUP_COMPLETE), false, this, mUserId);
+            fireZenAvailableChanged(isZenAvailable());
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            if (Global.getUriFor(Global.DEVICE_PROVISIONED).equals(uri)
+                    || Secure.getUriFor(Secure.USER_SETUP_COMPLETE).equals(uri)) {
+                fireZenAvailableChanged(isZenAvailable());
+            }
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/stack/AmbientState.java b/com/android/systemui/statusbar/stack/AmbientState.java
new file mode 100644
index 0000000..4d8da44
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/AmbientState.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2014 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.view.View;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ActivatableNotificationView;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * A global state to track all input states for the algorithm.
+ */
+public class AmbientState {
+    private ArrayList<View> mDraggedViews = new ArrayList<View>();
+    private int mScrollY;
+    private boolean mDimmed;
+    private ActivatableNotificationView mActivatedChild;
+    private float mOverScrollTopAmount;
+    private float mOverScrollBottomAmount;
+    private int mSpeedBumpIndex = -1;
+    private boolean mDark;
+    private boolean mHideSensitive;
+    private HeadsUpManager mHeadsUpManager;
+    private float mStackTranslation;
+    private int mLayoutHeight;
+    private int mTopPadding;
+    private boolean mShadeExpanded;
+    private float mMaxHeadsUpTranslation;
+    private boolean mDismissAllInProgress;
+    private int mLayoutMinHeight;
+    private NotificationShelf mShelf;
+    private int mZDistanceBetweenElements;
+    private int mBaseZHeight;
+    private int mMaxLayoutHeight;
+    private ActivatableNotificationView mLastVisibleBackgroundChild;
+    private float mCurrentScrollVelocity;
+    private int mStatusBarState;
+    private float mExpandingVelocity;
+    private boolean mPanelTracking;
+    private boolean mExpansionChanging;
+    private boolean mPanelFullWidth;
+    private Collection<HeadsUpManager.HeadsUpEntry> mPulsing;
+    private boolean mUnlockHintRunning;
+    private boolean mQsCustomizerShowing;
+    private int mIntrinsicPadding;
+
+    public AmbientState(Context context) {
+        reload(context);
+    }
+
+    /**
+     * Reload the dimens e.g. if the density changed.
+     */
+    public void reload(Context context) {
+        mZDistanceBetweenElements = Math.max(1, context.getResources()
+                .getDimensionPixelSize(R.dimen.z_distance_between_notifications));
+        mBaseZHeight = 4 * mZDistanceBetweenElements;
+    }
+
+    /**
+     * @return the basic Z height on which notifications remain.
+     */
+    public int getBaseZHeight() {
+        return mBaseZHeight;
+    }
+
+    /**
+     * @return the distance in Z between two overlaying notifications.
+     */
+    public int getZDistanceBetweenElements() {
+        return mZDistanceBetweenElements;
+    }
+
+    public int getScrollY() {
+        return mScrollY;
+    }
+
+    public void setScrollY(int scrollY) {
+        this.mScrollY = scrollY;
+    }
+
+    public void onBeginDrag(View view) {
+        mDraggedViews.add(view);
+    }
+
+    public void onDragFinished(View view) {
+        mDraggedViews.remove(view);
+    }
+
+    public ArrayList<View> getDraggedViews() {
+        return mDraggedViews;
+    }
+
+    /**
+     * @param dimmed Whether we are in a dimmed state (on the lockscreen), where the backgrounds are
+     *               translucent and everything is scaled back a bit.
+     */
+    public void setDimmed(boolean dimmed) {
+        mDimmed = dimmed;
+    }
+
+    /** In dark mode, we draw as little as possible, assuming a black background */
+    public void setDark(boolean dark) {
+        mDark = dark;
+    }
+
+    public void setHideSensitive(boolean hideSensitive) {
+        mHideSensitive = hideSensitive;
+    }
+
+    /**
+     * In dimmed mode, a child can be activated, which happens on the first tap of the double-tap
+     * interaction. This child is then scaled normally and its background is fully opaque.
+     */
+    public void setActivatedChild(ActivatableNotificationView activatedChild) {
+        mActivatedChild = activatedChild;
+    }
+
+    public boolean isDimmed() {
+        return mDimmed;
+    }
+
+    public boolean isDark() {
+        return mDark;
+    }
+
+    public boolean isHideSensitive() {
+        return mHideSensitive;
+    }
+
+    public ActivatableNotificationView getActivatedChild() {
+        return mActivatedChild;
+    }
+
+    public void setOverScrollAmount(float amount, boolean onTop) {
+        if (onTop) {
+            mOverScrollTopAmount = amount;
+        } else {
+            mOverScrollBottomAmount = amount;
+        }
+    }
+
+    public float getOverScrollAmount(boolean top) {
+        return top ? mOverScrollTopAmount : mOverScrollBottomAmount;
+    }
+
+    public int getSpeedBumpIndex() {
+        return mSpeedBumpIndex;
+    }
+
+    public void setSpeedBumpIndex(int shelfIndex) {
+        mSpeedBumpIndex = shelfIndex;
+    }
+
+    public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+        mHeadsUpManager = headsUpManager;
+    }
+
+    public float getStackTranslation() {
+        return mStackTranslation;
+    }
+
+    public void setStackTranslation(float stackTranslation) {
+        mStackTranslation = stackTranslation;
+    }
+
+    public void setLayoutHeight(int layoutHeight) {
+        mLayoutHeight = layoutHeight;
+    }
+
+    public float getTopPadding() {
+        return mTopPadding;
+    }
+
+    public void setTopPadding(int topPadding) {
+        mTopPadding = topPadding;
+    }
+
+    public int getInnerHeight() {
+        return Math.max(Math.min(mLayoutHeight, mMaxLayoutHeight) - mTopPadding, mLayoutMinHeight);
+    }
+
+    public boolean isShadeExpanded() {
+        return mShadeExpanded;
+    }
+
+    public void setShadeExpanded(boolean shadeExpanded) {
+        mShadeExpanded = shadeExpanded;
+    }
+
+    public void setMaxHeadsUpTranslation(float maxHeadsUpTranslation) {
+        mMaxHeadsUpTranslation = maxHeadsUpTranslation;
+    }
+
+    public float getMaxHeadsUpTranslation() {
+        return mMaxHeadsUpTranslation;
+    }
+
+    public void setDismissAllInProgress(boolean dismissAllInProgress) {
+        mDismissAllInProgress = dismissAllInProgress;
+    }
+
+    public boolean isDismissAllInProgress() {
+        return mDismissAllInProgress;
+    }
+
+    public void setLayoutMinHeight(int layoutMinHeight) {
+        mLayoutMinHeight = layoutMinHeight;
+    }
+
+    public void setShelf(NotificationShelf shelf) {
+        mShelf = shelf;
+    }
+
+    public NotificationShelf getShelf() {
+        return mShelf;
+    }
+
+    public void setLayoutMaxHeight(int maxLayoutHeight) {
+        mMaxLayoutHeight = maxLayoutHeight;
+    }
+
+    /**
+     * Sets the last visible view of the host layout, that has a background, i.e the very last
+     * view in the shade, without the clear all button.
+     */
+    public void setLastVisibleBackgroundChild(
+            ActivatableNotificationView lastVisibleBackgroundChild) {
+        mLastVisibleBackgroundChild = lastVisibleBackgroundChild;
+    }
+
+    public ActivatableNotificationView getLastVisibleBackgroundChild() {
+        return mLastVisibleBackgroundChild;
+    }
+
+    public void setCurrentScrollVelocity(float currentScrollVelocity) {
+        mCurrentScrollVelocity = currentScrollVelocity;
+    }
+
+    public float getCurrentScrollVelocity() {
+        return mCurrentScrollVelocity;
+    }
+
+    public boolean isOnKeyguard() {
+        return mStatusBarState == StatusBarState.KEYGUARD;
+    }
+
+    public void setStatusBarState(int statusBarState) {
+        mStatusBarState = statusBarState;
+    }
+
+    public void setExpandingVelocity(float expandingVelocity) {
+        mExpandingVelocity = expandingVelocity;
+    }
+
+    public void setExpansionChanging(boolean expansionChanging) {
+        mExpansionChanging = expansionChanging;
+    }
+
+    public boolean isExpansionChanging() {
+        return mExpansionChanging;
+    }
+
+    public float getExpandingVelocity() {
+        return mExpandingVelocity;
+    }
+
+    public void setPanelTracking(boolean panelTracking) {
+        mPanelTracking = panelTracking;
+    }
+
+    public boolean hasPulsingNotifications() {
+        return mPulsing != null;
+    }
+
+    public void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> hasPulsing) {
+        mPulsing = hasPulsing;
+    }
+
+    public boolean isPulsing(NotificationData.Entry entry) {
+        if (mPulsing == null) {
+            return false;
+        }
+        for (HeadsUpManager.HeadsUpEntry e : mPulsing) {
+            if (e.entry == entry) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public boolean isPanelTracking() {
+        return mPanelTracking;
+    }
+
+    public boolean isPanelFullWidth() {
+        return mPanelFullWidth;
+    }
+
+    public void setPanelFullWidth(boolean panelFullWidth) {
+        mPanelFullWidth = panelFullWidth;
+    }
+
+    public void setUnlockHintRunning(boolean unlockHintRunning) {
+        mUnlockHintRunning = unlockHintRunning;
+    }
+
+    public boolean isUnlockHintRunning() {
+        return mUnlockHintRunning;
+    }
+
+    public boolean isQsCustomizerShowing() {
+        return mQsCustomizerShowing;
+    }
+
+    public void setQsCustomizerShowing(boolean qsCustomizerShowing) {
+        mQsCustomizerShowing = qsCustomizerShowing;
+    }
+
+    public void setIntrinsicPadding(int intrinsicPadding) {
+        mIntrinsicPadding = intrinsicPadding;
+    }
+
+    public int getIntrinsicPadding() {
+        return mIntrinsicPadding;
+    }
+
+    /**
+     * Similar to the normal is above shelf logic but doesn't allow it to be above in AOD1.
+     *
+     * @param expandableView the view to check
+     */
+    public boolean isAboveShelf(ExpandableView expandableView) {
+        if (!(expandableView instanceof ExpandableNotificationRow)) {
+            return expandableView.isAboveShelf();
+        }
+        ExpandableNotificationRow row = (ExpandableNotificationRow) expandableView;
+        return row.isAboveShelf() && !isDozingAndNotPulsing(row);
+    }
+
+    /**
+     * @return whether a view is dozing and not pulsing right now
+     */
+    public boolean isDozingAndNotPulsing(ExpandableView view) {
+        if (view instanceof ExpandableNotificationRow) {
+            return isDozingAndNotPulsing((ExpandableNotificationRow) view);
+        }
+        return false;
+    }
+
+    /**
+     * @return whether a row is dozing and not pulsing right now
+     */
+    public boolean isDozingAndNotPulsing(ExpandableNotificationRow row) {
+        return isDark() && !isPulsing(row.getEntry());
+    }
+}
diff --git a/com/android/systemui/statusbar/stack/AnimationFilter.java b/com/android/systemui/statusbar/stack/AnimationFilter.java
new file mode 100644
index 0000000..53377d9
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/AnimationFilter.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2014 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.support.v4.util.ArraySet;
+import android.util.Property;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+/**
+ * Filters the animations for only a certain type of properties.
+ */
+public class AnimationFilter {
+    boolean animateAlpha;
+    boolean animateX;
+    boolean animateY;
+    ArraySet<View> animateYViews = new ArraySet<>();
+    boolean animateZ;
+    boolean animateHeight;
+    boolean animateTopInset;
+    boolean animateDimmed;
+    boolean animateDark;
+    boolean animateHideSensitive;
+    public boolean animateShadowAlpha;
+    boolean hasDelays;
+    boolean hasGoToFullShadeEvent;
+    boolean hasHeadsUpDisappearClickEvent;
+    private ArraySet<Property> mAnimatedProperties = new ArraySet<>();
+
+    public AnimationFilter animateAlpha() {
+        animateAlpha = true;
+        return this;
+    }
+
+    public AnimationFilter animateScale() {
+        animate(View.SCALE_X);
+        animate(View.SCALE_Y);
+        return this;
+    }
+
+    public AnimationFilter animateX() {
+        animateX = true;
+        return this;
+    }
+
+    public AnimationFilter animateY() {
+        animateY = true;
+        return this;
+    }
+
+    public AnimationFilter hasDelays() {
+        hasDelays = true;
+        return this;
+    }
+
+    public AnimationFilter animateZ() {
+        animateZ = true;
+        return this;
+    }
+
+    public AnimationFilter animateHeight() {
+        animateHeight = true;
+        return this;
+    }
+
+    public AnimationFilter animateTopInset() {
+        animateTopInset = true;
+        return this;
+    }
+
+    public AnimationFilter animateDimmed() {
+        animateDimmed = true;
+        return this;
+    }
+
+    public AnimationFilter animateDark() {
+        animateDark = true;
+        return this;
+    }
+
+    public AnimationFilter animateHideSensitive() {
+        animateHideSensitive = true;
+        return this;
+    }
+
+    public AnimationFilter animateShadowAlpha() {
+        animateShadowAlpha = true;
+        return this;
+    }
+
+    public AnimationFilter animateY(View view) {
+        animateYViews.add(view);
+        return this;
+    }
+
+    public boolean shouldAnimateY(View view) {
+        return animateY || animateYViews.contains(view);
+    }
+
+    /**
+     * Combines multiple filters into {@code this} filter, using or as the operand .
+     *
+     * @param events The animation events from the filters to combine.
+     */
+    public void applyCombination(ArrayList<NotificationStackScrollLayout.AnimationEvent> events) {
+        reset();
+        int size = events.size();
+        for (int i = 0; i < size; i++) {
+            NotificationStackScrollLayout.AnimationEvent ev = events.get(i);
+            combineFilter(events.get(i).filter);
+            if (ev.animationType ==
+                    NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_GO_TO_FULL_SHADE) {
+                hasGoToFullShadeEvent = true;
+            }
+            if (ev.animationType == NotificationStackScrollLayout.AnimationEvent
+                    .ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK) {
+                hasHeadsUpDisappearClickEvent = true;
+            }
+        }
+    }
+
+    public void combineFilter(AnimationFilter filter) {
+        animateAlpha |= filter.animateAlpha;
+        animateX |= filter.animateX;
+        animateY |= filter.animateY;
+        animateYViews.addAll(filter.animateYViews);
+        animateZ |= filter.animateZ;
+        animateHeight |= filter.animateHeight;
+        animateTopInset |= filter.animateTopInset;
+        animateDimmed |= filter.animateDimmed;
+        animateDark |= filter.animateDark;
+        animateHideSensitive |= filter.animateHideSensitive;
+        animateShadowAlpha |= filter.animateShadowAlpha;
+        hasDelays |= filter.hasDelays;
+        mAnimatedProperties.addAll(filter.mAnimatedProperties);
+    }
+
+    public void reset() {
+        animateAlpha = false;
+        animateX = false;
+        animateY = false;
+        animateYViews.clear();
+        animateZ = false;
+        animateHeight = false;
+        animateShadowAlpha = false;
+        animateTopInset = false;
+        animateDimmed = false;
+        animateDark = false;
+        animateHideSensitive = false;
+        hasDelays = false;
+        hasGoToFullShadeEvent = false;
+        hasHeadsUpDisappearClickEvent = false;
+        mAnimatedProperties.clear();
+    }
+
+    public AnimationFilter animate(Property property) {
+        mAnimatedProperties.add(property);
+        return this;
+    }
+
+    public boolean shouldAnimateProperty(Property property) {
+        // TODO: migrate all existing animators to properties
+        return mAnimatedProperties.contains(property);
+    }
+}
diff --git a/com/android/systemui/statusbar/stack/AnimationProperties.java b/com/android/systemui/statusbar/stack/AnimationProperties.java
new file mode 100644
index 0000000..ebb0a6d
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/AnimationProperties.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 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.animation.AnimatorListenerAdapter;
+import android.util.ArrayMap;
+import android.util.Property;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import java.util.HashMap;
+
+/**
+ * Properties for a View animation
+ */
+public class AnimationProperties {
+    public long duration;
+    public long delay;
+    private ArrayMap<Property, Interpolator> mInterpolatorMap;
+
+    /**
+     * @return an animation filter for this animation.
+     */
+    public AnimationFilter getAnimationFilter() {
+        return new AnimationFilter();
+    }
+
+    /**
+     * @return a listener that should be run whenever any property finished its animation
+     */
+    public AnimatorListenerAdapter getAnimationFinishListener() {
+        return null;
+    }
+
+    public boolean wasAdded(View view) {
+        return false;
+    }
+
+    /**
+     * Get a custom interpolator for a property instead of the normal one.
+     */
+    public Interpolator getCustomInterpolator(View child, Property property) {
+        return mInterpolatorMap != null ? mInterpolatorMap.get(property) : null;
+    }
+
+
+    public void combineCustomInterpolators(AnimationProperties iconAnimationProperties) {
+        ArrayMap<Property, Interpolator> map = iconAnimationProperties.mInterpolatorMap;
+        if (map != null) {
+            if (mInterpolatorMap == null) {
+                mInterpolatorMap = new ArrayMap<>();
+            }
+            mInterpolatorMap.putAll(map);
+        }
+    }
+
+    /**
+     * Set a custom interpolator to use for all views for a property.
+     */
+    public AnimationProperties setCustomInterpolator(Property property, Interpolator interpolator) {
+        if (mInterpolatorMap == null) {
+            mInterpolatorMap = new ArrayMap<>();
+        }
+        mInterpolatorMap.put(property, interpolator);
+        return this;
+    }
+
+    public AnimationProperties setDuration(long duration) {
+        this.duration = duration;
+        return this;
+    }
+
+    public AnimationProperties setDelay(long delay) {
+        this.delay = delay;
+        return this;
+    }
+
+    public AnimationProperties resetCustomInterpolators() {
+        mInterpolatorMap = null;
+        return this;
+    }
+}
diff --git a/com/android/systemui/statusbar/stack/ExpandableViewState.java b/com/android/systemui/statusbar/stack/ExpandableViewState.java
new file mode 100644
index 0000000..e0fd481
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/ExpandableViewState.java
@@ -0,0 +1,455 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.view.View;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
+
+/**
+* A state of an expandable view
+*/
+public class ExpandableViewState extends ViewState {
+
+    private static final int TAG_ANIMATOR_HEIGHT = R.id.height_animator_tag;
+    private static final int TAG_ANIMATOR_TOP_INSET = R.id.top_inset_animator_tag;
+    private static final int TAG_ANIMATOR_SHADOW_ALPHA = R.id.shadow_alpha_animator_tag;
+    private static final int TAG_END_HEIGHT = R.id.height_animator_end_value_tag;
+    private static final int TAG_END_TOP_INSET = R.id.top_inset_animator_end_value_tag;
+    private static final int TAG_END_SHADOW_ALPHA = R.id.shadow_alpha_animator_end_value_tag;
+    private static final int TAG_START_HEIGHT = R.id.height_animator_start_value_tag;
+    private static final int TAG_START_TOP_INSET = R.id.top_inset_animator_start_value_tag;
+    private static final int TAG_START_SHADOW_ALPHA = R.id.shadow_alpha_animator_start_value_tag;
+
+    // These are flags such that we can create masks for filtering.
+
+    /**
+     * No known location. This is the default and should not be set after an invocation of the
+     * algorithm.
+     */
+    public static final int LOCATION_UNKNOWN = 0x00;
+
+    /**
+     * The location is the first heads up notification, so on the very top.
+     */
+    public static final int LOCATION_FIRST_HUN = 0x01;
+
+    /**
+     * The location is hidden / scrolled away on the top.
+     */
+    public static final int LOCATION_HIDDEN_TOP = 0x02;
+
+    /**
+     * The location is in the main area of the screen and visible.
+     */
+    public static final int LOCATION_MAIN_AREA = 0x04;
+
+    /**
+     * The location is in the bottom stack and it's peeking
+     */
+    public static final int LOCATION_BOTTOM_STACK_PEEKING = 0x08;
+
+    /**
+     * The location is in the bottom stack and it's hidden.
+     */
+    public static final int LOCATION_BOTTOM_STACK_HIDDEN = 0x10;
+
+    /**
+     * The view isn't laid out at all.
+     */
+    public static final int LOCATION_GONE = 0x40;
+
+    /**
+     * The visible locations of a view.
+     */
+    public static final int VISIBLE_LOCATIONS = ExpandableViewState.LOCATION_FIRST_HUN
+            | ExpandableViewState.LOCATION_MAIN_AREA;
+
+    public int height;
+    public boolean dimmed;
+    public boolean dark;
+    public boolean hideSensitive;
+    public boolean belowSpeedBump;
+    public float shadowAlpha;
+    public boolean inShelf;
+
+    /**
+     * How much the child overlaps with the previous child on top. This is used to
+     * show the background properly when the child on top is translating away.
+     */
+    public int clipTopAmount;
+
+    /**
+     * The index of the view, only accounting for views not equal to GONE
+     */
+    public int notGoneIndex;
+
+    /**
+     * The location this view is currently rendered at.
+     *
+     * <p>See <code>LOCATION_</code> flags.</p>
+     */
+    public int location;
+
+    @Override
+    public void copyFrom(ViewState viewState) {
+        super.copyFrom(viewState);
+        if (viewState instanceof ExpandableViewState) {
+            ExpandableViewState svs = (ExpandableViewState) viewState;
+            height = svs.height;
+            dimmed = svs.dimmed;
+            shadowAlpha = svs.shadowAlpha;
+            dark = svs.dark;
+            hideSensitive = svs.hideSensitive;
+            belowSpeedBump = svs.belowSpeedBump;
+            clipTopAmount = svs.clipTopAmount;
+            notGoneIndex = svs.notGoneIndex;
+            location = svs.location;
+        }
+    }
+
+    /**
+     * Applies a {@link ExpandableViewState} to a {@link ExpandableView}.
+     */
+    @Override
+    public void applyToView(View view) {
+        super.applyToView(view);
+        if (view instanceof ExpandableView) {
+            ExpandableView expandableView = (ExpandableView) view;
+
+            int height = expandableView.getActualHeight();
+            int newHeight = this.height;
+
+            // apply height
+            if (height != newHeight) {
+                expandableView.setActualHeight(newHeight, false /* notifyListeners */);
+            }
+
+            float shadowAlpha = expandableView.getShadowAlpha();
+            float newShadowAlpha = this.shadowAlpha;
+
+            // apply shadowAlpha
+            if (shadowAlpha != newShadowAlpha) {
+                expandableView.setShadowAlpha(newShadowAlpha);
+            }
+
+            // apply dimming
+            expandableView.setDimmed(this.dimmed, false /* animate */);
+
+            // apply hiding sensitive
+            expandableView.setHideSensitive(
+                    this.hideSensitive, false /* animated */, 0 /* delay */, 0 /* duration */);
+
+            // apply below shelf speed bump
+            expandableView.setBelowSpeedBump(this.belowSpeedBump);
+
+            // apply dark
+            expandableView.setDark(this.dark, false /* animate */, 0 /* delay */);
+
+            // apply clipping
+            float oldClipTopAmount = expandableView.getClipTopAmount();
+            if (oldClipTopAmount != this.clipTopAmount) {
+                expandableView.setClipTopAmount(this.clipTopAmount);
+            }
+
+            expandableView.setTransformingInShelf(false);
+            expandableView.setInShelf(inShelf);
+        }
+    }
+
+    @Override
+    public void animateTo(View child, AnimationProperties properties) {
+        super.animateTo(child, properties);
+        if (!(child instanceof ExpandableView)) {
+            return;
+        }
+        ExpandableView expandableView = (ExpandableView) child;
+        AnimationFilter animationFilter = properties.getAnimationFilter();
+
+        // start height animation
+        if (this.height != expandableView.getActualHeight()) {
+            startHeightAnimation(expandableView, properties);
+        }  else {
+            abortAnimation(child, TAG_ANIMATOR_HEIGHT);
+        }
+
+        // start shadow alpha animation
+        if (this.shadowAlpha != expandableView.getShadowAlpha()) {
+            startShadowAlphaAnimation(expandableView, properties);
+        } else {
+            abortAnimation(child, TAG_ANIMATOR_SHADOW_ALPHA);
+        }
+
+        // start top inset animation
+        if (this.clipTopAmount != expandableView.getClipTopAmount()) {
+            startInsetAnimation(expandableView, properties);
+        } else {
+            abortAnimation(child, TAG_ANIMATOR_TOP_INSET);
+        }
+
+        // start dimmed animation
+        expandableView.setDimmed(this.dimmed, animationFilter.animateDimmed);
+
+        // apply below the speed bump
+        expandableView.setBelowSpeedBump(this.belowSpeedBump);
+
+        // start hiding sensitive animation
+        expandableView.setHideSensitive(this.hideSensitive, animationFilter.animateHideSensitive,
+                properties.delay, properties.duration);
+
+        // start dark animation
+        expandableView.setDark(this.dark, animationFilter.animateDark, properties.delay);
+
+        if (properties.wasAdded(child) && !hidden) {
+            expandableView.performAddAnimation(properties.delay, properties.duration);
+        }
+
+        if (!expandableView.isInShelf() && this.inShelf) {
+            expandableView.setTransformingInShelf(true);
+        }
+        expandableView.setInShelf(this.inShelf);
+    }
+
+    private void startHeightAnimation(final ExpandableView child, AnimationProperties properties) {
+        Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT);
+        Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT);
+        int newEndValue = this.height;
+        if (previousEndValue != null && previousEndValue == newEndValue) {
+            return;
+        }
+        ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT);
+        AnimationFilter filter = properties.getAnimationFilter();
+        if (!filter.animateHeight) {
+            // just a local update was performed
+            if (previousAnimator != null) {
+                // we need to increase all animation keyframes of the previous animator by the
+                // relative change to the end value
+                PropertyValuesHolder[] values = previousAnimator.getValues();
+                int relativeDiff = newEndValue - previousEndValue;
+                int newStartValue = previousStartValue + relativeDiff;
+                values[0].setIntValues(newStartValue, newEndValue);
+                child.setTag(TAG_START_HEIGHT, newStartValue);
+                child.setTag(TAG_END_HEIGHT, newEndValue);
+                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+                return;
+            } else {
+                // no new animation needed, let's just apply the value
+                child.setActualHeight(newEndValue, false);
+                return;
+            }
+        }
+
+        ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), newEndValue);
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                child.setActualHeight((int) animation.getAnimatedValue(),
+                        false /* notifyListeners */);
+            }
+        });
+        animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
+        animator.setDuration(newDuration);
+        if (properties.delay > 0 && (previousAnimator == null
+                || previousAnimator.getAnimatedFraction() == 0)) {
+            animator.setStartDelay(properties.delay);
+        }
+        AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+        if (listener != null) {
+            animator.addListener(listener);
+        }
+        // remove the tag when the animation is finished
+        animator.addListener(new AnimatorListenerAdapter() {
+            boolean mWasCancelled;
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                child.setTag(TAG_ANIMATOR_HEIGHT, null);
+                child.setTag(TAG_START_HEIGHT, null);
+                child.setTag(TAG_END_HEIGHT, null);
+                child.setActualHeightAnimating(false);
+                if (!mWasCancelled && child instanceof ExpandableNotificationRow) {
+                    ((ExpandableNotificationRow) child).setGroupExpansionChanging(
+                            false /* isExpansionChanging */);
+                }
+            }
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mWasCancelled = false;
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mWasCancelled = true;
+            }
+        });
+        startAnimator(animator, listener);
+        child.setTag(TAG_ANIMATOR_HEIGHT, animator);
+        child.setTag(TAG_START_HEIGHT, child.getActualHeight());
+        child.setTag(TAG_END_HEIGHT, newEndValue);
+        child.setActualHeightAnimating(true);
+    }
+
+    private void startShadowAlphaAnimation(final ExpandableView child,
+            AnimationProperties properties) {
+        Float previousStartValue = getChildTag(child, TAG_START_SHADOW_ALPHA);
+        Float previousEndValue = getChildTag(child, TAG_END_SHADOW_ALPHA);
+        float newEndValue = this.shadowAlpha;
+        if (previousEndValue != null && previousEndValue == newEndValue) {
+            return;
+        }
+        ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_SHADOW_ALPHA);
+        AnimationFilter filter = properties.getAnimationFilter();
+        if (!filter.animateShadowAlpha) {
+            // just a local update was performed
+            if (previousAnimator != null) {
+                // we need to increase all animation keyframes of the previous animator by the
+                // relative change to the end value
+                PropertyValuesHolder[] values = previousAnimator.getValues();
+                float relativeDiff = newEndValue - previousEndValue;
+                float newStartValue = previousStartValue + relativeDiff;
+                values[0].setFloatValues(newStartValue, newEndValue);
+                child.setTag(TAG_START_SHADOW_ALPHA, newStartValue);
+                child.setTag(TAG_END_SHADOW_ALPHA, newEndValue);
+                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+                return;
+            } else {
+                // no new animation needed, let's just apply the value
+                child.setShadowAlpha(newEndValue);
+                return;
+            }
+        }
+
+        ValueAnimator animator = ValueAnimator.ofFloat(child.getShadowAlpha(), newEndValue);
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                child.setShadowAlpha((float) animation.getAnimatedValue());
+            }
+        });
+        animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
+        animator.setDuration(newDuration);
+        if (properties.delay > 0 && (previousAnimator == null
+                || previousAnimator.getAnimatedFraction() == 0)) {
+            animator.setStartDelay(properties.delay);
+        }
+        AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+        if (listener != null) {
+            animator.addListener(listener);
+        }
+        // remove the tag when the animation is finished
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                child.setTag(TAG_ANIMATOR_SHADOW_ALPHA, null);
+                child.setTag(TAG_START_SHADOW_ALPHA, null);
+                child.setTag(TAG_END_SHADOW_ALPHA, null);
+            }
+        });
+        startAnimator(animator, listener);
+        child.setTag(TAG_ANIMATOR_SHADOW_ALPHA, animator);
+        child.setTag(TAG_START_SHADOW_ALPHA, child.getShadowAlpha());
+        child.setTag(TAG_END_SHADOW_ALPHA, newEndValue);
+    }
+
+    private void startInsetAnimation(final ExpandableView child, AnimationProperties properties) {
+        Integer previousStartValue = getChildTag(child, TAG_START_TOP_INSET);
+        Integer previousEndValue = getChildTag(child, TAG_END_TOP_INSET);
+        int newEndValue = this.clipTopAmount;
+        if (previousEndValue != null && previousEndValue == newEndValue) {
+            return;
+        }
+        ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TOP_INSET);
+        AnimationFilter filter = properties.getAnimationFilter();
+        if (!filter.animateTopInset) {
+            // just a local update was performed
+            if (previousAnimator != null) {
+                // we need to increase all animation keyframes of the previous animator by the
+                // relative change to the end value
+                PropertyValuesHolder[] values = previousAnimator.getValues();
+                int relativeDiff = newEndValue - previousEndValue;
+                int newStartValue = previousStartValue + relativeDiff;
+                values[0].setIntValues(newStartValue, newEndValue);
+                child.setTag(TAG_START_TOP_INSET, newStartValue);
+                child.setTag(TAG_END_TOP_INSET, newEndValue);
+                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+                return;
+            } else {
+                // no new animation needed, let's just apply the value
+                child.setClipTopAmount(newEndValue);
+                return;
+            }
+        }
+
+        ValueAnimator animator = ValueAnimator.ofInt(child.getClipTopAmount(), newEndValue);
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                child.setClipTopAmount((int) animation.getAnimatedValue());
+            }
+        });
+        animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
+        animator.setDuration(newDuration);
+        if (properties.delay > 0 && (previousAnimator == null
+                || previousAnimator.getAnimatedFraction() == 0)) {
+            animator.setStartDelay(properties.delay);
+        }
+        AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+        if (listener != null) {
+            animator.addListener(listener);
+        }
+        // remove the tag when the animation is finished
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                child.setTag(TAG_ANIMATOR_TOP_INSET, null);
+                child.setTag(TAG_START_TOP_INSET, null);
+                child.setTag(TAG_END_TOP_INSET, null);
+            }
+        });
+        startAnimator(animator, listener);
+        child.setTag(TAG_ANIMATOR_TOP_INSET, animator);
+        child.setTag(TAG_START_TOP_INSET, child.getClipTopAmount());
+        child.setTag(TAG_END_TOP_INSET, newEndValue);
+    }
+
+    /**
+     * Get the end value of the height animation running on a view or the actualHeight
+     * if no animation is running.
+     */
+    public static int getFinalActualHeight(ExpandableView view) {
+        if (view == null) {
+            return 0;
+        }
+        ValueAnimator heightAnimator = getChildTag(view, TAG_ANIMATOR_HEIGHT);
+        if (heightAnimator == null) {
+            return view.getActualHeight();
+        } else {
+            return getChildTag(view, TAG_END_HEIGHT);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/stack/HeadsUpAppearInterpolator.java b/com/android/systemui/statusbar/stack/HeadsUpAppearInterpolator.java
new file mode 100644
index 0000000..05c0099
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/HeadsUpAppearInterpolator.java
@@ -0,0 +1,51 @@
+/*
+ * 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.graphics.Path;
+import android.view.animation.PathInterpolator;
+
+/**
+ * An interpolator specifically designed for the appear animation of heads up notifications.
+ */
+public class HeadsUpAppearInterpolator extends PathInterpolator {
+    public HeadsUpAppearInterpolator() {
+        super(getAppearPath());
+    }
+
+    private static Path getAppearPath() {
+        Path path = new Path();
+        path.moveTo(0, 0);
+        float x1 = 250f;
+        float x2 = 150f;
+        float x3 = 100f;
+        float y1 = 90f;
+        float y2 = 78f;
+        float y3 = 80f;
+        float xTot = (x1 + x2 + x3);
+        path.cubicTo(x1 * 0.9f / xTot, 0f,
+                x1 * 0.8f / xTot, y1 / y3,
+                x1 / xTot , y1 / y3);
+        path.cubicTo((x1 + x2 * 0.4f) / xTot, y1 / y3,
+                (x1 + x2 * 0.2f) / xTot, y2 / y3,
+                (x1 + x2) / xTot, y2 / y3);
+        path.cubicTo((x1 + x2 + x3 * 0.4f) / xTot, y2 / y3,
+                (x1 + x2 + x3 * 0.2f) / xTot, 1f,
+                1f, 1f);
+        return path;
+    }
+}
diff --git a/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java b/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
new file mode 100644
index 0000000..fe53104
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
@@ -0,0 +1,1263 @@
+/*
+ * 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.app.Notification;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.drawable.ColorDrawable;
+import android.service.notification.StatusBarNotification;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.NotificationHeaderView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationHeaderUtil;
+import com.android.systemui.statusbar.notification.HybridGroupManager;
+import com.android.systemui.statusbar.notification.HybridNotificationView;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.NotificationViewWrapper;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A container containing child notifications
+ */
+public class NotificationChildrenContainer extends ViewGroup {
+
+    private static final int NUMBER_OF_CHILDREN_WHEN_COLLAPSED = 2;
+    private static final int NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED = 5;
+    private static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8;
+    private static final int NUMBER_OF_CHILDREN_WHEN_AMBIENT = 3;
+    private static final AnimationProperties ALPHA_FADE_IN = new AnimationProperties() {
+        private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
+
+        @Override
+        public AnimationFilter getAnimationFilter() {
+            return mAnimationFilter;
+        }
+    }.setDuration(200);
+
+    private final List<View> mDividers = new ArrayList<>();
+    private final List<ExpandableNotificationRow> mChildren = new ArrayList<>();
+    private final HybridGroupManager mHybridGroupManager;
+    private int mChildPadding;
+    private int mDividerHeight;
+    private float mDividerAlpha;
+    private int mNotificationHeaderMargin;
+
+    private int mNotificatonTopPadding;
+    private float mCollapsedBottompadding;
+    private boolean mChildrenExpanded;
+    private ExpandableNotificationRow mContainingNotification;
+    private TextView mOverflowNumber;
+    private ViewState mGroupOverFlowState;
+    private int mRealHeight;
+    private boolean mUserLocked;
+    private int mActualHeight;
+    private boolean mNeverAppliedGroupState;
+    private int mHeaderHeight;
+
+    /**
+     * Whether or not individual notifications that are part of this container will have shadows.
+     */
+    private boolean mEnableShadowOnChildNotifications;
+
+    private NotificationHeaderView mNotificationHeader;
+    private NotificationViewWrapper mNotificationHeaderWrapper;
+    private NotificationHeaderView mNotificationHeaderLowPriority;
+    private NotificationViewWrapper mNotificationHeaderWrapperLowPriority;
+    private ViewGroup mNotificationHeaderAmbient;
+    private NotificationViewWrapper mNotificationHeaderWrapperAmbient;
+    private NotificationHeaderUtil mHeaderUtil;
+    private ViewState mHeaderViewState;
+    private int mClipBottomAmount;
+    private boolean mIsLowPriority;
+    private OnClickListener mHeaderClickListener;
+    private ViewGroup mCurrentHeader;
+
+    private boolean mShowDividersWhenExpanded;
+    private boolean mHideDividersDuringExpand;
+
+    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);
+        initDimens();
+        mHybridGroupManager = new HybridGroupManager(getContext(), this);
+    }
+
+    private void initDimens() {
+        Resources res = getResources();
+        mChildPadding = res.getDimensionPixelSize(R.dimen.notification_children_padding);
+        mDividerHeight = res.getDimensionPixelSize(
+                R.dimen.notification_children_container_divider_height);
+        mDividerAlpha = res.getFloat(R.dimen.notification_divider_alpha);
+        mHeaderHeight = res.getDimensionPixelSize(
+                R.dimen.notification_children_container_header_height);
+        mNotificationHeaderMargin = res.getDimensionPixelSize(
+                R.dimen.notification_children_container_margin_top);
+        mNotificatonTopPadding = res.getDimensionPixelSize(
+                R.dimen.notification_children_container_top_padding);
+        mCollapsedBottompadding = res.getDimensionPixelSize(
+                com.android.internal.R.dimen.notification_content_margin_bottom);
+        mEnableShadowOnChildNotifications =
+                res.getBoolean(R.bool.config_enableShadowOnChildNotifications);
+        mShowDividersWhenExpanded =
+                res.getBoolean(R.bool.config_showDividersWhenGroupNotificationExpanded);
+        mHideDividersDuringExpand =
+                res.getBoolean(R.bool.config_hideDividersDuringExpand);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
+        for (int i = 0; i < childCount; i++) {
+            View child = mChildren.get(i);
+            // We need to layout all children even the GONE ones, such that the heights are
+            // calculated correctly as they are used to calculate how many we can fit on the screen
+            child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
+            mDividers.get(i).layout(0, 0, getWidth(), mDividerHeight);
+        }
+        if (mOverflowNumber != null) {
+            boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+            int left = (isRtl ? 0 : getWidth() - mOverflowNumber.getMeasuredWidth());
+            int right = left + mOverflowNumber.getMeasuredWidth();
+            mOverflowNumber.layout(left, 0, right, mOverflowNumber.getMeasuredHeight());
+        }
+        if (mNotificationHeader != null) {
+            mNotificationHeader.layout(0, 0, mNotificationHeader.getMeasuredWidth(),
+                    mNotificationHeader.getMeasuredHeight());
+        }
+        if (mNotificationHeaderLowPriority != null) {
+            mNotificationHeaderLowPriority.layout(0, 0,
+                    mNotificationHeaderLowPriority.getMeasuredWidth(),
+                    mNotificationHeaderLowPriority.getMeasuredHeight());
+        }
+        if (mNotificationHeaderAmbient != null) {
+            mNotificationHeaderAmbient.layout(0, 0,
+                    mNotificationHeaderAmbient.getMeasuredWidth(),
+                    mNotificationHeaderAmbient.getMeasuredHeight());
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
+        boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
+        int size = MeasureSpec.getSize(heightMeasureSpec);
+        int newHeightSpec = heightMeasureSpec;
+        if (hasFixedHeight || isHeightLimited) {
+            newHeightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
+        }
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+        if (mOverflowNumber != null) {
+            mOverflowNumber.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
+                    newHeightSpec);
+        }
+        int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY);
+        int height = mNotificationHeaderMargin + mNotificatonTopPadding;
+        int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
+        int collapsedChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
+        int overflowIndex = childCount > collapsedChildren ? collapsedChildren - 1 : -1;
+        for (int i = 0; i < childCount; i++) {
+            ExpandableNotificationRow child = mChildren.get(i);
+            // We need to measure all children even the GONE ones, such that the heights are
+            // calculated correctly as they are used to calculate how many we can fit on the screen.
+            boolean isOverflow = i == overflowIndex;
+            child.setSingleLineWidthIndention(isOverflow && mOverflowNumber != null
+                    ? mOverflowNumber.getMeasuredWidth()
+                    : 0);
+            child.measure(widthMeasureSpec, newHeightSpec);
+            // layout the divider
+            View divider = mDividers.get(i);
+            divider.measure(widthMeasureSpec, dividerHeightSpec);
+            if (child.getVisibility() != GONE) {
+                height += child.getMeasuredHeight() + mDividerHeight;
+            }
+        }
+        mRealHeight = height;
+        if (heightMode != MeasureSpec.UNSPECIFIED) {
+            height = Math.min(height, size);
+        }
+
+        int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY);
+        if (mNotificationHeader != null) {
+            mNotificationHeader.measure(widthMeasureSpec, headerHeightSpec);
+        }
+        if (mNotificationHeaderLowPriority != null) {
+            headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY);
+            mNotificationHeaderLowPriority.measure(widthMeasureSpec, headerHeightSpec);
+        }
+        if (mNotificationHeaderAmbient != null) {
+            headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY);
+            mNotificationHeaderAmbient.measure(widthMeasureSpec, headerHeightSpec);
+        }
+
+        setMeasuredDimension(width, height);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    @Override
+    public boolean pointInView(float localX, float localY, float slop) {
+        return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
+                localY < (mRealHeight + slop);
+    }
+
+    /**
+     * 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);
+        row.setUserLocked(mUserLocked);
+
+        View divider = inflateDivider();
+        addView(divider);
+        mDividers.add(newIndex, divider);
+
+        updateGroupOverflow();
+        row.setContentTransformationAmount(0, false /* isLastChild */);
+    }
+
+    public void removeNotification(ExpandableNotificationRow row) {
+        int childIndex = mChildren.indexOf(row);
+        mChildren.remove(row);
+        removeView(row);
+
+        final View divider = mDividers.remove(childIndex);
+        removeView(divider);
+        getOverlay().add(divider);
+        CrossFadeHelper.fadeOut(divider, new Runnable() {
+            @Override
+            public void run() {
+                getOverlay().remove(divider);
+            }
+        });
+
+        row.setSystemChildExpanded(false);
+        row.setUserLocked(false);
+        updateGroupOverflow();
+        if (!row.isRemoved()) {
+            mHeaderUtil.restoreNotificationHeader(row);
+        }
+    }
+
+    /**
+     * @return The number of notification children in the container.
+     */
+    public int getNotificationChildCount() {
+        return mChildren.size();
+    }
+
+    public void recreateNotificationHeader(OnClickListener listener) {
+        mHeaderClickListener = listener;
+        StatusBarNotification notification = mContainingNotification.getStatusBarNotification();
+        final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(),
+                notification.getNotification());
+        RemoteViews header = builder.makeNotificationHeader(false /* ambient */);
+        if (mNotificationHeader == null) {
+            mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this);
+            final View expandButton = mNotificationHeader.findViewById(
+                    com.android.internal.R.id.expand_button);
+            expandButton.setVisibility(VISIBLE);
+            mNotificationHeader.setOnClickListener(mHeaderClickListener);
+            mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(),
+                    mNotificationHeader, mContainingNotification);
+            addView(mNotificationHeader, 0);
+            invalidate();
+        } else {
+            header.reapply(getContext(), mNotificationHeader);
+        }
+        mNotificationHeaderWrapper.onContentUpdated(mContainingNotification);
+        recreateLowPriorityHeader(builder);
+        recreateAmbientHeader(builder);
+        updateHeaderVisibility(false /* animate */);
+        updateChildrenHeaderAppearance();
+    }
+
+    private void recreateAmbientHeader(Notification.Builder builder) {
+        RemoteViews header;
+        StatusBarNotification notification = mContainingNotification.getStatusBarNotification();
+        if (builder == null) {
+            builder = Notification.Builder.recoverBuilder(getContext(),
+                    notification.getNotification());
+        }
+        header = builder.makeNotificationHeader(true /* ambient */);
+        if (mNotificationHeaderAmbient == null) {
+            mNotificationHeaderAmbient = (ViewGroup) header.apply(getContext(), this);
+            mNotificationHeaderWrapperAmbient = NotificationViewWrapper.wrap(getContext(),
+                    mNotificationHeaderAmbient, mContainingNotification);
+            mNotificationHeaderWrapperAmbient.onContentUpdated(mContainingNotification);
+            addView(mNotificationHeaderAmbient, 0);
+            invalidate();
+        } else {
+            header.reapply(getContext(), mNotificationHeaderAmbient);
+        }
+        resetHeaderVisibilityIfNeeded(mNotificationHeaderAmbient, calculateDesiredHeader());
+        mNotificationHeaderWrapperAmbient.onContentUpdated(mContainingNotification);
+    }
+
+    /**
+     * Recreate the low-priority header.
+     *
+     * @param builder a builder to reuse. Otherwise the builder will be recovered.
+     */
+    private void recreateLowPriorityHeader(Notification.Builder builder) {
+        RemoteViews header;
+        StatusBarNotification notification = mContainingNotification.getStatusBarNotification();
+        if (mIsLowPriority) {
+            if (builder == null) {
+                builder = Notification.Builder.recoverBuilder(getContext(),
+                        notification.getNotification());
+            }
+            header = builder.makeLowPriorityContentView(true /* useRegularSubtext */);
+            if (mNotificationHeaderLowPriority == null) {
+                mNotificationHeaderLowPriority = (NotificationHeaderView) header.apply(getContext(),
+                        this);
+                final View expandButton = mNotificationHeaderLowPriority.findViewById(
+                        com.android.internal.R.id.expand_button);
+                expandButton.setVisibility(VISIBLE);
+                mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener);
+                mNotificationHeaderWrapperLowPriority = NotificationViewWrapper.wrap(getContext(),
+                        mNotificationHeaderLowPriority, mContainingNotification);
+                addView(mNotificationHeaderLowPriority, 0);
+                invalidate();
+            } else {
+                header.reapply(getContext(), mNotificationHeaderLowPriority);
+            }
+            mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification);
+            resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, calculateDesiredHeader());
+        } else {
+            removeView(mNotificationHeaderLowPriority);
+            mNotificationHeaderLowPriority = null;
+            mNotificationHeaderWrapperLowPriority = null;
+        }
+    }
+
+    public void updateChildrenHeaderAppearance() {
+        mHeaderUtil.updateChildrenHeaderAppearance();
+    }
+
+    public void updateGroupOverflow() {
+        int childCount = mChildren.size();
+        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
+        if (childCount > maxAllowedVisibleChildren) {
+            mOverflowNumber = mHybridGroupManager.bindOverflowNumber(
+                    mOverflowNumber, childCount - maxAllowedVisibleChildren);
+            if (mGroupOverFlowState == null) {
+                mGroupOverFlowState = new ViewState();
+                mNeverAppliedGroupState = true;
+            }
+        } else if (mOverflowNumber != null) {
+            removeView(mOverflowNumber);
+            if (isShown()) {
+                final View removedOverflowNumber = mOverflowNumber;
+                addTransientView(removedOverflowNumber, getTransientViewCount());
+                CrossFadeHelper.fadeOut(removedOverflowNumber, new Runnable() {
+                    @Override
+                    public void run() {
+                        removeTransientView(removedOverflowNumber);
+                    }
+                });
+            }
+            mOverflowNumber = null;
+            mGroupOverFlowState = null;
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        updateGroupOverflow();
+    }
+
+    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
+     * @param visualStabilityManager
+     * @param callback
+     * @return whether the list order has changed
+     */
+    public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder,
+            VisualStabilityManager visualStabilityManager,
+            VisualStabilityManager.Callback callback) {
+        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) {
+                if (visualStabilityManager.canReorderNotification(desiredChild)) {
+                    mChildren.remove(desiredChild);
+                    mChildren.add(i, desiredChild);
+                    result = true;
+                } else {
+                    visualStabilityManager.addReorderingAllowedCallback(callback);
+                }
+            }
+        }
+        updateExpansionStates();
+        return result;
+    }
+
+    private void updateExpansionStates() {
+        if (mChildrenExpanded || mUserLocked) {
+            // we don't modify it the group is expanded or if we are expanding it
+            return;
+        }
+        int size = mChildren.size();
+        for (int i = 0; i < size; i++) {
+            ExpandableNotificationRow child = mChildren.get(i);
+            child.setSystemChildExpanded(i == 0 && size == 1);
+        }
+    }
+
+    /**
+     *
+     * @return the intrinsic size of this children container, i.e the natural fully expanded state
+     */
+    public int getIntrinsicHeight() {
+        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
+        return getIntrinsicHeight(maxAllowedVisibleChildren);
+    }
+
+    /**
+     * @return the intrinsic height with a number of children given
+     *         in @param maxAllowedVisibleChildren
+     */
+    private int getIntrinsicHeight(float maxAllowedVisibleChildren) {
+        if (showingAsLowPriority()) {
+            return mNotificationHeaderLowPriority.getHeight();
+        }
+        int intrinsicHeight = mNotificationHeaderMargin;
+        int visibleChildren = 0;
+        int childCount = mChildren.size();
+        boolean firstChild = true;
+        float expandFactor = 0;
+        if (mUserLocked) {
+            expandFactor = getGroupExpandFraction();
+        }
+        boolean childrenExpanded = mChildrenExpanded || mContainingNotification.isShowingAmbient();
+        for (int i = 0; i < childCount; i++) {
+            if (visibleChildren >= maxAllowedVisibleChildren) {
+                break;
+            }
+            if (!firstChild) {
+                if (mUserLocked) {
+                    intrinsicHeight += NotificationUtils.interpolate(mChildPadding, mDividerHeight,
+                            expandFactor);
+                } else {
+                    intrinsicHeight += childrenExpanded ? mDividerHeight : mChildPadding;
+                }
+            } else {
+                if (mUserLocked) {
+                    intrinsicHeight += NotificationUtils.interpolate(
+                            0,
+                            mNotificatonTopPadding + mDividerHeight,
+                            expandFactor);
+                } else {
+                    intrinsicHeight += childrenExpanded
+                            ? mNotificatonTopPadding + mDividerHeight
+                            : 0;
+                }
+                firstChild = false;
+            }
+            ExpandableNotificationRow child = mChildren.get(i);
+            intrinsicHeight += child.getIntrinsicHeight();
+            visibleChildren++;
+        }
+        if (mUserLocked) {
+            intrinsicHeight += NotificationUtils.interpolate(mCollapsedBottompadding, 0.0f,
+                    expandFactor);
+        } else if (!childrenExpanded) {
+            intrinsicHeight += mCollapsedBottompadding;
+        }
+        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, ExpandableViewState parentState) {
+        int childCount = mChildren.size();
+        int yPosition = mNotificationHeaderMargin;
+        boolean firstChild = true;
+        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
+        int lastVisibleIndex = maxAllowedVisibleChildren - 1;
+        int firstOverflowIndex = lastVisibleIndex + 1;
+        float expandFactor = 0;
+        boolean expandingToExpandedGroup = mUserLocked && !showingAsLowPriority();
+        if (mUserLocked) {
+            expandFactor = getGroupExpandFraction();
+            firstOverflowIndex = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
+        }
+
+        boolean childrenExpandedAndNotAnimating = mChildrenExpanded
+                && !mContainingNotification.isGroupExpansionChanging();
+        for (int i = 0; i < childCount; i++) {
+            ExpandableNotificationRow child = mChildren.get(i);
+            if (!firstChild) {
+                if (expandingToExpandedGroup) {
+                    yPosition += NotificationUtils.interpolate(mChildPadding, mDividerHeight,
+                            expandFactor);
+                } else {
+                    yPosition += mChildrenExpanded ? mDividerHeight : mChildPadding;
+                }
+            } else {
+                if (expandingToExpandedGroup) {
+                    yPosition += NotificationUtils.interpolate(
+                            0,
+                            mNotificatonTopPadding + mDividerHeight,
+                            expandFactor);
+                } else {
+                    yPosition += mChildrenExpanded ? mNotificatonTopPadding + mDividerHeight : 0;
+                }
+                firstChild = false;
+            }
+
+            ExpandableViewState childState = resultState.getViewStateForView(child);
+            int intrinsicHeight = child.getIntrinsicHeight();
+            childState.height = intrinsicHeight;
+            childState.yTranslation = yPosition;
+            childState.hidden = false;
+            // When the group is expanded, the children cast the shadows rather than the parent
+            // so use the parent's elevation here.
+            childState.zTranslation =
+                    (childrenExpandedAndNotAnimating && mEnableShadowOnChildNotifications)
+                    ? mContainingNotification.getTranslationZ()
+                    : 0;
+            childState.dimmed = parentState.dimmed;
+            childState.dark = parentState.dark;
+            childState.hideSensitive = parentState.hideSensitive;
+            childState.belowSpeedBump = parentState.belowSpeedBump;
+            childState.clipTopAmount = 0;
+            childState.alpha = 0;
+            if (i < firstOverflowIndex) {
+                childState.alpha = showingAsLowPriority() ? expandFactor : 1.0f;
+            } else if (expandFactor == 1.0f && i <= lastVisibleIndex) {
+                childState.alpha = (mActualHeight - childState.yTranslation) / childState.height;
+                childState.alpha = Math.max(0.0f, Math.min(1.0f, childState.alpha));
+            }
+            childState.location = parentState.location;
+            childState.inShelf = parentState.inShelf;
+            yPosition += intrinsicHeight;
+
+        }
+        if (mOverflowNumber != null) {
+            ExpandableNotificationRow overflowView = mChildren.get(Math.min(
+                    getMaxAllowedVisibleChildren(true /* likeCollpased */), childCount) - 1);
+            mGroupOverFlowState.copyFrom(resultState.getViewStateForView(overflowView));
+
+            if (mContainingNotification.isShowingAmbient() || !mChildrenExpanded) {
+                HybridNotificationView alignView = null;
+                if (mContainingNotification.isShowingAmbient()) {
+                    alignView = overflowView.getAmbientSingleLineView();
+                } else if (mUserLocked) {
+                    alignView = overflowView.getSingleLineView();
+                }
+                if (alignView != null) {
+                    View mirrorView = alignView.getTextView();
+                    if (mirrorView.getVisibility() == GONE) {
+                        mirrorView = alignView.getTitleView();
+                    }
+                    if (mirrorView.getVisibility() == GONE) {
+                        mirrorView = alignView;
+                    }
+                    mGroupOverFlowState.yTranslation += NotificationUtils.getRelativeYOffset(
+                            mirrorView, overflowView);
+                    mGroupOverFlowState.alpha = mirrorView.getAlpha();
+                }
+            } else {
+                mGroupOverFlowState.yTranslation += mNotificationHeaderMargin;
+                mGroupOverFlowState.alpha = 0.0f;
+            }
+        }
+        if (mNotificationHeader != null) {
+            if (mHeaderViewState == null) {
+                mHeaderViewState = new ViewState();
+            }
+            mHeaderViewState.initFrom(mNotificationHeader);
+            mHeaderViewState.zTranslation = childrenExpandedAndNotAnimating
+                    ? mContainingNotification.getTranslationZ()
+                    : 0;
+        }
+    }
+
+    /**
+     * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its
+     * height, children in the group after this are gone.
+     *
+     * @param child the child who's height to adjust.
+     * @param parentHeight the height of the parent.
+     * @param childState the state to update.
+     * @param yPosition the yPosition of the view.
+     * @return true if children after this one should be hidden.
+     */
+    private boolean updateChildStateForExpandedGroup(ExpandableNotificationRow child,
+            int parentHeight, ExpandableViewState childState, int yPosition) {
+        final int top = yPosition + child.getClipTopAmount();
+        final int intrinsicHeight = child.getIntrinsicHeight();
+        final int bottom = top + intrinsicHeight;
+        int newHeight = intrinsicHeight;
+        if (bottom >= parentHeight) {
+            // Child is either clipped or gone
+            newHeight = Math.max((parentHeight - top), 0);
+        }
+        childState.hidden = newHeight == 0;
+        childState.height = newHeight;
+        return childState.height != intrinsicHeight && !childState.hidden;
+    }
+
+    private int getMaxAllowedVisibleChildren() {
+        return getMaxAllowedVisibleChildren(false /* likeCollapsed */);
+    }
+
+    private int getMaxAllowedVisibleChildren(boolean likeCollapsed) {
+        if (mContainingNotification.isShowingAmbient()) {
+            return NUMBER_OF_CHILDREN_WHEN_AMBIENT;
+        }
+        if (!likeCollapsed && (mChildrenExpanded || mContainingNotification.isUserLocked())) {
+            return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED;
+        }
+        if (mIsLowPriority || !mContainingNotification.isOnKeyguard()
+                && (mContainingNotification.isExpanded() || mContainingNotification.isHeadsUp())) {
+            return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED;
+        }
+        return NUMBER_OF_CHILDREN_WHEN_COLLAPSED;
+    }
+
+    public void applyState(StackScrollState state) {
+        int childCount = mChildren.size();
+        ViewState tmpState = new ViewState();
+        float expandFraction = 0.0f;
+        if (mUserLocked) {
+            expandFraction = getGroupExpandFraction();
+        }
+        final boolean dividersVisible = mUserLocked && !showingAsLowPriority()
+                || (mChildrenExpanded && mShowDividersWhenExpanded)
+                || (mContainingNotification.isGroupExpansionChanging()
+                && !mHideDividersDuringExpand);
+        for (int i = 0; i < childCount; i++) {
+            ExpandableNotificationRow child = mChildren.get(i);
+            ExpandableViewState viewState = state.getViewStateForView(child);
+            viewState.applyToView(child);
+
+            // layout the divider
+            View divider = mDividers.get(i);
+            tmpState.initFrom(divider);
+            tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
+            float alpha = mChildrenExpanded && viewState.alpha != 0 ? mDividerAlpha : 0;
+            if (mUserLocked && !showingAsLowPriority() && viewState.alpha != 0) {
+                alpha = NotificationUtils.interpolate(0, 0.5f,
+                        Math.min(viewState.alpha, expandFraction));
+            }
+            tmpState.hidden = !dividersVisible;
+            tmpState.alpha = alpha;
+            tmpState.applyToView(divider);
+            // There is no fake shadow to be drawn on the children
+            child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
+        }
+        if (mGroupOverFlowState != null) {
+            mGroupOverFlowState.applyToView(mOverflowNumber);
+            mNeverAppliedGroupState = false;
+        }
+        if (mHeaderViewState != null) {
+            mHeaderViewState.applyToView(mNotificationHeader);
+        }
+        updateChildrenClipping();
+    }
+
+    private void updateChildrenClipping() {
+        int childCount = mChildren.size();
+        int layoutEnd = mContainingNotification.getActualHeight() - mClipBottomAmount;
+        for (int i = 0; i < childCount; i++) {
+            ExpandableNotificationRow child = mChildren.get(i);
+            if (child.getVisibility() == GONE) {
+                continue;
+            }
+            float childTop = child.getTranslationY();
+            float childBottom = childTop + child.getActualHeight();
+            boolean visible = true;
+            int clipBottomAmount = 0;
+            if (childTop > layoutEnd) {
+                visible = false;
+            } else if (childBottom > layoutEnd) {
+                clipBottomAmount = (int) (childBottom - layoutEnd);
+            }
+
+            boolean isVisible = child.getVisibility() == VISIBLE;
+            if (visible != isVisible) {
+                child.setVisibility(visible ? VISIBLE : INVISIBLE);
+            }
+
+            child.setClipBottomAmount(clipBottomAmount);
+        }
+    }
+
+    /**
+     * 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) {
+        // TODO: do something that makes sense, like placing the invisible views correctly
+        return;
+    }
+
+    public void startAnimationToState(StackScrollState state, AnimationProperties properties) {
+        int childCount = mChildren.size();
+        ViewState tmpState = new ViewState();
+        float expandFraction = getGroupExpandFraction();
+        final boolean dividersVisible = mUserLocked && !showingAsLowPriority()
+                || (mChildrenExpanded && mShowDividersWhenExpanded)
+                || (mContainingNotification.isGroupExpansionChanging()
+                && !mHideDividersDuringExpand);
+        for (int i = childCount - 1; i >= 0; i--) {
+            ExpandableNotificationRow child = mChildren.get(i);
+            ExpandableViewState viewState = state.getViewStateForView(child);
+            viewState.animateTo(child, properties);
+
+            // layout the divider
+            View divider = mDividers.get(i);
+            tmpState.initFrom(divider);
+            tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
+            float alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0;
+            if (mUserLocked && !showingAsLowPriority() && viewState.alpha != 0) {
+                alpha = NotificationUtils.interpolate(0, 0.5f,
+                        Math.min(viewState.alpha, expandFraction));
+            }
+            tmpState.hidden = !dividersVisible;
+            tmpState.alpha = alpha;
+            tmpState.animateTo(divider, properties);
+            // There is no fake shadow to be drawn on the children
+            child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
+        }
+        if (mOverflowNumber != null) {
+            if (mNeverAppliedGroupState) {
+                float alpha = mGroupOverFlowState.alpha;
+                mGroupOverFlowState.alpha = 0;
+                mGroupOverFlowState.applyToView(mOverflowNumber);
+                mGroupOverFlowState.alpha = alpha;
+                mNeverAppliedGroupState = false;
+            }
+            mGroupOverFlowState.animateTo(mOverflowNumber, properties);
+        }
+        if (mNotificationHeader != null) {
+            mHeaderViewState.applyToView(mNotificationHeader);
+        }
+        updateChildrenClipping();
+    }
+
+    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 setChildrenExpanded(boolean childrenExpanded) {
+        mChildrenExpanded = childrenExpanded;
+        updateExpansionStates();
+        if (mNotificationHeader != null) {
+            mNotificationHeader.setExpanded(childrenExpanded);
+        }
+        final int count = mChildren.size();
+        for (int childIdx = 0; childIdx < count; childIdx++) {
+            ExpandableNotificationRow child = mChildren.get(childIdx);
+            child.setChildrenExpanded(childrenExpanded, false);
+        }
+    }
+
+    public void setContainingNotification(ExpandableNotificationRow parent) {
+        mContainingNotification = parent;
+        mHeaderUtil = new NotificationHeaderUtil(mContainingNotification);
+    }
+
+    public ExpandableNotificationRow getContainingNotification() {
+        return mContainingNotification;
+    }
+
+    public NotificationHeaderView getHeaderView() {
+        return mNotificationHeader;
+    }
+
+    public NotificationHeaderView getLowPriorityHeaderView() {
+        return mNotificationHeaderLowPriority;
+    }
+
+    @VisibleForTesting
+    public ViewGroup getCurrentHeaderView() {
+        return mCurrentHeader;
+    }
+
+    public void notifyShowAmbientChanged() {
+        updateHeaderVisibility(false);
+    }
+
+    private void updateHeaderVisibility(boolean animate) {
+        ViewGroup desiredHeader;
+        ViewGroup currentHeader = mCurrentHeader;
+        desiredHeader = calculateDesiredHeader();
+
+        if (currentHeader == desiredHeader) {
+            return;
+        }
+        if (desiredHeader == mNotificationHeaderAmbient
+                || currentHeader == mNotificationHeaderAmbient) {
+            animate = false;
+        }
+
+        if (animate) {
+            if (desiredHeader != null && currentHeader != null) {
+                currentHeader.setVisibility(VISIBLE);
+                desiredHeader.setVisibility(VISIBLE);
+                NotificationViewWrapper visibleWrapper = getWrapperForView(desiredHeader);
+                NotificationViewWrapper hiddenWrapper = getWrapperForView(currentHeader);
+                visibleWrapper.transformFrom(hiddenWrapper);
+                hiddenWrapper.transformTo(visibleWrapper, () -> updateHeaderVisibility(false));
+                startChildAlphaAnimations(desiredHeader == mNotificationHeader);
+            } else {
+                animate = false;
+            }
+        }
+        if (!animate) {
+            if (desiredHeader != null) {
+                getWrapperForView(desiredHeader).setVisible(true);
+                desiredHeader.setVisibility(VISIBLE);
+            }
+            if (currentHeader != null) {
+                // Wrapper can be null if we were a low priority notification
+                // and just destroyed it by calling setIsLowPriority(false)
+                NotificationViewWrapper wrapper = getWrapperForView(currentHeader);
+                if (wrapper != null) {
+                    wrapper.setVisible(false);
+                }
+                currentHeader.setVisibility(INVISIBLE);
+            }
+        }
+
+        resetHeaderVisibilityIfNeeded(mNotificationHeader, desiredHeader);
+        resetHeaderVisibilityIfNeeded(mNotificationHeaderAmbient, desiredHeader);
+        resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, desiredHeader);
+
+        mCurrentHeader = desiredHeader;
+    }
+
+    private void resetHeaderVisibilityIfNeeded(View header, View desiredHeader) {
+        if (header == null) {
+            return;
+        }
+        if (header != mCurrentHeader && header != desiredHeader) {
+            getWrapperForView(header).setVisible(false);
+            header.setVisibility(INVISIBLE);
+        }
+        if (header == desiredHeader && header.getVisibility() != VISIBLE) {
+            getWrapperForView(header).setVisible(true);
+            header.setVisibility(VISIBLE);
+        }
+    }
+
+    private ViewGroup calculateDesiredHeader() {
+        ViewGroup desiredHeader;
+        if (mContainingNotification.isShowingAmbient()) {
+            desiredHeader = mNotificationHeaderAmbient;
+        } else if (showingAsLowPriority()) {
+            desiredHeader = mNotificationHeaderLowPriority;
+        } else {
+            desiredHeader = mNotificationHeader;
+        }
+        return desiredHeader;
+    }
+
+    private void startChildAlphaAnimations(boolean toVisible) {
+        float target = toVisible ? 1.0f : 0.0f;
+        float start = 1.0f - target;
+        int childCount = mChildren.size();
+        for (int i = 0; i < childCount; i++) {
+            if (i >= NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED) {
+                break;
+            }
+            ExpandableNotificationRow child = mChildren.get(i);
+            child.setAlpha(start);
+            ViewState viewState = new ViewState();
+            viewState.initFrom(child);
+            viewState.alpha = target;
+            ALPHA_FADE_IN.setDelay(i * 50);
+            viewState.animateTo(child, ALPHA_FADE_IN);
+        }
+    }
+
+
+    private void updateHeaderTransformation() {
+        if (mUserLocked && showingAsLowPriority()) {
+            float fraction = getGroupExpandFraction();
+            mNotificationHeaderWrapper.transformFrom(mNotificationHeaderWrapperLowPriority,
+                    fraction);
+            mNotificationHeader.setVisibility(VISIBLE);
+            mNotificationHeaderWrapperLowPriority.transformTo(mNotificationHeaderWrapper,
+                    fraction);
+        }
+
+    }
+
+    private NotificationViewWrapper getWrapperForView(View visibleHeader) {
+        if (visibleHeader == mNotificationHeader) {
+            return mNotificationHeaderWrapper;
+        }
+        if (visibleHeader == mNotificationHeaderAmbient) {
+            return mNotificationHeaderWrapperAmbient;
+        }
+        return mNotificationHeaderWrapperLowPriority;
+    }
+
+    /**
+     * Called when a groups expansion changes to adjust the background of the header view.
+     *
+     * @param expanded whether the group is expanded.
+     */
+    public void updateHeaderForExpansion(boolean expanded) {
+        if (mNotificationHeader != null) {
+            if (expanded) {
+                ColorDrawable cd = new ColorDrawable();
+                cd.setColor(mContainingNotification.calculateBgColor());
+                mNotificationHeader.setHeaderBackgroundDrawable(cd);
+            } else {
+                mNotificationHeader.setHeaderBackgroundDrawable(null);
+            }
+        }
+    }
+
+    public int getMaxContentHeight() {
+        if (showingAsLowPriority()) {
+            return getMinHeight(NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED, true
+                    /* likeHighPriority */);
+        }
+        int maxContentHeight = mNotificationHeaderMargin + mNotificatonTopPadding;
+        int visibleChildren = 0;
+        int childCount = mChildren.size();
+        for (int i = 0; i < childCount; i++) {
+            if (visibleChildren >= NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED) {
+                break;
+            }
+            ExpandableNotificationRow child = mChildren.get(i);
+            float childHeight = child.isExpanded(true /* allowOnKeyguard */)
+                    ? child.getMaxExpandHeight()
+                    : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
+            maxContentHeight += childHeight;
+            visibleChildren++;
+        }
+        if (visibleChildren > 0) {
+            maxContentHeight += visibleChildren * mDividerHeight;
+        }
+        return maxContentHeight;
+    }
+
+    public void setActualHeight(int actualHeight) {
+        if (!mUserLocked) {
+            return;
+        }
+        mActualHeight = actualHeight;
+        float fraction = getGroupExpandFraction();
+        boolean showingLowPriority = showingAsLowPriority();
+        updateHeaderTransformation();
+        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
+        int childCount = mChildren.size();
+        for (int i = 0; i < childCount; i++) {
+            ExpandableNotificationRow child = mChildren.get(i);
+            float childHeight;
+            if (showingLowPriority) {
+                childHeight = child.getShowingLayout().getMinHeight(false /* likeGroupExpanded */);
+            } else if (child.isExpanded(true /* allowOnKeyguard */)) {
+                childHeight = child.getMaxExpandHeight();
+            } else {
+                childHeight = child.getShowingLayout().getMinHeight(
+                        true /* likeGroupExpanded */);
+            }
+            if (i < maxAllowedVisibleChildren) {
+                float singleLineHeight = child.getShowingLayout().getMinHeight(
+                        false /* likeGroupExpanded */);
+                child.setActualHeight((int) NotificationUtils.interpolate(singleLineHeight,
+                        childHeight, fraction), false);
+            } else {
+                child.setActualHeight((int) childHeight, false);
+            }
+        }
+    }
+
+    public float getGroupExpandFraction() {
+        int visibleChildrenExpandedHeight = showingAsLowPriority() ? getMaxContentHeight()
+                : getVisibleChildrenExpandHeight();
+        int minExpandHeight = getCollapsedHeight();
+        float factor = (mActualHeight - minExpandHeight)
+                / (float) (visibleChildrenExpandedHeight - minExpandHeight);
+        return Math.max(0.0f, Math.min(1.0f, factor));
+    }
+
+    private int getVisibleChildrenExpandHeight() {
+        int intrinsicHeight = mNotificationHeaderMargin + mNotificatonTopPadding + mDividerHeight;
+        int visibleChildren = 0;
+        int childCount = mChildren.size();
+        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
+        for (int i = 0; i < childCount; i++) {
+            if (visibleChildren >= maxAllowedVisibleChildren) {
+                break;
+            }
+            ExpandableNotificationRow child = mChildren.get(i);
+            float childHeight = child.isExpanded(true /* allowOnKeyguard */)
+                    ? child.getMaxExpandHeight()
+                    : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
+            intrinsicHeight += childHeight;
+            visibleChildren++;
+        }
+        return intrinsicHeight;
+    }
+
+    public int getMinHeight() {
+        return getMinHeight(mContainingNotification.isShowingAmbient()
+                ? NUMBER_OF_CHILDREN_WHEN_AMBIENT
+                : NUMBER_OF_CHILDREN_WHEN_COLLAPSED, false /* likeHighPriority */);
+    }
+
+    public int getCollapsedHeight() {
+        return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */),
+                false /* likeHighPriority */);
+    }
+
+    /**
+     * Get the minimum Height for this group.
+     *
+     * @param maxAllowedVisibleChildren the number of children that should be visible
+     * @param likeHighPriority if the height should be calculated as if it were not low priority
+     */
+    private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority) {
+        if (!likeHighPriority && showingAsLowPriority()) {
+            return mNotificationHeaderLowPriority.getHeight();
+        }
+        int minExpandHeight = mNotificationHeaderMargin;
+        int visibleChildren = 0;
+        boolean firstChild = true;
+        int childCount = mChildren.size();
+        for (int i = 0; i < childCount; i++) {
+            if (visibleChildren >= maxAllowedVisibleChildren) {
+                break;
+            }
+            if (!firstChild) {
+                minExpandHeight += mChildPadding;
+            } else {
+                firstChild = false;
+            }
+            ExpandableNotificationRow child = mChildren.get(i);
+            minExpandHeight += child.getSingleLineView().getHeight();
+            visibleChildren++;
+        }
+        minExpandHeight += mCollapsedBottompadding;
+        return minExpandHeight;
+    }
+
+    public boolean showingAsLowPriority() {
+        return mIsLowPriority && !mContainingNotification.isExpanded();
+    }
+
+    public void setDark(boolean dark, boolean fade, long delay) {
+        if (mOverflowNumber != null) {
+            mHybridGroupManager.setOverflowNumberDark(mOverflowNumber, dark, fade, delay);
+        }
+        mNotificationHeaderWrapper.setDark(dark, fade, delay);
+    }
+
+    public void reInflateViews(OnClickListener listener, StatusBarNotification notification) {
+        if (mNotificationHeader != null) {
+            removeView(mNotificationHeader);
+            mNotificationHeader = null;
+        }
+        if (mNotificationHeaderLowPriority != null) {
+            removeView(mNotificationHeaderLowPriority);
+            mNotificationHeaderLowPriority = null;
+        }
+        if (mNotificationHeaderAmbient != null) {
+            removeView(mNotificationHeaderAmbient);
+            mNotificationHeaderAmbient = null;
+        }
+        recreateNotificationHeader(listener);
+        initDimens();
+        for (int i = 0; i < mDividers.size(); i++) {
+            View prevDivider = mDividers.get(i);
+            int index = indexOfChild(prevDivider);
+            removeView(prevDivider);
+            View divider = inflateDivider();
+            addView(divider, index);
+            mDividers.set(i, divider);
+        }
+        removeView(mOverflowNumber);
+        mOverflowNumber = null;
+        mGroupOverFlowState = null;
+        updateGroupOverflow();
+    }
+
+    public void setUserLocked(boolean userLocked) {
+        mUserLocked = userLocked;
+        if (!mUserLocked) {
+            updateHeaderVisibility(false /* animate */);
+        }
+        int childCount = mChildren.size();
+        for (int i = 0; i < childCount; i++) {
+            ExpandableNotificationRow child = mChildren.get(i);
+            child.setUserLocked(userLocked && !showingAsLowPriority());
+        }
+    }
+
+    public void onNotificationUpdated() {
+        mHybridGroupManager.setOverflowNumberColor(mOverflowNumber,
+                mContainingNotification.getNotificationColor(),
+                mContainingNotification.getNotificationColorAmbient());
+    }
+
+    public int getPositionInLinearLayout(View childInGroup) {
+        int position = mNotificationHeaderMargin + mNotificatonTopPadding;
+
+        for (int i = 0; i < mChildren.size(); i++) {
+            ExpandableNotificationRow child = mChildren.get(i);
+            boolean notGone = child.getVisibility() != View.GONE;
+            if (notGone) {
+                position += mDividerHeight;
+            }
+            if (child == childInGroup) {
+                return position;
+            }
+            if (notGone) {
+                position += child.getIntrinsicHeight();
+            }
+        }
+        return 0;
+    }
+
+    public void setIconsVisible(boolean iconsVisible) {
+        if (mNotificationHeaderWrapper != null) {
+            NotificationHeaderView header = mNotificationHeaderWrapper.getNotificationHeader();
+            if (header != null) {
+                header.getIcon().setForceHidden(!iconsVisible);
+            }
+        }
+        if (mNotificationHeaderWrapperLowPriority != null) {
+            NotificationHeaderView header
+                    = mNotificationHeaderWrapperLowPriority.getNotificationHeader();
+            if (header != null) {
+                header.getIcon().setForceHidden(!iconsVisible);
+            }
+        }
+    }
+
+    public void setClipBottomAmount(int clipBottomAmount) {
+        mClipBottomAmount = clipBottomAmount;
+        updateChildrenClipping();
+    }
+
+    public void setIsLowPriority(boolean isLowPriority) {
+        mIsLowPriority = isLowPriority;
+        if (mContainingNotification != null) { /* we're not yet set up yet otherwise */
+            recreateLowPriorityHeader(null /* existingBuilder */);
+            updateHeaderVisibility(false /* animate */);
+        }
+        if (mUserLocked) {
+            setUserLocked(mUserLocked);
+        }
+    }
+
+    public NotificationHeaderView getVisibleHeader() {
+        NotificationHeaderView header = mNotificationHeader;
+        if (showingAsLowPriority()) {
+            header = mNotificationHeaderLowPriority;
+        }
+        return header;
+    }
+
+    public void onExpansionChanged() {
+        if (mIsLowPriority) {
+            if (mUserLocked) {
+                setUserLocked(mUserLocked);
+            }
+            updateHeaderVisibility(true /* animate */);
+        }
+    }
+
+    public float getIncreasedPaddingAmount() {
+        if (showingAsLowPriority()) {
+            return 0.0f;
+        }
+        return getGroupExpandFraction();
+    }
+
+    @VisibleForTesting
+    public boolean isUserLocked() {
+        return mUserLocked;
+    }
+}
diff --git a/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
new file mode 100644
index 0000000..75532d9
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -0,0 +1,4886 @@
+/*
+ * Copyright (C) 2014 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.TimeAnimator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.annotation.FloatRange;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
+import android.service.notification.StatusBarNotification;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Property;
+import android.view.ContextThemeWrapper;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.OverScroller;
+import android.widget.ScrollView;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.settingslib.Utils;
+import com.android.systemui.ExpandHelper;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.SwipeHelper;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
+import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
+import com.android.systemui.statusbar.ActivatableNotificationView;
+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.NotificationData;
+import com.android.systemui.statusbar.NotificationGuts;
+import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.NotificationSnooze;
+import com.android.systemui.statusbar.StackScrollerDecorView;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.FakeShadowView;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.phone.ScrimController;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.ScrollAdapter;
+
+import android.support.v4.graphics.ColorUtils;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack.
+ */
+public class NotificationStackScrollLayout extends ViewGroup
+        implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter,
+        ExpandableView.OnHeightChangedListener, NotificationGroupManager.OnGroupChangeListener,
+        NotificationMenuRowPlugin.OnMenuEventListener, ScrollContainer,
+        VisibilityLocationProvider {
+
+    public static final float BACKGROUND_ALPHA_DIMMED = 0.7f;
+    private static final String TAG = "StackScroller";
+    private static final boolean DEBUG = false;
+    private static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f;
+    private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f;
+    private static final float RUBBER_BAND_FACTOR_ON_PANEL_EXPAND = 0.21f;
+    /**
+     * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
+     */
+    private static final int INVALID_POINTER = -1;
+
+    private ExpandHelper mExpandHelper;
+    private NotificationSwipeHelper mSwipeHelper;
+    private boolean mSwipingInProgress;
+    private int mCurrentStackHeight = Integer.MAX_VALUE;
+    private final Paint mBackgroundPaint = new Paint();
+    private final boolean mShouldDrawNotificationBackground;
+
+    private float mExpandedHeight;
+    private int mOwnScrollY;
+    private int mMaxLayoutHeight;
+
+    private VelocityTracker mVelocityTracker;
+    private OverScroller mScroller;
+    private Runnable mFinishScrollingCallback;
+    private int mTouchSlop;
+    private int mMinimumVelocity;
+    private int mMaximumVelocity;
+    private int mOverflingDistance;
+    private float mMaxOverScroll;
+    private boolean mIsBeingDragged;
+    private int mLastMotionY;
+    private int mDownX;
+    private int mActivePointerId = INVALID_POINTER;
+    private boolean mTouchIsClick;
+    private float mInitialTouchX;
+    private float mInitialTouchY;
+
+    private Paint mDebugPaint;
+    private int mContentHeight;
+    private int mCollapsedSize;
+    private int mPaddingBetweenElements;
+    private int mIncreasedPaddingBetweenElements;
+    private int mTopPadding;
+    private int mBottomMargin;
+    private int mBottomInset = 0;
+
+    /**
+     * The algorithm which calculates the properties for our children
+     */
+    protected final StackScrollAlgorithm mStackScrollAlgorithm;
+
+    /**
+     * The current State this Layout is in
+     */
+    private StackScrollState mCurrentStackScrollState = new StackScrollState(this);
+    private final AmbientState mAmbientState;
+    private NotificationGroupManager mGroupManager;
+    private HashSet<View> mChildrenToAddAnimated = new HashSet<>();
+    private ArrayList<View> mAddedHeadsUpChildren = new ArrayList<>();
+    private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<>();
+    private ArrayList<View> mSnappedBackChildren = new ArrayList<>();
+    private ArrayList<View> mDragAnimPendingChildren = new ArrayList<>();
+    private ArrayList<View> mChildrenChangingPositions = new ArrayList<>();
+    private HashSet<View> mFromMoreCardAdditions = new HashSet<>();
+    private ArrayList<AnimationEvent> mAnimationEvents = new ArrayList<>();
+    private ArrayList<View> mSwipedOutViews = new ArrayList<>();
+    private final StackStateAnimator mStateAnimator = new StackStateAnimator(this);
+    private boolean mAnimationsEnabled;
+    private boolean mChangePositionInProgress;
+    private boolean mChildTransferInProgress;
+
+    /**
+     * The raw amount of the overScroll on the top, which is not rubber-banded.
+     */
+    private float mOverScrolledTopPixels;
+
+    /**
+     * The raw amount of the overScroll on the bottom, which is not rubber-banded.
+     */
+    private float mOverScrolledBottomPixels;
+    private OnChildLocationsChangedListener mListener;
+    private OnOverscrollTopChangedListener mOverscrollTopChangedListener;
+    private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
+    private OnEmptySpaceClickListener mOnEmptySpaceClickListener;
+    private boolean mNeedsAnimation;
+    private boolean mTopPaddingNeedsAnimation;
+    private boolean mDimmedNeedsAnimation;
+    private boolean mHideSensitiveNeedsAnimation;
+    private boolean mDarkNeedsAnimation;
+    private int mDarkAnimationOriginIndex;
+    private boolean mActivateNeedsAnimation;
+    private boolean mGoToFullShadeNeedsAnimation;
+    private boolean mIsExpanded = true;
+    private boolean mChildrenUpdateRequested;
+    private boolean mIsExpansionChanging;
+    private boolean mPanelTracking;
+    private boolean mExpandingNotification;
+    private boolean mExpandedInThisMotion;
+    protected boolean mScrollingEnabled;
+    protected DismissView mDismissView;
+    protected EmptyShadeView mEmptyShadeView;
+    private boolean mDismissAllInProgress;
+    private boolean mFadeNotificationsOnDismiss;
+
+    /**
+     * Was the scroller scrolled to the top when the down motion was observed?
+     */
+    private boolean mScrolledToTopOnFirstDown;
+    /**
+     * The minimal amount of over scroll which is needed in order to switch to the quick settings
+     * when over scrolling on a expanded card.
+     */
+    private float mMinTopOverScrollToEscape;
+    private int mIntrinsicPadding;
+    private float mStackTranslation;
+    private float mTopPaddingOverflow;
+    private boolean mDontReportNextOverScroll;
+    private boolean mDontClampNextScroll;
+    private boolean mNeedViewResizeAnimation;
+    private View mExpandedGroupView;
+    private boolean mEverythingNeedsAnimation;
+
+    /**
+     * The maximum scrollPosition which we are allowed to reach when a notification was expanded.
+     * This is needed to avoid scrolling too far after the notification was collapsed in the same
+     * motion.
+     */
+    private int mMaxScrollAfterExpand;
+    private SwipeHelper.LongPressListener mLongPressListener;
+
+    private NotificationMenuRowPlugin mCurrMenuRow;
+    private View mTranslatingParentView;
+    private View mMenuExposedView;
+    boolean mCheckForLeavebehind;
+
+    /**
+     * Should in this touch motion only be scrolling allowed? It's true when the scroller was
+     * animating.
+     */
+    private boolean mOnlyScrollingInThisMotion;
+    private boolean mDisallowDismissInThisMotion;
+    private boolean mInterceptDelegateEnabled;
+    private boolean mDelegateToScrollView;
+    private boolean mDisallowScrollingInThisMotion;
+    private long mGoToFullShadeDelay;
+    private ViewTreeObserver.OnPreDrawListener mChildrenUpdater
+            = new ViewTreeObserver.OnPreDrawListener() {
+        @Override
+        public boolean onPreDraw() {
+            updateForcedScroll();
+            updateChildren();
+            mChildrenUpdateRequested = false;
+            getViewTreeObserver().removeOnPreDrawListener(this);
+            return true;
+        }
+    };
+    private StatusBar mStatusBar;
+    private int[] mTempInt2 = new int[2];
+    private boolean mGenerateChildOrderChangedEvent;
+    private HashSet<Runnable> mAnimationFinishedRunnables = new HashSet<>();
+    private HashSet<View> mClearOverlayViewsWhenFinished = new HashSet<>();
+    private HashSet<Pair<ExpandableNotificationRow, Boolean>> mHeadsUpChangeAnimations
+            = new HashSet<>();
+    private HeadsUpManager mHeadsUpManager;
+    private boolean mTrackingHeadsUp;
+    private ScrimController mScrimController;
+    private boolean mForceNoOverlappingRendering;
+    private final ArrayList<Pair<ExpandableNotificationRow, Boolean>> mTmpList = new ArrayList<>();
+    private FalsingManager mFalsingManager;
+    private boolean mAnimationRunning;
+    private ViewTreeObserver.OnPreDrawListener mRunningAnimationUpdater
+            = new ViewTreeObserver.OnPreDrawListener() {
+        @Override
+        public boolean onPreDraw() {
+            onPreDrawDuringAnimation();
+            return true;
+        }
+    };
+    private Rect mBackgroundBounds = new Rect();
+    private Rect mStartAnimationRect = new Rect();
+    private Rect mEndAnimationRect = new Rect();
+    private Rect mCurrentBounds = new Rect(-1, -1, -1, -1);
+    private boolean mAnimateNextBackgroundBottom;
+    private boolean mAnimateNextBackgroundTop;
+    private ObjectAnimator mBottomAnimator = null;
+    private ObjectAnimator mTopAnimator = null;
+    private ActivatableNotificationView mFirstVisibleBackgroundChild = null;
+    private ActivatableNotificationView mLastVisibleBackgroundChild = null;
+    private int mBgColor;
+    private float mDimAmount;
+    private ValueAnimator mDimAnimator;
+    private ArrayList<ExpandableView> mTmpSortedChildren = new ArrayList<>();
+    private Animator.AnimatorListener mDimEndListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mDimAnimator = null;
+        }
+    };
+    private ValueAnimator.AnimatorUpdateListener mDimUpdateListener
+            = new ValueAnimator.AnimatorUpdateListener() {
+
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            setDimAmount((Float) animation.getAnimatedValue());
+        }
+    };
+    protected ViewGroup mQsContainer;
+    private boolean mContinuousShadowUpdate;
+    private ViewTreeObserver.OnPreDrawListener mShadowUpdater
+            = new ViewTreeObserver.OnPreDrawListener() {
+
+        @Override
+        public boolean onPreDraw() {
+            updateViewShadows();
+            return true;
+        }
+    };
+    private Comparator<ExpandableView> mViewPositionComparator = new Comparator<ExpandableView>() {
+        @Override
+        public int compare(ExpandableView view, ExpandableView otherView) {
+            float endY = view.getTranslationY() + view.getActualHeight();
+            float otherEndY = otherView.getTranslationY() + otherView.getActualHeight();
+            if (endY < otherEndY) {
+                return -1;
+            } else if (endY > otherEndY) {
+                return 1;
+            } else {
+                // The two notifications end at the same location
+                return 0;
+            }
+        }
+    };
+    private PorterDuffXfermode mSrcMode = new PorterDuffXfermode(PorterDuff.Mode.SRC);
+    private Collection<HeadsUpManager.HeadsUpEntry> mPulsing;
+    private boolean mDrawBackgroundAsSrc;
+    private boolean mFadingOut;
+    private boolean mParentNotFullyVisible;
+    private boolean mGroupExpandedForMeasure;
+    private boolean mScrollable;
+    private View mForcedScroll;
+    private float mBackgroundFadeAmount = 1.0f;
+    private static final Property<NotificationStackScrollLayout, Float> BACKGROUND_FADE =
+            new FloatProperty<NotificationStackScrollLayout>("backgroundFade") {
+                @Override
+                public void setValue(NotificationStackScrollLayout object, float value) {
+                    object.setBackgroundFadeAmount(value);
+                }
+
+                @Override
+                public Float get(NotificationStackScrollLayout object) {
+                    return object.getBackgroundFadeAmount();
+                }
+            };
+    private boolean mUsingLightTheme;
+    private boolean mQsExpanded;
+    private boolean mForwardScrollable;
+    private boolean mBackwardScrollable;
+    private NotificationShelf mShelf;
+    private int mMaxDisplayedNotifications = -1;
+    private int mStatusBarHeight;
+    private int mMinInteractionHeight;
+    private boolean mNoAmbient;
+    private final Rect mClipRect = new Rect();
+    private boolean mIsClipped;
+    private Rect mRequestedClipBounds;
+    private boolean mInHeadsUpPinnedMode;
+    private boolean mHeadsUpAnimatingAway;
+    private int mStatusBarState;
+    private int mCachedBackgroundColor;
+    private boolean mHeadsUpGoingAwayAnimationsAllowed = true;
+    private Runnable mAnimateScroll = this::animateScroll;
+
+    public NotificationStackScrollLayout(Context context) {
+        this(context, null);
+    }
+
+    public NotificationStackScrollLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        Resources res = getResources();
+
+        mAmbientState = new AmbientState(context);
+        mBgColor = context.getColor(R.color.notification_shade_background_color);
+        int minHeight = res.getDimensionPixelSize(R.dimen.notification_min_height);
+        int maxHeight = res.getDimensionPixelSize(R.dimen.notification_max_height);
+        mExpandHelper = new ExpandHelper(getContext(), this,
+                minHeight, maxHeight);
+        mExpandHelper.setEventSource(this);
+        mExpandHelper.setScrollAdapter(this);
+        mSwipeHelper = new NotificationSwipeHelper(SwipeHelper.X, this, getContext());
+        mSwipeHelper.setLongPressListener(mLongPressListener);
+        mStackScrollAlgorithm = createStackScrollAlgorithm(context);
+        initView(context);
+        mFalsingManager = FalsingManager.getInstance(context);
+        mShouldDrawNotificationBackground =
+                res.getBoolean(R.bool.config_drawNotificationBackground);
+        mFadeNotificationsOnDismiss =
+                res.getBoolean(R.bool.config_fadeNotificationsOnDismiss);
+
+        updateWillNotDraw();
+        if (DEBUG) {
+            mDebugPaint = new Paint();
+            mDebugPaint.setColor(0xffff0000);
+            mDebugPaint.setStrokeWidth(2);
+            mDebugPaint.setStyle(Paint.Style.STROKE);
+        }
+    }
+
+    public NotificationSwipeActionHelper getSwipeActionHelper() {
+        return mSwipeHelper;
+    }
+
+    @Override
+    public void onMenuClicked(View view, int x, int y, MenuItem item) {
+        if (mLongPressListener == null) {
+            return;
+        }
+        if (view instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+            MetricsLogger.action(mContext, MetricsEvent.ACTION_TOUCH_GEAR,
+                    row.getStatusBarNotification().getPackageName());
+        }
+        mLongPressListener.onLongPress(view, x, y, item);
+    }
+
+    @Override
+    public void onMenuReset(View row) {
+        if (mTranslatingParentView != null && row == mTranslatingParentView) {
+            mMenuExposedView = null;
+            mTranslatingParentView = null;
+        }
+    }
+
+    @Override
+    public void onMenuShown(View row) {
+        mMenuExposedView = mTranslatingParentView;
+        if (row instanceof ExpandableNotificationRow) {
+            MetricsLogger.action(mContext, MetricsEvent.ACTION_REVEAL_GEAR,
+                    ((ExpandableNotificationRow) row).getStatusBarNotification()
+                            .getPackageName());
+        }
+        mSwipeHelper.onMenuShown(row);
+    }
+
+    protected void onDraw(Canvas canvas) {
+        if (mShouldDrawNotificationBackground && !mAmbientState.isDark()
+                && mCurrentBounds.top < mCurrentBounds.bottom) {
+            canvas.drawRect(0, mCurrentBounds.top, getWidth(), mCurrentBounds.bottom,
+                    mBackgroundPaint);
+        }
+
+        if (DEBUG) {
+            int y = mTopPadding;
+            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
+            y = getLayoutHeight();
+            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
+            y = getHeight() - getEmptyBottomMargin();
+            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
+        }
+    }
+
+    private void updateBackgroundDimming() {
+        // No need to update the background color if it's not being drawn.
+        if (!mShouldDrawNotificationBackground) {
+            return;
+        }
+
+        float alpha = BACKGROUND_ALPHA_DIMMED + (1 - BACKGROUND_ALPHA_DIMMED) * (1.0f - mDimAmount);
+        alpha *= mBackgroundFadeAmount;
+        // We need to manually blend in the background color
+        int scrimColor = mScrimController.getBackgroundColor();
+        int color = ColorUtils.blendARGB(scrimColor, mBgColor, alpha);
+        if (mCachedBackgroundColor != color) {
+            mCachedBackgroundColor = color;
+            mBackgroundPaint.setColor(color);
+            invalidate();
+        }
+    }
+
+    private void initView(Context context) {
+        mScroller = new OverScroller(getContext());
+        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+        setClipChildren(false);
+        final ViewConfiguration configuration = ViewConfiguration.get(context);
+        mTouchSlop = configuration.getScaledTouchSlop();
+        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
+        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+        mOverflingDistance = configuration.getScaledOverflingDistance();
+
+        Resources res = context.getResources();
+        mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height);
+        mStackScrollAlgorithm.initView(context);
+        mAmbientState.reload(context);
+        mPaddingBetweenElements = Math.max(1,
+                res.getDimensionPixelSize(R.dimen.notification_divider_height));
+        mIncreasedPaddingBetweenElements =
+                res.getDimensionPixelSize(R.dimen.notification_divider_height_increased);
+        mMinTopOverScrollToEscape = res.getDimensionPixelSize(
+                R.dimen.min_top_overscroll_to_qs);
+        mStatusBarHeight = res.getDimensionPixelOffset(R.dimen.status_bar_height);
+        mBottomMargin = res.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom);
+        mMinInteractionHeight = res.getDimensionPixelSize(
+                R.dimen.notification_min_interaction_height);
+    }
+
+    public void setDrawBackgroundAsSrc(boolean asSrc) {
+        mDrawBackgroundAsSrc = asSrc;
+        updateSrcDrawing();
+    }
+
+    private void updateSrcDrawing() {
+        if (!mShouldDrawNotificationBackground) {
+            return;
+        }
+
+        mBackgroundPaint.setXfermode(mDrawBackgroundAsSrc && !mFadingOut && !mParentNotFullyVisible
+                ? mSrcMode : null);
+        invalidate();
+    }
+
+    private void notifyHeightChangeListener(ExpandableView view) {
+        if (mOnHeightChangedListener != null) {
+            mOnHeightChangedListener.onHeightChanged(view, false /* needsAnimation */);
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        // We need to measure all children even the GONE ones, such that the heights are calculated
+        // correctly as they are used to calculate how many we can fit on the screen.
+        final int size = getChildCount();
+        for (int i = 0; i < size; i++) {
+            measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        // we layout all our children centered on the top
+        float centerX = getWidth() / 2.0f;
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            // We need to layout all children even the GONE ones, such that the heights are
+            // calculated correctly as they are used to calculate how many we can fit on the screen
+            float width = child.getMeasuredWidth();
+            float height = child.getMeasuredHeight();
+            child.layout((int) (centerX - width / 2.0f),
+                    0,
+                    (int) (centerX + width / 2.0f),
+                    (int) height);
+        }
+        setMaxLayoutHeight(getHeight());
+        updateContentHeight();
+        clampScrollPosition();
+        requestChildrenUpdate();
+        updateFirstAndLastBackgroundViews();
+        updateAlgorithmLayoutMinHeight();
+    }
+
+    private void requestAnimationOnViewResize(ExpandableNotificationRow row) {
+        if (mAnimationsEnabled && (mIsExpanded || row != null && row.isPinned())) {
+            mNeedViewResizeAnimation = true;
+            mNeedsAnimation = true;
+        }
+    }
+
+    public void updateSpeedBumpIndex(int newIndex, boolean noAmbient) {
+        mAmbientState.setSpeedBumpIndex(newIndex);
+        mNoAmbient = noAmbient;
+    }
+
+    public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public boolean isInVisibleLocation(ExpandableNotificationRow row) {
+        ExpandableViewState childViewState = mCurrentStackScrollState.getViewStateForView(row);
+        if (childViewState == null) {
+            return false;
+        }
+        if ((childViewState.location &= ExpandableViewState.VISIBLE_LOCATIONS) == 0) {
+            return false;
+        }
+        if (row.getVisibility() != View.VISIBLE) {
+            return false;
+        }
+        return true;
+    }
+
+    private void setMaxLayoutHeight(int maxLayoutHeight) {
+        mMaxLayoutHeight = maxLayoutHeight;
+        mShelf.setMaxLayoutHeight(maxLayoutHeight);
+        updateAlgorithmHeightAndPadding();
+    }
+
+    private void updateAlgorithmHeightAndPadding() {
+        mAmbientState.setLayoutHeight(getLayoutHeight());
+        updateAlgorithmLayoutMinHeight();
+        mAmbientState.setTopPadding(mTopPadding);
+    }
+
+    private void updateAlgorithmLayoutMinHeight() {
+        mAmbientState.setLayoutMinHeight(mQsExpanded && !onKeyguard() ? getLayoutMinHeight() : 0);
+    }
+
+    /**
+     * Updates the children views according to the stack scroll algorithm. Call this whenever
+     * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout.
+     */
+    private void updateChildren() {
+        updateScrollStateForAddedChildren();
+        mAmbientState.setCurrentScrollVelocity(mScroller.isFinished()
+                ? 0
+                : mScroller.getCurrVelocity());
+        mAmbientState.setScrollY(mOwnScrollY);
+        mStackScrollAlgorithm.getStackScrollState(mAmbientState, mCurrentStackScrollState);
+        if (!isCurrentlyAnimating() && !mNeedsAnimation) {
+            applyCurrentState();
+        } else {
+            startAnimationToState();
+        }
+    }
+
+    private void onPreDrawDuringAnimation() {
+        mShelf.updateAppearance();
+        if (!mNeedsAnimation && !mChildrenUpdateRequested) {
+            updateBackground();
+        }
+    }
+
+    private void updateScrollStateForAddedChildren() {
+        if (mChildrenToAddAnimated.isEmpty()) {
+            return;
+        }
+        for (int i = 0; i < getChildCount(); i++) {
+            ExpandableView child = (ExpandableView) getChildAt(i);
+            if (mChildrenToAddAnimated.contains(child)) {
+                int startingPosition = getPositionInLinearLayout(child);
+                float increasedPaddingAmount = child.getIncreasedPaddingAmount();
+                int padding = increasedPaddingAmount == 1.0f ? mIncreasedPaddingBetweenElements
+                        : increasedPaddingAmount == -1.0f ? 0 : mPaddingBetweenElements;
+                int childHeight = getIntrinsicHeight(child) + padding;
+                if (startingPosition < mOwnScrollY) {
+                    // This child starts off screen, so let's keep it offscreen to keep the others visible
+
+                    setOwnScrollY(mOwnScrollY + childHeight);
+                }
+            }
+        }
+        clampScrollPosition();
+    }
+
+    private void updateForcedScroll() {
+        if (mForcedScroll != null && (!mForcedScroll.hasFocus()
+                || !mForcedScroll.isAttachedToWindow())) {
+            mForcedScroll = null;
+        }
+        if (mForcedScroll != null) {
+            ExpandableView expandableView = (ExpandableView) mForcedScroll;
+            int positionInLinearLayout = getPositionInLinearLayout(expandableView);
+            int targetScroll = targetScrollForView(expandableView, positionInLinearLayout);
+            int outOfViewScroll = positionInLinearLayout + expandableView.getIntrinsicHeight();
+
+            targetScroll = Math.max(0, Math.min(targetScroll, getScrollRange()));
+
+            // Only apply the scroll if we're scrolling the view upwards, or the view is so far up
+            // that it is not visible anymore.
+            if (mOwnScrollY < targetScroll || outOfViewScroll < mOwnScrollY) {
+                setOwnScrollY(targetScroll);
+            }
+        }
+    }
+
+    private void requestChildrenUpdate() {
+        if (!mChildrenUpdateRequested) {
+            getViewTreeObserver().addOnPreDrawListener(mChildrenUpdater);
+            mChildrenUpdateRequested = true;
+            invalidate();
+        }
+    }
+
+    private boolean isCurrentlyAnimating() {
+        return mStateAnimator.isRunning();
+    }
+
+    private void clampScrollPosition() {
+        int scrollRange = getScrollRange();
+        if (scrollRange < mOwnScrollY) {
+            setOwnScrollY(scrollRange);
+        }
+    }
+
+    public int getTopPadding() {
+        return mTopPadding;
+    }
+
+    private void setTopPadding(int topPadding, boolean animate) {
+        if (mTopPadding != topPadding) {
+            mTopPadding = topPadding;
+            updateAlgorithmHeightAndPadding();
+            updateContentHeight();
+            if (animate && mAnimationsEnabled && mIsExpanded) {
+                mTopPaddingNeedsAnimation = true;
+                mNeedsAnimation =  true;
+            }
+            requestChildrenUpdate();
+            notifyHeightChangeListener(null);
+        }
+    }
+
+    /**
+     * Update the height of the panel.
+     *
+     * @param height the expanded height of the panel
+     */
+    public void setExpandedHeight(float height) {
+        mExpandedHeight = height;
+        setIsExpanded(height > 0);
+        int minExpansionHeight = getMinExpansionHeight();
+        if (height < minExpansionHeight) {
+            mClipRect.left = 0;
+            mClipRect.right = getWidth();
+            mClipRect.top = 0;
+            mClipRect.bottom = (int) height;
+            height = minExpansionHeight;
+            setRequestedClipBounds(mClipRect);
+        } else {
+            setRequestedClipBounds(null);
+        }
+        int stackHeight;
+        float translationY;
+        float appearEndPosition = getAppearEndPosition();
+        float appearStartPosition = getAppearStartPosition();
+        if (height >= appearEndPosition) {
+            translationY = 0;
+            stackHeight = (int) height;
+        } else {
+            float appearFraction = getAppearFraction(height);
+            if (appearFraction >= 0) {
+                translationY = NotificationUtils.interpolate(getExpandTranslationStart(), 0,
+                        appearFraction);
+            } else {
+                // This may happen when pushing up a heads up. We linearly push it up from the
+                // start
+                translationY = height - appearStartPosition + getExpandTranslationStart();
+            }
+            stackHeight = (int) (height - translationY);
+        }
+        if (stackHeight != mCurrentStackHeight) {
+            mCurrentStackHeight = stackHeight;
+            updateAlgorithmHeightAndPadding();
+            requestChildrenUpdate();
+        }
+        setStackTranslation(translationY);
+    }
+
+    private void setRequestedClipBounds(Rect clipRect) {
+        mRequestedClipBounds = clipRect;
+        updateClipping();
+    }
+
+    public void updateClipping() {
+        boolean clipped = mRequestedClipBounds != null && !mInHeadsUpPinnedMode
+                && !mHeadsUpAnimatingAway;
+        if (mIsClipped != clipped) {
+            mIsClipped = clipped;
+            updateFadingState();
+        }
+        if (clipped) {
+            setClipBounds(mRequestedClipBounds);
+        } else {
+            setClipBounds(null);
+        }
+    }
+
+    /**
+     * @return The translation at the beginning when expanding.
+     *         Measured relative to the resting position.
+     */
+    private float getExpandTranslationStart() {
+        return - mTopPadding;
+    }
+
+    /**
+     * @return the position from where the appear transition starts when expanding.
+     *         Measured in absolute height.
+     */
+    private float getAppearStartPosition() {
+        if (mTrackingHeadsUp && mFirstVisibleBackgroundChild != null) {
+            if (mAmbientState.isAboveShelf(mFirstVisibleBackgroundChild)) {
+                // If we ever expanded beyond the first notification, it's allowed to merge into
+                // the shelf
+                return mFirstVisibleBackgroundChild.getPinnedHeadsUpHeight();
+            }
+        }
+        return getMinExpansionHeight();
+    }
+
+    /**
+     * @return the position from where the appear transition ends when expanding.
+     *         Measured in absolute height.
+     */
+    private float getAppearEndPosition() {
+        int appearPosition;
+        int notGoneChildCount = getNotGoneChildCount();
+        if (mEmptyShadeView.getVisibility() == GONE && notGoneChildCount != 0) {
+            int minNotificationsForShelf = 1;
+            if (mTrackingHeadsUp
+                    || (mHeadsUpManager.hasPinnedHeadsUp() && !mAmbientState.isDark())) {
+                appearPosition = mHeadsUpManager.getTopHeadsUpPinnedHeight();
+                minNotificationsForShelf = 2;
+            } else {
+                appearPosition = 0;
+            }
+            if (notGoneChildCount >= minNotificationsForShelf
+                    && mShelf.getVisibility() != GONE) {
+                appearPosition += mShelf.getIntrinsicHeight();
+            }
+        } else {
+            appearPosition = mEmptyShadeView.getHeight();
+        }
+        return appearPosition + (onKeyguard() ? mTopPadding : mIntrinsicPadding);
+    }
+
+    /**
+     * @param height the height of the panel
+     * @return the fraction of the appear animation that has been performed
+     */
+    public float getAppearFraction(float height) {
+        float appearEndPosition = getAppearEndPosition();
+        float appearStartPosition = getAppearStartPosition();
+        return (height - appearStartPosition)
+                / (appearEndPosition - appearStartPosition);
+    }
+
+    public float getStackTranslation() {
+        return mStackTranslation;
+    }
+
+    private void setStackTranslation(float stackTranslation) {
+        if (stackTranslation != mStackTranslation) {
+            mStackTranslation = stackTranslation;
+            mAmbientState.setStackTranslation(stackTranslation);
+            requestChildrenUpdate();
+        }
+    }
+
+    /**
+     * Get the current height of the view. This is at most the msize of the view given by a the
+     * layout but it can also be made smaller by setting {@link #mCurrentStackHeight}
+     *
+     * @return either the layout height or the externally defined height, whichever is smaller
+     */
+    private int getLayoutHeight() {
+        return Math.min(mMaxLayoutHeight, mCurrentStackHeight);
+    }
+
+    public int getFirstItemMinHeight() {
+        final ExpandableView firstChild = getFirstChildNotGone();
+        return firstChild != null ? firstChild.getMinHeight() : mCollapsedSize;
+    }
+
+    public void setLongPressListener(SwipeHelper.LongPressListener listener) {
+        mSwipeHelper.setLongPressListener(listener);
+        mLongPressListener = listener;
+    }
+
+    public void setQsContainer(ViewGroup qsContainer) {
+        mQsContainer = qsContainer;
+    }
+
+    @Override
+    public void onChildDismissed(View v) {
+        ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+        if (!row.isDismissed()) {
+            handleChildDismissed(v);
+        }
+        ViewGroup transientContainer = row.getTransientContainer();
+        if (transientContainer != null) {
+            transientContainer.removeTransientView(v);
+        }
+    }
+
+    private void handleChildDismissed(View v) {
+        if (mDismissAllInProgress) {
+            return;
+        }
+        setSwipingInProgress(false);
+        if (mDragAnimPendingChildren.contains(v)) {
+            // We start the swipe and finish it in the same frame, we don't want any animation
+            // for the drag
+            mDragAnimPendingChildren.remove(v);
+        }
+        mSwipedOutViews.add(v);
+        mAmbientState.onDragFinished(v);
+        updateContinuousShadowDrawing();
+        if (v instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+            if (row.isHeadsUp()) {
+                mHeadsUpManager.addSwipedOutNotification(row.getStatusBarNotification().getKey());
+            }
+        }
+        performDismiss(v, mGroupManager, false /* fromAccessibility */);
+
+        mFalsingManager.onNotificationDismissed();
+        if (mFalsingManager.shouldEnforceBouncer()) {
+            mStatusBar.executeRunnableDismissingKeyguard(null, null /* cancelAction */,
+                    false /* dismissShade */, true /* afterKeyguardGone */, false /* deferred */);
+        }
+    }
+
+    public static void performDismiss(View v, NotificationGroupManager groupManager,
+            boolean fromAccessibility) {
+        if (!(v instanceof ExpandableNotificationRow)) {
+            return;
+        }
+        ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+        if (groupManager.isOnlyChildInGroup(row.getStatusBarNotification())) {
+            ExpandableNotificationRow groupSummary =
+                    groupManager.getLogicalGroupSummary(row.getStatusBarNotification());
+            if (groupSummary.isClearable()) {
+                performDismiss(groupSummary, groupManager, fromAccessibility);
+            }
+        }
+        row.setDismissed(true, fromAccessibility);
+        if (row.isClearable()) {
+            row.performDismiss();
+        }
+        if (DEBUG) Log.v(TAG, "onChildDismissed: " + v);
+    }
+
+    @Override
+    public void onChildSnappedBack(View animView, float targetLeft) {
+        mAmbientState.onDragFinished(animView);
+        updateContinuousShadowDrawing();
+        if (!mDragAnimPendingChildren.contains(animView)) {
+            if (mAnimationsEnabled) {
+                mSnappedBackChildren.add(animView);
+                mNeedsAnimation = true;
+            }
+            requestChildrenUpdate();
+        } else {
+            // We start the swipe and snap back in the same frame, we don't want any animation
+            mDragAnimPendingChildren.remove(animView);
+        }
+        if (mCurrMenuRow != null && targetLeft == 0) {
+            mCurrMenuRow.resetMenu();
+            mCurrMenuRow = null;
+        }
+    }
+
+    @Override
+    public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) {
+        if (!mIsExpanded && isPinnedHeadsUp(animView) && canChildBeDismissed(animView)) {
+            mScrimController.setTopHeadsUpDragAmount(animView,
+                    Math.min(Math.abs(swipeProgress / 2f - 1.0f), 1.0f));
+        }
+        // Returning true prevents alpha fading.
+        return !mFadeNotificationsOnDismiss;
+    }
+
+    @Override
+    public void onBeginDrag(View v) {
+        mFalsingManager.onNotificatonStartDismissing();
+        setSwipingInProgress(true);
+        mAmbientState.onBeginDrag(v);
+        updateContinuousShadowDrawing();
+        if (mAnimationsEnabled && (mIsExpanded || !isPinnedHeadsUp(v))) {
+            mDragAnimPendingChildren.add(v);
+            mNeedsAnimation = true;
+        }
+        requestChildrenUpdate();
+    }
+
+    public static boolean isPinnedHeadsUp(View v) {
+        if (v instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+            return row.isHeadsUp() && row.isPinned();
+        }
+        return false;
+    }
+
+    private boolean isHeadsUp(View v) {
+        if (v instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+            return row.isHeadsUp();
+        }
+        return false;
+    }
+
+    @Override
+    public void onDragCancelled(View v) {
+        mFalsingManager.onNotificatonStopDismissing();
+        setSwipingInProgress(false);
+    }
+
+    @Override
+    public float getFalsingThresholdFactor() {
+        return mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
+    }
+
+    @Override
+    public View getChildAtPosition(MotionEvent ev) {
+        View child = getChildAtPosition(ev.getX(), ev.getY());
+        if (child instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+            ExpandableNotificationRow parent = row.getNotificationParent();
+            if (parent != null && parent.areChildrenExpanded()
+                    && (parent.areGutsExposed()
+                            || mMenuExposedView == parent
+                        || (parent.getNotificationChildren().size() == 1
+                                && parent.isClearable()))) {
+                // In this case the group is expanded and showing the menu for the
+                // group, further interaction should apply to the group, not any
+                // child notifications so we use the parent of the child. We also do the same
+                // if we only have a single child.
+                child = parent;
+            }
+        }
+        return child;
+    }
+
+    public ExpandableView getClosestChildAtRawPosition(float touchX, float touchY) {
+        getLocationOnScreen(mTempInt2);
+        float localTouchY = touchY - mTempInt2[1];
+
+        ExpandableView closestChild = null;
+        float minDist = Float.MAX_VALUE;
+
+        // find the view closest to the location, accounting for GONE views
+        final int count = getChildCount();
+        for (int childIdx = 0; childIdx < count; childIdx++) {
+            ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx);
+            if (slidingChild.getVisibility() == GONE
+                    || slidingChild instanceof StackScrollerDecorView) {
+                continue;
+            }
+            float childTop = slidingChild.getTranslationY();
+            float top = childTop + slidingChild.getClipTopAmount();
+            float bottom = childTop + slidingChild.getActualHeight()
+                    - slidingChild.getClipBottomAmount();
+
+            float dist = Math.min(Math.abs(top - localTouchY), Math.abs(bottom - localTouchY));
+            if (dist < minDist) {
+                closestChild = slidingChild;
+                minDist = dist;
+            }
+        }
+        return closestChild;
+    }
+
+    @Override
+    public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
+        getLocationOnScreen(mTempInt2);
+        return getChildAtPosition(touchX - mTempInt2[0], touchY - mTempInt2[1]);
+    }
+
+    @Override
+    public ExpandableView getChildAtPosition(float touchX, float touchY) {
+        return getChildAtPosition(touchX, touchY, true /* requireMinHeight */);
+
+    }
+
+    /**
+     * Get the child at a certain screen location.
+     *
+     * @param touchX the x coordinate
+     * @param touchY the y coordinate
+     * @param requireMinHeight Whether a minimum height is required for a child to be returned.
+     * @return the child at the given location.
+     */
+    private ExpandableView getChildAtPosition(float touchX, float touchY,
+            boolean requireMinHeight) {
+        // find the view under the pointer, accounting for GONE views
+        final int count = getChildCount();
+        for (int childIdx = 0; childIdx < count; childIdx++) {
+            ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx);
+            if (slidingChild.getVisibility() != VISIBLE
+                    || slidingChild instanceof StackScrollerDecorView) {
+                continue;
+            }
+            float childTop = slidingChild.getTranslationY();
+            float top = childTop + slidingChild.getClipTopAmount();
+            float bottom = childTop + slidingChild.getActualHeight()
+                    - slidingChild.getClipBottomAmount();
+
+            // Allow the full width of this view to prevent gesture conflict on Keyguard (phone and
+            // camera affordance).
+            int left = 0;
+            int right = getWidth();
+
+            if ((bottom - top >= mMinInteractionHeight || !requireMinHeight)
+                    && touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
+                if (slidingChild instanceof ExpandableNotificationRow) {
+                    ExpandableNotificationRow row = (ExpandableNotificationRow) slidingChild;
+                    if (!mIsExpanded && row.isHeadsUp() && row.isPinned()
+                            && mHeadsUpManager.getTopEntry().entry.row != row
+                            && mGroupManager.getGroupSummary(
+                                mHeadsUpManager.getTopEntry().entry.row.getStatusBarNotification())
+                                != row) {
+                        continue;
+                    }
+                    return row.getViewAtPosition(touchY - childTop);
+                }
+                return slidingChild;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public boolean canChildBeExpanded(View v) {
+        return v instanceof ExpandableNotificationRow
+                && ((ExpandableNotificationRow) v).isExpandable()
+                && !((ExpandableNotificationRow) v).areGutsExposed()
+                && (mIsExpanded || !((ExpandableNotificationRow) v).isPinned());
+    }
+
+    /* Only ever called as a consequence of an expansion gesture in the shade. */
+    @Override
+    public void setUserExpandedChild(View v, boolean userExpanded) {
+        if (v instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+            if (userExpanded && onKeyguard()) {
+                // Due to a race when locking the screen while touching, a notification may be
+                // expanded even after we went back to keyguard. An example of this happens if
+                // you click in the empty space while expanding a group.
+
+                // We also need to un-user lock it here, since otherwise the content height
+                // calculated might be wrong. We also can't invert the two calls since
+                // un-userlocking it will trigger a layout switch in the content view.
+                row.setUserLocked(false);
+                updateContentHeight();
+                notifyHeightChangeListener(row);
+                return;
+            }
+            row.setUserExpanded(userExpanded, true /* allowChildrenExpansion */);
+            row.onExpandedByGesture(userExpanded);
+        }
+    }
+
+    @Override
+    public void setExpansionCancelled(View v) {
+        if (v instanceof ExpandableNotificationRow) {
+            ((ExpandableNotificationRow) v).setGroupExpansionChanging(false);
+        }
+    }
+
+    @Override
+    public void setUserLockedChild(View v, boolean userLocked) {
+        if (v instanceof ExpandableNotificationRow) {
+            ((ExpandableNotificationRow) v).setUserLocked(userLocked);
+        }
+        removeLongPressCallback();
+        requestDisallowInterceptTouchEvent(true);
+    }
+
+    @Override
+    public void expansionStateChanged(boolean isExpanding) {
+        mExpandingNotification = isExpanding;
+        if (!mExpandedInThisMotion) {
+            mMaxScrollAfterExpand = mOwnScrollY;
+            mExpandedInThisMotion = true;
+        }
+    }
+
+    @Override
+    public int getMaxExpandHeight(ExpandableView view) {
+        return view.getMaxContentHeight();
+    }
+
+    public void setScrollingEnabled(boolean enable) {
+        mScrollingEnabled = enable;
+    }
+
+    @Override
+    public void lockScrollTo(View v) {
+        if (mForcedScroll == v) {
+            return;
+        }
+        mForcedScroll = v;
+        scrollTo(v);
+    }
+
+    @Override
+    public boolean scrollTo(View v) {
+        ExpandableView expandableView = (ExpandableView) v;
+        int positionInLinearLayout = getPositionInLinearLayout(v);
+        int targetScroll = targetScrollForView(expandableView, positionInLinearLayout);
+        int outOfViewScroll = positionInLinearLayout + expandableView.getIntrinsicHeight();
+
+        // Only apply the scroll if we're scrolling the view upwards, or the view is so far up
+        // that it is not visible anymore.
+        if (mOwnScrollY < targetScroll || outOfViewScroll < mOwnScrollY) {
+            mScroller.startScroll(mScrollX, mOwnScrollY, 0, targetScroll - mOwnScrollY);
+            mDontReportNextOverScroll = true;
+            animateScroll();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * @return the scroll necessary to make the bottom edge of {@param v} align with the top of
+     *         the IME.
+     */
+    private int targetScrollForView(ExpandableView v, int positionInLinearLayout) {
+        return positionInLinearLayout + v.getIntrinsicHeight() +
+                getImeInset() - getHeight() + getTopPadding();
+    }
+
+    @Override
+    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+        mBottomInset = insets.getSystemWindowInsetBottom();
+
+        int range = getScrollRange();
+        if (mOwnScrollY > range) {
+            // HACK: We're repeatedly getting staggered insets here while the IME is
+            // animating away. To work around that we'll wait until things have settled.
+            removeCallbacks(mReclamp);
+            postDelayed(mReclamp, 50);
+        } else if (mForcedScroll != null) {
+            // The scroll was requested before we got the actual inset - in case we need
+            // to scroll up some more do so now.
+            scrollTo(mForcedScroll);
+        }
+        return insets;
+    }
+
+    private Runnable mReclamp = new Runnable() {
+        @Override
+        public void run() {
+            int range = getScrollRange();
+            mScroller.startScroll(mScrollX, mOwnScrollY, 0, range - mOwnScrollY);
+            mDontReportNextOverScroll = true;
+            mDontClampNextScroll = true;
+            animateScroll();
+        }
+    };
+
+    public void setExpandingEnabled(boolean enable) {
+        mExpandHelper.setEnabled(enable);
+    }
+
+    private boolean isScrollingEnabled() {
+        return mScrollingEnabled;
+    }
+
+    @Override
+    public boolean canChildBeDismissed(View v) {
+        return StackScrollAlgorithm.canChildBeDismissed(v);
+    }
+
+    @Override
+    public boolean isAntiFalsingNeeded() {
+        return onKeyguard();
+    }
+
+    private boolean onKeyguard() {
+        return mStatusBarState == StatusBarState.KEYGUARD;
+    }
+
+    private void setSwipingInProgress(boolean isSwiped) {
+        mSwipingInProgress = isSwiped;
+        if(isSwiped) {
+            requestDisallowInterceptTouchEvent(true);
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        float densityScale = getResources().getDisplayMetrics().density;
+        mSwipeHelper.setDensityScale(densityScale);
+        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
+        mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
+        initView(getContext());
+    }
+
+    public void dismissViewAnimated(View child, Runnable endRunnable, int delay, long duration) {
+        mSwipeHelper.dismissChild(child, 0, endRunnable, delay, true, duration,
+                true /* isDismissAll */);
+    }
+
+    public void snapViewIfNeeded(ExpandableNotificationRow child) {
+        boolean animate = mIsExpanded || isPinnedHeadsUp(child);
+        // If the child is showing the notification menu snap to that
+        float targetLeft = child.getProvider().isMenuVisible() ? child.getTranslation() : 0;
+        mSwipeHelper.snapChildIfNeeded(child, animate, targetLeft);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        boolean isCancelOrUp = ev.getActionMasked() == MotionEvent.ACTION_CANCEL
+                || ev.getActionMasked()== MotionEvent.ACTION_UP;
+        handleEmptySpaceClick(ev);
+        boolean expandWantsIt = false;
+        if (mIsExpanded && !mSwipingInProgress && !mOnlyScrollingInThisMotion) {
+            if (isCancelOrUp) {
+                mExpandHelper.onlyObserveMovements(false);
+            }
+            boolean wasExpandingBefore = mExpandingNotification;
+            expandWantsIt = mExpandHelper.onTouchEvent(ev);
+            if (mExpandedInThisMotion && !mExpandingNotification && wasExpandingBefore
+                    && !mDisallowScrollingInThisMotion) {
+                dispatchDownEventToScroller(ev);
+            }
+        }
+        boolean scrollerWantsIt = false;
+        if (mIsExpanded && !mSwipingInProgress && !mExpandingNotification
+                && !mDisallowScrollingInThisMotion) {
+            scrollerWantsIt = onScrollTouch(ev);
+        }
+        boolean horizontalSwipeWantsIt = false;
+        if (!mIsBeingDragged
+                && !mExpandingNotification
+                && !mExpandedInThisMotion
+                && !mOnlyScrollingInThisMotion
+                && !mDisallowDismissInThisMotion) {
+            horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
+        }
+
+        // Check if we need to clear any snooze leavebehinds
+        NotificationGuts guts = mStatusBar.getExposedGuts();
+        if (guts != null && !isTouchInView(ev, guts)
+                && guts.getGutsContent() instanceof NotificationSnooze) {
+            NotificationSnooze ns = (NotificationSnooze) guts.getGutsContent();
+            if ((ns.isExpanded() && isCancelOrUp)
+                    || (!horizontalSwipeWantsIt && scrollerWantsIt)) {
+                // If the leavebehind is expanded we clear it on the next up event, otherwise we
+                // clear it on the next non-horizontal swipe or expand event.
+                checkSnoozeLeavebehind();
+            }
+        }
+        if (ev.getActionMasked() == MotionEvent.ACTION_UP) {
+            mCheckForLeavebehind = true;
+        }
+        return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || super.onTouchEvent(ev);
+    }
+
+    private void dispatchDownEventToScroller(MotionEvent ev) {
+        MotionEvent downEvent = MotionEvent.obtain(ev);
+        downEvent.setAction(MotionEvent.ACTION_DOWN);
+        onScrollTouch(downEvent);
+        downEvent.recycle();
+    }
+
+    @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        if (!isScrollingEnabled() || !mIsExpanded || mSwipingInProgress || mExpandingNotification
+                || mDisallowScrollingInThisMotion) {
+            return false;
+        }
+        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_SCROLL: {
+                    if (!mIsBeingDragged) {
+                        final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                        if (vscroll != 0) {
+                            final int delta = (int) (vscroll * getVerticalScrollFactor());
+                            final int range = getScrollRange();
+                            int oldScrollY = mOwnScrollY;
+                            int newScrollY = oldScrollY - delta;
+                            if (newScrollY < 0) {
+                                newScrollY = 0;
+                            } else if (newScrollY > range) {
+                                newScrollY = range;
+                            }
+                            if (newScrollY != oldScrollY) {
+                                setOwnScrollY(newScrollY);
+                                return true;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return super.onGenericMotionEvent(event);
+    }
+
+    private boolean onScrollTouch(MotionEvent ev) {
+        if (!isScrollingEnabled()) {
+            return false;
+        }
+        if (isInsideQsContainer(ev) && !mIsBeingDragged) {
+            return false;
+        }
+        mForcedScroll = null;
+        initVelocityTrackerIfNotExists();
+        mVelocityTracker.addMovement(ev);
+
+        final int action = ev.getAction();
+
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN: {
+                if (getChildCount() == 0 || !isInContentBounds(ev)) {
+                    return false;
+                }
+                boolean isBeingDragged = !mScroller.isFinished();
+                setIsBeingDragged(isBeingDragged);
+                /*
+                 * If being flinged and user touches, stop the fling. isFinished
+                 * will be false if being flinged.
+                 */
+                if (!mScroller.isFinished()) {
+                    mScroller.forceFinished(true);
+                }
+
+                // Remember where the motion event started
+                mLastMotionY = (int) ev.getY();
+                mDownX = (int) ev.getX();
+                mActivePointerId = ev.getPointerId(0);
+                break;
+            }
+            case MotionEvent.ACTION_MOVE:
+                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+                if (activePointerIndex == -1) {
+                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
+                    break;
+                }
+
+                final int y = (int) ev.getY(activePointerIndex);
+                final int x = (int) ev.getX(activePointerIndex);
+                int deltaY = mLastMotionY - y;
+                final int xDiff = Math.abs(x - mDownX);
+                final int yDiff = Math.abs(deltaY);
+                if (!mIsBeingDragged && yDiff > mTouchSlop && yDiff > xDiff) {
+                    setIsBeingDragged(true);
+                    if (deltaY > 0) {
+                        deltaY -= mTouchSlop;
+                    } else {
+                        deltaY += mTouchSlop;
+                    }
+                }
+                if (mIsBeingDragged) {
+                    // Scroll to follow the motion event
+                    mLastMotionY = y;
+                    int range = getScrollRange();
+                    if (mExpandedInThisMotion) {
+                        range = Math.min(range, mMaxScrollAfterExpand);
+                    }
+
+                    float scrollAmount;
+                    if (deltaY < 0) {
+                        scrollAmount = overScrollDown(deltaY);
+                    } else {
+                        scrollAmount = overScrollUp(deltaY, range);
+                    }
+
+                    // Calling customOverScrollBy will call onCustomOverScrolled, which
+                    // sets the scrolling if applicable.
+                    if (scrollAmount != 0.0f) {
+                        // The scrolling motion could not be compensated with the
+                        // existing overScroll, we have to scroll the view
+                        customOverScrollBy((int) scrollAmount, mOwnScrollY,
+                                range, getHeight() / 2);
+                        // If we're scrolling, leavebehinds should be dismissed
+                        checkSnoozeLeavebehind();
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mIsBeingDragged) {
+                    final VelocityTracker velocityTracker = mVelocityTracker;
+                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
+
+                    if (shouldOverScrollFling(initialVelocity)) {
+                        onOverScrollFling(true, initialVelocity);
+                    } else {
+                        if (getChildCount() > 0) {
+                            if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
+                                float currentOverScrollTop = getCurrentOverScrollAmount(true);
+                                if (currentOverScrollTop == 0.0f || initialVelocity > 0) {
+                                    fling(-initialVelocity);
+                                } else {
+                                    onOverScrollFling(false, initialVelocity);
+                                }
+                            } else {
+                                if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
+                                        getScrollRange())) {
+                                    animateScroll();
+                                }
+                            }
+                        }
+                    }
+                    mActivePointerId = INVALID_POINTER;
+                    endDrag();
+                }
+
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                if (mIsBeingDragged && getChildCount() > 0) {
+                    if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
+                        animateScroll();
+                    }
+                    mActivePointerId = INVALID_POINTER;
+                    endDrag();
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN: {
+                final int index = ev.getActionIndex();
+                mLastMotionY = (int) ev.getY(index);
+                mDownX = (int) ev.getX(index);
+                mActivePointerId = ev.getPointerId(index);
+                break;
+            }
+            case MotionEvent.ACTION_POINTER_UP:
+                onSecondaryPointerUp(ev);
+                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
+                mDownX = (int) ev.getX(ev.findPointerIndex(mActivePointerId));
+                break;
+        }
+        return true;
+    }
+
+    protected boolean isInsideQsContainer(MotionEvent ev) {
+        return ev.getY() < mQsContainer.getBottom();
+    }
+
+    private void onOverScrollFling(boolean open, int initialVelocity) {
+        if (mOverscrollTopChangedListener != null) {
+            mOverscrollTopChangedListener.flingTopOverscroll(initialVelocity, open);
+        }
+        mDontReportNextOverScroll = true;
+        setOverScrollAmount(0.0f, true, false);
+    }
+
+    /**
+     * Perform a scroll upwards and adapt the overscroll amounts accordingly
+     *
+     * @param deltaY The amount to scroll upwards, has to be positive.
+     * @return The amount of scrolling to be performed by the scroller,
+     *         not handled by the overScroll amount.
+     */
+    private float overScrollUp(int deltaY, int range) {
+        deltaY = Math.max(deltaY, 0);
+        float currentTopAmount = getCurrentOverScrollAmount(true);
+        float newTopAmount = currentTopAmount - deltaY;
+        if (currentTopAmount > 0) {
+            setOverScrollAmount(newTopAmount, true /* onTop */,
+                    false /* animate */);
+        }
+        // Top overScroll might not grab all scrolling motion,
+        // we have to scroll as well.
+        float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f;
+        float newScrollY = mOwnScrollY + scrollAmount;
+        if (newScrollY > range) {
+            if (!mExpandedInThisMotion) {
+                float currentBottomPixels = getCurrentOverScrolledPixels(false);
+                // We overScroll on the top
+                setOverScrolledPixels(currentBottomPixels + newScrollY - range,
+                        false /* onTop */,
+                        false /* animate */);
+            }
+            setOwnScrollY(range);
+            scrollAmount = 0.0f;
+        }
+        return scrollAmount;
+    }
+
+    /**
+     * Perform a scroll downward and adapt the overscroll amounts accordingly
+     *
+     * @param deltaY The amount to scroll downwards, has to be negative.
+     * @return The amount of scrolling to be performed by the scroller,
+     *         not handled by the overScroll amount.
+     */
+    private float overScrollDown(int deltaY) {
+        deltaY = Math.min(deltaY, 0);
+        float currentBottomAmount = getCurrentOverScrollAmount(false);
+        float newBottomAmount = currentBottomAmount + deltaY;
+        if (currentBottomAmount > 0) {
+            setOverScrollAmount(newBottomAmount, false /* onTop */,
+                    false /* animate */);
+        }
+        // Bottom overScroll might not grab all scrolling motion,
+        // we have to scroll as well.
+        float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f;
+        float newScrollY = mOwnScrollY + scrollAmount;
+        if (newScrollY < 0) {
+            float currentTopPixels = getCurrentOverScrolledPixels(true);
+            // We overScroll on the top
+            setOverScrolledPixels(currentTopPixels - newScrollY,
+                    true /* onTop */,
+                    false /* animate */);
+            setOwnScrollY(0);
+            scrollAmount = 0.0f;
+        }
+        return scrollAmount;
+    }
+
+    private void onSecondaryPointerUp(MotionEvent ev) {
+        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
+                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        final int pointerId = ev.getPointerId(pointerIndex);
+        if (pointerId == mActivePointerId) {
+            // This was our active pointer going up. Choose a new
+            // active pointer and adjust accordingly.
+            // TODO: Make this decision more intelligent.
+            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+            mLastMotionY = (int) ev.getY(newPointerIndex);
+            mActivePointerId = ev.getPointerId(newPointerIndex);
+            if (mVelocityTracker != null) {
+                mVelocityTracker.clear();
+            }
+        }
+    }
+
+    private void initVelocityTrackerIfNotExists() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+    }
+
+    private void recycleVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    private void initOrResetVelocityTracker() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        } else {
+            mVelocityTracker.clear();
+        }
+    }
+
+    public void setFinishScrollingCallback(Runnable runnable) {
+        mFinishScrollingCallback = runnable;
+    }
+
+    private void animateScroll() {
+        if (mScroller.computeScrollOffset()) {
+            int oldY = mOwnScrollY;
+            int y = mScroller.getCurrY();
+
+            if (oldY != y) {
+                int range = getScrollRange();
+                if (y < 0 && oldY >= 0 || y > range && oldY <= range) {
+                    float currVelocity = mScroller.getCurrVelocity();
+                    if (currVelocity >= mMinimumVelocity) {
+                        mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance;
+                    }
+                }
+
+                if (mDontClampNextScroll) {
+                    range = Math.max(range, oldY);
+                }
+                customOverScrollBy(y - oldY, oldY, range,
+                        (int) (mMaxOverScroll));
+            }
+
+            postOnAnimation(mAnimateScroll);
+        } else {
+            mDontClampNextScroll = false;
+            if (mFinishScrollingCallback != null) {
+                mFinishScrollingCallback.run();
+            }
+        }
+    }
+
+    private boolean customOverScrollBy(int deltaY, int scrollY, int scrollRangeY,
+            int maxOverScrollY) {
+
+        int newScrollY = scrollY + deltaY;
+        final int top = -maxOverScrollY;
+        final int bottom = maxOverScrollY + scrollRangeY;
+
+        boolean clampedY = false;
+        if (newScrollY > bottom) {
+            newScrollY = bottom;
+            clampedY = true;
+        } else if (newScrollY < top) {
+            newScrollY = top;
+            clampedY = true;
+        }
+
+        onCustomOverScrolled(newScrollY, clampedY);
+
+        return clampedY;
+    }
+
+    /**
+     * Set the amount of overScrolled pixels which will force the view to apply a rubber-banded
+     * overscroll effect based on numPixels. By default this will also cancel animations on the
+     * same overScroll edge.
+     *
+     * @param numPixels The amount of pixels to overScroll by. These will be scaled according to
+     *                  the rubber-banding logic.
+     * @param onTop Should the effect be applied on top of the scroller.
+     * @param animate Should an animation be performed.
+     */
+    public void setOverScrolledPixels(float numPixels, boolean onTop, boolean animate) {
+        setOverScrollAmount(numPixels * getRubberBandFactor(onTop), onTop, animate, true);
+    }
+
+    /**
+     * Set the effective overScroll amount which will be directly reflected in the layout.
+     * By default this will also cancel animations on the same overScroll edge.
+     *
+     * @param amount The amount to overScroll by.
+     * @param onTop Should the effect be applied on top of the scroller.
+     * @param animate Should an animation be performed.
+     */
+    public void setOverScrollAmount(float amount, boolean onTop, boolean animate) {
+        setOverScrollAmount(amount, onTop, animate, true);
+    }
+
+    /**
+     * Set the effective overScroll amount which will be directly reflected in the layout.
+     *
+     * @param amount The amount to overScroll by.
+     * @param onTop Should the effect be applied on top of the scroller.
+     * @param animate Should an animation be performed.
+     * @param cancelAnimators Should running animations be cancelled.
+     */
+    public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
+            boolean cancelAnimators) {
+        setOverScrollAmount(amount, onTop, animate, cancelAnimators, isRubberbanded(onTop));
+    }
+
+    /**
+     * Set the effective overScroll amount which will be directly reflected in the layout.
+     *
+     * @param amount The amount to overScroll by.
+     * @param onTop Should the effect be applied on top of the scroller.
+     * @param animate Should an animation be performed.
+     * @param cancelAnimators Should running animations be cancelled.
+     * @param isRubberbanded The value which will be passed to
+     *                     {@link OnOverscrollTopChangedListener#onOverscrollTopChanged}
+     */
+    public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
+            boolean cancelAnimators, boolean isRubberbanded) {
+        if (cancelAnimators) {
+            mStateAnimator.cancelOverScrollAnimators(onTop);
+        }
+        setOverScrollAmountInternal(amount, onTop, animate, isRubberbanded);
+    }
+
+    private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate,
+            boolean isRubberbanded) {
+        amount = Math.max(0, amount);
+        if (animate) {
+            mStateAnimator.animateOverScrollToAmount(amount, onTop, isRubberbanded);
+        } else {
+            setOverScrolledPixels(amount / getRubberBandFactor(onTop), onTop);
+            mAmbientState.setOverScrollAmount(amount, onTop);
+            if (onTop) {
+                notifyOverscrollTopListener(amount, isRubberbanded);
+            }
+            requestChildrenUpdate();
+        }
+    }
+
+    private void notifyOverscrollTopListener(float amount, boolean isRubberbanded) {
+        mExpandHelper.onlyObserveMovements(amount > 1.0f);
+        if (mDontReportNextOverScroll) {
+            mDontReportNextOverScroll = false;
+            return;
+        }
+        if (mOverscrollTopChangedListener != null) {
+            mOverscrollTopChangedListener.onOverscrollTopChanged(amount, isRubberbanded);
+        }
+    }
+
+    public void setOverscrollTopChangedListener(
+            OnOverscrollTopChangedListener overscrollTopChangedListener) {
+        mOverscrollTopChangedListener = overscrollTopChangedListener;
+    }
+
+    public float getCurrentOverScrollAmount(boolean top) {
+        return mAmbientState.getOverScrollAmount(top);
+    }
+
+    public float getCurrentOverScrolledPixels(boolean top) {
+        return top? mOverScrolledTopPixels : mOverScrolledBottomPixels;
+    }
+
+    private void setOverScrolledPixels(float amount, boolean onTop) {
+        if (onTop) {
+            mOverScrolledTopPixels = amount;
+        } else {
+            mOverScrolledBottomPixels = amount;
+        }
+    }
+
+    private void onCustomOverScrolled(int scrollY, boolean clampedY) {
+        // Treat animating scrolls differently; see #computeScroll() for why.
+        if (!mScroller.isFinished()) {
+            setOwnScrollY(scrollY);
+            if (clampedY) {
+                springBack();
+            } else {
+                float overScrollTop = getCurrentOverScrollAmount(true);
+                if (mOwnScrollY < 0) {
+                    notifyOverscrollTopListener(-mOwnScrollY, isRubberbanded(true));
+                } else {
+                    notifyOverscrollTopListener(overScrollTop, isRubberbanded(true));
+                }
+            }
+        } else {
+            setOwnScrollY(scrollY);
+        }
+    }
+
+    private void springBack() {
+        int scrollRange = getScrollRange();
+        boolean overScrolledTop = mOwnScrollY <= 0;
+        boolean overScrolledBottom = mOwnScrollY >= scrollRange;
+        if (overScrolledTop || overScrolledBottom) {
+            boolean onTop;
+            float newAmount;
+            if (overScrolledTop) {
+                onTop = true;
+                newAmount = -mOwnScrollY;
+                setOwnScrollY(0);
+                mDontReportNextOverScroll = true;
+            } else {
+                onTop = false;
+                newAmount = mOwnScrollY - scrollRange;
+                setOwnScrollY(scrollRange);
+            }
+            setOverScrollAmount(newAmount, onTop, false);
+            setOverScrollAmount(0.0f, onTop, true);
+            mScroller.forceFinished(true);
+        }
+    }
+
+    private int getScrollRange() {
+        int contentHeight = getContentHeight();
+        int scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight);
+        int imeInset = getImeInset();
+        scrollRange += Math.min(imeInset, Math.max(0,
+                getContentHeight() - (getHeight() - imeInset)));
+        return scrollRange;
+    }
+
+    private int getImeInset() {
+        return Math.max(0, mBottomInset - (getRootView().getHeight() - getHeight()));
+    }
+
+    /**
+     * @return the first child which has visibility unequal to GONE
+     */
+    public ExpandableView getFirstChildNotGone() {
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            if (child.getVisibility() != View.GONE && child != mShelf) {
+                return (ExpandableView) child;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @return the child before the given view which has visibility unequal to GONE
+     */
+    public ExpandableView getViewBeforeView(ExpandableView view) {
+        ExpandableView previousView = null;
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            if (child == view) {
+                return previousView;
+            }
+            if (child.getVisibility() != View.GONE) {
+                previousView = (ExpandableView) child;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @return The first child which has visibility unequal to GONE which is currently below the
+     *         given translationY or equal to it.
+     */
+    private View getFirstChildBelowTranlsationY(float translationY, boolean ignoreChildren) {
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            if (child.getVisibility() == View.GONE) {
+                continue;
+            }
+            float rowTranslation = child.getTranslationY();
+            if (rowTranslation >= translationY) {
+                return child;
+            } else if (!ignoreChildren && child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                if (row.isSummaryWithChildren() && row.areChildrenExpanded()) {
+                    List<ExpandableNotificationRow> notificationChildren =
+                            row.getNotificationChildren();
+                    for (int childIndex = 0; childIndex < notificationChildren.size();
+                            childIndex++) {
+                        ExpandableNotificationRow rowChild = notificationChildren.get(childIndex);
+                        if (rowChild.getTranslationY() + rowTranslation >= translationY) {
+                            return rowChild;
+                        }
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @return the last child which has visibility unequal to GONE
+     */
+    public View getLastChildNotGone() {
+        int childCount = getChildCount();
+        for (int i = childCount - 1; i >= 0; i--) {
+            View child = getChildAt(i);
+            if (child.getVisibility() != View.GONE && child != mShelf) {
+                return child;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @return the number of children which have visibility unequal to GONE
+     */
+    public int getNotGoneChildCount() {
+        int childCount = getChildCount();
+        int count = 0;
+        for (int i = 0; i < childCount; i++) {
+            ExpandableView child = (ExpandableView) getChildAt(i);
+            if (child.getVisibility() != View.GONE && !child.willBeGone() && child != mShelf) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    public int getContentHeight() {
+        return mContentHeight;
+    }
+
+    private void updateContentHeight() {
+        int height = 0;
+        float previousPaddingRequest = mPaddingBetweenElements;
+        float previousPaddingAmount = 0.0f;
+        int numShownItems = 0;
+        boolean finish = false;
+        int maxDisplayedNotifications = mAmbientState.isDark()
+                ? (hasPulsingNotifications() ? 1 : 0)
+                : mMaxDisplayedNotifications;
+
+        for (int i = 0; i < getChildCount(); i++) {
+            ExpandableView expandableView = (ExpandableView) getChildAt(i);
+            if (expandableView.getVisibility() != View.GONE
+                    && !expandableView.hasNoContentHeight()) {
+                boolean limitReached = maxDisplayedNotifications != -1
+                        && numShownItems >= maxDisplayedNotifications;
+                boolean notificationOnAmbientThatIsNotPulsing = mAmbientState.isDark()
+                        && hasPulsingNotifications()
+                        && expandableView instanceof ExpandableNotificationRow
+                        && !isPulsing(((ExpandableNotificationRow) expandableView).getEntry());
+                if (limitReached || notificationOnAmbientThatIsNotPulsing) {
+                    expandableView = mShelf;
+                    finish = true;
+                }
+                float increasedPaddingAmount = expandableView.getIncreasedPaddingAmount();
+                float padding;
+                if (increasedPaddingAmount >= 0.0f) {
+                    padding = (int) NotificationUtils.interpolate(
+                            previousPaddingRequest,
+                            mIncreasedPaddingBetweenElements,
+                            increasedPaddingAmount);
+                    previousPaddingRequest = (int) NotificationUtils.interpolate(
+                            mPaddingBetweenElements,
+                            mIncreasedPaddingBetweenElements,
+                            increasedPaddingAmount);
+                } else {
+                    int ownPadding = (int) NotificationUtils.interpolate(
+                            0,
+                            mPaddingBetweenElements,
+                            1.0f + increasedPaddingAmount);
+                    if (previousPaddingAmount > 0.0f) {
+                        padding = (int) NotificationUtils.interpolate(
+                                ownPadding,
+                                mIncreasedPaddingBetweenElements,
+                                previousPaddingAmount);
+                    } else {
+                        padding = ownPadding;
+                    }
+                    previousPaddingRequest = ownPadding;
+                }
+                if (height != 0) {
+                    height += padding;
+                }
+                previousPaddingAmount = increasedPaddingAmount;
+                height += expandableView.getIntrinsicHeight();
+                numShownItems++;
+                if (finish) {
+                    break;
+                }
+            }
+        }
+        mContentHeight = height + mTopPadding + mBottomMargin;
+        updateScrollability();
+        clampScrollPosition();
+        mAmbientState.setLayoutMaxHeight(mContentHeight);
+    }
+
+    private boolean isPulsing(NotificationData.Entry entry) {
+        return mAmbientState.isPulsing(entry);
+    }
+
+    public boolean hasPulsingNotifications() {
+        return mPulsing != null;
+    }
+
+    private void updateScrollability() {
+        boolean scrollable = getScrollRange() > 0;
+        if (scrollable != mScrollable) {
+            mScrollable = scrollable;
+            setFocusable(scrollable);
+            updateForwardAndBackwardScrollability();
+        }
+    }
+
+    private void updateForwardAndBackwardScrollability() {
+        boolean forwardScrollable = mScrollable && mOwnScrollY < getScrollRange();
+        boolean backwardsScrollable = mScrollable && mOwnScrollY > 0;
+        boolean changed = forwardScrollable != mForwardScrollable
+                || backwardsScrollable != mBackwardScrollable;
+        mForwardScrollable = forwardScrollable;
+        mBackwardScrollable = backwardsScrollable;
+        if (changed) {
+            sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+        }
+    }
+
+    private void updateBackground() {
+        // No need to update the background color if it's not being drawn.
+        if (!mShouldDrawNotificationBackground || mAmbientState.isDark()) {
+            return;
+        }
+
+        updateBackgroundBounds();
+        if (!mCurrentBounds.equals(mBackgroundBounds)) {
+            boolean animate = mAnimateNextBackgroundTop || mAnimateNextBackgroundBottom
+                    || areBoundsAnimating();
+            if (!isExpanded()) {
+                abortBackgroundAnimators();
+                animate = false;
+            }
+            if (animate) {
+                startBackgroundAnimation();
+            } else {
+                mCurrentBounds.set(mBackgroundBounds);
+                applyCurrentBackgroundBounds();
+            }
+        } else {
+            abortBackgroundAnimators();
+        }
+        mAnimateNextBackgroundBottom = false;
+        mAnimateNextBackgroundTop = false;
+    }
+
+    private void abortBackgroundAnimators() {
+        if (mBottomAnimator != null) {
+            mBottomAnimator.cancel();
+        }
+        if (mTopAnimator != null) {
+            mTopAnimator.cancel();
+        }
+    }
+
+    private boolean areBoundsAnimating() {
+        return mBottomAnimator != null || mTopAnimator != null;
+    }
+
+    private void startBackgroundAnimation() {
+        // left and right are always instantly applied
+        mCurrentBounds.left = mBackgroundBounds.left;
+        mCurrentBounds.right = mBackgroundBounds.right;
+        startBottomAnimation();
+        startTopAnimation();
+    }
+
+    private void startTopAnimation() {
+        int previousEndValue = mEndAnimationRect.top;
+        int newEndValue = mBackgroundBounds.top;
+        ObjectAnimator previousAnimator = mTopAnimator;
+        if (previousAnimator != null && previousEndValue == newEndValue) {
+            return;
+        }
+        if (!mAnimateNextBackgroundTop) {
+            // just a local update was performed
+            if (previousAnimator != null) {
+                // we need to increase all animation keyframes of the previous animator by the
+                // relative change to the end value
+                int previousStartValue = mStartAnimationRect.top;
+                PropertyValuesHolder[] values = previousAnimator.getValues();
+                values[0].setIntValues(previousStartValue, newEndValue);
+                mStartAnimationRect.top = previousStartValue;
+                mEndAnimationRect.top = newEndValue;
+                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+                return;
+            } else {
+                // no new animation needed, let's just apply the value
+                setBackgroundTop(newEndValue);
+                return;
+            }
+        }
+        if (previousAnimator != null) {
+            previousAnimator.cancel();
+        }
+        ObjectAnimator animator = ObjectAnimator.ofInt(this, "backgroundTop",
+                mCurrentBounds.top, newEndValue);
+        Interpolator interpolator = Interpolators.FAST_OUT_SLOW_IN;
+        animator.setInterpolator(interpolator);
+        animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+        // remove the tag when the animation is finished
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mStartAnimationRect.top = -1;
+                mEndAnimationRect.top = -1;
+                mTopAnimator = null;
+            }
+        });
+        animator.start();
+        mStartAnimationRect.top = mCurrentBounds.top;
+        mEndAnimationRect.top = newEndValue;
+        mTopAnimator = animator;
+    }
+
+    private void startBottomAnimation() {
+        int previousStartValue = mStartAnimationRect.bottom;
+        int previousEndValue = mEndAnimationRect.bottom;
+        int newEndValue = mBackgroundBounds.bottom;
+        ObjectAnimator previousAnimator = mBottomAnimator;
+        if (previousAnimator != null && previousEndValue == newEndValue) {
+            return;
+        }
+        if (!mAnimateNextBackgroundBottom) {
+            // just a local update was performed
+            if (previousAnimator != null) {
+                // we need to increase all animation keyframes of the previous animator by the
+                // relative change to the end value
+                PropertyValuesHolder[] values = previousAnimator.getValues();
+                values[0].setIntValues(previousStartValue, newEndValue);
+                mStartAnimationRect.bottom = previousStartValue;
+                mEndAnimationRect.bottom = newEndValue;
+                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+                return;
+            } else {
+                // no new animation needed, let's just apply the value
+                setBackgroundBottom(newEndValue);
+                return;
+            }
+        }
+        if (previousAnimator != null) {
+            previousAnimator.cancel();
+        }
+        ObjectAnimator animator = ObjectAnimator.ofInt(this, "backgroundBottom",
+                mCurrentBounds.bottom, newEndValue);
+        Interpolator interpolator = Interpolators.FAST_OUT_SLOW_IN;
+        animator.setInterpolator(interpolator);
+        animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+        // remove the tag when the animation is finished
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mStartAnimationRect.bottom = -1;
+                mEndAnimationRect.bottom = -1;
+                mBottomAnimator = null;
+            }
+        });
+        animator.start();
+        mStartAnimationRect.bottom = mCurrentBounds.bottom;
+        mEndAnimationRect.bottom = newEndValue;
+        mBottomAnimator = animator;
+    }
+
+    private void setBackgroundTop(int top) {
+        mCurrentBounds.top = top;
+        applyCurrentBackgroundBounds();
+    }
+
+    public void setBackgroundBottom(int bottom) {
+        mCurrentBounds.bottom = bottom;
+        applyCurrentBackgroundBounds();
+    }
+
+    private void applyCurrentBackgroundBounds() {
+        // If the background of the notification is not being drawn, then there is no need to
+        // exclude an area in the scrim. Rather, the scrim's color should serve as the background.
+        if (!mShouldDrawNotificationBackground) {
+            return;
+        }
+
+        mScrimController.setExcludedBackgroundArea(
+                mFadingOut || mParentNotFullyVisible || mAmbientState.isDark() || mIsClipped ? null
+                        : mCurrentBounds);
+        invalidate();
+    }
+
+    /**
+     * Update the background bounds to the new desired bounds
+     */
+    private void updateBackgroundBounds() {
+        if (mAmbientState.isPanelFullWidth()) {
+            mBackgroundBounds.left = 0;
+            mBackgroundBounds.right = getWidth();
+        } else {
+            getLocationInWindow(mTempInt2);
+            mBackgroundBounds.left = mTempInt2[0];
+            mBackgroundBounds.right = mTempInt2[0] + getWidth();
+        }
+        if (!mIsExpanded) {
+            mBackgroundBounds.top = 0;
+            mBackgroundBounds.bottom = 0;
+            return;
+        }
+        ActivatableNotificationView firstView = mFirstVisibleBackgroundChild;
+        int top = 0;
+        if (firstView != null) {
+            // Round Y up to avoid seeing the background during animation
+            int finalTranslationY = (int) Math.ceil(ViewState.getFinalTranslationY(firstView));
+            if (mAnimateNextBackgroundTop
+                    || mTopAnimator == null && mCurrentBounds.top == finalTranslationY
+                    || mTopAnimator != null && mEndAnimationRect.top == finalTranslationY) {
+                // we're ending up at the same location as we are now, lets just skip the animation
+                top = finalTranslationY;
+            } else {
+                top = (int) Math.ceil(firstView.getTranslationY());
+            }
+        }
+        ActivatableNotificationView lastView =
+                mShelf.hasItemsInStableShelf() && mShelf.getVisibility() != GONE
+                        ? mShelf
+                        : mLastVisibleBackgroundChild;
+        int bottom;
+        if (lastView != null) {
+            int finalTranslationY;
+            if (lastView == mShelf) {
+                finalTranslationY = (int) mShelf.getTranslationY();
+            } else {
+                finalTranslationY = (int) ViewState.getFinalTranslationY(lastView);
+            }
+            int finalHeight = ExpandableViewState.getFinalActualHeight(lastView);
+            int finalBottom = finalTranslationY + finalHeight - lastView.getClipBottomAmount();
+            finalBottom = Math.min(finalBottom, getHeight());
+            if (mAnimateNextBackgroundBottom
+                    || mBottomAnimator == null && mCurrentBounds.bottom == finalBottom
+                    || mBottomAnimator != null && mEndAnimationRect.bottom == finalBottom) {
+                // we're ending up at the same location as we are now, lets just skip the animation
+                bottom = finalBottom;
+            } else {
+                bottom = (int) (lastView.getTranslationY() + lastView.getActualHeight()
+                        - lastView.getClipBottomAmount());
+                bottom = Math.min(bottom, getHeight());
+            }
+        } else {
+            top = mTopPadding;
+            bottom = top;
+        }
+        if (mStatusBarState != StatusBarState.KEYGUARD) {
+            top = (int) Math.max(mTopPadding + mStackTranslation, top);
+        } else {
+            // otherwise the animation from the shade to the keyguard will jump as it's maxed
+            top = Math.max(0, top);
+        }
+        mBackgroundBounds.top = top;
+        mBackgroundBounds.bottom = Math.min(getHeight(), Math.max(bottom, top));
+    }
+
+    private ActivatableNotificationView getFirstPinnedHeadsUp() {
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            if (child.getVisibility() != View.GONE
+                    && child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                if (row.isPinned()) {
+                    return row;
+                }
+            }
+        }
+        return null;
+    }
+
+    private ActivatableNotificationView getLastChildWithBackground() {
+        int childCount = getChildCount();
+        for (int i = childCount - 1; i >= 0; i--) {
+            View child = getChildAt(i);
+            if (child.getVisibility() != View.GONE && child instanceof ActivatableNotificationView
+                    && child != mShelf) {
+                return (ActivatableNotificationView) child;
+            }
+        }
+        return null;
+    }
+
+    private ActivatableNotificationView getFirstChildWithBackground() {
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            if (child.getVisibility() != View.GONE && child instanceof ActivatableNotificationView
+                    && child != mShelf) {
+                return (ActivatableNotificationView) child;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Fling the scroll view
+     *
+     * @param velocityY The initial velocity in the Y direction. Positive
+     *                  numbers mean that the finger/cursor is moving down the screen,
+     *                  which means we want to scroll towards the top.
+     */
+    protected void fling(int velocityY) {
+        if (getChildCount() > 0) {
+            int scrollRange = getScrollRange();
+
+            float topAmount = getCurrentOverScrollAmount(true);
+            float bottomAmount = getCurrentOverScrollAmount(false);
+            if (velocityY < 0 && topAmount > 0) {
+                setOwnScrollY(mOwnScrollY - (int) topAmount);
+                mDontReportNextOverScroll = true;
+                setOverScrollAmount(0, true, false);
+                mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor(true /* onTop */)
+                        * mOverflingDistance + topAmount;
+            } else if (velocityY > 0 && bottomAmount > 0) {
+                setOwnScrollY((int) (mOwnScrollY + bottomAmount));
+                setOverScrollAmount(0, false, false);
+                mMaxOverScroll = Math.abs(velocityY) / 1000f
+                        * getRubberBandFactor(false /* onTop */) * mOverflingDistance
+                        +  bottomAmount;
+            } else {
+                // it will be set once we reach the boundary
+                mMaxOverScroll = 0.0f;
+            }
+            int minScrollY = Math.max(0, scrollRange);
+            if (mExpandedInThisMotion) {
+                minScrollY = Math.min(minScrollY, mMaxScrollAfterExpand);
+            }
+            mScroller.fling(mScrollX, mOwnScrollY, 1, velocityY, 0, 0, 0, minScrollY, 0,
+                    mExpandedInThisMotion && mOwnScrollY >= 0 ? 0 : Integer.MAX_VALUE / 2);
+
+            animateScroll();
+        }
+    }
+
+    /**
+     * @return Whether a fling performed on the top overscroll edge lead to the expanded
+     * overScroll view (i.e QS).
+     */
+    private boolean shouldOverScrollFling(int initialVelocity) {
+        float topOverScroll = getCurrentOverScrollAmount(true);
+        return mScrolledToTopOnFirstDown
+                && !mExpandedInThisMotion
+                && topOverScroll > mMinTopOverScrollToEscape
+                && initialVelocity > 0;
+    }
+
+    /**
+     * Updates the top padding of the notifications, taking {@link #getIntrinsicPadding()} into
+     * account.
+     *
+     * @param qsHeight the top padding imposed by the quick settings panel
+     * @param animate whether to animate the change
+     * @param ignoreIntrinsicPadding if true, {@link #getIntrinsicPadding()} is ignored and
+     *                               {@code qsHeight} is the final top padding
+     */
+    public void updateTopPadding(float qsHeight, boolean animate,
+            boolean ignoreIntrinsicPadding) {
+        int topPadding = (int) qsHeight;
+        int minStackHeight = getLayoutMinHeight();
+        if (topPadding + minStackHeight > getHeight()) {
+            mTopPaddingOverflow = topPadding + minStackHeight - getHeight();
+        } else {
+            mTopPaddingOverflow = 0;
+        }
+        setTopPadding(ignoreIntrinsicPadding ? topPadding : clampPadding(topPadding),
+                animate);
+        setExpandedHeight(mExpandedHeight);
+    }
+
+    public int getLayoutMinHeight() {
+        return mShelf.getVisibility() == GONE ? 0 : mShelf.getIntrinsicHeight();
+    }
+
+    public int getFirstChildIntrinsicHeight() {
+        final ExpandableView firstChild = getFirstChildNotGone();
+        int firstChildMinHeight = firstChild != null
+                ? firstChild.getIntrinsicHeight()
+                : mEmptyShadeView != null
+                        ? mEmptyShadeView.getIntrinsicHeight()
+                        : mCollapsedSize;
+        if (mOwnScrollY > 0) {
+            firstChildMinHeight = Math.max(firstChildMinHeight - mOwnScrollY, mCollapsedSize);
+        }
+        return firstChildMinHeight;
+    }
+
+    public float getTopPaddingOverflow() {
+        return mTopPaddingOverflow;
+    }
+
+    public int getPeekHeight() {
+        final ExpandableView firstChild = getFirstChildNotGone();
+        final int firstChildMinHeight = firstChild != null ? firstChild.getCollapsedHeight()
+                : mCollapsedSize;
+        int shelfHeight = 0;
+        if (mLastVisibleBackgroundChild != null && mShelf.getVisibility() != GONE) {
+            shelfHeight = mShelf.getIntrinsicHeight();
+        }
+        return mIntrinsicPadding + firstChildMinHeight + shelfHeight;
+    }
+
+    private int clampPadding(int desiredPadding) {
+        return Math.max(desiredPadding, mIntrinsicPadding);
+    }
+
+    private float getRubberBandFactor(boolean onTop) {
+        if (!onTop) {
+            return RUBBER_BAND_FACTOR_NORMAL;
+        }
+        if (mExpandedInThisMotion) {
+            return RUBBER_BAND_FACTOR_AFTER_EXPAND;
+        } else if (mIsExpansionChanging || mPanelTracking) {
+            return RUBBER_BAND_FACTOR_ON_PANEL_EXPAND;
+        } else if (mScrolledToTopOnFirstDown) {
+            return 1.0f;
+        }
+        return RUBBER_BAND_FACTOR_NORMAL;
+    }
+
+    /**
+     * Accompanying function for {@link #getRubberBandFactor}: Returns true if the overscroll is
+     * rubberbanded, false if it is technically an overscroll but rather a motion to expand the
+     * overscroll view (e.g. expand QS).
+     */
+    private boolean isRubberbanded(boolean onTop) {
+        return !onTop || mExpandedInThisMotion || mIsExpansionChanging || mPanelTracking
+                || !mScrolledToTopOnFirstDown;
+    }
+
+    private void endDrag() {
+        setIsBeingDragged(false);
+
+        recycleVelocityTracker();
+
+        if (getCurrentOverScrollAmount(true /* onTop */) > 0) {
+            setOverScrollAmount(0, true /* onTop */, true /* animate */);
+        }
+        if (getCurrentOverScrollAmount(false /* onTop */) > 0) {
+            setOverScrollAmount(0, false /* onTop */, true /* animate */);
+        }
+    }
+
+    private void transformTouchEvent(MotionEvent ev, View sourceView, View targetView) {
+        ev.offsetLocation(sourceView.getX(), sourceView.getY());
+        ev.offsetLocation(-targetView.getX(), -targetView.getY());
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        initDownStates(ev);
+        handleEmptySpaceClick(ev);
+        boolean expandWantsIt = false;
+        if (!mSwipingInProgress && !mOnlyScrollingInThisMotion) {
+            expandWantsIt = mExpandHelper.onInterceptTouchEvent(ev);
+        }
+        boolean scrollWantsIt = false;
+        if (!mSwipingInProgress && !mExpandingNotification) {
+            scrollWantsIt = onInterceptTouchEventScroll(ev);
+        }
+        boolean swipeWantsIt = false;
+        if (!mIsBeingDragged
+                && !mExpandingNotification
+                && !mExpandedInThisMotion
+                && !mOnlyScrollingInThisMotion
+                && !mDisallowDismissInThisMotion) {
+            swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
+        }
+        // Check if we need to clear any snooze leavebehinds
+        boolean isUp = ev.getActionMasked() == MotionEvent.ACTION_UP;
+        NotificationGuts guts = mStatusBar.getExposedGuts();
+        if (!isTouchInView(ev, guts) && isUp && !swipeWantsIt && !expandWantsIt
+                && !scrollWantsIt) {
+            mCheckForLeavebehind = false;
+            mStatusBar.closeAndSaveGuts(true /* removeLeavebehind */, false /* force */,
+                    false /* removeControls */, -1 /* x */, -1 /* y */, false /* resetMenu */);
+        }
+        if (ev.getActionMasked() == MotionEvent.ACTION_UP) {
+            mCheckForLeavebehind = true;
+        }
+        return swipeWantsIt || scrollWantsIt || expandWantsIt || super.onInterceptTouchEvent(ev);
+    }
+
+    private void handleEmptySpaceClick(MotionEvent ev) {
+        switch (ev.getActionMasked()) {
+            case MotionEvent.ACTION_MOVE:
+                if (mTouchIsClick && (Math.abs(ev.getY() - mInitialTouchY) > mTouchSlop
+                        || Math.abs(ev.getX() - mInitialTouchX) > mTouchSlop )) {
+                    mTouchIsClick = false;
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mStatusBarState != StatusBarState.KEYGUARD && mTouchIsClick &&
+                        isBelowLastNotification(mInitialTouchX, mInitialTouchY)) {
+                    mOnEmptySpaceClickListener.onEmptySpaceClicked(mInitialTouchX, mInitialTouchY);
+                }
+                break;
+        }
+    }
+
+    private void initDownStates(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            mExpandedInThisMotion = false;
+            mOnlyScrollingInThisMotion = !mScroller.isFinished();
+            mDisallowScrollingInThisMotion = false;
+            mDisallowDismissInThisMotion = false;
+            mTouchIsClick = true;
+            mInitialTouchX = ev.getX();
+            mInitialTouchY = ev.getY();
+        }
+    }
+
+    public void setChildTransferInProgress(boolean childTransferInProgress) {
+        mChildTransferInProgress = childTransferInProgress;
+    }
+
+    @Override
+    public 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 (!mChildTransferInProgress) {
+            onViewRemovedInternal(child, this);
+        }
+    }
+
+    /**
+     * Called when a notification is removed from the shade. This cleans up the state for a given
+     * view.
+     */
+    public void cleanUpViewState(View child) {
+        if (child == mTranslatingParentView) {
+            mTranslatingParentView = null;
+        }
+        mCurrentStackScrollState.removeViewStateForView(child);
+    }
+
+    @Override
+    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+        super.requestDisallowInterceptTouchEvent(disallowIntercept);
+        if (disallowIntercept) {
+            mSwipeHelper.removeLongPressCallback();
+        }
+    }
+
+    private void onViewRemovedInternal(View child, ViewGroup container) {
+        if (mChangePositionInProgress) {
+            // This is only a position change, don't do anything special
+            return;
+        }
+        ExpandableView expandableView = (ExpandableView) child;
+        expandableView.setOnHeightChangedListener(null);
+        mCurrentStackScrollState.removeViewStateForView(child);
+        updateScrollStateForRemovedChild(expandableView);
+        boolean animationGenerated = generateRemoveAnimation(child);
+        if (animationGenerated) {
+            if (!mSwipedOutViews.contains(child)) {
+                container.getOverlay().add(child);
+            } else if (Math.abs(expandableView.getTranslation()) != expandableView.getWidth()) {
+                container.addTransientView(child, 0);
+                expandableView.setTransientContainer(container);
+            }
+        } else {
+            mSwipedOutViews.remove(child);
+        }
+        updateAnimationState(false, child);
+
+        focusNextViewIfFocused(child);
+    }
+
+    private void focusNextViewIfFocused(View view) {
+        if (view instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+            if (row.shouldRefocusOnDismiss()) {
+                View nextView = row.getChildAfterViewWhenDismissed();
+                if (nextView == null) {
+                    View groupParentWhenDismissed = row.getGroupParentWhenDismissed();
+                    nextView = getFirstChildBelowTranlsationY(groupParentWhenDismissed != null
+                            ? groupParentWhenDismissed.getTranslationY()
+                            : view.getTranslationY(), true /* ignoreChildren */);
+                }
+                if (nextView != null) {
+                    nextView.requestAccessibilityFocus();
+                }
+            }
+        }
+
+    }
+
+    private boolean isChildInGroup(View child) {
+        return child instanceof ExpandableNotificationRow
+                && mGroupManager.isChildInGroupWithSummary(
+                        ((ExpandableNotificationRow) child).getStatusBarNotification());
+    }
+
+    /**
+     * Generate a remove animation for a child view.
+     *
+     * @param child The view to generate the remove animation for.
+     * @return Whether an animation was generated.
+     */
+    private boolean generateRemoveAnimation(View child) {
+        if (removeRemovedChildFromHeadsUpChangeAnimations(child)) {
+            mAddedHeadsUpChildren.remove(child);
+            return false;
+        }
+        if (isClickedHeadsUp(child)) {
+            // An animation is already running, add it to the Overlay
+            mClearOverlayViewsWhenFinished.add(child);
+            return true;
+        }
+        if (mIsExpanded && mAnimationsEnabled && !isChildInInvisibleGroup(child)) {
+            if (!mChildrenToAddAnimated.contains(child)) {
+                // Generate Animations
+                mChildrenToRemoveAnimated.add(child);
+                mNeedsAnimation = true;
+                return true;
+            } else {
+                mChildrenToAddAnimated.remove(child);
+                mFromMoreCardAdditions.remove(child);
+                return false;
+            }
+        }
+        return false;
+    }
+
+    private boolean isClickedHeadsUp(View child) {
+        return HeadsUpManager.isClickedHeadsUpNotification(child);
+    }
+
+    /**
+     * Remove a removed child view from the heads up animations if it was just added there
+     *
+     * @return whether any child was removed from the list to animate
+     */
+    private boolean removeRemovedChildFromHeadsUpChangeAnimations(View child) {
+        boolean hasAddEvent = false;
+        for (Pair<ExpandableNotificationRow, Boolean> eventPair : mHeadsUpChangeAnimations) {
+            ExpandableNotificationRow row = eventPair.first;
+            boolean isHeadsUp = eventPair.second;
+            if (child == row) {
+                mTmpList.add(eventPair);
+                hasAddEvent |= isHeadsUp;
+            }
+        }
+        if (hasAddEvent) {
+            // This child was just added lets remove all events.
+            mHeadsUpChangeAnimations.removeAll(mTmpList);
+            ((ExpandableNotificationRow ) child).setHeadsUpAnimatingAway(false);
+        }
+        mTmpList.clear();
+        return hasAddEvent;
+    }
+
+    /**
+     * @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 row.getVisibility() == View.INVISIBLE;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Updates the scroll position when a child was removed
+     *
+     * @param removedChild the removed child
+     */
+    private void updateScrollStateForRemovedChild(ExpandableView removedChild) {
+        int startingPosition = getPositionInLinearLayout(removedChild);
+        float increasedPaddingAmount = removedChild.getIncreasedPaddingAmount();
+        int padding;
+        if (increasedPaddingAmount >= 0) {
+            padding = (int) NotificationUtils.interpolate(
+                    mPaddingBetweenElements,
+                    mIncreasedPaddingBetweenElements,
+                    increasedPaddingAmount);
+        } else {
+            padding = (int) NotificationUtils.interpolate(
+                    0,
+                    mPaddingBetweenElements,
+                    1.0f + increasedPaddingAmount);
+        }
+        int childHeight = getIntrinsicHeight(removedChild) + padding;
+        int endPosition = startingPosition + childHeight;
+        if (endPosition <= mOwnScrollY) {
+            // This child is fully scrolled of the top, so we have to deduct its height from the
+            // scrollPosition
+            setOwnScrollY(mOwnScrollY - childHeight);
+        } else if (startingPosition < mOwnScrollY) {
+            // This child is currently being scrolled into, set the scroll position to the start of
+            // this child
+            setOwnScrollY(startingPosition);
+        }
+    }
+
+    private int getIntrinsicHeight(View view) {
+        if (view instanceof ExpandableView) {
+            ExpandableView expandableView = (ExpandableView) view;
+            return expandableView.getIntrinsicHeight();
+        }
+        return view.getHeight();
+    }
+
+    public int getPositionInLinearLayout(View requestedView) {
+        ExpandableNotificationRow childInGroup = null;
+        ExpandableNotificationRow requestedRow = null;
+        if (isChildInGroup(requestedView)) {
+            // We're asking for a child in a group. Calculate the position of the parent first,
+            // then within the parent.
+            childInGroup = (ExpandableNotificationRow) requestedView;
+            requestedView = requestedRow = childInGroup.getNotificationParent();
+        }
+        int position = 0;
+        float previousPaddingRequest = mPaddingBetweenElements;
+        float previousPaddingAmount = 0.0f;
+        for (int i = 0; i < getChildCount(); i++) {
+            ExpandableView child = (ExpandableView) getChildAt(i);
+            boolean notGone = child.getVisibility() != View.GONE;
+            if (notGone && !child.hasNoContentHeight()) {
+                float increasedPaddingAmount = child.getIncreasedPaddingAmount();
+                float padding;
+                if (increasedPaddingAmount >= 0.0f) {
+                    padding = (int) NotificationUtils.interpolate(
+                            previousPaddingRequest,
+                            mIncreasedPaddingBetweenElements,
+                            increasedPaddingAmount);
+                    previousPaddingRequest = (int) NotificationUtils.interpolate(
+                            mPaddingBetweenElements,
+                            mIncreasedPaddingBetweenElements,
+                            increasedPaddingAmount);
+                } else {
+                    int ownPadding = (int) NotificationUtils.interpolate(
+                            0,
+                            mPaddingBetweenElements,
+                            1.0f + increasedPaddingAmount);
+                    if (previousPaddingAmount > 0.0f) {
+                        padding = (int) NotificationUtils.interpolate(
+                                ownPadding,
+                                mIncreasedPaddingBetweenElements,
+                                previousPaddingAmount);
+                    } else {
+                        padding = ownPadding;
+                    }
+                    previousPaddingRequest = ownPadding;
+                }
+                if (position != 0) {
+                    position += padding;
+                }
+                previousPaddingAmount = increasedPaddingAmount;
+            }
+            if (child == requestedView) {
+                if (requestedRow != null) {
+                    position += requestedRow.getPositionOfChild(childInGroup);
+                }
+                return position;
+            }
+            if (notGone) {
+                position += getIntrinsicHeight(child);
+            }
+        }
+        return 0;
+    }
+
+    @Override
+    public void onViewAdded(View child) {
+        super.onViewAdded(child);
+        onViewAddedInternal(child);
+    }
+
+    private void updateFirstAndLastBackgroundViews() {
+        ActivatableNotificationView firstChild = getFirstChildWithBackground();
+        ActivatableNotificationView lastChild = getLastChildWithBackground();
+        if (mAnimationsEnabled && mIsExpanded) {
+            mAnimateNextBackgroundTop = firstChild != mFirstVisibleBackgroundChild;
+            mAnimateNextBackgroundBottom = lastChild != mLastVisibleBackgroundChild;
+        } else {
+            mAnimateNextBackgroundTop = false;
+            mAnimateNextBackgroundBottom = false;
+        }
+        mFirstVisibleBackgroundChild = firstChild;
+        mLastVisibleBackgroundChild = lastChild;
+        mAmbientState.setLastVisibleBackgroundChild(lastChild);
+    }
+
+    private void onViewAddedInternal(View child) {
+        updateHideSensitiveForChild(child);
+        ((ExpandableView) child).setOnHeightChangedListener(this);
+        generateAddAnimation(child, false /* fromMoreCard */);
+        updateAnimationState(child);
+        updateChronometerForChild(child);
+    }
+
+    private void updateHideSensitiveForChild(View child) {
+        if (child instanceof ExpandableView) {
+            ExpandableView expandableView = (ExpandableView) child;
+            expandableView.setHideSensitiveForIntrinsicHeight(mAmbientState.isHideSensitive());
+        }
+    }
+
+    public void notifyGroupChildRemoved(View row, ViewGroup childrenContainer) {
+        onViewRemovedInternal(row, childrenContainer);
+    }
+
+    public void notifyGroupChildAdded(View row) {
+        onViewAddedInternal(row);
+    }
+
+    public void setAnimationsEnabled(boolean animationsEnabled) {
+        mAnimationsEnabled = animationsEnabled;
+        updateNotificationAnimationStates();
+    }
+
+    private void updateNotificationAnimationStates() {
+        boolean running = mAnimationsEnabled || hasPulsingNotifications();
+        mShelf.setAnimationsEnabled(running);
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            running &= mIsExpanded || isPinnedHeadsUp(child);
+            updateAnimationState(running, child);
+        }
+    }
+
+    private void updateAnimationState(View child) {
+        updateAnimationState((mAnimationsEnabled || hasPulsingNotifications())
+                && (mIsExpanded || isPinnedHeadsUp(child)), child);
+    }
+
+
+    private void updateAnimationState(boolean running, View child) {
+        if (child instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+            row.setIconAnimationRunning(running);
+        }
+    }
+
+    public boolean isAddOrRemoveAnimationPending() {
+        return mNeedsAnimation
+                && (!mChildrenToAddAnimated.isEmpty() || !mChildrenToRemoveAnimated.isEmpty());
+    }
+    /**
+     * Generate an animation for an added child view.
+     *
+     * @param child The view to be added.
+     * @param fromMoreCard Whether this add is coming from the "more" card on lockscreen.
+     */
+    public void generateAddAnimation(View child, boolean fromMoreCard) {
+        if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress) {
+            // Generate Animations
+            mChildrenToAddAnimated.add(child);
+            if (fromMoreCard) {
+                mFromMoreCardAdditions.add(child);
+            }
+            mNeedsAnimation = true;
+        }
+        if (isHeadsUp(child) && mAnimationsEnabled && !mChangePositionInProgress) {
+            mAddedHeadsUpChildren.add(child);
+            mChildrenToAddAnimated.remove(child);
+        }
+    }
+
+    /**
+     * Change the position of child to a new location
+     *
+     * @param child the view to change the position for
+     * @param newIndex the new index
+     */
+    public void changeViewPosition(View child, int newIndex) {
+        int currentIndex = indexOfChild(child);
+        if (child != null && child.getParent() == this && currentIndex != newIndex) {
+            mChangePositionInProgress = true;
+            ((ExpandableView)child).setChangingPosition(true);
+            removeView(child);
+            addView(child, newIndex);
+            ((ExpandableView)child).setChangingPosition(false);
+            mChangePositionInProgress = false;
+            if (mIsExpanded && mAnimationsEnabled && child.getVisibility() != View.GONE) {
+                mChildrenChangingPositions.add(child);
+                mNeedsAnimation = true;
+            }
+        }
+    }
+
+    private void startAnimationToState() {
+        if (mNeedsAnimation) {
+            generateChildHierarchyEvents();
+            mNeedsAnimation = false;
+        }
+        if (!mAnimationEvents.isEmpty() || isCurrentlyAnimating()) {
+            setAnimationRunning(true);
+            mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState,
+                    mGoToFullShadeDelay);
+            mAnimationEvents.clear();
+            updateBackground();
+            updateViewShadows();
+        } else {
+            applyCurrentState();
+        }
+        mGoToFullShadeDelay = 0;
+    }
+
+    private void generateChildHierarchyEvents() {
+        generateHeadsUpAnimationEvents();
+        generateChildRemovalEvents();
+        generateChildAdditionEvents();
+        generatePositionChangeEvents();
+        generateSnapBackEvents();
+        generateDragEvents();
+        generateTopPaddingEvent();
+        generateActivateEvent();
+        generateDimmedEvent();
+        generateHideSensitiveEvent();
+        generateDarkEvent();
+        generateGoToFullShadeEvent();
+        generateViewResizeEvent();
+        generateGroupExpansionEvent();
+        generateAnimateEverythingEvent();
+        mNeedsAnimation = false;
+    }
+
+    private void generateHeadsUpAnimationEvents() {
+        for (Pair<ExpandableNotificationRow, Boolean> eventPair : mHeadsUpChangeAnimations) {
+            ExpandableNotificationRow row = eventPair.first;
+            boolean isHeadsUp = eventPair.second;
+            int type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_OTHER;
+            boolean onBottom = false;
+            boolean pinnedAndClosed = row.isPinned() && !mIsExpanded;
+            if (!mIsExpanded && !isHeadsUp) {
+                type = row.wasJustClicked()
+                        ? AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
+                        : AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR;
+                if (row.isChildInGroup()) {
+                    // We can otherwise get stuck in there if it was just isolated
+                    row.setHeadsUpAnimatingAway(false);
+                }
+            } else {
+                ExpandableViewState viewState = mCurrentStackScrollState.getViewStateForView(row);
+                if (viewState == null) {
+                    // A view state was never generated for this view, so we don't need to animate
+                    // this. This may happen with notification children.
+                    continue;
+                }
+                if (isHeadsUp && (mAddedHeadsUpChildren.contains(row) || pinnedAndClosed)) {
+                    if (pinnedAndClosed || shouldHunAppearFromBottom(viewState)) {
+                        // Our custom add animation
+                        type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR;
+                    } else {
+                        // Normal add animation
+                        type = AnimationEvent.ANIMATION_TYPE_ADD;
+                    }
+                    onBottom = !pinnedAndClosed;
+                }
+            }
+            AnimationEvent event = new AnimationEvent(row, type);
+            event.headsUpFromBottom = onBottom;
+            mAnimationEvents.add(event);
+        }
+        mHeadsUpChangeAnimations.clear();
+        mAddedHeadsUpChildren.clear();
+    }
+
+    private boolean shouldHunAppearFromBottom(ExpandableViewState viewState) {
+        if (viewState.yTranslation + viewState.height < mAmbientState.getMaxHeadsUpTranslation()) {
+            return false;
+        }
+        return true;
+    }
+
+    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(
+                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_VIEW_RESIZE));
+        }
+        mNeedViewResizeAnimation = false;
+    }
+
+    private void generateSnapBackEvents() {
+        for (View child : mSnappedBackChildren) {
+            mAnimationEvents.add(new AnimationEvent(child,
+                    AnimationEvent.ANIMATION_TYPE_SNAP_BACK));
+        }
+        mSnappedBackChildren.clear();
+    }
+
+    private void generateDragEvents() {
+        for (View child : mDragAnimPendingChildren) {
+            mAnimationEvents.add(new AnimationEvent(child,
+                    AnimationEvent.ANIMATION_TYPE_START_DRAG));
+        }
+        mDragAnimPendingChildren.clear();
+    }
+
+    private void generateChildRemovalEvents() {
+        for (View child : mChildrenToRemoveAnimated) {
+            boolean childWasSwipedOut = mSwipedOutViews.contains(child);
+
+            // we need to know the view after this one
+            float removedTranslation = child.getTranslationY();
+            boolean ignoreChildren = true;
+            if (child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                if (row.isRemoved() && row.wasChildInGroupWhenRemoved()) {
+                    removedTranslation = row.getTranslationWhenRemoved();
+                    ignoreChildren = false;
+                }
+                childWasSwipedOut |= Math.abs(row.getTranslation()) == row.getWidth();
+            }
+            if (!childWasSwipedOut) {
+                Rect clipBounds = child.getClipBounds();
+                childWasSwipedOut = clipBounds.height() == 0;
+            }
+            int animationType = childWasSwipedOut
+                    ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT
+                    : AnimationEvent.ANIMATION_TYPE_REMOVE;
+            AnimationEvent event = new AnimationEvent(child, animationType);
+            event.viewAfterChangingView = getFirstChildBelowTranlsationY(removedTranslation,
+                    ignoreChildren);
+            mAnimationEvents.add(event);
+            mSwipedOutViews.remove(child);
+        }
+        mChildrenToRemoveAnimated.clear();
+    }
+
+    private void generatePositionChangeEvents() {
+        for (View child : mChildrenChangingPositions) {
+            mAnimationEvents.add(new AnimationEvent(child,
+                    AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION));
+        }
+        mChildrenChangingPositions.clear();
+        if (mGenerateChildOrderChangedEvent) {
+            mAnimationEvents.add(new AnimationEvent(null,
+                    AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION));
+            mGenerateChildOrderChangedEvent = false;
+        }
+    }
+
+    private void generateChildAdditionEvents() {
+        for (View child : mChildrenToAddAnimated) {
+            if (mFromMoreCardAdditions.contains(child)) {
+                mAnimationEvents.add(new AnimationEvent(child,
+                        AnimationEvent.ANIMATION_TYPE_ADD,
+                        StackStateAnimator.ANIMATION_DURATION_STANDARD));
+            } else {
+                mAnimationEvents.add(new AnimationEvent(child,
+                        AnimationEvent.ANIMATION_TYPE_ADD));
+            }
+        }
+        mChildrenToAddAnimated.clear();
+        mFromMoreCardAdditions.clear();
+    }
+
+    private void generateTopPaddingEvent() {
+        if (mTopPaddingNeedsAnimation) {
+            mAnimationEvents.add(
+                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED));
+        }
+        mTopPaddingNeedsAnimation = false;
+    }
+
+    private void generateActivateEvent() {
+        if (mActivateNeedsAnimation) {
+            mAnimationEvents.add(
+                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_ACTIVATED_CHILD));
+        }
+        mActivateNeedsAnimation = false;
+    }
+
+    private void generateAnimateEverythingEvent() {
+        if (mEverythingNeedsAnimation) {
+            mAnimationEvents.add(
+                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_EVERYTHING));
+        }
+        mEverythingNeedsAnimation = false;
+    }
+
+    private void generateDimmedEvent() {
+        if (mDimmedNeedsAnimation) {
+            mAnimationEvents.add(
+                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DIMMED));
+        }
+        mDimmedNeedsAnimation = false;
+    }
+
+    private void generateHideSensitiveEvent() {
+        if (mHideSensitiveNeedsAnimation) {
+            mAnimationEvents.add(
+                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_HIDE_SENSITIVE));
+        }
+        mHideSensitiveNeedsAnimation = false;
+    }
+
+    private void generateDarkEvent() {
+        if (mDarkNeedsAnimation) {
+            AnimationEvent ev = new AnimationEvent(null,
+                    AnimationEvent.ANIMATION_TYPE_DARK,
+                    new AnimationFilter()
+                            .animateDark()
+                            .animateY(mShelf));
+            ev.darkAnimationOriginIndex = mDarkAnimationOriginIndex;
+            mAnimationEvents.add(ev);
+            startBackgroundFadeIn();
+        }
+        mDarkNeedsAnimation = false;
+    }
+
+    private void generateGoToFullShadeEvent() {
+        if (mGoToFullShadeNeedsAnimation) {
+            mAnimationEvents.add(
+                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_GO_TO_FULL_SHADE));
+        }
+        mGoToFullShadeNeedsAnimation = false;
+    }
+
+    private boolean onInterceptTouchEventScroll(MotionEvent ev) {
+        if (!isScrollingEnabled()) {
+            return false;
+        }
+        /*
+         * This method JUST determines whether we want to intercept the motion.
+         * If we return true, onMotionEvent will be called and we do the actual
+         * scrolling there.
+         */
+
+        /*
+        * Shortcut the most recurring case: the user is in the dragging
+        * state and is moving their finger.  We want to intercept this
+        * motion.
+        */
+        final int action = ev.getAction();
+        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
+            return true;
+        }
+
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_MOVE: {
+                /*
+                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
+                 * whether the user has moved far enough from the original down touch.
+                 */
+
+                /*
+                * Locally do absolute value. mLastMotionY is set to the y value
+                * of the down event.
+                */
+                final int activePointerId = mActivePointerId;
+                if (activePointerId == INVALID_POINTER) {
+                    // If we don't have a valid id, the touch down wasn't on content.
+                    break;
+                }
+
+                final int pointerIndex = ev.findPointerIndex(activePointerId);
+                if (pointerIndex == -1) {
+                    Log.e(TAG, "Invalid pointerId=" + activePointerId
+                            + " in onInterceptTouchEvent");
+                    break;
+                }
+
+                final int y = (int) ev.getY(pointerIndex);
+                final int x = (int) ev.getX(pointerIndex);
+                final int yDiff = Math.abs(y - mLastMotionY);
+                final int xDiff = Math.abs(x - mDownX);
+                if (yDiff > mTouchSlop && yDiff > xDiff) {
+                    setIsBeingDragged(true);
+                    mLastMotionY = y;
+                    mDownX = x;
+                    initVelocityTrackerIfNotExists();
+                    mVelocityTracker.addMovement(ev);
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_DOWN: {
+                final int y = (int) ev.getY();
+                mScrolledToTopOnFirstDown = isScrolledToTop();
+                if (getChildAtPosition(ev.getX(), y, false /* requireMinHeight */) == null) {
+                    setIsBeingDragged(false);
+                    recycleVelocityTracker();
+                    break;
+                }
+
+                /*
+                 * Remember location of down touch.
+                 * ACTION_DOWN always refers to pointer index 0.
+                 */
+                mLastMotionY = y;
+                mDownX = (int) ev.getX();
+                mActivePointerId = ev.getPointerId(0);
+
+                initOrResetVelocityTracker();
+                mVelocityTracker.addMovement(ev);
+                /*
+                * If being flinged and user touches the screen, initiate drag;
+                * otherwise don't.  mScroller.isFinished should be false when
+                * being flinged.
+                */
+                boolean isBeingDragged = !mScroller.isFinished();
+                setIsBeingDragged(isBeingDragged);
+                break;
+            }
+
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                /* Release the drag */
+                setIsBeingDragged(false);
+                mActivePointerId = INVALID_POINTER;
+                recycleVelocityTracker();
+                if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
+                    animateScroll();
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                onSecondaryPointerUp(ev);
+                break;
+        }
+
+        /*
+        * The only time we want to intercept motion events is if we are in the
+        * drag mode.
+        */
+        return mIsBeingDragged;
+    }
+
+    protected StackScrollAlgorithm createStackScrollAlgorithm(Context context) {
+        return new StackScrollAlgorithm(context);
+    }
+
+    /**
+     * @return Whether the specified motion event is actually happening over the content.
+     */
+    private boolean isInContentBounds(MotionEvent event) {
+        return isInContentBounds(event.getY());
+    }
+
+    /**
+     * @return Whether a y coordinate is inside the content.
+     */
+    public boolean isInContentBounds(float y) {
+        return y < getHeight() - getEmptyBottomMargin();
+    }
+
+    private void setIsBeingDragged(boolean isDragged) {
+        mIsBeingDragged = isDragged;
+        if (isDragged) {
+            requestDisallowInterceptTouchEvent(true);
+            removeLongPressCallback();
+        }
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        super.onWindowFocusChanged(hasWindowFocus);
+        if (!hasWindowFocus) {
+            removeLongPressCallback();
+        }
+    }
+
+    @Override
+    public void clearChildFocus(View child) {
+        super.clearChildFocus(child);
+        if (mForcedScroll == child) {
+            mForcedScroll = null;
+        }
+    }
+
+    @Override
+    public void requestDisallowLongPress() {
+        removeLongPressCallback();
+    }
+
+    @Override
+    public void requestDisallowDismiss() {
+        mDisallowDismissInThisMotion = true;
+    }
+
+    public void removeLongPressCallback() {
+        mSwipeHelper.removeLongPressCallback();
+    }
+
+    @Override
+    public boolean isScrolledToTop() {
+        return mOwnScrollY == 0;
+    }
+
+    @Override
+    public boolean isScrolledToBottom() {
+        return mOwnScrollY >= getScrollRange();
+    }
+
+    @Override
+    public View getHostView() {
+        return this;
+    }
+
+    public int getEmptyBottomMargin() {
+        return Math.max(mMaxLayoutHeight - mContentHeight, 0);
+    }
+
+    public void checkSnoozeLeavebehind() {
+        if (mCheckForLeavebehind) {
+            mStatusBar.closeAndSaveGuts(true /* removeLeavebehind */, false /* force */,
+                    false /* removeControls */, -1 /* x */, -1 /* y */, false /* resetMenu */);
+            mCheckForLeavebehind = false;
+        }
+    }
+
+    public void resetCheckSnoozeLeavebehind() {
+        mCheckForLeavebehind = true;
+    }
+
+    public void onExpansionStarted() {
+        mIsExpansionChanging = true;
+        mAmbientState.setExpansionChanging(true);
+        checkSnoozeLeavebehind();
+    }
+
+    public void onExpansionStopped() {
+        mIsExpansionChanging = false;
+        resetCheckSnoozeLeavebehind();
+        mAmbientState.setExpansionChanging(false);
+        if (!mIsExpanded) {
+            setOwnScrollY(0);
+            mStatusBar.resetUserExpandedStates();
+            clearTemporaryViews();
+            clearUserLockedViews();
+        }
+    }
+
+    private void clearUserLockedViews() {
+        for (int i = 0; i < getChildCount(); i++) {
+            ExpandableView child = (ExpandableView) getChildAt(i);
+            if (child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                row.setUserLocked(false);
+            }
+        }
+    }
+
+    private void clearTemporaryViews() {
+        // lets make sure nothing is in the overlay / transient anymore
+        clearTemporaryViews(this);
+        for (int i = 0; i < getChildCount(); i++) {
+            ExpandableView child = (ExpandableView) getChildAt(i);
+            if (child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                clearTemporaryViews(row.getChildrenContainer());
+            }
+        }
+    }
+
+    private void clearTemporaryViews(ViewGroup viewGroup) {
+        while (viewGroup != null && viewGroup.getTransientViewCount() != 0) {
+            viewGroup.removeTransientView(viewGroup.getTransientView(0));
+        }
+        if (viewGroup != null) {
+            viewGroup.getOverlay().clear();
+        }
+    }
+
+    public void onPanelTrackingStarted() {
+        mPanelTracking = true;
+        mAmbientState.setPanelTracking(true);
+    }
+    public void onPanelTrackingStopped() {
+        mPanelTracking = false;
+        mAmbientState.setPanelTracking(false);
+    }
+
+    public void resetScrollPosition() {
+        mScroller.abortAnimation();
+        setOwnScrollY(0);
+    }
+
+    private void setIsExpanded(boolean isExpanded) {
+        boolean changed = isExpanded != mIsExpanded;
+        mIsExpanded = isExpanded;
+        mStackScrollAlgorithm.setIsExpanded(isExpanded);
+        if (changed) {
+            if (!mIsExpanded) {
+                mGroupManager.collapseAllGroups();
+                mExpandHelper.cancelImmediately();
+            }
+            updateNotificationAnimationStates();
+            updateChronometers();
+            requestChildrenUpdate();
+        }
+    }
+
+    private void updateChronometers() {
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            updateChronometerForChild(getChildAt(i));
+        }
+    }
+
+    private void updateChronometerForChild(View child) {
+        if (child instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+            row.setChronometerRunning(mIsExpanded);
+        }
+    }
+
+    @Override
+    public void onHeightChanged(ExpandableView view, boolean needsAnimation) {
+        updateContentHeight();
+        updateScrollPositionOnExpandInBottom(view);
+        clampScrollPosition();
+        notifyHeightChangeListener(view);
+        ExpandableNotificationRow row = view instanceof ExpandableNotificationRow
+                ? (ExpandableNotificationRow) view
+                : null;
+        if (row != null && (row == mFirstVisibleBackgroundChild
+                || row.getNotificationParent() == mFirstVisibleBackgroundChild)) {
+            updateAlgorithmLayoutMinHeight();
+        }
+        if (needsAnimation) {
+            requestAnimationOnViewResize(row);
+        }
+        requestChildrenUpdate();
+    }
+
+    @Override
+    public void onReset(ExpandableView view) {
+        updateAnimationState(view);
+        updateChronometerForChild(view);
+    }
+
+    private void updateScrollPositionOnExpandInBottom(ExpandableView view) {
+        if (view instanceof ExpandableNotificationRow && !onKeyguard()) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+            if (row.isUserLocked() && row != getFirstChildNotGone()) {
+                if (row.isSummaryWithChildren()) {
+                    return;
+                }
+                // We are actually expanding this view
+                float endPosition = row.getTranslationY() + row.getActualHeight();
+                if (row.isChildInGroup()) {
+                    endPosition += row.getNotificationParent().getTranslationY();
+                }
+                int layoutEnd = mMaxLayoutHeight + (int) mStackTranslation;
+                if (row != mLastVisibleBackgroundChild && mShelf.getVisibility() != GONE) {
+                    layoutEnd -= mShelf.getIntrinsicHeight() + mPaddingBetweenElements;
+                }
+                if (endPosition > layoutEnd) {
+                    setOwnScrollY((int) (mOwnScrollY + endPosition - layoutEnd));
+                    mDisallowScrollingInThisMotion = true;
+                }
+            }
+        }
+    }
+
+    public void setOnHeightChangedListener(
+            ExpandableView.OnHeightChangedListener mOnHeightChangedListener) {
+        this.mOnHeightChangedListener = mOnHeightChangedListener;
+    }
+
+    public void setOnEmptySpaceClickListener(OnEmptySpaceClickListener listener) {
+        mOnEmptySpaceClickListener = listener;
+    }
+
+    public void onChildAnimationFinished() {
+        setAnimationRunning(false);
+        requestChildrenUpdate();
+        runAnimationFinishedRunnables();
+        clearViewOverlays();
+        clearHeadsUpDisappearRunning();
+    }
+
+    private void clearHeadsUpDisappearRunning() {
+        for (int i = 0; i < getChildCount(); i++) {
+            View view = getChildAt(i);
+            if (view instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+                row.setHeadsUpAnimatingAway(false);
+                if (row.isSummaryWithChildren()) {
+                    for (ExpandableNotificationRow child : row.getNotificationChildren()) {
+                        child.setHeadsUpAnimatingAway(false);
+                    }
+                }
+            }
+        }
+    }
+
+    private void clearViewOverlays() {
+        for (View view : mClearOverlayViewsWhenFinished) {
+            StackStateAnimator.removeFromOverlay(view);
+        }
+        mClearOverlayViewsWhenFinished.clear();
+    }
+
+    private void runAnimationFinishedRunnables() {
+        for (Runnable runnable : mAnimationFinishedRunnables) {
+            runnable.run();
+        }
+        mAnimationFinishedRunnables.clear();
+    }
+
+    /**
+     * See {@link AmbientState#setDimmed}.
+     */
+    public void setDimmed(boolean dimmed, boolean animate) {
+        dimmed &= onKeyguard();
+        mAmbientState.setDimmed(dimmed);
+        if (animate && mAnimationsEnabled) {
+            mDimmedNeedsAnimation = true;
+            mNeedsAnimation =  true;
+            animateDimmed(dimmed);
+        } else {
+            setDimAmount(dimmed ? 1.0f : 0.0f);
+        }
+        requestChildrenUpdate();
+    }
+
+    @VisibleForTesting
+    boolean isDimmed() {
+        return mAmbientState.isDimmed();
+    }
+
+    private void setDimAmount(float dimAmount) {
+        mDimAmount = dimAmount;
+        updateBackgroundDimming();
+    }
+
+    private void animateDimmed(boolean dimmed) {
+        if (mDimAnimator != null) {
+            mDimAnimator.cancel();
+        }
+        float target = dimmed ? 1.0f : 0.0f;
+        if (target == mDimAmount) {
+            return;
+        }
+        mDimAnimator = TimeAnimator.ofFloat(mDimAmount, target);
+        mDimAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED);
+        mDimAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        mDimAnimator.addListener(mDimEndListener);
+        mDimAnimator.addUpdateListener(mDimUpdateListener);
+        mDimAnimator.start();
+    }
+
+    public void setHideSensitive(boolean hideSensitive, boolean animate) {
+        if (hideSensitive != mAmbientState.isHideSensitive()) {
+            int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                ExpandableView v = (ExpandableView) getChildAt(i);
+                v.setHideSensitiveForIntrinsicHeight(hideSensitive);
+            }
+            mAmbientState.setHideSensitive(hideSensitive);
+            if (animate && mAnimationsEnabled) {
+                mHideSensitiveNeedsAnimation = true;
+                mNeedsAnimation =  true;
+            }
+            requestChildrenUpdate();
+        }
+    }
+
+    /**
+     * See {@link AmbientState#setActivatedChild}.
+     */
+    public void setActivatedChild(ActivatableNotificationView activatedChild) {
+        mAmbientState.setActivatedChild(activatedChild);
+        if (mAnimationsEnabled) {
+            mActivateNeedsAnimation = true;
+            mNeedsAnimation =  true;
+        }
+        requestChildrenUpdate();
+    }
+
+    public ActivatableNotificationView getActivatedChild() {
+        return mAmbientState.getActivatedChild();
+    }
+
+    private void applyCurrentState() {
+        mCurrentStackScrollState.apply();
+        if (mListener != null) {
+            mListener.onChildLocationsChanged(this);
+        }
+        runAnimationFinishedRunnables();
+        setAnimationRunning(false);
+        updateBackground();
+        updateViewShadows();
+    }
+
+    private void updateViewShadows() {
+        // we need to work around an issue where the shadow would not cast between siblings when
+        // their z difference is between 0 and 0.1
+
+        // Lefts first sort by Z difference
+        for (int i = 0; i < getChildCount(); i++) {
+            ExpandableView child = (ExpandableView) getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                mTmpSortedChildren.add(child);
+            }
+        }
+        Collections.sort(mTmpSortedChildren, mViewPositionComparator);
+
+        // Now lets update the shadow for the views
+        ExpandableView previous = null;
+        for (int i = 0; i < mTmpSortedChildren.size(); i++) {
+            ExpandableView expandableView = mTmpSortedChildren.get(i);
+            float translationZ = expandableView.getTranslationZ();
+            float otherZ = previous == null ? translationZ : previous.getTranslationZ();
+            float diff = otherZ - translationZ;
+            if (diff <= 0.0f || diff >= FakeShadowView.SHADOW_SIBLING_TRESHOLD) {
+                // There is no fake shadow to be drawn
+                expandableView.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
+            } else {
+                float yLocation = previous.getTranslationY() + previous.getActualHeight() -
+                        expandableView.getTranslationY() - previous.getExtraBottomPadding();
+                expandableView.setFakeShadowIntensity(
+                        diff / FakeShadowView.SHADOW_SIBLING_TRESHOLD,
+                        previous.getOutlineAlpha(), (int) yLocation,
+                        previous.getOutlineTranslation());
+            }
+            previous = expandableView;
+        }
+
+        mTmpSortedChildren.clear();
+    }
+
+    /**
+     * Update colors of "dismiss" and "empty shade" views.
+     *
+     * @param lightTheme True if light theme should be used.
+     */
+    public void updateDecorViews(boolean lightTheme) {
+        if (lightTheme == mUsingLightTheme) {
+            return;
+        }
+        mUsingLightTheme = lightTheme;
+        Context context = new ContextThemeWrapper(mContext,
+                lightTheme ? R.style.Theme_SystemUI_Light : R.style.Theme_SystemUI);
+        final int textColor = Utils.getColorAttr(context, R.attr.wallpaperTextColor);
+        mDismissView.setTextColor(textColor);
+        mEmptyShadeView.setTextColor(textColor);
+    }
+
+    public void goToFullShade(long delay) {
+        if (mDismissView != null) {
+            mDismissView.setInvisible();
+        }
+        mEmptyShadeView.setInvisible();
+        mGoToFullShadeNeedsAnimation = true;
+        mGoToFullShadeDelay = delay;
+        mNeedsAnimation = true;
+        requestChildrenUpdate();
+    }
+
+    public void cancelExpandHelper() {
+        mExpandHelper.cancel();
+    }
+
+    public void setIntrinsicPadding(int intrinsicPadding) {
+        mIntrinsicPadding = intrinsicPadding;
+        mAmbientState.setIntrinsicPadding(intrinsicPadding);
+    }
+
+    public int getIntrinsicPadding() {
+        return mIntrinsicPadding;
+    }
+
+    /**
+     * @return the y position of the first notification
+     */
+    public float getNotificationsTopY() {
+        return mTopPadding + getStackTranslation();
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return true;
+    }
+
+    /**
+     * See {@link AmbientState#setDark}.
+     */
+    public void setDark(boolean dark, boolean animate, @Nullable PointF touchWakeUpScreenLocation) {
+        if (mAmbientState.isDark() == dark) {
+            return;
+        }
+        mAmbientState.setDark(dark);
+        if (animate && mAnimationsEnabled) {
+            mDarkNeedsAnimation = true;
+            mDarkAnimationOriginIndex = findDarkAnimationOriginIndex(touchWakeUpScreenLocation);
+            mNeedsAnimation =  true;
+            setBackgroundFadeAmount(0.0f);
+        } else if (!dark) {
+            setBackgroundFadeAmount(1.0f);
+        }
+        requestChildrenUpdate();
+        if (dark) {
+            mScrimController.setExcludedBackgroundArea(null);
+        } else {
+            updateBackground();
+        }
+
+        updateWillNotDraw();
+        updateContentHeight();
+        notifyHeightChangeListener(mShelf);
+    }
+
+    /**
+     * Updates whether or not this Layout will perform its own custom drawing (i.e. whether or
+     * not {@link #onDraw(Canvas)} is called). This method should be called whenever the
+     * {@link #mAmbientState}'s dark mode is toggled.
+     */
+    private void updateWillNotDraw() {
+        boolean willDraw = !mAmbientState.isDark() && mShouldDrawNotificationBackground || DEBUG;
+        setWillNotDraw(!willDraw);
+    }
+
+    private void setBackgroundFadeAmount(float fadeAmount) {
+        mBackgroundFadeAmount = fadeAmount;
+        updateBackgroundDimming();
+    }
+
+    public float getBackgroundFadeAmount() {
+        return mBackgroundFadeAmount;
+    }
+
+    private void startBackgroundFadeIn() {
+        ObjectAnimator fadeAnimator = ObjectAnimator.ofFloat(this, BACKGROUND_FADE, 0f, 1f);
+        fadeAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_WAKEUP);
+        fadeAnimator.setInterpolator(Interpolators.ALPHA_IN);
+        fadeAnimator.start();
+    }
+
+    private int findDarkAnimationOriginIndex(@Nullable PointF screenLocation) {
+        if (screenLocation == null || screenLocation.y < mTopPadding) {
+            return AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_ABOVE;
+        }
+        if (screenLocation.y > getBottomMostNotificationBottom()) {
+            return AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_BELOW;
+        }
+        View child = getClosestChildAtRawPosition(screenLocation.x, screenLocation.y);
+        if (child != null) {
+            return getNotGoneIndex(child);
+        } else {
+            return AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_ABOVE;
+        }
+    }
+
+    private int getNotGoneIndex(View child) {
+        int count = getChildCount();
+        int notGoneIndex = 0;
+        for (int i = 0; i < count; i++) {
+            View v = getChildAt(i);
+            if (child == v) {
+                return notGoneIndex;
+            }
+            if (v.getVisibility() != View.GONE) {
+                notGoneIndex++;
+            }
+        }
+        return -1;
+    }
+
+    public void setDismissView(@NonNull DismissView dismissView) {
+        int index = -1;
+        if (mDismissView != null) {
+            index = indexOfChild(mDismissView);
+            removeView(mDismissView);
+        }
+        mDismissView = dismissView;
+        addView(mDismissView, index);
+    }
+
+    public void setEmptyShadeView(EmptyShadeView emptyShadeView) {
+        int index = -1;
+        if (mEmptyShadeView != null) {
+            index = indexOfChild(mEmptyShadeView);
+            removeView(mEmptyShadeView);
+        }
+        mEmptyShadeView = emptyShadeView;
+        addView(mEmptyShadeView, index);
+    }
+
+    public void updateEmptyShadeView(boolean visible) {
+        int oldVisibility = mEmptyShadeView.willBeGone() ? GONE : mEmptyShadeView.getVisibility();
+        int newVisibility = visible ? VISIBLE : GONE;
+        if (oldVisibility != newVisibility) {
+            if (newVisibility != GONE) {
+                if (mEmptyShadeView.willBeGone()) {
+                    mEmptyShadeView.cancelAnimation();
+                } else {
+                    mEmptyShadeView.setInvisible();
+                }
+                mEmptyShadeView.setVisibility(newVisibility);
+                mEmptyShadeView.setWillBeGone(false);
+                updateContentHeight();
+                notifyHeightChangeListener(mEmptyShadeView);
+            } else {
+                Runnable onFinishedRunnable = new Runnable() {
+                    @Override
+                    public void run() {
+                        mEmptyShadeView.setVisibility(GONE);
+                        mEmptyShadeView.setWillBeGone(false);
+                        updateContentHeight();
+                        notifyHeightChangeListener(mEmptyShadeView);
+                    }
+                };
+                if (mAnimationsEnabled && mIsExpanded) {
+                    mEmptyShadeView.setWillBeGone(true);
+                    mEmptyShadeView.performVisibilityAnimation(false, onFinishedRunnable);
+                } else {
+                    mEmptyShadeView.setInvisible();
+                    onFinishedRunnable.run();
+                }
+            }
+        }
+    }
+
+    public void updateDismissView(boolean visible) {
+        if (mDismissView == null) {
+            return;
+        }
+
+        int oldVisibility = mDismissView.willBeGone() ? GONE : mDismissView.getVisibility();
+        int newVisibility = visible ? VISIBLE : GONE;
+        if (oldVisibility != newVisibility) {
+            if (newVisibility != GONE) {
+                if (mDismissView.willBeGone()) {
+                    mDismissView.cancelAnimation();
+                } else {
+                    mDismissView.setInvisible();
+                }
+                mDismissView.setVisibility(newVisibility);
+                mDismissView.setWillBeGone(false);
+                updateContentHeight();
+                notifyHeightChangeListener(mDismissView);
+            } else {
+                Runnable dimissHideFinishRunnable = new Runnable() {
+                    @Override
+                    public void run() {
+                        mDismissView.setVisibility(GONE);
+                        mDismissView.setWillBeGone(false);
+                        updateContentHeight();
+                        notifyHeightChangeListener(mDismissView);
+                    }
+                };
+                if (mDismissView.isButtonVisible() && mIsExpanded && mAnimationsEnabled) {
+                    mDismissView.setWillBeGone(true);
+                    mDismissView.performVisibilityAnimation(false, dimissHideFinishRunnable);
+                } else {
+                    dimissHideFinishRunnable.run();
+                }
+            }
+        }
+    }
+
+    public void setDismissAllInProgress(boolean dismissAllInProgress) {
+        mDismissAllInProgress = dismissAllInProgress;
+        mAmbientState.setDismissAllInProgress(dismissAllInProgress);
+        handleDismissAllClipping();
+    }
+
+    private void handleDismissAllClipping() {
+        final int count = getChildCount();
+        boolean previousChildWillBeDismissed = false;
+        for (int i = 0; i < count; i++) {
+            ExpandableView child = (ExpandableView) getChildAt(i);
+            if (child.getVisibility() == GONE) {
+                continue;
+            }
+            if (mDismissAllInProgress && previousChildWillBeDismissed) {
+                child.setMinClipTopAmount(child.getClipTopAmount());
+            } else {
+                child.setMinClipTopAmount(0);
+            }
+            previousChildWillBeDismissed = canChildBeDismissed(child);
+        }
+    }
+
+    public boolean isDismissViewNotGone() {
+        return mDismissView != null
+                && mDismissView.getVisibility() != View.GONE
+                && !mDismissView.willBeGone();
+    }
+
+    public boolean isDismissViewVisible() {
+        return mDismissView != null && mDismissView.isVisible();
+    }
+
+    public int getDismissViewHeight() {
+        return mDismissView == null ? 0 : mDismissView.getHeight() + mPaddingBetweenElements;
+    }
+
+    public int getEmptyShadeViewHeight() {
+        return mEmptyShadeView.getHeight();
+    }
+
+    public float getBottomMostNotificationBottom() {
+        final int count = getChildCount();
+        float max = 0;
+        for (int childIdx = 0; childIdx < count; childIdx++) {
+            ExpandableView child = (ExpandableView) getChildAt(childIdx);
+            if (child.getVisibility() == GONE) {
+                continue;
+            }
+            float bottom = child.getTranslationY() + child.getActualHeight()
+                    - child.getClipBottomAmount();
+            if (bottom > max) {
+                max = bottom;
+            }
+        }
+        return max + getStackTranslation();
+    }
+
+    public void setStatusBar(StatusBar statusBar) {
+        this.mStatusBar = statusBar;
+    }
+
+    public void setGroupManager(NotificationGroupManager groupManager) {
+        this.mGroupManager = groupManager;
+    }
+
+    public void onGoToKeyguard() {
+        requestAnimateEverything();
+    }
+
+    private void requestAnimateEverything() {
+        if (mIsExpanded && mAnimationsEnabled) {
+            mEverythingNeedsAnimation = true;
+            mNeedsAnimation = true;
+            requestChildrenUpdate();
+        }
+    }
+
+    public boolean isBelowLastNotification(float touchX, float touchY) {
+        int childCount = getChildCount();
+        for (int i = childCount - 1; i >= 0; i--) {
+            ExpandableView child = (ExpandableView) getChildAt(i);
+            if (child.getVisibility() != View.GONE) {
+                float childTop = child.getY();
+                if (childTop > touchY) {
+                    // we are above a notification entirely let's abort
+                    return false;
+                }
+                boolean belowChild = touchY > childTop + child.getActualHeight()
+                        - child.getClipBottomAmount();
+                if (child == mDismissView) {
+                    if(!belowChild && !mDismissView.isOnEmptySpace(touchX - mDismissView.getX(),
+                                    touchY - childTop)) {
+                        // We clicked on the dismiss button
+                        return false;
+                    }
+                } else if (child == mEmptyShadeView) {
+                    // We arrived at the empty shade view, for which we accept all clicks
+                    return true;
+                } else if (!belowChild){
+                    // We are on a child
+                    return false;
+                }
+            }
+        }
+        return touchY > mTopPadding + mStackTranslation;
+    }
+
+    @Override
+    public void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded) {
+        boolean animated = !mGroupExpandedForMeasure && mAnimationsEnabled
+                && (mIsExpanded || changedRow.isPinned());
+        if (animated) {
+            mExpandedGroupView = changedRow;
+            mNeedsAnimation = true;
+        }
+        changedRow.setChildrenExpanded(expanded, animated);
+        if (!mGroupExpandedForMeasure) {
+            onHeightChanged(changedRow, false /* needsAnimation */);
+        }
+        runAfterAnimationFinished(new Runnable() {
+            @Override
+            public void run() {
+                changedRow.onFinishedExpansionChange();
+            }
+        });
+    }
+
+    @Override
+    public void onGroupCreatedFromChildren(NotificationGroupManager.NotificationGroup group) {
+        mStatusBar.requestNotificationUpdate();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEventInternal(event);
+        event.setScrollable(mScrollable);
+        event.setScrollX(mScrollX);
+        event.setScrollY(mOwnScrollY);
+        event.setMaxScrollX(mScrollX);
+        event.setMaxScrollY(getScrollRange());
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        if (mScrollable) {
+            info.setScrollable(true);
+            if (mBackwardScrollable) {
+                info.addAction(
+                        AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
+                info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP);
+            }
+            if (mForwardScrollable) {
+                info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
+                info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN);
+            }
+        }
+        // Talkback only listenes to scroll events of certain classes, let's make us a scrollview
+        info.setClassName(ScrollView.class.getName());
+    }
+
+    /** @hide */
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+        if (!isEnabled()) {
+            return false;
+        }
+        int direction = -1;
+        switch (action) {
+            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+                // fall through
+            case android.R.id.accessibilityActionScrollDown:
+                direction = 1;
+                // fall through
+            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+                // fall through
+            case android.R.id.accessibilityActionScrollUp:
+                final int viewportHeight = getHeight() - mPaddingBottom - mTopPadding - mPaddingTop
+                        - mShelf.getIntrinsicHeight();
+                final int targetScrollY = Math.max(0,
+                        Math.min(mOwnScrollY + direction * viewportHeight, getScrollRange()));
+                if (targetScrollY != mOwnScrollY) {
+                    mScroller.startScroll(mScrollX, mOwnScrollY, 0, targetScrollY - mOwnScrollY);
+                    animateScroll();
+                    return true;
+                }
+                break;
+        }
+        return false;
+    }
+
+    @Override
+    public void onGroupsChanged() {
+        mStatusBar.requestNotificationUpdate();
+    }
+
+    public void generateChildOrderChangedEvent() {
+        if (mIsExpanded && mAnimationsEnabled) {
+            mGenerateChildOrderChangedEvent = true;
+            mNeedsAnimation = true;
+            requestChildrenUpdate();
+        }
+    }
+
+    public void runAfterAnimationFinished(Runnable runnable) {
+        mAnimationFinishedRunnables.add(runnable);
+    }
+
+    public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+        mHeadsUpManager = headsUpManager;
+        mAmbientState.setHeadsUpManager(headsUpManager);
+    }
+
+    public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) {
+        if (mAnimationsEnabled && (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed)) {
+            mHeadsUpChangeAnimations.add(new Pair<>(row, isHeadsUp));
+            mNeedsAnimation = true;
+            if (!mIsExpanded && !isHeadsUp) {
+                row.setHeadsUpAnimatingAway(true);
+            }
+            requestChildrenUpdate();
+        }
+    }
+
+    public void setShadeExpanded(boolean shadeExpanded) {
+        mAmbientState.setShadeExpanded(shadeExpanded);
+        mStateAnimator.setShadeExpanded(shadeExpanded);
+    }
+
+    /**
+     * Set the boundary for the bottom heads up position. The heads up will always be above this
+     * position.
+     *
+     * @param height the height of the screen
+     * @param bottomBarHeight the height of the bar on the bottom
+     */
+    public void setHeadsUpBoundaries(int height, int bottomBarHeight) {
+        mAmbientState.setMaxHeadsUpTranslation(height - bottomBarHeight);
+        mStateAnimator.setHeadsUpAppearHeightBottom(height);
+        requestChildrenUpdate();
+    }
+
+    public void setTrackingHeadsUp(boolean trackingHeadsUp) {
+        mTrackingHeadsUp = trackingHeadsUp;
+    }
+
+    public void setScrimController(ScrimController scrimController) {
+        mScrimController = scrimController;
+        mScrimController.setScrimBehindChangeRunnable(this::updateBackgroundDimming);
+    }
+
+    public void forceNoOverlappingRendering(boolean force) {
+        mForceNoOverlappingRendering = force;
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return !mForceNoOverlappingRendering && super.hasOverlappingRendering();
+    }
+
+    public void setAnimationRunning(boolean animationRunning) {
+        if (animationRunning != mAnimationRunning) {
+            if (animationRunning) {
+                getViewTreeObserver().addOnPreDrawListener(mRunningAnimationUpdater);
+            } else {
+                getViewTreeObserver().removeOnPreDrawListener(mRunningAnimationUpdater);
+            }
+            mAnimationRunning = animationRunning;
+            updateContinuousShadowDrawing();
+        }
+    }
+
+    public boolean isExpanded() {
+        return mIsExpanded;
+    }
+
+    public void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> pulsing) {
+        if (mPulsing == null && pulsing == null) {
+            return;
+        }
+        mPulsing = pulsing;
+        mAmbientState.setPulsing(pulsing);
+        updateNotificationAnimationStates();
+        updateContentHeight();
+        notifyHeightChangeListener(mShelf);
+        requestChildrenUpdate();
+    }
+
+    public void setFadingOut(boolean fadingOut) {
+        if (fadingOut != mFadingOut) {
+            mFadingOut = fadingOut;
+            updateFadingState();
+        }
+    }
+
+    public void setParentNotFullyVisible(boolean parentNotFullyVisible) {
+        if (mScrimController == null) {
+            // we're not set up yet.
+            return;
+        }
+        if (parentNotFullyVisible != mParentNotFullyVisible) {
+            mParentNotFullyVisible = parentNotFullyVisible;
+            updateFadingState();
+        }
+    }
+
+    private void updateFadingState() {
+        applyCurrentBackgroundBounds();
+        updateSrcDrawing();
+    }
+
+    @Override
+    public void setAlpha(@FloatRange(from = 0.0, to = 1.0) float alpha) {
+        super.setAlpha(alpha);
+        setFadingOut(alpha != 1.0f);
+    }
+
+    public void setQsExpanded(boolean qsExpanded) {
+        mQsExpanded = qsExpanded;
+        updateAlgorithmLayoutMinHeight();
+    }
+
+    public void setOwnScrollY(int ownScrollY) {
+        if (ownScrollY != mOwnScrollY) {
+            // We still want to call the normal scrolled changed for accessibility reasons
+            onScrollChanged(mScrollX, ownScrollY, mScrollX, mOwnScrollY);
+            mOwnScrollY = ownScrollY;
+            updateForwardAndBackwardScrollability();
+            requestChildrenUpdate();
+        }
+    }
+
+    public void setShelf(NotificationShelf shelf) {
+        int index = -1;
+        if (mShelf != null) {
+            index = indexOfChild(mShelf);
+            removeView(mShelf);
+        }
+        mShelf = shelf;
+        addView(mShelf, index);
+        mAmbientState.setShelf(shelf);
+        mStateAnimator.setShelf(shelf);
+        shelf.bind(mAmbientState, this);
+    }
+
+    public NotificationShelf getNotificationShelf() {
+        return mShelf;
+    }
+
+    public void setMaxDisplayedNotifications(int maxDisplayedNotifications) {
+        if (mMaxDisplayedNotifications != maxDisplayedNotifications) {
+            mMaxDisplayedNotifications = maxDisplayedNotifications;
+            updateContentHeight();
+            notifyHeightChangeListener(mShelf);
+        }
+    }
+
+    public int getMinExpansionHeight() {
+        return mShelf.getIntrinsicHeight() - (mShelf.getIntrinsicHeight() - mStatusBarHeight) / 2;
+    }
+
+    public void setInHeadsUpPinnedMode(boolean inHeadsUpPinnedMode) {
+        mInHeadsUpPinnedMode = inHeadsUpPinnedMode;
+        updateClipping();
+    }
+
+    public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {
+        mHeadsUpAnimatingAway = headsUpAnimatingAway;
+        updateClipping();
+    }
+
+    public void setStatusBarState(int statusBarState) {
+        mStatusBarState = statusBarState;
+        mAmbientState.setStatusBarState(statusBarState);
+    }
+
+    public void setExpandingVelocity(float expandingVelocity) {
+        mAmbientState.setExpandingVelocity(expandingVelocity);
+    }
+
+    public float getOpeningHeight() {
+        if (mEmptyShadeView.getVisibility() == GONE) {
+            return getMinExpansionHeight();
+        } else {
+            return getAppearEndPosition();
+        }
+    }
+
+    public void setIsFullWidth(boolean isFullWidth) {
+        mAmbientState.setPanelFullWidth(isFullWidth);
+    }
+
+    public void setUnlockHintRunning(boolean running) {
+        mAmbientState.setUnlockHintRunning(running);
+    }
+
+    public void setQsCustomizerShowing(boolean isShowing) {
+        mAmbientState.setQsCustomizerShowing(isShowing);
+        requestChildrenUpdate();
+    }
+
+    public void setHeadsUpGoingAwayAnimationsAllowed(boolean headsUpGoingAwayAnimationsAllowed) {
+        mHeadsUpGoingAwayAnimationsAllowed = headsUpGoingAwayAnimationsAllowed;
+    }
+
+    public void setDarkShelfOffsetX(int shelfOffsetX) {
+        mShelf.setDarkOffsetX(shelfOffsetX);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println(String.format("[%s: pulsing=%s qsCustomizerShowing=%s visibility=%s"
+                        + " alpha:%f scrollY:%d]",
+                this.getClass().getSimpleName(),
+                mPulsing != null ?"T":"f",
+                mAmbientState.isQsCustomizerShowing() ? "T":"f",
+                getVisibility() == View.VISIBLE ? "visible"
+                        : getVisibility() == View.GONE ? "gone"
+                                : "invisible",
+                getAlpha(),
+                mAmbientState.getScrollY()));
+    }
+
+    public void setTouchActive(boolean touchActive) {
+        mShelf.setTouchActive(touchActive);
+    }
+
+    /**
+     * A listener that is notified when some child locations might have changed.
+     */
+    public interface OnChildLocationsChangedListener {
+        void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout);
+    }
+
+    /**
+     * A listener that is notified when the empty space below the notifications is clicked on
+     */
+    public interface OnEmptySpaceClickListener {
+        void onEmptySpaceClicked(float x, float y);
+    }
+
+    /**
+     * A listener that gets notified when the overscroll at the top has changed.
+     */
+    public interface OnOverscrollTopChangedListener {
+
+        /**
+         * Notifies a listener that the overscroll has changed.
+         *
+         * @param amount the amount of overscroll, in pixels
+         * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an
+         *                     unrubberbanded motion to directly expand overscroll view (e.g expand
+         *                     QS)
+         */
+        void onOverscrollTopChanged(float amount, boolean isRubberbanded);
+
+        /**
+         * Notify a listener that the scroller wants to escape from the scrolling motion and
+         * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS)
+         *
+         * @param velocity The velocity that the Scroller had when over flinging
+         * @param open Should the fling open or close the overscroll view.
+         */
+        void flingTopOverscroll(float velocity, boolean open);
+    }
+
+    private class NotificationSwipeHelper extends SwipeHelper
+            implements NotificationSwipeActionHelper {
+        private static final long COVER_MENU_DELAY = 4000;
+        private Runnable mFalsingCheck;
+        private Handler mHandler;
+
+        public NotificationSwipeHelper(int swipeDirection, Callback callback, Context context) {
+            super(swipeDirection, callback, context);
+            mHandler = new Handler();
+            mFalsingCheck = new Runnable() {
+                @Override
+                public void run() {
+                    resetExposedMenuView(true /* animate */, true /* force */);
+                }
+            };
+        }
+
+        @Override
+        public void onDownUpdate(View currView, MotionEvent ev) {
+            mTranslatingParentView = currView;
+            if (mCurrMenuRow != null) {
+                mCurrMenuRow.onTouchEvent(currView, ev, 0 /* velocity */);
+            }
+            mCurrMenuRow = null;
+            mHandler.removeCallbacks(mFalsingCheck);
+
+            // Slide back any notifications that might be showing a menu
+            resetExposedMenuView(true /* animate */, false /* force */);
+
+            if (currView instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) currView;
+                mCurrMenuRow = row.createMenu();
+                mCurrMenuRow.setSwipeActionHelper(NotificationSwipeHelper.this);
+                mCurrMenuRow.setMenuClickListener(NotificationStackScrollLayout.this);
+                mCurrMenuRow.onTouchEvent(currView, ev, 0 /* velocity */);
+            }
+        }
+
+        @Override
+        public void onMoveUpdate(View view, MotionEvent ev, float translation, float delta) {
+            mHandler.removeCallbacks(mFalsingCheck);
+            if (mCurrMenuRow != null) {
+                mCurrMenuRow.onTouchEvent(view, ev, 0 /* velocity */);
+            }
+        }
+
+        @Override
+        public boolean handleUpEvent(MotionEvent ev, View animView, float velocity,
+                float translation) {
+            if (mCurrMenuRow != null) {
+                return mCurrMenuRow.onTouchEvent(animView, ev, velocity);
+            }
+            return false;
+        }
+
+        @Override
+        public void dismissChild(final View view, float velocity,
+                boolean useAccelerateInterpolator) {
+            super.dismissChild(view, velocity, useAccelerateInterpolator);
+            if (mIsExpanded) {
+                // We don't want to quick-dismiss when it's a heads up as this might lead to closing
+                // of the panel early.
+                handleChildDismissed(view);
+            }
+            mStatusBar.closeAndSaveGuts(true /* removeLeavebehind */, false /* force */,
+                    false /* removeControls */, -1 /* x */, -1 /* y */, false /* resetMenu */);
+            handleMenuCoveredOrDismissed();
+        }
+
+        @Override
+        public void snapChild(final View animView, final float targetLeft, float velocity) {
+            super.snapChild(animView, targetLeft, velocity);
+            onDragCancelled(animView);
+            if (targetLeft == 0) {
+                handleMenuCoveredOrDismissed();
+            }
+        }
+
+        @Override
+        public void snooze(StatusBarNotification sbn, SnoozeOption snoozeOption) {
+            mStatusBar.setNotificationSnoozed(sbn, snoozeOption);
+        }
+
+        public boolean isFalseGesture(MotionEvent ev) {
+            return super.isFalseGesture(ev);
+        }
+
+        private void handleMenuCoveredOrDismissed() {
+            if (mMenuExposedView != null && mMenuExposedView == mTranslatingParentView) {
+                mMenuExposedView = null;
+            }
+        }
+
+        @Override
+        public Animator getViewTranslationAnimator(View v, float target,
+                AnimatorUpdateListener listener) {
+            if (v instanceof ExpandableNotificationRow) {
+                return ((ExpandableNotificationRow) v).getTranslateViewAnimator(target, listener);
+            } else {
+                return super.getViewTranslationAnimator(v, target, listener);
+            }
+        }
+
+        @Override
+        public void setTranslation(View v, float translate) {
+            ((ExpandableView) v).setTranslation(translate);
+        }
+
+        @Override
+        public float getTranslation(View v) {
+            return ((ExpandableView) v).getTranslation();
+        }
+
+        @Override
+        public void dismiss(View animView, float velocity) {
+            dismissChild(animView, velocity,
+                    !swipedFastEnough(0, 0) /* useAccelerateInterpolator */);
+        }
+
+        @Override
+        public void snap(View animView, float targetLeft, float velocity) {
+            snapChild(animView, targetLeft, velocity);
+        }
+
+        @Override
+        public boolean swipedFarEnough(float translation, float viewSize) {
+            return swipedFarEnough();
+        }
+
+        @Override
+        public boolean swipedFastEnough(float translation, float velocity) {
+            return swipedFastEnough();
+        }
+
+        @Override
+        public float getMinDismissVelocity() {
+            return getEscapeVelocity();
+        }
+
+        public void onMenuShown(View animView) {
+            onDragCancelled(animView);
+
+            // If we're on the lockscreen we want to false this.
+            if (isAntiFalsingNeeded()) {
+                mHandler.removeCallbacks(mFalsingCheck);
+                mHandler.postDelayed(mFalsingCheck, COVER_MENU_DELAY);
+            }
+        }
+
+        public void closeControlsIfOutsideTouch(MotionEvent ev) {
+            NotificationGuts guts = mStatusBar.getExposedGuts();
+            View view = null;
+            if (guts != null && !guts.getGutsContent().isLeavebehind()) {
+                // Only close visible guts if they're not a leavebehind.
+                view = guts;
+            } else if (mCurrMenuRow != null && mCurrMenuRow.isMenuVisible()
+                    && mTranslatingParentView != null) {
+                // Checking menu
+                view = mTranslatingParentView;
+            }
+            if (view != null && !isTouchInView(ev, view)) {
+                // Touch was outside visible guts / menu notification, close what's visible
+                mStatusBar.closeAndSaveGuts(false /* removeLeavebehind */, false /* force */,
+                        true /* removeControls */, -1 /* x */, -1 /* y */, false /* resetMenu */);
+                resetExposedMenuView(true /* animate */, true /* force */);
+            }
+        }
+
+        public void resetExposedMenuView(boolean animate, boolean force) {
+            if (mMenuExposedView == null
+                    || (!force && mMenuExposedView == mTranslatingParentView)) {
+                // If no menu is showing or it's showing for this view we do nothing.
+                return;
+            }
+            final View prevMenuExposedView = mMenuExposedView;
+            if (animate) {
+                Animator anim = getViewTranslationAnimator(prevMenuExposedView,
+                        0 /* leftTarget */, null /* updateListener */);
+                if (anim != null) {
+                    anim.start();
+                }
+            } else if (mMenuExposedView instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) mMenuExposedView;
+                if (!row.isRemoved()) {
+                    row.resetTranslation();
+                }
+            }
+            mMenuExposedView = null;
+        }
+    }
+
+    private boolean isTouchInView(MotionEvent ev, View view) {
+        if (view == null) {
+            return false;
+        }
+        final int height = (view instanceof ExpandableView)
+                ? ((ExpandableView) view).getActualHeight()
+                : view.getHeight();
+        final int rx = (int) ev.getRawX();
+        final int ry = (int) ev.getRawY();
+        view.getLocationOnScreen(mTempInt2);
+        final int x = mTempInt2[0];
+        final int y = mTempInt2[1];
+        Rect rect = new Rect(x, y, x + view.getWidth(), y + height);
+        boolean ret = rect.contains(rx, ry);
+        return ret;
+    }
+
+    private void updateContinuousShadowDrawing() {
+        boolean continuousShadowUpdate = mAnimationRunning
+                || !mAmbientState.getDraggedViews().isEmpty();
+        if (continuousShadowUpdate != mContinuousShadowUpdate) {
+            if (continuousShadowUpdate) {
+                getViewTreeObserver().addOnPreDrawListener(mShadowUpdater);
+            } else {
+                getViewTreeObserver().removeOnPreDrawListener(mShadowUpdater);
+            }
+            mContinuousShadowUpdate = continuousShadowUpdate;
+        }
+    }
+
+    public void resetExposedMenuView(boolean animate, boolean force) {
+        mSwipeHelper.resetExposedMenuView(animate, force);
+    }
+
+    public void closeControlsIfOutsideTouch(MotionEvent ev) {
+        mSwipeHelper.closeControlsIfOutsideTouch(ev);
+    }
+
+    static class AnimationEvent {
+
+        static AnimationFilter[] FILTERS = new AnimationFilter[] {
+
+                // ANIMATION_TYPE_ADD
+                new AnimationFilter()
+                        .animateShadowAlpha()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateZ()
+                        .hasDelays(),
+
+                // ANIMATION_TYPE_REMOVE
+                new AnimationFilter()
+                        .animateShadowAlpha()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateZ()
+                        .hasDelays(),
+
+                // ANIMATION_TYPE_REMOVE_SWIPED_OUT
+                new AnimationFilter()
+                        .animateShadowAlpha()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateZ()
+                        .hasDelays(),
+
+                // ANIMATION_TYPE_TOP_PADDING_CHANGED
+                new AnimationFilter()
+                        .animateShadowAlpha()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateDimmed()
+                        .animateZ(),
+
+                // ANIMATION_TYPE_START_DRAG
+                new AnimationFilter()
+                        .animateShadowAlpha(),
+
+                // ANIMATION_TYPE_SNAP_BACK
+                new AnimationFilter()
+                        .animateShadowAlpha()
+                        .animateHeight(),
+
+                // ANIMATION_TYPE_ACTIVATED_CHILD
+                new AnimationFilter()
+                        .animateZ(),
+
+                // ANIMATION_TYPE_DIMMED
+                new AnimationFilter()
+                        .animateDimmed(),
+
+                // ANIMATION_TYPE_CHANGE_POSITION
+                new AnimationFilter()
+                        .animateAlpha() // maybe the children change positions
+                        .animateShadowAlpha()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateZ(),
+
+                // ANIMATION_TYPE_DARK
+                null, // Unused
+
+                // ANIMATION_TYPE_GO_TO_FULL_SHADE
+                new AnimationFilter()
+                        .animateShadowAlpha()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateDimmed()
+                        .animateZ()
+                        .hasDelays(),
+
+                // ANIMATION_TYPE_HIDE_SENSITIVE
+                new AnimationFilter()
+                        .animateHideSensitive(),
+
+                // ANIMATION_TYPE_VIEW_RESIZE
+                new AnimationFilter()
+                        .animateShadowAlpha()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateZ(),
+
+                // ANIMATION_TYPE_GROUP_EXPANSION_CHANGED
+                new AnimationFilter()
+                        .animateAlpha()
+                        .animateShadowAlpha()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateZ(),
+
+                // ANIMATION_TYPE_HEADS_UP_APPEAR
+                new AnimationFilter()
+                        .animateShadowAlpha()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateZ(),
+
+                // ANIMATION_TYPE_HEADS_UP_DISAPPEAR
+                new AnimationFilter()
+                        .animateShadowAlpha()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateZ(),
+
+                // ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
+                new AnimationFilter()
+                        .animateShadowAlpha()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateZ()
+                        .hasDelays(),
+
+                // ANIMATION_TYPE_HEADS_UP_OTHER
+                new AnimationFilter()
+                        .animateShadowAlpha()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateZ(),
+
+                // ANIMATION_TYPE_EVERYTHING
+                new AnimationFilter()
+                        .animateAlpha()
+                        .animateShadowAlpha()
+                        .animateDark()
+                        .animateDimmed()
+                        .animateHideSensitive()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateZ(),
+        };
+
+        static int[] LENGTHS = new int[] {
+
+                // ANIMATION_TYPE_ADD
+                StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
+
+                // ANIMATION_TYPE_REMOVE
+                StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
+
+                // ANIMATION_TYPE_REMOVE_SWIPED_OUT
+                StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+                // ANIMATION_TYPE_TOP_PADDING_CHANGED
+                StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+                // ANIMATION_TYPE_START_DRAG
+                StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+                // ANIMATION_TYPE_SNAP_BACK
+                StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+                // ANIMATION_TYPE_ACTIVATED_CHILD
+                StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
+
+                // ANIMATION_TYPE_DIMMED
+                StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
+
+                // ANIMATION_TYPE_CHANGE_POSITION
+                StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+                // ANIMATION_TYPE_DARK
+                StackStateAnimator.ANIMATION_DURATION_WAKEUP,
+
+                // ANIMATION_TYPE_GO_TO_FULL_SHADE
+                StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE,
+
+                // ANIMATION_TYPE_HIDE_SENSITIVE
+                StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+                // ANIMATION_TYPE_VIEW_RESIZE
+                StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+                // ANIMATION_TYPE_GROUP_EXPANSION_CHANGED
+                StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+                // ANIMATION_TYPE_HEADS_UP_APPEAR
+                StackStateAnimator.ANIMATION_DURATION_HEADS_UP_APPEAR,
+
+                // ANIMATION_TYPE_HEADS_UP_DISAPPEAR
+                StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR,
+
+                // ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
+                StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR,
+
+                // ANIMATION_TYPE_HEADS_UP_OTHER
+                StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+                // ANIMATION_TYPE_EVERYTHING
+                StackStateAnimator.ANIMATION_DURATION_STANDARD,
+        };
+
+        static final int ANIMATION_TYPE_ADD = 0;
+        static final int ANIMATION_TYPE_REMOVE = 1;
+        static final int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2;
+        static final int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3;
+        static final int ANIMATION_TYPE_START_DRAG = 4;
+        static final int ANIMATION_TYPE_SNAP_BACK = 5;
+        static final int ANIMATION_TYPE_ACTIVATED_CHILD = 6;
+        static final int ANIMATION_TYPE_DIMMED = 7;
+        static final int ANIMATION_TYPE_CHANGE_POSITION = 8;
+        static final int ANIMATION_TYPE_DARK = 9;
+        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_GROUP_EXPANSION_CHANGED = 13;
+        static final int ANIMATION_TYPE_HEADS_UP_APPEAR = 14;
+        static final int ANIMATION_TYPE_HEADS_UP_DISAPPEAR = 15;
+        static final int ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK = 16;
+        static final int ANIMATION_TYPE_HEADS_UP_OTHER = 17;
+        static final int ANIMATION_TYPE_EVERYTHING = 18;
+
+        static final int DARK_ANIMATION_ORIGIN_INDEX_ABOVE = -1;
+        static final int DARK_ANIMATION_ORIGIN_INDEX_BELOW = -2;
+
+        final long eventStartTime;
+        final View changingView;
+        final int animationType;
+        final AnimationFilter filter;
+        final long length;
+        View viewAfterChangingView;
+        int darkAnimationOriginIndex;
+        boolean headsUpFromBottom;
+
+        AnimationEvent(View view, int type) {
+            this(view, type, LENGTHS[type]);
+        }
+
+        AnimationEvent(View view, int type, AnimationFilter filter) {
+            this(view, type, LENGTHS[type], filter);
+        }
+
+        AnimationEvent(View view, int type, long length) {
+            this(view, type, length, FILTERS[type]);
+        }
+
+        AnimationEvent(View view, int type, long length, AnimationFilter filter) {
+            eventStartTime = AnimationUtils.currentAnimationTimeMillis();
+            changingView = view;
+            animationType = type;
+            this.length = length;
+            this.filter = filter;
+        }
+
+        /**
+         * Combines the length of several animation events into a single value.
+         *
+         * @param events The events of the lengths to combine.
+         * @return The combined length. Depending on the event types, this might be the maximum of
+         *         all events or the length of a specific event.
+         */
+        static long combineLength(ArrayList<AnimationEvent> events) {
+            long length = 0;
+            int size = events.size();
+            for (int i = 0; i < size; i++) {
+                AnimationEvent event = events.get(i);
+                length = Math.max(length, event.length);
+                if (event.animationType == ANIMATION_TYPE_GO_TO_FULL_SHADE) {
+                    return event.length;
+                }
+            }
+            return length;
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/stack/ScrollContainer.java b/com/android/systemui/statusbar/stack/ScrollContainer.java
new file mode 100644
index 0000000..b9d12ce
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/ScrollContainer.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 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.view.View;
+
+/**
+ * Interface for container layouts that scroll and listen for long presses. A child that
+ * wants to handle long press can use this to cancel the parents long press logic or request
+ * to be made visible by scrolling to it.
+ */
+public interface ScrollContainer {
+    /**
+     * Request that the view does not perform long press for the current touch.
+     */
+    void requestDisallowLongPress();
+
+    /**
+     * Request that the view is made visible by scrolling to it.
+     * Return true if it scrolls.
+     */
+    boolean scrollTo(View v);
+
+    /**
+     * Like {@link #scrollTo(View)}, but keeps the scroll locked until the user
+     * scrolls, or {@param v} loses focus or is detached.
+     */
+    void lockScrollTo(View v);
+
+    /**
+     * Request that the view does not dismiss for the current touch.
+     */
+    void requestDisallowDismiss();
+}
diff --git a/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
new file mode 100644
index 0000000..c060b08
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
@@ -0,0 +1,599 @@
+/*
+ * Copyright (C) 2014 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.content.res.Resources;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+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.NotificationShelf;
+import com.android.systemui.statusbar.notification.NotificationUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * The Algorithm of the {@link com.android.systemui.statusbar.stack
+ * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar
+ * .stack.StackScrollState}
+ */
+public class StackScrollAlgorithm {
+
+    private static final String LOG_TAG = "StackScrollAlgorithm";
+
+    private int mPaddingBetweenElements;
+    private int mIncreasedPaddingBetweenElements;
+    private int mCollapsedSize;
+
+    private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
+    private boolean mIsExpanded;
+    private boolean mClipNotificationScrollToTop;
+    private int mStatusBarHeight;
+
+    public StackScrollAlgorithm(Context context) {
+        initView(context);
+    }
+
+    public void initView(Context context) {
+        initConstants(context);
+    }
+
+    private void initConstants(Context context) {
+        Resources res = context.getResources();
+        mPaddingBetweenElements = res.getDimensionPixelSize(
+                R.dimen.notification_divider_height);
+        mIncreasedPaddingBetweenElements =
+                res.getDimensionPixelSize(R.dimen.notification_divider_height_increased);
+        mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height);
+        mStatusBarHeight = res.getDimensionPixelSize(R.dimen.status_bar_height);
+        mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop);
+    }
+
+    public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) {
+        // The state of the local variables are saved in an algorithmState to easily subdivide it
+        // into multiple phases.
+        StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
+
+        // First we reset the view states to their default values.
+        resultState.resetViewStates();
+
+        initAlgorithmState(resultState, algorithmState, ambientState);
+
+        updatePositionsForState(resultState, algorithmState, ambientState);
+
+        updateZValuesForState(resultState, algorithmState, ambientState);
+
+        updateHeadsUpStates(resultState, algorithmState, ambientState);
+
+        handleDraggedViews(ambientState, resultState, algorithmState);
+        updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState);
+        updateClipping(resultState, algorithmState, ambientState);
+        updateSpeedBumpState(resultState, algorithmState, ambientState);
+        updateShelfState(resultState, ambientState);
+        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,
+            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
+        int childCount = algorithmState.visibleChildren.size();
+        int belowSpeedBump = ambientState.getSpeedBumpIndex();
+        for (int i = 0; i < childCount; i++) {
+            View child = algorithmState.visibleChildren.get(i);
+            ExpandableViewState childViewState = resultState.getViewStateForView(child);
+
+            // The speed bump can also be gone, so equality needs to be taken when comparing
+            // indices.
+            childViewState.belowSpeedBump = i >= belowSpeedBump;
+        }
+
+    }
+    private void updateShelfState(StackScrollState resultState, AmbientState ambientState) {
+        NotificationShelf shelf = ambientState.getShelf();
+        shelf.updateState(resultState, ambientState);
+    }
+
+    private void updateClipping(StackScrollState resultState,
+            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
+        float drawStart = !ambientState.isOnKeyguard() ? ambientState.getTopPadding()
+                + ambientState.getStackTranslation() : 0;
+        float previousNotificationEnd = 0;
+        float previousNotificationStart = 0;
+        int childCount = algorithmState.visibleChildren.size();
+        for (int i = 0; i < childCount; i++) {
+            ExpandableView child = algorithmState.visibleChildren.get(i);
+            ExpandableViewState state = resultState.getViewStateForView(child);
+            if (!child.mustStayOnScreen()) {
+                previousNotificationEnd = Math.max(drawStart, previousNotificationEnd);
+                previousNotificationStart = Math.max(drawStart, previousNotificationStart);
+            }
+            float newYTranslation = state.yTranslation;
+            float newHeight = state.height;
+            float newNotificationEnd = newYTranslation + newHeight;
+            boolean isHeadsUp = (child instanceof ExpandableNotificationRow)
+                    && ((ExpandableNotificationRow) child).isPinned();
+            if (mClipNotificationScrollToTop
+                    && !state.inShelf && newYTranslation < previousNotificationEnd
+                    && (!isHeadsUp || ambientState.isShadeExpanded())) {
+                // The previous view is overlapping on top, clip!
+                float overlapAmount = previousNotificationEnd - newYTranslation;
+                state.clipTopAmount = (int) overlapAmount;
+            } else {
+                state.clipTopAmount = 0;
+            }
+
+            if (!child.isTransparent()) {
+                // Only update the previous values if we are not transparent,
+                // otherwise we would clip to a transparent view.
+                previousNotificationEnd = newNotificationEnd;
+                previousNotificationStart = newYTranslation;
+            }
+        }
+    }
+
+    public static boolean canChildBeDismissed(View v) {
+        if (!(v instanceof ExpandableNotificationRow)) {
+            return false;
+        }
+        ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+        if (row.areGutsExposed()) {
+            return false;
+        }
+        return row.canViewBeDismissed();
+    }
+
+    /**
+     * Updates the dimmed, activated and hiding sensitive states of the children.
+     */
+    private void updateDimmedActivatedHideSensitive(AmbientState ambientState,
+            StackScrollState resultState, StackScrollAlgorithmState algorithmState) {
+        boolean dimmed = ambientState.isDimmed();
+        boolean dark = ambientState.isDark();
+        boolean hideSensitive = ambientState.isHideSensitive();
+        View activatedChild = ambientState.getActivatedChild();
+        int childCount = algorithmState.visibleChildren.size();
+        for (int i = 0; i < childCount; i++) {
+            View child = algorithmState.visibleChildren.get(i);
+            ExpandableViewState childViewState = resultState.getViewStateForView(child);
+            childViewState.dimmed = dimmed;
+            childViewState.dark = dark;
+            childViewState.hideSensitive = hideSensitive;
+            boolean isActivatedChild = activatedChild == child;
+            if (dimmed && isActivatedChild) {
+                childViewState.zTranslation += 2.0f * ambientState.getZDistanceBetweenElements();
+            }
+        }
+    }
+
+    /**
+     * Handle the special state when views are being dragged
+     */
+    private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState,
+            StackScrollAlgorithmState algorithmState) {
+        ArrayList<View> draggedViews = ambientState.getDraggedViews();
+        for (View draggedView : draggedViews) {
+            int childIndex = algorithmState.visibleChildren.indexOf(draggedView);
+            if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) {
+                View nextChild = algorithmState.visibleChildren.get(childIndex + 1);
+                if (!draggedViews.contains(nextChild)) {
+                    // only if the view is not dragged itself we modify its state to be fully
+                    // visible
+                    ExpandableViewState viewState = resultState.getViewStateForView(
+                            nextChild);
+                    // The child below the dragged one must be fully visible
+                    if (ambientState.isShadeExpanded()) {
+                        viewState.shadowAlpha = 1;
+                        viewState.hidden = false;
+                    }
+                }
+
+                // Lets set the alpha to the one it currently has, as its currently being dragged
+                ExpandableViewState viewState = resultState.getViewStateForView(draggedView);
+                // The dragged child should keep the set alpha
+                viewState.alpha = draggedView.getAlpha();
+            }
+        }
+    }
+
+    /**
+     * Initialize the algorithm state like updating the visible children.
+     */
+    private void initAlgorithmState(StackScrollState resultState, StackScrollAlgorithmState state,
+            AmbientState ambientState) {
+        float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */);
+
+        int scrollY = ambientState.getScrollY();
+
+        // Due to the overScroller, the stackscroller can have negative scroll state. This is
+        // already accounted for by the top padding and doesn't need an additional adaption
+        scrollY = Math.max(0, scrollY);
+        state.scrollY = (int) (scrollY + bottomOverScroll);
+
+        //now init the visible children and update paddings
+        ViewGroup hostView = resultState.getHostView();
+        int childCount = hostView.getChildCount();
+        state.visibleChildren.clear();
+        state.visibleChildren.ensureCapacity(childCount);
+        state.paddingMap.clear();
+        int notGoneIndex = 0;
+        ExpandableView lastView = null;
+        int firstHiddenIndex = ambientState.isDark()
+                ? (ambientState.hasPulsingNotifications() ? 1 : 0)
+                : childCount;
+
+        // The goal here is to fill the padding map, by iterating over how much padding each child
+        // needs. The map is thereby reused, by first filling it with the padding amount and when
+        // iterating over it again, it's filled with the actual resolved value.
+
+        for (int i = 0; i < childCount; i++) {
+            ExpandableView v = (ExpandableView) hostView.getChildAt(i);
+            if (v.getVisibility() != View.GONE) {
+                if (v == ambientState.getShelf()) {
+                    continue;
+                }
+                if (i >= firstHiddenIndex) {
+                    // we need normal padding now, to be in sync with what the stack calculates
+                    lastView = null;
+                    ExpandableViewState viewState = resultState.getViewStateForView(v);
+                    viewState.hidden = true;
+                }
+                notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
+                float increasedPadding = v.getIncreasedPaddingAmount();
+                if (increasedPadding != 0.0f) {
+                    state.paddingMap.put(v, increasedPadding);
+                    if (lastView != null) {
+                        Float prevValue = state.paddingMap.get(lastView);
+                        float newValue = getPaddingForValue(increasedPadding);
+                        if (prevValue != null) {
+                            float prevPadding = getPaddingForValue(prevValue);
+                            if (increasedPadding > 0) {
+                                newValue = NotificationUtils.interpolate(
+                                        prevPadding,
+                                        newValue,
+                                        increasedPadding);
+                            } else if (prevValue > 0) {
+                                newValue = NotificationUtils.interpolate(
+                                        newValue,
+                                        prevPadding,
+                                        prevValue);
+                            }
+                        }
+                        state.paddingMap.put(lastView, newValue);
+                    }
+                } else if (lastView != null) {
+
+                    // Let's now resolve the value to an actual padding
+                    float newValue = getPaddingForValue(state.paddingMap.get(lastView));
+                    state.paddingMap.put(lastView, newValue);
+                }
+                if (v instanceof ExpandableNotificationRow) {
+                    ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+
+                    // handle the notgoneIndex for the children as well
+                    List<ExpandableNotificationRow> children =
+                            row.getNotificationChildren();
+                    if (row.isSummaryWithChildren() && children != null) {
+                        for (ExpandableNotificationRow childRow : children) {
+                            if (childRow.getVisibility() != View.GONE) {
+                                ExpandableViewState childState
+                                        = resultState.getViewStateForView(childRow);
+                                childState.notGoneIndex = notGoneIndex;
+                                notGoneIndex++;
+                            }
+                        }
+                    }
+                }
+                lastView = v;
+            }
+        }
+    }
+
+    private float getPaddingForValue(Float increasedPadding) {
+        if (increasedPadding == null) {
+            return mPaddingBetweenElements;
+        } else if (increasedPadding >= 0.0f) {
+            return NotificationUtils.interpolate(
+                    mPaddingBetweenElements,
+                    mIncreasedPaddingBetweenElements,
+                    increasedPadding);
+        } else {
+            return NotificationUtils.interpolate(
+                    0,
+                    mPaddingBetweenElements,
+                    1.0f + increasedPadding);
+        }
+    }
+
+    private int updateNotGoneIndex(StackScrollState resultState,
+            StackScrollAlgorithmState state, int notGoneIndex,
+            ExpandableView v) {
+        ExpandableViewState viewState = resultState.getViewStateForView(v);
+        viewState.notGoneIndex = notGoneIndex;
+        state.visibleChildren.add(v);
+        notGoneIndex++;
+        return notGoneIndex;
+    }
+
+    /**
+     * Determine the positions for the views. This is the main part of the algorithm.
+     *
+     * @param resultState The result state to update if a change to the properties of a child occurs
+     * @param algorithmState The state in which the current pass of the algorithm is currently in
+     * @param ambientState The current ambient state
+     */
+    private void updatePositionsForState(StackScrollState resultState,
+            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
+
+        // The y coordinate of the current child.
+        float currentYPosition = -algorithmState.scrollY;
+        int childCount = algorithmState.visibleChildren.size();
+        for (int i = 0; i < childCount; i++) {
+            currentYPosition = updateChild(i, resultState, algorithmState, ambientState,
+                    currentYPosition);
+        }
+    }
+
+    protected float updateChild(int i, StackScrollState resultState,
+            StackScrollAlgorithmState algorithmState, AmbientState ambientState,
+            float currentYPosition) {
+        ExpandableView child = algorithmState.visibleChildren.get(i);
+        ExpandableViewState childViewState = resultState.getViewStateForView(child);
+        childViewState.location = ExpandableViewState.LOCATION_UNKNOWN;
+        int paddingAfterChild = getPaddingAfterChild(algorithmState, child);
+        int childHeight = getMaxAllowedChildHeight(child);
+        childViewState.yTranslation = currentYPosition;
+        boolean isDismissView = child instanceof DismissView;
+        boolean isEmptyShadeView = child instanceof EmptyShadeView;
+
+        childViewState.location = ExpandableViewState.LOCATION_MAIN_AREA;
+        if (isDismissView) {
+            childViewState.yTranslation = Math.min(childViewState.yTranslation,
+                    ambientState.getInnerHeight() - childHeight);
+        } else if (isEmptyShadeView) {
+            childViewState.yTranslation = ambientState.getInnerHeight() - childHeight
+                    + ambientState.getStackTranslation() * 0.25f;
+        } else {
+            clampPositionToShelf(childViewState, ambientState);
+        }
+
+        currentYPosition = childViewState.yTranslation + childHeight + paddingAfterChild;
+        if (currentYPosition <= 0) {
+            childViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP;
+        }
+        if (childViewState.location == ExpandableViewState.LOCATION_UNKNOWN) {
+            Log.wtf(LOG_TAG, "Failed to assign location for child " + i);
+        }
+
+        childViewState.yTranslation += ambientState.getTopPadding()
+                + ambientState.getStackTranslation();
+        return currentYPosition;
+    }
+
+    protected int getPaddingAfterChild(StackScrollAlgorithmState algorithmState,
+            ExpandableView child) {
+        return algorithmState.getPaddingAfterChild(child);
+    }
+
+    private void updateHeadsUpStates(StackScrollState resultState,
+            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
+        int childCount = algorithmState.visibleChildren.size();
+        ExpandableNotificationRow topHeadsUpEntry = null;
+        for (int i = 0; i < childCount; i++) {
+            View child = algorithmState.visibleChildren.get(i);
+            if (!(child instanceof ExpandableNotificationRow)) {
+                break;
+            }
+            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+            if (!row.isHeadsUp()) {
+                break;
+            }
+            ExpandableViewState childState = resultState.getViewStateForView(row);
+            if (topHeadsUpEntry == null) {
+                topHeadsUpEntry = row;
+                childState.location = ExpandableViewState.LOCATION_FIRST_HUN;
+            }
+            boolean isTopEntry = topHeadsUpEntry == row;
+            float unmodifiedEndLocation = childState.yTranslation + childState.height;
+            if (mIsExpanded) {
+                // Ensure that the heads up is always visible even when scrolled off
+                clampHunToTop(ambientState, row, childState);
+                if (i == 0 && ambientState.isAboveShelf(row)) {
+                    // the first hun can't get off screen.
+                    clampHunToMaxTranslation(ambientState, row, childState);
+                    childState.hidden = false;
+                }
+            }
+            if (row.isPinned()) {
+                childState.yTranslation = Math.max(childState.yTranslation, 0);
+                childState.height = Math.max(row.getIntrinsicHeight(), childState.height);
+                childState.hidden = false;
+                ExpandableViewState topState = resultState.getViewStateForView(topHeadsUpEntry);
+                if (!isTopEntry && (!mIsExpanded
+                        || unmodifiedEndLocation < topState.yTranslation + topState.height)) {
+                    // Ensure that a headsUp doesn't vertically extend further than the heads-up at
+                    // the top most z-position
+                    childState.height = row.getIntrinsicHeight();
+                    childState.yTranslation = topState.yTranslation + topState.height
+                            - childState.height;
+                }
+            }
+            if (row.isHeadsUpAnimatingAway()) {
+                childState.hidden = false;
+            }
+        }
+    }
+
+    private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row,
+            ExpandableViewState childState) {
+        float newTranslation = Math.max(ambientState.getTopPadding()
+                + ambientState.getStackTranslation(), childState.yTranslation);
+        childState.height = (int) Math.max(childState.height - (newTranslation
+                - childState.yTranslation), row.getCollapsedHeight());
+        childState.yTranslation = newTranslation;
+    }
+
+    private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
+            ExpandableViewState childState) {
+        float newTranslation;
+        float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation();
+        float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
+                + ambientState.getStackTranslation();
+        maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition);
+        float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight();
+        newTranslation = Math.min(childState.yTranslation, bottomPosition);
+        childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation
+                - newTranslation);
+        childState.yTranslation = newTranslation;
+    }
+
+    /**
+     * Clamp the height of the child down such that its end is at most on the beginning of
+     * the shelf.
+     *
+     * @param childViewState the view state of the child
+     * @param ambientState the ambient state
+     */
+    private void clampPositionToShelf(ExpandableViewState childViewState,
+            AmbientState ambientState) {
+        int shelfStart = ambientState.getInnerHeight()
+                - ambientState.getShelf().getIntrinsicHeight();
+        childViewState.yTranslation = Math.min(childViewState.yTranslation, shelfStart);
+        if (childViewState.yTranslation >= shelfStart) {
+            childViewState.hidden = true;
+            childViewState.inShelf = true;
+        }
+        if (!ambientState.isShadeExpanded()) {
+            childViewState.height = (int) (mStatusBarHeight - childViewState.yTranslation);
+        }
+    }
+
+    protected int getMaxAllowedChildHeight(View child) {
+        if (child instanceof ExpandableView) {
+            ExpandableView expandableView = (ExpandableView) child;
+            return expandableView.getIntrinsicHeight();
+        }
+        return child == null? mCollapsedSize : child.getHeight();
+    }
+
+    /**
+     * Calculate the Z positions for all children based on the number of items in both stacks and
+     * save it in the resultState
+     *  @param resultState The result state to update the zTranslation values
+     * @param algorithmState The state in which the current pass of the algorithm is currently in
+     * @param ambientState The ambient state of the algorithm
+     */
+    private void updateZValuesForState(StackScrollState resultState,
+            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
+        int childCount = algorithmState.visibleChildren.size();
+        float childrenOnTop = 0.0f;
+        for (int i = childCount - 1; i >= 0; i--) {
+            childrenOnTop = updateChildZValue(i, childrenOnTop,
+                    resultState, algorithmState, ambientState);
+        }
+    }
+
+    protected float updateChildZValue(int i, float childrenOnTop,
+            StackScrollState resultState, StackScrollAlgorithmState algorithmState,
+            AmbientState ambientState) {
+        ExpandableView child = algorithmState.visibleChildren.get(i);
+        ExpandableViewState childViewState = resultState.getViewStateForView(child);
+        int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements();
+        float baseZ = ambientState.getBaseZHeight();
+        if (child.mustStayOnScreen() && !ambientState.isDozingAndNotPulsing(child)
+                && childViewState.yTranslation < ambientState.getTopPadding()
+                + ambientState.getStackTranslation()) {
+            if (childrenOnTop != 0.0f) {
+                childrenOnTop++;
+            } else {
+                float overlap = ambientState.getTopPadding()
+                        + ambientState.getStackTranslation() - childViewState.yTranslation;
+                childrenOnTop += Math.min(1.0f, overlap / childViewState.height);
+            }
+            childViewState.zTranslation = baseZ
+                    + childrenOnTop * zDistanceBetweenElements;
+        } else if (i == 0 && ambientState.isAboveShelf(child)) {
+            // In case this is a new view that has never been measured before, we don't want to
+            // elevate if we are currently expanded more then the notification
+            int shelfHeight = ambientState.getShelf().getIntrinsicHeight();
+            float shelfStart = ambientState.getInnerHeight()
+                    - shelfHeight + ambientState.getTopPadding()
+                    + ambientState.getStackTranslation();
+            float notificationEnd = childViewState.yTranslation + child.getPinnedHeadsUpHeight()
+                    + mPaddingBetweenElements;
+            if (shelfStart > notificationEnd) {
+                childViewState.zTranslation = baseZ;
+            } else {
+                float factor = (notificationEnd - shelfStart) / shelfHeight;
+                factor = Math.min(factor, 1.0f);
+                childViewState.zTranslation = baseZ + factor * zDistanceBetweenElements;
+            }
+        } else {
+            childViewState.zTranslation = baseZ;
+        }
+        return childrenOnTop;
+    }
+
+    public void setIsExpanded(boolean isExpanded) {
+        this.mIsExpanded = isExpanded;
+    }
+
+    public class StackScrollAlgorithmState {
+
+        /**
+         * The scroll position of the algorithm
+         */
+        public int scrollY;
+
+        /**
+         * The children from the host view which are not gone.
+         */
+        public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>();
+
+        /**
+         * The padding after each child measured in pixels.
+         */
+        public final HashMap<ExpandableView, Float> paddingMap = new HashMap<>();
+
+        public int getPaddingAfterChild(ExpandableView child) {
+            Float padding = paddingMap.get(child);
+            if (padding == null) {
+                // Should only happen for the last view
+                return mPaddingBetweenElements;
+            }
+            return (int) padding.floatValue();
+        }
+    }
+
+}
diff --git a/com/android/systemui/statusbar/stack/StackScrollState.java b/com/android/systemui/statusbar/stack/StackScrollState.java
new file mode 100644
index 0000000..e3c746b
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/StackScrollState.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2014 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.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
+
+import java.util.List;
+import java.util.WeakHashMap;
+
+/**
+ * A state of a {@link com.android.systemui.statusbar.stack.NotificationStackScrollLayout} which
+ * can be applied to a viewGroup.
+ */
+public class StackScrollState {
+
+    private static final String CHILD_NOT_FOUND_TAG = "StackScrollStateNoSuchChild";
+
+    private final ViewGroup mHostView;
+    private WeakHashMap<ExpandableView, ExpandableViewState> mStateMap;
+
+    public StackScrollState(ViewGroup hostView) {
+        mHostView = hostView;
+        mStateMap = new WeakHashMap<>();
+    }
+
+    public ViewGroup getHostView() {
+        return mHostView;
+    }
+
+    public void resetViewStates() {
+        int numChildren = mHostView.getChildCount();
+        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.isSummaryWithChildren() && children != null) {
+                    for (ExpandableNotificationRow childRow : children) {
+                        resetViewState(childRow);
+                    }
+                }
+            }
+        }
+    }
+
+    private void resetViewState(ExpandableView view) {
+        ExpandableViewState viewState = mStateMap.get(view);
+        if (viewState == null) {
+            viewState = view.createNewViewState(this);
+            mStateMap.put(view, viewState);
+        }
+        // initialize with the default values of the view
+        viewState.height = view.getIntrinsicHeight();
+        viewState.gone = view.getVisibility() == View.GONE;
+        viewState.alpha = 1f;
+        viewState.shadowAlpha = 1f;
+        viewState.notGoneIndex = -1;
+        viewState.xTranslation = view.getTranslationX();
+        viewState.hidden = false;
+        viewState.scaleX = view.getScaleX();
+        viewState.scaleY = view.getScaleY();
+        viewState.inShelf = false;
+    }
+
+    public ExpandableViewState getViewStateForView(View requestedView) {
+        return mStateMap.get(requestedView);
+    }
+
+    public void removeViewStateForView(View child) {
+        mStateMap.remove(child);
+    }
+
+    /**
+     * Apply the properties saved in {@link #mStateMap} to the children of the {@link #mHostView}.
+     * The properties are only applied if they effectively changed.
+     */
+    public void apply() {
+        int numChildren = mHostView.getChildCount();
+        for (int i = 0; i < numChildren; i++) {
+            ExpandableView child = (ExpandableView) mHostView.getChildAt(i);
+            ExpandableViewState state = mStateMap.get(child);
+            if (state == null) {
+                Log.wtf(CHILD_NOT_FOUND_TAG, "No child state was found when applying this state " +
+                        "to the hostView");
+                continue;
+            }
+            if (state.gone) {
+                continue;
+            }
+            state.applyToView(child);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/stack/StackStateAnimator.java b/com/android/systemui/statusbar/stack/StackStateAnimator.java
new file mode 100644
index 0000000..f78a718
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/StackStateAnimator.java
@@ -0,0 +1,521 @@
+/*
+ * Copyright (C) 2014 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.util.Property;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.NotificationShelf;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Stack;
+
+/**
+ * An stack state animator which handles animations to new StackScrollStates
+ */
+public class StackStateAnimator {
+
+    public static final int ANIMATION_DURATION_STANDARD = 360;
+    public static final int ANIMATION_DURATION_WAKEUP = 200;
+    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_DIMMED_ACTIVATED = 220;
+    public static final int ANIMATION_DURATION_CLOSE_REMOTE_INPUT = 150;
+    public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 650;
+    public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 230;
+    public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
+    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 DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
+    public static final int ANIMATION_DELAY_HEADS_UP = 120;
+
+    private final int mGoToFullShadeAppearingTranslation;
+    private final ExpandableViewState mTmpState = new ExpandableViewState();
+    private final AnimationProperties mAnimationProperties;
+    public NotificationStackScrollLayout mHostLayout;
+    private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents =
+            new ArrayList<>();
+    private ArrayList<View> mNewAddChildren = new ArrayList<>();
+    private HashSet<View> mHeadsUpAppearChildren = new HashSet<>();
+    private HashSet<View> mHeadsUpDisappearChildren = new HashSet<>();
+    private HashSet<Animator> mAnimatorSet = new HashSet<>();
+    private Stack<AnimatorListenerAdapter> mAnimationListenerPool = new Stack<>();
+    private AnimationFilter mAnimationFilter = new AnimationFilter();
+    private long mCurrentLength;
+    private long mCurrentAdditionalDelay;
+
+    /** The current index for the last child which was not added in this event set. */
+    private int mCurrentLastNotAddedIndex;
+    private ValueAnimator mTopOverScrollAnimator;
+    private ValueAnimator mBottomOverScrollAnimator;
+    private int mHeadsUpAppearHeightBottom;
+    private boolean mShadeExpanded;
+    private ArrayList<View> mChildrenToClearFromOverlay = new ArrayList<>();
+    private NotificationShelf mShelf;
+
+    public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
+        mHostLayout = hostLayout;
+        mGoToFullShadeAppearingTranslation =
+                hostLayout.getContext().getResources().getDimensionPixelSize(
+                        R.dimen.go_to_full_shade_appearing_translation);
+        mAnimationProperties = new AnimationProperties() {
+            @Override
+            public AnimationFilter getAnimationFilter() {
+                return mAnimationFilter;
+            }
+
+            @Override
+            public AnimatorListenerAdapter getAnimationFinishListener() {
+                return getGlobalAnimationFinishedListener();
+            }
+
+            @Override
+            public boolean wasAdded(View view) {
+                return mNewAddChildren.contains(view);
+            }
+
+            @Override
+            public Interpolator getCustomInterpolator(View child, Property property) {
+                if (mHeadsUpAppearChildren.contains(child) && View.TRANSLATION_Y.equals(property)) {
+                    return Interpolators.HEADS_UP_APPEAR;
+                }
+                return null;
+            }
+        };
+    }
+
+    public boolean isRunning() {
+        return !mAnimatorSet.isEmpty();
+    }
+
+    public void startAnimationForEvents(
+            ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents,
+            StackScrollState finalState, long additionalDelay) {
+
+        processAnimationEvents(mAnimationEvents, finalState);
+
+        int childCount = mHostLayout.getChildCount();
+        mAnimationFilter.applyCombination(mNewEvents);
+        mCurrentAdditionalDelay = additionalDelay;
+        mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents);
+        mCurrentLastNotAddedIndex = findLastNotAddedIndex(finalState);
+        for (int i = 0; i < childCount; i++) {
+            final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
+
+            ExpandableViewState viewState = finalState.getViewStateForView(child);
+            if (viewState == null || child.getVisibility() == View.GONE
+                    || applyWithoutAnimation(child, viewState, finalState)) {
+                continue;
+            }
+
+            initAnimationProperties(finalState, child, viewState);
+            viewState.animateTo(child, mAnimationProperties);
+        }
+        if (!isRunning()) {
+            // no child has preformed any animation, lets finish
+            onAnimationFinished();
+        }
+        mHeadsUpAppearChildren.clear();
+        mHeadsUpDisappearChildren.clear();
+        mNewEvents.clear();
+        mNewAddChildren.clear();
+    }
+
+    private void initAnimationProperties(StackScrollState finalState, ExpandableView child,
+            ExpandableViewState viewState) {
+        boolean wasAdded = mAnimationProperties.wasAdded(child);
+        mAnimationProperties.duration = mCurrentLength;
+        adaptDurationWhenGoingToFullShade(child, viewState, wasAdded);
+        mAnimationProperties.delay = 0;
+        if (wasAdded || mAnimationFilter.hasDelays
+                        && (viewState.yTranslation != child.getTranslationY()
+                        || viewState.zTranslation != child.getTranslationZ()
+                        || viewState.alpha != child.getAlpha()
+                        || viewState.height != child.getActualHeight()
+                        || viewState.clipTopAmount != child.getClipTopAmount()
+                        || viewState.dark != child.isDark()
+                        || viewState.shadowAlpha != child.getShadowAlpha())) {
+            mAnimationProperties.delay = mCurrentAdditionalDelay
+                    + calculateChildAnimationDelay(viewState, finalState);
+        }
+    }
+
+    private void adaptDurationWhenGoingToFullShade(ExpandableView child,
+            ExpandableViewState viewState, boolean wasAdded) {
+        if (wasAdded && mAnimationFilter.hasGoToFullShadeEvent) {
+            child.setTranslationY(child.getTranslationY() + mGoToFullShadeAppearingTranslation);
+            float longerDurationFactor = viewState.notGoneIndex - mCurrentLastNotAddedIndex;
+            longerDurationFactor = (float) Math.pow(longerDurationFactor, 0.7f);
+            mAnimationProperties.duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50 +
+                    (long) (100 * longerDurationFactor);
+        }
+    }
+
+    /**
+     * Determines if a view should not perform an animation and applies it directly.
+     *
+     * @return true if no animation should be performed
+     */
+    private boolean applyWithoutAnimation(ExpandableView child, ExpandableViewState viewState,
+            StackScrollState finalState) {
+        if (mShadeExpanded) {
+            return false;
+        }
+        if (ViewState.isAnimatingY(child)) {
+            // A Y translation animation is running
+            return false;
+        }
+        if (mHeadsUpDisappearChildren.contains(child) || mHeadsUpAppearChildren.contains(child)) {
+            // This is a heads up animation
+            return false;
+        }
+        if (NotificationStackScrollLayout.isPinnedHeadsUp(child)) {
+            // This is another headsUp which might move. Let's animate!
+            return false;
+        }
+        viewState.applyToView(child);
+        return true;
+    }
+
+    private int findLastNotAddedIndex(StackScrollState finalState) {
+        int childCount = mHostLayout.getChildCount();
+        for (int i = childCount - 1; i >= 0; i--) {
+            final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
+
+            ExpandableViewState viewState = finalState.getViewStateForView(child);
+            if (viewState == null || child.getVisibility() == View.GONE) {
+                continue;
+            }
+            if (!mNewAddChildren.contains(child)) {
+                return viewState.notGoneIndex;
+            }
+        }
+        return -1;
+    }
+
+    private long calculateChildAnimationDelay(ExpandableViewState viewState,
+            StackScrollState finalState) {
+        if (mAnimationFilter.hasGoToFullShadeEvent) {
+            return calculateDelayGoToFullShade(viewState);
+        }
+        if (mAnimationFilter.hasHeadsUpDisappearClickEvent) {
+            return ANIMATION_DELAY_HEADS_UP;
+        }
+        long minDelay = 0;
+        for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) {
+            long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING;
+            switch (event.animationType) {
+                case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: {
+                    int ownIndex = viewState.notGoneIndex;
+                    int changingIndex = finalState
+                            .getViewStateForView(event.changingView).notGoneIndex;
+                    int difference = Math.abs(ownIndex - changingIndex);
+                    difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
+                            difference - 1));
+                    long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement;
+                    minDelay = Math.max(delay, minDelay);
+                    break;
+                }
+                case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT:
+                    delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL;
+                case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: {
+                    int ownIndex = viewState.notGoneIndex;
+                    boolean noNextView = event.viewAfterChangingView == null;
+                    View viewAfterChangingView = noNextView
+                            ? mHostLayout.getLastChildNotGone()
+                            : event.viewAfterChangingView;
+                    if (viewAfterChangingView == null) {
+                        // This can happen when the last view in the list is removed.
+                        // Since the shelf is still around and the only view, the code still goes
+                        // in here and tries to calculate the delay for it when case its properties
+                        // have changed.
+                        continue;
+                    }
+                    int nextIndex = finalState
+                            .getViewStateForView(viewAfterChangingView).notGoneIndex;
+                    if (ownIndex >= nextIndex) {
+                        // we only have the view afterwards
+                        ownIndex++;
+                    }
+                    int difference = Math.abs(ownIndex - nextIndex);
+                    difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
+                            difference - 1));
+                    long delay = difference * delayPerElement;
+                    minDelay = Math.max(delay, minDelay);
+                    break;
+                }
+                default:
+                    break;
+            }
+        }
+        return minDelay;
+    }
+
+    private long calculateDelayGoToFullShade(ExpandableViewState viewState) {
+        int shelfIndex = mShelf.getNotGoneIndex();
+        float index = viewState.notGoneIndex;
+        long result = 0;
+        if (index > shelfIndex) {
+            float diff = index - shelfIndex;
+            diff = (float) Math.pow(diff, 0.7f);
+            result += (long) (diff * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE * 0.25);
+            index = shelfIndex;
+        }
+        index = (float) Math.pow(index, 0.7f);
+        result += (long) (index * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE);
+        return result;
+    }
+
+    /**
+     * @return an adapter which ensures that onAnimationFinished is called once no animation is
+     *         running anymore
+     */
+    private AnimatorListenerAdapter getGlobalAnimationFinishedListener() {
+        if (!mAnimationListenerPool.empty()) {
+            return mAnimationListenerPool.pop();
+        }
+
+        // We need to create a new one, no reusable ones found
+        return new AnimatorListenerAdapter() {
+            private boolean mWasCancelled;
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mAnimatorSet.remove(animation);
+                if (mAnimatorSet.isEmpty() && !mWasCancelled) {
+                    onAnimationFinished();
+                }
+                mAnimationListenerPool.push(this);
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mWasCancelled = true;
+            }
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mWasCancelled = false;
+                mAnimatorSet.add(animation);
+            }
+        };
+    }
+
+    private void onAnimationFinished() {
+        mHostLayout.onChildAnimationFinished();
+        for (View v : mChildrenToClearFromOverlay) {
+            removeFromOverlay(v);
+        }
+        mChildrenToClearFromOverlay.clear();
+    }
+
+    /**
+     * Process the animationEvents for a new animation
+     *
+     * @param animationEvents the animation events for the animation to perform
+     * @param finalState the final state to animate to
+     */
+    private void processAnimationEvents(
+            ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents,
+            StackScrollState finalState) {
+        for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) {
+            final ExpandableView changingView = (ExpandableView) event.changingView;
+            if (event.animationType ==
+                    NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) {
+
+                // This item is added, initialize it's properties.
+                ExpandableViewState viewState = finalState
+                        .getViewStateForView(changingView);
+                if (viewState == null) {
+                    // The position for this child was never generated, let's continue.
+                    continue;
+                }
+                viewState.applyToView(changingView);
+                mNewAddChildren.add(changingView);
+
+            } else if (event.animationType ==
+                    NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) {
+                if (changingView.getVisibility() != View.VISIBLE) {
+                    removeFromOverlay(changingView);
+                    continue;
+                }
+
+                // Find the amount to translate up. This is needed in order to understand the
+                // direction of the remove animation (either downwards or upwards)
+                ExpandableViewState viewState = finalState
+                        .getViewStateForView(event.viewAfterChangingView);
+                int actualHeight = changingView.getActualHeight();
+                // upwards by default
+                float translationDirection = -1.0f;
+                if (viewState != null) {
+                    float ownPosition = changingView.getTranslationY();
+                    if (changingView instanceof ExpandableNotificationRow
+                            && event.viewAfterChangingView instanceof ExpandableNotificationRow) {
+                        ExpandableNotificationRow changingRow =
+                                (ExpandableNotificationRow) changingView;
+                        ExpandableNotificationRow nextRow =
+                                (ExpandableNotificationRow) event.viewAfterChangingView;
+                        if (changingRow.isRemoved()
+                                && changingRow.wasChildInGroupWhenRemoved()
+                                && !nextRow.isChildInGroup()) {
+                            // the next row isn't actually a child from a group! Let's
+                            // compare absolute positions!
+                            ownPosition = changingRow.getTranslationWhenRemoved();
+                        }
+                    }
+                    // there was a view after this one, Approximate the distance the next child
+                    // travelled
+                    translationDirection = ((viewState.yTranslation
+                            - (ownPosition + actualHeight / 2.0f)) * 2 /
+                            actualHeight);
+                    translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f);
+
+                }
+                changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR,
+                        translationDirection, new Runnable() {
+                    @Override
+                    public void run() {
+                        // remove the temporary overlay
+                        removeFromOverlay(changingView);
+                    }
+                });
+            } else if (event.animationType ==
+                NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) {
+                // A race condition can trigger the view to be added to the overlay even though
+                // it was fully swiped out. So let's remove it
+                mHostLayout.getOverlay().remove(changingView);
+                if (Math.abs(changingView.getTranslation()) == changingView.getWidth()
+                        && changingView.getTransientContainer() != null) {
+                    changingView.getTransientContainer().removeTransientView(changingView);
+                }
+            } else if (event.animationType == NotificationStackScrollLayout
+                    .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) event.changingView;
+                row.prepareExpansionChanged(finalState);
+            } else if (event.animationType == NotificationStackScrollLayout
+                    .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) {
+                // This item is added, initialize it's properties.
+                ExpandableViewState viewState = finalState.getViewStateForView(changingView);
+                mTmpState.copyFrom(viewState);
+                if (event.headsUpFromBottom) {
+                    mTmpState.yTranslation = mHeadsUpAppearHeightBottom;
+                } else {
+                    mTmpState.yTranslation = -mTmpState.height;
+                }
+                mHeadsUpAppearChildren.add(changingView);
+                mTmpState.applyToView(changingView);
+            } else if (event.animationType == NotificationStackScrollLayout
+                            .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR ||
+                    event.animationType == NotificationStackScrollLayout
+                            .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK) {
+                mHeadsUpDisappearChildren.add(changingView);
+                if (changingView.getParent() == null) {
+                    // This notification was actually removed, so we need to add it to the overlay
+                    mHostLayout.getOverlay().add(changingView);
+                    mTmpState.initFrom(changingView);
+                    mTmpState.yTranslation = -changingView.getActualHeight();
+                    // We temporarily enable Y animations, the real filter will be combined
+                    // afterwards anyway
+                    mAnimationFilter.animateY = true;
+                    mAnimationProperties.delay =
+                            event.animationType == NotificationStackScrollLayout
+                                    .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
+                            ? ANIMATION_DELAY_HEADS_UP
+                            : 0;
+                    mAnimationProperties.duration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR;
+                    mTmpState.animateTo(changingView, mAnimationProperties);
+                    mChildrenToClearFromOverlay.add(changingView);
+                }
+            }
+            mNewEvents.add(event);
+        }
+    }
+
+    public static void removeFromOverlay(View changingView) {
+        ViewGroup parent = (ViewGroup) changingView.getParent();
+        if (parent != null) {
+            parent.removeView(changingView);
+        }
+    }
+
+    public void animateOverScrollToAmount(float targetAmount, final boolean onTop,
+            final boolean isRubberbanded) {
+        final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
+        if (targetAmount == startOverScrollAmount) {
+            return;
+        }
+        cancelOverScrollAnimators(onTop);
+        ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount,
+                targetAmount);
+        overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD);
+        overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                float currentOverScroll = (float) animation.getAnimatedValue();
+                mHostLayout.setOverScrollAmount(
+                        currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */,
+                        isRubberbanded);
+            }
+        });
+        overScrollAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        overScrollAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (onTop) {
+                    mTopOverScrollAnimator = null;
+                } else {
+                    mBottomOverScrollAnimator = null;
+                }
+            }
+        });
+        overScrollAnimator.start();
+        if (onTop) {
+            mTopOverScrollAnimator = overScrollAnimator;
+        } else {
+            mBottomOverScrollAnimator = overScrollAnimator;
+        }
+    }
+
+    public void cancelOverScrollAnimators(boolean onTop) {
+        ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator;
+        if (currentAnimator != null) {
+            currentAnimator.cancel();
+        }
+    }
+
+    public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) {
+        mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom;
+    }
+
+    public void setShadeExpanded(boolean shadeExpanded) {
+        mShadeExpanded = shadeExpanded;
+    }
+
+    public void setShelf(NotificationShelf shelf) {
+        mShelf = shelf;
+    }
+}
diff --git a/com/android/systemui/statusbar/stack/ViewState.java b/com/android/systemui/statusbar/stack/ViewState.java
new file mode 100644
index 0000000..27b730c
--- /dev/null
+++ b/com/android/systemui/statusbar/stack/ViewState.java
@@ -0,0 +1,698 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.app.Notification;
+import android.util.Property;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.notification.PropertyAnimator;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+
+/**
+ * A state of a view. This can be used to apply a set of view properties to a view with
+ * {@link com.android.systemui.statusbar.stack.StackScrollState} or start animations with
+ * {@link com.android.systemui.statusbar.stack.StackStateAnimator}.
+*/
+public class ViewState {
+
+    /**
+     * Some animation properties that can be used to update running animations but not creating
+     * any new ones.
+     */
+    protected static final AnimationProperties NO_NEW_ANIMATIONS = new AnimationProperties() {
+        AnimationFilter mAnimationFilter = new AnimationFilter();
+        @Override
+        public AnimationFilter getAnimationFilter() {
+            return mAnimationFilter;
+        }
+    };
+    private static final int TAG_ANIMATOR_TRANSLATION_X = R.id.translation_x_animator_tag;
+    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;
+    private static final int TAG_ANIMATOR_ALPHA = R.id.alpha_animator_tag;
+    private static final int TAG_END_TRANSLATION_X = R.id.translation_x_animator_end_value_tag;
+    private static final int TAG_END_TRANSLATION_Y = R.id.translation_y_animator_end_value_tag;
+    private static final int TAG_END_TRANSLATION_Z = R.id.translation_z_animator_end_value_tag;
+    private static final int TAG_END_ALPHA = R.id.alpha_animator_end_value_tag;
+    private static final int TAG_START_TRANSLATION_X = R.id.translation_x_animator_start_value_tag;
+    private static final int TAG_START_TRANSLATION_Y = R.id.translation_y_animator_start_value_tag;
+    private static final int TAG_START_TRANSLATION_Z = R.id.translation_z_animator_start_value_tag;
+    private static final int TAG_START_ALPHA = R.id.alpha_animator_start_value_tag;
+
+    private static final PropertyAnimator.AnimatableProperty SCALE_X_PROPERTY
+            = new PropertyAnimator.AnimatableProperty() {
+
+        @Override
+        public int getAnimationStartTag() {
+            return R.id.scale_x_animator_start_value_tag;
+        }
+
+        @Override
+        public int getAnimationEndTag() {
+            return R.id.scale_x_animator_end_value_tag;
+        }
+
+        @Override
+        public int getAnimatorTag() {
+            return R.id.scale_x_animator_tag;
+        }
+
+        @Override
+        public Property getProperty() {
+            return View.SCALE_X;
+        }
+    };
+
+    private static final PropertyAnimator.AnimatableProperty SCALE_Y_PROPERTY
+            = new PropertyAnimator.AnimatableProperty() {
+
+        @Override
+        public int getAnimationStartTag() {
+            return R.id.scale_y_animator_start_value_tag;
+        }
+
+        @Override
+        public int getAnimationEndTag() {
+            return R.id.scale_y_animator_end_value_tag;
+        }
+
+        @Override
+        public int getAnimatorTag() {
+            return R.id.scale_y_animator_tag;
+        }
+
+        @Override
+        public Property getProperty() {
+            return View.SCALE_Y;
+        }
+    };
+
+    public float alpha;
+    public float xTranslation;
+    public float yTranslation;
+    public float zTranslation;
+    public boolean gone;
+    public boolean hidden;
+    public float scaleX = 1.0f;
+    public float scaleY = 1.0f;
+
+    public void copyFrom(ViewState viewState) {
+        alpha = viewState.alpha;
+        xTranslation = viewState.xTranslation;
+        yTranslation = viewState.yTranslation;
+        zTranslation = viewState.zTranslation;
+        gone = viewState.gone;
+        hidden = viewState.hidden;
+        scaleX = viewState.scaleX;
+        scaleY = viewState.scaleY;
+    }
+
+    public void initFrom(View view) {
+        alpha = view.getAlpha();
+        xTranslation = view.getTranslationX();
+        yTranslation = view.getTranslationY();
+        zTranslation = view.getTranslationZ();
+        gone = view.getVisibility() == View.GONE;
+        hidden = view.getVisibility() == View.INVISIBLE;
+        scaleX = view.getScaleX();
+        scaleY = view.getScaleY();
+    }
+
+    /**
+     * Applies a {@link ViewState} to a normal view.
+     */
+    public void applyToView(View view) {
+        if (this.gone) {
+            // don't do anything with it
+            return;
+        }
+
+        // apply xTranslation
+        boolean animatingX = isAnimating(view, TAG_ANIMATOR_TRANSLATION_X);
+        if (animatingX) {
+            updateAnimationX(view);
+        } else if (view.getTranslationX() != this.xTranslation){
+            view.setTranslationX(this.xTranslation);
+        }
+
+        // apply yTranslation
+        boolean animatingY = isAnimating(view, TAG_ANIMATOR_TRANSLATION_Y);
+        if (animatingY) {
+            updateAnimationY(view);
+        } else if (view.getTranslationY() != this.yTranslation) {
+            view.setTranslationY(this.yTranslation);
+        }
+
+        // apply zTranslation
+        boolean animatingZ = isAnimating(view, TAG_ANIMATOR_TRANSLATION_Z);
+        if (animatingZ) {
+            updateAnimationZ(view);
+        } else if (view.getTranslationZ() != this.zTranslation) {
+            view.setTranslationZ(this.zTranslation);
+        }
+
+        // apply scaleX
+        boolean animatingScaleX = isAnimating(view, SCALE_X_PROPERTY);
+        if (animatingScaleX) {
+            updateAnimation(view, SCALE_X_PROPERTY, scaleX);
+        } else if (view.getScaleX() != scaleX) {
+            view.setScaleX(scaleX);
+        }
+
+        // apply scaleY
+        boolean animatingScaleY = isAnimating(view, SCALE_Y_PROPERTY);
+        if (animatingScaleY) {
+            updateAnimation(view, SCALE_Y_PROPERTY, scaleY);
+        } else if (view.getScaleY() != scaleY) {
+            view.setScaleY(scaleY);
+        }
+
+        int oldVisibility = view.getVisibility();
+        boolean becomesInvisible = this.alpha == 0.0f
+                || (this.hidden && (!isAnimating(view) || oldVisibility != View.VISIBLE));
+        boolean animatingAlpha = isAnimating(view, TAG_ANIMATOR_ALPHA);
+        if (animatingAlpha) {
+            updateAlphaAnimation(view);
+        } else if (view.getAlpha() != this.alpha) {
+            // apply layer type
+            boolean becomesFullyVisible = this.alpha == 1.0f;
+            boolean newLayerTypeIsHardware = !becomesInvisible && !becomesFullyVisible
+                    && view.hasOverlappingRendering();
+            int layerType = view.getLayerType();
+            int newLayerType = newLayerTypeIsHardware
+                    ? View.LAYER_TYPE_HARDWARE
+                    : View.LAYER_TYPE_NONE;
+            if (layerType != newLayerType) {
+                view.setLayerType(newLayerType, null);
+            }
+
+            // apply alpha
+            view.setAlpha(this.alpha);
+        }
+
+        // apply visibility
+        int newVisibility = becomesInvisible ? View.INVISIBLE : View.VISIBLE;
+        if (newVisibility != oldVisibility) {
+            if (!(view instanceof ExpandableView) || !((ExpandableView) view).willBeGone()) {
+                // We don't want views to change visibility when they are animating to GONE
+                view.setVisibility(newVisibility);
+            }
+        }
+    }
+
+    public boolean isAnimating(View view) {
+        if (isAnimating(view, TAG_ANIMATOR_TRANSLATION_X)) {
+            return true;
+        }
+        if (isAnimating(view, TAG_ANIMATOR_TRANSLATION_Y)) {
+            return true;
+        }
+        if (isAnimating(view, TAG_ANIMATOR_TRANSLATION_Z)) {
+            return true;
+        }
+        if (isAnimating(view, TAG_ANIMATOR_ALPHA)) {
+            return true;
+        }
+        if (isAnimating(view, SCALE_X_PROPERTY)) {
+            return true;
+        }
+        if (isAnimating(view, SCALE_Y_PROPERTY)) {
+            return true;
+        }
+        return false;
+    }
+
+    private static boolean isAnimating(View view, int tag) {
+        return getChildTag(view, tag) != null;
+    }
+
+    public static boolean isAnimating(View view, PropertyAnimator.AnimatableProperty property) {
+        return getChildTag(view, property.getAnimatorTag()) != null;
+    }
+
+    /**
+     * Start an animation to this viewstate
+     * @param child the view to animate
+     * @param animationProperties the properties of the animation
+     */
+    public void animateTo(View child, AnimationProperties animationProperties) {
+        boolean wasVisible = child.getVisibility() == View.VISIBLE;
+        final float alpha = this.alpha;
+        if (!wasVisible && (alpha != 0 || child.getAlpha() != 0)
+                && !this.gone && !this.hidden) {
+            child.setVisibility(View.VISIBLE);
+        }
+        float childAlpha = child.getAlpha();
+        boolean alphaChanging = this.alpha != childAlpha;
+        if (child instanceof ExpandableView) {
+            // We don't want views to change visibility when they are animating to GONE
+            alphaChanging &= !((ExpandableView) child).willBeGone();
+        }
+
+        // start translationX animation
+        if (child.getTranslationX() != this.xTranslation) {
+            startXTranslationAnimation(child, animationProperties);
+        } else {
+            abortAnimation(child, TAG_ANIMATOR_TRANSLATION_X);
+        }
+
+        // start translationY animation
+        if (child.getTranslationY() != this.yTranslation) {
+            startYTranslationAnimation(child, animationProperties);
+        } else {
+            abortAnimation(child, TAG_ANIMATOR_TRANSLATION_Y);
+        }
+
+        // start translationZ animation
+        if (child.getTranslationZ() != this.zTranslation) {
+            startZTranslationAnimation(child, animationProperties);
+        } else {
+            abortAnimation(child, TAG_ANIMATOR_TRANSLATION_Z);
+        }
+
+        // start scaleX animation
+        if (child.getScaleX() != scaleX) {
+            PropertyAnimator.startAnimation(child, SCALE_X_PROPERTY, scaleX, animationProperties);
+        } else {
+            abortAnimation(child, SCALE_X_PROPERTY.getAnimatorTag());
+        }
+
+        // start scaleX animation
+        if (child.getScaleY() != scaleY) {
+            PropertyAnimator.startAnimation(child, SCALE_Y_PROPERTY, scaleY, animationProperties);
+        } else {
+            abortAnimation(child, SCALE_Y_PROPERTY.getAnimatorTag());
+        }
+
+        // start alpha animation
+        if (alphaChanging) {
+            startAlphaAnimation(child, animationProperties);
+        }  else {
+            abortAnimation(child, TAG_ANIMATOR_ALPHA);
+        }
+    }
+
+    private void updateAlphaAnimation(View view) {
+        startAlphaAnimation(view, NO_NEW_ANIMATIONS);
+    }
+
+    private void startAlphaAnimation(final View child, AnimationProperties properties) {
+        Float previousStartValue = getChildTag(child,TAG_START_ALPHA);
+        Float previousEndValue = getChildTag(child,TAG_END_ALPHA);
+        final float newEndValue = this.alpha;
+        if (previousEndValue != null && previousEndValue == newEndValue) {
+            return;
+        }
+        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_ALPHA);
+        AnimationFilter filter = properties.getAnimationFilter();
+        if (!filter.animateAlpha) {
+            // just a local update was performed
+            if (previousAnimator != null) {
+                // we need to increase all animation keyframes of the previous animator by the
+                // relative change to the end value
+                PropertyValuesHolder[] values = previousAnimator.getValues();
+                float relativeDiff = newEndValue - previousEndValue;
+                float newStartValue = previousStartValue + relativeDiff;
+                values[0].setFloatValues(newStartValue, newEndValue);
+                child.setTag(TAG_START_ALPHA, newStartValue);
+                child.setTag(TAG_END_ALPHA, newEndValue);
+                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+                return;
+            } else {
+                // no new animation needed, let's just apply the value
+                child.setAlpha(newEndValue);
+                if (newEndValue == 0) {
+                    child.setVisibility(View.INVISIBLE);
+                }
+            }
+        }
+
+        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.ALPHA,
+                child.getAlpha(), newEndValue);
+        animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        // Handle layer type
+        child.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+        animator.addListener(new AnimatorListenerAdapter() {
+            public boolean mWasCancelled;
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                child.setLayerType(View.LAYER_TYPE_NONE, null);
+                if (newEndValue == 0 && !mWasCancelled) {
+                    child.setVisibility(View.INVISIBLE);
+                }
+                // remove the tag when the animation is finished
+                child.setTag(TAG_ANIMATOR_ALPHA, null);
+                child.setTag(TAG_START_ALPHA, null);
+                child.setTag(TAG_END_ALPHA, null);
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mWasCancelled = true;
+            }
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mWasCancelled = false;
+            }
+        });
+        long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
+        animator.setDuration(newDuration);
+        if (properties.delay > 0 && (previousAnimator == null
+                || previousAnimator.getAnimatedFraction() == 0)) {
+            animator.setStartDelay(properties.delay);
+        }
+        AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+        if (listener != null) {
+            animator.addListener(listener);
+        }
+
+        startAnimator(animator, listener);
+        child.setTag(TAG_ANIMATOR_ALPHA, animator);
+        child.setTag(TAG_START_ALPHA, child.getAlpha());
+        child.setTag(TAG_END_ALPHA, newEndValue);
+    }
+
+    private void updateAnimationZ(View view) {
+        startZTranslationAnimation(view, NO_NEW_ANIMATIONS);
+    }
+
+    private void updateAnimation(View view, PropertyAnimator.AnimatableProperty property,
+            float endValue) {
+        PropertyAnimator.startAnimation(view, property, endValue, NO_NEW_ANIMATIONS);
+    }
+
+    private void startZTranslationAnimation(final View child, AnimationProperties properties) {
+        Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Z);
+        Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Z);
+        float newEndValue = this.zTranslation;
+        if (previousEndValue != null && previousEndValue == newEndValue) {
+            return;
+        }
+        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Z);
+        AnimationFilter filter = properties.getAnimationFilter();
+        if (!filter.animateZ) {
+            // just a local update was performed
+            if (previousAnimator != null) {
+                // we need to increase all animation keyframes of the previous animator by the
+                // relative change to the end value
+                PropertyValuesHolder[] values = previousAnimator.getValues();
+                float relativeDiff = newEndValue - previousEndValue;
+                float newStartValue = previousStartValue + relativeDiff;
+                values[0].setFloatValues(newStartValue, newEndValue);
+                child.setTag(TAG_START_TRANSLATION_Z, newStartValue);
+                child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
+                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+                return;
+            } else {
+                // no new animation needed, let's just apply the value
+                child.setTranslationZ(newEndValue);
+            }
+        }
+
+        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Z,
+                child.getTranslationZ(), newEndValue);
+        animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
+        animator.setDuration(newDuration);
+        if (properties.delay > 0 && (previousAnimator == null
+                || previousAnimator.getAnimatedFraction() == 0)) {
+            animator.setStartDelay(properties.delay);
+        }
+        AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+        if (listener != null) {
+            animator.addListener(listener);
+        }
+        // remove the tag when the animation is finished
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                child.setTag(TAG_ANIMATOR_TRANSLATION_Z, null);
+                child.setTag(TAG_START_TRANSLATION_Z, null);
+                child.setTag(TAG_END_TRANSLATION_Z, null);
+            }
+        });
+        startAnimator(animator, listener);
+        child.setTag(TAG_ANIMATOR_TRANSLATION_Z, animator);
+        child.setTag(TAG_START_TRANSLATION_Z, child.getTranslationZ());
+        child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
+    }
+
+    private void updateAnimationX(View view) {
+        startXTranslationAnimation(view, NO_NEW_ANIMATIONS);
+    }
+
+    private void startXTranslationAnimation(final View child, AnimationProperties properties) {
+        Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_X);
+        Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_X);
+        float newEndValue = this.xTranslation;
+        if (previousEndValue != null && previousEndValue == newEndValue) {
+            return;
+        }
+        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_X);
+        AnimationFilter filter = properties.getAnimationFilter();
+        if (!filter.animateX) {
+            // just a local update was performed
+            if (previousAnimator != null) {
+                // we need to increase all animation keyframes of the previous animator by the
+                // relative change to the end value
+                PropertyValuesHolder[] values = previousAnimator.getValues();
+                float relativeDiff = newEndValue - previousEndValue;
+                float newStartValue = previousStartValue + relativeDiff;
+                values[0].setFloatValues(newStartValue, newEndValue);
+                child.setTag(TAG_START_TRANSLATION_X, newStartValue);
+                child.setTag(TAG_END_TRANSLATION_X, newEndValue);
+                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+                return;
+            } else {
+                // no new animation needed, let's just apply the value
+                child.setTranslationX(newEndValue);
+                return;
+            }
+        }
+
+        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_X,
+                child.getTranslationX(), newEndValue);
+        Interpolator customInterpolator = properties.getCustomInterpolator(child,
+                View.TRANSLATION_X);
+        Interpolator interpolator =  customInterpolator != null ? customInterpolator
+                : Interpolators.FAST_OUT_SLOW_IN;
+        animator.setInterpolator(interpolator);
+        long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
+        animator.setDuration(newDuration);
+        if (properties.delay > 0 && (previousAnimator == null
+                || previousAnimator.getAnimatedFraction() == 0)) {
+            animator.setStartDelay(properties.delay);
+        }
+        AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+        if (listener != null) {
+            animator.addListener(listener);
+        }
+        // remove the tag when the animation is finished
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                child.setTag(TAG_ANIMATOR_TRANSLATION_X, null);
+                child.setTag(TAG_START_TRANSLATION_X, null);
+                child.setTag(TAG_END_TRANSLATION_X, null);
+            }
+        });
+        startAnimator(animator, listener);
+        child.setTag(TAG_ANIMATOR_TRANSLATION_X, animator);
+        child.setTag(TAG_START_TRANSLATION_X, child.getTranslationX());
+        child.setTag(TAG_END_TRANSLATION_X, newEndValue);
+    }
+
+    private void updateAnimationY(View view) {
+        startYTranslationAnimation(view, NO_NEW_ANIMATIONS);
+    }
+
+    private void startYTranslationAnimation(final View child, AnimationProperties properties) {
+        Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Y);
+        Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Y);
+        float newEndValue = this.yTranslation;
+        if (previousEndValue != null && previousEndValue == newEndValue) {
+            return;
+        }
+        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y);
+        AnimationFilter filter = properties.getAnimationFilter();
+        if (!filter.shouldAnimateY(child)) {
+            // just a local update was performed
+            if (previousAnimator != null) {
+                // we need to increase all animation keyframes of the previous animator by the
+                // relative change to the end value
+                PropertyValuesHolder[] values = previousAnimator.getValues();
+                float relativeDiff = newEndValue - previousEndValue;
+                float newStartValue = previousStartValue + relativeDiff;
+                values[0].setFloatValues(newStartValue, newEndValue);
+                child.setTag(TAG_START_TRANSLATION_Y, newStartValue);
+                child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
+                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+                return;
+            } else {
+                // no new animation needed, let's just apply the value
+                child.setTranslationY(newEndValue);
+                return;
+            }
+        }
+
+        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Y,
+                child.getTranslationY(), newEndValue);
+        Interpolator customInterpolator = properties.getCustomInterpolator(child,
+                View.TRANSLATION_Y);
+        Interpolator interpolator =  customInterpolator != null ? customInterpolator
+                : Interpolators.FAST_OUT_SLOW_IN;
+        animator.setInterpolator(interpolator);
+        long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
+        animator.setDuration(newDuration);
+        if (properties.delay > 0 && (previousAnimator == null
+                || previousAnimator.getAnimatedFraction() == 0)) {
+            animator.setStartDelay(properties.delay);
+        }
+        AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+        if (listener != null) {
+            animator.addListener(listener);
+        }
+        // remove the tag when the animation is finished
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                HeadsUpManager.setIsClickedNotification(child, false);
+                child.setTag(TAG_ANIMATOR_TRANSLATION_Y, null);
+                child.setTag(TAG_START_TRANSLATION_Y, null);
+                child.setTag(TAG_END_TRANSLATION_Y, null);
+                onYTranslationAnimationFinished(child);
+            }
+        });
+        startAnimator(animator, listener);
+        child.setTag(TAG_ANIMATOR_TRANSLATION_Y, animator);
+        child.setTag(TAG_START_TRANSLATION_Y, child.getTranslationY());
+        child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
+    }
+
+    protected void onYTranslationAnimationFinished(View view) {
+        if (hidden && !gone) {
+            view.setVisibility(View.INVISIBLE);
+        }
+    }
+
+    public static void startAnimator(Animator animator, AnimatorListenerAdapter listener) {
+        if (listener != null) {
+            // Even if there's a delay we'd want to notify it of the start immediately.
+            listener.onAnimationStart(animator);
+        }
+        animator.start();
+    }
+
+    public static <T> T getChildTag(View child, int tag) {
+        return (T) child.getTag(tag);
+    }
+
+    protected void abortAnimation(View child, int animatorTag) {
+        Animator previousAnimator = getChildTag(child, animatorTag);
+        if (previousAnimator != null) {
+            previousAnimator.cancel();
+        }
+    }
+
+    /**
+     * Cancel the previous animator and get the duration of the new animation.
+     *
+     * @param duration the new duration
+     * @param previousAnimator the animator which was running before
+     * @return the new duration
+     */
+    public static long cancelAnimatorAndGetNewDuration(long duration,
+            ValueAnimator previousAnimator) {
+        long newDuration = duration;
+        if (previousAnimator != null) {
+            // We take either the desired length of the new animation or the remaining time of
+            // the previous animator, whichever is longer.
+            newDuration = Math.max(previousAnimator.getDuration()
+                    - previousAnimator.getCurrentPlayTime(), newDuration);
+            previousAnimator.cancel();
+        }
+        return newDuration;
+    }
+
+    /**
+     * Get the end value of the yTranslation animation running on a view or the yTranslation
+     * if no animation is running.
+     */
+    public static float getFinalTranslationY(View view) {
+        if (view == null) {
+            return 0;
+        }
+        ValueAnimator yAnimator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_Y);
+        if (yAnimator == null) {
+            return view.getTranslationY();
+        } else {
+            return getChildTag(view, TAG_END_TRANSLATION_Y);
+        }
+    }
+
+    /**
+     * Get the end value of the zTranslation animation running on a view or the zTranslation
+     * if no animation is running.
+     */
+    public static float getFinalTranslationZ(View view) {
+        if (view == null) {
+            return 0;
+        }
+        ValueAnimator zAnimator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_Z);
+        if (zAnimator == null) {
+            return view.getTranslationZ();
+        } else {
+            return getChildTag(view, TAG_END_TRANSLATION_Z);
+        }
+    }
+
+    public static boolean isAnimatingY(View child) {
+        return getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y) != null;
+    }
+
+    public void cancelAnimations(View view) {
+        Animator animator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_X);
+        if (animator != null) {
+            animator.cancel();
+        }
+        animator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_Y);
+        if (animator != null) {
+            animator.cancel();
+        }
+        animator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_Z);
+        if (animator != null) {
+            animator.cancel();
+        }
+        animator = getChildTag(view, TAG_ANIMATOR_ALPHA);
+        if (animator != null) {
+            animator.cancel();
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/tv/TvStatusBar.java b/com/android/systemui/statusbar/tv/TvStatusBar.java
new file mode 100644
index 0000000..b5d92a5
--- /dev/null
+++ b/com/android/systemui/statusbar/tv/TvStatusBar.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2012 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.tv;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.SystemUI;
+import com.android.systemui.pip.tv.PipManager;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.CommandQueue.Callbacks;
+
+import java.util.ArrayList;
+
+/**
+ * Status bar implementation for "large screen" products that mostly present no on-screen nav
+ */
+
+public class TvStatusBar extends SystemUI implements Callbacks {
+
+    private IStatusBarService mBarService;
+
+    @Override
+    public void start() {
+        putComponent(TvStatusBar.class, this);
+        CommandQueue commandQueue = getComponent(CommandQueue.class);
+        commandQueue.addCallbacks(this);
+        int[] switches = new int[9];
+        ArrayList<IBinder> binders = new ArrayList<>();
+        ArrayList<String> iconSlots = new ArrayList<>();
+        ArrayList<StatusBarIcon> icons = new ArrayList<>();
+        Rect fullscreenStackBounds = new Rect();
+        Rect dockedStackBounds = new Rect();
+        mBarService = IStatusBarService.Stub.asInterface(
+                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+        try {
+            mBarService.registerStatusBar(commandQueue, iconSlots, icons, switches, binders,
+                    fullscreenStackBounds, dockedStackBounds);
+        } catch (RemoteException ex) {
+            // If the system process isn't there we're doomed anyway.
+        }
+    }
+
+}