/*
 * 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.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();
        }
    }
}
