android.support.v17.leanback.app.BackgroundManager.java Source code

Java tutorial

Introduction

Here is the source code for android.support.v17.leanback.app.BackgroundManager.java

Source

/*
 * 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 android.support.v17.leanback.app;

import java.lang.ref.WeakReference;

import android.support.annotation.ColorInt;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.support.v17.leanback.R;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Handler;
import android.support.v17.leanback.widget.BackgroundHelper;
import android.support.v4.view.animation.FastOutLinearInInterpolator;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.view.animation.Interpolator;
import android.view.animation.AnimationUtils;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.ContextCompat;

/**
 * Supports background image continuity between multiple Activities.
 *
 * <p>An Activity should instantiate a BackgroundManager and {@link #attach}
 * to the Activity's window.  When the Activity is started, the background is
 * initialized to the current background values stored in a continuity service.
 * The background continuity service is updated as the background is updated.
 *
 * <p>At some point, for example when it is stopped, the Activity may release
 * its background state.
 *
 * <p>When an Activity is resumed, if the BackgroundManager has not been
 * released, the continuity service is updated from the BackgroundManager state.
 * If the BackgroundManager was released, the BackgroundManager inherits the
 * current state from the continuity service.
 *
 * <p>When the last Activity is destroyed, the background state is reset.
 *
 * <p>Backgrounds consist of several layers, from back to front:
 * <ul>
 *   <li>the background Drawable of the theme</li>
 *   <li>a solid color (set via {@link #setColor})</li>
 *   <li>two Drawables, previous and current (set via {@link #setBitmap} or
 *   {@link #setDrawable}), which may be in transition</li>
 * </ul>
 *
 * <p>BackgroundManager holds references to potentially large bitmap Drawables.
 * Call {@link #release} to release these references when the Activity is not
 * visible.
 */
// TODO: support for multiple app processes requires a proper android service
// instead of the shared memory "service" implemented here. Such a service could
// support continuity between fragments of different applications if desired.
public final class BackgroundManager {

    interface FragmentStateQueriable {
        public boolean isResumed();
    }

    private static final String TAG = "BackgroundManager";
    private static final boolean DEBUG = false;

    private static final int FULL_ALPHA = 255;
    private static final int DIM_ALPHA_ON_SOLID = (int) (0.8f * FULL_ALPHA);
    private static final int CHANGE_BG_DELAY_MS = 500;
    private static final int FADE_DURATION = 500;

    /**
     * Using a separate window for backgrounds can improve graphics performance by
     * leveraging hardware display layers.
     * TODO: support a leanback configuration option.
     */
    private static final boolean USE_SEPARATE_WINDOW = false;

    private static final String WINDOW_NAME = "BackgroundManager";
    private static final String FRAGMENT_TAG = BackgroundManager.class.getCanonicalName();

    private Context mContext;
    private Handler mHandler;
    private Window mWindow;
    private WindowManager mWindowManager;
    private View mBgView;
    private BackgroundContinuityService mService;
    private int mThemeDrawableResourceId;
    private FragmentStateQueriable mFragmentState;

    private int mHeightPx;
    private int mWidthPx;
    private Drawable mBackgroundDrawable;
    private int mBackgroundColor;
    private boolean mAttached;
    private long mLastSetTime;

    private final Interpolator mAccelerateInterpolator;
    private final Interpolator mDecelerateInterpolator;
    private final ValueAnimator mAnimator;
    private final ValueAnimator mDimAnimator;

    private static class BitmapDrawable extends Drawable {

        static class ConstantState extends Drawable.ConstantState {
            Bitmap mBitmap;
            Matrix mMatrix;
            Paint mPaint;

            @Override
            public Drawable newDrawable() {
                return new BitmapDrawable(null, mBitmap, mMatrix);
            }

            @Override
            public int getChangingConfigurations() {
                return 0;
            }
        }

        private ConstantState mState = new ConstantState();

        BitmapDrawable(Resources resources, Bitmap bitmap) {
            this(resources, bitmap, null);
        }

        BitmapDrawable(Resources resources, Bitmap bitmap, Matrix matrix) {
            mState.mBitmap = bitmap;
            mState.mMatrix = matrix != null ? matrix : new Matrix();
            mState.mPaint = new Paint();
            mState.mPaint.setFilterBitmap(true);
        }

        Bitmap getBitmap() {
            return mState.mBitmap;
        }

        @Override
        public void draw(Canvas canvas) {
            if (mState.mBitmap == null) {
                return;
            }
            if (mState.mPaint.getAlpha() < FULL_ALPHA && mState.mPaint.getColorFilter() != null) {
                throw new IllegalStateException("Can't draw with translucent alpha and color filter");
            }
            canvas.drawBitmap(mState.mBitmap, mState.mMatrix, mState.mPaint);
        }

        @Override
        public int getOpacity() {
            return android.graphics.PixelFormat.TRANSLUCENT;
        }

        @Override
        public void setAlpha(int alpha) {
            if (mState.mPaint.getAlpha() != alpha) {
                mState.mPaint.setAlpha(alpha);
                invalidateSelf();
            }
        }

        /**
         * Does not invalidateSelf to avoid recursion issues.
         * Caller must ensure appropriate invalidation.
         */
        @Override
        public void setColorFilter(ColorFilter cf) {
            mState.mPaint.setColorFilter(cf);
        }

        public ColorFilter getColorFilter() {
            return mState.mPaint.getColorFilter();
        }

        @Override
        public ConstantState getConstantState() {
            return mState;
        }
    }

    private static class DrawableWrapper {
        private int mAlpha = FULL_ALPHA;
        private Drawable mDrawable;
        private ColorFilter mColorFilter;

        public DrawableWrapper(Drawable drawable) {
            mDrawable = drawable;
            updateAlpha();
            updateColorFilter();
        }

        public DrawableWrapper(DrawableWrapper wrapper, Drawable drawable) {
            mDrawable = drawable;
            mAlpha = wrapper.getAlpha();
            updateAlpha();
            mColorFilter = wrapper.getColorFilter();
            updateColorFilter();
        }

        public Drawable getDrawable() {
            return mDrawable;
        }

        public void setAlpha(int alpha) {
            mAlpha = alpha;
            updateAlpha();
        }

        public int getAlpha() {
            return mAlpha;
        }

        private void updateAlpha() {
            mDrawable.setAlpha(mAlpha);
        }

        public ColorFilter getColorFilter() {
            return mColorFilter;
        }

        public void setColorFilter(ColorFilter colorFilter) {
            mColorFilter = colorFilter;
            updateColorFilter();
        }

        private void updateColorFilter() {
            mDrawable.setColorFilter(mColorFilter);
        }

        public void setColor(int color) {
            ((ColorDrawable) mDrawable).setColor(color);
        }
    }

    static class TranslucentLayerDrawable extends LayerDrawable {
        private DrawableWrapper[] mWrapper;
        private Paint mPaint = new Paint();

        public TranslucentLayerDrawable(Drawable[] drawables) {
            super(drawables);
            int count = drawables.length;
            mWrapper = new DrawableWrapper[count];
            for (int i = 0; i < count; i++) {
                mWrapper[i] = new DrawableWrapper(drawables[i]);
            }
        }

        @Override
        public void setAlpha(int alpha) {
            if (mPaint.getAlpha() != alpha) {
                int previousAlpha = mPaint.getAlpha();
                mPaint.setAlpha(alpha);
                invalidateSelf();
                onAlphaChanged(previousAlpha, alpha);
            }
        }

        // Queried by system transitions
        public int getAlpha() {
            return mPaint.getAlpha();
        }

        protected void onAlphaChanged(int oldAlpha, int newAlpha) {
        }

        @Override
        public Drawable mutate() {
            Drawable drawable = super.mutate();
            int count = getNumberOfLayers();
            for (int i = 0; i < count; i++) {
                if (mWrapper[i] != null) {
                    mWrapper[i] = new DrawableWrapper(mWrapper[i], getDrawable(i));
                }
            }
            invalidateSelf();
            return drawable;
        }

        @Override
        public int getOpacity() {
            return PixelFormat.TRANSLUCENT;
        }

        @Override
        public boolean setDrawableByLayerId(int id, Drawable drawable) {
            return updateDrawable(id, drawable) != null;
        }

        public DrawableWrapper updateDrawable(int id, Drawable drawable) {
            super.setDrawableByLayerId(id, drawable);
            for (int i = 0; i < getNumberOfLayers(); i++) {
                if (getId(i) == id) {
                    mWrapper[i] = new DrawableWrapper(drawable);
                    // Must come after mWrapper was updated so it can be seen by updateColorFilter
                    invalidateSelf();
                    return mWrapper[i];
                }
            }
            return null;
        }

        public void clearDrawable(int id, Context context) {
            for (int i = 0; i < getNumberOfLayers(); i++) {
                if (getId(i) == id) {
                    mWrapper[i] = null;
                    super.setDrawableByLayerId(id, createEmptyDrawable(context));
                    break;
                }
            }
        }

        public DrawableWrapper findWrapperById(int id) {
            for (int i = 0; i < getNumberOfLayers(); i++) {
                if (getId(i) == id) {
                    return mWrapper[i];
                }
            }
            return null;
        }

        @Override
        public void draw(Canvas canvas) {
            if (mPaint.getAlpha() < FULL_ALPHA) {
                canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
            }
            super.draw(canvas);
            if (mPaint.getAlpha() < FULL_ALPHA) {
                canvas.restore();
            }
        }
    }

    /**
     * Optimizes drawing when the dim drawable is an alpha-only color and imagein is opaque.
     * When the layer drawable is translucent (activity transition) then we can avoid the slow
     * saveLayer/restore draw path.
     */
    private class OptimizedTranslucentLayerDrawable extends TranslucentLayerDrawable {
        private PorterDuffColorFilter mColorFilter;
        private boolean mUpdatingColorFilter;

        public OptimizedTranslucentLayerDrawable(Drawable[] drawables) {
            super(drawables);
        }

        @Override
        protected void onAlphaChanged(int oldAlpha, int newAlpha) {
            if (newAlpha == FULL_ALPHA && oldAlpha < FULL_ALPHA) {
                if (DEBUG)
                    Log.v(TAG, "transition complete");
                postChangeRunnable();
            }
        }

        @Override
        public void invalidateSelf() {
            super.invalidateSelf();
            updateColorFilter();
        }

        @Override
        public void invalidateDrawable(Drawable who) {
            if (!mUpdatingColorFilter) {
                invalidateSelf();
            }
        }

        private void updateColorFilter() {
            DrawableWrapper dimWrapper = findWrapperById(R.id.background_dim);
            DrawableWrapper imageInWrapper = findWrapperById(R.id.background_imagein);
            DrawableWrapper imageOutWrapper = findWrapperById(R.id.background_imageout);

            mColorFilter = null;
            if (imageInWrapper != null && imageInWrapper.getAlpha() == FULL_ALPHA
                    && dimWrapper.getDrawable() instanceof ColorDrawable) {
                int dimColor = ((ColorDrawable) dimWrapper.getDrawable()).getColor();
                if (Color.red(dimColor) == 0 && Color.green(dimColor) == 0 && Color.blue(dimColor) == 0) {
                    int dimAlpha = 255 - Color.alpha(dimColor);
                    int color = Color.argb(getAlpha(), dimAlpha, dimAlpha, dimAlpha);
                    mColorFilter = new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY);
                }
            }
            mUpdatingColorFilter = true;
            if (imageInWrapper != null) {
                imageInWrapper.setColorFilter(mColorFilter);
            }
            if (imageOutWrapper != null) {
                imageOutWrapper.setColorFilter(null);
            }
            mUpdatingColorFilter = false;
        }

        @Override
        public void draw(Canvas canvas) {
            DrawableWrapper imageInWrapper = findWrapperById(R.id.background_imagein);
            if (imageInWrapper != null && imageInWrapper.getDrawable() != null
                    && imageInWrapper.getColorFilter() != null) {
                imageInWrapper.getDrawable().draw(canvas);
            } else {
                super.draw(canvas);
            }
        }
    }

    private TranslucentLayerDrawable createOptimizedTranslucentLayerDrawable(LayerDrawable layerDrawable) {
        int numChildren = layerDrawable.getNumberOfLayers();
        Drawable[] drawables = new Drawable[numChildren];
        for (int i = 0; i < numChildren; i++) {
            drawables[i] = layerDrawable.getDrawable(i);
        }
        TranslucentLayerDrawable result = new OptimizedTranslucentLayerDrawable(drawables);
        for (int i = 0; i < numChildren; i++) {
            result.setId(i, layerDrawable.getId(i));
        }
        return result;
    }

    private TranslucentLayerDrawable mLayerDrawable;
    private Drawable mDimDrawable;
    private ChangeBackgroundRunnable mChangeRunnable;
    private boolean mChangeRunnablePending;

    private final Animator.AnimatorListener mAnimationListener = new Animator.AnimatorListener() {
        final Runnable mRunnable = new Runnable() {
            @Override
            public void run() {
                postChangeRunnable();
            }
        };

        @Override
        public void onAnimationStart(Animator animation) {
        }

        @Override
        public void onAnimationRepeat(Animator animation) {
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            if (mLayerDrawable != null) {
                mLayerDrawable.clearDrawable(R.id.background_imageout, mContext);
            }
            mHandler.post(mRunnable);
        }

        @Override
        public void onAnimationCancel(Animator animation) {
        }
    };

    private final ValueAnimator.AnimatorUpdateListener mAnimationUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            int fadeInAlpha = (Integer) animation.getAnimatedValue();
            DrawableWrapper imageInWrapper = getImageInWrapper();
            if (imageInWrapper != null) {
                imageInWrapper.setAlpha(fadeInAlpha);
            } else {
                DrawableWrapper imageOutWrapper = getImageOutWrapper();
                if (imageOutWrapper != null) {
                    imageOutWrapper.setAlpha(255 - fadeInAlpha);
                }
            }
        }
    };

    private final ValueAnimator.AnimatorUpdateListener mDimUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            DrawableWrapper dimWrapper = getDimWrapper();
            if (dimWrapper != null) {
                dimWrapper.setAlpha((Integer) animation.getAnimatedValue());
            }
        }
    };

    /**
     * Shared memory continuity service.
     */
    private static class BackgroundContinuityService {
        private static final String TAG = "BackgroundContinuityService";
        private static boolean DEBUG = BackgroundManager.DEBUG;

        private static BackgroundContinuityService sService = new BackgroundContinuityService();

        private int mColor;
        private Drawable mDrawable;
        private int mCount;

        /** Single cache of theme drawable */
        private int mLastThemeDrawableId;
        private WeakReference<Drawable.ConstantState> mLastThemeDrawableState;

        private BackgroundContinuityService() {
            reset();
        }

        private void reset() {
            mColor = Color.TRANSPARENT;
            mDrawable = null;
        }

        public static BackgroundContinuityService getInstance() {
            final int count = sService.mCount++;
            if (DEBUG)
                Log.v(TAG, "Returning instance with new count " + count);
            return sService;
        }

        public void unref() {
            if (mCount <= 0)
                throw new IllegalStateException("Can't unref, count " + mCount);
            if (--mCount == 0) {
                if (DEBUG)
                    Log.v(TAG, "mCount is zero, resetting");
                reset();
            }
        }

        public int getColor() {
            return mColor;
        }

        public Drawable getDrawable() {
            return mDrawable;
        }

        public void setColor(int color) {
            mColor = color;
        }

        public void setDrawable(Drawable drawable) {
            mDrawable = drawable;
        }

        public Drawable getThemeDrawable(Context context, int themeDrawableId) {
            Drawable drawable = null;
            if (mLastThemeDrawableState != null && mLastThemeDrawableId == themeDrawableId) {
                Drawable.ConstantState drawableState = mLastThemeDrawableState.get();
                if (DEBUG)
                    Log.v(TAG, "got cached theme drawable state " + drawableState);
                if (drawableState != null) {
                    drawable = drawableState.newDrawable();
                }
            }
            if (drawable == null) {
                drawable = ContextCompat.getDrawable(context, themeDrawableId);
                if (DEBUG)
                    Log.v(TAG, "loaded theme drawable " + drawable);
                mLastThemeDrawableState = new WeakReference<Drawable.ConstantState>(drawable.getConstantState());
                mLastThemeDrawableId = themeDrawableId;
            }
            // No mutate required because this drawable is never manipulated.
            return drawable;
        }
    }

    private Drawable getThemeDrawable() {
        Drawable drawable = null;
        if (mThemeDrawableResourceId != -1) {
            drawable = mService.getThemeDrawable(mContext, mThemeDrawableResourceId);
        }
        if (drawable == null) {
            drawable = createEmptyDrawable(mContext);
        }
        return drawable;
    }

    /**
     * Returns the BackgroundManager associated with the given Activity.
     * <p>
     * The BackgroundManager will be created on-demand for each individual
     * Activity. Subsequent calls will return the same BackgroundManager created
     * for this Activity.
     */
    public static BackgroundManager getInstance(Activity activity) {
        if (activity instanceof FragmentActivity) {
            return getSupportInstance((FragmentActivity) activity);
        }
        BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager()
                .findFragmentByTag(FRAGMENT_TAG);
        if (fragment != null) {
            BackgroundManager manager = fragment.getBackgroundManager();
            if (manager != null) {
                return manager;
            }
            // manager is null: this is a fragment restored by FragmentManager,
            // fall through to create a BackgroundManager attach to it.
        }
        return new BackgroundManager(activity, false);
    }

    private static BackgroundManager getSupportInstance(FragmentActivity activity) {
        BackgroundSupportFragment fragment = (BackgroundSupportFragment) activity.getSupportFragmentManager()
                .findFragmentByTag(FRAGMENT_TAG);
        if (fragment != null) {
            BackgroundManager manager = fragment.getBackgroundManager();
            if (manager != null) {
                return manager;
            }
            // manager is null: this is a fragment restored by FragmentManager,
            // fall through to create a BackgroundManager attach to it.
        }
        return new BackgroundManager(activity, true);
    }

    private BackgroundManager(Activity activity, boolean isSupportFragmentActivity) {
        mContext = activity;
        mService = BackgroundContinuityService.getInstance();
        mHeightPx = mContext.getResources().getDisplayMetrics().heightPixels;
        mWidthPx = mContext.getResources().getDisplayMetrics().widthPixels;
        mHandler = new Handler();

        Interpolator defaultInterpolator = new FastOutLinearInInterpolator();
        mAccelerateInterpolator = AnimationUtils.loadInterpolator(mContext, android.R.anim.accelerate_interpolator);
        mDecelerateInterpolator = AnimationUtils.loadInterpolator(mContext, android.R.anim.decelerate_interpolator);

        mAnimator = ValueAnimator.ofInt(0, FULL_ALPHA);
        mAnimator.addListener(mAnimationListener);
        mAnimator.addUpdateListener(mAnimationUpdateListener);
        mAnimator.setInterpolator(defaultInterpolator);

        mDimAnimator = new ValueAnimator();
        mDimAnimator.addUpdateListener(mDimUpdateListener);

        TypedArray ta = activity.getTheme().obtainStyledAttributes(new int[] { android.R.attr.windowBackground });
        mThemeDrawableResourceId = ta.getResourceId(0, -1);
        if (mThemeDrawableResourceId < 0) {
            if (DEBUG)
                Log.v(TAG, "BackgroundManager no window background resource!");
        }
        ta.recycle();

        if (isSupportFragmentActivity) {
            createSupportFragment((FragmentActivity) activity);
        } else {
            createFragment(activity);
        }
    }

    private void createFragment(Activity activity) {
        // Use a fragment to ensure the background manager gets detached properly.
        BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager()
                .findFragmentByTag(FRAGMENT_TAG);
        if (fragment == null) {
            fragment = new BackgroundFragment();
            activity.getFragmentManager().beginTransaction().add(fragment, FRAGMENT_TAG).commit();
        } else {
            if (fragment.getBackgroundManager() != null) {
                throw new IllegalStateException("Created duplicated BackgroundManager for same "
                        + "activity, please use getInstance() instead");
            }
        }
        fragment.setBackgroundManager(this);
        mFragmentState = fragment;
    }

    private void createSupportFragment(FragmentActivity activity) {
        // Use a fragment to ensure the background manager gets detached properly.
        BackgroundSupportFragment fragment = (BackgroundSupportFragment) activity.getSupportFragmentManager()
                .findFragmentByTag(FRAGMENT_TAG);
        if (fragment == null) {
            fragment = new BackgroundSupportFragment();
            activity.getSupportFragmentManager().beginTransaction().add(fragment, FRAGMENT_TAG).commit();
        } else {
            if (fragment.getBackgroundManager() != null) {
                throw new IllegalStateException("Created duplicated BackgroundManager for same "
                        + "activity, please use getInstance() instead");
            }
        }
        fragment.setBackgroundManager(this);
        mFragmentState = fragment;
    }

    private DrawableWrapper getImageInWrapper() {
        return mLayerDrawable == null ? null : mLayerDrawable.findWrapperById(R.id.background_imagein);
    }

    private DrawableWrapper getImageOutWrapper() {
        return mLayerDrawable == null ? null : mLayerDrawable.findWrapperById(R.id.background_imageout);
    }

    private DrawableWrapper getDimWrapper() {
        return mLayerDrawable == null ? null : mLayerDrawable.findWrapperById(R.id.background_dim);
    }

    private DrawableWrapper getColorWrapper() {
        return mLayerDrawable == null ? null : mLayerDrawable.findWrapperById(R.id.background_color);
    }

    /**
     * Synchronizes state when the owning Activity is started.
     * At that point the view becomes visible.
     */
    void onActivityStart() {
        if (mService == null) {
            return;
        }
        if (mLayerDrawable == null) {
            if (DEBUG)
                Log.v(TAG, "onActivityStart " + this + " released state, syncing with service");
            syncWithService();
        } else {
            if (DEBUG)
                Log.v(TAG, "onActivityStart " + this + " updating service color " + mBackgroundColor + " drawable "
                        + mBackgroundDrawable);
            mService.setColor(mBackgroundColor);
            mService.setDrawable(mBackgroundDrawable);
        }
    }

    void onResume() {
        if (DEBUG)
            Log.v(TAG, "onResume " + this);
        postChangeRunnable();
    }

    private void syncWithService() {
        int color = mService.getColor();
        Drawable drawable = mService.getDrawable();

        if (DEBUG)
            Log.v(TAG, "syncWithService color " + Integer.toHexString(color) + " drawable " + drawable);

        mBackgroundColor = color;
        mBackgroundDrawable = drawable == null ? null : drawable.getConstantState().newDrawable().mutate();

        updateImmediate();
    }

    private void lazyInit() {
        if (mLayerDrawable != null) {
            return;
        }

        LayerDrawable layerDrawable = (LayerDrawable) ContextCompat.getDrawable(mContext, R.drawable.lb_background)
                .mutate();
        mLayerDrawable = createOptimizedTranslucentLayerDrawable(layerDrawable);
        BackgroundHelper.setBackgroundPreservingAlpha(mBgView, mLayerDrawable);

        mLayerDrawable.clearDrawable(R.id.background_imageout, mContext);
        mLayerDrawable.updateDrawable(R.id.background_theme, getThemeDrawable());

        updateDimWrapper();
    }

    private void updateDimWrapper() {
        if (mDimDrawable == null) {
            mDimDrawable = getDefaultDimLayer();
        }
        Drawable dimDrawable = mDimDrawable.getConstantState().newDrawable(mContext.getResources()).mutate();
        if (mLayerDrawable != null) {
            mLayerDrawable.updateDrawable(R.id.background_dim, dimDrawable);
        }
    }

    /**
     * Makes the background visible on the given Window.  The background manager must be attached
     * when the background is set.
     */
    public void attach(Window window) {
        if (USE_SEPARATE_WINDOW) {
            attachBehindWindow(window);
        } else {
            attachToView(window.getDecorView());
        }
    }

    /**
     * Sets the resource id for the drawable to be shown when there is no background set.
     * Overrides the window background drawable from the theme. This should
     * be called before attaching.
     */
    public void setThemeDrawableResourceId(int resourceId) {
        mThemeDrawableResourceId = resourceId;
    }

    private void attachBehindWindow(Window window) {
        if (DEBUG)
            Log.v(TAG, "attachBehindWindow " + window);
        mWindow = window;
        mWindowManager = window.getWindowManager();

        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                // Media window sits behind the main application window
                WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA,
                // Avoid default to software format RGBA
                WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, android.graphics.PixelFormat.TRANSLUCENT);
        params.setTitle(WINDOW_NAME);
        params.width = ViewGroup.LayoutParams.MATCH_PARENT;
        params.height = ViewGroup.LayoutParams.MATCH_PARENT;

        View backgroundView = LayoutInflater.from(mContext).inflate(R.layout.lb_background_window, null);
        mWindowManager.addView(backgroundView, params);

        attachToView(backgroundView);
    }

    private void attachToView(View sceneRoot) {
        mBgView = sceneRoot;
        mAttached = true;
        syncWithService();
    }

    /**
     * Returns true if the background manager is currently attached; false otherwise.
     */
    public boolean isAttached() {
        return mAttached;
    }

    /**
     * Release references to Drawables and put the BackgroundManager into the
     * detached state. Called when the associated Activity is destroyed.
     * @hide
     */
    void detach() {
        if (DEBUG)
            Log.v(TAG, "detach " + this);
        release();

        if (mWindowManager != null && mBgView != null) {
            mWindowManager.removeViewImmediate(mBgView);
        }

        mWindowManager = null;
        mWindow = null;
        mBgView = null;
        mAttached = false;

        if (mService != null) {
            mService.unref();
            mService = null;
        }
    }

    /**
     * Release references to Drawables. Typically called to reduce memory
     * overhead when not visible.
     * <p>
     * When an Activity is started, if the BackgroundManager has not been
     * released, the continuity service is updated from the BackgroundManager
     * state. If the BackgroundManager was released, the BackgroundManager
     * inherits the current state from the continuity service.
     */
    public void release() {
        if (DEBUG)
            Log.v(TAG, "release " + this);
        if (mLayerDrawable != null) {
            mLayerDrawable.clearDrawable(R.id.background_imagein, mContext);
            mLayerDrawable.clearDrawable(R.id.background_imageout, mContext);
            mLayerDrawable.clearDrawable(R.id.background_theme, mContext);
            mLayerDrawable = null;
        }
        if (mChangeRunnable != null) {
            mHandler.removeCallbacks(mChangeRunnable);
            mChangeRunnable = null;
        }
        releaseBackgroundBitmap();
    }

    private void releaseBackgroundBitmap() {
        mBackgroundDrawable = null;
    }

    private void setBackgroundDrawable(Drawable drawable) {
        mBackgroundDrawable = drawable;
        mService.setDrawable(mBackgroundDrawable);
    }

    /**
     * Sets the drawable used as a dim layer.
     */
    public void setDimLayer(Drawable drawable) {
        mDimDrawable = drawable;
        updateDimWrapper();
    }

    /**
     * Returns the drawable used as a dim layer.
     */
    public Drawable getDimLayer() {
        return mDimDrawable;
    }

    /**
     * Returns the default drawable used as a dim layer.
     */
    public Drawable getDefaultDimLayer() {
        return ContextCompat.getDrawable(mContext, R.color.lb_background_protection);
    }

    private void postChangeRunnable() {
        if (mChangeRunnable == null || !mChangeRunnablePending) {
            return;
        }

        // Postpone a pending change runnable until: no existing change animation in progress &&
        // activity is resumed (in the foreground) && layerdrawable fully opaque.
        // If the layerdrawable is translucent then an activity transition is in progress
        // and we want to use the optimized drawing path for performance reasons (see
        // OptimizedTranslucentLayerDrawable).
        if (mAnimator.isStarted()) {
            if (DEBUG)
                Log.v(TAG, "animation in progress");
        } else if (!mFragmentState.isResumed()) {
            if (DEBUG)
                Log.v(TAG, "not resumed");
        } else if (mLayerDrawable.getAlpha() < FULL_ALPHA) {
            if (DEBUG)
                Log.v(TAG, "in transition, alpha " + mLayerDrawable.getAlpha());
        } else {
            long delayMs = getRunnableDelay();
            if (DEBUG)
                Log.v(TAG, "posting runnable delayMs " + delayMs);
            mLastSetTime = System.currentTimeMillis();
            mHandler.postDelayed(mChangeRunnable, delayMs);
            mChangeRunnablePending = false;
        }
    }

    private void updateImmediate() {
        lazyInit();

        DrawableWrapper colorWrapper = getColorWrapper();
        if (colorWrapper != null) {
            colorWrapper.setColor(mBackgroundColor);
        }
        DrawableWrapper dimWrapper = getDimWrapper();
        if (dimWrapper != null) {
            dimWrapper.setAlpha(mBackgroundColor == Color.TRANSPARENT ? 0 : DIM_ALPHA_ON_SOLID);
        }
        showWallpaper(mBackgroundColor == Color.TRANSPARENT);

        if (mBackgroundDrawable == null) {
            mLayerDrawable.clearDrawable(R.id.background_imagein, mContext);
        } else {
            if (DEBUG)
                Log.v(TAG, "Background drawable is available");
            mLayerDrawable.updateDrawable(R.id.background_imagein, mBackgroundDrawable);
            if (dimWrapper != null) {
                dimWrapper.setAlpha(FULL_ALPHA);
            }
        }
    }

    /**
     * Sets the background to the given color. The timing for when this becomes
     * visible in the app is undefined and may take place after a small delay.
     */
    public void setColor(@ColorInt int color) {
        if (DEBUG)
            Log.v(TAG, "setColor " + Integer.toHexString(color));

        mBackgroundColor = color;
        mService.setColor(mBackgroundColor);

        DrawableWrapper colorWrapper = getColorWrapper();
        if (colorWrapper != null) {
            colorWrapper.setColor(mBackgroundColor);
        }
    }

    /**
     * Sets the given drawable into the background. The provided Drawable will be
     * used unmodified as the background, without any scaling or cropping
     * applied to it. The timing for when this becomes visible in the app is
     * undefined and may take place after a small delay.
     */
    public void setDrawable(Drawable drawable) {
        if (DEBUG)
            Log.v(TAG, "setBackgroundDrawable " + drawable);
        setDrawableInternal(drawable);
    }

    private void setDrawableInternal(Drawable drawable) {
        if (!mAttached) {
            throw new IllegalStateException("Must attach before setting background drawable");
        }

        if (mChangeRunnable != null) {
            if (sameDrawable(drawable, mChangeRunnable.mDrawable)) {
                if (DEBUG)
                    Log.v(TAG, "new drawable same as pending");
                return;
            }
            mHandler.removeCallbacks(mChangeRunnable);
            mChangeRunnable = null;
        }

        // If layer drawable is null then the activity hasn't started yet.
        // If the layer drawable alpha is zero then the activity transition hasn't started yet.
        // In these cases we can update the background immediately and let activity transition
        // fade it in.
        if (mLayerDrawable == null || mLayerDrawable.getAlpha() == 0) {
            if (DEBUG)
                Log.v(TAG, "setDrawableInternal null or alpha is zero");
            setBackgroundDrawable(drawable);
            updateImmediate();
            return;
        }

        mChangeRunnable = new ChangeBackgroundRunnable(drawable);
        mChangeRunnablePending = true;

        postChangeRunnable();
    }

    private long getRunnableDelay() {
        return Math.max(0, mLastSetTime + CHANGE_BG_DELAY_MS - System.currentTimeMillis());
    }

    /**
     * Sets the given bitmap into the background. When using setBitmap to set the
     * background, the provided bitmap will be scaled and cropped to correctly
     * fit within the dimensions of the view. The timing for when this becomes
     * visible in the app is undefined and may take place after a small delay.
     */
    public void setBitmap(Bitmap bitmap) {
        if (DEBUG) {
            Log.v(TAG, "setBitmap " + bitmap);
        }

        if (bitmap == null) {
            setDrawableInternal(null);
            return;
        }

        if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
            if (DEBUG) {
                Log.v(TAG, "invalid bitmap width or height");
            }
            return;
        }

        Matrix matrix = null;

        if ((bitmap.getWidth() != mWidthPx || bitmap.getHeight() != mHeightPx)) {
            int dwidth = bitmap.getWidth();
            int dheight = bitmap.getHeight();
            float scale;

            // Scale proportionately to fit width and height.
            if (dwidth * mHeightPx > mWidthPx * dheight) {
                scale = (float) mHeightPx / (float) dheight;
            } else {
                scale = (float) mWidthPx / (float) dwidth;
            }

            int subX = Math.min((int) (mWidthPx / scale), dwidth);
            int dx = Math.max(0, (dwidth - subX) / 2);

            matrix = new Matrix();
            matrix.setScale(scale, scale);
            matrix.preTranslate(-dx, 0);

            if (DEBUG)
                Log.v(TAG, "original image size " + bitmap.getWidth() + "x" + bitmap.getHeight() + " scale " + scale
                        + " dx " + dx);
        }

        BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap, matrix);

        setDrawableInternal(bitmapDrawable);
    }

    private void applyBackgroundChanges() {
        if (!mAttached) {
            return;
        }

        if (DEBUG)
            Log.v(TAG, "applyBackgroundChanges drawable " + mBackgroundDrawable);

        int dimAlpha = -1;

        if (getImageOutWrapper() != null) {
            dimAlpha = mBackgroundColor == Color.TRANSPARENT ? 0 : DIM_ALPHA_ON_SOLID;
        }

        DrawableWrapper imageInWrapper = getImageInWrapper();
        if (imageInWrapper == null && mBackgroundDrawable != null) {
            if (DEBUG)
                Log.v(TAG, "creating new imagein drawable");
            imageInWrapper = mLayerDrawable.updateDrawable(R.id.background_imagein, mBackgroundDrawable);
            if (DEBUG)
                Log.v(TAG, "imageInWrapper animation starting");
            imageInWrapper.setAlpha(0);
            dimAlpha = FULL_ALPHA;
        }

        mAnimator.setDuration(FADE_DURATION);
        mAnimator.start();

        DrawableWrapper dimWrapper = getDimWrapper();
        if (dimWrapper != null && dimAlpha >= 0) {
            if (DEBUG)
                Log.v(TAG, "dimwrapper animation starting to " + dimAlpha);
            mDimAnimator.cancel();
            mDimAnimator.setIntValues(dimWrapper.getAlpha(), dimAlpha);
            mDimAnimator.setDuration(FADE_DURATION);
            mDimAnimator
                    .setInterpolator(dimAlpha == FULL_ALPHA ? mDecelerateInterpolator : mAccelerateInterpolator);
            mDimAnimator.start();
        }
    }

    /**
     * Returns the current background color.
     */
    @ColorInt
    public final int getColor() {
        return mBackgroundColor;
    }

    /**
     * Returns the current background {@link Drawable}.
     */
    public Drawable getDrawable() {
        return mBackgroundDrawable;
    }

    private boolean sameDrawable(Drawable first, Drawable second) {
        if (first == null || second == null) {
            return false;
        }
        if (first == second) {
            return true;
        }
        if (first instanceof BitmapDrawable && second instanceof BitmapDrawable) {
            if (((BitmapDrawable) first).getBitmap().sameAs(((BitmapDrawable) second).getBitmap())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Task which changes the background.
     */
    class ChangeBackgroundRunnable implements Runnable {
        private Drawable mDrawable;

        ChangeBackgroundRunnable(Drawable drawable) {
            mDrawable = drawable;
        }

        @Override
        public void run() {
            runTask();
            mChangeRunnable = null;
        }

        private void runTask() {
            if (mLayerDrawable == null) {
                if (DEBUG)
                    Log.v(TAG, "runTask while released - should not happen");
                return;
            }

            if (sameDrawable(mDrawable, mBackgroundDrawable)) {
                if (DEBUG)
                    Log.v(TAG, "new drawable same as current");
                return;
            }

            releaseBackgroundBitmap();

            DrawableWrapper imageInWrapper = getImageInWrapper();
            if (imageInWrapper != null) {
                if (DEBUG)
                    Log.v(TAG, "moving image in to image out");
                // Order is important! Setting a drawable "removes" the
                // previous one from the view
                mLayerDrawable.clearDrawable(R.id.background_imagein, mContext);
                mLayerDrawable.updateDrawable(R.id.background_imageout, imageInWrapper.getDrawable());
            }

            setBackgroundDrawable(mDrawable);
            applyBackgroundChanges();
        }
    }

    private static Drawable createEmptyDrawable(Context context) {
        Bitmap bitmap = null;
        return new BitmapDrawable(context.getResources(), bitmap);
    }

    private void showWallpaper(boolean show) {
        if (mWindow == null) {
            return;
        }

        WindowManager.LayoutParams layoutParams = mWindow.getAttributes();
        if (show) {
            if ((layoutParams.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) {
                return;
            }
            if (DEBUG)
                Log.v(TAG, "showing wallpaper");
            layoutParams.flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
        } else {
            if ((layoutParams.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) == 0) {
                return;
            }
            if (DEBUG)
                Log.v(TAG, "hiding wallpaper");
            layoutParams.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
        }

        mWindow.setAttributes(layoutParams);
    }
}