BackgroundManager.java revision 86c973d53a08fdd1081be12c10c86e06e0172cd3
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).mutate();
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    private BackgroundManager(Activity activity) {
335        mContext = activity;
336        mService = BackgroundContinuityService.getInstance();
337        mHeightPx = mContext.getResources().getDisplayMetrics().heightPixels;
338        mWidthPx = mContext.getResources().getDisplayMetrics().widthPixels;
339        mHandler = new Handler();
340
341        TypedArray ta = activity.getTheme().obtainStyledAttributes(new int[] {
342                android.R.attr.windowBackground });
343        mThemeDrawableResourceId = ta.getResourceId(0, -1);
344        if (mThemeDrawableResourceId < 0) {
345            if (DEBUG) Log.v(TAG, "BackgroundManager no window background resource!");
346        }
347        ta.recycle();
348
349        createFragment(activity);
350    }
351
352    private void createFragment(Activity activity) {
353        // Use a fragment to ensure the background manager gets detached properly.
354        BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager()
355                .findFragmentByTag(FRAGMENT_TAG);
356        if (fragment == null) {
357            fragment = new BackgroundFragment();
358            activity.getFragmentManager().beginTransaction().add(fragment, FRAGMENT_TAG).commit();
359        } else {
360            if (fragment.getBackgroundManager() != null) {
361                throw new IllegalStateException("Created duplicated BackgroundManager for same " +
362                        "activity, please use getInstance() instead");
363            }
364        }
365        fragment.setBackgroundManager(this);
366    }
367
368    /**
369     * Synchronizes state when the owning Activity is resumed.
370     */
371    void onActivityResume() {
372        if (mService == null) {
373            return;
374        }
375        if (mLayerDrawable == null) {
376            if (DEBUG) Log.v(TAG, "onActivityResume " + this +
377                    " released state, syncing with service");
378            syncWithService();
379        } else {
380            if (DEBUG) Log.v(TAG, "onActivityResume " + this + " updating service color "
381                    + mBackgroundColor + " drawable " + mBackgroundDrawable);
382            mService.setColor(mBackgroundColor);
383            mService.setDrawable(mBackgroundDrawable);
384        }
385    }
386
387    private void syncWithService() {
388        int color = mService.getColor();
389        Drawable drawable = mService.getDrawable();
390
391        if (DEBUG) Log.v(TAG, "syncWithService color " + Integer.toHexString(color)
392                + " drawable " + drawable);
393
394        mBackgroundColor = color;
395        mBackgroundDrawable = drawable == null ? null :
396            drawable.getConstantState().newDrawable().mutate();
397
398        updateImmediate();
399    }
400
401    private void lazyInit() {
402        if (mLayerDrawable != null) {
403            return;
404        }
405
406        mLayerDrawable = (LayerDrawable) mContext.getResources().getDrawable(
407                R.drawable.lb_background).mutate();
408        mBgView.setBackground(mLayerDrawable);
409
410        mLayerDrawable.setDrawableByLayerId(R.id.background_imageout, createEmptyDrawable());
411
412        mDimWrapper = new DrawableWrapper(
413                mLayerDrawable.findDrawableByLayerId(R.id.background_dim));
414
415        mLayerWrapper = new DrawableWrapper(mLayerDrawable);
416
417        mColorWrapper = new DrawableWrapper(
418                mLayerDrawable.findDrawableByLayerId(R.id.background_color));
419    }
420
421    /**
422     * Make the background visible on the given Window.
423     */
424    public void attach(Window window) {
425        if (USE_SEPARATE_WINDOW) {
426            attachBehindWindow(window);
427        } else {
428            attachToView(window.getDecorView());
429        }
430    }
431
432    private void attachBehindWindow(Window window) {
433        if (DEBUG) Log.v(TAG, "attachBehindWindow " + window);
434        mWindow = window;
435        mWindowManager = window.getWindowManager();
436
437        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
438                // Media window sits behind the main application window
439                WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA,
440                // Avoid default to software format RGBA
441                WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
442                android.graphics.PixelFormat.TRANSLUCENT);
443        params.setTitle(WINDOW_NAME);
444        params.width = ViewGroup.LayoutParams.MATCH_PARENT;
445        params.height = ViewGroup.LayoutParams.MATCH_PARENT;
446
447        View backgroundView = LayoutInflater.from(mContext).inflate(
448                R.layout.lb_background_window, null);
449        mWindowManager.addView(backgroundView, params);
450
451        attachToView(backgroundView);
452    }
453
454    private void attachToView(View sceneRoot) {
455        mBgView = sceneRoot;
456        mAttached = true;
457        syncWithService();
458    }
459
460    /**
461     * Release references to Drawables and put the BackgroundManager into the
462     * detached state. Called when the associated Activity is destroyed.
463     * @hide
464     */
465    void detach() {
466        if (DEBUG) Log.v(TAG, "detach " + this);
467        release();
468
469        if (mWindowManager != null && mBgView != null) {
470            mWindowManager.removeViewImmediate(mBgView);
471        }
472
473        mWindowManager = null;
474        mWindow = null;
475        mBgView = null;
476        mAttached = false;
477
478        if (mService != null) {
479            mService.unref();
480            mService = null;
481        }
482    }
483
484    /**
485     * Release references to Drawables. Typically called to reduce memory
486     * overhead when not visible.
487     * <p>
488     * When an Activity is resumed, if the BackgroundManager has not been
489     * released, the continuity service is updated from the BackgroundManager
490     * state. If the BackgroundManager was released, the BackgroundManager
491     * inherits the current state from the continuity service.
492     */
493    public void release() {
494        if (DEBUG) Log.v(TAG, "release " + this);
495        if (mLayerDrawable != null) {
496            mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable());
497            mLayerDrawable.setDrawableByLayerId(R.id.background_imageout, createEmptyDrawable());
498            mLayerDrawable = null;
499        }
500        mLayerWrapper = null;
501        mImageInWrapper = null;
502        mImageOutWrapper = null;
503        mColorWrapper = null;
504        mDimWrapper = null;
505        mThemeDrawable = null;
506        if (mChangeRunnable != null) {
507            mChangeRunnable.cancel();
508            mChangeRunnable = null;
509        }
510        releaseBackgroundBitmap();
511    }
512
513    private void releaseBackgroundBitmap() {
514        mBackgroundDrawable = null;
515    }
516
517    private void updateImmediate() {
518        lazyInit();
519
520        mColorWrapper.setColor(mBackgroundColor);
521        if (mDimWrapper != null) {
522            mDimWrapper.setAlpha(mBackgroundColor == Color.TRANSPARENT ? 0 : DIM_ALPHA_ON_SOLID);
523        }
524        showWallpaper(mBackgroundColor == Color.TRANSPARENT);
525
526        mThemeDrawable = getThemeDrawable();
527        mLayerDrawable.setDrawableByLayerId(R.id.background_theme, mThemeDrawable);
528
529        if (mBackgroundDrawable == null) {
530            mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable());
531        } else {
532            if (DEBUG) Log.v(TAG, "Background drawable is available");
533            mImageInWrapper = new DrawableWrapper(mBackgroundDrawable);
534            mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, mBackgroundDrawable);
535            if (mDimWrapper != null) {
536                mDimWrapper.setAlpha(FULL_ALPHA);
537            }
538        }
539    }
540
541    /**
542     * Set the background to the given color. The timing for when this becomes
543     * visible in the app is undefined and may take place after a small delay.
544     */
545    public void setColor(int color) {
546        if (DEBUG) Log.v(TAG, "setColor " + Integer.toHexString(color));
547
548        mBackgroundColor = color;
549        mService.setColor(mBackgroundColor);
550
551        if (mColorWrapper != null) {
552            mColorWrapper.setColor(mBackgroundColor);
553        }
554    }
555
556    /**
557     * Set the given drawable into the background. The provided Drawable will be
558     * used unmodified as the background, without any scaling or cropping
559     * applied to it. The timing for when this becomes visible in the app is
560     * undefined and may take place after a small delay.
561     */
562    public void setDrawable(Drawable drawable) {
563        if (DEBUG) Log.v(TAG, "setBackgroundDrawable " + drawable);
564        setDrawableInternal(drawable);
565    }
566
567    private void setDrawableInternal(Drawable drawable) {
568        if (!mAttached) {
569            throw new IllegalStateException("Must attach before setting background drawable");
570        }
571
572        if (mChangeRunnable != null) {
573            if (sameDrawable(drawable, mChangeRunnable.mDrawable)) {
574                if (DEBUG) Log.v(TAG, "setting same drawable");
575                return;
576            }
577            mChangeRunnable.cancel();
578        }
579        mChangeRunnable = new ChangeBackgroundRunnable(drawable);
580
581        if (mImageInWrapper != null && mImageInWrapper.isAnimationStarted()) {
582            if (DEBUG) Log.v(TAG, "animation in progress");
583        } else {
584            mHandler.postDelayed(mChangeRunnable, CHANGE_BG_DELAY_MS);
585        }
586    }
587
588    /**
589     * Set the given bitmap into the background. When using setBitmap to set the
590     * background, the provided bitmap will be scaled and cropped to correctly
591     * fit within the dimensions of the view. The timing for when this becomes
592     * visible in the app is undefined and may take place after a small delay.
593     */
594    public void setBitmap(Bitmap bitmap) {
595        if (DEBUG) {
596            Log.v(TAG, "setBitmap " + bitmap);
597        }
598
599        if (bitmap == null) {
600            setDrawableInternal(null);
601            return;
602        }
603
604        if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
605            if (DEBUG) {
606                Log.v(TAG, "invalid bitmap width or height");
607            }
608            return;
609        }
610
611        Matrix matrix = null;
612
613        if ((bitmap.getWidth() != mWidthPx || bitmap.getHeight() != mHeightPx)) {
614            int dwidth = bitmap.getWidth();
615            int dheight = bitmap.getHeight();
616            float scale;
617
618            // Scale proportionately to fit width and height.
619            if (dwidth * mHeightPx > mWidthPx * dheight) {
620                scale = (float) mHeightPx / (float) dheight;
621            } else {
622                scale = (float) mWidthPx / (float) dwidth;
623            }
624
625            int subX = Math.min((int) (mWidthPx / scale), dwidth);
626            int dx = Math.max(0, (dwidth - subX) / 2);
627
628            matrix = new Matrix();
629            matrix.setScale(scale, scale);
630            matrix.preTranslate(-dx, 0);
631
632            if (DEBUG) Log.v(TAG, "original image size " + bitmap.getWidth() + "x" + bitmap.getHeight() +
633                    " scale " + scale + " dx " + dx);
634        }
635
636        BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap, matrix);
637
638        setDrawableInternal(bitmapDrawable);
639    }
640
641    private void applyBackgroundChanges() {
642        if (!mAttached || mLayerWrapper == null) {
643            return;
644        }
645
646        if (DEBUG) Log.v(TAG, "applyBackgroundChanges drawable " + mBackgroundDrawable);
647
648        int dimAlpha = 0;
649
650        if (mImageOutWrapper != null && mImageOutWrapper.isAnimationPending()) {
651            if (DEBUG) Log.v(TAG, "mImageOutWrapper animation starting");
652            mImageOutWrapper.startAnimation();
653            mImageOutWrapper = null;
654            dimAlpha = DIM_ALPHA_ON_SOLID;
655        }
656
657        if (mImageInWrapper == null && mBackgroundDrawable != null) {
658            if (DEBUG) Log.v(TAG, "creating new imagein drawable");
659            mImageInWrapper = new DrawableWrapper(mBackgroundDrawable);
660            mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, mBackgroundDrawable);
661            if (DEBUG) Log.v(TAG, "mImageInWrapper animation starting");
662            mImageInWrapper.setAlpha(0);
663            mImageInWrapper.fadeIn(FADE_DURATION, 0);
664            mImageInWrapper.startAnimation(mImageInListener);
665            dimAlpha = FULL_ALPHA;
666        }
667
668        if (mDimWrapper != null && dimAlpha != 0) {
669            if (DEBUG) Log.v(TAG, "dimwrapper animation starting to " + dimAlpha);
670            mDimWrapper.fade(FADE_DURATION, 0, dimAlpha);
671            mDimWrapper.startAnimation();
672        }
673    }
674
675    private final Animator.AnimatorListener mImageInListener = new Animator.AnimatorListener() {
676        @Override
677        public void onAnimationStart(Animator animation) {
678        }
679        @Override
680        public void onAnimationRepeat(Animator animation) {
681        }
682        @Override
683        public void onAnimationEnd(Animator animation) {
684            if (mChangeRunnable != null) {
685                if (DEBUG) Log.v(TAG, "animation ended, found change runnable");
686                mChangeRunnable.run();
687            }
688        }
689        @Override
690        public void onAnimationCancel(Animator animation) {
691        }
692    };
693
694    /**
695     * Returns the current background color.
696     */
697    public final int getColor() {
698        return mBackgroundColor;
699    }
700
701    /**
702     * Returns the current background {@link Drawable}.
703     */
704    public Drawable getDrawable() {
705        return mBackgroundDrawable;
706    }
707
708    private boolean sameDrawable(Drawable first, Drawable second) {
709        if (first == null || second == null) {
710            return false;
711        }
712        if (first == second) {
713            return true;
714        }
715        if (first instanceof BitmapDrawable && second instanceof BitmapDrawable) {
716            if (((BitmapDrawable) first).getBitmap().sameAs(((BitmapDrawable) second).getBitmap())) {
717                return true;
718            }
719        }
720        return false;
721    }
722
723    /**
724     * Task which changes the background.
725     */
726    class ChangeBackgroundRunnable implements Runnable {
727        private Drawable mDrawable;
728        private boolean mCancel;
729
730        ChangeBackgroundRunnable(Drawable drawable) {
731            mDrawable = drawable;
732        }
733
734        public void cancel() {
735            mCancel = true;
736        }
737
738        @Override
739        public void run() {
740            if (!mCancel) {
741                runTask();
742            }
743        }
744
745        private void runTask() {
746            lazyInit();
747
748            if (sameDrawable(mDrawable, mBackgroundDrawable)) {
749                if (DEBUG) Log.v(TAG, "same bitmap detected");
750                return;
751            }
752
753            releaseBackgroundBitmap();
754
755            if (mImageInWrapper != null) {
756                mImageOutWrapper = new DrawableWrapper(mImageInWrapper.getDrawable());
757                mImageOutWrapper.setAlpha(mImageInWrapper.getAlpha());
758                mImageOutWrapper.fadeOut(FADE_DURATION);
759
760                // Order is important! Setting a drawable "removes" the
761                // previous one from the view
762                mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable());
763                mLayerDrawable.setDrawableByLayerId(R.id.background_imageout,
764                        mImageOutWrapper.getDrawable());
765                mImageInWrapper.setAlpha(0);
766                mImageInWrapper = null;
767            }
768
769            mBackgroundDrawable = mDrawable;
770            mService.setDrawable(mBackgroundDrawable);
771
772            applyBackgroundChanges();
773
774            mChangeRunnable = null;
775        }
776    }
777
778    private Drawable createEmptyDrawable() {
779        Bitmap bitmap = null;
780        return new BitmapDrawable(mContext.getResources(), bitmap);
781    }
782
783    private void showWallpaper(boolean show) {
784        if (mWindow == null) {
785            return;
786        }
787
788        WindowManager.LayoutParams layoutParams = mWindow.getAttributes();
789        if (show) {
790            if ((layoutParams.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) {
791                return;
792            }
793            if (DEBUG) Log.v(TAG, "showing wallpaper");
794            layoutParams.flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
795        } else {
796            if ((layoutParams.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) == 0) {
797                return;
798            }
799            if (DEBUG) Log.v(TAG, "hiding wallpaper");
800            layoutParams.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
801        }
802
803        mWindow.setAttributes(layoutParams);
804    }
805}
806