/* * Copyright (C) 2019 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 com.android.wallpaper.asset; import android.app.Activity; import android.app.ActivityManager; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Point; import android.graphics.Rect; import android.util.LruCache; import android.widget.ImageView; import androidx.annotation.Nullable; import androidx.core.app.ActivityManagerCompat; import java.util.Map; import java.util.Objects; /** * Implementation of {@link Asset} that wraps another {@link Asset} but keeps an LRU cache of * bitmaps generated by {@link #decodeBitmap(int, int, BitmapReceiver)} to avoid having to decode * the same bitmap multiple times. * The cache key is the wrapped Asset and the target Width and Height requested, so that we only * reuse bitmaps of the same size. */ public class BitmapCachingAsset extends Asset { private static class CacheKey { final Asset mAsset; /** a (width x height) of (0 x 0) represents the full image */ final int mWidth; final int mHeight; final boolean mRtl; final Rect mRect; CacheKey(Asset asset, int width, int height) { this(asset, width, height, false, null); } CacheKey(Asset asset, int width, int height, boolean rtl, Rect rect) { mAsset = asset; mWidth = width; mHeight = height; mRtl = rtl; mRect = rect; } @Override public int hashCode() { return Objects.hash(mAsset, mWidth, mHeight); } @Override public boolean equals(Object obj) { return obj instanceof CacheKey && (Objects.equals(this.mAsset, ((CacheKey) obj).mAsset)) && ((CacheKey) obj).mWidth == this.mWidth && ((CacheKey) obj).mHeight == this.mHeight && ((CacheKey) obj).mRtl == this.mRtl && (Objects.equals(this.mRect, ((CacheKey) obj).mRect)); } } private static int cacheSize = 100 * 1024 * 1024; // 100MiB private static LruCache sCache = new LruCache(cacheSize) { @Override protected int sizeOf(CacheKey key, Bitmap value) { return value.getByteCount(); } }; private final boolean mIsLowRam; private final Asset mOriginalAsset; public BitmapCachingAsset(Context context, Asset originalAsset) { mOriginalAsset = originalAsset instanceof BitmapCachingAsset ? ((BitmapCachingAsset) originalAsset).mOriginalAsset : originalAsset; mIsLowRam = ActivityManagerCompat.isLowRamDevice( (ActivityManager) context.getApplicationContext().getSystemService( Context.ACTIVITY_SERVICE)); } @Override public void decodeBitmap(int targetWidth, int targetHeight, boolean useHardwareBitmapIfPossible, BitmapReceiver receiver) { // Skip the cache in low ram devices if (mIsLowRam) { mOriginalAsset.decodeBitmap(targetWidth, targetHeight, useHardwareBitmapIfPossible, receiver); return; } CacheKey key = new CacheKey(mOriginalAsset, targetWidth, targetHeight); Bitmap cached = sCache.get(key); if (cached != null) { receiver.onBitmapDecoded(cached); } else { BitmapReceiver cachingReceiver = bitmap -> { if (bitmap != null) { sCache.put(key, bitmap); } receiver.onBitmapDecoded(bitmap); }; if (targetWidth == 0 && targetHeight == 0) { mOriginalAsset.decodeBitmap(cachingReceiver); } else { mOriginalAsset.decodeBitmap(targetWidth, targetHeight, useHardwareBitmapIfPossible, cachingReceiver); } } } @Override public void decodeBitmap(BitmapReceiver receiver) { decodeBitmap(0, 0, receiver); } @Override public void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, boolean shouldAdjustForRtl, BitmapReceiver receiver) { // Skip the cache in low ram devices if (mIsLowRam) { mOriginalAsset.decodeBitmapRegion(rect, targetWidth, targetHeight, shouldAdjustForRtl, receiver); return; } CacheKey key = new CacheKey(mOriginalAsset, targetWidth, targetHeight, shouldAdjustForRtl, rect); Bitmap cached = sCache.get(key); if (cached != null) { receiver.onBitmapDecoded(cached); } else { mOriginalAsset.decodeBitmapRegion(rect, targetWidth, targetHeight, shouldAdjustForRtl, bitmap -> { if (bitmap != null) { sCache.put(key, bitmap); } receiver.onBitmapDecoded(bitmap); }); } } @Override public void decodeRawDimensions(@Nullable Activity activity, DimensionsReceiver receiver) { mOriginalAsset.decodeRawDimensions(activity, receiver); } @Override public boolean supportsTiling() { return mOriginalAsset.supportsTiling(); } @Override public void loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor, boolean offsetToStart) { // Honor the original Asset's preview image loading mOriginalAsset.loadPreviewImage(activity, imageView, placeholderColor, offsetToStart); } @Override public void loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor, boolean offsetToStart, @Nullable Map cropHints) { // Honor the original Asset's preview image loading mOriginalAsset.loadPreviewImage(activity, imageView, placeholderColor, offsetToStart, cropHints); } }