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         final int mWidth;
43         final int mHeight;
44         final boolean mRtl;
45         final Rect mRect;
46 
CacheKey(Asset asset, int width, int height)47         CacheKey(Asset asset, int width, int height) {
48             this(asset, width, height, false, null);
49         }
50 
CacheKey(Asset asset, int width, int height, boolean rtl, Rect rect)51         CacheKey(Asset asset, int width, int height, boolean rtl, Rect rect) {
52             mAsset = asset;
53             mWidth = width;
54             mHeight = height;
55             mRtl = rtl;
56             mRect = rect;
57         }
58 
59         @Override
hashCode()60         public int hashCode() {
61             return Objects.hash(mAsset, mWidth, mHeight);
62         }
63 
64         @Override
equals(Object obj)65         public boolean equals(Object obj) {
66             return obj instanceof CacheKey
67                     && (Objects.equals(this.mAsset, ((CacheKey) obj).mAsset))
68                     && ((CacheKey) obj).mWidth == this.mWidth
69                     && ((CacheKey) obj).mHeight == this.mHeight
70                     && ((CacheKey) obj).mRtl == this.mRtl
71                     && (Objects.equals(this.mRect, ((CacheKey) obj).mRect));
72         }
73     }
74 
75     private static int cacheSize = 100 * 1024 * 1024; // 100MiB
76     private static LruCache<CacheKey, Bitmap> sCache = new LruCache<CacheKey, Bitmap>(cacheSize) {
77         @Override protected int sizeOf(CacheKey key, Bitmap value) {
78             return value.getByteCount();
79         }
80     };
81 
82     private final boolean mIsLowRam;
83     private final Asset mOriginalAsset;
84 
BitmapCachingAsset(Context context, Asset originalAsset)85     public BitmapCachingAsset(Context context, Asset originalAsset) {
86         mOriginalAsset = originalAsset instanceof BitmapCachingAsset
87                 ? ((BitmapCachingAsset) originalAsset).mOriginalAsset : originalAsset;
88         mIsLowRam = ActivityManagerCompat.isLowRamDevice(
89                 (ActivityManager) context.getApplicationContext().getSystemService(
90                         Context.ACTIVITY_SERVICE));
91     }
92 
93     @Override
decodeBitmap(int targetWidth, int targetHeight, BitmapReceiver receiver)94     public void decodeBitmap(int targetWidth, int targetHeight, BitmapReceiver receiver) {
95         // Skip the cache in low ram devices
96         if (mIsLowRam) {
97             mOriginalAsset.decodeBitmap(targetWidth, targetHeight, receiver::onBitmapDecoded);
98             return;
99         }
100         CacheKey key = new CacheKey(mOriginalAsset, targetWidth, targetHeight);
101         Bitmap cached = sCache.get(key);
102         if (cached != null) {
103             receiver.onBitmapDecoded(cached);
104         } else {
105             mOriginalAsset.decodeBitmap(targetWidth, targetHeight, bitmap -> {
106                 if (bitmap != null) {
107                     sCache.put(key, bitmap);
108                 }
109                 receiver.onBitmapDecoded(bitmap);
110             });
111         }
112     }
113 
114     @Override
decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, boolean shouldAdjustForRtl, BitmapReceiver receiver)115     public void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight,
116             boolean shouldAdjustForRtl, BitmapReceiver receiver) {
117         // Skip the cache in low ram devices
118         if (mIsLowRam) {
119             mOriginalAsset.decodeBitmapRegion(rect, targetWidth, targetHeight, shouldAdjustForRtl,
120                     receiver);
121             return;
122         }
123         CacheKey key = new CacheKey(mOriginalAsset, targetWidth, targetHeight, shouldAdjustForRtl,
124                 rect);
125         Bitmap cached = sCache.get(key);
126         if (cached != null) {
127             receiver.onBitmapDecoded(cached);
128         } else {
129             mOriginalAsset.decodeBitmapRegion(rect, targetWidth, targetHeight, shouldAdjustForRtl,
130                     bitmap -> {
131                         if (bitmap != null) {
132                             sCache.put(key, bitmap);
133                         }
134                         receiver.onBitmapDecoded(bitmap);
135                     });
136         }
137     }
138 
139     @Override
decodeRawDimensions(@ullable Activity activity, DimensionsReceiver receiver)140     public void decodeRawDimensions(@Nullable Activity activity, DimensionsReceiver receiver) {
141         mOriginalAsset.decodeRawDimensions(activity, receiver);
142     }
143 
144     @Override
supportsTiling()145     public boolean supportsTiling() {
146         return mOriginalAsset.supportsTiling();
147     }
148 
149     @Override
loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor)150     public void loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor) {
151         // Honor the original Asset's preview image loading
152         mOriginalAsset.loadPreviewImage(activity, imageView, placeholderColor);
153     }
154 }
155