Browse Source

Merge pull request #5707 from nextcloud/fixOOMduringImages

Fix OOM during image browsing
Tobias Kaminsky 5 years ago
parent
commit
1013b6b977

+ 1 - 1
scripts/analysis/findbugs-results.txt

@@ -1 +1 @@
-381
+383

+ 17 - 0
src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java

@@ -211,6 +211,23 @@ public final class ThumbnailsCacheManager {
         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) {
         synchronized (mThumbnailsDiskCacheLock) {
             // Wait while disk cache is started from background thread

+ 84 - 6
src/main/java/com/owncloud/android/ui/adapter/DiskLruImageCache.java

@@ -28,6 +28,7 @@ import android.graphics.BitmapFactory;
 import com.jakewharton.disklrucache.DiskLruCache;
 import com.owncloud.android.BuildConfig;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.utils.BitmapUtils;
 
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
@@ -49,7 +50,7 @@ public class DiskLruImageCache {
     private static final String TAG = DiskLruImageCache.class.getSimpleName();
 
     public DiskLruImageCache(File diskCacheDir, int diskCacheSize, CompressFormat compressFormat, int quality)
-            throws IOException {
+        throws IOException {
         mDiskCache = DiskLruCache.open(diskCacheDir, CACHE_VERSION, VALUE_COUNT, diskCacheSize);
         mCompressFormat = compressFormat;
         mCompressQuality = quality;
@@ -103,10 +104,75 @@ public class DiskLruImageCache {
         }
     }
 
-    public Bitmap getBitmap(String key) {
+    public Bitmap getScaledBitmap(String key, int width, int height) {
+        Bitmap bitmap = null;
+        DiskLruCache.Snapshot snapshot = null;
+        InputStream inputStream = null;
+        BufferedInputStream buffIn = null;
+        String validKey = convertToValidKey(key);
+
+        try {
+            snapshot = mDiskCache.get(validKey);
+            if (snapshot == null) {
+                return null;
+            }
+            inputStream = snapshot.getInputStream(0);
+            if (inputStream != null) {
+                buffIn = new BufferedInputStream(inputStream, IO_BUFFER_SIZE);
+
+                // 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 (Exception e) {
+            Log_OC.e(TAG, e.getMessage(), e);
+        } finally {
+            if (snapshot != null) {
+                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) {
+            Log_OC.d(CACHE_TEST_DISK, bitmap == null ? "not found" : "image read from disk " + validKey);
+        }
+
+        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 {
@@ -114,10 +180,9 @@ public class DiskLruImageCache {
             if (snapshot == null) {
                 return null;
             }
-            final InputStream in = snapshot.getInputStream(0);
+            in = snapshot.getInputStream(0);
             if (in != null) {
-                final BufferedInputStream buffIn =
-                        new BufferedInputStream(in, IO_BUFFER_SIZE);
+                buffIn = new BufferedInputStream(in, IO_BUFFER_SIZE);
                 bitmap = BitmapFactory.decodeStream(buffIn);
             }
         } catch (IOException e) {
@@ -126,6 +191,20 @@ public class DiskLruImageCache {
             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) {
@@ -133,7 +212,6 @@ public class DiskLruImageCache {
         }
 
         return bitmap;
-
     }
 
     public boolean containsKey(String key) {

+ 41 - 17
src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.java

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

+ 1 - 1
src/main/java/com/owncloud/android/utils/BitmapUtils.java

@@ -104,7 +104,7 @@ public final class BitmapUtils {
      * @return The largest inSampleSize value that is a power of 2 and keeps both
      *                      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 width = options.outWidth;