/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package android.support.v17.leanback.app; import android.support.v17.leanback.R; import android.animation.ObjectAnimator; import android.app.Activity; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.os.Handler; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.view.animation.LinearInterpolator; /** * Supports background image continuity between multiple Activities. * *
An Activity should instantiate a BackgroundManager and {@link #attach} * to the Activity's window. When the Activity is started, the background is * initialized to the current background values stored in a continuity service. * The background continuity service is updated as the background is updated. * *
At some point, for example when it is stopped, the Activity may release * its background state. * *
When an Activity is resumed, if the BackgroundManager has not been * released, the continuity service is updated from the BackgroundManager state. * If the BackgroundManager was released, the BackgroundManager inherits the * current state from the continuity service. * *
When the last Activity is destroyed, the background state is reset. * *
Backgrounds consist of several layers, from back to front: *
BackgroundManager holds references to potentially large bitmap Drawables. * Call {@link #release} to release these references when the Activity is not * visible. */ // TODO: support for multiple app processes requires a proper android service // instead of the shared memory "service" implemented here. Such a service could // support continuity between fragments of different applications if desired. public final class BackgroundManager { private static final String TAG = "BackgroundManager"; private static final boolean DEBUG = false; private static final int FULL_ALPHA = 255; private static final int DIM_ALPHA_ON_SOLID = (int) (0.8f * FULL_ALPHA); private static final int CHANGE_BG_DELAY_MS = 500; private static final int FADE_DURATION_QUICK = 200; private static final int FADE_DURATION_SLOW = 1000; /** * Using a separate window for backgrounds can improve graphics performance by * leveraging hardware display layers. * TODO: support a leanback configuration option. */ private static final boolean USE_SEPARATE_WINDOW = false; /** * If true, bitmaps will be scaled to the exact display size. * Small bitmaps will be scaled up, using more memory but improving display quality. * Large bitmaps will be scaled down to use less memory. * Introduces an allocation overhead. * TODO: support a leanback configuration option. */ private static final boolean SCALE_BITMAPS_TO_FIT = true; private static final String WINDOW_NAME = "BackgroundManager"; private static final String FRAGMENT_TAG = BackgroundManager.class.getCanonicalName(); private Context mContext; private Handler mHandler; private Window mWindow; private WindowManager mWindowManager; private View mBgView; private BackgroundContinuityService mService; private int mThemeDrawableResourceId; private int mHeightPx; private int mWidthPx; private Drawable mBackgroundDrawable; private int mBackgroundColor; private boolean mAttached; private class DrawableWrapper { protected int mAlpha; protected Drawable mDrawable; protected ObjectAnimator mAnimator; protected boolean mAnimationPending; public DrawableWrapper(Drawable drawable) { mDrawable = drawable; setAlpha(FULL_ALPHA); } public Drawable getDrawable() { return mDrawable; } public void setAlpha(int alpha) { mAlpha = alpha; mDrawable.setAlpha(alpha); } public int getAlpha() { return mAlpha; } public void setColor(int color) { ((ColorDrawable) mDrawable).setColor(color); } public void fadeIn(int durationMs, int delayMs) { fade(durationMs, delayMs, FULL_ALPHA); } public void fadeOut(int durationMs) { fade(durationMs, 0, 0); } public void fade(int durationMs, int delayMs, int alpha) { if (mAnimator != null && mAnimator.isStarted()) { mAnimator.cancel(); } mAnimator = ObjectAnimator.ofInt(this, "alpha", alpha); mAnimator.setInterpolator(new LinearInterpolator()); mAnimator.setDuration(durationMs); mAnimator.setStartDelay(delayMs); mAnimationPending = true; } public boolean isAnimationPending() { return mAnimationPending; } public boolean isAnimationStarted() { return mAnimator != null && mAnimator.isStarted(); } public void startAnimation() { mAnimator.start(); mAnimationPending = false; } } private LayerDrawable mLayerDrawable; private DrawableWrapper mLayerWrapper; private DrawableWrapper mImageInWrapper; private DrawableWrapper mImageOutWrapper; private DrawableWrapper mColorWrapper; private DrawableWrapper mDimWrapper; private Drawable mThemeDrawable; private ChangeBackgroundRunnable mChangeRunnable; /** * Shared memory continuity service. */ private static class BackgroundContinuityService { private static final String TAG = "BackgroundContinuityService"; private static boolean DEBUG = BackgroundManager.DEBUG; private static BackgroundContinuityService sService = new BackgroundContinuityService(); private int mColor; private Drawable mDrawable; private int mCount; private BackgroundContinuityService() { reset(); } private void reset() { mColor = Color.TRANSPARENT; mDrawable = null; } public static BackgroundContinuityService getInstance() { final int count = sService.mCount++; if (DEBUG) Log.v(TAG, "Returning instance with new count " + count); return sService; } public void unref() { if (mCount <= 0) throw new IllegalStateException("Can't unref, count " + mCount); if (--mCount == 0) { if (DEBUG) Log.v(TAG, "mCount is zero, resetting"); reset(); } } public int getColor() { return mColor; } public Drawable getDrawable() { return mDrawable; } public void setColor(int color) { mColor = color; } public void setDrawable(Drawable drawable) { mDrawable = drawable; } } private Drawable getThemeDrawable() { Drawable drawable = null; if (mThemeDrawableResourceId != -1) { drawable = mContext.getResources().getDrawable(mThemeDrawableResourceId); } if (drawable == null) { drawable = createEmptyDrawable(); } return drawable; } /** * Get the BackgroundManager associated with the Activity. *
* The BackgroundManager will be created on-demand for each individual * Activity. Subsequent calls will return the same BackgroundManager created * for this Activity. */ public static BackgroundManager getInstance(Activity activity) { BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager() .findFragmentByTag(FRAGMENT_TAG); if (fragment != null) { BackgroundManager manager = fragment.getBackgroundManager(); if (manager != null) { return manager; } // manager is null: this is a fragment restored by FragmentManager, // fall through to create a BackgroundManager attach to it. } return new BackgroundManager(activity); } /** * Construct a BackgroundManager instance. The Initial background is set * from the continuity service. * @deprecated Use getInstance(Activity). */ @Deprecated public BackgroundManager(Activity activity) { mContext = activity; mService = BackgroundContinuityService.getInstance(); mHeightPx = mContext.getResources().getDisplayMetrics().heightPixels; mWidthPx = mContext.getResources().getDisplayMetrics().widthPixels; mHandler = new Handler(); TypedArray ta = activity.getTheme().obtainStyledAttributes(new int[] { android.R.attr.windowBackground }); mThemeDrawableResourceId = ta.getResourceId(0, -1); if (mThemeDrawableResourceId < 0) { if (DEBUG) Log.v(TAG, "BackgroundManager no window background resource!"); } ta.recycle(); createFragment(activity); } private void createFragment(Activity activity) { // Use a fragment to ensure the background manager gets detached properly. BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager() .findFragmentByTag(FRAGMENT_TAG); if (fragment == null) { fragment = new BackgroundFragment(); activity.getFragmentManager().beginTransaction().add(fragment, FRAGMENT_TAG).commit(); } else { if (fragment.getBackgroundManager() != null) { throw new IllegalStateException("Created duplicated BackgroundManager for same " + "activity, please use getInstance() instead"); } } fragment.setBackgroundManager(this); } /** * Synchronizes state when the owning Activity is resumed. */ void onActivityResume() { if (mService == null) { return; } if (mLayerDrawable == null) { if (DEBUG) Log.v(TAG, "onActivityResume: released state, syncing with service"); syncWithService(); } else { if (DEBUG) Log.v(TAG, "onActivityResume: updating service color " + mBackgroundColor + " drawable " + mBackgroundDrawable); mService.setColor(mBackgroundColor); mService.setDrawable(mBackgroundDrawable); } } private void syncWithService() { int color = mService.getColor(); Drawable drawable = mService.getDrawable(); if (DEBUG) Log.v(TAG, "syncWithService color " + Integer.toHexString(color) + " drawable " + drawable); if (drawable != null) { drawable = drawable.getConstantState().newDrawable(mContext.getResources()).mutate(); } mBackgroundColor = color; mBackgroundDrawable = drawable; updateImmediate(); } private void lazyInit() { if (mLayerDrawable != null) { return; } mLayerDrawable = (LayerDrawable) mContext.getResources().getDrawable( R.drawable.lb_background); mBgView.setBackground(mLayerDrawable); mLayerDrawable.setDrawableByLayerId(R.id.background_imageout, createEmptyDrawable()); mDimWrapper = new DrawableWrapper( mLayerDrawable.findDrawableByLayerId(R.id.background_dim)); mLayerWrapper = new DrawableWrapper(mLayerDrawable); mColorWrapper = new DrawableWrapper( mLayerDrawable.findDrawableByLayerId(R.id.background_color)); } /** * Make the background visible on the given Window. */ public void attach(Window window) { if (USE_SEPARATE_WINDOW) { attachBehindWindow(window); } else { attachToView(window.getDecorView()); } } private void attachBehindWindow(Window window) { if (DEBUG) Log.v(TAG, "attachBehindWindow " + window); mWindow = window; mWindowManager = window.getWindowManager(); WindowManager.LayoutParams params = new WindowManager.LayoutParams( // Media window sits behind the main application window WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA, // Avoid default to software format RGBA WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, android.graphics.PixelFormat.TRANSLUCENT); params.setTitle(WINDOW_NAME); params.width = ViewGroup.LayoutParams.MATCH_PARENT; params.height = ViewGroup.LayoutParams.MATCH_PARENT; View backgroundView = LayoutInflater.from(mContext).inflate( R.layout.lb_background_window, null); mWindowManager.addView(backgroundView, params); attachToView(backgroundView); } private void attachToView(View sceneRoot) { mBgView = sceneRoot; mAttached = true; syncWithService(); } /** * Release references to Drawables and put the BackgroundManager into the * detached state. Called when the associated Activity is destroyed. * @hide */ void detach() { if (DEBUG) Log.v(TAG, "detach"); release(); if (mWindowManager != null && mBgView != null) { mWindowManager.removeViewImmediate(mBgView); } mWindowManager = null; mWindow = null; mBgView = null; mAttached = false; if (mService != null) { mService.unref(); mService = null; } } /** * Release references to Drawables. Typically called to reduce memory * overhead when not visible. *
* When an Activity is resumed, if the BackgroundManager has not been * released, the continuity service is updated from the BackgroundManager * state. If the BackgroundManager was released, the BackgroundManager * inherits the current state from the continuity service. */ public void release() { if (DEBUG) Log.v(TAG, "release"); if (mLayerDrawable != null) { mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable()); mLayerDrawable.setDrawableByLayerId(R.id.background_imageout, createEmptyDrawable()); mLayerDrawable = null; } mLayerWrapper = null; mImageInWrapper = null; mImageOutWrapper = null; mColorWrapper = null; mDimWrapper = null; mThemeDrawable = null; if (mChangeRunnable != null) { mChangeRunnable.cancel(); mChangeRunnable = null; } releaseBackgroundBitmap(); } private void releaseBackgroundBitmap() { mBackgroundDrawable = null; } private void updateImmediate() { lazyInit(); mColorWrapper.setColor(mBackgroundColor); if (mDimWrapper != null) { mDimWrapper.setAlpha(mBackgroundColor == Color.TRANSPARENT ? 0 : DIM_ALPHA_ON_SOLID); } showWallpaper(mBackgroundColor == Color.TRANSPARENT); mThemeDrawable = getThemeDrawable(); mLayerDrawable.setDrawableByLayerId(R.id.background_theme, mThemeDrawable); if (mBackgroundDrawable == null) { mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable()); } else { if (DEBUG) Log.v(TAG, "Background drawable is available"); mImageInWrapper = new DrawableWrapper(mBackgroundDrawable); mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, mBackgroundDrawable); if (mDimWrapper != null) { mDimWrapper.setAlpha(FULL_ALPHA); } } } /** * Set the background to the given color. The timing for when this becomes * visible in the app is undefined and may take place after a small delay. */ public void setColor(int color) { if (DEBUG) Log.v(TAG, "setColor " + Integer.toHexString(color)); mBackgroundColor = color; mService.setColor(mBackgroundColor); if (mColorWrapper != null) { mColorWrapper.setColor(mBackgroundColor); } } /** * Set the given drawable into the background. The provided Drawable will be * used unmodified as the background, without any scaling or cropping * applied to it. The timing for when this becomes visible in the app is * undefined and may take place after a small delay. */ public void setDrawable(Drawable drawable) { if (DEBUG) Log.v(TAG, "setBackgroundDrawable " + drawable); setDrawableInternal(drawable); } private void setDrawableInternal(Drawable drawable) { if (!mAttached) { throw new IllegalStateException("Must attach before setting background drawable"); } if (mChangeRunnable != null) { mChangeRunnable.cancel(); } mChangeRunnable = new ChangeBackgroundRunnable(drawable); mHandler.postDelayed(mChangeRunnable, CHANGE_BG_DELAY_MS); } /** * Set the given bitmap into the background. When using setBitmap to set the * background, the provided bitmap will be scaled and cropped to correctly * fit within the dimensions of the view. The timing for when this becomes * visible in the app is undefined and may take place after a small delay. */ public void setBitmap(Bitmap bitmap) { if (DEBUG) { Log.v(TAG, "setBitmap " + bitmap); } if (bitmap == null) { setDrawableInternal(null); return; } if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { if (DEBUG) { Log.v(TAG, "invalid bitmap width or height"); } return; } if (mBackgroundDrawable instanceof BitmapDrawable && ((BitmapDrawable) mBackgroundDrawable).getBitmap() == bitmap) { if (DEBUG) { Log.v(TAG, "same bitmap detected"); } mService.setDrawable(mBackgroundDrawable); return; } if (SCALE_BITMAPS_TO_FIT && (bitmap.getWidth() != mWidthPx || bitmap.getHeight() != mHeightPx)) { // Scale proportionately to fit width and height. Matrix matrix = new Matrix(); int dwidth = bitmap.getWidth(); int dheight = bitmap.getHeight(); float scale; int dx; if (DEBUG) { Log.v(TAG, "original image size " + dwidth + "x" + dheight); } if (dwidth * mHeightPx > mWidthPx * dheight) { scale = (float) mHeightPx / (float) dheight; } else { scale = (float) mWidthPx / (float) dwidth; } matrix.setScale(scale, scale); if (DEBUG) { Log.v(TAG, "original image size " + bitmap.getWidth() + "x" + bitmap.getHeight()); } int subX = Math.min((int) (mWidthPx / scale), dwidth); int subY = Math.min((int) (mHeightPx / scale), dheight); dx = Math.max(0, (dwidth - subX) / 2); bitmap = Bitmap.createBitmap(bitmap, dx, 0, subX, subY, matrix, true); if (DEBUG) { Log.v(TAG, "new image size " + bitmap.getWidth() + "x" + bitmap.getHeight()); } } BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap); setDrawableInternal(bitmapDrawable); } private void applyBackgroundChanges() { if (!mAttached || mLayerWrapper == null) { return; } if (DEBUG) Log.v(TAG, "applyBackgroundChanges drawable " + mBackgroundDrawable); int dimAlpha = 0; if (mImageOutWrapper != null && mImageOutWrapper.isAnimationPending()) { if (DEBUG) Log.v(TAG, "mImageOutWrapper animation starting"); mImageOutWrapper.startAnimation(); mImageOutWrapper = null; dimAlpha = DIM_ALPHA_ON_SOLID; } if (mImageInWrapper == null && mBackgroundDrawable != null) { if (DEBUG) Log.v(TAG, "creating new imagein drawable"); mImageInWrapper = new DrawableWrapper(mBackgroundDrawable); mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, mBackgroundDrawable); if (DEBUG) Log.v(TAG, "mImageInWrapper animation starting"); mImageInWrapper.setAlpha(0); mImageInWrapper.fadeIn(FADE_DURATION_SLOW, 0); mImageInWrapper.startAnimation(); dimAlpha = FULL_ALPHA; } if (mDimWrapper != null && dimAlpha != 0) { if (DEBUG) Log.v(TAG, "dimwrapper animation starting to " + dimAlpha); mDimWrapper.fade(FADE_DURATION_SLOW, 0, dimAlpha); mDimWrapper.startAnimation(); } } /** * Returns the current background color. */ public final int getColor() { return mBackgroundColor; } /** * Returns the current background {@link Drawable}. */ public Drawable getDrawable() { return mBackgroundDrawable; } /** * Task which changes the background. */ class ChangeBackgroundRunnable implements Runnable { private Drawable mDrawable; private boolean mCancel; ChangeBackgroundRunnable(Drawable drawable) { mDrawable = drawable; } public void cancel() { mCancel = true; } @Override public void run() { if (!mCancel) { runTask(); } } private void runTask() { boolean newBackground = false; lazyInit(); if (mDrawable != mBackgroundDrawable) { newBackground = true; if (mDrawable instanceof BitmapDrawable && mBackgroundDrawable instanceof BitmapDrawable) { if (((BitmapDrawable) mDrawable).getBitmap() == ((BitmapDrawable) mBackgroundDrawable).getBitmap()) { if (DEBUG) Log.v(TAG, "same underlying bitmap detected"); newBackground = false; } } } if (!newBackground) { return; } releaseBackgroundBitmap(); if (mImageInWrapper != null) { mImageOutWrapper = new DrawableWrapper(mImageInWrapper.getDrawable()); mImageOutWrapper.setAlpha(mImageInWrapper.getAlpha()); mImageOutWrapper.fadeOut(FADE_DURATION_QUICK); // Order is important! Setting a drawable "removes" the // previous one from the view mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable()); mLayerDrawable.setDrawableByLayerId(R.id.background_imageout, mImageOutWrapper.getDrawable()); mImageInWrapper.setAlpha(0); mImageInWrapper = null; } mBackgroundDrawable = mDrawable; mService.setDrawable(mBackgroundDrawable); applyBackgroundChanges(); } } private Drawable createEmptyDrawable() { Bitmap bitmap = null; return new BitmapDrawable(mContext.getResources(), bitmap); } private void showWallpaper(boolean show) { if (mWindow == null) { return; } WindowManager.LayoutParams layoutParams = mWindow.getAttributes(); if (show) { if ((layoutParams.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) { return; } if (DEBUG) Log.v(TAG, "showing wallpaper"); layoutParams.flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; } else { if ((layoutParams.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) == 0) { return; } if (DEBUG) Log.v(TAG, "hiding wallpaper"); layoutParams.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; } mWindow.setAttributes(layoutParams); } }