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.annotation.ColorInt; 19import android.graphics.PixelFormat; 20import android.graphics.PorterDuff; 21import android.graphics.PorterDuffColorFilter; 22import android.support.v17.leanback.R; 23import android.animation.Animator; 24import android.animation.ValueAnimator; 25import android.app.Activity; 26import android.content.Context; 27import android.content.res.Resources; 28import android.content.res.TypedArray; 29import android.graphics.Bitmap; 30import android.graphics.Canvas; 31import android.graphics.Color; 32import android.graphics.ColorFilter; 33import android.graphics.Matrix; 34import android.graphics.Paint; 35import android.graphics.drawable.ColorDrawable; 36import android.graphics.drawable.Drawable; 37import android.graphics.drawable.LayerDrawable; 38import android.os.Handler; 39import android.support.v17.leanback.widget.BackgroundHelper; 40import android.support.v4.view.animation.FastOutLinearInInterpolator; 41import android.util.Log; 42import android.view.LayoutInflater; 43import android.view.View; 44import android.view.ViewGroup; 45import android.view.Window; 46import android.view.WindowManager; 47import android.view.animation.Interpolator; 48import android.view.animation.AnimationUtils; 49import android.support.v4.app.FragmentActivity; 50import android.support.v4.content.ContextCompat; 51 52/** 53 * Supports background image continuity between multiple Activities. 54 * 55 * <p>An Activity should instantiate a BackgroundManager and {@link #attach} 56 * to the Activity's window. When the Activity is started, the background is 57 * initialized to the current background values stored in a continuity service. 58 * The background continuity service is updated as the background is updated. 59 * 60 * <p>At some point, for example when it is stopped, the Activity may release 61 * its background state. 62 * 63 * <p>When an Activity is resumed, if the BackgroundManager has not been 64 * released, the continuity service is updated from the BackgroundManager state. 65 * If the BackgroundManager was released, the BackgroundManager inherits the 66 * current state from the continuity service. 67 * 68 * <p>When the last Activity is destroyed, the background state is reset. 69 * 70 * <p>Backgrounds consist of several layers, from back to front: 71 * <ul> 72 * <li>the background Drawable of the theme</li> 73 * <li>a solid color (set via {@link #setColor})</li> 74 * <li>two Drawables, previous and current (set via {@link #setBitmap} or 75 * {@link #setDrawable}), which may be in transition</li> 76 * </ul> 77 * 78 * <p>BackgroundManager holds references to potentially large bitmap Drawables. 79 * Call {@link #release} to release these references when the Activity is not 80 * visible. 81 */ 82// TODO: support for multiple app processes requires a proper android service 83// instead of the shared memory "service" implemented here. Such a service could 84// support continuity between fragments of different applications if desired. 85public final class BackgroundManager { 86 87 interface FragmentStateQueriable { 88 public boolean isResumed(); 89 } 90 91 private static final String TAG = "BackgroundManager"; 92 private static final boolean DEBUG = false; 93 94 private static final int FULL_ALPHA = 255; 95 private static final int DIM_ALPHA_ON_SOLID = (int) (0.8f * FULL_ALPHA); 96 private static final int CHANGE_BG_DELAY_MS = 500; 97 private static final int FADE_DURATION = 500; 98 99 /** 100 * Using a separate window for backgrounds can improve graphics performance by 101 * leveraging hardware display layers. 102 * TODO: support a leanback configuration option. 103 */ 104 private static final boolean USE_SEPARATE_WINDOW = false; 105 106 private static final String WINDOW_NAME = "BackgroundManager"; 107 private static final String FRAGMENT_TAG = BackgroundManager.class.getCanonicalName(); 108 109 private Context mContext; 110 private Handler mHandler; 111 private Window mWindow; 112 private WindowManager mWindowManager; 113 private View mBgView; 114 private BackgroundContinuityService mService; 115 private int mThemeDrawableResourceId; 116 private FragmentStateQueriable mFragmentState; 117 118 private int mHeightPx; 119 private int mWidthPx; 120 private Drawable mBackgroundDrawable; 121 private int mBackgroundColor; 122 private boolean mAttached; 123 private long mLastSetTime; 124 125 private final Interpolator mAccelerateInterpolator; 126 private final Interpolator mDecelerateInterpolator; 127 private final ValueAnimator mAnimator; 128 private final ValueAnimator mDimAnimator; 129 130 private static class BitmapDrawable extends Drawable { 131 132 static class ConstantState extends Drawable.ConstantState { 133 Bitmap mBitmap; 134 Matrix mMatrix; 135 Paint mPaint; 136 137 @Override 138 public Drawable newDrawable() { 139 return new BitmapDrawable(null, mBitmap, mMatrix); 140 } 141 142 @Override 143 public int getChangingConfigurations() { 144 return 0; 145 } 146 } 147 148 private ConstantState mState = new ConstantState(); 149 150 BitmapDrawable(Resources resources, Bitmap bitmap) { 151 this(resources, bitmap, null); 152 } 153 154 BitmapDrawable(Resources resources, Bitmap bitmap, Matrix matrix) { 155 mState.mBitmap = bitmap; 156 mState.mMatrix = matrix != null ? matrix : new Matrix(); 157 mState.mPaint = new Paint(); 158 mState.mPaint.setFilterBitmap(true); 159 } 160 161 Bitmap getBitmap() { 162 return mState.mBitmap; 163 } 164 165 @Override 166 public void draw(Canvas canvas) { 167 if (mState.mBitmap == null) { 168 return; 169 } 170 if (mState.mPaint.getAlpha() < FULL_ALPHA && mState.mPaint.getColorFilter() != null) { 171 throw new IllegalStateException("Can't draw with translucent alpha and color filter"); 172 } 173 canvas.drawBitmap(mState.mBitmap, mState.mMatrix, mState.mPaint); 174 } 175 176 @Override 177 public int getOpacity() { 178 return android.graphics.PixelFormat.TRANSLUCENT; 179 } 180 181 @Override 182 public void setAlpha(int alpha) { 183 if (mState.mPaint.getAlpha() != alpha) { 184 mState.mPaint.setAlpha(alpha); 185 invalidateSelf(); 186 } 187 } 188 189 /** 190 * Does not invalidateSelf to avoid recursion issues. 191 * Caller must ensure appropriate invalidation. 192 */ 193 @Override 194 public void setColorFilter(ColorFilter cf) { 195 mState.mPaint.setColorFilter(cf); 196 } 197 198 public ColorFilter getColorFilter() { 199 return mState.mPaint.getColorFilter(); 200 } 201 202 @Override 203 public ConstantState getConstantState() { 204 return mState; 205 } 206 } 207 208 private static class DrawableWrapper { 209 private int mAlpha = FULL_ALPHA; 210 private Drawable mDrawable; 211 private ColorFilter mColorFilter; 212 213 public DrawableWrapper(Drawable drawable) { 214 mDrawable = drawable; 215 updateAlpha(); 216 updateColorFilter(); 217 } 218 public DrawableWrapper(DrawableWrapper wrapper, Drawable drawable) { 219 mDrawable = drawable; 220 mAlpha = wrapper.getAlpha(); 221 updateAlpha(); 222 mColorFilter = wrapper.getColorFilter(); 223 updateColorFilter(); 224 } 225 226 public Drawable getDrawable() { 227 return mDrawable; 228 } 229 public void setAlpha(int alpha) { 230 mAlpha = alpha; 231 updateAlpha(); 232 } 233 public int getAlpha() { 234 return mAlpha; 235 } 236 private void updateAlpha() { 237 mDrawable.setAlpha(mAlpha); 238 } 239 240 public ColorFilter getColorFilter() { 241 return mColorFilter; 242 } 243 public void setColorFilter(ColorFilter colorFilter) { 244 mColorFilter = colorFilter; 245 updateColorFilter(); 246 } 247 private void updateColorFilter() { 248 mDrawable.setColorFilter(mColorFilter); 249 } 250 251 public void setColor(int color) { 252 ((ColorDrawable) mDrawable).setColor(color); 253 } 254 } 255 256 static class TranslucentLayerDrawable extends LayerDrawable { 257 private DrawableWrapper[] mWrapper; 258 private Paint mPaint = new Paint(); 259 260 public TranslucentLayerDrawable(Drawable[] drawables) { 261 super(drawables); 262 int count = drawables.length; 263 mWrapper = new DrawableWrapper[count]; 264 for (int i = 0; i < count; i++) { 265 mWrapper[i] = new DrawableWrapper(drawables[i]); 266 } 267 } 268 269 @Override 270 public void setAlpha(int alpha) { 271 if (mPaint.getAlpha() != alpha) { 272 int previousAlpha = mPaint.getAlpha(); 273 mPaint.setAlpha(alpha); 274 invalidateSelf(); 275 onAlphaChanged(previousAlpha, alpha); 276 } 277 } 278 279 // Queried by system transitions 280 public int getAlpha() { 281 return mPaint.getAlpha(); 282 } 283 284 protected void onAlphaChanged(int oldAlpha, int newAlpha) { 285 } 286 287 @Override 288 public Drawable mutate() { 289 Drawable drawable = super.mutate(); 290 int count = getNumberOfLayers(); 291 for (int i = 0; i < count; i++) { 292 if (mWrapper[i] != null) { 293 mWrapper[i] = new DrawableWrapper(mWrapper[i], getDrawable(i)); 294 } 295 } 296 invalidateSelf(); 297 return drawable; 298 } 299 300 @Override 301 public int getOpacity() { 302 return PixelFormat.TRANSLUCENT; 303 } 304 305 @Override 306 public boolean setDrawableByLayerId(int id, Drawable drawable) { 307 return updateDrawable(id, drawable) != null; 308 } 309 310 public DrawableWrapper updateDrawable(int id, Drawable drawable) { 311 super.setDrawableByLayerId(id, drawable); 312 for (int i = 0; i < getNumberOfLayers(); i++) { 313 if (getId(i) == id) { 314 mWrapper[i] = new DrawableWrapper(drawable); 315 // Must come after mWrapper was updated so it can be seen by updateColorFilter 316 invalidateSelf(); 317 return mWrapper[i]; 318 } 319 } 320 return null; 321 } 322 323 public void clearDrawable(int id, Context context) { 324 for (int i = 0; i < getNumberOfLayers(); i++) { 325 if (getId(i) == id) { 326 mWrapper[i] = null; 327 super.setDrawableByLayerId(id, createEmptyDrawable(context)); 328 break; 329 } 330 } 331 } 332 333 public DrawableWrapper findWrapperById(int id) { 334 for (int i = 0; i < getNumberOfLayers(); i++) { 335 if (getId(i) == id) { 336 return mWrapper[i]; 337 } 338 } 339 return null; 340 } 341 342 @Override 343 public void draw(Canvas canvas) { 344 if (mPaint.getAlpha() < FULL_ALPHA) { 345 canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), 346 mPaint, Canvas.ALL_SAVE_FLAG); 347 } 348 super.draw(canvas); 349 if (mPaint.getAlpha() < FULL_ALPHA) { 350 canvas.restore(); 351 } 352 } 353 } 354 355 /** 356 * Optimizes drawing when the dim drawable is an alpha-only color and imagein is opaque. 357 * When the layer drawable is translucent (activity transition) then we can avoid the slow 358 * saveLayer/restore draw path. 359 */ 360 private class OptimizedTranslucentLayerDrawable extends TranslucentLayerDrawable { 361 private PorterDuffColorFilter mColorFilter; 362 private boolean mUpdatingColorFilter; 363 364 public OptimizedTranslucentLayerDrawable(Drawable[] drawables) { 365 super(drawables); 366 } 367 368 @Override 369 protected void onAlphaChanged(int oldAlpha, int newAlpha) { 370 if (newAlpha == FULL_ALPHA && oldAlpha < FULL_ALPHA) { 371 if (DEBUG) Log.v(TAG, "transition complete"); 372 postChangeRunnable(); 373 } 374 } 375 376 @Override 377 public void invalidateSelf() { 378 super.invalidateSelf(); 379 updateColorFilter(); 380 } 381 382 @Override 383 public void invalidateDrawable(Drawable who) { 384 if (!mUpdatingColorFilter) { 385 invalidateSelf(); 386 } 387 } 388 389 private void updateColorFilter() { 390 DrawableWrapper dimWrapper = findWrapperById(R.id.background_dim); 391 DrawableWrapper imageInWrapper = findWrapperById(R.id.background_imagein); 392 DrawableWrapper imageOutWrapper = findWrapperById(R.id.background_imageout); 393 394 mColorFilter = null; 395 if (imageInWrapper != null && imageInWrapper.getAlpha() == FULL_ALPHA && 396 dimWrapper.getDrawable() instanceof ColorDrawable) { 397 int dimColor = ((ColorDrawable) dimWrapper.getDrawable()).getColor(); 398 if (Color.red(dimColor) == 0 && 399 Color.green(dimColor) == 0 && 400 Color.blue(dimColor) == 0) { 401 int dimAlpha = 255 - Color.alpha(dimColor); 402 int color = Color.argb(getAlpha(), dimAlpha, dimAlpha, dimAlpha); 403 mColorFilter = new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY); 404 } 405 } 406 mUpdatingColorFilter = true; 407 if (imageInWrapper != null) { 408 imageInWrapper.setColorFilter(mColorFilter); 409 } 410 if (imageOutWrapper != null) { 411 imageOutWrapper.setColorFilter(null); 412 } 413 mUpdatingColorFilter = false; 414 } 415 416 @Override 417 public void draw(Canvas canvas) { 418 DrawableWrapper imageInWrapper = findWrapperById(R.id.background_imagein); 419 if (imageInWrapper != null && imageInWrapper.getDrawable() != null && 420 imageInWrapper.getColorFilter() != null) { 421 imageInWrapper.getDrawable().draw(canvas); 422 } else { 423 super.draw(canvas); 424 } 425 } 426 } 427 428 private TranslucentLayerDrawable createOptimizedTranslucentLayerDrawable( 429 LayerDrawable layerDrawable) { 430 int numChildren = layerDrawable.getNumberOfLayers(); 431 Drawable[] drawables = new Drawable[numChildren]; 432 for (int i = 0; i < numChildren; i++) { 433 drawables[i] = layerDrawable.getDrawable(i); 434 } 435 TranslucentLayerDrawable result = new OptimizedTranslucentLayerDrawable(drawables); 436 for (int i = 0; i < numChildren; i++) { 437 result.setId(i, layerDrawable.getId(i)); 438 } 439 return result; 440 } 441 442 private TranslucentLayerDrawable mLayerDrawable; 443 private Drawable mDimDrawable; 444 private ChangeBackgroundRunnable mChangeRunnable; 445 private boolean mChangeRunnablePending; 446 447 private final Animator.AnimatorListener mAnimationListener = new Animator.AnimatorListener() { 448 final Runnable mRunnable = new Runnable() { 449 @Override 450 public void run() { 451 postChangeRunnable(); 452 } 453 }; 454 455 @Override 456 public void onAnimationStart(Animator animation) { 457 } 458 @Override 459 public void onAnimationRepeat(Animator animation) { 460 } 461 @Override 462 public void onAnimationEnd(Animator animation) { 463 if (mLayerDrawable != null) { 464 mLayerDrawable.clearDrawable(R.id.background_imageout, mContext); 465 } 466 mHandler.post(mRunnable); 467 } 468 @Override 469 public void onAnimationCancel(Animator animation) { 470 } 471 }; 472 473 private final ValueAnimator.AnimatorUpdateListener mAnimationUpdateListener = 474 new ValueAnimator.AnimatorUpdateListener() { 475 @Override 476 public void onAnimationUpdate(ValueAnimator animation) { 477 int fadeInAlpha = (Integer) animation.getAnimatedValue(); 478 DrawableWrapper imageInWrapper = getImageInWrapper(); 479 if (imageInWrapper != null) { 480 imageInWrapper.setAlpha(fadeInAlpha); 481 } else { 482 DrawableWrapper imageOutWrapper = getImageOutWrapper(); 483 if (imageOutWrapper != null) { 484 imageOutWrapper.setAlpha(255 - fadeInAlpha); 485 } 486 } 487 } 488 }; 489 490 private final ValueAnimator.AnimatorUpdateListener mDimUpdateListener = 491 new ValueAnimator.AnimatorUpdateListener() { 492 @Override 493 public void onAnimationUpdate(ValueAnimator animation) { 494 DrawableWrapper dimWrapper = getDimWrapper(); 495 if (dimWrapper != null) { 496 dimWrapper.setAlpha((Integer) animation.getAnimatedValue()); 497 } 498 } 499 }; 500 501 /** 502 * Shared memory continuity service. 503 */ 504 private static class BackgroundContinuityService { 505 private static final String TAG = "BackgroundContinuityService"; 506 private static boolean DEBUG = BackgroundManager.DEBUG; 507 508 private static BackgroundContinuityService sService = new BackgroundContinuityService(); 509 510 private int mColor; 511 private Drawable mDrawable; 512 private int mCount; 513 514 /** Single cache of theme drawable */ 515 private int mLastThemeDrawableId; 516 private WeakReference<Drawable.ConstantState> mLastThemeDrawableState; 517 518 private BackgroundContinuityService() { 519 reset(); 520 } 521 522 private void reset() { 523 mColor = Color.TRANSPARENT; 524 mDrawable = null; 525 } 526 527 public static BackgroundContinuityService getInstance() { 528 final int count = sService.mCount++; 529 if (DEBUG) Log.v(TAG, "Returning instance with new count " + count); 530 return sService; 531 } 532 533 public void unref() { 534 if (mCount <= 0) throw new IllegalStateException("Can't unref, count " + mCount); 535 if (--mCount == 0) { 536 if (DEBUG) Log.v(TAG, "mCount is zero, resetting"); 537 reset(); 538 } 539 } 540 public int getColor() { 541 return mColor; 542 } 543 public Drawable getDrawable() { 544 return mDrawable; 545 } 546 public void setColor(int color) { 547 mColor = color; 548 } 549 public void setDrawable(Drawable drawable) { 550 mDrawable = drawable; 551 } 552 public Drawable getThemeDrawable(Context context, int themeDrawableId) { 553 Drawable drawable = null; 554 if (mLastThemeDrawableState != null && mLastThemeDrawableId == themeDrawableId) { 555 Drawable.ConstantState drawableState = mLastThemeDrawableState.get(); 556 if (DEBUG) Log.v(TAG, "got cached theme drawable state " + drawableState); 557 if (drawableState != null) { 558 drawable = drawableState.newDrawable(); 559 } 560 } 561 if (drawable == null) { 562 drawable = ContextCompat.getDrawable(context, themeDrawableId); 563 if (DEBUG) Log.v(TAG, "loaded theme drawable " + drawable); 564 mLastThemeDrawableState = new WeakReference<Drawable.ConstantState>( 565 drawable.getConstantState()); 566 mLastThemeDrawableId = themeDrawableId; 567 } 568 // No mutate required because this drawable is never manipulated. 569 return drawable; 570 } 571 } 572 573 private Drawable getThemeDrawable() { 574 Drawable drawable = null; 575 if (mThemeDrawableResourceId != -1) { 576 drawable = mService.getThemeDrawable(mContext, mThemeDrawableResourceId); 577 } 578 if (drawable == null) { 579 drawable = createEmptyDrawable(mContext); 580 } 581 return drawable; 582 } 583 584 /** 585 * Returns the BackgroundManager associated with the given Activity. 586 * <p> 587 * The BackgroundManager will be created on-demand for each individual 588 * Activity. Subsequent calls will return the same BackgroundManager created 589 * for this Activity. 590 */ 591 public static BackgroundManager getInstance(Activity activity) { 592 if (activity instanceof FragmentActivity) { 593 return getSupportInstance((FragmentActivity) activity); 594 } 595 BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager() 596 .findFragmentByTag(FRAGMENT_TAG); 597 if (fragment != null) { 598 BackgroundManager manager = fragment.getBackgroundManager(); 599 if (manager != null) { 600 return manager; 601 } 602 // manager is null: this is a fragment restored by FragmentManager, 603 // fall through to create a BackgroundManager attach to it. 604 } 605 return new BackgroundManager(activity, false); 606 } 607 608 private static BackgroundManager getSupportInstance(FragmentActivity activity) { 609 BackgroundSupportFragment fragment = (BackgroundSupportFragment) activity 610 .getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG); 611 if (fragment != null) { 612 BackgroundManager manager = fragment.getBackgroundManager(); 613 if (manager != null) { 614 return manager; 615 } 616 // manager is null: this is a fragment restored by FragmentManager, 617 // fall through to create a BackgroundManager attach to it. 618 } 619 return new BackgroundManager(activity, true); 620 } 621 622 private BackgroundManager(Activity activity, boolean isSupportFragmentActivity) { 623 mContext = activity; 624 mService = BackgroundContinuityService.getInstance(); 625 mHeightPx = mContext.getResources().getDisplayMetrics().heightPixels; 626 mWidthPx = mContext.getResources().getDisplayMetrics().widthPixels; 627 mHandler = new Handler(); 628 629 Interpolator defaultInterpolator = new FastOutLinearInInterpolator(); 630 mAccelerateInterpolator = AnimationUtils.loadInterpolator(mContext, 631 android.R.anim.accelerate_interpolator); 632 mDecelerateInterpolator = AnimationUtils.loadInterpolator(mContext, 633 android.R.anim.decelerate_interpolator); 634 635 mAnimator = ValueAnimator.ofInt(0, FULL_ALPHA); 636 mAnimator.addListener(mAnimationListener); 637 mAnimator.addUpdateListener(mAnimationUpdateListener); 638 mAnimator.setInterpolator(defaultInterpolator); 639 640 mDimAnimator = new ValueAnimator(); 641 mDimAnimator.addUpdateListener(mDimUpdateListener); 642 643 TypedArray ta = activity.getTheme().obtainStyledAttributes(new int[] { 644 android.R.attr.windowBackground }); 645 mThemeDrawableResourceId = ta.getResourceId(0, -1); 646 if (mThemeDrawableResourceId < 0) { 647 if (DEBUG) Log.v(TAG, "BackgroundManager no window background resource!"); 648 } 649 ta.recycle(); 650 651 if (isSupportFragmentActivity) { 652 createSupportFragment((FragmentActivity) activity); 653 } else { 654 createFragment(activity); 655 } 656 } 657 658 private void createFragment(Activity activity) { 659 // Use a fragment to ensure the background manager gets detached properly. 660 BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager() 661 .findFragmentByTag(FRAGMENT_TAG); 662 if (fragment == null) { 663 fragment = new BackgroundFragment(); 664 activity.getFragmentManager().beginTransaction().add(fragment, FRAGMENT_TAG).commit(); 665 } else { 666 if (fragment.getBackgroundManager() != null) { 667 throw new IllegalStateException("Created duplicated BackgroundManager for same " + 668 "activity, please use getInstance() instead"); 669 } 670 } 671 fragment.setBackgroundManager(this); 672 mFragmentState = fragment; 673 } 674 675 private void createSupportFragment(FragmentActivity activity) { 676 // Use a fragment to ensure the background manager gets detached properly. 677 BackgroundSupportFragment fragment = (BackgroundSupportFragment) activity 678 .getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG); 679 if (fragment == null) { 680 fragment = new BackgroundSupportFragment(); 681 activity.getSupportFragmentManager().beginTransaction().add(fragment, FRAGMENT_TAG) 682 .commit(); 683 } else { 684 if (fragment.getBackgroundManager() != null) { 685 throw new IllegalStateException("Created duplicated BackgroundManager for same " + 686 "activity, please use getInstance() instead"); 687 } 688 } 689 fragment.setBackgroundManager(this); 690 mFragmentState = fragment; 691 } 692 693 private DrawableWrapper getImageInWrapper() { 694 return mLayerDrawable == null ? null : 695 mLayerDrawable.findWrapperById(R.id.background_imagein); 696 } 697 698 private DrawableWrapper getImageOutWrapper() { 699 return mLayerDrawable == null ? null : 700 mLayerDrawable.findWrapperById(R.id.background_imageout); 701 } 702 703 private DrawableWrapper getDimWrapper() { 704 return mLayerDrawable == null ? null : 705 mLayerDrawable.findWrapperById(R.id.background_dim); 706 } 707 708 private DrawableWrapper getColorWrapper() { 709 return mLayerDrawable == null ? null : 710 mLayerDrawable.findWrapperById(R.id.background_color); 711 } 712 713 /** 714 * Synchronizes state when the owning Activity is started. 715 * At that point the view becomes visible. 716 */ 717 void onActivityStart() { 718 if (mService == null) { 719 return; 720 } 721 if (mLayerDrawable == null) { 722 if (DEBUG) Log.v(TAG, "onActivityStart " + this + 723 " released state, syncing with service"); 724 syncWithService(); 725 } else { 726 if (DEBUG) Log.v(TAG, "onActivityStart " + this + " updating service color " 727 + mBackgroundColor + " drawable " + mBackgroundDrawable); 728 mService.setColor(mBackgroundColor); 729 mService.setDrawable(mBackgroundDrawable); 730 } 731 } 732 733 void onResume() { 734 if (DEBUG) Log.v(TAG, "onResume " + this); 735 postChangeRunnable(); 736 } 737 738 private void syncWithService() { 739 int color = mService.getColor(); 740 Drawable drawable = mService.getDrawable(); 741 742 if (DEBUG) Log.v(TAG, "syncWithService color " + Integer.toHexString(color) 743 + " drawable " + drawable); 744 745 mBackgroundColor = color; 746 mBackgroundDrawable = drawable == null ? null : 747 drawable.getConstantState().newDrawable().mutate(); 748 749 updateImmediate(); 750 } 751 752 private void lazyInit() { 753 if (mLayerDrawable != null) { 754 return; 755 } 756 757 LayerDrawable layerDrawable = (LayerDrawable) 758 ContextCompat.getDrawable(mContext, R.drawable.lb_background).mutate(); 759 mLayerDrawable = createOptimizedTranslucentLayerDrawable(layerDrawable); 760 BackgroundHelper.setBackgroundPreservingAlpha(mBgView, mLayerDrawable); 761 762 mLayerDrawable.clearDrawable(R.id.background_imageout, mContext); 763 mLayerDrawable.updateDrawable(R.id.background_theme, getThemeDrawable()); 764 765 updateDimWrapper(); 766 } 767 768 private void updateDimWrapper() { 769 if (mDimDrawable == null) { 770 mDimDrawable = getDefaultDimLayer(); 771 } 772 Drawable dimDrawable = mDimDrawable.getConstantState().newDrawable( 773 mContext.getResources()).mutate(); 774 if (mLayerDrawable != null) { 775 mLayerDrawable.updateDrawable(R.id.background_dim, dimDrawable); 776 } 777 } 778 779 /** 780 * Makes the background visible on the given Window. The background manager must be attached 781 * when the background is set. 782 */ 783 public void attach(Window window) { 784 if (USE_SEPARATE_WINDOW) { 785 attachBehindWindow(window); 786 } else { 787 attachToView(window.getDecorView()); 788 } 789 } 790 791 /** 792 * Sets the resource id for the drawable to be shown when there is no background set. 793 * Overrides the window background drawable from the theme. This should 794 * be called before attaching. 795 */ 796 public void setThemeDrawableResourceId(int resourceId) { 797 mThemeDrawableResourceId = resourceId; 798 } 799 800 private void attachBehindWindow(Window window) { 801 if (DEBUG) Log.v(TAG, "attachBehindWindow " + window); 802 mWindow = window; 803 mWindowManager = window.getWindowManager(); 804 805 WindowManager.LayoutParams params = new WindowManager.LayoutParams( 806 // Media window sits behind the main application window 807 WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA, 808 // Avoid default to software format RGBA 809 WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, 810 android.graphics.PixelFormat.TRANSLUCENT); 811 params.setTitle(WINDOW_NAME); 812 params.width = ViewGroup.LayoutParams.MATCH_PARENT; 813 params.height = ViewGroup.LayoutParams.MATCH_PARENT; 814 815 View backgroundView = LayoutInflater.from(mContext).inflate( 816 R.layout.lb_background_window, null); 817 mWindowManager.addView(backgroundView, params); 818 819 attachToView(backgroundView); 820 } 821 822 private void attachToView(View sceneRoot) { 823 mBgView = sceneRoot; 824 mAttached = true; 825 syncWithService(); 826 } 827 828 /** 829 * Returns true if the background manager is currently attached; false otherwise. 830 */ 831 public boolean isAttached() { 832 return mAttached; 833 } 834 835 /** 836 * Release references to Drawables and put the BackgroundManager into the 837 * detached state. Called when the associated Activity is destroyed. 838 * @hide 839 */ 840 void detach() { 841 if (DEBUG) Log.v(TAG, "detach " + this); 842 release(); 843 844 if (mWindowManager != null && mBgView != null) { 845 mWindowManager.removeViewImmediate(mBgView); 846 } 847 848 mWindowManager = null; 849 mWindow = null; 850 mBgView = null; 851 mAttached = false; 852 853 if (mService != null) { 854 mService.unref(); 855 mService = null; 856 } 857 } 858 859 /** 860 * Release references to Drawables. Typically called to reduce memory 861 * overhead when not visible. 862 * <p> 863 * When an Activity is started, if the BackgroundManager has not been 864 * released, the continuity service is updated from the BackgroundManager 865 * state. If the BackgroundManager was released, the BackgroundManager 866 * inherits the current state from the continuity service. 867 */ 868 public void release() { 869 if (DEBUG) Log.v(TAG, "release " + this); 870 if (mLayerDrawable != null) { 871 mLayerDrawable.clearDrawable(R.id.background_imagein, mContext); 872 mLayerDrawable.clearDrawable(R.id.background_imageout, mContext); 873 mLayerDrawable.clearDrawable(R.id.background_theme, mContext); 874 mLayerDrawable = null; 875 } 876 if (mChangeRunnable != null) { 877 mHandler.removeCallbacks(mChangeRunnable); 878 mChangeRunnable = null; 879 } 880 releaseBackgroundBitmap(); 881 } 882 883 private void releaseBackgroundBitmap() { 884 mBackgroundDrawable = null; 885 } 886 887 private void setBackgroundDrawable(Drawable drawable) { 888 mBackgroundDrawable = drawable; 889 mService.setDrawable(mBackgroundDrawable); 890 } 891 892 /** 893 * Sets the drawable used as a dim layer. 894 */ 895 public void setDimLayer(Drawable drawable) { 896 mDimDrawable = drawable; 897 updateDimWrapper(); 898 } 899 900 /** 901 * Returns the drawable used as a dim layer. 902 */ 903 public Drawable getDimLayer() { 904 return mDimDrawable; 905 } 906 907 /** 908 * Returns the default drawable used as a dim layer. 909 */ 910 public Drawable getDefaultDimLayer() { 911 return ContextCompat.getDrawable(mContext, R.color.lb_background_protection); 912 } 913 914 private void postChangeRunnable() { 915 if (mChangeRunnable == null || !mChangeRunnablePending) { 916 return; 917 } 918 919 // Postpone a pending change runnable until: no existing change animation in progress && 920 // activity is resumed (in the foreground) && layerdrawable fully opaque. 921 // If the layerdrawable is translucent then an activity transition is in progress 922 // and we want to use the optimized drawing path for performance reasons (see 923 // OptimizedTranslucentLayerDrawable). 924 if (mAnimator.isStarted()) { 925 if (DEBUG) Log.v(TAG, "animation in progress"); 926 } else if (!mFragmentState.isResumed()) { 927 if (DEBUG) Log.v(TAG, "not resumed"); 928 } else if (mLayerDrawable.getAlpha() < FULL_ALPHA) { 929 if (DEBUG) Log.v(TAG, "in transition, alpha " + mLayerDrawable.getAlpha()); 930 } else { 931 long delayMs = getRunnableDelay(); 932 if (DEBUG) Log.v(TAG, "posting runnable delayMs " + delayMs); 933 mLastSetTime = System.currentTimeMillis(); 934 mHandler.postDelayed(mChangeRunnable, delayMs); 935 mChangeRunnablePending = false; 936 } 937 } 938 939 private void updateImmediate() { 940 lazyInit(); 941 942 DrawableWrapper colorWrapper = getColorWrapper(); 943 if (colorWrapper != null) { 944 colorWrapper.setColor(mBackgroundColor); 945 } 946 DrawableWrapper dimWrapper = getDimWrapper(); 947 if (dimWrapper != null) { 948 dimWrapper.setAlpha(mBackgroundColor == Color.TRANSPARENT ? 0 : DIM_ALPHA_ON_SOLID); 949 } 950 showWallpaper(mBackgroundColor == Color.TRANSPARENT); 951 952 if (mBackgroundDrawable == null) { 953 mLayerDrawable.clearDrawable(R.id.background_imagein, mContext); 954 } else { 955 if (DEBUG) Log.v(TAG, "Background drawable is available"); 956 mLayerDrawable.updateDrawable( 957 R.id.background_imagein, mBackgroundDrawable); 958 if (dimWrapper != null) { 959 dimWrapper.setAlpha(FULL_ALPHA); 960 } 961 } 962 } 963 964 /** 965 * Sets the background to the given color. The timing for when this becomes 966 * visible in the app is undefined and may take place after a small delay. 967 */ 968 public void setColor(@ColorInt int color) { 969 if (DEBUG) Log.v(TAG, "setColor " + Integer.toHexString(color)); 970 971 mBackgroundColor = color; 972 mService.setColor(mBackgroundColor); 973 974 DrawableWrapper colorWrapper = getColorWrapper(); 975 if (colorWrapper != null) { 976 colorWrapper.setColor(mBackgroundColor); 977 } 978 } 979 980 /** 981 * Sets the given drawable into the background. The provided Drawable will be 982 * used unmodified as the background, without any scaling or cropping 983 * applied to it. The timing for when this becomes visible in the app is 984 * undefined and may take place after a small delay. 985 */ 986 public void setDrawable(Drawable drawable) { 987 if (DEBUG) Log.v(TAG, "setBackgroundDrawable " + drawable); 988 setDrawableInternal(drawable); 989 } 990 991 private void setDrawableInternal(Drawable drawable) { 992 if (!mAttached) { 993 throw new IllegalStateException("Must attach before setting background drawable"); 994 } 995 996 if (mChangeRunnable != null) { 997 if (sameDrawable(drawable, mChangeRunnable.mDrawable)) { 998 if (DEBUG) Log.v(TAG, "new drawable same as pending"); 999 return; 1000 } 1001 mHandler.removeCallbacks(mChangeRunnable); 1002 mChangeRunnable = null; 1003 } 1004 1005 // If layer drawable is null then the activity hasn't started yet. 1006 // If the layer drawable alpha is zero then the activity transition hasn't started yet. 1007 // In these cases we can update the background immediately and let activity transition 1008 // fade it in. 1009 if (mLayerDrawable == null || mLayerDrawable.getAlpha() == 0) { 1010 if (DEBUG) Log.v(TAG, "setDrawableInternal null or alpha is zero"); 1011 setBackgroundDrawable(drawable); 1012 updateImmediate(); 1013 return; 1014 } 1015 1016 mChangeRunnable = new ChangeBackgroundRunnable(drawable); 1017 mChangeRunnablePending = true; 1018 1019 postChangeRunnable(); 1020 } 1021 1022 private long getRunnableDelay() { 1023 return Math.max(0, mLastSetTime + CHANGE_BG_DELAY_MS - System.currentTimeMillis()); 1024 } 1025 1026 /** 1027 * Sets the given bitmap into the background. When using setBitmap to set the 1028 * background, the provided bitmap will be scaled and cropped to correctly 1029 * fit within the dimensions of the view. The timing for when this becomes 1030 * visible in the app is undefined and may take place after a small delay. 1031 */ 1032 public void setBitmap(Bitmap bitmap) { 1033 if (DEBUG) { 1034 Log.v(TAG, "setBitmap " + bitmap); 1035 } 1036 1037 if (bitmap == null) { 1038 setDrawableInternal(null); 1039 return; 1040 } 1041 1042 if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { 1043 if (DEBUG) { 1044 Log.v(TAG, "invalid bitmap width or height"); 1045 } 1046 return; 1047 } 1048 1049 Matrix matrix = null; 1050 1051 if ((bitmap.getWidth() != mWidthPx || bitmap.getHeight() != mHeightPx)) { 1052 int dwidth = bitmap.getWidth(); 1053 int dheight = bitmap.getHeight(); 1054 float scale; 1055 1056 // Scale proportionately to fit width and height. 1057 if (dwidth * mHeightPx > mWidthPx * dheight) { 1058 scale = (float) mHeightPx / (float) dheight; 1059 } else { 1060 scale = (float) mWidthPx / (float) dwidth; 1061 } 1062 1063 int subX = Math.min((int) (mWidthPx / scale), dwidth); 1064 int dx = Math.max(0, (dwidth - subX) / 2); 1065 1066 matrix = new Matrix(); 1067 matrix.setScale(scale, scale); 1068 matrix.preTranslate(-dx, 0); 1069 1070 if (DEBUG) Log.v(TAG, "original image size " + bitmap.getWidth() + "x" + bitmap.getHeight() + 1071 " scale " + scale + " dx " + dx); 1072 } 1073 1074 BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap, matrix); 1075 1076 setDrawableInternal(bitmapDrawable); 1077 } 1078 1079 private void applyBackgroundChanges() { 1080 if (!mAttached) { 1081 return; 1082 } 1083 1084 if (DEBUG) Log.v(TAG, "applyBackgroundChanges drawable " + mBackgroundDrawable); 1085 1086 int dimAlpha = -1; 1087 1088 if (getImageOutWrapper() != null) { 1089 dimAlpha = mBackgroundColor == Color.TRANSPARENT ? 0 : DIM_ALPHA_ON_SOLID; 1090 } 1091 1092 DrawableWrapper imageInWrapper = getImageInWrapper(); 1093 if (imageInWrapper == null && mBackgroundDrawable != null) { 1094 if (DEBUG) Log.v(TAG, "creating new imagein drawable"); 1095 imageInWrapper = mLayerDrawable.updateDrawable( 1096 R.id.background_imagein, mBackgroundDrawable); 1097 if (DEBUG) Log.v(TAG, "imageInWrapper animation starting"); 1098 imageInWrapper.setAlpha(0); 1099 dimAlpha = FULL_ALPHA; 1100 } 1101 1102 mAnimator.setDuration(FADE_DURATION); 1103 mAnimator.start(); 1104 1105 DrawableWrapper dimWrapper = getDimWrapper(); 1106 if (dimWrapper != null && dimAlpha >= 0) { 1107 if (DEBUG) Log.v(TAG, "dimwrapper animation starting to " + dimAlpha); 1108 mDimAnimator.cancel(); 1109 mDimAnimator.setIntValues(dimWrapper.getAlpha(), dimAlpha); 1110 mDimAnimator.setDuration(FADE_DURATION); 1111 mDimAnimator.setInterpolator( 1112 dimAlpha == FULL_ALPHA ? mDecelerateInterpolator : mAccelerateInterpolator); 1113 mDimAnimator.start(); 1114 } 1115 } 1116 1117 /** 1118 * Returns the current background color. 1119 */ 1120 @ColorInt 1121 public final int getColor() { 1122 return mBackgroundColor; 1123 } 1124 1125 /** 1126 * Returns the current background {@link Drawable}. 1127 */ 1128 public Drawable getDrawable() { 1129 return mBackgroundDrawable; 1130 } 1131 1132 private boolean sameDrawable(Drawable first, Drawable second) { 1133 if (first == null || second == null) { 1134 return false; 1135 } 1136 if (first == second) { 1137 return true; 1138 } 1139 if (first instanceof BitmapDrawable && second instanceof BitmapDrawable) { 1140 if (((BitmapDrawable) first).getBitmap().sameAs(((BitmapDrawable) second).getBitmap())) { 1141 return true; 1142 } 1143 } 1144 return false; 1145 } 1146 1147 /** 1148 * Task which changes the background. 1149 */ 1150 class ChangeBackgroundRunnable implements Runnable { 1151 private Drawable mDrawable; 1152 1153 ChangeBackgroundRunnable(Drawable drawable) { 1154 mDrawable = drawable; 1155 } 1156 1157 @Override 1158 public void run() { 1159 runTask(); 1160 mChangeRunnable = null; 1161 } 1162 1163 private void runTask() { 1164 if (mLayerDrawable == null) { 1165 if (DEBUG) Log.v(TAG, "runTask while released - should not happen"); 1166 return; 1167 } 1168 1169 if (sameDrawable(mDrawable, mBackgroundDrawable)) { 1170 if (DEBUG) Log.v(TAG, "new drawable same as current"); 1171 return; 1172 } 1173 1174 releaseBackgroundBitmap(); 1175 1176 DrawableWrapper imageInWrapper = getImageInWrapper(); 1177 if (imageInWrapper != null) { 1178 if (DEBUG) Log.v(TAG, "moving image in to image out"); 1179 // Order is important! Setting a drawable "removes" the 1180 // previous one from the view 1181 mLayerDrawable.clearDrawable(R.id.background_imagein, mContext); 1182 mLayerDrawable.updateDrawable(R.id.background_imageout, 1183 imageInWrapper.getDrawable()); 1184 } 1185 1186 setBackgroundDrawable(mDrawable); 1187 applyBackgroundChanges(); 1188 } 1189 } 1190 1191 private static Drawable createEmptyDrawable(Context context) { 1192 Bitmap bitmap = null; 1193 return new BitmapDrawable(context.getResources(), bitmap); 1194 } 1195 1196 private void showWallpaper(boolean show) { 1197 if (mWindow == null) { 1198 return; 1199 } 1200 1201 WindowManager.LayoutParams layoutParams = mWindow.getAttributes(); 1202 if (show) { 1203 if ((layoutParams.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) { 1204 return; 1205 } 1206 if (DEBUG) Log.v(TAG, "showing wallpaper"); 1207 layoutParams.flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; 1208 } else { 1209 if ((layoutParams.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) == 0) { 1210 return; 1211 } 1212 if (DEBUG) Log.v(TAG, "hiding wallpaper"); 1213 layoutParams.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; 1214 } 1215 1216 mWindow.setAttributes(layoutParams); 1217 } 1218} 1219