Fix OOM during image browsing

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
This commit is contained in:
tobiasKaminsky 2020-03-23 17:48:35 +01:00
parent 2c9b0758e4
commit 45315f1486
No known key found for this signature in database
GPG key ID: 0E00D4D47D0C5AF7
6 changed files with 149 additions and 31 deletions

View file

@ -1 +1 @@
381 383

View file

@ -211,6 +211,23 @@ public final class ThumbnailsCacheManager {
return mThumbnailCache.containsKey(key); return mThumbnailCache.containsKey(key);
} }
public static Bitmap getScaledBitmapFromDiskCache(String key, int width, int height) {
synchronized (mThumbnailsDiskCacheLock) {
// Wait while disk cache is started from background thread
while (mThumbnailCacheStarting) {
try {
mThumbnailsDiskCacheLock.wait();
} catch (InterruptedException e) {
Log_OC.e(TAG, "Wait in mThumbnailsDiskCacheLock was interrupted", e);
}
}
if (mThumbnailCache != null) {
return mThumbnailCache.getScaledBitmap(key, width, height);
}
}
return null;
}
public static Bitmap getBitmapFromDiskCache(String key) { public static Bitmap getBitmapFromDiskCache(String key) {
synchronized (mThumbnailsDiskCacheLock) { synchronized (mThumbnailsDiskCacheLock) {
// Wait while disk cache is started from background thread // Wait while disk cache is started from background thread

View file

@ -133,7 +133,7 @@ public class FileMenuFilter {
List<Integer> toShow = new ArrayList<>(); List<Integer> toShow = new ArrayList<>();
List<Integer> toHide = new ArrayList<>(); List<Integer> toHide = new ArrayList<>();
filter(toShow, toHide, inSingleFileFragment, isMediaSupported, menu); filter(toShow, toHide, inSingleFileFragment, isMediaSupported);
for (int i : toShow) { for (int i : toShow) {
showMenuItem(menu.findItem(i)); showMenuItem(menu.findItem(i));
@ -193,8 +193,7 @@ public class FileMenuFilter {
private void filter(List<Integer> toShow, private void filter(List<Integer> toShow,
List<Integer> toHide, List<Integer> toHide,
boolean inSingleFileFragment, boolean inSingleFileFragment,
boolean isMediaSupported, boolean isMediaSupported) {
Menu menu) {
boolean synchronizing = anyFileSynchronizing(); boolean synchronizing = anyFileSynchronizing();
OCCapability capability = componentsGetter.getStorageManager().getCapability(account.name); OCCapability capability = componentsGetter.getStorageManager().getCapability(account.name);
boolean endToEndEncryptionEnabled = capability.getEndToEndEncryption().isTrue(); boolean endToEndEncryptionEnabled = capability.getEndToEndEncryption().isTrue();

View file

@ -28,6 +28,7 @@ import android.graphics.BitmapFactory;
import com.jakewharton.disklrucache.DiskLruCache; import com.jakewharton.disklrucache.DiskLruCache;
import com.owncloud.android.BuildConfig; import com.owncloud.android.BuildConfig;
import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.utils.BitmapUtils;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
@ -49,7 +50,7 @@ public class DiskLruImageCache {
private static final String TAG = DiskLruImageCache.class.getSimpleName(); private static final String TAG = DiskLruImageCache.class.getSimpleName();
public DiskLruImageCache(File diskCacheDir, int diskCacheSize, CompressFormat compressFormat, int quality) public DiskLruImageCache(File diskCacheDir, int diskCacheSize, CompressFormat compressFormat, int quality)
throws IOException { throws IOException {
mDiskCache = DiskLruCache.open(diskCacheDir, CACHE_VERSION, VALUE_COUNT, diskCacheSize); mDiskCache = DiskLruCache.open(diskCacheDir, CACHE_VERSION, VALUE_COUNT, diskCacheSize);
mCompressFormat = compressFormat; mCompressFormat = compressFormat;
mCompressQuality = quality; mCompressQuality = quality;
@ -103,10 +104,11 @@ public class DiskLruImageCache {
} }
} }
public Bitmap getBitmap(String key) { public Bitmap getScaledBitmap(String key, int width, int height) {
Bitmap bitmap = null; Bitmap bitmap = null;
DiskLruCache.Snapshot snapshot = null; DiskLruCache.Snapshot snapshot = null;
InputStream inputStream = null;
BufferedInputStream buffIn = null;
String validKey = convertToValidKey(key); String validKey = convertToValidKey(key);
try { try {
@ -114,18 +116,49 @@ public class DiskLruImageCache {
if (snapshot == null) { if (snapshot == null) {
return null; return null;
} }
final InputStream in = snapshot.getInputStream(0); inputStream = snapshot.getInputStream(0);
if (in != null) { if (inputStream != null) {
final BufferedInputStream buffIn = buffIn = new BufferedInputStream(inputStream, IO_BUFFER_SIZE);
new BufferedInputStream(in, IO_BUFFER_SIZE);
bitmap = BitmapFactory.decodeStream(buffIn); // First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = true;
options.inPurgeable = true;
options.inPreferQualityOverSpeed = false;
options.inMutable = false;
options.inJustDecodeBounds = true;
bitmap = BitmapFactory.decodeStream(buffIn, null, options);
// Calculate inSampleSize
options.inSampleSize = BitmapUtils.calculateSampleFactor(options, width, height);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
bitmap = BitmapFactory.decodeStream(buffIn, null, options);
} }
} catch (IOException e) { } catch (Exception e) {
Log_OC.e(TAG, e.getMessage(), e); Log_OC.e(TAG, e.getMessage(), e);
} finally { } finally {
if (snapshot != null) { if (snapshot != null) {
snapshot.close(); snapshot.close();
} }
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// nothing to do
}
}
if (buffIn != null) {
try {
buffIn.close();
} catch (IOException e) {
// nothing to do
}
}
} }
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
@ -133,7 +166,52 @@ public class DiskLruImageCache {
} }
return bitmap; return bitmap;
}
public Bitmap getBitmap(String key) {
Bitmap bitmap = null;
DiskLruCache.Snapshot snapshot = null;
InputStream in = null;
BufferedInputStream buffIn = null;
String validKey = convertToValidKey(key);
try {
snapshot = mDiskCache.get(validKey);
if (snapshot == null) {
return null;
}
in = snapshot.getInputStream(0);
if (in != null) {
buffIn = new BufferedInputStream(in, IO_BUFFER_SIZE);
bitmap = BitmapFactory.decodeStream(buffIn);
}
} catch (IOException e) {
Log_OC.e(TAG, e.getMessage(), e);
} finally {
if (snapshot != null) {
snapshot.close();
}
if (buffIn != null) {
try {
buffIn.close();
} catch (IOException e) {
Log_OC.e(TAG, e.getMessage(), e);
}
}
if (in != null) {
try {
in.close();
} catch (IOException e) {
Log_OC.e(TAG, e.getMessage(), e);
}
}
}
if (BuildConfig.DEBUG) {
Log_OC.d(CACHE_TEST_DISK, bitmap == null ? "not found" : "image read from disk " + validKey);
}
return bitmap;
} }
public boolean containsKey(String key) { public boolean containsKey(String key) {

View file

@ -81,6 +81,7 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
@ -149,7 +150,8 @@ public class PreviewImageFragment extends FileFragment implements Injectable {
* {@link FragmentStatePagerAdapter} * {@link FragmentStatePagerAdapter}
* ; TODO better solution * ; TODO better solution
*/ */
public static PreviewImageFragment newInstance(@NonNull OCFile imageFile, boolean ignoreFirstSavedState, public static PreviewImageFragment newInstance(@NonNull OCFile imageFile,
boolean ignoreFirstSavedState,
boolean showResizedImage) { boolean showResizedImage) {
PreviewImageFragment frag = new PreviewImageFragment(); PreviewImageFragment frag = new PreviewImageFragment();
frag.mShowResizedImage = showResizedImage; frag.mShowResizedImage = showResizedImage;
@ -250,9 +252,12 @@ public class PreviewImageFragment extends FileFragment implements Injectable {
if (getFile() != null) { if (getFile() != null) {
mImageView.setTag(getFile().getFileId()); mImageView.setTag(getFile().getFileId());
Point screenSize = DisplayUtils.getScreenSize(getActivity());
int width = screenSize.x;
int height = screenSize.y;
if (mShowResizedImage) { if (mShowResizedImage) {
Bitmap resizedImage = ThumbnailsCacheManager.getBitmapFromDiskCache( Bitmap resizedImage = getResizedBitmap(getFile(), width, height);
String.valueOf(ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + getFile().getRemoteId()));
if (resizedImage != null && !getFile().isUpdateThumbnailNeeded()) { if (resizedImage != null && !getFile().isUpdateThumbnailNeeded()) {
mImageView.setImageBitmap(resizedImage); mImageView.setImageBitmap(resizedImage);
@ -260,8 +265,7 @@ public class PreviewImageFragment extends FileFragment implements Injectable {
mBitmap = resizedImage; mBitmap = resizedImage;
} else { } else {
// show thumbnail while loading resized image // show thumbnail while loading resized image
Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( Bitmap thumbnail = getResizedBitmap(getFile(), width, height);
String.valueOf(ThumbnailsCacheManager.PREFIX_THUMBNAIL + getFile().getRemoteId()));
if (thumbnail != null) { if (thumbnail != null) {
mImageView.setImageBitmap(thumbnail); mImageView.setImageBitmap(thumbnail);
@ -273,22 +277,22 @@ public class PreviewImageFragment extends FileFragment implements Injectable {
// generate new resized image // generate new resized image
if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(getFile(), mImageView) && if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(getFile(), mImageView) &&
containerActivity.getStorageManager() != null) { containerActivity.getStorageManager() != null) {
final ThumbnailsCacheManager.ResizedImageGenerationTask task = final ThumbnailsCacheManager.ResizedImageGenerationTask task =
new ThumbnailsCacheManager.ResizedImageGenerationTask(this, new ThumbnailsCacheManager.ResizedImageGenerationTask(this,
mImageView, mImageView,
containerActivity.getStorageManager(), containerActivity.getStorageManager(),
connectivityService, connectivityService,
containerActivity.getStorageManager().getAccount()); containerActivity.getStorageManager().getAccount());
if (resizedImage == null) { if (resizedImage == null) {
resizedImage = thumbnail; resizedImage = thumbnail;
} }
final ThumbnailsCacheManager.AsyncResizedImageDrawable asyncDrawable = final ThumbnailsCacheManager.AsyncResizedImageDrawable asyncDrawable =
new ThumbnailsCacheManager.AsyncResizedImageDrawable( new ThumbnailsCacheManager.AsyncResizedImageDrawable(
MainApp.getAppContext().getResources(), MainApp.getAppContext().getResources(),
resizedImage, resizedImage,
task task
); );
mImageView.setImageDrawable(asyncDrawable); mImageView.setImageDrawable(asyncDrawable);
task.execute(getFile()); task.execute(getFile());
} }
@ -306,6 +310,27 @@ public class PreviewImageFragment extends FileFragment implements Injectable {
} }
} }
private @Nullable
Bitmap getResizedBitmap(OCFile file, int width, int height) {
Bitmap cachedImage = null;
int scaledWidth = width;
int scaledHeight = height;
for (int i = 0; i < 3 && cachedImage == null; i++) {
try {
cachedImage = ThumbnailsCacheManager.getScaledBitmapFromDiskCache(
ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.getRemoteId(),
scaledWidth,
scaledHeight);
} catch (OutOfMemoryError e) {
scaledWidth = scaledWidth / 2;
scaledHeight = scaledHeight / 2;
}
}
return cachedImage;
}
@Override @Override
public void onStop() { public void onStop() {
Log_OC.d(TAG, "onStop starts"); Log_OC.d(TAG, "onStop starts");
@ -494,7 +519,6 @@ public class PreviewImageFragment extends FileFragment implements Injectable {
OCFile ocFile = params[0]; OCFile ocFile = params[0];
String storagePath = ocFile.getStoragePath(); String storagePath = ocFile.getStoragePath();
try { try {
int maxDownScale = 3; // could be a parameter passed to doInBackground(...) int maxDownScale = 3; // could be a parameter passed to doInBackground(...)
Point screenSize = DisplayUtils.getScreenSize(getActivity()); Point screenSize = DisplayUtils.getScreenSize(getActivity());
int minWidth = screenSize.x; int minWidth = screenSize.x;

View file

@ -104,7 +104,7 @@ public final class BitmapUtils {
* @return The largest inSampleSize value that is a power of 2 and keeps both * @return The largest inSampleSize value that is a power of 2 and keeps both
* height and width larger than reqWidth and reqHeight. * height and width larger than reqWidth and reqHeight.
*/ */
private static int calculateSampleFactor(Options options, int reqWidth, int reqHeight) { public static int calculateSampleFactor(Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight; final int height = options.outHeight;
final int width = options.outWidth; final int width = options.outWidth;