1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.app;
15
16import java.lang.ref.WeakReference;
17
18import android.support.annotation.ColorInt;
19import android.graphics.PixelFormat;
20import android.graphics.PorterDuff;
21import android.graphics.PorterDuffColorFilter;
22import android.support.v17.leanback.R;
23import android.animation.Animator;
24import android.animation.ValueAnimator;
25import android.app.Activity;
26import android.content.Context;
27import android.content.res.Resources;
28import android.content.res.TypedArray;
29import android.graphics.Bitmap;
30import android.graphics.Canvas;
31import android.graphics.Color;
32import android.graphics.ColorFilter;
33import android.graphics.Matrix;
34import android.graphics.Paint;
35import android.graphics.drawable.ColorDrawable;
36import android.graphics.drawable.Drawable;
37import android.graphics.drawable.LayerDrawable;
38import android.os.Handler;
39import android.support.v17.leanback.widget.BackgroundHelper;
40import android.support.v4.view.animation.FastOutLinearInInterpolator;
41import android.util.Log;
42import android.view.LayoutInflater;
43import android.view.View;
44import android.view.ViewGroup;
45import android.view.Window;
46import android.view.WindowManager;
47import android.view.animation.Interpolator;
48import android.view.animation.AnimationUtils;
49import android.support.v4.app.FragmentActivity;
50import android.support.v4.content.ContextCompat;
51
52/**
53 * Supports background image continuity between multiple Activities.
54 *
55 * <p>An Activity should instantiate a BackgroundManager and {@link #attach}
56 * to the Activity's window.  When the Activity is started, the background is
57 * initialized to the current background values stored in a continuity service.
58 * The background continuity service is updated as the background is updated.
59 *
60 * <p>At some point, for example when it is stopped, the Activity may release
61 * its background state.
62 *
63 * <p>When an Activity is resumed, if the BackgroundManager has not been
64 * released, the continuity service is updated from the BackgroundManager state.
65 * If the BackgroundManager was released, the BackgroundManager inherits the
66 * current state from the continuity service.
67 *
68 * <p>When the last Activity is destroyed, the background state is reset.
69 *
70 * <p>Backgrounds consist of several layers, from back to front:
71 * <ul>
72 *   <li>the background Drawable of the theme</li>
73 *   <li>a solid color (set via {@link #setColor})</li>
74 *   <li>two Drawables, previous and current (set via {@link #setBitmap} or
75 *   {@link #setDrawable}), which may be in transition</li>
76 * </ul>
77 *
78 * <p>BackgroundManager holds references to potentially large bitmap Drawables.
79 * Call {@link #release} to release these references when the Activity is not
80 * visible.
81 */
82// TODO: support for multiple app processes requires a proper android service
83// instead of the shared memory "service" implemented here. Such a service could
84// support continuity between fragments of different applications if desired.
85public final class BackgroundManager {
86
87    interface FragmentStateQueriable {
88        public boolean isResumed();
89    }
90
91    private static final String TAG = "BackgroundManager";
92    private static final boolean DEBUG = false;
93
94    private static final int FULL_ALPHA = 255;
95    private static final int DIM_ALPHA_ON_SOLID = (int) (0.8f * FULL_ALPHA);
96    private static final int CHANGE_BG_DELAY_MS = 500;
97    private static final int FADE_DURATION = 500;
98
99    /**
100     * Using a separate window for backgrounds can improve graphics performance by
101     * leveraging hardware display layers.
102     * TODO: support a leanback configuration option.
103     */
104    private static final boolean USE_SEPARATE_WINDOW = false;
105
106    private static final String WINDOW_NAME = "BackgroundManager";
107    private static final String FRAGMENT_TAG = BackgroundManager.class.getCanonicalName();
108
109    private Context mContext;
110    private Handler mHandler;
111    private Window mWindow;
112    private WindowManager mWindowManager;
113    private View mBgView;
114    private BackgroundContinuityService mService;
115    private int mThemeDrawableResourceId;
116    private FragmentStateQueriable mFragmentState;
117
118    private int mHeightPx;
119    private int mWidthPx;
120    private Drawable mBackgroundDrawable;
121    private int mBackgroundColor;
122    private boolean mAttached;
123    private long mLastSetTime;
124
125    private final Interpolator mAccelerateInterpolator;
126    private final Interpolator mDecelerateInterpolator;
127    private final ValueAnimator mAnimator;
128    private final ValueAnimator mDimAnimator;
129
130    private static class BitmapDrawable extends Drawable {
131
132        static class ConstantState extends Drawable.ConstantState {
133            Bitmap mBitmap;
134            Matrix mMatrix;
135            Paint mPaint;
136
137            @Override
138            public Drawable newDrawable() {
139                return new BitmapDrawable(null, mBitmap, mMatrix);
140            }
141
142            @Override
143            public int getChangingConfigurations() {
144                return 0;
145            }
146        }
147
148        private ConstantState mState = new ConstantState();
149
150        BitmapDrawable(Resources resources, Bitmap bitmap) {
151            this(resources, bitmap, null);
152        }
153
154        BitmapDrawable(Resources resources, Bitmap bitmap, Matrix matrix) {
155            mState.mBitmap = bitmap;
156            mState.mMatrix = matrix != null ? matrix : new Matrix();
157            mState.mPaint = new Paint();
158            mState.mPaint.setFilterBitmap(true);
159        }
160
161        Bitmap getBitmap() {
162            return mState.mBitmap;
163        }
164
165        @Override
166        public void draw(Canvas canvas) {
167            if (mState.mBitmap == null) {
168                return;
169            }
170            if (mState.mPaint.getAlpha() < FULL_ALPHA && mState.mPaint.getColorFilter() != null) {
171                throw new IllegalStateException("Can't draw with translucent alpha and color filter");
172            }
173            canvas.drawBitmap(mState.mBitmap, mState.mMatrix, mState.mPaint);
174        }
175
176        @Override
177        public int getOpacity() {
178            return android.graphics.PixelFormat.TRANSLUCENT;
179        }
180
181        @Override
182        public void setAlpha(int alpha) {
183            if (mState.mPaint.getAlpha() != alpha) {
184                mState.mPaint.setAlpha(alpha);
185                invalidateSelf();
186            }
187        }
188
189        /**
190         * Does not invalidateSelf to avoid recursion issues.
191         * Caller must ensure appropriate invalidation.
192         */
193        @Override
194        public void setColorFilter(ColorFilter cf) {
195            mState.mPaint.setColorFilter(cf);
196        }
197
198        public ColorFilter getColorFilter() {
199            return mState.mPaint.getColorFilter();
200        }
201
202        @Override
203        public ConstantState getConstantState() {
204            return mState;
205        }
206    }
207
208    private static class DrawableWrapper {
209        private int mAlpha = FULL_ALPHA;
210        private Drawable mDrawable;
211        private ColorFilter mColorFilter;
212
213        public DrawableWrapper(Drawable drawable) {
214            mDrawable = drawable;
215            updateAlpha();
216            updateColorFilter();
217        }
218        public DrawableWrapper(DrawableWrapper wrapper, Drawable drawable) {
219            mDrawable = drawable;
220            mAlpha = wrapper.getAlpha();
221            updateAlpha();
222            mColorFilter = wrapper.getColorFilter();
223            updateColorFilter();
224        }
225
226        public Drawable getDrawable() {
227            return mDrawable;
228        }
229        public void setAlpha(int alpha) {
230            mAlpha = alpha;
231            updateAlpha();
232        }
233        public int getAlpha() {
234            return mAlpha;
235        }
236        private void updateAlpha() {
237            mDrawable.setAlpha(mAlpha);
238        }
239
240        public ColorFilter getColorFilter() {
241            return mColorFilter;
242        }
243        public void setColorFilter(ColorFilter colorFilter) {
244            mColorFilter = colorFilter;
245            updateColorFilter();
246        }
247        private void updateColorFilter() {
248            mDrawable.setColorFilter(mColorFilter);
249        }
250
251        public void setColor(int color) {
252            ((ColorDrawable) mDrawable).setColor(color);
253        }
254    }
255
256    static class TranslucentLayerDrawable extends LayerDrawable {
257        private DrawableWrapper[] mWrapper;
258        private Paint mPaint = new Paint();
259
260        public TranslucentLayerDrawable(Drawable[] drawables) {
261            super(drawables);
262            int count = drawables.length;
263            mWrapper = new DrawableWrapper[count];
264            for (int i = 0; i < count; i++) {
265                mWrapper[i] = new DrawableWrapper(drawables[i]);
266            }
267        }
268
269        @Override
270        public void setAlpha(int alpha) {
271            if (mPaint.getAlpha() != alpha) {
272                int previousAlpha = mPaint.getAlpha();
273                mPaint.setAlpha(alpha);
274                invalidateSelf();
275                onAlphaChanged(previousAlpha, alpha);
276            }
277        }
278
279        // Queried by system transitions
280        public int getAlpha() {
281            return mPaint.getAlpha();
282        }
283
284        protected void onAlphaChanged(int oldAlpha, int newAlpha) {
285        }
286
287        @Override
288        public Drawable mutate() {
289            Drawable drawable = super.mutate();
290            int count = getNumberOfLayers();
291            for (int i = 0; i < count; i++) {
292                if (mWrapper[i] != null) {
293                    mWrapper[i] = new DrawableWrapper(mWrapper[i], getDrawable(i));
294                }
295            }
296            invalidateSelf();
297            return drawable;
298        }
299
300        @Override
301        public int getOpacity() {
302            return PixelFormat.TRANSLUCENT;
303        }
304
305        @Override
306        public boolean setDrawableByLayerId(int id, Drawable drawable) {
307            return updateDrawable(id, drawable) != null;
308        }
309
310        public DrawableWrapper updateDrawable(int id, Drawable drawable) {
311            super.setDrawableByLayerId(id, drawable);
312            for (int i = 0; i < getNumberOfLayers(); i++) {
313                if (getId(i) == id) {
314                    mWrapper[i] = new DrawableWrapper(drawable);
315                    // Must come after mWrapper was updated so it can be seen by updateColorFilter
316                    invalidateSelf();
317                    return mWrapper[i];
318                }
319            }
320            return null;
321        }
322
323        public void clearDrawable(int id, Context context) {
324            for (int i = 0; i < getNumberOfLayers(); i++) {
325                if (getId(i) == id) {
326                    mWrapper[i] = null;
327                    super.setDrawableByLayerId(id, createEmptyDrawable(context));
328                    break;
329                }
330            }
331        }
332
333        public DrawableWrapper findWrapperById(int id) {
334            for (int i = 0; i < getNumberOfLayers(); i++) {
335                if (getId(i) == id) {
336                    return mWrapper[i];
337                }
338            }
339            return null;
340        }
341
342        @Override
343        public void draw(Canvas canvas) {
344            if (mPaint.getAlpha() < FULL_ALPHA) {
345                canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(),
346                        mPaint, Canvas.ALL_SAVE_FLAG);
347            }
348            super.draw(canvas);
349            if (mPaint.getAlpha() < FULL_ALPHA) {
350                canvas.restore();
351            }
352        }
353    }
354
355    /**
356     * Optimizes drawing when the dim drawable is an alpha-only color and imagein is opaque.
357     * When the layer drawable is translucent (activity transition) then we can avoid the slow
358     * saveLayer/restore draw path.
359     */
360    private class OptimizedTranslucentLayerDrawable extends TranslucentLayerDrawable {
361        private PorterDuffColorFilter mColorFilter;
362        private boolean mUpdatingColorFilter;
363
364        public OptimizedTranslucentLayerDrawable(Drawable[] drawables) {
365            super(drawables);
366        }
367
368        @Override
369        protected void onAlphaChanged(int oldAlpha, int newAlpha) {
370            if (newAlpha == FULL_ALPHA && oldAlpha < FULL_ALPHA) {
371                if (DEBUG) Log.v(TAG, "transition complete");
372                postChangeRunnable();
373            }
374        }
375
376        @Override
377        public void invalidateSelf() {
378            super.invalidateSelf();
379            updateColorFilter();
380        }
381
382        @Override
383        public void invalidateDrawable(Drawable who) {
384            if (!mUpdatingColorFilter) {
385                invalidateSelf();
386            }
387        }
388
389        private void updateColorFilter() {
390            DrawableWrapper dimWrapper = findWrapperById(R.id.background_dim);
391            DrawableWrapper imageInWrapper = findWrapperById(R.id.background_imagein);
392            DrawableWrapper imageOutWrapper = findWrapperById(R.id.background_imageout);
393
394            mColorFilter = null;
395            if (imageInWrapper != null && imageInWrapper.getAlpha() == FULL_ALPHA &&
396                    dimWrapper.getDrawable() instanceof ColorDrawable) {
397                int dimColor = ((ColorDrawable) dimWrapper.getDrawable()).getColor();
398                if (Color.red(dimColor) == 0 &&
399                        Color.green(dimColor) == 0 &&
400                        Color.blue(dimColor) == 0) {
401                    int dimAlpha = 255 - Color.alpha(dimColor);
402                    int color = Color.argb(getAlpha(), dimAlpha, dimAlpha, dimAlpha);
403                    mColorFilter = new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY);
404                }
405            }
406            mUpdatingColorFilter = true;
407            if (imageInWrapper != null) {
408                imageInWrapper.setColorFilter(mColorFilter);
409            }
410            if (imageOutWrapper != null) {
411                imageOutWrapper.setColorFilter(null);
412            }
413            mUpdatingColorFilter = false;
414        }
415
416        @Override
417        public void draw(Canvas canvas) {
418            DrawableWrapper imageInWrapper = findWrapperById(R.id.background_imagein);
419            if (imageInWrapper != null && imageInWrapper.getDrawable() != null &&
420                    imageInWrapper.getColorFilter() != null) {
421                imageInWrapper.getDrawable().draw(canvas);
422            } else {
423                super.draw(canvas);
424            }
425        }
426    }
427
428    private TranslucentLayerDrawable createOptimizedTranslucentLayerDrawable(
429            LayerDrawable layerDrawable) {
430        int numChildren = layerDrawable.getNumberOfLayers();
431        Drawable[] drawables = new Drawable[numChildren];
432        for (int i = 0; i < numChildren; i++) {
433            drawables[i] = layerDrawable.getDrawable(i);
434        }
435        TranslucentLayerDrawable result = new OptimizedTranslucentLayerDrawable(drawables);
436        for (int i = 0; i < numChildren; i++) {
437            result.setId(i, layerDrawable.getId(i));
438        }
439        return result;
440    }
441
442    private TranslucentLayerDrawable mLayerDrawable;
443    private Drawable mDimDrawable;
444    private ChangeBackgroundRunnable mChangeRunnable;
445    private boolean mChangeRunnablePending;
446
447    private final Animator.AnimatorListener mAnimationListener = new Animator.AnimatorListener() {
448        final Runnable mRunnable = new Runnable() {
449            @Override
450            public void run() {
451                postChangeRunnable();
452            }
453        };
454
455        @Override
456        public void onAnimationStart(Animator animation) {
457        }
458        @Override
459        public void onAnimationRepeat(Animator animation) {
460        }
461        @Override
462        public void onAnimationEnd(Animator animation) {
463            if (mLayerDrawable != null) {
464                mLayerDrawable.clearDrawable(R.id.background_imageout, mContext);
465            }
466            mHandler.post(mRunnable);
467        }
468        @Override
469        public void onAnimationCancel(Animator animation) {
470        }
471    };
472
473    private final ValueAnimator.AnimatorUpdateListener mAnimationUpdateListener =
474            new ValueAnimator.AnimatorUpdateListener() {
475        @Override
476        public void onAnimationUpdate(ValueAnimator animation) {
477            int fadeInAlpha = (Integer) animation.getAnimatedValue();
478            DrawableWrapper imageInWrapper = getImageInWrapper();
479            if (imageInWrapper != null) {
480                imageInWrapper.setAlpha(fadeInAlpha);
481            } else {
482                DrawableWrapper imageOutWrapper = getImageOutWrapper();
483                if (imageOutWrapper != null) {
484                    imageOutWrapper.setAlpha(255 - fadeInAlpha);
485                }
486            }
487        }
488    };
489
490    private final ValueAnimator.AnimatorUpdateListener mDimUpdateListener =
491            new ValueAnimator.AnimatorUpdateListener() {
492        @Override
493        public void onAnimationUpdate(ValueAnimator animation) {
494            DrawableWrapper dimWrapper = getDimWrapper();
495            if (dimWrapper != null) {
496                dimWrapper.setAlpha((Integer) animation.getAnimatedValue());
497            }
498        }
499    };
500
501    /**
502     * Shared memory continuity service.
503     */
504    private static class BackgroundContinuityService {
505        private static final String TAG = "BackgroundContinuityService";
506        private static boolean DEBUG = BackgroundManager.DEBUG;
507
508        private static BackgroundContinuityService sService = new BackgroundContinuityService();
509
510        private int mColor;
511        private Drawable mDrawable;
512        private int mCount;
513
514        /** Single cache of theme drawable */
515        private int mLastThemeDrawableId;
516        private WeakReference<Drawable.ConstantState> mLastThemeDrawableState;
517
518        private BackgroundContinuityService() {
519            reset();
520        }
521
522        private void reset() {
523            mColor = Color.TRANSPARENT;
524            mDrawable = null;
525        }
526
527        public static BackgroundContinuityService getInstance() {
528            final int count = sService.mCount++;
529            if (DEBUG) Log.v(TAG, "Returning instance with new count " + count);
530            return sService;
531        }
532
533        public void unref() {
534            if (mCount <= 0) throw new IllegalStateException("Can't unref, count " + mCount);
535            if (--mCount == 0) {
536                if (DEBUG) Log.v(TAG, "mCount is zero, resetting");
537                reset();
538            }
539        }
540        public int getColor() {
541            return mColor;
542        }
543        public Drawable getDrawable() {
544            return mDrawable;
545        }
546        public void setColor(int color) {
547            mColor = color;
548        }
549        public void setDrawable(Drawable drawable) {
550            mDrawable = drawable;
551        }
552        public Drawable getThemeDrawable(Context context, int themeDrawableId) {
553            Drawable drawable = null;
554            if (mLastThemeDrawableState != null && mLastThemeDrawableId == themeDrawableId) {
555                Drawable.ConstantState drawableState = mLastThemeDrawableState.get();
556                if (DEBUG) Log.v(TAG, "got cached theme drawable state " + drawableState);
557                if (drawableState != null) {
558                    drawable = drawableState.newDrawable();
559                }
560            }
561            if (drawable == null) {
562                drawable = ContextCompat.getDrawable(context, themeDrawableId);
563                if (DEBUG) Log.v(TAG, "loaded theme drawable " + drawable);
564                mLastThemeDrawableState = new WeakReference<Drawable.ConstantState>(
565                        drawable.getConstantState());
566                mLastThemeDrawableId = themeDrawableId;
567            }
568            // No mutate required because this drawable is never manipulated.
569            return drawable;
570        }
571    }
572
573    private Drawable getThemeDrawable() {
574        Drawable drawable = null;
575        if (mThemeDrawableResourceId != -1) {
576            drawable = mService.getThemeDrawable(mContext, mThemeDrawableResourceId);
577        }
578        if (drawable == null) {
579            drawable = createEmptyDrawable(mContext);
580        }
581        return drawable;
582    }
583
584    /**
585     * Returns the BackgroundManager associated with the given Activity.
586     * <p>
587     * The BackgroundManager will be created on-demand for each individual
588     * Activity. Subsequent calls will return the same BackgroundManager created
589     * for this Activity.
590     */
591    public static BackgroundManager getInstance(Activity activity) {
592        if (activity instanceof FragmentActivity) {
593            return getSupportInstance((FragmentActivity) activity);
594        }
595        BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager()
596                .findFragmentByTag(FRAGMENT_TAG);
597        if (fragment != null) {
598            BackgroundManager manager = fragment.getBackgroundManager();
599            if (manager != null) {
600                return manager;
601            }
602            // manager is null: this is a fragment restored by FragmentManager,
603            // fall through to create a BackgroundManager attach to it.
604        }
605        return new BackgroundManager(activity, false);
606    }
607
608    private static BackgroundManager getSupportInstance(FragmentActivity activity) {
609        BackgroundSupportFragment fragment = (BackgroundSupportFragment) activity
610                .getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG);
611        if (fragment != null) {
612            BackgroundManager manager = fragment.getBackgroundManager();
613            if (manager != null) {
614                return manager;
615            }
616            // manager is null: this is a fragment restored by FragmentManager,
617            // fall through to create a BackgroundManager attach to it.
618        }
619        return new BackgroundManager(activity, true);
620    }
621
622    private BackgroundManager(Activity activity, boolean isSupportFragmentActivity) {
623        mContext = activity;
624        mService = BackgroundContinuityService.getInstance();
625        mHeightPx = mContext.getResources().getDisplayMetrics().heightPixels;
626        mWidthPx = mContext.getResources().getDisplayMetrics().widthPixels;
627        mHandler = new Handler();
628
629        Interpolator defaultInterpolator = new FastOutLinearInInterpolator();
630        mAccelerateInterpolator = AnimationUtils.loadInterpolator(mContext,
631                android.R.anim.accelerate_interpolator);
632        mDecelerateInterpolator = AnimationUtils.loadInterpolator(mContext,
633                android.R.anim.decelerate_interpolator);
634
635        mAnimator = ValueAnimator.ofInt(0, FULL_ALPHA);
636        mAnimator.addListener(mAnimationListener);
637        mAnimator.addUpdateListener(mAnimationUpdateListener);
638        mAnimator.setInterpolator(defaultInterpolator);
639
640        mDimAnimator = new ValueAnimator();
641        mDimAnimator.addUpdateListener(mDimUpdateListener);
642
643        TypedArray ta = activity.getTheme().obtainStyledAttributes(new int[] {
644                android.R.attr.windowBackground });
645        mThemeDrawableResourceId = ta.getResourceId(0, -1);
646        if (mThemeDrawableResourceId < 0) {
647            if (DEBUG) Log.v(TAG, "BackgroundManager no window background resource!");
648        }
649        ta.recycle();
650
651        if (isSupportFragmentActivity) {
652            createSupportFragment((FragmentActivity) activity);
653        } else {
654            createFragment(activity);
655        }
656    }
657
658    private void createFragment(Activity activity) {
659        // Use a fragment to ensure the background manager gets detached properly.
660        BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager()
661                .findFragmentByTag(FRAGMENT_TAG);
662        if (fragment == null) {
663            fragment = new BackgroundFragment();
664            activity.getFragmentManager().beginTransaction().add(fragment, FRAGMENT_TAG).commit();
665        } else {
666            if (fragment.getBackgroundManager() != null) {
667                throw new IllegalStateException("Created duplicated BackgroundManager for same " +
668                        "activity, please use getInstance() instead");
669            }
670        }
671        fragment.setBackgroundManager(this);
672        mFragmentState = fragment;
673    }
674
675    private void createSupportFragment(FragmentActivity activity) {
676        // Use a fragment to ensure the background manager gets detached properly.
677        BackgroundSupportFragment fragment = (BackgroundSupportFragment) activity
678                .getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG);
679        if (fragment == null) {
680            fragment = new BackgroundSupportFragment();
681            activity.getSupportFragmentManager().beginTransaction().add(fragment, FRAGMENT_TAG)
682                    .commit();
683        } else {
684            if (fragment.getBackgroundManager() != null) {
685                throw new IllegalStateException("Created duplicated BackgroundManager for same " +
686                    "activity, please use getInstance() instead");
687            }
688        }
689        fragment.setBackgroundManager(this);
690        mFragmentState = fragment;
691    }
692
693    private DrawableWrapper getImageInWrapper() {
694        return mLayerDrawable == null ? null :
695                mLayerDrawable.findWrapperById(R.id.background_imagein);
696    }
697
698    private DrawableWrapper getImageOutWrapper() {
699        return mLayerDrawable == null ? null :
700                mLayerDrawable.findWrapperById(R.id.background_imageout);
701    }
702
703    private DrawableWrapper getDimWrapper() {
704        return mLayerDrawable == null ? null :
705                mLayerDrawable.findWrapperById(R.id.background_dim);
706    }
707
708    private DrawableWrapper getColorWrapper() {
709        return mLayerDrawable == null ? null :
710                mLayerDrawable.findWrapperById(R.id.background_color);
711    }
712
713    /**
714     * Synchronizes state when the owning Activity is started.
715     * At that point the view becomes visible.
716     */
717    void onActivityStart() {
718        if (mService == null) {
719            return;
720        }
721        if (mLayerDrawable == null) {
722            if (DEBUG) Log.v(TAG, "onActivityStart " + this +
723                    " released state, syncing with service");
724            syncWithService();
725        } else {
726            if (DEBUG) Log.v(TAG, "onActivityStart " + this + " updating service color "
727                    + mBackgroundColor + " drawable " + mBackgroundDrawable);
728            mService.setColor(mBackgroundColor);
729            mService.setDrawable(mBackgroundDrawable);
730        }
731    }
732
733    void onResume() {
734        if (DEBUG) Log.v(TAG, "onResume " + this);
735        postChangeRunnable();
736    }
737
738    private void syncWithService() {
739        int color = mService.getColor();
740        Drawable drawable = mService.getDrawable();
741
742        if (DEBUG) Log.v(TAG, "syncWithService color " + Integer.toHexString(color)
743                + " drawable " + drawable);
744
745        mBackgroundColor = color;
746        mBackgroundDrawable = drawable == null ? null :
747            drawable.getConstantState().newDrawable().mutate();
748
749        updateImmediate();
750    }
751
752    private void lazyInit() {
753        if (mLayerDrawable != null) {
754            return;
755        }
756
757        LayerDrawable layerDrawable = (LayerDrawable)
758                ContextCompat.getDrawable(mContext, R.drawable.lb_background).mutate();
759        mLayerDrawable = createOptimizedTranslucentLayerDrawable(layerDrawable);
760        BackgroundHelper.setBackgroundPreservingAlpha(mBgView, mLayerDrawable);
761
762        mLayerDrawable.clearDrawable(R.id.background_imageout, mContext);
763        mLayerDrawable.updateDrawable(R.id.background_theme, getThemeDrawable());
764
765        updateDimWrapper();
766    }
767
768    private void updateDimWrapper() {
769        if (mDimDrawable == null) {
770            mDimDrawable = getDefaultDimLayer();
771        }
772        Drawable dimDrawable = mDimDrawable.getConstantState().newDrawable(
773                mContext.getResources()).mutate();
774        if (mLayerDrawable != null) {
775            mLayerDrawable.updateDrawable(R.id.background_dim, dimDrawable);
776        }
777    }
778
779    /**
780     * Makes the background visible on the given Window.  The background manager must be attached
781     * when the background is set.
782     */
783    public void attach(Window window) {
784        if (USE_SEPARATE_WINDOW) {
785            attachBehindWindow(window);
786        } else {
787            attachToView(window.getDecorView());
788        }
789    }
790
791    /**
792     * Sets the resource id for the drawable to be shown when there is no background set.
793     * Overrides the window background drawable from the theme. This should
794     * be called before attaching.
795     */
796    public void setThemeDrawableResourceId(int resourceId) {
797        mThemeDrawableResourceId = resourceId;
798    }
799
800    private void attachBehindWindow(Window window) {
801        if (DEBUG) Log.v(TAG, "attachBehindWindow " + window);
802        mWindow = window;
803        mWindowManager = window.getWindowManager();
804
805        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
806                // Media window sits behind the main application window
807                WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA,
808                // Avoid default to software format RGBA
809                WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
810                android.graphics.PixelFormat.TRANSLUCENT);
811        params.setTitle(WINDOW_NAME);
812        params.width = ViewGroup.LayoutParams.MATCH_PARENT;
813        params.height = ViewGroup.LayoutParams.MATCH_PARENT;
814
815        View backgroundView = LayoutInflater.from(mContext).inflate(
816                R.layout.lb_background_window, null);
817        mWindowManager.addView(backgroundView, params);
818
819        attachToView(backgroundView);
820    }
821
822    private void attachToView(View sceneRoot) {
823        mBgView = sceneRoot;
824        mAttached = true;
825        syncWithService();
826    }
827
828    /**
829     * Returns true if the background manager is currently attached; false otherwise.
830     */
831    public boolean isAttached() {
832        return mAttached;
833    }
834
835    /**
836     * Release references to Drawables and put the BackgroundManager into the
837     * detached state. Called when the associated Activity is destroyed.
838     * @hide
839     */
840    void detach() {
841        if (DEBUG) Log.v(TAG, "detach " + this);
842        release();
843
844        if (mWindowManager != null && mBgView != null) {
845            mWindowManager.removeViewImmediate(mBgView);
846        }
847
848        mWindowManager = null;
849        mWindow = null;
850        mBgView = null;
851        mAttached = false;
852
853        if (mService != null) {
854            mService.unref();
855            mService = null;
856        }
857    }
858
859    /**
860     * Release references to Drawables. Typically called to reduce memory
861     * overhead when not visible.
862     * <p>
863     * When an Activity is started, if the BackgroundManager has not been
864     * released, the continuity service is updated from the BackgroundManager
865     * state. If the BackgroundManager was released, the BackgroundManager
866     * inherits the current state from the continuity service.
867     */
868    public void release() {
869        if (DEBUG) Log.v(TAG, "release " + this);
870        if (mLayerDrawable != null) {
871            mLayerDrawable.clearDrawable(R.id.background_imagein, mContext);
872            mLayerDrawable.clearDrawable(R.id.background_imageout, mContext);
873            mLayerDrawable.clearDrawable(R.id.background_theme, mContext);
874            mLayerDrawable = null;
875        }
876        if (mChangeRunnable != null) {
877            mHandler.removeCallbacks(mChangeRunnable);
878            mChangeRunnable = null;
879        }
880        releaseBackgroundBitmap();
881    }
882
883    private void releaseBackgroundBitmap() {
884        mBackgroundDrawable = null;
885    }
886
887    private void setBackgroundDrawable(Drawable drawable) {
888        mBackgroundDrawable = drawable;
889        mService.setDrawable(mBackgroundDrawable);
890    }
891
892    /**
893     * Sets the drawable used as a dim layer.
894     */
895    public void setDimLayer(Drawable drawable) {
896        mDimDrawable = drawable;
897        updateDimWrapper();
898    }
899
900    /**
901     * Returns the drawable used as a dim layer.
902     */
903    public Drawable getDimLayer() {
904        return mDimDrawable;
905    }
906
907    /**
908     * Returns the default drawable used as a dim layer.
909     */
910    public Drawable getDefaultDimLayer() {
911        return ContextCompat.getDrawable(mContext, R.color.lb_background_protection);
912    }
913
914    private void postChangeRunnable() {
915        if (mChangeRunnable == null || !mChangeRunnablePending) {
916            return;
917        }
918
919        // Postpone a pending change runnable until: no existing change animation in progress &&
920        // activity is resumed (in the foreground) && layerdrawable fully opaque.
921        // If the layerdrawable is translucent then an activity transition is in progress
922        // and we want to use the optimized drawing path for performance reasons (see
923        // OptimizedTranslucentLayerDrawable).
924        if (mAnimator.isStarted()) {
925            if (DEBUG) Log.v(TAG, "animation in progress");
926        } else if (!mFragmentState.isResumed()) {
927            if (DEBUG) Log.v(TAG, "not resumed");
928        } else if (mLayerDrawable.getAlpha() < FULL_ALPHA) {
929            if (DEBUG) Log.v(TAG, "in transition, alpha " + mLayerDrawable.getAlpha());
930        } else {
931            long delayMs = getRunnableDelay();
932            if (DEBUG) Log.v(TAG, "posting runnable delayMs " + delayMs);
933            mLastSetTime = System.currentTimeMillis();
934            mHandler.postDelayed(mChangeRunnable, delayMs);
935            mChangeRunnablePending = false;
936        }
937    }
938
939    private void updateImmediate() {
940        lazyInit();
941
942        DrawableWrapper colorWrapper = getColorWrapper();
943        if (colorWrapper != null) {
944            colorWrapper.setColor(mBackgroundColor);
945        }
946        DrawableWrapper dimWrapper = getDimWrapper();
947        if (dimWrapper != null) {
948            dimWrapper.setAlpha(mBackgroundColor == Color.TRANSPARENT ? 0 : DIM_ALPHA_ON_SOLID);
949        }
950        showWallpaper(mBackgroundColor == Color.TRANSPARENT);
951
952        if (mBackgroundDrawable == null) {
953            mLayerDrawable.clearDrawable(R.id.background_imagein, mContext);
954        } else {
955            if (DEBUG) Log.v(TAG, "Background drawable is available");
956            mLayerDrawable.updateDrawable(
957                    R.id.background_imagein, mBackgroundDrawable);
958            if (dimWrapper != null) {
959                dimWrapper.setAlpha(FULL_ALPHA);
960            }
961        }
962    }
963
964    /**
965     * Sets the background to the given color. The timing for when this becomes
966     * visible in the app is undefined and may take place after a small delay.
967     */
968    public void setColor(@ColorInt int color) {
969        if (DEBUG) Log.v(TAG, "setColor " + Integer.toHexString(color));
970
971        mBackgroundColor = color;
972        mService.setColor(mBackgroundColor);
973
974        DrawableWrapper colorWrapper = getColorWrapper();
975        if (colorWrapper != null) {
976            colorWrapper.setColor(mBackgroundColor);
977        }
978    }
979
980    /**
981     * Sets the given drawable into the background. The provided Drawable will be
982     * used unmodified as the background, without any scaling or cropping
983     * applied to it. The timing for when this becomes visible in the app is
984     * undefined and may take place after a small delay.
985     */
986    public void setDrawable(Drawable drawable) {
987        if (DEBUG) Log.v(TAG, "setBackgroundDrawable " + drawable);
988        setDrawableInternal(drawable);
989    }
990
991    private void setDrawableInternal(Drawable drawable) {
992        if (!mAttached) {
993            throw new IllegalStateException("Must attach before setting background drawable");
994        }
995
996        if (mChangeRunnable != null) {
997            if (sameDrawable(drawable, mChangeRunnable.mDrawable)) {
998                if (DEBUG) Log.v(TAG, "new drawable same as pending");
999                return;
1000            }
1001            mHandler.removeCallbacks(mChangeRunnable);
1002            mChangeRunnable = null;
1003        }
1004
1005        // If layer drawable is null then the activity hasn't started yet.
1006        // If the layer drawable alpha is zero then the activity transition hasn't started yet.
1007        // In these cases we can update the background immediately and let activity transition
1008        // fade it in.
1009        if (mLayerDrawable == null || mLayerDrawable.getAlpha() == 0) {
1010            if (DEBUG) Log.v(TAG, "setDrawableInternal null or alpha is zero");
1011            setBackgroundDrawable(drawable);
1012            updateImmediate();
1013            return;
1014        }
1015
1016        mChangeRunnable = new ChangeBackgroundRunnable(drawable);
1017        mChangeRunnablePending = true;
1018
1019        postChangeRunnable();
1020    }
1021
1022    private long getRunnableDelay() {
1023        return Math.max(0, mLastSetTime + CHANGE_BG_DELAY_MS - System.currentTimeMillis());
1024    }
1025
1026    /**
1027     * Sets the given bitmap into the background. When using setBitmap to set the
1028     * background, the provided bitmap will be scaled and cropped to correctly
1029     * fit within the dimensions of the view. The timing for when this becomes
1030     * visible in the app is undefined and may take place after a small delay.
1031     */
1032    public void setBitmap(Bitmap bitmap) {
1033        if (DEBUG) {
1034            Log.v(TAG, "setBitmap " + bitmap);
1035        }
1036
1037        if (bitmap == null) {
1038            setDrawableInternal(null);
1039            return;
1040        }
1041
1042        if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
1043            if (DEBUG) {
1044                Log.v(TAG, "invalid bitmap width or height");
1045            }
1046            return;
1047        }
1048
1049        Matrix matrix = null;
1050
1051        if ((bitmap.getWidth() != mWidthPx || bitmap.getHeight() != mHeightPx)) {
1052            int dwidth = bitmap.getWidth();
1053            int dheight = bitmap.getHeight();
1054            float scale;
1055
1056            // Scale proportionately to fit width and height.
1057            if (dwidth * mHeightPx > mWidthPx * dheight) {
1058                scale = (float) mHeightPx / (float) dheight;
1059            } else {
1060                scale = (float) mWidthPx / (float) dwidth;
1061            }
1062
1063            int subX = Math.min((int) (mWidthPx / scale), dwidth);
1064            int dx = Math.max(0, (dwidth - subX) / 2);
1065
1066            matrix = new Matrix();
1067            matrix.setScale(scale, scale);
1068            matrix.preTranslate(-dx, 0);
1069
1070            if (DEBUG) Log.v(TAG, "original image size " + bitmap.getWidth() + "x" + bitmap.getHeight() +
1071                    " scale " + scale + " dx " + dx);
1072        }
1073
1074        BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap, matrix);
1075
1076        setDrawableInternal(bitmapDrawable);
1077    }
1078
1079    private void applyBackgroundChanges() {
1080        if (!mAttached) {
1081            return;
1082        }
1083
1084        if (DEBUG) Log.v(TAG, "applyBackgroundChanges drawable " + mBackgroundDrawable);
1085
1086        int dimAlpha = -1;
1087
1088        if (getImageOutWrapper() != null) {
1089            dimAlpha = mBackgroundColor == Color.TRANSPARENT ? 0 : DIM_ALPHA_ON_SOLID;
1090        }
1091
1092        DrawableWrapper imageInWrapper = getImageInWrapper();
1093        if (imageInWrapper == null && mBackgroundDrawable != null) {
1094            if (DEBUG) Log.v(TAG, "creating new imagein drawable");
1095            imageInWrapper = mLayerDrawable.updateDrawable(
1096                    R.id.background_imagein, mBackgroundDrawable);
1097            if (DEBUG) Log.v(TAG, "imageInWrapper animation starting");
1098            imageInWrapper.setAlpha(0);
1099            dimAlpha = FULL_ALPHA;
1100        }
1101
1102        mAnimator.setDuration(FADE_DURATION);
1103        mAnimator.start();
1104
1105        DrawableWrapper dimWrapper = getDimWrapper();
1106        if (dimWrapper != null && dimAlpha >= 0) {
1107            if (DEBUG) Log.v(TAG, "dimwrapper animation starting to " + dimAlpha);
1108            mDimAnimator.cancel();
1109            mDimAnimator.setIntValues(dimWrapper.getAlpha(), dimAlpha);
1110            mDimAnimator.setDuration(FADE_DURATION);
1111            mDimAnimator.setInterpolator(
1112                    dimAlpha == FULL_ALPHA ? mDecelerateInterpolator : mAccelerateInterpolator);
1113            mDimAnimator.start();
1114        }
1115    }
1116
1117    /**
1118     * Returns the current background color.
1119     */
1120    @ColorInt
1121    public final int getColor() {
1122        return mBackgroundColor;
1123    }
1124
1125    /**
1126     * Returns the current background {@link Drawable}.
1127     */
1128    public Drawable getDrawable() {
1129        return mBackgroundDrawable;
1130    }
1131
1132    private boolean sameDrawable(Drawable first, Drawable second) {
1133        if (first == null || second == null) {
1134            return false;
1135        }
1136        if (first == second) {
1137            return true;
1138        }
1139        if (first instanceof BitmapDrawable && second instanceof BitmapDrawable) {
1140            if (((BitmapDrawable) first).getBitmap().sameAs(((BitmapDrawable) second).getBitmap())) {
1141                return true;
1142            }
1143        }
1144        return false;
1145    }
1146
1147    /**
1148     * Task which changes the background.
1149     */
1150    class ChangeBackgroundRunnable implements Runnable {
1151        private Drawable mDrawable;
1152
1153        ChangeBackgroundRunnable(Drawable drawable) {
1154            mDrawable = drawable;
1155        }
1156
1157        @Override
1158        public void run() {
1159            runTask();
1160            mChangeRunnable = null;
1161        }
1162
1163        private void runTask() {
1164            if (mLayerDrawable == null) {
1165                if (DEBUG) Log.v(TAG, "runTask while released - should not happen");
1166                return;
1167            }
1168
1169            if (sameDrawable(mDrawable, mBackgroundDrawable)) {
1170                if (DEBUG) Log.v(TAG, "new drawable same as current");
1171                return;
1172            }
1173
1174            releaseBackgroundBitmap();
1175
1176            DrawableWrapper imageInWrapper = getImageInWrapper();
1177            if (imageInWrapper != null) {
1178                if (DEBUG) Log.v(TAG, "moving image in to image out");
1179                // Order is important! Setting a drawable "removes" the
1180                // previous one from the view
1181                mLayerDrawable.clearDrawable(R.id.background_imagein, mContext);
1182                mLayerDrawable.updateDrawable(R.id.background_imageout,
1183                        imageInWrapper.getDrawable());
1184            }
1185
1186            setBackgroundDrawable(mDrawable);
1187            applyBackgroundChanges();
1188        }
1189    }
1190
1191    private static Drawable createEmptyDrawable(Context context) {
1192        Bitmap bitmap = null;
1193        return new BitmapDrawable(context.getResources(), bitmap);
1194    }
1195
1196    private void showWallpaper(boolean show) {
1197        if (mWindow == null) {
1198            return;
1199        }
1200
1201        WindowManager.LayoutParams layoutParams = mWindow.getAttributes();
1202        if (show) {
1203            if ((layoutParams.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) {
1204                return;
1205            }
1206            if (DEBUG) Log.v(TAG, "showing wallpaper");
1207            layoutParams.flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
1208        } else {
1209            if ((layoutParams.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) == 0) {
1210                return;
1211            }
1212            if (DEBUG) Log.v(TAG, "hiding wallpaper");
1213            layoutParams.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
1214        }
1215
1216        mWindow.setAttributes(layoutParams);
1217    }
1218}
1219