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.Point; 23 import android.graphics.Rect; 24 import android.util.LruCache; 25 import android.widget.ImageView; 26 27 import androidx.annotation.Nullable; 28 import androidx.core.app.ActivityManagerCompat; 29 30 import java.util.Map; 31 import java.util.Objects; 32 33 /** 34 * Implementation of {@link Asset} that wraps another {@link Asset} but keeps an LRU cache of 35 * bitmaps generated by {@link #decodeBitmap(int, int, BitmapReceiver)} to avoid having to decode 36 * the same bitmap multiple times. 37 * The cache key is the wrapped Asset and the target Width and Height requested, so that we only 38 * reuse bitmaps of the same size. 39 */ 40 public class BitmapCachingAsset extends Asset { 41 42 private static class CacheKey { 43 final Asset mAsset; 44 45 /** a (width x height) of (0 x 0) represents the full image */ 46 final int mWidth; 47 final int mHeight; 48 final boolean mRtl; 49 final Rect mRect; 50 CacheKey(Asset asset, int width, int height)51 CacheKey(Asset asset, int width, int height) { 52 this(asset, width, height, false, null); 53 } 54 CacheKey(Asset asset, int width, int height, boolean rtl, Rect rect)55 CacheKey(Asset asset, int width, int height, boolean rtl, Rect rect) { 56 mAsset = asset; 57 mWidth = width; 58 mHeight = height; 59 mRtl = rtl; 60 mRect = rect; 61 } 62 63 @Override hashCode()64 public int hashCode() { 65 return Objects.hash(mAsset, mWidth, mHeight); 66 } 67 68 @Override equals(Object obj)69 public boolean equals(Object obj) { 70 return obj instanceof CacheKey 71 && (Objects.equals(this.mAsset, ((CacheKey) obj).mAsset)) 72 && ((CacheKey) obj).mWidth == this.mWidth 73 && ((CacheKey) obj).mHeight == this.mHeight 74 && ((CacheKey) obj).mRtl == this.mRtl 75 && (Objects.equals(this.mRect, ((CacheKey) obj).mRect)); 76 } 77 } 78 79 private static int cacheSize = 100 * 1024 * 1024; // 100MiB 80 private static LruCache<CacheKey, Bitmap> sCache = new LruCache<CacheKey, Bitmap>(cacheSize) { 81 @Override protected int sizeOf(CacheKey key, Bitmap value) { 82 return value.getByteCount(); 83 } 84 }; 85 86 private final boolean mIsLowRam; 87 private final Asset mOriginalAsset; 88 BitmapCachingAsset(Context context, Asset originalAsset)89 public BitmapCachingAsset(Context context, Asset originalAsset) { 90 mOriginalAsset = originalAsset instanceof BitmapCachingAsset 91 ? ((BitmapCachingAsset) originalAsset).mOriginalAsset : originalAsset; 92 mIsLowRam = ActivityManagerCompat.isLowRamDevice( 93 (ActivityManager) context.getApplicationContext().getSystemService( 94 Context.ACTIVITY_SERVICE)); 95 } 96 97 @Override decodeBitmap(int targetWidth, int targetHeight, boolean useHardwareBitmapIfPossible, BitmapReceiver receiver)98 public void decodeBitmap(int targetWidth, int targetHeight, boolean useHardwareBitmapIfPossible, 99 BitmapReceiver receiver) { 100 // Skip the cache in low ram devices 101 if (mIsLowRam) { 102 mOriginalAsset.decodeBitmap(targetWidth, targetHeight, useHardwareBitmapIfPossible, 103 receiver); 104 return; 105 } 106 CacheKey key = new CacheKey(mOriginalAsset, targetWidth, targetHeight); 107 Bitmap cached = sCache.get(key); 108 if (cached != null) { 109 receiver.onBitmapDecoded(cached); 110 } else { 111 BitmapReceiver cachingReceiver = bitmap -> { 112 if (bitmap != null) { 113 sCache.put(key, bitmap); 114 } 115 receiver.onBitmapDecoded(bitmap); 116 }; 117 if (targetWidth == 0 && targetHeight == 0) { 118 mOriginalAsset.decodeBitmap(cachingReceiver); 119 } else { 120 mOriginalAsset.decodeBitmap(targetWidth, targetHeight, useHardwareBitmapIfPossible, 121 cachingReceiver); 122 } 123 } 124 } 125 126 @Override decodeBitmap(BitmapReceiver receiver)127 public void decodeBitmap(BitmapReceiver receiver) { 128 decodeBitmap(0, 0, receiver); 129 } 130 131 @Override decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, boolean shouldAdjustForRtl, BitmapReceiver receiver)132 public void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, 133 boolean shouldAdjustForRtl, BitmapReceiver receiver) { 134 // Skip the cache in low ram devices 135 if (mIsLowRam) { 136 mOriginalAsset.decodeBitmapRegion(rect, targetWidth, targetHeight, shouldAdjustForRtl, 137 receiver); 138 return; 139 } 140 CacheKey key = new CacheKey(mOriginalAsset, targetWidth, targetHeight, shouldAdjustForRtl, 141 rect); 142 Bitmap cached = sCache.get(key); 143 if (cached != null) { 144 receiver.onBitmapDecoded(cached); 145 } else { 146 mOriginalAsset.decodeBitmapRegion(rect, targetWidth, targetHeight, shouldAdjustForRtl, 147 bitmap -> { 148 if (bitmap != null) { 149 sCache.put(key, bitmap); 150 } 151 receiver.onBitmapDecoded(bitmap); 152 }); 153 } 154 } 155 156 @Override decodeRawDimensions(@ullable Activity activity, DimensionsReceiver receiver)157 public void decodeRawDimensions(@Nullable Activity activity, DimensionsReceiver receiver) { 158 mOriginalAsset.decodeRawDimensions(activity, receiver); 159 } 160 161 @Override supportsTiling()162 public boolean supportsTiling() { 163 return mOriginalAsset.supportsTiling(); 164 } 165 166 @Override loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor, boolean offsetToStart)167 public void loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor, 168 boolean offsetToStart) { 169 // Honor the original Asset's preview image loading 170 mOriginalAsset.loadPreviewImage(activity, imageView, placeholderColor, offsetToStart); 171 } 172 173 @Override loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor, boolean offsetToStart, @Nullable Map<Point, Rect> cropHints)174 public void loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor, 175 boolean offsetToStart, @Nullable Map<Point, Rect> cropHints) { 176 // Honor the original Asset's preview image loading 177 mOriginalAsset.loadPreviewImage(activity, imageView, placeholderColor, offsetToStart, 178 cropHints); 179 } 180 } 181