1 /* ownCloud Android client application 
   2  *   Copyright (C) 2012-2014 ownCloud Inc.  
   4  *   This program is free software: you can redistribute it and/or modify 
   5  *   it under the terms of the GNU General Public License version 2, 
   6  *   as published by the Free Software Foundation. 
   8  *   This program is distributed in the hope that it will be useful, 
   9  *   but WITHOUT ANY WARRANTY; without even the implied warranty of 
  10  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  11  *   GNU General Public License for more details. 
  13  *   You should have received a copy of the GNU General Public License 
  14  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. 
  17 package com
.owncloud
.android
.ui
.preview
; 
  19 import java
.io
.BufferedInputStream
; 
  21 import java
.io
.FileInputStream
; 
  22 import java
.io
.FilterInputStream
; 
  23 import java
.io
.IOException
; 
  24 import java
.io
.InputStream
; 
  25 import java
.lang
.ref
.WeakReference
; 
  27 import android
.accounts
.Account
; 
  28 import android
.annotation
.SuppressLint
; 
  29 import android
.app
.Activity
; 
  30 import android
.graphics
.Bitmap
; 
  31 import android
.graphics
.BitmapFactory
; 
  32 import android
.graphics
.BitmapFactory
.Options
; 
  33 import android
.graphics
.Point
; 
  34 import android
.os
.AsyncTask
; 
  35 import android
.os
.Bundle
; 
  36 import android
.support
.v4
.app
.FragmentStatePagerAdapter
; 
  37 import android
.view
.Display
; 
  38 import android
.view
.LayoutInflater
; 
  39 import android
.view
.View
; 
  40 import android
.view
.View
.OnClickListener
; 
  41 import android
.view
.ViewGroup
; 
  42 import android
.widget
.ImageView
; 
  43 import android
.widget
.ProgressBar
; 
  44 import android
.widget
.TextView
; 
  46 import com
.actionbarsherlock
.view
.Menu
; 
  47 import com
.actionbarsherlock
.view
.MenuInflater
; 
  48 import com
.actionbarsherlock
.view
.MenuItem
; 
  49 import com
.owncloud
.android
.R
; 
  50 import com
.owncloud
.android
.datamodel
.OCFile
; 
  51 import com
.owncloud
.android
.files
.FileMenuFilter
; 
  52 import com
.owncloud
.android
.lib
.common
.utils
.Log_OC
; 
  53 import com
.owncloud
.android
.ui
.dialog
.ConfirmationDialogFragment
; 
  54 import com
.owncloud
.android
.ui
.dialog
.RemoveFileDialogFragment
; 
  55 import com
.owncloud
.android
.ui
.fragment
.FileFragment
; 
  56 import com
.owncloud
.android
.utils
.TouchImageViewCustom
; 
  61  * This fragment shows a preview of a downloaded image. 
  63  * Trying to get an instance with NULL {@link OCFile} or ownCloud {@link Account} values will produce an {@link IllegalStateException}. 
  65  * If the {@link OCFile} passed is not downloaded, an {@link IllegalStateException} is generated on instantiation too. 
  67  * @author David A. Velasco 
  69 public class PreviewImageFragment 
extends FileFragment 
{ 
  71     public static final String EXTRA_FILE 
= "FILE"; 
  72     public static final String EXTRA_ACCOUNT 
= "ACCOUNT"; 
  75     private Account mAccount
; 
  76     private TouchImageViewCustom mImageView
; 
  77     private TextView mMessageView
; 
  78     private ProgressBar mProgressWheel
; 
  80     public Bitmap mBitmap 
= null
; 
  82     private static final String TAG 
= PreviewImageFragment
.class.getSimpleName(); 
  84     private boolean mIgnoreFirstSavedState
; 
  88      * Creates a fragment to preview an image. 
  90      * When 'imageFile' or 'ocAccount' are null 
  92      * @param imageFile                 An {@link OCFile} to preview as an image in the fragment 
  93      * @param ocAccount                 An ownCloud account; needed to start downloads 
  94      * @param ignoreFirstSavedState     Flag to work around an unexpected behaviour of {@link FragmentStatePagerAdapter}; TODO better solution  
  96     public PreviewImageFragment(OCFile fileToDetail
, Account ocAccount
, boolean ignoreFirstSavedState
) { 
  99         mIgnoreFirstSavedState 
= ignoreFirstSavedState
; 
 104      *  Creates an empty fragment for image previews. 
 106      *  MUST BE KEPT: the system uses it when tries to reinstantiate a fragment automatically (for instance, when the device is turned a aside). 
 108      *  DO NOT CALL IT: an {@link OCFile} and {@link Account} must be provided for a successful construction  
 110     public PreviewImageFragment() { 
 113         mIgnoreFirstSavedState 
= false
; 
 121     public void onCreate(Bundle savedInstanceState
) { 
 122         super.onCreate(savedInstanceState
); 
 123         setHasOptionsMenu(true
); 
 131     public View 
onCreateView(LayoutInflater inflater
, ViewGroup container
, 
 132             Bundle savedInstanceState
) { 
 133         super.onCreateView(inflater
, container
, savedInstanceState
); 
 134         mView 
= inflater
.inflate(R
.layout
.preview_image_fragment
, container
, false
); 
 135         mImageView 
= (TouchImageViewCustom
) mView
.findViewById(R
.id
.image
); 
 136         mImageView
.setVisibility(View
.GONE
); 
 137         mImageView
.setOnClickListener(new OnClickListener() { 
 139             public void onClick(View v
) { 
 140                 ((PreviewImageActivity
) getActivity()).toggleFullScreen(); 
 144         mMessageView 
= (TextView
)mView
.findViewById(R
.id
.message
); 
 145         mMessageView
.setVisibility(View
.GONE
); 
 146         mProgressWheel 
= (ProgressBar
)mView
.findViewById(R
.id
.progressWheel
); 
 147         mProgressWheel
.setVisibility(View
.VISIBLE
); 
 155     public void onActivityCreated(Bundle savedInstanceState
) { 
 156         super.onActivityCreated(savedInstanceState
); 
 157         if (savedInstanceState 
!= null
) { 
 158             if (!mIgnoreFirstSavedState
) { 
 159                 OCFile file 
= (OCFile
)savedInstanceState
.getParcelable(PreviewImageFragment
.EXTRA_FILE
); 
 161                 mAccount 
= savedInstanceState
.getParcelable(PreviewImageFragment
.EXTRA_ACCOUNT
); 
 163                 mIgnoreFirstSavedState 
= false
; 
 166         if (getFile() == null
) { 
 167             throw new IllegalStateException("Instanced with a NULL OCFile"); 
 169         if (mAccount 
== null
) { 
 170             throw new IllegalStateException("Instanced with a NULL ownCloud Account"); 
 172         if (!getFile().isDown()) { 
 173             throw new IllegalStateException("There is no local file to preview"); 
 182     public void onSaveInstanceState(Bundle outState
) { 
 183         super.onSaveInstanceState(outState
); 
 184         outState
.putParcelable(PreviewImageFragment
.EXTRA_FILE
, getFile()); 
 185         outState
.putParcelable(PreviewImageFragment
.EXTRA_ACCOUNT
, mAccount
); 
 190     public void onStart() { 
 192         if (getFile() != null
) { 
 193            BitmapLoader bl 
= new BitmapLoader(mImageView
, mMessageView
, mProgressWheel
); 
 194            bl
.execute(new String
[]{getFile().getStoragePath()}); 
 203     public void onCreateOptionsMenu(Menu menu
, MenuInflater inflater
) { 
 204         super.onCreateOptionsMenu(menu
, inflater
); 
 205         inflater
.inflate(R
.menu
.file_actions_menu
, menu
); 
 212     public void onPrepareOptionsMenu(Menu menu
) { 
 213         super.onPrepareOptionsMenu(menu
); 
 215         if (mContainerActivity
.getStorageManager() != null
) { 
 217             setFile(mContainerActivity
.getStorageManager().getFileById(getFile().getFileId())); 
 219             FileMenuFilter mf 
= new FileMenuFilter( 
 221                 mContainerActivity
.getStorageManager().getAccount(), 
 223                 getSherlockActivity() 
 228         // additional restriction for this fragment  
 229         // TODO allow renaming in PreviewImageFragment 
 230         MenuItem item 
= menu
.findItem(R
.id
.action_rename_file
); 
 232             item
.setVisible(false
); 
 233             item
.setEnabled(false
); 
 236         // additional restriction for this fragment  
 237         // TODO allow refresh file in PreviewImageFragment 
 238         item 
= menu
.findItem(R
.id
.action_sync_file
); 
 240             item
.setVisible(false
); 
 241             item
.setEnabled(false
); 
 244         // additional restriction for this fragment 
 245         item 
= menu
.findItem(R
.id
.action_move
); 
 247             item
.setVisible(false
); 
 248             item
.setEnabled(false
); 
 259     public boolean onOptionsItemSelected(MenuItem item
) { 
 260         switch (item
.getItemId()) { 
 261             case R
.id
.action_share_file
: { 
 262                 mContainerActivity
.getFileOperationsHelper().shareFileWithLink(getFile()); 
 265             case R
.id
.action_unshare_file
: { 
 266                 mContainerActivity
.getFileOperationsHelper().unshareFileWithLink(getFile()); 
 269             case R
.id
.action_open_file_with
: { 
 273             case R
.id
.action_remove_file
: { 
 274                 RemoveFileDialogFragment dialog 
= RemoveFileDialogFragment
.newInstance(getFile()); 
 275                 dialog
.show(getFragmentManager(), ConfirmationDialogFragment
.FTAG_CONFIRMATION
); 
 278             case R
.id
.action_see_details
: { 
 282             case R
.id
.action_send_file
: { 
 283                 mContainerActivity
.getFileOperationsHelper().sendDownloadedFile(getFile()); 
 286             case R
.id
.action_sync_file
: { 
 287                 mContainerActivity
.getFileOperationsHelper().syncFile(getFile()); 
 297     private void seeDetails() { 
 298         mContainerActivity
.showDetails(getFile());         
 303     public void onResume() { 
 309     public void onPause() { 
 314     public void onDestroy() { 
 315         if (mBitmap 
!= null
) { 
 324      * Opens the previewed image with an external application. 
 326     private void openFile() { 
 327         mContainerActivity
.getFileOperationsHelper().openFile(getFile()); 
 332     private class BitmapLoader 
extends AsyncTask
<String
, Void
, Bitmap
> { 
 335          * Weak reference to the target {@link ImageView} where the bitmap will be loaded into. 
 337          * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes. 
 339         private final WeakReference
<ImageViewCustom
> mImageViewRef
; 
 342          * Weak reference to the target {@link TextView} where error messages will be written. 
 344          * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes. 
 346         private final WeakReference
<TextView
> mMessageViewRef
; 
 350          * Weak reference to the target {@link Progressbar} shown while the load is in progress. 
 352          * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes. 
 354         private final WeakReference
<ProgressBar
> mProgressWheelRef
; 
 358          * Error message to show when a load fails  
 360         private int mErrorMessageId
; 
 366          * @param imageView     Target {@link ImageView} where the bitmap will be loaded into. 
 368         public BitmapLoader(ImageViewCustom imageView
, TextView messageView
, ProgressBar progressWheel
) { 
 369             mImageViewRef 
= new WeakReference
<ImageViewCustom
>(imageView
); 
 370             mMessageViewRef 
= new WeakReference
<TextView
>(messageView
); 
 371             mProgressWheelRef 
= new WeakReference
<ProgressBar
>(progressWheel
); 
 376         protected Bitmap 
doInBackground(String
... params
) { 
 377             Bitmap result 
= null
; 
 378             if (params
.length 
!= 1) return result
; 
 379             String storagePath 
= params
[0]; 
 382                 File picture 
= new File(storagePath
); 
 384                 if (picture 
!= null
) { 
 385                     //Decode file into a bitmap in real size for being able to make zoom on the image 
 386                     result 
= BitmapFactory
.decodeStream(new FlushedInputStream
 
 387                             (new BufferedInputStream(new FileInputStream(picture
)))); 
 390                 if (result 
== null
) { 
 391                     mErrorMessageId 
= R
.string
.preview_image_error_unknown_format
; 
 392                     Log_OC
.e(TAG
, "File could not be loaded as a bitmap: " + storagePath
); 
 395             } catch (OutOfMemoryError e
) { 
 396                 Log_OC
.e(TAG
, "Out of memory occured for file " + storagePath
, e
); 
 398                 // If out of memory error when loading image, try to load it scaled 
 399                 result 
= loadScaledImage(storagePath
); 
 401                 if (result 
== null
) { 
 402                     mErrorMessageId 
= R
.string
.preview_image_error_unknown_format
; 
 403                     Log_OC
.e(TAG
, "File could not be loaded as a bitmap: " + storagePath
); 
 406             } catch (NoSuchFieldError e
) { 
 407                 mErrorMessageId 
= R
.string
.common_error_unknown
; 
 408                 Log_OC
.e(TAG
, "Error from access to unexisting field despite protection; file " + storagePath
, e
); 
 410             } catch (Throwable t
) { 
 411                 mErrorMessageId 
= R
.string
.common_error_unknown
; 
 412                 Log_OC
.e(TAG
, "Unexpected error loading " + getFile().getStoragePath(), t
); 
 419         protected void onPostExecute(Bitmap result
) { 
 421             if (result 
!= null
) { 
 422                 showLoadedImage(result
); 
 428         @SuppressLint("InlinedApi") 
 429         private void showLoadedImage(Bitmap result
) { 
 430             if (mImageViewRef 
!= null
) { 
 431                 final ImageViewCustom imageView 
= mImageViewRef
.get(); 
 432                 if (imageView 
!= null
) { 
 433                     imageView
.setBitmap(result
); 
 434                     imageView
.setImageBitmap(result
); 
 435                     imageView
.setVisibility(View
.VISIBLE
); 
 437                 } // else , silently finish, the fragment was destroyed 
 439             if (mMessageViewRef 
!= null
) { 
 440                 final TextView messageView 
= mMessageViewRef
.get(); 
 441                 if (messageView 
!= null
) { 
 442                     messageView
.setVisibility(View
.GONE
); 
 443                 } // else , silently finish, the fragment was destroyed 
 447         private void showErrorMessage() { 
 448             if (mImageViewRef 
!= null
) { 
 449                 final ImageView imageView 
= mImageViewRef
.get(); 
 450                 if (imageView 
!= null
) { 
 451                     // shows the default error icon 
 452                     imageView
.setVisibility(View
.VISIBLE
); 
 453                 } // else , silently finish, the fragment was destroyed 
 455             if (mMessageViewRef 
!= null
) { 
 456                 final TextView messageView 
= mMessageViewRef
.get(); 
 457                 if (messageView 
!= null
) { 
 458                     messageView
.setText(mErrorMessageId
); 
 459                     messageView
.setVisibility(View
.VISIBLE
); 
 460                 } // else , silently finish, the fragment was destroyed 
 464         private void hideProgressWheel() { 
 465             if (mProgressWheelRef 
!= null
) { 
 466                 final ProgressBar progressWheel 
= mProgressWheelRef
.get(); 
 467                 if (progressWheel 
!= null
) { 
 468                     progressWheel
.setVisibility(View
.GONE
); 
 476      * Helper method to test if an {@link OCFile} can be passed to a {@link PreviewImageFragment} to be previewed. 
 478      * @param file      File to test if can be previewed. 
 479      * @return          'True' if the file can be handled by the fragment. 
 481     public static boolean canBePreviewed(OCFile file
) { 
 482         return (file 
!= null 
&& file
.isImage()); 
 487      * Finishes the preview 
 489     private void finish() { 
 490         Activity container 
= getActivity(); 
 494     public TouchImageViewCustom 
getImageView() { 
 498     static class FlushedInputStream 
extends FilterInputStream 
{ 
 499         public FlushedInputStream(InputStream inputStream
) { 
 504         public long skip(long n
) throws IOException 
{ 
 505             long totalBytesSkipped 
= 0L; 
 506             while (totalBytesSkipped 
< n
) { 
 507                 long bytesSkipped 
= in.skip(n 
- totalBytesSkipped
); 
 508                 if (bytesSkipped 
== 0L) { 
 509                       int byteValue 
= read(); 
 511                           break;  // we reached EOF 
 513                           bytesSkipped 
= 1; // we read one byte 
 516                totalBytesSkipped 
+= bytesSkipped
; 
 518             return totalBytesSkipped
; 
 524      * @param storagePath: path of the image 
 527     @SuppressWarnings("deprecation") 
 528     private Bitmap 
loadScaledImage(String storagePath
) { 
 530         Log_OC
.d(TAG
, "Loading image scaled"); 
 532         // set desired options that will affect the size of the bitmap 
 533         BitmapFactory
.Options options 
= new Options(); 
 534         options
.inScaled 
= true
; 
 535         options
.inPurgeable 
= true
; 
 536         if (android
.os
.Build
.VERSION
.SDK_INT 
>= android
.os
.Build
.VERSION_CODES
.GINGERBREAD_MR1
) { 
 537             options
.inPreferQualityOverSpeed 
= false
; 
 539         if (android
.os
.Build
.VERSION
.SDK_INT 
>= android
.os
.Build
.VERSION_CODES
.HONEYCOMB
) { 
 540             options
.inMutable 
= false
; 
 542         // make a false load of the bitmap - just to be able to read outWidth, outHeight and outMimeType 
 543         options
.inJustDecodeBounds 
= true
; 
 544         BitmapFactory
.decodeFile(storagePath
, options
); 
 546         int width 
= options
.outWidth
; 
 547         int height 
= options
.outHeight
; 
 550         Display display 
= getActivity().getWindowManager().getDefaultDisplay(); 
 551         Point size 
= new Point(); 
 554         if (android
.os
.Build
.VERSION
.SDK_INT 
>= android
.os
.Build
.VERSION_CODES
.HONEYCOMB_MR2
) { 
 555             display
.getSize(size
); 
 556             screenWidth 
= size
.x
; 
 557             screenHeight 
= size
.y
; 
 559             screenWidth 
= display
.getWidth(); 
 560             screenHeight 
= display
.getHeight(); 
 563         if (width 
> screenWidth
) { 
 564             // second try to scale down the image , this time depending upon the screen size  
 565             scale 
= (int) Math
.floor((float)width 
/ screenWidth
); 
 567         if (height 
> screenHeight
) { 
 568             scale 
= Math
.max(scale
, (int) Math
.floor((float)height 
/ screenHeight
)); 
 570         options
.inSampleSize 
= scale
; 
 572         // really load the bitmap 
 573         options
.inJustDecodeBounds 
= false
; // the next decodeFile call will be real 
 574         return BitmapFactory
.decodeFile(storagePath
, options
);