BackgroundManager.java revision 7e22f555da71f49a32420965817c760522b95963
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 android.support.v17.leanback.R;
17import android.animation.Animator;
18import android.animation.ValueAnimator;
19import android.app.Activity;
20import android.content.Context;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.graphics.Bitmap;
24import android.graphics.Canvas;
25import android.graphics.Color;
26import android.graphics.ColorFilter;
27import android.graphics.Matrix;
28import android.graphics.Paint;
29import android.graphics.drawable.ColorDrawable;
30import android.graphics.drawable.Drawable;
31import android.graphics.drawable.LayerDrawable;
32import android.os.Handler;
33import android.util.Log;
34import android.view.LayoutInflater;
35import android.view.View;
36import android.view.ViewGroup;
37import android.view.Window;
38import android.view.WindowManager;
39import android.view.animation.Interpolator;
40import android.view.animation.LinearInterpolator;
41
42/**
43 * Supports background image continuity between multiple Activities.
44 *
45 * <p>An Activity should instantiate a BackgroundManager and {@link #attach}
46 * to the Activity's window.  When the Activity is started, the background is
47 * initialized to the current background values stored in a continuity service.
48 * The background continuity service is updated as the background is updated.
49 *
50 * <p>At some point, for example when it is stopped, the Activity may release
51 * its background state.
52 *
53 * <p>When an Activity is resumed, if the BackgroundManager has not been
54 * released, the continuity service is updated from the BackgroundManager state.
55 * If the BackgroundManager was released, the BackgroundManager inherits the
56 * current state from the continuity service.
57 *
58 * <p>When the last Activity is destroyed, the background state is reset.
59 *
60 * <p>Backgrounds consist of several layers, from back to front:
61 * <ul>
62 *   <li>the background Drawable of the theme</li>
63 *   <li>a solid color (set via {@link #setColor})</li>
64 *   <li>two Drawables, previous and current (set via {@link #setBitmap} or
65 *   {@link #setDrawable}), which may be in transition</li>
66 * </ul>
67 *
68 * <p>BackgroundManager holds references to potentially large bitmap Drawables.
69 * Call {@link #release} to release these references when the Activity is not
70 * visible.
71 */
72// TODO: support for multiple app processes requires a proper android service
73// instead of the shared memory "service" implemented here. Such a service could
74// support continuity between fragments of different applications if desired.
75public final class BackgroundManager {
76    private static final String TAG = "BackgroundManager";
77    private static final boolean DEBUG = false;
78
79    private static final int FULL_ALPHA = 255;
80    private static final int DIM_ALPHA_ON_SOLID = (int) (0.8f * FULL_ALPHA);
81    private static final int CHANGE_BG_DELAY_MS = 500;
82    private static final int FADE_DURATION = 500;
83
84    /**
85     * Using a separate window for backgrounds can improve graphics performance by
86     * leveraging hardware display layers.
87     * TODO: support a leanback configuration option.
88     */
89    private static final boolean USE_SEPARATE_WINDOW = false;
90
91    private static final String WINDOW_NAME = "BackgroundManager";
92    private static final String FRAGMENT_TAG = BackgroundManager.class.getCanonicalName();
93
94    private Context mContext;
95    private Handler mHandler;
96    private Window mWindow;
97    private WindowManager mWindowManager;
98    private View mBgView;
99    private BackgroundContinuityService mService;
100    private int mThemeDrawableResourceId;
101
102    private int mHeightPx;
103    private int mWidthPx;
104    private Drawable mBackgroundDrawable;
105    private int mBackgroundColor;
106    private boolean mAttached;
107
108    private static class BitmapDrawable extends Drawable {
109
110        static class ConstantState extends Drawable.ConstantState {
111            Bitmap mBitmap;
112            Matrix mMatrix;
113            Paint mPaint;
114
115            @Override
116            public Drawable newDrawable() {
117                return new BitmapDrawable(null, mBitmap, mMatrix);
118            }
119
120            @Override
121            public int getChangingConfigurations() {
122                return 0;
123            }
124        }
125
126        private ConstantState mState = new ConstantState();
127
128        BitmapDrawable(Resources resources, Bitmap bitmap) {
129            this(resources, bitmap, null);
130        }
131
132        BitmapDrawable(Resources resources, Bitmap bitmap, Matrix matrix) {
133            mState.mBitmap = bitmap;
134            mState.mMatrix = matrix != null ? matrix : new Matrix();
135            mState.mPaint = new Paint();
136            mState.mPaint.setFilterBitmap(true);
137        }
138
139        Bitmap getBitmap() {
140            return mState.mBitmap;
141        }
142
143        @Override
144        public void draw(Canvas canvas) {
145            if (mState.mBitmap == null) {
146                return;
147            }
148            canvas.drawBitmap(mState.mBitmap, mState.mMatrix, mState.mPaint);
149        }
150
151        @Override
152        public int getOpacity() {
153            return android.graphics.PixelFormat.OPAQUE;
154        }
155
156        @Override
157        public void setAlpha(int alpha) {
158            if (mState.mPaint.getAlpha() != alpha) {
159                mState.mPaint.setAlpha(alpha);
160                invalidateSelf();
161            }
162        }
163
164        @Override
165        public void setColorFilter(ColorFilter cf) {
166            // Abstract in Drawable, not implemented
167        }
168
169        @Override
170        public ConstantState getConstantState() {
171            return mState;
172        }
173    }
174
175    private static class DrawableWrapper {
176        protected int mAlpha;
177        protected Drawable mDrawable;
178        protected ValueAnimator mAnimator;
179        protected boolean mAnimationPending;
180
181        private final Interpolator mInterpolator = new LinearInterpolator();
182        private final ValueAnimator.AnimatorUpdateListener mAnimationUpdateListener =
183                new ValueAnimator.AnimatorUpdateListener() {
184            @Override
185            public void onAnimationUpdate(ValueAnimator animation) {
186                setAlpha((Integer) animation.getAnimatedValue());
187            }
188        };
189
190        public DrawableWrapper(Drawable drawable) {
191            mDrawable = drawable;
192            setAlpha(FULL_ALPHA);
193        }
194
195        public Drawable getDrawable() {
196            return mDrawable;
197        }
198        public void setAlpha(int alpha) {
199            mAlpha = alpha;
200            mDrawable.setAlpha(alpha);
201        }
202        public int getAlpha() {
203            return mAlpha;
204        }
205        public void setColor(int color) {
206            ((ColorDrawable) mDrawable).setColor(color);
207        }
208        public void fadeIn(int durationMs, int delayMs) {
209            fade(durationMs, delayMs, FULL_ALPHA);
210        }
211        public void fadeOut(int durationMs) {
212            fade(durationMs, 0, 0);
213        }
214        public void fade(int durationMs, int delayMs, int alpha) {
215            if (mAnimator != null && mAnimator.isStarted()) {
216                mAnimator.cancel();
217            }
218            mAnimator = ValueAnimator.ofInt(getAlpha(), alpha);
219            mAnimator.addUpdateListener(mAnimationUpdateListener);
220            mAnimator.setInterpolator(mInterpolator);
221            mAnimator.setDuration(durationMs);
222            mAnimator.setStartDelay(delayMs);
223            mAnimationPending = true;
224        }
225        public boolean isAnimationPending() {
226            return mAnimationPending;
227        }
228        public boolean isAnimationStarted() {
229            return mAnimator != null && mAnimator.isStarted();
230        }
231        public void startAnimation() {
232            startAnimation(null);
233        }
234        public void startAnimation(Animator.AnimatorListener listener) {
235            if (listener != null) {
236                mAnimator.addListener(listener);
237            }
238            mAnimator.start();
239            mAnimationPending = false;
240        }
241    }
242
243    private LayerDrawable mLayerDrawable;
244    private DrawableWrapper mLayerWrapper;
245    private DrawableWrapper mImageInWrapper;
246    private DrawableWrapper mImageOutWrapper;
247    private DrawableWrapper mColorWrapper;
248    private DrawableWrapper mDimWrapper;
249
250    private Drawable mThemeDrawable;
251    private ChangeBackgroundRunnable mChangeRunnable;
252
253    /**
254     * Shared memory continuity service.
255     */
256    private static class BackgroundContinuityService {
257        private static final String TAG = "BackgroundContinuityService";
258        private static boolean DEBUG = BackgroundManager.DEBUG;
259
260        private static BackgroundContinuityService sService = new BackgroundContinuityService();
261
262        private int mColor;
263        private Drawable mDrawable;
264        private int mCount;
265
266        private BackgroundContinuityService() {
267            reset();
268        }
269
270        private void reset() {
271            mColor = Color.TRANSPARENT;
272            mDrawable = null;
273        }
274
275        public static BackgroundContinuityService getInstance() {
276            final int count = sService.mCount++;
277            if (DEBUG) Log.v(TAG, "Returning instance with new count " + count);
278            return sService;
279        }
280
281        public void unref() {
282            if (mCount <= 0) throw new IllegalStateException("Can't unref, count " + mCount);
283            if (--mCount == 0) {
284                if (DEBUG) Log.v(TAG, "mCount is zero, resetting");
285                reset();
286            }
287        }
288        public int getColor() {
289            return mColor;
290        }
291        public Drawable getDrawable() {
292            return mDrawable;
293        }
294        public void setColor(int color) {
295            mColor = color;
296        }
297        public void setDrawable(Drawable drawable) {
298            mDrawable = drawable;
299        }
300    }
301
302    private Drawable getThemeDrawable() {
303        Drawable drawable = null;
304        if (mThemeDrawableResourceId != -1) {
305            drawable = mContext.getResources().getDrawable(mThemeDrawableResourceId);
306        }
307        if (drawable == null) {
308            drawable = createEmptyDrawable();
309        }
310        return drawable;
311    }
312
313    /**
314     * Get the BackgroundManager associated with the Activity.
315     * <p>
316     * The BackgroundManager will be created on-demand for each individual
317     * Activity. Subsequent calls will return the same BackgroundManager created
318     * for this Activity.
319     */
320    public static BackgroundManager getInstance(Activity activity) {
321        BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager()
322                .findFragmentByTag(FRAGMENT_TAG);
323        if (fragment != null) {
324            BackgroundManager manager = fragment.getBackgroundManager();
325            if (manager != null) {
326                return manager;
327            }
328            // manager is null: this is a fragment restored by FragmentManager,
329            // fall through to create a BackgroundManager attach to it.
330        }
331        return new BackgroundManager(activity);
332    }
333
334    /**
335     * Construct a BackgroundManager instance. The Initial background is set
336     * from the continuity service.
337     * @deprecated Use getInstance(Activity).
338     */
339    @Deprecated
340    public BackgroundManager(Activity activity) {
341        mContext = activity;
342        mService = BackgroundContinuityService.getInstance();
343        mHeightPx = mContext.getResources().getDisplayMetrics().heightPixels;
344        mWidthPx = mContext.getResources().getDisplayMetrics().widthPixels;
345        mHandler = new Handler();
346
347        TypedArray ta = activity.getTheme().obtainStyledAttributes(new int[] {
348                android.R.attr.windowBackground });
349        mThemeDrawableResourceId = ta.getResourceId(0, -1);
350        if (mThemeDrawableResourceId < 0) {
351            if (DEBUG) Log.v(TAG, "BackgroundManager no window background resource!");
352        }
353        ta.recycle();
354
355        createFragment(activity);
356    }
357
358    private void createFragment(Activity activity) {
359        // Use a fragment to ensure the background manager gets detached properly.
360        BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager()
361                .findFragmentByTag(FRAGMENT_TAG);
362        if (fragment == null) {
363            fragment = new BackgroundFragment();
364            activity.getFragmentManager().beginTransaction().add(fragment, FRAGMENT_TAG).commit();
365        } else {
366            if (fragment.getBackgroundManager() != null) {
367                throw new IllegalStateException("Created duplicated BackgroundManager for same " +
368                        "activity, please use getInstance() instead");
369            }
370        }
371        fragment.setBackgroundManager(this);
372    }
373
374    /**
375     * Synchronizes state when the owning Activity is resumed.
376     */
377    void onActivityResume() {
378        if (mService == null) {
379            return;
380        }
381        if (mLayerDrawable == null) {
382            if (DEBUG) Log.v(TAG, "onActivityResume " + this +
383                    " released state, syncing with service");
384            syncWithService();
385        } else {
386            if (DEBUG) Log.v(TAG, "onActivityResume " + this + " updating service color "
387                    + mBackgroundColor + " drawable " + mBackgroundDrawable);
388            mService.setColor(mBackgroundColor);
389            mService.setDrawable(mBackgroundDrawable);
390        }
391    }
392
393    private void syncWithService() {
394        int color = mService.getColor();
395        Drawable drawable = mService.getDrawable();
396
397        if (DEBUG) Log.v(TAG, "syncWithService color " + Integer.toHexString(color)
398                + " drawable " + drawable);
399
400        mBackgroundColor = color;
401        mBackgroundDrawable = drawable == null ? null :
402            drawable.getConstantState().newDrawable().mutate();
403
404        updateImmediate();
405    }
406
407    private void lazyInit() {
408        if (mLayerDrawable != null) {
409            return;
410        }
411
412        mLayerDrawable = (LayerDrawable) mContext.getResources().getDrawable(
413                R.drawable.lb_background).mutate();
414        mBgView.setBackground(mLayerDrawable);
415
416        mLayerDrawable.setDrawableByLayerId(R.id.background_imageout, createEmptyDrawable());
417
418        mDimWrapper = new DrawableWrapper(
419                mLayerDrawable.findDrawableByLayerId(R.id.background_dim));
420
421        mLayerWrapper = new DrawableWrapper(mLayerDrawable);
422
423        mColorWrapper = new DrawableWrapper(
424                mLayerDrawable.findDrawableByLayerId(R.id.background_color));
425    }
426
427    /**
428     * Make the background visible on the given Window.
429     */
430    public void attach(Window window) {
431        if (USE_SEPARATE_WINDOW) {
432            attachBehindWindow(window);
433        } else {
434            attachToView(window.getDecorView());
435        }
436    }
437
438    private void attachBehindWindow(Window window) {
439        if (DEBUG) Log.v(TAG, "attachBehindWindow " + window);
440        mWindow = window;
441        mWindowManager = window.getWindowManager();
442
443        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
444                // Media window sits behind the main application window
445                WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA,
446                // Avoid default to software format RGBA
447                WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
448                android.graphics.PixelFormat.TRANSLUCENT);
449        params.setTitle(WINDOW_NAME);
450        params.width = ViewGroup.LayoutParams.MATCH_PARENT;
451        params.height = ViewGroup.LayoutParams.MATCH_PARENT;
452
453        View backgroundView = LayoutInflater.from(mContext).inflate(
454                R.layout.lb_background_window, null);
455        mWindowManager.addView(backgroundView, params);
456
457        attachToView(backgroundView);
458    }
459
460    private void attachToView(View sceneRoot) {
461        mBgView = sceneRoot;
462        mAttached = true;
463        syncWithService();
464    }
465
466    /**
467     * Release references to Drawables and put the BackgroundManager into the
468     * detached state. Called when the associated Activity is destroyed.
469     * @hide
470     */
471    void detach() {
472        if (DEBUG) Log.v(TAG, "detach " + this);
473        release();
474
475        if (mWindowManager != null && mBgView != null) {
476            mWindowManager.removeViewImmediate(mBgView);
477        }
478
479        mWindowManager = null;
480        mWindow = null;
481        mBgView = null;
482        mAttached = false;
483
484        if (mService != null) {
485            mService.unref();
486            mService = null;
487        }
488    }
489
490    /**
491     * Release references to Drawables. Typically called to reduce memory
492     * overhead when not visible.
493     * <p>
494     * When an Activity is resumed, if the BackgroundManager has not been
495     * released, the continuity service is updated from the BackgroundManager
496     * state. If the BackgroundManager was released, the BackgroundManager
497     * inherits the current state from the continuity service.
498     */
499    public void release() {
500        if (DEBUG) Log.v(TAG, "release " + this);
501        if (mLayerDrawable != null) {
502            mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable());
503            mLayerDrawable.setDrawableByLayerId(R.id.background_imageout, createEmptyDrawable());
504            mLayerDrawable = null;
505        }
506        mLayerWrapper = null;
507        mImageInWrapper = null;
508        mImageOutWrapper = null;
509        mColorWrapper = null;
510        mDimWrapper = null;
511        mThemeDrawable = null;
512        if (mChangeRunnable != null) {
513            mChangeRunnable.cancel();
514            mChangeRunnable = null;
515        }
516        releaseBackgroundBitmap();
517    }
518
519    private void releaseBackgroundBitmap() {
520        mBackgroundDrawable = null;
521    }
522
523    private void updateImmediate() {
524        lazyInit();
525
526        mColorWrapper.setColor(mBackgroundColor);
527        if (mDimWrapper != null) {
528            mDimWrapper.setAlpha(mBackgroundColor == Color.TRANSPARENT ? 0 : DIM_ALPHA_ON_SOLID);
529        }
530        showWallpaper(mBackgroundColor == Color.TRANSPARENT);
531
532        mThemeDrawable = getThemeDrawable();
533        mLayerDrawable.setDrawableByLayerId(R.id.background_theme, mThemeDrawable);
534
535        if (mBackgroundDrawable == null) {
536            mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable());
537        } else {
538            if (DEBUG) Log.v(TAG, "Background drawable is available");
539            mImageInWrapper = new DrawableWrapper(mBackgroundDrawable);
540            mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, mBackgroundDrawable);
541            if (mDimWrapper != null) {
542                mDimWrapper.setAlpha(FULL_ALPHA);
543            }
544        }
545    }
546
547    /**
548     * Set the background to the given color. The timing for when this becomes
549     * visible in the app is undefined and may take place after a small delay.
550     */
551    public void setColor(int color) {
552        if (DEBUG) Log.v(TAG, "setColor " + Integer.toHexString(color));
553
554        mBackgroundColor = color;
555        mService.setColor(mBackgroundColor);
556
557        if (mColorWrapper != null) {
558            mColorWrapper.setColor(mBackgroundColor);
559        }
560    }
561
562    /**
563     * Set the given drawable into the background. The provided Drawable will be
564     * used unmodified as the background, without any scaling or cropping
565     * applied to it. The timing for when this becomes visible in the app is
566     * undefined and may take place after a small delay.
567     */
568    public void setDrawable(Drawable drawable) {
569        if (DEBUG) Log.v(TAG, "setBackgroundDrawable " + drawable);
570        setDrawableInternal(drawable);
571    }
572
573    private void setDrawableInternal(Drawable drawable) {
574        if (!mAttached) {
575            throw new IllegalStateException("Must attach before setting background drawable");
576        }
577
578        if (mChangeRunnable != null) {
579            mChangeRunnable.cancel();
580        }
581        mChangeRunnable = new ChangeBackgroundRunnable(drawable);
582
583        if (mImageInWrapper != null && mImageInWrapper.isAnimationStarted()) {
584            if (DEBUG) Log.v(TAG, "animation in progress");
585        } else {
586            mHandler.postDelayed(mChangeRunnable, CHANGE_BG_DELAY_MS);
587        }
588    }
589
590    /**
591     * Set the given bitmap into the background. When using setBitmap to set the
592     * background, the provided bitmap will be scaled and cropped to correctly
593     * fit within the dimensions of the view. The timing for when this becomes
594     * visible in the app is undefined and may take place after a small delay.
595     */
596    public void setBitmap(Bitmap bitmap) {
597        if (DEBUG) {
598            Log.v(TAG, "setBitmap " + bitmap);
599        }
600
601        if (bitmap == null) {
602            setDrawableInternal(null);
603            return;
604        }
605
606        if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
607            if (DEBUG) {
608                Log.v(TAG, "invalid bitmap width or height");
609            }
610            return;
611        }
612
613        Matrix matrix = null;
614
615        if ((bitmap.getWidth() != mWidthPx || bitmap.getHeight() != mHeightPx)) {
616            int dwidth = bitmap.getWidth();
617            int dheight = bitmap.getHeight();
618            float scale;
619
620            // Scale proportionately to fit width and height.
621            if (dwidth * mHeightPx > mWidthPx * dheight) {
622                scale = (float) mHeightPx / (float) dheight;
623            } else {
624                scale = (float) mWidthPx / (float) dwidth;
625            }
626
627            int subX = Math.min((int) (mWidthPx / scale), dwidth);
628            int dx = Math.max(0, (dwidth - subX) / 2);
629
630            matrix = new Matrix();
631            matrix.setScale(scale, scale);
632            matrix.preTranslate(-dx, 0);
633
634            if (DEBUG) Log.v(TAG, "original image size " + bitmap.getWidth() + "x" + bitmap.getHeight() +
635                    " scale " + scale + " dx " + dx);
636        }
637
638        BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap, matrix);
639
640        setDrawableInternal(bitmapDrawable);
641    }
642
643    private void applyBackgroundChanges() {
644        if (!mAttached || mLayerWrapper == null) {
645            return;
646        }
647
648        if (DEBUG) Log.v(TAG, "applyBackgroundChanges drawable " + mBackgroundDrawable);
649
650        int dimAlpha = 0;
651
652        if (mImageOutWrapper != null && mImageOutWrapper.isAnimationPending()) {
653            if (DEBUG) Log.v(TAG, "mImageOutWrapper animation starting");
654            mImageOutWrapper.startAnimation();
655            mImageOutWrapper = null;
656            dimAlpha = DIM_ALPHA_ON_SOLID;
657        }
658
659        if (mImageInWrapper == null && mBackgroundDrawable != null) {
660            if (DEBUG) Log.v(TAG, "creating new imagein drawable");
661            mImageInWrapper = new DrawableWrapper(mBackgroundDrawable);
662            mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, mBackgroundDrawable);
663            if (DEBUG) Log.v(TAG, "mImageInWrapper animation starting");
664            mImageInWrapper.setAlpha(0);
665            mImageInWrapper.fadeIn(FADE_DURATION, 0);
666            mImageInWrapper.startAnimation(mImageInListener);
667            dimAlpha = FULL_ALPHA;
668        }
669
670        if (mDimWrapper != null && dimAlpha != 0) {
671            if (DEBUG) Log.v(TAG, "dimwrapper animation starting to " + dimAlpha);
672            mDimWrapper.fade(FADE_DURATION, 0, dimAlpha);
673            mDimWrapper.startAnimation();
674        }
675    }
676
677    private final Animator.AnimatorListener mImageInListener = new Animator.AnimatorListener() {
678        @Override
679        public void onAnimationStart(Animator animation) {
680        }
681        @Override
682        public void onAnimationRepeat(Animator animation) {
683        }
684        @Override
685        public void onAnimationEnd(Animator animation) {
686            if (mChangeRunnable != null) {
687                if (DEBUG) Log.v(TAG, "animation ended, found change runnable");
688                mChangeRunnable.run();
689            }
690        }
691        @Override
692        public void onAnimationCancel(Animator animation) {
693        }
694    };
695
696    /**
697     * Returns the current background color.
698     */
699    public final int getColor() {
700        return mBackgroundColor;
701    }
702
703    /**
704     * Returns the current background {@link Drawable}.
705     */
706    public Drawable getDrawable() {
707        return mBackgroundDrawable;
708    }
709
710    private boolean sameDrawable(Drawable first, Drawable second) {
711        if (first == null || second == null) {
712            return false;
713        }
714        if (first == second) {
715            return true;
716        }
717        if (first instanceof BitmapDrawable && second instanceof BitmapDrawable) {
718            if (((BitmapDrawable) first).getBitmap().sameAs(((BitmapDrawable) second).getBitmap())) {
719                return true;
720            }
721        }
722        return false;
723    }
724
725    /**
726     * Task which changes the background.
727     */
728    class ChangeBackgroundRunnable implements Runnable {
729        private Drawable mDrawable;
730        private boolean mCancel;
731
732        ChangeBackgroundRunnable(Drawable drawable) {
733            mDrawable = drawable;
734        }
735
736        public void cancel() {
737            mCancel = true;
738        }
739
740        @Override
741        public void run() {
742            if (!mCancel) {
743                runTask();
744            }
745        }
746
747        private void runTask() {
748            lazyInit();
749
750            if (sameDrawable(mDrawable, mBackgroundDrawable)) {
751                if (DEBUG) Log.v(TAG, "same bitmap detected");
752                return;
753            }
754
755            releaseBackgroundBitmap();
756
757            if (mImageInWrapper != null) {
758                mImageOutWrapper = new DrawableWrapper(mImageInWrapper.getDrawable());
759                mImageOutWrapper.setAlpha(mImageInWrapper.getAlpha());
760                mImageOutWrapper.fadeOut(FADE_DURATION);
761
762                // Order is important! Setting a drawable "removes" the
763                // previous one from the view
764                mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable());
765                mLayerDrawable.setDrawableByLayerId(R.id.background_imageout,
766                        mImageOutWrapper.getDrawable());
767                mImageInWrapper.setAlpha(0);
768                mImageInWrapper = null;
769            }
770
771            mBackgroundDrawable = mDrawable;
772            mService.setDrawable(mBackgroundDrawable);
773
774            applyBackgroundChanges();
775
776            mChangeRunnable = null;
777        }
778    }
779
780    private Drawable createEmptyDrawable() {
781        Bitmap bitmap = null;
782        return new BitmapDrawable(mContext.getResources(), bitmap);
783    }
784
785    private void showWallpaper(boolean show) {
786        if (mWindow == null) {
787            return;
788        }
789
790        WindowManager.LayoutParams layoutParams = mWindow.getAttributes();
791        if (show) {
792            if ((layoutParams.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) {
793                return;
794            }
795            if (DEBUG) Log.v(TAG, "showing wallpaper");
796            layoutParams.flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
797        } else {
798            if ((layoutParams.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) == 0) {
799                return;
800            }
801            if (DEBUG) Log.v(TAG, "hiding wallpaper");
802            layoutParams.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
803        }
804
805        mWindow.setAttributes(layoutParams);
806    }
807}
808