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