Revision control
Copy as Markdown
package org.mozilla.vrbrowser.utils;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.SurfaceTexture;
import android.util.Log;
import android.util.LruCache;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.jakewharton.disklrucache.DiskLruCache;
import org.mozilla.vrbrowser.VRBrowserApplication;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
public class BitmapCache {
private Context mContext;
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskCache;
private Executor mIOExecutor;
private Executor mMainThreadExecutor;
private final Object mLock = new Object();
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 100; // 100MB
private static final String LOGTAG = SystemUtils.createLogtag(BitmapCache.class);
private SurfaceTexture mCaptureSurfaceTexture;
private Surface mCaptureSurface;
private boolean mCapturedAcquired;
public static BitmapCache getInstance(Context aContext) {
return ((VRBrowserApplication)aContext.getApplicationContext()).getBitmapCache();
}
public BitmapCache(@NonNull Context aContext, @NonNull Executor aIOExecutor, @NonNull Executor aMainThreadExecutor) {
mContext = aContext;
mIOExecutor = aIOExecutor;
mMainThreadExecutor = aMainThreadExecutor;
}
public void onCreate() {
initMemoryCache();
initDiskCache();
}
void initMemoryCache() {
// Get available VM memory in KB.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// Use KB as the size of the item
return bitmap.getByteCount() / 1024;
}
};
}
void initDiskCache() {
String path = mContext.getCacheDir() + File.separator + "snapshots";
mIOExecutor.execute(() -> {
try {
mDiskCache = DiskLruCache.open(new File(path), 1, 1, DISK_CACHE_SIZE);
}
catch (Exception ex) {
Log.e(LOGTAG, "Failed to initialize DiskLruCache:" + ex.getMessage());
}
});
}
public void addBitmap(@NonNull String aKey, @NonNull Bitmap aBitmap) {
mMemoryCache.put(aKey, aBitmap);
runIO(() -> {
DiskLruCache.Editor editor = null;
try {
editor = mDiskCache.edit(aKey);
if (editor != null) {
aBitmap.compress(Bitmap.CompressFormat.PNG, 80, editor.newOutputStream(0));
editor.commit();
}
}
catch (Exception ex) {
Log.e(LOGTAG, "Failed to add Bitmap to DiskLruCache:" + ex.getMessage());
if (editor != null) {
try {
editor.abort();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
public @NonNull CompletableFuture<Bitmap> getBitmap(@NonNull String aKey) {
Bitmap cached = mMemoryCache.get(aKey);
if (cached != null) {
return CompletableFuture.completedFuture(cached);
} else {
CompletableFuture<Bitmap> result = new CompletableFuture<>();
runIO(() -> {
try (DiskLruCache.Snapshot snapshot = mDiskCache.get(aKey)){
if (snapshot != null) {
Bitmap bitmap = BitmapFactory.decodeStream(snapshot.getInputStream(0));
if (bitmap != null) {
mMainThreadExecutor.execute(() -> {
if (mMemoryCache.get(aKey) == null) {
// Do not update cache if it already contains a value
// A tab could have saved a new image while we were loading the cached disk image.
mMemoryCache.put(aKey, bitmap);
}
result.complete(bitmap);
});
return;
}
}
}
catch (Exception ex) {
Log.e(LOGTAG, "Failed to get Bitmap from DiskLruCache:" + ex.getMessage());
}
mMainThreadExecutor.execute(() -> result.complete(null));
});
return result;
}
}
public void removeBitmap(@NonNull String aKey) {
mMemoryCache.remove(aKey);
runIO(() -> {
try {
mDiskCache.remove(aKey);
} catch (Exception ex) {
Log.e(LOGTAG, "Failed to remove Bitmap from DiskLruCache:" + ex.getMessage());
}
});
}
public boolean hasBitmap(@NonNull String aKey) {
return mMemoryCache.get(aKey) != null;
}
private void runIO(Runnable aRunnable) {
mIOExecutor.execute(() -> {
if (mDiskCache != null) {
synchronized (mLock) {
aRunnable.run();
}
}
});
}
public CompletableFuture<Bitmap> scaleBitmap(Bitmap aBitmap, int aMaxWidth, int aMaxHeight) {
int w = aBitmap.getWidth();
int h = aBitmap.getHeight();
if (w <= aMaxWidth && h <= aMaxHeight) {
return CompletableFuture.completedFuture(aBitmap);
}
float aspect = (float)w / (float)h;
if (w / aMaxWidth > h / aMaxHeight) {
w = aMaxWidth;
h = (int) (w / aspect);
} else {
h = aMaxHeight;
w = (int)(h * aspect);
}
final int scaledW = w;
final int scaleH = h;
CompletableFuture<Bitmap> result = new CompletableFuture<>();
runIO(() -> {
Bitmap scaled = Bitmap.createScaledBitmap(aBitmap, scaledW, scaleH, true);
if (scaled != null && scaled != aBitmap) {
aBitmap.recycle();
mMainThreadExecutor.execute(() -> result.complete(scaled));
} else {
mMainThreadExecutor.execute(() -> result.complete(aBitmap));
}
});
return result;
}
public void setCaptureSurface(SurfaceTexture aSurfaceTexture) {
mCaptureSurfaceTexture = aSurfaceTexture;
mCaptureSurface = new Surface(aSurfaceTexture);
}
public @Nullable Surface acquireCaptureSurface(int width, int height) {
if (mCapturedAcquired) {
return null;
}
mCapturedAcquired = true;
mCaptureSurfaceTexture.setDefaultBufferSize(width, height);
return mCaptureSurface;
}
public void releaseCaptureSurface() {
mCapturedAcquired = false;
}
public void onDestroy() {
if (mDiskCache != null) {
runIO(() -> {
try {
mDiskCache.close();
} catch (IOException ex) {
Log.e(LOGTAG, "Failed to close DiskLruCache:" + ex.getMessage());
}
mDiskCache = null;
});
}
if (mCaptureSurface != null) {
mCaptureSurface.release();
mCaptureSurface = null;
}
if (mCaptureSurfaceTexture != null) {
mCaptureSurfaceTexture.release();
mCaptureSurfaceTexture = null;
}
}
}