com.aretha.content.image.AsyncImageLoader.java Source code

Java tutorial

Introduction

Here is the source code for com.aretha.content.image.AsyncImageLoader.java

Source

/* Copyright (c) 2011-2012 Tang Ke
 *
 * 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.aretha.content.image;

import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URI;
import java.util.LinkedList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.util.DisplayMetrics;
import android.util.Log;

import com.aretha.content.CacheManager;
import com.aretha.content.FileCacheManager;
import com.aretha.content.FileCacheManager.OnWriteListener;
import com.aretha.net.HttpConnectionHelper;

/**
 * Load remote image to local cache, then notify the UI thread, then read from
 * cache and decode bitmap avoid {@link OutOfMemoryError}
 * 
 * @author Tank
 */
public class AsyncImageLoader {
    private final static int STATUS_SUCCESS = 1 << 0;
    private final static int STATUS_ERROR = 1 << 1;
    private final static int STATUS_CANCEL = 1 << 2;

    private final static String LOG_TAG = "AsyncImageLoader";
    private Context mContext;

    private static AsyncImageLoader mImageLoader;

    private FileCacheManager mFileCacheManager;

    private ExecutorService mExecutor;
    private volatile LinkedList<ImageLoadingTask> mTaskList;

    private Handler mImageLoadedHandler;

    private ReentrantReadWriteLock mMainLock = new ReentrantReadWriteLock();

    private int mScreenWidth;
    private int mScreenHeight;

    public static AsyncImageLoader getInstance(Context context) {
        if (mImageLoader == null) {
            mImageLoader = new AsyncImageLoader(context);
        }
        return mImageLoader;
    }

    private AsyncImageLoader(Context context) {
        mContext = context.getApplicationContext();
        mFileCacheManager = new FileCacheManager(context);
        mExecutor = Executors.newCachedThreadPool();
        mTaskList = new LinkedList<ImageLoadingTask>();
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        mScreenWidth = displayMetrics.widthPixels;
        mScreenHeight = displayMetrics.heightPixels;
        // will notify the main thread
        mImageLoadedHandler = new ImageLoadHandler(context.getMainLooper());
    }

    /**
     * Add image load request
     * 
     * @param uri
     * @param listener
     */
    public void loadImage(Uri uri, OnImageLoadListener listener) {
        loadImage(uri, 0, 0, listener, true);
    }

    /**
     * Add image load request
     * 
     * @param uri
     * @param listener
     * @param readCache
     *            true read cache file if exist
     */
    public void loadImage(Uri uri, int targetWidth, int targetHeight, OnImageLoadListener listener,
            boolean readCacheIfExist) {
        doLoadImage(obtainImageLoadingTask(uri, targetWidth, targetHeight, listener, readCacheIfExist));
    }

    /**
     * @see #loadImage(URI, OnImageLoadListener)
     * @param url
     * @param listener
     */
    public void loadImage(String url, OnImageLoadListener listener) {
        if (null == url || url.length() <= 0) {
            return;
        }
        loadImage(Uri.parse(url), listener);
    }

    /**
     * Cancel a image load request before is was been execute. if you want to
     * cancel it in progress, please see {@link OnImageLoadListener}
     * 
     * @param uri
     */
    public void cancel(Uri uri) {
        Lock lock = mMainLock.writeLock();
        ImageLoadingTask task = obtainImageLoadingTask(uri, 0, 0, null, false);
        lock.lock();
        mTaskList.remove(task);
        lock.unlock();
    }

    /**
     * @see #cancel(URI)
     * @param url
     */
    public void cancel(String url) {
        if (null == url) {
            return;
        }
        cancel(Uri.parse(url));
    }

    /**
     * Generate the load task
     * 
     * @param uri
     * @param listener
     * @param readCacheIfExist
     * @return
     */
    private ImageLoadingTask obtainImageLoadingTask(Uri uri, int width, int height, OnImageLoadListener listener,
            boolean readCacheIfExist) {
        if (null == uri || null == listener) {
            return null;
        }
        ImageLoadingTask task = new ImageLoadingTask();
        task.listener = listener;
        task.uri = uri;
        task.targetWidth = width <= 0 ? mScreenWidth : width;
        task.targetHeight = height <= 0 ? mScreenHeight : height;
        task.readCacheIfExist = readCacheIfExist;
        return task;
    }

    /**
     * Execute the load action
     */
    private void doLoadImage(ImageLoadingTask imageLoadingTask) {
        if (null == imageLoadingTask) {
            return;
        }

        mTaskList.add(imageLoadingTask);
        mExecutor.execute(imageLoadingTask);
    }

    /**
     * Save the image {@link InputStream}
     * 
     * @param context
     * @param imageIdentifier
     * @param inputstream
     * @return
     */
    public boolean saveBitmapStream(String imageIdentifier, InputStream inputStream) {
        return saveBitmapStream(imageIdentifier, inputStream, null);
    }

    /**
     * Save the image {@link InputStream}
     * 
     * @param imageIdentifier
     * @param inputStream
     * @param onWriteListener
     * @return
     */
    public boolean saveBitmapStream(String imageIdentifier, InputStream inputStream,
            OnWriteListener onWriteListener) {
        return mFileCacheManager.writeCacheFile(imageIdentifier, inputStream, onWriteListener);
    }

    /**
     * According imageIdentifier to get cached {@link Bitmap} If the
     * {@link OutOfMemoryError} occur. method will reduce the quality of
     * {@link Bitmap}
     * 
     * @see CacheManager
     * 
     * @param imageIdentifier
     * @param sampleSize
     *            If set to a value > 1, requests the decoder to sub sample the
     *            original image, returning a smaller image to save memory.
     * @return The cached bitmap, or null not found.
     */
    public Bitmap readCachedBitmap(String imageIdentifier, int targetWidth, int targetHeight) {
        InputStream inputStream = mFileCacheManager.readCacheFile(imageIdentifier);

        if (inputStream == null) {
            return null;
        }

        Options decodeOptions = new Options();
        decodeOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(inputStream, null, decodeOptions);
        int factor = (int) Math.ceil(decodeOptions.outWidth * 1.0f / targetWidth);
        factor = (int) Math.max(factor, decodeOptions.outHeight * 1.0f / targetHeight);
        Log.d(LOG_TAG, "Current image factor: " + factor);
        decodeOptions.inJustDecodeBounds = false;
        decodeOptions.inSampleSize = factor;
        // the system can purge the space of Bitmap use automatically
        decodeOptions.inPurgeable = true;
        decodeOptions.inInputShareable = true;

        return BitmapFactory.decodeStream(mFileCacheManager.readCacheFile(imageIdentifier), null, decodeOptions);
    }

    /**
     * The listener to listen the image load action
     * 
     * @author Tank
     * 
     */
    public interface OnImageLoadListener {
        /**
         * Invoked when the image has been loaded
         * 
         * @param bitmap
         * @param imageUrl
         * @param fromCache
         */
        public void onLoadSuccess(Bitmap bitmap, String imageUrl, boolean fromCache);

        /**
         * 
         * @param imageUrl
         */
        public void onLoadError(String imageUrl);

        /**
         * You can decide whether to intercept the image load request in this
         * method. Such as Wi-Fi only or this image is with the extension name
         * 'png'
         * 
         * @param imageUrl
         * @return true to cancel this request, otherwise.
         */
        public boolean onPreLoad(String imageUrl);

        /**
         * Publish the progress of one image loading task. invoke from
         * <b>NOT</b> main thread
         * 
         * @param imageUrl
         * @param loadedLength
         * @param totalLength
         */
        public void onLoading(String imageUrl, long loadedLength, long totalLength);
    }

    private class ImageLoadingTask implements Runnable, OnWriteListener {
        public Uri uri;
        public OnImageLoadListener listener;
        public Bitmap bitmap;
        public boolean isLoadFromCache;
        public boolean readCacheIfExist;
        public long totleBytes;
        public int targetWidth;
        public int targetHeight;

        @Override
        public boolean equals(Object o) {
            if (o instanceof ImageLoadingTask) {
                return uri.equals(((ImageLoadingTask) o).uri);
            }

            return super.equals(o);
        }

        @Override
        public void run() {
            Message message = mImageLoadedHandler.obtainMessage(STATUS_SUCCESS, this);
            if (listener != null && listener.onPreLoad(uri.toString())) {
                message.what = STATUS_CANCEL;
                message.sendToTarget();
                return;
            }
            String cacheIdentifier = uri.toString();
            long cacheLength = mFileCacheManager.getCacheFileLength(cacheIdentifier);
            if (cacheLength > 0 && readCacheIfExist) {
                bitmap = readCachedBitmap(cacheIdentifier, targetWidth, targetHeight);
                if (null != bitmap) {
                    Log.d(LOG_TAG, "Image cache found!");
                    isLoadFromCache = true;
                    message.sendToTarget();
                    return;
                }
            }

            Log.d(LOG_TAG, "Image cache not found, get it in async method!");
            InputStream inputStream = null;
            try {
                if (uri.getScheme().startsWith("content:")) {
                    ParcelFileDescriptor fileDescriptor = mContext.getContentResolver().openFileDescriptor(uri,
                            "r");
                    totleBytes = fileDescriptor.getStatSize();
                    inputStream = new FileInputStream(fileDescriptor.getFileDescriptor());
                } else {
                    // Began to load from network
                    HttpConnectionHelper connection = HttpConnectionHelper.getInstance();
                    HttpResponse response = connection
                            .execute(connection.obtainHttpGetRequest(URI.create(uri.toString()), null));

                    HttpEntity entity = response.getEntity();
                    inputStream = entity.getContent();
                    totleBytes = entity.getContentLength();
                }

                saveBitmapStream(uri.toString(), inputStream, this);
                bitmap = readCachedBitmap(uri.toString(), targetWidth, targetHeight);
                if (null == bitmap) {
                    Log.d(LOG_TAG, String.format("Delete the broken image cache! url: %s", uri.toString()));
                    mFileCacheManager.deleteCache(uri.toString());
                    message.what = STATUS_ERROR;
                    message.sendToTarget();
                    return;
                }
                message.sendToTarget();
                return;
            } catch (Exception e) {
                Log.d(LOG_TAG, e.getMessage());
            } finally {
                try {
                    inputStream.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            message.what = STATUS_ERROR;
            message.sendToTarget();
        }

        @Override
        public void onWriting(int saveBytes) {
            listener.onLoading(uri.toString(), saveBytes, totleBytes);
        }
    }

    private class ImageLoadHandler extends Handler {
        public ImageLoadHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            ImageLoadingTask task = (ImageLoadingTask) msg.obj;
            boolean isRemove = mTaskList.remove(task);
            switch (msg.what) {
            case STATUS_SUCCESS:
                // if this ImageLoadingTask has been canceled before it done. we
                // can not invoke the callback.
                if (isRemove) {
                    task.listener.onLoadSuccess(task.bitmap, task.uri.toString(), task.isLoadFromCache);
                }
                break;
            case STATUS_ERROR:
                task.listener.onLoadError(task.uri.toString());
                break;
            case STATUS_CANCEL:
                break;
            }
        }
    }
}