fr.eoidb.util.ImageDownloader.java Source code

Java tutorial

Introduction

Here is the source code for fr.eoidb.util.ImageDownloader.java

Source

/**
 * Copyright (C) 2012 Picon software
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * 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, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */
package fr.eoidb.util;

import java.io.*;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map.Entry;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.http.AndroidHttpClient;
import android.os.AsyncTask;
import android.support.v4.util.LruCache;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpGet;

import fr.eoidb.BuildConfig;
import fr.eoidb.R;

/**
 * This helper class download images from the Internet and binds those with the provided ImageView.
 *
 * <p>It requires the INTERNET permission, which should be added to your application's manifest
 * file.</p>
 *
 * A local cache of downloaded images is maintained internally to improve performance.
 */
public class ImageDownloader {
    private static final String LOG_TAG = "ImageDownloader";

    private static final String cacheFileName = "eoidb.icn.cache";

    // Hard cache, with a fixed maximum capacity and a life duration
    int cacheSize = 4 * 1024 * 1024; // 4MiB
    LruCache<String, Bitmap> sHardBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getRowBytes() * value.getHeight();
        }
    };

    @SuppressWarnings("unchecked")
    public void loadImageCache(Context context) {
        if (BuildConfig.DEBUG)
            Log.v(LOG_TAG, "Loading image cache...");
        File cacheFile = new File(context.getCacheDir(), cacheFileName);

        if (cacheFile.exists() && cacheFile.length() > 0) {
            ObjectInputStream ois = null;
            try {
                HashMap<String, String> cacheDescription = new HashMap<String, String>();
                ois = new ObjectInputStream(new FileInputStream(cacheFile));
                cacheDescription = (HashMap<String, String>) ois.readObject();

                for (Entry<String, String> cacheDescEntry : cacheDescription.entrySet()) {
                    loadSingleCacheFile(cacheDescEntry.getKey(), context);
                }
            } catch (StreamCorruptedException e) {
                Log.e(LOG_TAG, e.getMessage(), e);
            } catch (FileNotFoundException e) {
                Log.e(LOG_TAG, e.getMessage(), e);
            } catch (EOFException e) {
                //delete the corrupted cache file
                Log.w(LOG_TAG, "Deleting the corrupted cache file.", e);
                cacheFile.delete();
            } catch (IOException e) {
                Log.e(LOG_TAG, e.getMessage(), e);
            } catch (ClassNotFoundException e) {
                Log.e(LOG_TAG, e.getMessage(), e);
            } finally {
                if (ois != null) {
                    try {
                        ois.close();
                    } catch (IOException e) {
                        Log.e(LOG_TAG, e.getMessage(), e);
                    }
                }
            }
        }
    }

    private Bitmap loadSingleCacheFile(String url, Context context) {
        Bitmap bitmap = null;
        File cacheIconFile = new File(context.getCacheDir(), getCacheFileName(url));
        if (cacheIconFile.exists()) {
            if (BuildConfig.DEBUG)
                Log.v(LOG_TAG, "Loading cache file : " + url);

            bitmap = BitmapFactory.decodeFile(cacheIconFile.getAbsolutePath());

            if (bitmap != null) {
                sHardBitmapCache.put(url, bitmap);
            }
        }

        return bitmap;
    }

    private void flushSingleCacheFileToDisk(Bitmap bitmap, String cacheIconName, Context context)
            throws IOException {
        File cacheIconFile = new File(context.getCacheDir(), cacheIconName);
        if (!cacheIconFile.exists()) {
            if (BuildConfig.DEBUG)
                Log.v(LOG_TAG, "Flushing bitmap " + cacheIconName + " to disk...");
            cacheIconFile.createNewFile();

            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(cacheIconFile));
            bitmap.compress(CompressFormat.PNG, 100, bos);
            bos.flush();
            bos.close();
        }
    }

    private String getCacheFileName(String key) {
        return key.replaceAll("/", "").replaceAll(":", "").replaceAll("\\.", "") + ".cache";
    }

    /**
     * Download the specified image from the Internet and binds it to the provided ImageView. The
     * binding is immediate if the image is found in the cache and will be done asynchronously
     * otherwise. A null bitmap will be associated to the ImageView if an error occurs.
     *
     * @param url The URL of the image to download.
     * @param imageView The ImageView to bind the downloaded image to.
     */
    public void download(String url, ImageView imageView) {
        download(url, imageView, null);
    }

    /**
     * Same as {@link #download(String, ImageView)}, with the possibility to provide an additional
     * cookie that will be used when the image will be retrieved.
     *
     * @param url The URL of the image to download.
     * @param imageView The ImageView to bind the downloaded image to.
     * @param cookie A cookie String that will be used by the http connection.
     */
    public void download(String url, ImageView imageView, String cookie) {
        Bitmap bitmap = getBitmapFromCache(url, imageView.getContext());

        if (bitmap == null) {
            forceDownload(url, imageView, cookie);
        } else {
            cancelPotentialDownload(url, imageView);
            imageView.setImageBitmap(bitmap);
            IconUtil.hideIconProgress(imageView);
        }
    }

    /**
     * Same as download but the image is always downloaded and the cache is not used.
     * Kept private at the moment as its interest is not clear.
     */
    private void forceDownload(String url, ImageView imageView, String cookie) {
        // State sanity: url is guaranteed to never be null in DownloadedDrawable and cache keys.
        if (url == null) {
            imageView.setImageDrawable(null);
            return;
        }

        if (cancelPotentialDownload(url, imageView)) {
            BitmapDownloaderTask task = new BitmapDownloaderTask(imageView);
            DownloadedDrawable downloadedDrawable = new DownloadedDrawable(task,
                    imageView.getContext().getResources());
            imageView.setImageDrawable(downloadedDrawable);
            task.execute(url, cookie);
        }
    }

    /**
     * Clears the image cache used internally to improve performance. Note that for memory
     * efficiency reasons, the cache will automatically be cleared after a certain inactivity delay.
     */
    public void clearCache() {
        sHardBitmapCache.evictAll();
    }

    /**
     * Returns true if the current download has been canceled or if there was no download in
     * progress on this image view.
     * Returns false if the download in progress deals with the same url. The download is not
     * stopped in that case.
     */
    private static boolean cancelPotentialDownload(String url, ImageView imageView) {
        BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);

        if (bitmapDownloaderTask != null) {
            String bitmapUrl = bitmapDownloaderTask.url;
            if ((bitmapUrl == null) || (!bitmapUrl.equals(url))) {
                bitmapDownloaderTask.cancel(true);
            } else {
                // The same URL is already being downloaded.
                return false;
            }
        }
        return true;
    }

    /**
     * @param imageView Any imageView
     * @return Retrieve the currently active download task (if any) associated with this imageView.
     * null if there is no such task.
     */
    private static BitmapDownloaderTask getBitmapDownloaderTask(ImageView imageView) {
        if (imageView != null) {
            Drawable drawable = imageView.getDrawable();
            if (drawable instanceof DownloadedDrawable) {
                DownloadedDrawable downloadedDrawable = (DownloadedDrawable) drawable;
                return downloadedDrawable.getBitmapDownloaderTask();
            }
        }
        return null;
    }

    /**
     * @param url The URL of the image that will be retrieved from the cache.
     * @return The cached bitmap or null if it was not found.
     */
    Bitmap getBitmapFromCache(String url, Context context) {
        return sHardBitmapCache.get(url);
    }

    /**
     * The actual AsyncTask that will asynchronously download the image.
     */
    class BitmapDownloaderTask extends AsyncTask<String, Void, Bitmap> {
        private static final int IO_BUFFER_SIZE = 4 * 1024;
        private String url;
        private final WeakReference<ImageView> imageViewReference;

        public BitmapDownloaderTask(ImageView imageView) {
            imageViewReference = new WeakReference<ImageView>(imageView);
        }

        /**
         * Actual download method.
         */
        @Override
        protected Bitmap doInBackground(String... params) {
            url = params[0];

            Context context = null;
            if (imageViewReference.get() != null) {
                context = imageViewReference.get().getContext();
                Bitmap cacheBitmap = loadSingleCacheFile(url, context);
                if (cacheBitmap != null) {
                    return cacheBitmap;
                }
            }

            final AndroidHttpClient client = AndroidHttpClient.newInstance("Android");
            final HttpGet getRequest = new HttpGet(url);
            String cookie = params[1];
            if (cookie != null) {
                getRequest.setHeader("cookie", cookie);
            }

            try {
                HttpResponse response = client.execute(getRequest);
                final int statusCode = response.getStatusLine().getStatusCode();
                if (statusCode != HttpStatus.SC_OK) {
                    Log.w("ImageDownloader", "Error " + statusCode + " while retrieving bitmap from " + url);
                    return null;
                }

                final HttpEntity entity = response.getEntity();
                if (entity != null) {
                    InputStream inputStream = null;
                    try {
                        inputStream = entity.getContent();

                        final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);

                        if (context != null) {
                            flushSingleCacheFileToDisk(bitmap, getCacheFileName(url), context);
                        }

                        return bitmap;

                    } finally {
                        if (inputStream != null) {
                            inputStream.close();
                        }
                        //if (outputStream != null) {
                        //   outputStream.close();
                        //}
                        entity.consumeContent();
                    }
                }
            } catch (IOException e) {
                getRequest.abort();
                Log.w(LOG_TAG, "I/O error while retrieving bitmap from " + url, e);
            } catch (IllegalStateException e) {
                getRequest.abort();
                Log.w(LOG_TAG, "Incorrect URL: " + url);
            } catch (Exception e) {
                getRequest.abort();
                Log.w(LOG_TAG, "Error while retrieving bitmap from " + url, e);
            } finally {
                if (client != null) {
                    client.close();
                }
            }
            return null;
        }

        /**
         * Once the image is downloaded, associates it to the imageView
         */
        @Override
        protected void onPostExecute(Bitmap bitmap) {
            if (isCancelled()) {
                bitmap = null;
            }

            // Add bitmap to cache
            if (bitmap != null) {
                sHardBitmapCache.put(url, bitmap);
            }

            if (imageViewReference != null) {
                ImageView imageView = imageViewReference.get();
                BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
                // Change bitmap only if this process is still associated with it
                if (this == bitmapDownloaderTask) {
                    imageView.setImageBitmap(bitmap);
                    IconUtil.hideIconProgress(imageView);
                }
            }
        }

        public void copy(InputStream in, OutputStream out) throws IOException {
            byte[] b = new byte[IO_BUFFER_SIZE];
            int read;
            while ((read = in.read(b)) != -1) {
                out.write(b, 0, read);
            }
        }
    }

    /**
     * A fake Drawable that will be attached to the imageView while the download is in progress.
     *
     * <p>Contains a reference to the actual download task, so that a download task can be stopped
     * if a new binding is required, and makes sure that only the last started download process can
     * bind its result, independently of the download finish order.</p>
     */
    static class DownloadedDrawable extends BitmapDrawable {
        private final WeakReference<BitmapDownloaderTask> bitmapDownloaderTaskReference;

        public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask, Resources resources) {
            super(resources, BitmapFactory.decodeResource(resources, R.drawable.unknown));

            bitmapDownloaderTaskReference = new WeakReference<BitmapDownloaderTask>(bitmapDownloaderTask);
        }

        public BitmapDownloaderTask getBitmapDownloaderTask() {
            return bitmapDownloaderTaskReference.get();
        }
    }
}