public final class

BackgroundManager

extends java.lang.Object

 java.lang.Object

↳androidx.leanback.app.BackgroundManager

Gradle dependencies

compile group: 'androidx.leanback', name: 'leanback', version: '1.2.0-alpha04'

  • groupId: androidx.leanback
  • artifactId: leanback
  • version: 1.2.0-alpha04

Artifact androidx.leanback:leanback:1.2.0-alpha04 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.leanback:leanback com.android.support:leanback-v17

Androidx class mapping:

androidx.leanback.app.BackgroundManager android.support.v17.leanback.app.BackgroundManager

Overview

Supports background image continuity between multiple Activities.

An Activity should instantiate a BackgroundManager and BackgroundManager.attach(Window) 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.

At some point, for example when it is stopped, the Activity may release its background state.

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.

When the last Activity is destroyed, the background state is reset.

Backgrounds consist of several layers, from back to front:

BackgroundManager holds references to potentially large bitmap Drawables. Call BackgroundManager.release() to release these references when the Activity is not visible.

Summary

Methods
public voidattach(Window window)

Makes the background visible on the given Window.

public voidattachToView(View sceneRoot)

Adds the composite drawable to the given view.

public voidclearDrawable()

Clears the Drawable set by BackgroundManager.setDrawable(Drawable) or BackgroundManager.setBitmap(Bitmap).

public final intgetColor()

Returns the current background color.

public DrawablegetDefaultDimLayer()

Returns the default drawable used as a dim layer.

public DrawablegetDimLayer()

Returns the drawable used as a dim layer.

public DrawablegetDrawable()

Returns the current background Drawable.

public static BackgroundManagergetInstance(Activity activity)

Returns the BackgroundManager associated with the given Activity.

public booleanisAttached()

Returns true if the background manager is currently attached; false otherwise.

public booleanisAutoReleaseOnStop()

public voidrelease()

Release references to Drawable/Bitmap.

public voidsetAutoReleaseOnStop(boolean autoReleaseOnStop)

Enable or disable call release() in Activity onStop().

public voidsetBitmap(Bitmap bitmap)

Sets the given bitmap into the background.

public voidsetColor(int color)

Sets the background to the given color.

public voidsetDimLayer(Drawable drawable)

Sets the drawable used as a dim layer.

public voidsetDrawable(Drawable drawable)

Sets the given drawable into the background.

public voidsetThemeDrawableResourceId(int resourceId)

Sets the resource id for the drawable to be shown when there is no background set.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Methods

public static BackgroundManager getInstance(Activity activity)

Returns the BackgroundManager associated with the given Activity.

The BackgroundManager will be created on-demand for each individual Activity. Subsequent calls will return the same BackgroundManager created for this Activity.

public void attach(Window window)

Makes the background visible on the given Window. The background manager must be attached when the background is set.

public void setThemeDrawableResourceId(int resourceId)

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 attachToView(View sceneRoot)

Adds the composite drawable to the given view.

public boolean isAttached()

Returns true if the background manager is currently attached; false otherwise.

public void release()

Release references to Drawable/Bitmap. Typically called in Activity onStop() to reduce memory overhead when not visible. It's app's responsibility to restore the drawable/bitmap in Activity onStart(). The method is automatically called in onStop() when BackgroundManager.isAutoReleaseOnStop() is true.

See also: BackgroundManager.setAutoReleaseOnStop(boolean)

public void setDimLayer(Drawable drawable)

Deprecated: No longer support dim layer.

Sets the drawable used as a dim layer.

public Drawable getDimLayer()

Deprecated: No longer support dim layer.

Returns the drawable used as a dim layer.

public Drawable getDefaultDimLayer()

Deprecated: No longer support dim layer.

Returns the default drawable used as a dim layer.

public void setColor(int color)

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 setDrawable(Drawable drawable)

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 clearDrawable()

Clears the Drawable set by BackgroundManager.setDrawable(Drawable) or BackgroundManager.setBitmap(Bitmap). BackgroundManager will show a solid color set by BackgroundManager.setColor(int) or theme drawable if color is not provided.

public void setBitmap(Bitmap bitmap)

Sets the given bitmap into the background. When using setCoverImageBitmap 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 setAutoReleaseOnStop(boolean autoReleaseOnStop)

Enable or disable call release() in Activity onStop(). Default is true.

Parameters:

autoReleaseOnStop: True to call release() in Activity onStop(), false otherwise.

public boolean isAutoReleaseOnStop()

Returns:

True if release() in Activity.onStop(), false otherwise.

public final int getColor()

Returns the current background color.

public Drawable getDrawable()

Returns the current background Drawable.

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 androidx.leanback.app;

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.PixelFormat;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.view.animation.Interpolator;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
import androidx.leanback.R;
import androidx.leanback.widget.BackgroundHelper;

import java.lang.ref.WeakReference;

/**
 * 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.
@SuppressWarnings("ObjectToString")
public final class BackgroundManager {

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

    static final int FULL_ALPHA = 255;
    private static final int CHANGE_BG_DELAY_MS = 500;
    private static final int FADE_DURATION = 500;

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

    Activity mContext;
    Handler mHandler;
    private View mBgView;
    private BackgroundContinuityService mService;
    private int mThemeDrawableResourceId;
    private BackgroundFragment mFragmentState;
    private boolean mAutoReleaseOnStop = true;

    private int mHeightPx;
    private int mWidthPx;
    int mBackgroundColor;
    Drawable mBackgroundDrawable;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    boolean mAttached;
    private long mLastSetTime;

    final ValueAnimator mAnimator;

    static class BitmapDrawable extends Drawable {

        static final class ConstantState extends Drawable.ConstantState {
            final Bitmap mBitmap;
            final Matrix mMatrix;
            final Paint mPaint = new Paint();

            ConstantState(Bitmap bitmap, Matrix matrix) {
                mBitmap = bitmap;
                mMatrix = matrix != null ? matrix : new Matrix();
                mPaint.setFilterBitmap(true);
            }

            ConstantState(ConstantState copyFrom) {
                mBitmap = copyFrom.mBitmap;
                mMatrix = copyFrom.mMatrix != null ? new Matrix(copyFrom.mMatrix) : new Matrix();
                if (copyFrom.mPaint.getAlpha() != FULL_ALPHA) {
                    mPaint.setAlpha(copyFrom.mPaint.getAlpha());
                }
                if (copyFrom.mPaint.getColorFilter() != null) {
                    mPaint.setColorFilter(copyFrom.mPaint.getColorFilter());
                }
                mPaint.setFilterBitmap(true);
            }

            @Override
            public Drawable newDrawable() {
                return new BitmapDrawable(this);
            }

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

        ConstantState mState;
        boolean mMutated;

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

        BitmapDrawable(Resources resources, Bitmap bitmap, Matrix matrix) {
            mState = new ConstantState(bitmap, matrix);
        }

        BitmapDrawable(ConstantState state) {
            mState = state;
        }

        Bitmap getBitmap() {
            return mState.mBitmap;
        }

        @Override
        public void draw(@NonNull 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) {
            mutate();
            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) {
            mutate();
            mState.mPaint.setColorFilter(cf);
            invalidateSelf();
        }

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

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

        @NonNull
        @Override
        public Drawable mutate() {
            if (!mMutated) {
                mMutated = true;
                mState = new ConstantState(mState);
            }
            return this;
        }
    }

    static final class DrawableWrapper {
        int mAlpha = FULL_ALPHA;
        final Drawable mDrawable;

        public DrawableWrapper(Drawable drawable) {
            mDrawable = drawable;
        }
        public DrawableWrapper(DrawableWrapper wrapper, Drawable drawable) {
            mDrawable = drawable;
            mAlpha = wrapper.mAlpha;
        }

        public Drawable getDrawable() {
            return mDrawable;
        }

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

    static final class TranslucentLayerDrawable extends LayerDrawable {
        DrawableWrapper[] mWrapper;
        int mAlpha = FULL_ALPHA;
        boolean mSuspendInvalidation;
        WeakReference<BackgroundManager> mManagerWeakReference;

        TranslucentLayerDrawable(BackgroundManager manager, Drawable[] drawables) {
            super(drawables);
            mManagerWeakReference = new WeakReference<>(manager);
            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 (mAlpha != alpha) {
                mAlpha = alpha;
                invalidateSelf();
                BackgroundManager manager = mManagerWeakReference.get();
                if (manager != null) {
                    manager.postChangeRunnable();
                }
            }
        }

        void setWrapperAlpha(int wrapperIndex, int alpha) {
            if (mWrapper[wrapperIndex] != null) {
                mWrapper[wrapperIndex].mAlpha = alpha;
                invalidateSelf();
            }
        }

        // Queried by system transitions
        @Override
        public int getAlpha() {
            return mAlpha;
        }

        @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));
                }
            }
            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;
                    if (!(getDrawable(i) instanceof EmptyDrawable)) {
                        super.setDrawableByLayerId(id, createEmptyDrawable(context));
                    }
                    break;
                }
            }
        }

        public int findWrapperIndexById(int id) {
            for (int i = 0; i < getNumberOfLayers(); i++) {
                if (getId(i) == id) {
                    return i;
                }
            }
            return -1;
        }

        @Override
        public void invalidateDrawable(Drawable who) {
            // Prevent invalidate when temporarily change child drawable's alpha in draw()
            if (!mSuspendInvalidation) {
                super.invalidateDrawable(who);
            }
        }

        @Override
        public void draw(@NonNull Canvas canvas) {
            for (int i = 0; i < mWrapper.length; i++) {
                final Drawable d;
                // For each child drawable, we multiple Wrapper's alpha and LayerDrawable's alpha
                // temporarily using mSuspendInvalidation to suppress invalidate event.
                if (mWrapper[i] != null && (d = mWrapper[i].getDrawable()) != null) {
                    int alpha = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
                            ? DrawableCompat.getAlpha(d) : FULL_ALPHA;
                    final int savedAlpha = alpha;
                    int multiple = 0;
                    if (mAlpha < FULL_ALPHA) {
                        alpha = alpha * mAlpha;
                        multiple++;
                    }
                    if (mWrapper[i].mAlpha < FULL_ALPHA) {
                        alpha = alpha * mWrapper[i].mAlpha;
                        multiple++;
                    }
                    if (multiple == 0) {
                        d.draw(canvas);
                    } else {
                        if (multiple == 1) {
                            alpha = alpha / FULL_ALPHA;
                        } else if (multiple == 2) {
                            alpha = alpha / (FULL_ALPHA * FULL_ALPHA);
                        }
                        try {
                            mSuspendInvalidation = true;
                            d.setAlpha(alpha);
                            d.draw(canvas);
                            d.setAlpha(savedAlpha);
                        } finally {
                            mSuspendInvalidation = false;
                        }
                    }
                }
            }
        }
    }

    TranslucentLayerDrawable createTranslucentLayerDrawable(
            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 TranslucentLayerDrawable(this, drawables);
        for (int i = 0; i < numChildren; i++) {
            result.setId(i, layerDrawable.getId(i));
        }
        return result;
    }

    TranslucentLayerDrawable mLayerDrawable;
    int mImageInWrapperIndex;
    int mImageOutWrapperIndex;
    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();
            if (mImageInWrapperIndex != -1) {
                mLayerDrawable.setWrapperAlpha(mImageInWrapperIndex, fadeInAlpha);
            }
        }
    };

    /**
     * Shared memory continuity service.
     */
    @SuppressWarnings("ObjectToString")
    private static class BackgroundContinuityService {
        private static final String TAG = "BackgroundContinuity";
        private static final 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;
            mDrawable = null;
        }
        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;
        }
    }

    Drawable getDefaultDrawable() {
        if (mBackgroundColor != Color.TRANSPARENT) {
            return new ColorDrawable(mBackgroundColor);
        } else {
            return getThemeDrawable();
        }
    }

    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) {
        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);
    }

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

        Interpolator defaultInterpolator = new FastOutLinearInInterpolator();

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

        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();

        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;
    }

    DrawableWrapper getImageInWrapper() {
        return mLayerDrawable == null
                ? null : mLayerDrawable.mWrapper[mImageInWrapperIndex];
    }

    DrawableWrapper getImageOutWrapper() {
        return mLayerDrawable == null
                ? null : mLayerDrawable.mWrapper[mImageOutWrapperIndex];
    }

    /**
     * Synchronizes state when the owning Activity is started.
     * At that point the view becomes visible.
     */
    void onActivityStart() {
        updateImmediate();
    }

    void onStop() {
        if (isAutoReleaseOnStop()) {
            release();
        }
    }

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

    /**
     * Makes the background visible on the given Window. The background manager must be attached
     * when the background is set.
     */
    public void attach(Window window) {
        attachToViewInternal(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;
    }

    /**
     * Adds the composite drawable to the given view.
     */
    public void attachToView(View sceneRoot) {
        attachToViewInternal(sceneRoot);
        // clear background to reduce overdraw since the View will act as background.
        // Activity transition below O has ghost effect for null window background where we
        // need set a transparent background to force redraw the whole window.
        mContext.getWindow().getDecorView().setBackground(
                Build.VERSION.SDK_INT >= 26 ? null : new ColorDrawable(Color.TRANSPARENT));
    }

    void attachToViewInternal(View sceneRoot) {
        if (mAttached) {
            throw new IllegalStateException("Already attached to " + mBgView);
        }
        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.
     */
    void detach() {
        if (DEBUG) Log.v(TAG, "detach " + this);
        release();

        mBgView = null;
        mAttached = false;

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

    /**
     * Release references to Drawable/Bitmap. Typically called in Activity onStop() to reduce memory
     * overhead when not visible. It's app's responsibility to restore the drawable/bitmap in
     * Activity onStart(). The method is automatically called in onStop() when
     * {@link #isAutoReleaseOnStop()} is true.
     * @see #setAutoReleaseOnStop(boolean)
     */
    public void release() {
        if (DEBUG) Log.v(TAG, "release " + this);
        if (mChangeRunnable != null) {
            mHandler.removeCallbacks(mChangeRunnable);
            mChangeRunnable = null;
        }
        if (mAnimator.isStarted()) {
            mAnimator.cancel();
        }
        if (mLayerDrawable != null) {
            mLayerDrawable.clearDrawable(R.id.background_imagein, mContext);
            mLayerDrawable.clearDrawable(R.id.background_imageout, mContext);
            mLayerDrawable = null;
        }
        mBackgroundDrawable = null;
    }

    /**
     * Sets the drawable used as a dim layer.
     * @deprecated No longer support dim layer.
     */
    @Deprecated
    public void setDimLayer(Drawable drawable) {
    }

    /**
     * Returns the drawable used as a dim layer.
     * @deprecated No longer support dim layer.
     */
    @Deprecated
    public Drawable getDimLayer() {
        return null;
    }

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

    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 lazyInit() {
        if (mLayerDrawable != null) {
            return;
        }

        LayerDrawable layerDrawable = (LayerDrawable)
                ContextCompat.getDrawable(mContext, R.drawable.lb_background).mutate();
        mLayerDrawable = createTranslucentLayerDrawable(layerDrawable);
        mImageInWrapperIndex = mLayerDrawable.findWrapperIndexById(R.id.background_imagein);
        mImageOutWrapperIndex = mLayerDrawable.findWrapperIndexById(R.id.background_imageout);
        BackgroundHelper.setBackgroundPreservingAlpha(mBgView, mLayerDrawable);
    }

    private void updateImmediate() {
        if (!mAttached) {
            return;
        }
        lazyInit();

        if (mBackgroundDrawable == null) {
            if (DEBUG) Log.v(TAG, "Use defefault background");
            mLayerDrawable.updateDrawable(R.id.background_imagein, getDefaultDrawable());
        } else {
            if (DEBUG) Log.v(TAG, "Background drawable is available " + mBackgroundDrawable);
            mLayerDrawable.updateDrawable(R.id.background_imagein, mBackgroundDrawable);
        }
        mLayerDrawable.clearDrawable(R.id.background_imageout, mContext);
    }

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

        mService.setColor(color);
        mBackgroundColor = color;
        mBackgroundDrawable = null;
        if (mLayerDrawable == null) {
            return;
        }
        setDrawableInternal(getDefaultDrawable());
    }

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

        mService.setDrawable(drawable);
        mBackgroundDrawable = drawable;
        if (mLayerDrawable == null) {
            return;
        }
        if (drawable == null) {
            setDrawableInternal(getDefaultDrawable());
        } else {
            setDrawableInternal(drawable);
        }
    }

    /**
     * Clears the Drawable set by {@link #setDrawable(Drawable)} or {@link #setBitmap(Bitmap)}.
     * BackgroundManager will show a solid color set by {@link #setColor(int)} or theme drawable
     * if color is not provided.
     */
    public void clearDrawable() {
        setDrawable(null);
    }

    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;
        }

        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 setCoverImageBitmap 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, "setCoverImageBitmap " + bitmap);
        }

        if (bitmap == null) {
            setDrawable(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);

        setDrawable(bitmapDrawable);
    }

    /**
     * Enable or disable call release() in Activity onStop(). Default is true.
     * @param autoReleaseOnStop True to call release() in Activity onStop(), false otherwise.
     */
    public void setAutoReleaseOnStop(boolean autoReleaseOnStop) {
        mAutoReleaseOnStop = autoReleaseOnStop;
    }

    /**
     * @return True if release() in Activity.onStop(), false otherwise.
     */
    public boolean isAutoReleaseOnStop() {
        return mAutoReleaseOnStop;
    }

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

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

    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;
            }
        }
        if (first instanceof ColorDrawable && second instanceof ColorDrawable) {
            if (((ColorDrawable) first).getColor() == ((ColorDrawable) second).getColor()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Task which changes the background.
     */
    final class ChangeBackgroundRunnable implements Runnable {
        final 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;
            }

            DrawableWrapper imageInWrapper = getImageInWrapper();
            if (imageInWrapper != null) {
                if (sameDrawable(mDrawable, imageInWrapper.getDrawable())) {
                    if (DEBUG) Log.v(TAG, "new drawable same as current");
                    return;
                }

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

            applyBackgroundChanges();
        }

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

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

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

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

        }

    }

    static class EmptyDrawable extends BitmapDrawable {
        EmptyDrawable(Resources res) {
            super(res, (Bitmap) null);
        }
    }

    static Drawable createEmptyDrawable(Context context) {
        return new EmptyDrawable(context.getResources());
    }

}