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