Merge remote-tracking branch 'upstream/develop' into resizedImages
[pub/Android/ownCloud.git] / src / com / owncloud / android / ui / preview / PreviewImageFragment.java
index 581526c..bb33a9c 100644 (file)
  */
 package com.owncloud.android.ui.preview;
 
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FilterInputStream;
-import java.io.IOException;
-import java.io.InputStream;
 import java.lang.ref.WeakReference;
 
 import android.accounts.Account;
 import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.BitmapFactory.Options;
 import android.graphics.Point;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.support.v4.app.FragmentStatePagerAdapter;
-import android.view.Display;
 import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
@@ -46,9 +40,6 @@ import android.widget.ImageView;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 
-import com.actionbarsherlock.view.Menu;
-import com.actionbarsherlock.view.MenuInflater;
-import com.actionbarsherlock.view.MenuItem;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.OCFile;
@@ -59,24 +50,27 @@ import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
 import com.owncloud.android.ui.dialog.RemoveFileDialogFragment;
 import com.owncloud.android.ui.fragment.FileFragment;
 import com.owncloud.android.utils.BitmapUtils;
+import com.owncloud.android.utils.DisplayUtils;
 
 import third_parties.michaelOrtiz.TouchImageViewCustom;
 
 
 /**
  * This fragment shows a preview of a downloaded image.
+ *
+ * Trying to get an instance with a NULL {@link OCFile} will produce an
+ * {@link IllegalStateException}.
  * 
- * Trying to get an instance with NULL {@link OCFile} or ownCloud {@link Account} values will produce an {@link IllegalStateException}.
- * 
- * If the {@link OCFile} passed is not downloaded, an {@link IllegalStateException} is generated on instantiation too.
+ * If the {@link OCFile} passed is not downloaded, an {@link IllegalStateException} is generated on
+ * instantiation too.
  */
 public class PreviewImageFragment extends FileFragment {
 
     public static final String EXTRA_FILE = "FILE";
-    public static final String EXTRA_ACCOUNT = "ACCOUNT";
 
-    private View mView;
-    private Account mAccount;
+    private static final String ARG_FILE = "FILE";
+    private static final String ARG_IGNORE_FIRST = "IGNORE_FIRST";
+
     private TouchImageViewCustom mImageView;
     private TextView mMessageView;
     private ProgressBar mProgressWheel;
@@ -91,34 +85,44 @@ public class PreviewImageFragment extends FileFragment {
     
     private LoadBitmapTask mLoadBitmapTask = null;
 
-    
+
     /**
-     * Creates a fragment to preview an image.
-     * 
-     * When 'imageFile' or 'ocAccount' are null
-     * 
-     * @param fileToDetail              An {@link OCFile} to preview as an image in the fragment
-     * @param ocAccount                 An ownCloud account; needed to start downloads
-     * @param ignoreFirstSavedState     Flag to work around an unexpected behaviour of {@link FragmentStatePagerAdapter}; TODO better solution 
+     * Public factory method to create a new fragment that previews an image.
+     *
+     * Android strongly recommends keep the empty constructor of fragments as the only public
+     * constructor, and
+     * use {@link #setArguments(Bundle)} to set the needed arguments.
+     *
+     * This method hides to client objects the need of doing the construction in two steps.
+     *
+     * @param imageFile                 An {@link OCFile} to preview as an image in the fragment
+     * @param ignoreFirstSavedState     Flag to work around an unexpected behaviour of
+     *                                  {@link FragmentStatePagerAdapter}
+     *                                  ; TODO better solution
      */
-    public PreviewImageFragment(OCFile fileToDetail, Account ocAccount, boolean ignoreFirstSavedState, boolean showResizedImage) {
-        super(fileToDetail);
-        mAccount = ocAccount;
-        mShowResizedImage = showResizedImage;
-        mIgnoreFirstSavedState = ignoreFirstSavedState;
+    public static PreviewImageFragment newInstance(OCFile imageFile, boolean ignoreFirstSavedState,
+            boolean showResizedImage){
+        PreviewImageFragment frag = new PreviewImageFragment();
+        frag.mShowResizedImage = showResizedImage;
+        Bundle args = new Bundle();
+        args.putParcelable(ARG_FILE, imageFile);
+        args.putBoolean(ARG_IGNORE_FIRST, ignoreFirstSavedState);
+        frag.setArguments(args);
+        return frag;
     }
+
     
     
     /**
      *  Creates an empty fragment for image previews.
      * 
-     *  MUST BE KEPT: the system uses it when tries to reinstantiate a fragment automatically (for instance, when the device is turned a aside).
+     *  MUST BE KEPT: the system uses it when tries to reinstantiate a fragment automatically
+     *  (for instance, when the device is turned a aside).
      * 
-     *  DO NOT CALL IT: an {@link OCFile} and {@link Account} must be provided for a successful construction 
+     *  DO NOT CALL IT: an {@link OCFile} and {@link Account} must be provided for a successful
+     *  construction
      */
     public PreviewImageFragment() {
-        super();
-        mAccount = null;
         mIgnoreFirstSavedState = false;
     }
     
@@ -129,6 +133,12 @@ public class PreviewImageFragment extends FileFragment {
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        Bundle args = getArguments();
+        setFile((OCFile)args.getParcelable(ARG_FILE));
+            // TODO better in super, but needs to check ALL the class extending FileFragment;
+            // not right now
+
+        mIgnoreFirstSavedState = args.getBoolean(ARG_IGNORE_FIRST);
         setHasOptionsMenu(true);
     }
     
@@ -140,8 +150,8 @@ public class PreviewImageFragment extends FileFragment {
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
         super.onCreateView(inflater, container, savedInstanceState);
-        mView = inflater.inflate(R.layout.preview_image_fragment, container, false);
-        mImageView = (TouchImageViewCustom) mView.findViewById(R.id.image);
+        View view = inflater.inflate(R.layout.preview_image_fragment, container, false);
+        mImageView = (TouchImageViewCustom) view.findViewById(R.id.image);
         mImageView.setVisibility(View.GONE);
         mImageView.setOnClickListener(new OnClickListener() {
             @Override
@@ -150,11 +160,11 @@ public class PreviewImageFragment extends FileFragment {
             }
 
         });
-        mMessageView = (TextView)mView.findViewById(R.id.message);
+        mMessageView = (TextView)view.findViewById(R.id.message);
         mMessageView.setVisibility(View.GONE);
-        mProgressWheel = (ProgressBar)mView.findViewById(R.id.progressWheel);
+        mProgressWheel = (ProgressBar)view.findViewById(R.id.progressWheel);
         mProgressWheel.setVisibility(View.VISIBLE);
-        return mView;
+        return view;
     }
 
     /**
@@ -165,9 +175,8 @@ public class PreviewImageFragment extends FileFragment {
         super.onActivityCreated(savedInstanceState);
         if (savedInstanceState != null) {
             if (!mIgnoreFirstSavedState) {
-                OCFile file = (OCFile)savedInstanceState.getParcelable(PreviewImageFragment.EXTRA_FILE);
+                OCFile file = savedInstanceState.getParcelable(PreviewImageFragment.EXTRA_FILE);
                 setFile(file);
-                mAccount = savedInstanceState.getParcelable(PreviewImageFragment.EXTRA_ACCOUNT);
             } else {
                 mIgnoreFirstSavedState = false;
             }
@@ -175,9 +184,6 @@ public class PreviewImageFragment extends FileFragment {
         if (getFile() == null) {
             throw new IllegalStateException("Instanced with a NULL OCFile");
         }
-        if (mAccount == null) {
-            throw new IllegalStateException("Instanced with a NULL ownCloud Account");
-        }
     }
         
 
@@ -188,7 +194,6 @@ public class PreviewImageFragment extends FileFragment {
     public void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
         outState.putParcelable(PreviewImageFragment.EXTRA_FILE, getFile());
-        outState.putParcelable(PreviewImageFragment.EXTRA_ACCOUNT, mAccount);
     }
     
 
@@ -206,7 +211,6 @@ public class PreviewImageFragment extends FileFragment {
                 if (thumbnail != null && !getFile().needsUpdateThumbnail()){
                     mProgressWheel.setVisibility(View.GONE);
                     mImageView.setImageBitmap(thumbnail);
-                    mImageView.setBitmap(thumbnail);
                     mImageView.setVisibility(View.VISIBLE);
                     mBitmap  = thumbnail;
                 } else {
@@ -214,8 +218,9 @@ public class PreviewImageFragment extends FileFragment {
                     if (ThumbnailsCacheManager.cancelPotentialWork(getFile(), mImageView)) {
                         final ThumbnailsCacheManager.ThumbnailGenerationTask task =
                                 new ThumbnailsCacheManager.ThumbnailGenerationTask(
-                                        mImageView, mContainerActivity.getStorageManager(), mAccount, mProgressWheel
-                                );
+                                        mImageView, mContainerActivity.getStorageManager(),
+                                        mContainerActivity.getStorageManager().getAccount(),
+                                        mProgressWheel);
                         if (thumbnail == null) {
                             thumbnail = ThumbnailsCacheManager.mDefaultImg;
                         }
@@ -231,7 +236,7 @@ public class PreviewImageFragment extends FileFragment {
             }
             } else {
                 mLoadBitmapTask = new LoadBitmapTask(mImageView, mMessageView, mProgressWheel);
-                mLoadBitmapTask.execute(new String[]{getFile().getStoragePath()});
+                mLoadBitmapTask.execute(getFile().getStoragePath());
             }
         }
     }
@@ -239,12 +244,12 @@ public class PreviewImageFragment extends FileFragment {
     
     @Override
     public void onStop() {
-        super.onStop();
+        Log_OC.d(TAG, "onStop starts");
         if (mLoadBitmapTask != null) {
             mLoadBitmapTask.cancel(true);
             mLoadBitmapTask = null;
         }
-        
+        super.onStop();
     }
     
     /**
@@ -271,7 +276,7 @@ public class PreviewImageFragment extends FileFragment {
                 getFile(),
                 mContainerActivity.getStorageManager().getAccount(),
                 mContainerActivity,
-                getSherlockActivity()
+                getActivity()
             );
             mf.filter(menu);
         }
@@ -371,6 +376,9 @@ public class PreviewImageFragment extends FileFragment {
         if (mBitmap != null) {
             mBitmap.recycle();
             System.gc();
+                // putting this in onStop() is just the same; the fragment is always destroyed by
+                // {@link FragmentStatePagerAdapter} when the fragment in swiped further than the
+                // valid offscreen distance, and onStop() is never called before than that
         }
         super.onDestroy();
     }
@@ -389,15 +397,17 @@ public class PreviewImageFragment extends FileFragment {
 
         /**
          * Weak reference to the target {@link ImageView} where the bitmap will be loaded into.
-         * 
-         * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes.
+         *
+         * Using a weak reference will avoid memory leaks if the target ImageView is retired from
+         * memory before the load finishes.
          */
         private final WeakReference<ImageViewCustom> mImageViewRef;
 
         /**
          * Weak reference to the target {@link TextView} where error messages will be written.
-         * 
-         * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes.
+         *
+         * Using a weak reference will avoid memory leaks if the target ImageView is retired from
+         * memory before the load finishes.
          */
         private final WeakReference<TextView> mMessageViewRef;
 
@@ -405,7 +415,8 @@ public class PreviewImageFragment extends FileFragment {
         /**
          * Weak reference to the target {@link ProgressBar} shown while the load is in progress.
          * 
-         * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes.
+         * Using a weak reference will avoid memory leaks if the target ImageView is retired from
+         * memory before the load finishes.
          */
         private final WeakReference<ProgressBar> mProgressWheelRef;
 
@@ -421,7 +432,8 @@ public class PreviewImageFragment extends FileFragment {
          * 
          * @param imageView     Target {@link ImageView} where the bitmap will be loaded into.
          */
-        public LoadBitmapTask(ImageViewCustom imageView, TextView messageView, ProgressBar progressWheel) {
+        public LoadBitmapTask(ImageViewCustom imageView, TextView messageView,
+                              ProgressBar progressWheel) {
             mImageViewRef = new WeakReference<ImageViewCustom>(imageView);
             mMessageViewRef = new WeakReference<TextView>(messageView);
             mProgressWheelRef = new WeakReference<ProgressBar>(progressWheel);
@@ -431,47 +443,50 @@ public class PreviewImageFragment extends FileFragment {
         @Override
         protected Bitmap doInBackground(String... params) {
             Bitmap result = null;
-            if (params.length != 1) return result;
+            if (params.length != 1) return null;
             String storagePath = params[0];
             try {
 
-                if (isCancelled()) return result;
-                
-                File picture = new File(storagePath);
-
-                if (picture != null) {
-                    // Decode file into a bitmap in real size for being able to make zoom on 
-                    // the image
-                    result = BitmapFactory.decodeStream(new FlushedInputStream
-                            (new BufferedInputStream(new FileInputStream(picture))));
-                }
+                int maxDownScale = 3;   // could be a parameter passed to doInBackground(...)
+                Point screenSize = DisplayUtils.getScreenSize(getActivity());
+                int minWidth = screenSize.x;
+                int minHeight = screenSize.y;
+                for (int i = 0; i < maxDownScale && result == null; i++) {
+                    if (isCancelled()) return null;
+                    try {
+                        result = BitmapUtils.decodeSampledBitmapFromFile(storagePath, minWidth,
+                                minHeight);
+
+                        if (isCancelled()) return result;
+
+                        if (result == null) {
+                            mErrorMessageId = R.string.preview_image_error_unknown_format;
+                            Log_OC.e(TAG, "File could not be loaded as a bitmap: " + storagePath);
+                            break;
+                        } else {
+                            // Rotate image, obeying exif tag.
+                            result = BitmapUtils.rotateImage(result, storagePath);
+                        }
 
-                if (isCancelled()) return result;
-                
-                if (result == null) {
-                    mErrorMessageId = R.string.preview_image_error_unknown_format;
-                    Log_OC.e(TAG, "File could not be loaded as a bitmap: " + storagePath);
-                } else {
-                    // Rotate image, obeying exif tag.
-                    result = BitmapUtils.rotateImage(result, storagePath);
+                    } catch (OutOfMemoryError e) {
+                        mErrorMessageId = R.string.common_error_out_memory;
+                        if (i < maxDownScale - 1) {
+                            Log_OC.w(TAG, "Out of memory rendering file " + storagePath +
+                                    " ; scaling down");
+                            minWidth = minWidth / 2;
+                            minHeight = minHeight / 2;
+
+                        } else {
+                            Log_OC.w(TAG, "Out of memory rendering file " + storagePath +
+                                    " ; failing");
+                        }
+                        if (result != null) {
+                            result.recycle();
+                        }
+                        result = null;
+                    }
                 }
-                
-            } catch (OutOfMemoryError e) {
-                Log_OC.e(TAG, "Out of memory occured for file " + storagePath, e);
 
-                if (isCancelled()) return result;
-                
-                // If out of memory error when loading or rotating image, try to load it scaled
-                result = loadScaledImage(storagePath);
-
-                if (result == null) {
-                    mErrorMessageId = R.string.preview_image_error_unknown_format;
-                    Log_OC.e(TAG, "File could not be loaded as a bitmap: " + storagePath);
-                } else {
-                    // Rotate scaled image, obeying exif tag
-                    result = BitmapUtils.rotateImage(result, storagePath);
-                }
-                    
             } catch (NoSuchFieldError e) {
                 mErrorMessageId = R.string.common_error_unknown;
                 Log_OC.e(TAG, "Error from access to unexisting field despite protection; file " 
@@ -501,57 +516,55 @@ public class PreviewImageFragment extends FileFragment {
             } else {
                 showErrorMessage();
             }
+            if (result != null && mBitmap != result)  {
+                // unused bitmap, release it! (just in case)
+                result.recycle();
+            }
         }
         
         @SuppressLint("InlinedApi")
         private void showLoadedImage(Bitmap result) {
-            if (mImageViewRef != null) {
-                final ImageViewCustom imageView = mImageViewRef.get();
-                if (imageView != null) {
-                    imageView.setBitmap(result);
-                    imageView.setImageBitmap(result);
-                    imageView.setVisibility(View.VISIBLE);
-                    mBitmap  = result;
-                } // else , silently finish, the fragment was destroyed
-            }
-            if (mMessageViewRef != null) {
-                final TextView messageView = mMessageViewRef.get();
-                if (messageView != null) {
-                    messageView.setVisibility(View.GONE);
-                } // else , silently finish, the fragment was destroyed
+            final ImageViewCustom imageView = mImageViewRef.get();
+            if (imageView != null) {
+                Log_OC.d(TAG, "Showing image with resolution " + result.getWidth() + "x" +
+                        result.getHeight());
+                imageView.setImageBitmap(result);
+                imageView.setVisibility(View.VISIBLE);
+                mBitmap  = result;  // needs to be kept for recycling when not useful
             }
+
+            final TextView messageView = mMessageViewRef.get();
+            if (messageView != null) {
+                messageView.setVisibility(View.GONE);
+            } // else , silently finish, the fragment was destroyed
         }
         
         private void showErrorMessage() {
-            if (mImageViewRef != null) {
-                final ImageView imageView = mImageViewRef.get();
-                if (imageView != null) {
-                    // shows the default error icon
-                    imageView.setVisibility(View.VISIBLE);
-                } // else , silently finish, the fragment was destroyed
-            }
-            if (mMessageViewRef != null) {
-                final TextView messageView = mMessageViewRef.get();
-                if (messageView != null) {
-                    messageView.setText(mErrorMessageId);
-                    messageView.setVisibility(View.VISIBLE);
-                } // else , silently finish, the fragment was destroyed
-            }
+            final ImageView imageView = mImageViewRef.get();
+            if (imageView != null) {
+                // shows the default error icon
+                imageView.setVisibility(View.VISIBLE);
+            } // else , silently finish, the fragment was destroyed
+
+            final TextView messageView = mMessageViewRef.get();
+            if (messageView != null) {
+                messageView.setText(mErrorMessageId);
+                messageView.setVisibility(View.VISIBLE);
+            } // else , silently finish, the fragment was destroyed
         }
         
         private void hideProgressWheel() {
-            if (mProgressWheelRef != null) {
-                final ProgressBar progressWheel = mProgressWheelRef.get();
-                if (progressWheel != null) {
-                    progressWheel.setVisibility(View.GONE);
-                }
+            final ProgressBar progressWheel = mProgressWheelRef.get();
+            if (progressWheel != null) {
+                progressWheel.setVisibility(View.GONE);
             }
         }
         
     }
 
     /**
-     * Helper method to test if an {@link OCFile} can be passed to a {@link PreviewImageFragment} to be previewed.
+     * Helper method to test if an {@link OCFile} can be passed to a {@link PreviewImageFragment}
+     * to be previewed.
      * 
      * @param file      File to test if can be previewed.
      * @return          'True' if the file can be handled by the fragment.
@@ -573,83 +586,4 @@ public class PreviewImageFragment extends FileFragment {
         return mImageView;
     }
 
-    static class FlushedInputStream extends FilterInputStream {
-        public FlushedInputStream(InputStream inputStream) {
-        super(inputStream);
-        }
-
-        @Override
-        public long skip(long n) throws IOException {
-            long totalBytesSkipped = 0L;
-            while (totalBytesSkipped < n) {
-                long bytesSkipped = in.skip(n - totalBytesSkipped);
-                if (bytesSkipped == 0L) {
-                      int byteValue = read();
-                      if (byteValue < 0) {
-                          break;  // we reached EOF
-                      } else {
-                          bytesSkipped = 1; // we read one byte
-                      }
-               }
-               totalBytesSkipped += bytesSkipped;
-            }
-            return totalBytesSkipped;
-        }
-    }
-
-    /**
-     * Load image scaled
-     * @param storagePath: path of the image
-     * @return Bitmap
-     */
-    @SuppressWarnings("deprecation")
-    private Bitmap loadScaledImage(String storagePath) {
-
-        Log_OC.d(TAG, "Loading image scaled");
-
-        // set desired options that will affect the size of the bitmap
-        BitmapFactory.Options options = new Options();
-        options.inScaled = true;
-        options.inPurgeable = true;
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
-            options.inPreferQualityOverSpeed = false;
-        }
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
-            options.inMutable = false;
-        }
-        // make a false load of the bitmap - just to be able to read outWidth, outHeight and outMimeType
-        options.inJustDecodeBounds = true;
-        BitmapFactory.decodeFile(storagePath, options);
-
-        int width = options.outWidth;
-        int height = options.outHeight;
-        int scale = 1;
-
-        Display display = getActivity().getWindowManager().getDefaultDisplay();
-        Point size = new Point();
-        int screenWidth;
-        int screenHeight;
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR2) {
-            display.getSize(size);
-            screenWidth = size.x;
-            screenHeight = size.y;
-        } else {
-            screenWidth = display.getWidth();
-            screenHeight = display.getHeight();
-        }
-
-        if (width > screenWidth) {
-            // second try to scale down the image , this time depending upon the screen size 
-            scale = (int) Math.floor((float)width / screenWidth);
-        }
-        if (height > screenHeight) {
-            scale = Math.max(scale, (int) Math.floor((float)height / screenHeight));
-        }
-        options.inSampleSize = scale;
-
-        // really load the bitmap
-        options.inJustDecodeBounds = false; // the next decodeFile call will be real
-        return BitmapFactory.decodeFile(storagePath, options);
-
-    }
 }