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