AOD clock and notification animation

Better transitions for pulsing notifications and other keyguard
messages.

Change-Id: I3e3ec844834bf7f5d9ab341f308e78a002248a87
Fixes: 77601057
Fixes: 67641732
Test: visual
Test: atest packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewTest.java
Test: atest packages/SystemUI/tests/src/com/android/systemui/util/wakelock/KeepAwakeAnimationListenerTest.java
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
index e936c3b..5ce3339 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
@@ -34,6 +34,7 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.animation.Animation;
 import android.widget.Button;
 import android.widget.LinearLayout;
@@ -46,8 +47,9 @@
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
 import com.android.systemui.keyguard.KeyguardSliceProvider;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.tuner.TunerService;
-import com.android.systemui.util.wakelock.WakeLock;
+import com.android.systemui.util.wakelock.KeepAwakeAnimationListener;
 
 import java.util.HashMap;
 import java.util.List;
@@ -64,21 +66,29 @@
  * View visible under the clock on the lock screen and AoD.
  */
 public class KeyguardSliceView extends LinearLayout implements View.OnClickListener,
-        Observer<Slice>, TunerService.Tunable {
+        Observer<Slice>, TunerService.Tunable, ConfigurationController.ConfigurationListener {
 
     private static final String TAG = "KeyguardSliceView";
+    public static final int DEFAULT_ANIM_DURATION = 550;
+
     private final HashMap<View, PendingIntent> mClickActions;
     private Uri mKeyguardSliceUri;
-    private TextView mTitle;
-    private LinearLayout mRow;
+    @VisibleForTesting
+    TextView mTitle;
+    private Row mRow;
     private int mTextColor;
     private float mDarkAmount = 0;
 
     private LiveData<Slice> mLiveData;
     private int mIconSize;
-    private Consumer<Boolean> mListener;
+    /**
+     * Listener called whenever the view contents change.
+     * Boolean will be true when the change happens animated.
+     */
+    private Consumer<Boolean> mContentChangeListener;
     private boolean mHasHeader;
-    private boolean mHideContent;
+    private Slice mSlice;
+    private boolean mPulsing;
 
     public KeyguardSliceView(Context context) {
         this(context, null, 0);
@@ -95,6 +105,18 @@
         tunerService.addTunable(this, Settings.Secure.KEYGUARD_SLICE_URI);
 
         mClickActions = new HashMap<>();
+
+        LayoutTransition transition = new LayoutTransition();
+        transition.setStagger(LayoutTransition.CHANGE_APPEARING, DEFAULT_ANIM_DURATION / 2);
+        transition.setDuration(LayoutTransition.APPEARING, DEFAULT_ANIM_DURATION);
+        transition.setDuration(LayoutTransition.DISAPPEARING, DEFAULT_ANIM_DURATION / 2);
+        transition.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
+        transition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
+        transition.setInterpolator(LayoutTransition.APPEARING, Interpolators.FAST_OUT_SLOW_IN);
+        transition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT);
+        transition.setAnimateParentHierarchy(false);
+        transition.addTransitionListener(new SliceViewTransitionListener());
+        setLayoutTransition(transition);
     }
 
     @Override
@@ -112,6 +134,7 @@
 
         // Make sure we always have the most current slice
         mLiveData.observeForever(this);
+        Dependency.get(ConfigurationController.class).addCallback(this);
     }
 
     @Override
@@ -119,17 +142,29 @@
         super.onDetachedFromWindow();
 
         mLiveData.removeObserver(this);
+        Dependency.get(ConfigurationController.class).removeCallback(this);
     }
 
-    private void showSlice(Slice slice) {
+    private void showSlice() {
+        if (mPulsing) {
+            mTitle.setVisibility(GONE);
+            mRow.setVisibility(GONE);
+            mContentChangeListener.accept(getLayoutTransition() != null);
+            return;
+        }
 
-        ListContent lc = new ListContent(getContext(), slice);
+        if (mSlice == null) {
+            return;
+        }
+
+        ListContent lc = new ListContent(getContext(), mSlice);
         mHasHeader = lc.hasHeader();
         List<SliceItem> subItems = lc.getRowItems();
         if (!mHasHeader) {
             mTitle.setVisibility(GONE);
         } else {
             mTitle.setVisibility(VISIBLE);
+
             // If there's a header it'll be the first subitem
             RowContent header = new RowContent(getContext(), subItems.get(0),
                     true /* showStartItem */);
@@ -154,6 +189,7 @@
         final int subItemsCount = subItems.size();
         final int blendedColor = getTextColor();
         final int startIndex = mHasHeader ? 1 : 0; // First item is header; skip it
+        mRow.setVisibility(subItemsCount > 0 ? VISIBLE : GONE);
         for (int i = startIndex; i < subItemsCount; i++) {
             SliceItem item = subItems.get(i);
             RowContent rc = new RowContent(getContext(), item, true /* showStartItem */);
@@ -200,15 +236,20 @@
             }
         }
 
-        updateVisibility();
-        mListener.accept(mHasHeader);
+        if (mContentChangeListener != null) {
+            mContentChangeListener.accept(getLayoutTransition() != null);
+        }
     }
 
-    private void updateVisibility() {
-        final boolean hasContent = mHasHeader || mRow.getChildCount() > 0;
-        final int visibility = hasContent && !mHideContent ? VISIBLE : GONE;
-        if (visibility != getVisibility()) {
-            setVisibility(visibility);
+    public void setPulsing(boolean pulsing, boolean animate) {
+        mPulsing = pulsing;
+        LayoutTransition transition = getLayoutTransition();
+        if (!animate) {
+            setLayoutTransition(null);
+        }
+        showSlice();
+        if (!animate) {
+            setLayoutTransition(transition);
         }
     }
 
@@ -252,8 +293,9 @@
         return optimalString.toString();
     }
 
-    public void setDark(float darkAmount) {
+    public void setDarkAmount(float darkAmount) {
         mDarkAmount = darkAmount;
+        mRow.setDarkAmount(darkAmount);
         updateTextColors();
     }
 
@@ -281,12 +323,17 @@
         }
     }
 
-    public void setListener(Consumer<Boolean> listener) {
-        mListener = listener;
+    /**
+     * Listener that gets invoked every time the title or the row visibility changes.
+     * Parameter will be {@code true} whenever the change happens animated.
+     * @param contentChangeListener The listener.
+     */
+    public void setContentChangeListener(Consumer<Boolean> contentChangeListener) {
+        mContentChangeListener = contentChangeListener;
     }
 
     public boolean hasHeader() {
-        return mHasHeader;
+        return mTitle.getVisibility() == VISIBLE;
     }
 
     /**
@@ -295,7 +342,8 @@
      */
     @Override
     public void onChanged(Slice slice) {
-        showSlice(slice);
+        mSlice = slice;
+        showSlice();
     }
 
     @Override
@@ -333,15 +381,20 @@
         updateTextColors();
     }
 
-    public void setHideContent(boolean hideContent) {
-        mHideContent = hideContent;
-        updateVisibility();
+    @Override
+    public void onDensityOrFontScaleChanged() {
+        mIconSize = mContext.getResources().getDimensionPixelSize(R.dimen.widget_icon_size);
     }
 
     public static class Row extends LinearLayout {
 
-        private static final long ROW_ANIM_DURATION = 350;
-        private final WakeLock mAnimationWakeLock;
+        /**
+         * This view is visible in AOD, which means that the device will sleep if we
+         * don't hold a wake lock. We want to enter doze only after all views have reached
+         * their desired positions.
+         */
+        private final Animation.AnimationListener mKeepAwakeListener;
+        private float mDarkAmount;
 
         public Row(Context context) {
             this(context, null);
@@ -357,16 +410,13 @@
 
         public Row(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
             super(context, attrs, defStyleAttr, defStyleRes);
-            mAnimationWakeLock = WakeLock.createPartial(context, "slice animation");
+            mKeepAwakeListener = new KeepAwakeAnimationListener(mContext);
         }
 
         @Override
         protected void onFinishInflate() {
             LayoutTransition transition = new LayoutTransition();
-            transition.setDuration(ROW_ANIM_DURATION);
-            transition.setStagger(LayoutTransition.CHANGING, ROW_ANIM_DURATION);
-            transition.setStagger(LayoutTransition.CHANGE_APPEARING, ROW_ANIM_DURATION);
-            transition.setStagger(LayoutTransition.CHANGE_DISAPPEARING, ROW_ANIM_DURATION);
+            transition.setDuration(DEFAULT_ANIM_DURATION);
 
             PropertyValuesHolder left = PropertyValuesHolder.ofInt("left", 0, 1);
             PropertyValuesHolder right = PropertyValuesHolder.ofInt("right", 0, 1);
@@ -378,7 +428,8 @@
                     Interpolators.ACCELERATE_DECELERATE);
             transition.setInterpolator(LayoutTransition.CHANGE_DISAPPEARING,
                     Interpolators.ACCELERATE_DECELERATE);
-            transition.setStartDelay(LayoutTransition.CHANGE_APPEARING, ROW_ANIM_DURATION);
+            transition.setStartDelay(LayoutTransition.CHANGE_APPEARING, DEFAULT_ANIM_DURATION);
+            transition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, DEFAULT_ANIM_DURATION);
 
             ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f);
             transition.setAnimator(LayoutTransition.APPEARING, appearAnimator);
@@ -386,31 +437,11 @@
 
             ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f);
             transition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT);
-            transition.setDuration(LayoutTransition.DISAPPEARING, ROW_ANIM_DURATION / 2);
+            transition.setDuration(LayoutTransition.DISAPPEARING, DEFAULT_ANIM_DURATION / 4);
             transition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator);
 
             transition.setAnimateParentHierarchy(false);
             setLayoutTransition(transition);
-
-            // This view is visible in AOD, which means that the device will sleep if we
-            // don't hold a wake lock. We want to enter doze only after all views have reached
-            // their desired positions.
-            setLayoutAnimationListener(new Animation.AnimationListener() {
-                @Override
-                public void onAnimationStart(Animation animation) {
-                    mAnimationWakeLock.acquire();
-                }
-
-                @Override
-                public void onAnimationEnd(Animation animation) {
-                    mAnimationWakeLock.release();
-                }
-
-                @Override
-                public void onAnimationRepeat(Animation animation) {
-
-                }
-            });
         }
 
         @Override
@@ -424,23 +455,58 @@
             }
             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
         }
+
+        public void setDarkAmount(float darkAmount) {
+            boolean isAwake = darkAmount != 0;
+            boolean wasAwake = mDarkAmount != 0;
+            if (isAwake == wasAwake) {
+                return;
+            }
+            mDarkAmount = darkAmount;
+            setLayoutAnimationListener(isAwake ? null : mKeepAwakeListener);
+        }
+
+        @Override
+        public boolean hasOverlappingRendering() {
+            return false;
+        }
     }
 
     /**
      * Representation of an item that appears under the clock on main keyguard message.
      */
-    private class KeyguardSliceButton extends Button {
+    @VisibleForTesting
+    static class KeyguardSliceButton extends Button implements
+            ConfigurationController.ConfigurationListener {
+        private final Context mContext;
 
         public KeyguardSliceButton(Context context) {
             super(context, null /* attrs */, 0 /* styleAttr */,
                     com.android.keyguard.R.style.TextAppearance_Keyguard_Secondary);
-            int horizontalPadding = (int) context.getResources()
+            mContext = context;
+            onDensityOrFontScaleChanged();
+            setEllipsize(TruncateAt.END);
+        }
+
+        @Override
+        protected void onAttachedToWindow() {
+            super.onAttachedToWindow();
+            Dependency.get(ConfigurationController.class).addCallback(this);
+        }
+
+        @Override
+        protected void onDetachedFromWindow() {
+            super.onDetachedFromWindow();
+            Dependency.get(ConfigurationController.class).removeCallback(this);
+        }
+
+        @Override
+        public void onDensityOrFontScaleChanged() {
+            int horizontalPadding = (int) mContext.getResources()
                     .getDimension(R.dimen.widget_horizontal_padding);
             setPadding(horizontalPadding / 2, 0, horizontalPadding / 2, 0);
-            setCompoundDrawablePadding((int) context.getResources()
+            setCompoundDrawablePadding((int) mContext.getResources()
                     .getDimension(R.dimen.widget_icon_padding));
-            setMaxLines(1);
-            setEllipsize(TruncateAt.END);
         }
 
         @Override
@@ -465,4 +531,38 @@
             }
         }
     }
+
+    private class SliceViewTransitionListener implements LayoutTransition.TransitionListener {
+        @Override
+        public void startTransition(LayoutTransition transition, ViewGroup container, View view,
+                int transitionType) {
+            switch (transitionType) {
+                case  LayoutTransition.APPEARING:
+                    int translation = getResources().getDimensionPixelSize(
+                            R.dimen.pulsing_notification_appear_translation);
+                    view.setTranslationY(translation);
+                    view.animate()
+                            .translationY(0)
+                            .setDuration(DEFAULT_ANIM_DURATION)
+                            .setInterpolator(Interpolators.ALPHA_IN)
+                            .start();
+                    break;
+                case LayoutTransition.DISAPPEARING:
+                    if (view == mTitle) {
+                        // Translate the view to the inverse of its height, so the layout event
+                        // won't misposition it.
+                        LayoutParams params = (LayoutParams) mTitle.getLayoutParams();
+                        int margin = params.topMargin + params.bottomMargin;
+                        mTitle.setTranslationY(-mTitle.getHeight() - margin);
+                    }
+                    break;
+            }
+        }
+
+        @Override
+        public void endTransition(LayoutTransition transition, ViewGroup container, View view,
+                int transitionType) {
+
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
index a4a3d14..acf9013 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
@@ -16,12 +16,11 @@
 
 package com.android.keyguard;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.app.ActivityManager;
-import android.app.AlarmManager;
 import android.app.IActivityManager;
 import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Color;
 import android.os.Handler;
@@ -37,18 +36,24 @@
 import android.util.Slog;
 import android.util.TypedValue;
 import android.view.View;
-import android.view.ViewGroup;
 import android.widget.GridLayout;
+import android.widget.RelativeLayout;
 import android.widget.TextClock;
 import android.widget.TextView;
 
 import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.ViewClippingUtil;
+import com.android.systemui.Dependency;
+import com.android.systemui.Interpolators;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.util.wakelock.KeepAwakeAnimationListener;
 
 import com.google.android.collect.Sets;
 
 import java.util.Locale;
 
-public class KeyguardStatusView extends GridLayout {
+public class KeyguardStatusView extends GridLayout implements
+        ConfigurationController.ConfigurationListener, View.OnLayoutChangeListener {
     private static final boolean DEBUG = KeyguardConstants.DEBUG;
     private static final String TAG = "KeyguardStatusView";
     private static final int MARQUEE_DELAY_MS = 2000;
@@ -56,13 +61,11 @@
     private final LockPatternUtils mLockPatternUtils;
     private final IActivityManager mIActivityManager;
     private final float mSmallClockScale;
-    private final float mWidgetPadding;
 
     private TextView mLogoutView;
     private TextClock mClockView;
     private View mClockSeparator;
     private TextView mOwnerInfo;
-    private ViewGroup mClockContainer;
     private KeyguardSliceView mKeyguardSlice;
     private Runnable mPendingMarqueeStart;
     private Handler mHandler;
@@ -71,6 +74,9 @@
     private boolean mPulsing;
     private float mDarkAmount = 0;
     private int mTextColor;
+    private float mWidgetPadding;
+    private boolean mAnimateLayout;
+    private int mLastLayoutHeight;
 
     private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
 
@@ -127,7 +133,7 @@
         mHandler = new Handler(Looper.myLooper());
         mSmallClockScale = getResources().getDimension(R.dimen.widget_small_font_size)
                 / getResources().getDimension(R.dimen.widget_big_font_size);
-        mWidgetPadding = getResources().getDimension(R.dimen.widget_vertical_padding);
+        onDensityOrFontScaleChanged();
     }
 
     private void setEnableMarquee(boolean enabled) {
@@ -160,7 +166,6 @@
         mLogoutView = findViewById(R.id.logout);
         mLogoutView.setOnClickListener(this::onLogoutClicked);
 
-        mClockContainer = findViewById(R.id.keyguard_clock_container);
         mClockView = findViewById(R.id.clock_view);
         mClockView.setShowCurrentUserTime(true);
         if (KeyguardClockAccessibilityDelegate.isNeeded(mContext)) {
@@ -169,46 +174,111 @@
         mOwnerInfo = findViewById(R.id.owner_info);
         mKeyguardSlice = findViewById(R.id.keyguard_status_area);
         mClockSeparator = findViewById(R.id.clock_separator);
-        mVisibleInDoze = Sets.newArraySet(mClockView, mKeyguardSlice, mClockSeparator);
+        mVisibleInDoze = Sets.newArraySet(mClockView, mKeyguardSlice);
         mTextColor = mClockView.getCurrentTextColor();
 
-        mKeyguardSlice.setListener(this::onSliceContentChanged);
-        onSliceContentChanged(mKeyguardSlice.hasHeader());
+        mClockView.addOnLayoutChangeListener(this);
+        mClockSeparator.addOnLayoutChangeListener(this);
+        mKeyguardSlice.setContentChangeListener(this::onSliceContentChanged);
+        onSliceContentChanged(false /* animated */);
 
         boolean shouldMarquee = KeyguardUpdateMonitor.getInstance(mContext).isDeviceInteractive();
         setEnableMarquee(shouldMarquee);
         refresh();
         updateOwnerInfo();
         updateLogoutView();
+        updateDark();
 
         // Disable elegant text height because our fancy colon makes the ymin value huge for no
         // reason.
         mClockView.setElegantTextHeight(false);
     }
 
-    private void onSliceContentChanged(boolean hasHeader) {
-        final boolean smallClock = hasHeader || mPulsing;
-        final float clockScale = smallClock ? mSmallClockScale : 1;
-        float translation = (mClockView.getHeight() - (mClockView.getHeight() * clockScale)) / 2f;
-        if (smallClock) {
-            translation -= mWidgetPadding;
+    private void onSliceContentChanged(boolean animated) {
+        mAnimateLayout = animated;
+        boolean smallClock = mKeyguardSlice.hasHeader() || mPulsing;
+        float clockScale = smallClock ? mSmallClockScale : 1;
+
+        RelativeLayout.LayoutParams layoutParams =
+                (RelativeLayout.LayoutParams) mClockView.getLayoutParams();
+        int height = mClockView.getHeight();
+        layoutParams.bottomMargin = (int) -(height - (clockScale * height));
+        mClockView.setLayoutParams(layoutParams);
+
+        layoutParams = (RelativeLayout.LayoutParams) mClockSeparator.getLayoutParams();
+        layoutParams.topMargin = smallClock ? (int) mWidgetPadding : 0;
+        layoutParams.bottomMargin = layoutParams.topMargin;
+        mClockSeparator.setLayoutParams(layoutParams);
+    }
+
+    /**
+     * Animate clock and its separator when necessary.
+     */
+    @Override
+    public void onLayoutChange(View view, int left, int top, int right, int bottom,
+            int oldLeft, int oldTop, int oldRight, int oldBottom) {
+        int heightOffset = mPulsing ? 0 : getHeight() - mLastLayoutHeight;
+        boolean hasHeader = mKeyguardSlice.hasHeader();
+        boolean smallClock = hasHeader || mPulsing;
+        long duration = KeyguardSliceView.DEFAULT_ANIM_DURATION;
+        long delay = smallClock ? 0 : duration / 4;
+
+        if (view == mClockView) {
+            float clockScale = smallClock ? mSmallClockScale : 1;
+            if (mAnimateLayout) {
+                mClockView.setY(oldTop + heightOffset);
+                mClockView.animate().cancel();
+                mClockView.animate()
+                        .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                        .setDuration(duration)
+                        .setListener(new ClipChildrenAnimationListener())
+                        .setStartDelay(delay)
+                        .y(top)
+                        .scaleX(clockScale)
+                        .scaleY(clockScale)
+                        .start();
+            } else {
+                mClockView.setY(top);
+                mClockView.setScaleX(clockScale);
+                mClockView.setScaleY(clockScale);
+            }
+        } else if (view == mClockSeparator) {
+            boolean hasSeparator = hasHeader && !mPulsing;
+            float alpha = hasSeparator ? 1 : 0;
+            if (mAnimateLayout) {
+                boolean isAwake = mDarkAmount != 0;
+                mClockSeparator.setY(oldTop + heightOffset);
+                mClockSeparator.animate().cancel();
+                mClockSeparator.animate()
+                        .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                        .setDuration(duration)
+                        .setListener(isAwake ? null : new KeepAwakeAnimationListener(getContext()))
+                        .setStartDelay(delay)
+                        .y(top)
+                        .alpha(alpha)
+                        .start();
+            } else {
+                mClockSeparator.setY(top);
+                mClockSeparator.setAlpha(alpha);
+            }
         }
-        mClockView.setTranslationY(translation);
-        mClockView.setScaleX(clockScale);
-        mClockView.setScaleY(clockScale);
-        mClockSeparator.setVisibility(hasHeader && !mPulsing ? VISIBLE : GONE);
     }
 
     @Override
-    protected void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-        mClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
-                getResources().getDimensionPixelSize(R.dimen.widget_big_font_size));
-        // Some layouts like burmese have a different margin for the clock
-        MarginLayoutParams layoutParams = (MarginLayoutParams) mClockView.getLayoutParams();
-        layoutParams.bottomMargin = getResources().getDimensionPixelSize(
-                R.dimen.bottom_text_spacing_digital);
-        mClockView.setLayoutParams(layoutParams);
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        mClockView.setPivotX(mClockView.getWidth() / 2);
+        mClockView.setPivotY(0);
+        mLastLayoutHeight = getHeight();
+    }
+
+    @Override
+    public void onDensityOrFontScaleChanged() {
+        mWidgetPadding = getResources().getDimension(R.dimen.widget_vertical_padding);
+        if (mClockView != null) {
+            mClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
+                    getResources().getDimensionPixelSize(R.dimen.widget_big_font_size));
+        }
         if (mOwnerInfo != null) {
             mOwnerInfo.setTextSize(TypedValue.COMPLEX_UNIT_PX,
                     getResources().getDimensionPixelSize(R.dimen.widget_label_font_size));
@@ -224,14 +294,6 @@
         refreshTime();
     }
 
-    public int getClockBottom() {
-        if (mOwnerInfo != null && mOwnerInfo.getVisibility() == VISIBLE) {
-            return mOwnerInfo.getBottom();
-        } else {
-            return mClockContainer.getBottom();
-        }
-    }
-
     public int getLogoutButtonHeight() {
         return mLogoutView.getVisibility() == VISIBLE ? mLogoutView.getHeight() : 0;
     }
@@ -249,34 +311,8 @@
 
     private void updateOwnerInfo() {
         if (mOwnerInfo == null) return;
-        String ownerInfo = getOwnerInfo();
-        if (!TextUtils.isEmpty(ownerInfo)) {
-            mOwnerInfo.setVisibility(View.VISIBLE);
-            mOwnerInfo.setText(ownerInfo);
-        } else {
-            mOwnerInfo.setVisibility(View.GONE);
-        }
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mInfoCallback);
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mInfoCallback);
-    }
-
-    private String getOwnerInfo() {
-        String info = null;
-        if (mLockPatternUtils.isDeviceOwnerInfoEnabled()) {
-            // Use the device owner information set by device policy client via
-            // device policy manager.
-            info = mLockPatternUtils.getDeviceOwnerInfo();
-        } else {
+        String info = mLockPatternUtils.getDeviceOwnerInfo();
+        if (info == null) {
             // Use the current user owner information if enabled.
             final boolean ownerInfoEnabled = mLockPatternUtils.isOwnerInfoEnabled(
                     KeyguardUpdateMonitor.getCurrentUser());
@@ -284,7 +320,21 @@
                 info = mLockPatternUtils.getOwnerInfo(KeyguardUpdateMonitor.getCurrentUser());
             }
         }
-        return info;
+        mOwnerInfo.setText(info);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mInfoCallback);
+        Dependency.get(ConfigurationController.class).addCallback(this);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mInfoCallback);
+        Dependency.get(ConfigurationController.class).removeCallback(this);
     }
 
     @Override
@@ -329,32 +379,27 @@
             return;
         }
         mDarkAmount = darkAmount;
-
-        boolean dark = darkAmount == 1;
-        mLogoutView.setAlpha(dark ? 0 : 1);
-        final int N = mClockContainer.getChildCount();
-        for (int i = 0; i < N; i++) {
-            View child = mClockContainer.getChildAt(i);
-            if (mVisibleInDoze.contains(child)) {
-                continue;
-            }
-            child.setAlpha(dark ? 0 : 1);
-        }
-        if (mOwnerInfo != null) {
-            mOwnerInfo.setAlpha(dark ? 0 : 1);
-        }
-
-        final int blendedTextColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, darkAmount);
-        updateDozeVisibleViews();
-        mKeyguardSlice.setDark(darkAmount);
-        mClockView.setTextColor(blendedTextColor);
-        mClockSeparator.setBackgroundTintList(ColorStateList.valueOf(blendedTextColor));
+        updateDark();
     }
 
-    public void setPulsing(boolean pulsing) {
+    private void updateDark() {
+        boolean dark = mDarkAmount == 1;
+        mLogoutView.setAlpha(dark ? 0 : 1);
+        if (mOwnerInfo != null) {
+            boolean hasText = !TextUtils.isEmpty(mOwnerInfo.getText());
+            mOwnerInfo.setVisibility(hasText && mDarkAmount != 1 ? VISIBLE : GONE);
+        }
+
+        final int blendedTextColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount);
+        updateDozeVisibleViews();
+        mKeyguardSlice.setDarkAmount(mDarkAmount);
+        mClockView.setTextColor(blendedTextColor);
+        mClockSeparator.setBackgroundColor(blendedTextColor);
+    }
+
+    public void setPulsing(boolean pulsing, boolean animate) {
         mPulsing = pulsing;
-        mKeyguardSlice.setHideContent(pulsing);
-        onSliceContentChanged(mKeyguardSlice.hasHeader());
+        mKeyguardSlice.setPulsing(pulsing, animate);
         updateDozeVisibleViews();
     }
 
@@ -378,4 +423,24 @@
             Log.e(TAG, "Failed to logout user", re);
         }
     }
+
+    private class ClipChildrenAnimationListener extends AnimatorListenerAdapter implements
+            ViewClippingUtil.ClippingParameters {
+
+        ClipChildrenAnimationListener() {
+            ViewClippingUtil.setClippingDeactivated(mClockView, true /* deactivated */,
+                    this /* clippingParams */);
+        }
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            ViewClippingUtil.setClippingDeactivated(mClockView, false /* deactivated */,
+                    this /* clippingParams */);
+        }
+
+        @Override
+        public boolean shouldFinish(View view) {
+            return view == getParent();
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java
index b0bda16..90140ff 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java
@@ -80,7 +80,9 @@
         }
 
         boolean messagePending = mHandler.hasCallbacks(mApplyPendingScreenState);
-        if (messagePending || oldState == DozeMachine.State.INITIALIZED) {
+        boolean pulseEnding = oldState  == DozeMachine.State.DOZE_PULSE_DONE
+                && newState == DozeMachine.State.DOZE_AOD;
+        if (messagePending || oldState == DozeMachine.State.INITIALIZED || pulseEnding) {
             // During initialization, we hide the navigation bar. That is however only applied after
             // a traversal; setting the screen state here is immediate however, so it can happen
             // that the screen turns on again before the navigation bar is hidden. To work around
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedTextView.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedTextView.java
new file mode 100644
index 0000000..742005d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedTextView.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2018 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.TextView;
+
+/**
+ * A TextView which does not have overlapping renderings commands and therefore does not need a
+ * layer when alpha is changed.
+ */
+public class AlphaOptimizedTextView extends TextView {
+    public AlphaOptimizedTextView(Context context) {
+        super(context);
+    }
+
+    public AlphaOptimizedTextView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public AlphaOptimizedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public AlphaOptimizedTextView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index a0a97c5..932c3c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -1866,7 +1866,7 @@
         if (view == null && mQsExpanded) {
             return;
         }
-        if (needsAnimation) {
+        if (needsAnimation && mDarkAmount == 0) {
             mAnimateNextPositionUpdate = true;
         }
         ExpandableView firstChildNotGone = mNotificationStackScroller.getFirstChildNotGone();
@@ -2652,10 +2652,13 @@
 
     public void setPulsing(boolean pulsing) {
         mPulsing = pulsing;
-        mKeyguardStatusView.setPulsing(pulsing);
-        positionClockAndNotifications();
-        mNotificationStackScroller.setPulsing(pulsing, mKeyguardStatusView.getLocationOnScreen()[1]
-                + mKeyguardStatusView.getClockBottom());
+        final boolean canAnimatePulse =
+                !DozeParameters.getInstance(mContext).getDisplayNeedsBlanking();
+        if (canAnimatePulse) {
+            mAnimateNextPositionUpdate = true;
+        }
+        mNotificationStackScroller.setPulsing(pulsing, canAnimatePulse);
+        mKeyguardStatusView.setPulsing(pulsing, canAnimatePulse);
     }
 
     public void setAmbientIndicationBottomPadding(int ambientIndicationBottomPadding) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
index 897d13b..00eb1ad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -69,6 +69,7 @@
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.keyguard.KeyguardSliceView;
 import com.android.settingslib.Utils;
 import com.android.systemui.Dependency;
 import com.android.systemui.ExpandHelper;
@@ -370,6 +371,7 @@
     private boolean mGroupExpandedForMeasure;
     private boolean mScrollable;
     private View mForcedScroll;
+    private View mNeedingPulseAnimation;
     private float mDarkAmount = 0f;
     private static final Property<NotificationStackScrollLayout, Float> DARK_AMOUNT =
             new FloatProperty<NotificationStackScrollLayout>("darkAmount") {
@@ -406,7 +408,6 @@
     private final int mSeparatorWidth;
     private final int mSeparatorThickness;
     private final Rect mBackgroundAnimationRect = new Rect();
-    private int mClockBottom;
     private int mAntiBurnInOffsetX;
     private ArrayList<BiConsumer<Float, Float>> mExpandedHeightListeners = new ArrayList<>();
     private int mHeadsUpInset;
@@ -717,11 +718,7 @@
     }
 
     private void updateAlgorithmHeightAndPadding() {
-        if (mPulsing) {
-            mTopPadding = mClockBottom;
-        } else {
-            mTopPadding = (int) MathUtils.lerp(mRegularTopPadding, mDarkTopPadding, mDarkAmount);
-        }
+        mTopPadding = (int) MathUtils.lerp(mRegularTopPadding, mDarkTopPadding, mDarkAmount);
         mAmbientState.setLayoutHeight(getLayoutHeight());
         updateAlgorithmLayoutMinHeight();
         mAmbientState.setTopPadding(mTopPadding);
@@ -3174,6 +3171,7 @@
         generateViewResizeEvent();
         generateGroupExpansionEvent();
         generateAnimateEverythingEvent();
+        generatePulsingAnimationEvent();
         mNeedsAnimation = false;
     }
 
@@ -4428,18 +4426,28 @@
         return mIsExpanded;
     }
 
-    public void setPulsing(boolean pulsing, int clockBottom) {
+    public void setPulsing(boolean pulsing, boolean animated) {
         if (!mPulsing && !pulsing) {
             return;
         }
         mPulsing = pulsing;
-        mClockBottom = clockBottom;
+        mNeedingPulseAnimation = animated ? getFirstChildNotGone() : null;
         mAmbientState.setPulsing(pulsing);
         updateNotificationAnimationStates();
         updateAlgorithmHeightAndPadding();
         updateContentHeight();
-        notifyHeightChangeListener(mShelf);
         requestChildrenUpdate();
+        notifyHeightChangeListener(null, animated);
+        mNeedsAnimation |= animated;
+    }
+
+    private void generatePulsingAnimationEvent() {
+        if (mNeedingPulseAnimation != null) {
+            int type = mPulsing ? AnimationEvent.ANIMATION_TYPE_PULSE_APPEAR
+                    : AnimationEvent.ANIMATION_TYPE_PULSE_DISAPPEAR;
+            mAnimationEvents.add(new AnimationEvent(mNeedingPulseAnimation, type));
+            mNeedingPulseAnimation = null;
+        }
     }
 
     public void setFadingOut(boolean fadingOut) {
@@ -5007,6 +5015,16 @@
                         .animateTopInset()
                         .animateY()
                         .animateZ(),
+
+                // ANIMATION_TYPE_PULSE_APPEAR
+                new AnimationFilter()
+                        .animateAlpha()
+                        .animateY(),
+
+                // ANIMATION_TYPE_PULSE_DISAPPEAR
+                new AnimationFilter()
+                        .animateAlpha()
+                        .animateY(),
         };
 
         static int[] LENGTHS = new int[] {
@@ -5067,6 +5085,12 @@
 
                 // ANIMATION_TYPE_EVERYTHING
                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+                // ANIMATION_TYPE_PULSE_APPEAR
+                KeyguardSliceView.DEFAULT_ANIM_DURATION,
+
+                // ANIMATION_TYPE_PULSE_DISAPPEAR
+                KeyguardSliceView.DEFAULT_ANIM_DURATION / 2,
         };
 
         static final int ANIMATION_TYPE_ADD = 0;
@@ -5088,6 +5112,8 @@
         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 ANIMATION_TYPE_PULSE_APPEAR = 19;
+        static final int ANIMATION_TYPE_PULSE_DISAPPEAR = 20;
 
         static final int DARK_ANIMATION_ORIGIN_INDEX_ABOVE = -1;
         static final int DARK_ANIMATION_ORIGIN_INDEX_BELOW = -2;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
index c80bdc6..d01db14 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
@@ -60,6 +60,7 @@
     public static final int ANIMATION_DELAY_HEADS_UP_CLICKED= 120;
 
     private final int mGoToFullShadeAppearingTranslation;
+    private final int mPulsingAppearingTranslation;
     private final ExpandableViewState mTmpState = new ExpandableViewState();
     private final AnimationProperties mAnimationProperties;
     public NotificationStackScrollLayout mHostLayout;
@@ -90,6 +91,9 @@
         mGoToFullShadeAppearingTranslation =
                 hostLayout.getContext().getResources().getDimensionPixelSize(
                         R.dimen.go_to_full_shade_appearing_translation);
+        mPulsingAppearingTranslation =
+                hostLayout.getContext().getResources().getDimensionPixelSize(
+                        R.dimen.pulsing_notification_appear_translation);
         mAnimationProperties = new AnimationProperties() {
             @Override
             public AnimationFilter getAnimationFilter() {
@@ -427,6 +431,18 @@
                 ExpandableNotificationRow row = (ExpandableNotificationRow) event.changingView;
                 row.prepareExpansionChanged(finalState);
             } else if (event.animationType == NotificationStackScrollLayout
+                    .AnimationEvent.ANIMATION_TYPE_PULSE_APPEAR) {
+                ExpandableViewState viewState = finalState.getViewStateForView(changingView);
+                mTmpState.copyFrom(viewState);
+                mTmpState.yTranslation += mPulsingAppearingTranslation;
+                mTmpState.alpha = 0;
+                mTmpState.applyToView(changingView);
+            } else if (event.animationType == NotificationStackScrollLayout
+                    .AnimationEvent.ANIMATION_TYPE_PULSE_DISAPPEAR) {
+                ExpandableViewState viewState = finalState.getViewStateForView(changingView);
+                viewState.yTranslation += mPulsingAppearingTranslation;
+                viewState.alpha = 0;
+            } else if (event.animationType == NotificationStackScrollLayout
                     .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) {
                 // This item is added, initialize it's properties.
                 ExpandableViewState viewState = finalState.getViewStateForView(changingView);
diff --git a/packages/SystemUI/src/com/android/systemui/util/wakelock/KeepAwakeAnimationListener.java b/packages/SystemUI/src/com/android/systemui/util/wakelock/KeepAwakeAnimationListener.java
new file mode 100644
index 0000000..a54b0e1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/wakelock/KeepAwakeAnimationListener.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2018 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.util.wakelock;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.view.animation.Animation;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.util.Assert;
+
+public class KeepAwakeAnimationListener extends AnimatorListenerAdapter
+        implements Animation.AnimationListener {
+    @VisibleForTesting
+    static WakeLock sWakeLock;
+
+    public KeepAwakeAnimationListener(Context context) {
+        Assert.isMainThread();
+        if (sWakeLock == null) {
+            sWakeLock = WakeLock.createPartial(context, "animation");
+        }
+    }
+
+    @Override
+    public void onAnimationStart(Animation animation) {
+        onStart();
+    }
+
+    @Override
+    public void onAnimationEnd(Animation animation) {
+        onEnd();
+    }
+
+    @Override
+    public void onAnimationRepeat(Animation animation) {
+
+    }
+
+    @Override
+    public void onAnimationStart(Animator animation) {
+        onStart();
+    }
+
+    @Override
+    public void onAnimationEnd(Animator animation) {
+        onEnd();
+    }
+
+    private void onStart() {
+        Assert.isMainThread();
+        sWakeLock.acquire();
+    }
+
+    private void onEnd() {
+        Assert.isMainThread();
+        sWakeLock.release();
+    }
+}