1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.wallpaper.asset; 17 18 import android.app.Activity; 19 import android.app.ActivityManager; 20 import android.content.Context; 21 import android.graphics.Bitmap; 22 import android.graphics.Rect; 23 import android.util.LruCache; 24 import android.widget.ImageView; 25 26 import androidx.annotation.Nullable; 27 import androidx.core.app.ActivityManagerCompat; 28 29 import java.util.Objects; 30 31 /** 32 * Implementation of {@link Asset} that wraps another {@link Asset} but keeps an LRU cache of 33 * bitmaps generated by {@link #decodeBitmap(int, int, BitmapReceiver)} to avoid having to decode 34 * the same bitmap multiple times. 35 * The cache key is the wrapped Asset and the target Width and Height requested, so that we only 36 * reuse bitmaps of the same size. 37 */ 38 public class BitmapCachingAsset extends Asset { 39 40 private static class CacheKey { 41 final Asset mAsset; 42 43 /** a (width x height) of (0 x 0) represents the full image */ 44 final int mWidth; 45 final int mHeight; 46 final boolean mRtl; 47 final Rect mRect; 48 CacheKey(Asset asset, int width, int height)49 CacheKey(Asset asset, int width, int height) { 50 this(asset, width, height, false, null); 51 } 52 CacheKey(Asset asset, int width, int height, boolean rtl, Rect rect)53 CacheKey(Asset asset, int width, int height, boolean rtl, Rect rect) { 54 mAsset = asset; 55 mWidth = width; 56 mHeight = height; 57 mRtl = rtl; 58 mRect = rect; 59 } 60 61 @Override hashCode()62 public int hashCode() { 63 return Objects.hash(mAsset, mWidth, mHeight); 64 } 65 66 @Override equals(Object obj)67 public boolean equals(Object obj) { 68 return obj instanceof CacheKey 69 && (Objects.equals(this.mAsset, ((CacheKey) obj).mAsset)) 70 && ((CacheKey) obj).mWidth == this.mWidth 71 && ((CacheKey) obj).mHeight == this.mHeight 72 && ((CacheKey) obj).mRtl == this.mRtl 73 && (Objects.equals(this.mRect, ((CacheKey) obj).mRect)); 74 } 75 } 76 77 private static int cacheSize = 100 * 1024 * 1024; // 100MiB 78 private static LruCache<CacheKey, Bitmap> sCache = new LruCache<CacheKey, Bitmap>(cacheSize) { 79 @Override protected int sizeOf(CacheKey key, Bitmap value) { 80 return value.getByteCount(); 81 } 82 }; 83 84 private final boolean mIsLowRam; 85 private final Asset mOriginalAsset; 86 BitmapCachingAsset(Context context, Asset originalAsset)87 public BitmapCachingAsset(Context context, Asset originalAsset) { 88 mOriginalAsset = originalAsset instanceof BitmapCachingAsset 89 ? ((BitmapCachingAsset) originalAsset).mOriginalAsset : originalAsset; 90 mIsLowRam = ActivityManagerCompat.isLowRamDevice( 91 (ActivityManager) context.getApplicationContext().getSystemService( 92 Context.ACTIVITY_SERVICE)); 93 } 94 95 @Override decodeBitmap(int targetWidth, int targetHeight, boolean useHardwareBitmapIfPossible, BitmapReceiver receiver)96 public void decodeBitmap(int targetWidth, int targetHeight, boolean useHardwareBitmapIfPossible, 97 BitmapReceiver receiver) { 98 // Skip the cache in low ram devices 99 if (mIsLowRam) { 100 mOriginalAsset.decodeBitmap(targetWidth, targetHeight, useHardwareBitmapIfPossible, 101 receiver); 102 return; 103 } 104 CacheKey key = new CacheKey(mOriginalAsset, targetWidth, targetHeight); 105 Bitmap cached = sCache.get(key); 106 if (cached != null) { 107 receiver.onBitmapDecoded(cached); 108 } else { 109 BitmapReceiver cachingReceiver = bitmap -> { 110 if (bitmap != null) { 111 sCache.put(key, bitmap); 112 } 113 receiver.onBitmapDecoded(bitmap); 114 }; 115 if (targetWidth == 0 && targetHeight == 0) { 116 mOriginalAsset.decodeBitmap(cachingReceiver); 117 } else { 118 mOriginalAsset.decodeBitmap(targetWidth, targetHeight, useHardwareBitmapIfPossible, 119 cachingReceiver); 120 } 121 } 122 } 123 124 @Override decodeBitmap(BitmapReceiver receiver)125 public void decodeBitmap(BitmapReceiver receiver) { 126 decodeBitmap(0, 0, receiver); 127 } 128 129 @Override decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, boolean shouldAdjustForRtl, BitmapReceiver receiver)130 public void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, 131 boolean shouldAdjustForRtl, BitmapReceiver receiver) { 132 // Skip the cache in low ram devices 133 if (mIsLowRam) { 134 mOriginalAsset.decodeBitmapRegion(rect, targetWidth, targetHeight, shouldAdjustForRtl, 135 receiver); 136 return; 137 } 138 CacheKey key = new CacheKey(mOriginalAsset, targetWidth, targetHeight, shouldAdjustForRtl, 139 rect); 140 Bitmap cached = sCache.get(key); 141 if (cached != null) { 142 receiver.onBitmapDecoded(cached); 143 } else { 144 mOriginalAsset.decodeBitmapRegion(rect, targetWidth, targetHeight, shouldAdjustForRtl, 145 bitmap -> { 146 if (bitmap != null) { 147 sCache.put(key, bitmap); 148 } 149 receiver.onBitmapDecoded(bitmap); 150 }); 151 } 152 } 153 154 @Override decodeRawDimensions(@ullable Activity activity, DimensionsReceiver receiver)155 public void decodeRawDimensions(@Nullable Activity activity, DimensionsReceiver receiver) { 156 mOriginalAsset.decodeRawDimensions(activity, receiver); 157 } 158 159 @Override supportsTiling()160 public boolean supportsTiling() { 161 return mOriginalAsset.supportsTiling(); 162 } 163 164 @Override loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor, boolean offsetToStart)165 public void loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor, 166 boolean offsetToStart) { 167 // Honor the original Asset's preview image loading 168 mOriginalAsset.loadPreviewImage(activity, imageView, placeholderColor, offsetToStart); 169 } 170 } 171