com.fivehundredpxdemo.android.storage.ImageFetcher.java Source code

Java tutorial

Introduction

Here is the source code for com.fivehundredpxdemo.android.storage.ImageFetcher.java

Source

/*
 * Copyright 2012 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.fivehundredpxdemo.android.storage;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.widget.ImageView;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;

/**
 * A subclass of {@link ImageWorker} that fetches images from a URL.
 */
public class ImageFetcher extends ImageWorker {
    private static final String TAG = "ImageFetcher";

    public static final int IO_BUFFER_SIZE_BYTES = 4 * 1024; // 4KB

    // Default fetcher params
    public static final int MAX_THUMBNAIL_BYTES = 70 * 1024; // 70KB
    private static final int HTTP_CACHE_SIZE = 5 * 1024 * 1024; // 5MB
    private static final String HTTP_CACHE_DIR = "http";
    private static final int DEFAULT_IMAGE_HEIGHT = 1024;
    private static final int DEFAULT_IMAGE_WIDTH = 1024;

    protected int mImageWidth;
    protected int mImageHeight;
    private DiskLruCache mHttpDiskCache;
    private File mHttpCacheDir;
    private boolean mHttpDiskCacheStarting = true;
    private final Object mHttpDiskCacheLock = new Object();
    private static final int DISK_CACHE_INDEX = 0;
    private String accessToken;
    private Context context;

    public static ImageFetcher getImageFetcher(final FragmentActivity activity, String accessToken) {
        ImageFetcher fetcher = new ImageFetcher(activity, accessToken);
        fetcher.addImageCache(activity);
        return fetcher;
    }

    /**
     * Create an ImageFetcher specifying max image loading width/height.
     */
    public ImageFetcher(Context context, int imageWidth, int imageHeight, String accessToken) {
        super(context);
        init(context, imageWidth, imageHeight, accessToken);
        this.accessToken = accessToken;
    }

    public ImageFetcher(Context context, int imageWidth, int imageHeight) {
        super(context);
        init(context, imageWidth, imageHeight, null);
    }

    /**
     * Create an ImageFetcher using defaults.
     */
    public ImageFetcher(Context context, String accessToken) {
        super(context);
        init(context, DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT, accessToken);
    }

    private void init(Context context, int imageWidth, int imageHeight, String accessToken) {
        mImageWidth = imageWidth;
        mImageHeight = imageHeight;
        mHttpCacheDir = ImageCache.getDiskCacheDir(context, HTTP_CACHE_DIR);
        if (!mHttpCacheDir.exists()) {
            mHttpCacheDir.mkdirs();
        }
        this.context = context.getApplicationContext();
        this.accessToken = accessToken;
    }

    public String getAccessToken() {
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    public void loadThumbnailImage(String key, ImageView imageView, Bitmap loadingBitmap) {
        loadImage(new ImageData(key, ImageData.IMAGE_TYPE_THUMBNAIL), imageView, loadingBitmap);
    }

    public void loadThumbnailImage(String key, ImageView imageView, int resId) {
        loadImage(new ImageData(key, ImageData.IMAGE_TYPE_THUMBNAIL), imageView, resId);
    }

    public void loadThumbnailImage(String key, ImageView imageView) {
        loadImage(new ImageData(key, ImageData.IMAGE_TYPE_THUMBNAIL), imageView, mLoadingBitmap);
    }

    public void loadImage(String key, ImageView imageView, Bitmap loadingBitmap) {
        loadImage(new ImageData(key, ImageData.IMAGE_TYPE_NORMAL), imageView, loadingBitmap);
    }

    public void loadImage(String key, ImageView imageView, int resId) {
        loadImage(new ImageData(key, ImageData.IMAGE_TYPE_NORMAL), imageView, resId);
    }

    public void loadImage(String key, ImageView imageView) {
        loadImage(new ImageData(key, ImageData.IMAGE_TYPE_NORMAL), imageView, mLoadingBitmap);
    }

    public void loadImageFromUri(Uri uri, ImageView imageView) {
        loadImage(new ImageData(uri, ImageData.IMAGE_TYPE_URI), imageView, mLoadingBitmap);
    }

    public void loadImageFromFilePath(String path, ImageView imageView) {
        loadImage(new ImageData(path, ImageData.IMAGE_TYPE_FILE), imageView, mLoadingBitmap);
    }

    /**
     * Set the target image width and height.
     */
    public void setImageSize(int width, int height) {
        mImageWidth = width;
        mImageHeight = height;
    }

    /**
     * Set the target image size (width and height will be the same).
     */
    public void setImageSize(int size) {
        setImageSize(size, size);
    }

    /**
     * The main process method, which will be called by the ImageWorker in the AsyncTask background
     * thread.
     */
    private Bitmap processBitmap(ImageData imageData) {
        // for thumbnails and images, we need to encode URLs because they can
        // contain UTF-8 characters (e.g. Japanese/Chinese characters) in the permalink
        // (the 2nd parameter is a list of characters not to encode)
        final String formattedURL = Uri.encode(imageData.toString(), ":/-_%|+?#=&$,.;@");

        if (imageData.mType == ImageData.IMAGE_TYPE_NORMAL) {
            return processNormalBitmap(formattedURL); // Process a regular, full sized bitmap
        } else if (imageData.mType == ImageData.IMAGE_TYPE_THUMBNAIL) {
            return processThumbnailBitmap(formattedURL); // Process a smaller, thumbnail bitmap
        } else if (imageData.mType == ImageData.IMAGE_TYPE_URI) {
            return processUriBitmap((Uri) imageData.mKey);
        } else if (imageData.mType == ImageData.IMAGE_TYPE_FILE) {
            return processFileBitmap(imageData.toString());
        }
        return null;
    }

    @Override
    protected Bitmap processBitmap(Object key) {
        final ImageData imageData = (ImageData) key;
        return processBitmap(imageData);
    }

    private Bitmap processUriBitmap(Uri uri) {
        try {
            return decodeSampledBitmapFromUri(context, uri, mImageWidth, mImageHeight);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private Bitmap processFileBitmap(String path) {
        return decodeSampledBitmapFromFile(path, mImageWidth, mImageHeight);
    }

    /**
     * Download and resize a normal sized remote bitmap from a HTTP URL using a HTTP cache.
     * @param urlString The URL of the image to download
     * @return The scaled bitmap
     */
    private Bitmap processNormalBitmap(String urlString) {
        final String key = ImageCache.hashKeyForDisk(urlString);
        FileDescriptor fileDescriptor = null;
        FileInputStream fileInputStream = null;
        DiskLruCache.Snapshot snapshot;
        synchronized (mHttpDiskCacheLock) {
            // Wait for disk cache to initialize
            while (mHttpDiskCacheStarting) {
                try {
                    mHttpDiskCacheLock.wait();
                } catch (InterruptedException e) {
                }
            }

            if (mHttpDiskCache != null) {
                try {
                    snapshot = mHttpDiskCache.get(key);
                    if (snapshot == null) {
                        Log.d(TAG, "processBitmap, not found in http cache, downloading...");
                        DiskLruCache.Editor editor = mHttpDiskCache.edit(key);
                        if (editor != null) {
                            if (downloadUrlToStream(urlString, editor.newOutputStream(DISK_CACHE_INDEX))) {
                                editor.commit();
                            } else {
                                editor.abort();
                            }
                        }
                        snapshot = mHttpDiskCache.get(key);
                    }
                    if (snapshot != null) {
                        fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
                        fileDescriptor = fileInputStream.getFD();
                    }
                } catch (IOException e) {
                    Log.e(TAG, "processBitmap - " + e);
                } catch (IllegalStateException e) {
                    Log.e(TAG, "processBitmap - " + e);
                } finally {
                    if (fileDescriptor == null && fileInputStream != null) {
                        try {
                            fileInputStream.close();
                        } catch (IOException e) {
                        }
                    }
                }
            }
        }

        Bitmap bitmap = null;
        if (fileDescriptor != null) {
            bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth, mImageHeight);
        }
        if (fileInputStream != null) {
            try {
                fileInputStream.close();
            } catch (IOException e) {
            }
        }
        return bitmap;
    }

    /**
     * Download a thumbnail sized remote bitmap from a HTTP URL. No HTTP caching is done (the
     * {@link ImageCache} that this eventually gets passed to will do it's own disk caching.
     * @param urlString The URL of the image to download
     * @return The bitmap
     */
    private Bitmap processThumbnailBitmap(String urlString) {
        final byte[] bitmapBytes = downloadBitmapToMemory(urlString, MAX_THUMBNAIL_BYTES, accessToken);
        if (bitmapBytes != null) {
            // Caution: we don't check the size of the bitmap here, we are relying on the output
            // of downloadBitmapToMemory to not exceed our memory limits and load a huge bitmap
            // into memory.
            return BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length);
        }
        return null;
    }

    public static Bitmap downloadBitmapToMemory(String urlString, String accessToken) {
        final byte[] bitmapBytes = downloadBitmapToMemory(urlString, MAX_THUMBNAIL_BYTES, accessToken);
        if (bitmapBytes != null) {
            // Caution: we don't check the size of the bitmap here, we are relying on the output
            // of downloadBitmapToMemory to not exceed our memory limits and load a huge bitmap
            // into memory.
            return BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length);
        }
        return null;
    }

    public synchronized static Bitmap decodeSampledBitmapFromUri(Context context, Uri uri, int reqWidth,
            int reqHeight) throws IOException {

        InputStream input = context.getContentResolver().openInputStream(uri);

        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(input, null, options);

        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        input = context.getContentResolver().openInputStream(uri);
        Bitmap bitmap = BitmapFactory.decodeStream(input, null, options); // Blocks forever reading from picasa sometimes?
        input.close();

        return bitmap;
    }

    /**
     * Download a bitmap from a URL, write it to a disk and return the File pointer. This
     * implementation uses a simple disk cache.
     *
     * @param urlString The URL to fetch
     * @param maxBytes The maximum number of bytes to read before returning null to protect against
     *                 OutOfMemory exceptions.
     * @return A File pointing to the fetched bitmap
     */
    public static byte[] downloadBitmapToMemory(String urlString, int maxBytes, String accessToken) {
        Log.d(TAG, "downloadBitmapToMemory - downloading - " + urlString);

        disableConnectionReuseIfNecessary();
        HttpURLConnection urlConnection = null;
        ByteArrayOutputStream out = null;
        InputStream in = null;

        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.addRequestProperty("Authorization", "Bearer " + accessToken);
            if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                return null;
            }
            in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE_BYTES);
            out = new ByteArrayOutputStream(IO_BUFFER_SIZE_BYTES);

            final byte[] buffer = new byte[128];
            int total = 0;
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                total += bytesRead;
                if (total > maxBytes) {
                    return null;
                }
                out.write(buffer, 0, bytesRead);
            }
            return out.toByteArray();
        } catch (final IOException e) {
            Log.e(TAG, "Error in downloadBitmapToMemory - " + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            try {
                if (in != null) {
                    in.close();
                }
                if (out != null) {
                    out.close();
                }
            } catch (final IOException e) {
            }
        }
        return null;
    }

    /**
     * Download a bitmap from a URL and write the content to an output stream.
     *
     * @param urlString The URL to fetch
     * @param outputStream The outputStream to write to
     * @return true if successful, false otherwise
     */
    public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
        disableConnectionReuseIfNecessary();
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;

        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.addRequestProperty("Authorization", "Bearer " + accessToken);
            in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE_BYTES);
            out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE_BYTES);

            int b;
            while ((b = in.read()) != -1) {
                out.write(b);
            }
            return true;
        } catch (final IOException e) {
            Log.e(TAG, "Error in downloadBitmap - " + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (final IOException e) {
            }
        }
        return false;
    }

    /**
     * Download a bitmap from a URL, write it to a disk and return the File pointer. This
     * implementation uses a simple disk cache.
     *
     * @param urlString The URL to fetch
     * @param cacheDir The directory to store the downloaded file
     * @return A File pointing to the fetched bitmap
     */
    public static File downloadBitmapToFile(String urlString, File cacheDir, String accessToken) {
        Log.d(TAG, "downloadBitmap - downloading - " + urlString);

        disableConnectionReuseIfNecessary();
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;

        try {
            final File tempFile = File.createTempFile("bitmap", null, cacheDir);

            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.addRequestProperty("Authorization", "Bearer " + accessToken);
            if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                return null;
            }
            in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE_BYTES);
            out = new BufferedOutputStream(new FileOutputStream(tempFile), IO_BUFFER_SIZE_BYTES);

            int b;
            while ((b = in.read()) != -1) {
                out.write(b);
            }
            return tempFile;
        } catch (final IOException e) {
            Log.e(TAG, "Error in downloadBitmap - " + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            try {
                if (in != null) {
                    in.close();
                }
                if (out != null) {
                    out.close();
                }
            } catch (final IOException e) {
            }
        }
        return null;
    }

    /**
     * Decode and sample down a bitmap from a file to the requested width and
     * height.
     *
     * @param filename The full path of the file to decode
     * @param reqWidth The requested width of the resulting bitmap
     * @param reqHeight The requested height of the resulting bitmap
     * @return A bitmap sampled down from the original with the same aspect
     *         ratio and dimensions that are equal to or greater than the
     *         requested width and height
     */
    public static Bitmap decodeSampledBitmapFromFile(String filename, int reqWidth, int reqHeight) {

        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(filename, options);

        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFile(filename, options);
    }

    /**
     * Decode and sample down a bitmap from a file input stream to the requested width and height.
     *
     * @param fileDescriptor The file descriptor to read from
     * @param reqWidth The requested width of the resulting bitmap
     * @param reqHeight The requested height of the resulting bitmap
     * @return A bitmap sampled down from the original with the same aspect ratio and dimensions
     *         that are equal to or greater than the requested width and height
     */
    public static Bitmap decodeSampledBitmapFromDescriptor(FileDescriptor fileDescriptor, int reqWidth,
            int reqHeight) {

        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);

        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
    }

    /**
     * Calculate an inSampleSize for use in a
     * {@link android.graphics.BitmapFactory.Options} object when decoding
     * bitmaps using the decode* methods from {@link android.graphics.BitmapFactory}. This
     * implementation calculates the closest inSampleSize that will result in
     * the final decoded bitmap having a width and height equal to or larger
     * than the requested width and height. This implementation does not ensure
     * a power of 2 is returned for inSampleSize which can be faster when
     * decoding but results in a larger bitmap which isn't as useful for caching
     * purposes.
     *
     * @param options An options object with out* params already populated (run
     *            through a decode* method with inJustDecodeBounds==true
     * @param reqWidth The requested width of the resulting bitmap
     * @param reqHeight The requested height of the resulting bitmap
     * @return The value to be used for inSampleSize
     */
    public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {
            if (width > height) {
                inSampleSize = Math.round((float) height / (float) reqHeight);
            } else {
                inSampleSize = Math.round((float) width / (float) reqWidth);
            }

            // This offers some additional logic in case the image has a strange
            // aspect ratio. For example, a panorama may have a much larger
            // width than height. In these cases the total pixels might still
            // end up being too large to fit comfortably in memory, so we should
            // be more aggressive with sample down the image (=larger
            // inSampleSize).

            final float totalPixels = width * height;

            // Anything more than 2x the requested pixels we'll sample down
            // further.
            final float totalReqPixelsCap = reqWidth * reqHeight * 2;

            while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
                inSampleSize++;
            }
        }
        return inSampleSize;
    }

    /**
     * Workaround for bug pre-Froyo, see here for more info:
     * http://android-developers.blogspot.com/2011/09/androids-http-clients.html
     */
    public static void disableConnectionReuseIfNecessary() {
        // HTTP connection reuse which was buggy pre-froyo
        if (hasHttpConnectionBug()) {
            System.setProperty("http.keepAlive", "false");
        }
    }

    /**
     * Check if OS version has a http URLConnection bug. See here for more
     * information:
     * http://android-developers.blogspot.com/2011/09/androids-http-clients.html
     *
     * @return true if this OS version is affected, false otherwise
     */
    public static boolean hasHttpConnectionBug() {
        return !(Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO);
    }

    @Override
    protected void initDiskCacheInternal() {
        super.initDiskCacheInternal();
        initHttpDiskCache();
    }

    private void initHttpDiskCache() {
        if (!mHttpCacheDir.exists()) {
            mHttpCacheDir.mkdirs();
        }
        synchronized (mHttpDiskCacheLock) {
            if (ImageCache.getUsableSpace(mHttpCacheDir) > HTTP_CACHE_SIZE) {
                try {
                    mHttpDiskCache = DiskLruCache.open(mHttpCacheDir, 1, 1, HTTP_CACHE_SIZE);
                    //                    Log.d(TAG, "HTTP cache initialized");
                } catch (IOException e) {
                    mHttpDiskCache = null;
                }
            }
            mHttpDiskCacheStarting = false;
            mHttpDiskCacheLock.notifyAll();
        }
    }

    @Override
    protected void clearCacheInternal() {
        super.clearCacheInternal();
        synchronized (mHttpDiskCacheLock) {
            if (mHttpDiskCache != null && !mHttpDiskCache.isClosed()) {
                try {
                    mHttpDiskCache.delete();
                    //                    Log.d(TAG, "HTTP cache cleared");
                } catch (IOException e) {
                    Log.e(TAG, "clearCacheInternal - " + e);
                }
                mHttpDiskCache = null;
                mHttpDiskCacheStarting = true;
                initHttpDiskCache();
            }
        }
    }

    @Override
    protected void flushCacheInternal() {
        super.flushCacheInternal();
        synchronized (mHttpDiskCacheLock) {
            if (mHttpDiskCache != null) {
                try {
                    mHttpDiskCache.flush();
                    //                    Log.d(TAG, "HTTP cache flushed");
                } catch (IOException e) {
                    Log.e(TAG, "flush - " + e);
                }
            }
        }
    }

    @Override
    protected void closeCacheInternal() {
        super.closeCacheInternal();
        synchronized (mHttpDiskCacheLock) {
            if (mHttpDiskCache != null) {
                try {
                    if (!mHttpDiskCache.isClosed()) {
                        mHttpDiskCache.close();
                        mHttpDiskCache = null;
                        Log.d(TAG, "HTTP cache closed");
                    }
                } catch (IOException e) {
                    Log.e(TAG, "closeCacheInternal - " + e);
                }
            }
        }
    }

    private static class ImageData {
        public static final int IMAGE_TYPE_THUMBNAIL = 0;
        public static final int IMAGE_TYPE_NORMAL = 1;
        public static final int IMAGE_TYPE_URI = 2;
        public static final int IMAGE_TYPE_FILE = 3;
        public Object mKey;
        public int mType;

        public ImageData(Object key, int type) {
            mKey = key;
            mType = type;
        }

        @Override
        public String toString() {
            return String.valueOf(mKey);
        }
    }
}