Merge pull request #641 from owncloud/thumbnails_for_downloaded_images
authorJavier Gonzalez <javiergonzper@gmail.com>
Wed, 1 Oct 2014 09:35:50 +0000 (11:35 +0200)
committerJavier Gonzalez <javiergonzper@gmail.com>
Wed, 1 Oct 2014 09:35:50 +0000 (11:35 +0200)
Thumbnails for downloaded images

.classpath
libs/disklrucache-2.0.2.jar [new file with mode: 0644]
lint.xml [deleted file]
src/com/owncloud/android/datamodel/FileDataStorageManager.java
src/com/owncloud/android/datamodel/OCFile.java
src/com/owncloud/android/db/ProviderMeta.java
src/com/owncloud/android/files/services/FileDownloader.java
src/com/owncloud/android/providers/FileContentProvider.java
src/com/owncloud/android/ui/adapter/DiskLruImageCache.java [new file with mode: 0644]
src/com/owncloud/android/ui/adapter/FileListListAdapter.java
src/com/owncloud/android/utils/BitmapUtils.java [new file with mode: 0644]

index 7bc01d9..5176974 100644 (file)
@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <classpath>
-       <classpathentry kind="src" path="src"/>
-       <classpathentry kind="src" path="gen"/>
        <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
        <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
        <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="src" path="gen"/>
        <classpathentry kind="output" path="bin/classes"/>
 </classpath>
diff --git a/libs/disklrucache-2.0.2.jar b/libs/disklrucache-2.0.2.jar
new file mode 100644 (file)
index 0000000..ca7907d
Binary files /dev/null and b/libs/disklrucache-2.0.2.jar differ
diff --git a/lint.xml b/lint.xml
deleted file mode 100644 (file)
index ee0eead..0000000
--- a/lint.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<lint>
-</lint>
\ No newline at end of file
index 4848246..795004a 100644 (file)
@@ -188,6 +188,7 @@ public class FileDataStorageManager {
         cv.put(ProviderTableMeta.FILE_PUBLIC_LINK, file.getPublicLink());
         cv.put(ProviderTableMeta.FILE_PERMISSIONS, file.getPermissions());
         cv.put(ProviderTableMeta.FILE_REMOTE_ID, file.getRemoteId());
+        cv.put(ProviderTableMeta.FILE_UPDATE_THUMBNAIL, file.needsUpdateThumbnail());
         
         boolean sameRemotePath = fileExists(file.getRemotePath());
         if (sameRemotePath ||
@@ -878,6 +879,8 @@ public class FileDataStorageManager {
             file.setPublicLink(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_PUBLIC_LINK)));
             file.setPermissions(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_PERMISSIONS)));
             file.setRemoteId(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_REMOTE_ID)));
+            file.setNeedsUpdateThumbnail(c.getInt(
+                    c.getColumnIndex(ProviderTableMeta.FILE_UPDATE_THUMBNAIL)) == 1 ? true : false);
                     
         }
         return file;
@@ -1222,6 +1225,7 @@ public class FileDataStorageManager {
                 cv.put(ProviderTableMeta.FILE_PUBLIC_LINK, file.getPublicLink());
                 cv.put(ProviderTableMeta.FILE_PERMISSIONS, file.getPermissions());
                 cv.put(ProviderTableMeta.FILE_REMOTE_ID, file.getRemoteId());
+                cv.put(ProviderTableMeta.FILE_UPDATE_THUMBNAIL, file.needsUpdateThumbnail() ? 1 : 0);
 
                 boolean existsByPath = fileExists(file.getRemotePath());
                 if (existsByPath || fileExists(file.getFileId())) {
index 5885106..c3284fc 100644 (file)
@@ -68,6 +68,8 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
     private String mPermissions;
     private String mRemoteId;
 
+    private boolean mNeedsUpdateThumbnail;
+
 
     /**
      * Create new {@link OCFile} with given path.
@@ -109,6 +111,8 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
         mPublicLink = source.readString();
         mPermissions = source.readString();
         mRemoteId = source.readString();
+        mNeedsUpdateThumbnail = source.readInt() == 0;
+
     }
 
     @Override
@@ -131,6 +135,7 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
         dest.writeString(mPublicLink);
         dest.writeString(mPermissions);
         dest.writeString(mRemoteId);
+        dest.writeInt(mNeedsUpdateThumbnail ? 1 : 0);
     }
     
     /**
@@ -343,6 +348,7 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
         mPublicLink = null;
         mPermissions = null;
         mRemoteId = null;
+        mNeedsUpdateThumbnail = false;
     }
 
     /**
@@ -408,6 +414,14 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
         return mNeedsUpdating;
     }
     
+    public boolean needsUpdateThumbnail() {
+        return mNeedsUpdateThumbnail;
+    }
+
+    public void setNeedsUpdateThumbnail(boolean needsUpdateThumbnail) {
+        this.mNeedsUpdateThumbnail = needsUpdateThumbnail;
+    }
+
     public long getLastSyncDateForProperties() {
         return mLastSyncDateForProperties;
     }
index 1adf697..bc59869 100644 (file)
@@ -31,7 +31,7 @@ import com.owncloud.android.MainApp;
 public class ProviderMeta {\r
 \r
     public static final String DB_NAME = "filelist";\r
-    public static final int DB_VERSION = 7;\r
+    public static final int DB_VERSION = 8;\r
 \r
     private ProviderMeta() {\r
     }\r
@@ -70,6 +70,7 @@ public class ProviderMeta {
         public static final String FILE_PUBLIC_LINK = "public_link";\r
         public static final String FILE_PERMISSIONS = "permissions";\r
         public static final String FILE_REMOTE_ID = "remote_id";\r
+        public static final String FILE_UPDATE_THUMBNAIL = "update_thumbnail";\r
 \r
         public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME\r
                 + " collate nocase asc";\r
index 2e423be..cb0f12c 100644 (file)
@@ -391,6 +391,7 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
         long syncDate = System.currentTimeMillis();
         file.setLastSyncDateForProperties(syncDate);
         file.setLastSyncDateForData(syncDate);
+        file.setNeedsUpdateThumbnail(true);
         file.setModificationTimestamp(mCurrentDownload.getModificationTimestamp());
         file.setModificationTimestampAtLastSyncForData(mCurrentDownload.getModificationTimestamp());
         // file.setEtag(mCurrentDownload.getEtag());    // TODO Etag, where available
index 8113e04..9792f3e 100644 (file)
@@ -97,6 +97,8 @@ public class FileContentProvider extends ContentProvider {
                 ProviderTableMeta.FILE_PERMISSIONS);
         mFileProjectionMap.put(ProviderTableMeta.FILE_REMOTE_ID,
                 ProviderTableMeta.FILE_REMOTE_ID);
+        mFileProjectionMap.put(ProviderTableMeta.FILE_UPDATE_THUMBNAIL,
+                ProviderTableMeta.FILE_UPDATE_THUMBNAIL);
     }
 
     private static final int SINGLE_FILE = 1;
@@ -559,7 +561,8 @@ public class FileContentProvider extends ContentProvider {
                     + ProviderTableMeta.FILE_SHARE_BY_LINK + " INTEGER, "
                     + ProviderTableMeta.FILE_PUBLIC_LINK  + " TEXT, "
                     + ProviderTableMeta.FILE_PERMISSIONS  + " TEXT null,"
-                    + ProviderTableMeta.FILE_REMOTE_ID  + " TEXT null);"
+                    + ProviderTableMeta.FILE_REMOTE_ID  + " TEXT null,"
+                    + ProviderTableMeta.FILE_UPDATE_THUMBNAIL  + " INTEGER);" //boolean
                     );
             
             // Create table ocshares
@@ -708,6 +711,23 @@ public class FileContentProvider extends ContentProvider {
             }
             if (!upgraded)
                 Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion + ", newVersion == " + newVersion);
+
+            if (oldVersion < 8 && newVersion >= 8) {
+                Log_OC.i("SQL", "Entering in the #8 ADD in onUpgrade");
+                db.beginTransaction();
+                try {
+                    db .execSQL("ALTER TABLE " + ProviderTableMeta.FILE_TABLE_NAME +
+                            " ADD COLUMN " + ProviderTableMeta.FILE_UPDATE_THUMBNAIL + " INTEGER " +
+                            " DEFAULT 0");
+
+                    upgraded = true;
+                    db.setTransactionSuccessful();
+                } finally {
+                    db.endTransaction();
+                }
+            }
+            if (!upgraded)
+                Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion + ", newVersion == " + newVersion);
         }
     }
 
diff --git a/src/com/owncloud/android/ui/adapter/DiskLruImageCache.java b/src/com/owncloud/android/ui/adapter/DiskLruImageCache.java
new file mode 100644 (file)
index 0000000..fceaba2
--- /dev/null
@@ -0,0 +1,189 @@
+package com.owncloud.android.ui.adapter;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+import com.jakewharton.disklrucache.DiskLruCache;
+import com.owncloud.android.BuildConfig;
+import com.owncloud.android.utils.Log_OC;
+
+public class DiskLruImageCache {
+
+    private DiskLruCache mDiskCache;
+    private CompressFormat mCompressFormat;
+    private int mCompressQuality;
+    private static final int CACHE_VERSION = 1;
+    private static final int VALUE_COUNT = 1;
+    private static final int IO_BUFFER_SIZE = 8 * 1024;
+    private static final Pattern CAPITAL_LETTERS = Pattern.compile("[A-Z]"); 
+
+    private StringBuffer mValidKeyBuffer = new StringBuffer(64);
+    private StringBuffer mConversionBuffer = new StringBuffer(2).append('_'); 
+            
+    private static final String TAG = "DiskLruImageCache";
+
+    public DiskLruImageCache( Context context,String uniqueName, int diskCacheSize,
+        CompressFormat compressFormat, int quality ) throws IOException {
+        final File diskCacheDir = getDiskCacheDir(context, uniqueName );
+        mDiskCache = DiskLruCache.open(
+                diskCacheDir, CACHE_VERSION, VALUE_COUNT, diskCacheSize 
+        );
+        mCompressFormat = compressFormat;
+        mCompressQuality = quality;
+    }
+
+    private boolean writeBitmapToFile( Bitmap bitmap, DiskLruCache.Editor editor )
+        throws IOException, FileNotFoundException {
+        OutputStream out = null;
+        try {
+            out = new BufferedOutputStream( editor.newOutputStream( 0 ), IO_BUFFER_SIZE );
+            return bitmap.compress( mCompressFormat, mCompressQuality, out );
+        } finally {
+            if ( out != null ) {
+                out.close();
+            }
+        }
+    }
+
+    private File getDiskCacheDir(Context context, String uniqueName) {
+
+    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
+    // otherwise use internal cache dir
+        final String cachePath = context.getExternalCacheDir().getPath();
+
+        Log_OC.d(TAG, "create dir: " + cachePath + File.separator + uniqueName);
+                    
+        return new File(cachePath + File.separator + uniqueName);
+    }
+
+    public void put( String key, Bitmap data ) {
+
+        DiskLruCache.Editor editor = null;
+        String validKey = convertToValidKey(key);
+        try {
+            editor = mDiskCache.edit( validKey );
+            if ( editor == null ) {
+                return;
+            }
+
+            if( writeBitmapToFile( data, editor ) ) {               
+                mDiskCache.flush();
+                editor.commit();
+                if ( BuildConfig.DEBUG ) {
+                   Log.d( "cache_test_DISK_", "image put on disk cache " + validKey );
+                }
+            } else {
+                editor.abort();
+                if ( BuildConfig.DEBUG ) {
+                    Log.d( "cache_test_DISK_", "ERROR on: image put on disk cache " + validKey );
+                }
+            }   
+        } catch (IOException e) {
+            if ( BuildConfig.DEBUG ) {
+                Log.d( "cache_test_DISK_", "ERROR on: image put on disk cache " + validKey );
+            }
+            try {
+                if ( editor != null ) {
+                    editor.abort();
+                }
+            } catch (IOException ignored) {
+            }           
+        }
+
+    }
+
+    public Bitmap getBitmap( String key ) {
+
+        Bitmap bitmap = null;
+        DiskLruCache.Snapshot snapshot = null;
+        String validKey = convertToValidKey(key);
+        try {
+
+            snapshot = mDiskCache.get( validKey );
+            if ( snapshot == null ) {
+                return null;
+            }
+            final InputStream in = snapshot.getInputStream( 0 );
+            if ( in != null ) {
+                final BufferedInputStream buffIn = 
+                new BufferedInputStream( in, IO_BUFFER_SIZE );
+                bitmap = BitmapFactory.decodeStream( buffIn );              
+            }   
+        } catch ( IOException e ) {
+            e.printStackTrace();
+        } finally {
+            if ( snapshot != null ) {
+                snapshot.close();
+            }
+        }
+
+        if ( BuildConfig.DEBUG ) {
+            Log.d("cache_test_DISK_", bitmap == null ? "not found" : "image read from disk " + validKey);
+        }
+
+        return bitmap;
+
+    }
+
+    public boolean containsKey( String key ) {
+
+        boolean contained = false;
+        DiskLruCache.Snapshot snapshot = null;
+        String validKey = convertToValidKey(key);
+        try {
+            snapshot = mDiskCache.get( validKey );
+            contained = snapshot != null;
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            if ( snapshot != null ) {
+                snapshot.close();
+            }
+        }
+
+        return contained;
+
+    }
+
+    public void clearCache() {
+        if ( BuildConfig.DEBUG ) {
+            Log.d( "cache_test_DISK_", "disk cache CLEARED");
+        }
+        try {
+            mDiskCache.delete();
+        } catch ( IOException e ) {
+            e.printStackTrace();
+        }
+    }
+
+    public File getCacheFolder() {
+        return mDiskCache.getDirectory();
+    }
+    
+    private String convertToValidKey(String key) {
+        Matcher capitalLettersMatcher = CAPITAL_LETTERS.matcher(key);
+        mValidKeyBuffer.delete(0, mValidKeyBuffer.length());
+        mConversionBuffer.delete(1, mConversionBuffer.length());
+        
+        while (capitalLettersMatcher.find()) {
+            mConversionBuffer.replace(1, 2, capitalLettersMatcher.group(0).toLowerCase()); 
+            capitalLettersMatcher.appendReplacement(mValidKeyBuffer, mConversionBuffer.toString());
+        }
+        capitalLettersMatcher.appendTail(mValidKeyBuffer);
+        return mValidKeyBuffer.toString();
+    }
+
+}
\ No newline at end of file
index 65a9c83..d748a70 100644 (file)
  */\r
 package com.owncloud.android.ui.adapter;\r
 \r
+import java.io.File;\r
+import java.lang.ref.WeakReference;\r
 import java.util.Vector;\r
 \r
 import android.accounts.Account;\r
 import android.content.Context;\r
+import android.content.res.Resources;\r
+import android.graphics.Bitmap;\r
+import android.graphics.Bitmap.CompressFormat;\r
+import android.graphics.BitmapFactory;\r
+import android.graphics.drawable.BitmapDrawable;\r
+import android.graphics.drawable.Drawable;\r
+import android.media.ThumbnailUtils;\r
+import android.os.AsyncTask;\r
+import android.util.TypedValue;\r
 import android.view.LayoutInflater;\r
 import android.view.View;\r
 import android.view.ViewGroup;\r
@@ -37,7 +48,9 @@ import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;\r
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;\r
 import com.owncloud.android.ui.activity.ComponentsGetter;\r
+import com.owncloud.android.utils.BitmapUtils;\r
 import com.owncloud.android.utils.DisplayUtils;\r
+import com.owncloud.android.utils.Log_OC;\r
 \r
 \r
 /**\r
@@ -45,10 +58,13 @@ import com.owncloud.android.utils.DisplayUtils;
  * instance.\r
  * \r
  * @author Bartek Przybylski\r
- * \r
+ * @author Tobias Kaminsky\r
+ * @author David A. Velasco\r
  */\r
 public class FileListListAdapter extends BaseAdapter implements ListAdapter {\r
     private final static String PERMISSION_SHARED_WITH_ME = "S";\r
+    \r
+    private static final String TAG = FileListListAdapter.class.getSimpleName();\r
 \r
     private Context mContext;\r
     private OCFile mFile = null;\r
@@ -59,15 +75,159 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
     private Account mAccount;
     private ComponentsGetter mTransferServiceGetter;\r
     \r
+    private final Object thumbnailDiskCacheLock = new Object();\r
+    private DiskLruImageCache mThumbnailCache;\r
+    private boolean mThumbnailCacheStarting = true;\r
+    private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB\r
+    private static final CompressFormat mCompressFormat = CompressFormat.JPEG;\r
+    private static final int mCompressQuality = 70;\r
+    private Bitmap defaultImg;\r
+        \r
     public FileListListAdapter(\r
             boolean justFolders, \r
             Context context, \r
             ComponentsGetter transferServiceGetter\r
             ) {\r
+        \r
         mJustFolders = justFolders;\r
         mContext = context;\r
         mAccount = AccountUtils.getCurrentOwnCloudAccount(mContext);\r
         mTransferServiceGetter = transferServiceGetter;\r
+        defaultImg = BitmapFactory.decodeResource(mContext.getResources(), \r
+                    DisplayUtils.getResourceId("image/png", "default.png"));\r
+        \r
+        // Initialise disk cache on background thread\r
+        new InitDiskCacheTask().execute();\r
+    }\r
+    \r
+    class InitDiskCacheTask extends AsyncTask<File, Void, Void> {\r
+        @Override\r
+        protected Void doInBackground(File... params) {\r
+            synchronized (thumbnailDiskCacheLock) {\r
+                try {\r
+                    mThumbnailCache = new DiskLruImageCache(mContext, "thumbnailCache", \r
+                                        DISK_CACHE_SIZE, mCompressFormat, mCompressQuality);\r
+                } catch (Exception e) {\r
+                    Log_OC.d(TAG, "Thumbnail cache could not be opened ", e);\r
+                    mThumbnailCache = null;\r
+                }\r
+                mThumbnailCacheStarting = false; // Finished initialization\r
+                thumbnailDiskCacheLock.notifyAll(); // Wake any waiting threads\r
+            }\r
+            return null;\r
+        }\r
+    }\r
+    \r
+    static class AsyncDrawable extends BitmapDrawable {\r
+        private final WeakReference<ThumbnailGenerationTask> bitmapWorkerTaskReference;\r
+\r
+        public AsyncDrawable(Resources res, Bitmap bitmap,\r
+                ThumbnailGenerationTask bitmapWorkerTask) {\r
+            super(res, bitmap);\r
+            bitmapWorkerTaskReference =\r
+                new WeakReference<ThumbnailGenerationTask>(bitmapWorkerTask);\r
+        }\r
+\r
+        public ThumbnailGenerationTask getBitmapWorkerTask() {\r
+            return bitmapWorkerTaskReference.get();\r
+        }\r
+    }\r
+\r
+    class ThumbnailGenerationTask extends AsyncTask<OCFile, Void, Bitmap> {\r
+        private final WeakReference<ImageView> imageViewReference;\r
+        private OCFile file;\r
+\r
+        \r
+        public ThumbnailGenerationTask(ImageView imageView) {\r
+         // Use a WeakReference to ensure the ImageView can be garbage collected\r
+            imageViewReference = new WeakReference<ImageView>(imageView);\r
+        }\r
+\r
+        // Decode image in background.\r
+        @Override\r
+        protected Bitmap doInBackground(OCFile... params) {\r
+            Bitmap thumbnail = null;\r
+            \r
+            try {\r
+                file = params[0];\r
+                final String imageKey = String.valueOf(file.getRemoteId());\r
+    \r
+                // Check disk cache in background thread\r
+                thumbnail = getBitmapFromDiskCache(imageKey);\r
+    \r
+                // Not found in disk cache\r
+                if (thumbnail == null || file.needsUpdateThumbnail()) { \r
+                    // Converts dp to pixel\r
+                    Resources r = mContext.getResources();\r
+                    int px = (int) Math.round(TypedValue.applyDimension(\r
+                            TypedValue.COMPLEX_UNIT_DIP, 150, r.getDisplayMetrics()\r
+                    ));\r
+                    \r
+                    if (file.isDown()){\r
+                        Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(\r
+                                file.getStoragePath(), px, px);\r
+                        \r
+                        if (bitmap != null) {\r
+                            thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px);\r
+    \r
+                            // Add thumbnail to cache\r
+                            addBitmapToCache(imageKey, thumbnail);\r
+\r
+                            file.setNeedsUpdateThumbnail(false);\r
+                            mStorageManager.saveFile(file);\r
+                        }\r
+    \r
+                    }\r
+                }\r
+                \r
+            } catch (Throwable t) {\r
+                // the app should never break due to a problem with thumbnails\r
+                Log_OC.e(TAG, "Generation of thumbnail for " + file + " failed", t);\r
+                if (t instanceof OutOfMemoryError) {\r
+                    System.gc();\r
+                }\r
+            }\r
+            \r
+            return thumbnail;\r
+        }\r
+        \r
+        protected void onPostExecute(Bitmap bitmap){\r
+            if (isCancelled()) {\r
+                bitmap = null;\r
+            }\r
+\r
+            if (imageViewReference != null && bitmap != null) {\r
+                final ImageView imageView = imageViewReference.get();\r
+                final ThumbnailGenerationTask bitmapWorkerTask =\r
+                        getBitmapWorkerTask(imageView);\r
+                if (this == bitmapWorkerTask && imageView != null) {\r
+                    imageView.setImageBitmap(bitmap);\r
+                }\r
+            }\r
+        }\r
+    }\r
+  \r
+    public void addBitmapToCache(String key, Bitmap bitmap) {\r
+        synchronized (thumbnailDiskCacheLock) {\r
+            if (mThumbnailCache != null) {\r
+                mThumbnailCache.put(key, bitmap);\r
+            }\r
+        }\r
+    }\r
+\r
+    public Bitmap getBitmapFromDiskCache(String key) {\r
+        synchronized (thumbnailDiskCacheLock) {\r
+            // Wait while disk cache is started from background thread\r
+            while (mThumbnailCacheStarting) {\r
+                try {\r
+                    thumbnailDiskCacheLock.wait();\r
+                } catch (InterruptedException e) {}\r
+            }\r
+            if (mThumbnailCache != null) {\r
+                return (Bitmap) mThumbnailCache.getBitmap(key);\r
+            }\r
+        }\r
+        return null;\r
     }
 \r
     @Override\r
@@ -112,10 +272,10 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
                     .getSystemService(Context.LAYOUT_INFLATER_SERVICE);\r
             view = inflator.inflate(R.layout.list_item, null);\r
         }\r
-    \r
+         \r
         if (mFiles != null && mFiles.size() > position) {\r
             OCFile file = mFiles.get(position);\r
-            TextView fileName = (TextView) view.findViewById(R.id.Filename);\r
+            TextView fileName = (TextView) view.findViewById(R.id.Filename);           \r
             String name = file.getFileName();\r
 \r
             fileName.setText(name);\r
@@ -126,7 +286,8 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
 \r
             ImageView localStateView = (ImageView) view.findViewById(R.id.imageView2);\r
             localStateView.bringToFront();\r
-            FileDownloaderBinder downloaderBinder = mTransferServiceGetter.getFileDownloaderBinder();\r
+            FileDownloaderBinder downloaderBinder = \r
+                    mTransferServiceGetter.getFileDownloaderBinder();\r
             FileUploaderBinder uploaderBinder = mTransferServiceGetter.getFileUploaderBinder();\r
             if (downloaderBinder != null && downloaderBinder.isDownloading(mAccount, file)) {\r
                 localStateView.setImageResource(R.drawable.downloading_file_indicator);\r
@@ -149,7 +310,9 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
                 fileSizeV.setVisibility(View.VISIBLE);\r
                 fileSizeV.setText(DisplayUtils.bytesToHumanReadable(file.getFileLength()));\r
                 lastModV.setVisibility(View.VISIBLE);\r
-                lastModV.setText(DisplayUtils.unixTimeToHumanReadable(file.getModificationTimestamp()));\r
+                lastModV.setText(\r
+                        DisplayUtils.unixTimeToHumanReadable(file.getModificationTimestamp())\r
+                );\r
                 // this if-else is needed even thoe fav icon is visible by default\r
                 // because android reuses views in listview\r
                 if (!file.keepInSync()) {\r
@@ -168,20 +331,42 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
                         checkBoxV.setImageResource(android.R.drawable.checkbox_off_background);\r
                     }\r
                     checkBoxV.setVisibility(View.VISIBLE);\r
+                }               \r
+                \r
+                // get Thumbnail if file is image\r
+                if (file.isImage()){\r
+                     // Thumbnail in Cache?\r
+                    Bitmap thumbnail = getBitmapFromDiskCache(String.valueOf(file.getRemoteId()));\r
+                    if (thumbnail != null && !file.needsUpdateThumbnail()){\r
+                        fileIcon.setImageBitmap(thumbnail);\r
+                    } else {\r
+                        // generate new Thumbnail\r
+                        if (cancelPotentialWork(file, fileIcon)) {\r
+                            final ThumbnailGenerationTask task = \r
+                                    new ThumbnailGenerationTask(fileIcon);\r
+                            final AsyncDrawable asyncDrawable =\r
+                                    new AsyncDrawable(mContext.getResources(), defaultImg, task);\r
+                            fileIcon.setImageDrawable(asyncDrawable);\r
+                            task.execute(file);\r
+                        }\r
+                    }\r
+                } else {\r
+                    fileIcon.setImageResource(\r
+                            DisplayUtils.getResourceId(file.getMimetype(), file.getFileName())\r
+                    );\r
                 }\r
-\r
-                fileIcon.setImageResource(DisplayUtils.getResourceId(file.getMimetype(), file.getFileName()));\r
-\r
+
                 if (checkIfFileIsSharedWithMe(file)) {\r
                     sharedWithMeIconV.setVisibility(View.VISIBLE);\r
                 }\r
             } \r
             else {\r
-                \r
                 fileSizeV.setVisibility(View.INVISIBLE);\r
                 //fileSizeV.setText(DisplayUtils.bytesToHumanReadable(file.getFileLength()));\r
                 lastModV.setVisibility(View.VISIBLE);\r
-                lastModV.setText(DisplayUtils.unixTimeToHumanReadable(file.getModificationTimestamp()));\r
+                lastModV.setText(\r
+                        DisplayUtils.unixTimeToHumanReadable(file.getModificationTimestamp())\r
+                );\r
                 checkBoxV.setVisibility(View.GONE);\r
                 view.findViewById(R.id.imageView3).setVisibility(View.GONE);\r
 \r
@@ -189,7 +374,9 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
                     fileIcon.setImageResource(R.drawable.shared_with_me_folder);\r
                     sharedWithMeIconV.setVisibility(View.VISIBLE);\r
                 } else {\r
-                    fileIcon.setImageResource(DisplayUtils.getResourceId(file.getMimetype(), file.getFileName()));\r
+                    fileIcon.setImageResource(\r
+                            DisplayUtils.getResourceId(file.getMimetype(), file.getFileName())\r
+                    );\r
                 }\r
 \r
                 // If folder is sharedByLink, icon folder must be changed to\r
@@ -208,6 +395,35 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
 \r
         return view;\r
     }\r
+    \r
+    public static boolean cancelPotentialWork(OCFile file, ImageView imageView) {\r
+        final ThumbnailGenerationTask bitmapWorkerTask = getBitmapWorkerTask(imageView);\r
+\r
+        if (bitmapWorkerTask != null) {\r
+            final OCFile bitmapData = bitmapWorkerTask.file;\r
+            // If bitmapData is not yet set or it differs from the new data\r
+            if (bitmapData == null || bitmapData != file) {\r
+                // Cancel previous task\r
+                bitmapWorkerTask.cancel(true);\r
+            } else {\r
+                // The same work is already in progress\r
+                return false;\r
+            }\r
+        }\r
+        // No task associated with the ImageView, or an existing task was cancelled\r
+        return true;\r
+    }\r
+    \r
+    private static ThumbnailGenerationTask getBitmapWorkerTask(ImageView imageView) {\r
+        if (imageView != null) {\r
+            final Drawable drawable = imageView.getDrawable();\r
+            if (drawable instanceof AsyncDrawable) {\r
+                final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;\r
+                return asyncDrawable.getBitmapWorkerTask();\r
+            }\r
+         }\r
+         return null;\r
+     }\r
 \r
     @Override\r
     public int getViewTypeCount() {\r
@@ -226,8 +442,10 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
 \r
     /**\r
      * Change the adapted directory for a new one\r
-     * @param directory                 New file to adapt. Can be NULL, meaning "no content to adapt".\r
-     * @param updatedStorageManager     Optional updated storage manager; used to replace mStorageManager if is different (and not NULL)\r
+     * @param directory                 New file to adapt. Can be NULL, meaning \r
+     *                                  "no content to adapt".\r
+     * @param updatedStorageManager     Optional updated storage manager; used to replace \r
+     *                                  mStorageManager if is different (and not NULL)\r
      */\r
     public void swapDirectory(OCFile directory, FileDataStorageManager updatedStorageManager) {\r
         mFile = directory;\r
@@ -273,7 +491,9 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
      * @return boolean: True if it is shared with me and false if it is not\r
      */\r
     private boolean checkIfFileIsSharedWithMe(OCFile file) {\r
-        return (mFile.getPermissions() != null && !mFile.getPermissions().contains(PERMISSION_SHARED_WITH_ME)\r
-                && file.getPermissions() != null && file.getPermissions().contains(PERMISSION_SHARED_WITH_ME));\r
+        return (mFile.getPermissions() != null \r
+                && !mFile.getPermissions().contains(PERMISSION_SHARED_WITH_ME)\r
+                && file.getPermissions() != null \r
+                && file.getPermissions().contains(PERMISSION_SHARED_WITH_ME));\r
     }\r
 }\r
diff --git a/src/com/owncloud/android/utils/BitmapUtils.java b/src/com/owncloud/android/utils/BitmapUtils.java
new file mode 100644 (file)
index 0000000..687b5a4
--- /dev/null
@@ -0,0 +1,99 @@
+/* ownCloud Android client application
+ *   Copyright (C) 2012-2014 ownCloud Inc.
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License version 2,
+ *   as published by the Free Software Foundation.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+package com.owncloud.android.utils;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
+
+/**
+ * Utility class with methods for decoding Bitmaps.
+ * 
+ * @author David A. Velasco
+ */
+public class BitmapUtils {
+    
+    
+    /**
+     * Decodes a bitmap from a file containing it minimizing the memory use, known that the bitmap
+     * will be drawn in a surface of reqWidth x reqHeight
+     * 
+     * @param srcPath       Absolute path to the file containing the image.
+     * @param reqWidth      Width of the surface where the Bitmap will be drawn on, in pixels.
+     * @param reqHeight     Height of the surface where the Bitmap will be drawn on, in pixels.
+     * @return
+     */
+    public static Bitmap decodeSampledBitmapFromFile(String srcPath, int reqWidth, int reqHeight) {
+    
+        // set desired options that will affect the size of the bitmap
+        final 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 to get its dimensions
+        options.inJustDecodeBounds = true;
+        
+        BitmapFactory.decodeFile(srcPath, options);   
+        
+        // calculate factor to subsample the bitmap
+        options.inSampleSize = calculateSampleFactor(options, reqWidth, reqHeight);
+
+        // decode bitmap with inSampleSize set
+        options.inJustDecodeBounds = false;
+        return BitmapFactory.decodeFile(srcPath, options);
+        
+    }    
+
+
+    /**
+     * Calculates a proper value for options.inSampleSize in order to decode a Bitmap minimizing 
+     * the memory overload and covering a target surface of reqWidth x reqHeight if the original
+     * image is big enough. 
+     * 
+     * @param options       Bitmap decoding options; options.outHeight and options.inHeight should
+     *                      be set. 
+     * @param reqWidth      Width of the surface where the Bitmap will be drawn on, in pixels.
+     * @param reqHeight     Height of the surface where the Bitmap will be drawn on, in pixels.
+     * @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) {
+        
+        final int height = options.outHeight;
+        final int width = options.outWidth;
+        int inSampleSize = 1;
+    
+        if (height > reqHeight || width > reqWidth) {
+            final int halfHeight = height / 2;
+            final int halfWidth = width / 2;
+    
+            while ((halfHeight / inSampleSize) > reqHeight
+                    && (halfWidth / inSampleSize) > reqWidth) {
+                inSampleSize *= 2;
+            }
+        }
+        
+        return inSampleSize;
+    }
+    
+}