Android Open Source - android-image-management Image Manager






From Project

Back to project page android-image-management.

License

The source code is released under:

Apache License

If you think the Android project android-image-management listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 * android-image-management for Android/*  w  w w.ja va 2s  . c  o m*/
 * Copyright (C) 2013 Laurence Dawson <contact@laurencedawson.com>
 *
 * 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.laurencedawson.image_management;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Comparator;
import java.util.Date;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.net.Uri;
import android.os.Build;
import android.support.v4.util.LruCache;
import android.webkit.MimeTypeMap;

public class ImageManager {

  public static final boolean DEBUG = false;

  public static final int QUEUE_SIZE = 30;

  public static final int LONG_DELAY = 160;
  public static final int SHORT_DELAY = 80;
  public static final int NO_DELAY = 0;

  public static final int LONG_CONNECTION_TIMEOUT = 10000;
  public static final int LONG_REQUEST_TIMEOUT = 10000;

  public static final int MAX_WIDTH = 480;
  public static final int MAX_HEIGHT = 720;

  public static final int UI_PRIORITY = 1;
  public static final int BACKGROUND_PRIORITY = 0;

  public static final String GIF_MIME = "image/gif";

  private Context mContext;
  private final LruCache<String, Bitmap> mBitmapCache;
  private final ExecutorService mThreadPool;

  // Inspired by Volley, only allow one thread to decode a bitmap
  private static final Object DECODE_LOCK = new Object();

  // Maintain 3 queues for image requests
  // The first queues requests to be processed immediately by the thread pool 
  private BlockingQueue<Runnable> mTaskQueue;
  // The second maintains a queue of active requests
  private ConcurrentLinkedQueue<Runnable> mActiveTasks;
  // The third maintains a queue of blocked requests. A request can be blocked
  // if a duplicate exists in the task or active queue
  private ConcurrentLinkedQueue<Runnable> mBlockedTasks;

  /**
   * Initialize a newly created ImageManager
   * @param context The application ofinal r activity context
   * @param cacheSize The size of the LRU cache
   * @param threads The number of threads for the pools to use
   */
  public ImageManager(final Context context, final int cacheSize, 
      final int threads ){

    // Instantiate the three queues. The task queue uses a custom comparator to 
    // change the ordering from FIFO (using the internal comparator) to respect
    // request priorities. If two requests have equal priorities, they are 
    // sorted according to creation date
    mTaskQueue =  new PriorityBlockingQueue<Runnable>(QUEUE_SIZE, 
        new ImageThreadComparator());
    mActiveTasks = new ConcurrentLinkedQueue<Runnable>();
    mBlockedTasks = new ConcurrentLinkedQueue<Runnable>();

    // The application context
    mContext = context;

    // Create a new threadpool using the taskQueue
    mThreadPool = new ThreadPoolExecutor(threads, threads, 
        Long.MAX_VALUE, TimeUnit.SECONDS, mTaskQueue){


      @Override
      protected void beforeExecute(final Thread thread, final Runnable run) {
        // Before executing a request, place the request on the active queue
        // This prevents new duplicate requests being placed in the active queue
        mActiveTasks.add(run);
        super.beforeExecute(thread, run);
      }

      @Override
      protected void afterExecute(final Runnable r, final Throwable t) {
        // After a request has finished executing, remove the request from
        // the active queue, this allows new duplicate requests to be submitted
        mActiveTasks.remove(r);

        // Perform a quick check to see if there are any remaining requests in
        // the blocked queue. Peek the head and check for duplicates in the 
        // active and task queues. If no duplicates exist, add the request to
        // the task queue. Repeat this until a duplicate is found
        synchronized (mBlockedTasks) {
          while(mBlockedTasks.peek()!=null && 
              !mTaskQueue.contains(mBlockedTasks.peek()) && 
              !mActiveTasks.contains(mBlockedTasks.peek())){
            Runnable runnable = mBlockedTasks.poll();
            if(runnable!=null){
              mThreadPool.execute(runnable);
            }
          }
        }
        super.afterExecute(r, t);
      }
    };

    // Calculate the cache size
    final int actualCacheSize = 
        ((int) (Runtime.getRuntime().maxMemory() / 1024)) / cacheSize;

    // Create the LRU cache
    // http://developer.android.com/reference/android/util/LruCache.html

    // The items are no longer recycled as they leave the cache, turns out this wasn't the right
    // way to go about this and often resulted in recycled bitmaps being drawn
    // http://stackoverflow.com/questions/10743381/when-should-i-recycle-a-bitmap-using-lrucache
    mBitmapCache = new LruCache<String, Bitmap>(actualCacheSize){
      protected int sizeOf(final String key, final Bitmap value) {
        return value.getByteCount() / 1024;
      }
    };
  }

  /**
   * Remove all images from the LRU cache
   */
  public void removeAll() {
    synchronized (mBitmapCache) {
      mBitmapCache.evictAll();
    }
  }

  /**
   * Remove an Image from the LRU cache
   * @param url The URL of the element to remove
   */
  public void removeEntry(final String url){
    if(url==null){
      return;
    }

    synchronized (mBitmapCache) {
      mBitmapCache.remove(url);
    }
  }

  /**
   * Add a bitmap to the cache
   * @param url URL of the image
   * @param bitmap The bitmap image
   */
  private void addEntry(final String url, final Bitmap bitmap){

    if(url == null || bitmap == null){
      return;
    }

    synchronized (mBitmapCache) {
      if(mBitmapCache.get(url) == null){
        mBitmapCache.put(url, bitmap);
      }
    }
  }

  /**
   * Given an image URL, check if the cached image is a GIF
   * @param url The URL of the image
   * @return True if the image is a GIF
   */
  public boolean isGif(String url){
    if(url==null){
      return false;
    }

    // First try to grab the mime from the options
    Options options = new Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(getFullCacheFileName(mContext, url), options);
    if(options.outMimeType!=null && 
        options.outMimeType.equals(ImageManager.GIF_MIME)){
      return true;
    }

    // Next, try to grab the mime type from the url
    final String extension = MimeTypeMap.getFileExtensionFromUrl(url);
    if(extension!=null){
      String mimeType = 
          MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
      if(mimeType!=null){
        return mimeType.equals(ImageManager.GIF_MIME);
      }
    }

    return false;
  }


  /**
   * Get a specified image from the cache
   * @param url The URL of the image
   * @return A Bitmap of the image requested
   */
  public Bitmap get(String url) {
    if(url!=null){
      synchronized (mBitmapCache) {

        // Get the image from the cache
        final Bitmap bitmap = mBitmapCache.get(url);

        // Check if the bitmap is in the cache
        if (bitmap != null) {
          return bitmap;
        }
      }
    }

    return null;
  }

  /**
   * Request an image to be downloaded, cached and loaded into the LRU cache.
   * 
   * @param url The URL of the image to grab
   * @param request The ImageRequest complete with image options
   */
  public void requestImage(final ImageRequest request) {

    // If the request has no URL, abandon it
    if(request.mUrl==null){
      return;
    }

    // Create the image download runnable
    final ImageDownloadThread imageDownloadThread = new ImageDownloadThread(){

      public void run() {

        // Sleep the request for the specified time
        if(request!=null && request.mLoadDelay>0){
          try {
            Thread.sleep(request.mLoadDelay);
          } catch (InterruptedException e){
            if(ImageManager.DEBUG){
              e.printStackTrace();
            }
          }
        }

        File file = null;

        // If the URL is not a local reseource, grab the file
        if(!getUrl().startsWith("content://")){

          // Grab a link to the file
          file = new File(getFullCacheFileName(mContext, getUrl()));

          // If the file doesn't exist, grab it from the network
          if (!file.exists()){
            cacheImage( file, request);
          } 

          // Otherwise let the callback know the image is cached
          else if(request!=null){
            request.sendCachedCallback(getUrl(), true);
          }

          // Check if the file is a gif
          boolean isGif = isGif(getUrl());

          // If the file downloaded was a gif, tell all the callbacks
          if( isGif && request!=null ){
            request.sendGifCallback(getUrl());
          }
        }

        // Check if we should cache the image and the dimens
        boolean shouldCache = false;
        int maxWidth = MAX_WIDTH;
        int maxHeight = MAX_HEIGHT;
        if(request!=null){
          maxWidth = request.mMaxWidth;
          maxHeight = request.mMaxHeight;
          shouldCache = request.mCacheImage;
        }

        // If any of the callbacks request the image should be cached, cache it
        if(shouldCache && 
            (request!=null&&request.mContext!=null)||request==null){

          // First check the image isn't actually in the cache
          Bitmap bitmap = get(getUrl());

          // If the bitmap isn't in the cache, try to grab it
          // Or the bitmap was in the cache, but is of no use
          if(bitmap == null){

            if(!getUrl().startsWith("content://")){
              bitmap = decodeBitmap(file, maxWidth, maxHeight);
            }else{
              Uri uri = Uri.parse(getUrl());
              try{
                InputStream input = mContext.getContentResolver().openInputStream(uri);
                bitmap = BitmapFactory.decodeStream(input);
                input.close();
              }catch(FileNotFoundException e){
                if(DEBUG){
                  e.printStackTrace();
                }
              }catch(IOException e){
                if(DEBUG){
                  e.printStackTrace();
                }
              }
            }

            // If we grabbed the image ok, add to the cache
            if(bitmap!=null){
              addEntry(getUrl(), bitmap);
            }
          }

          // Send the cached callback
          if(request!=null){
            request.sendCallback(getUrl(), bitmap);
          }
        }
      }
    };

    // Set the url of the request
    imageDownloadThread.setUrl(request.mUrl);

    // Set the creation time of the request
    imageDownloadThread.setCreated(request.mCreated);

    // Assign a priority to the request
    if(request.mImageListener==null){
      // If there is no image listener, assign it background priority
      imageDownloadThread.setPriority(BACKGROUND_PRIORITY);
    }else{
      // If there is an image listener, assign it UI priority
      imageDownloadThread.setPriority(UI_PRIORITY);
    }

    // If the new request is not a duplicate of an entry of the active and 
    // task queues, add the request to the task queue
    if(!mTaskQueue.contains(imageDownloadThread) && 
        !mActiveTasks.contains(imageDownloadThread)){
      mThreadPool.execute(imageDownloadThread);
    }

    // If the request is a duplicate, add it to the blocked tasks queue
    else{
      mBlockedTasks.add(imageDownloadThread);
    }
  }

  /**
   * Grab and save an image directly to disk
   * @param file The Bitmap file
   * @param url The URL of the image
   * @param imageCallback The callback associated with the request
   */
  public static void cacheImage(final File file, ImageRequest imageCallback) {

    HttpURLConnection urlConnection = null;
    FileOutputStream fileOutputStream = null;
    InputStream inputStream = null;
    boolean isGif = false;

    try{
      // Setup the connection
      urlConnection = (HttpURLConnection) new URL(imageCallback.mUrl).openConnection();
      urlConnection.setConnectTimeout(ImageManager.LONG_CONNECTION_TIMEOUT);
      urlConnection.setReadTimeout(ImageManager.LONG_REQUEST_TIMEOUT);
      urlConnection.setUseCaches(true);
      urlConnection.setInstanceFollowRedirects(true);

      // Set the progress to 0
      imageCallback.sendProgressUpdate(imageCallback.mUrl, 0);

      // Connect
      inputStream = urlConnection.getInputStream();

      // Do not proceed if the file wasn't downloaded
      if(urlConnection.getResponseCode()==404){
        urlConnection.disconnect();
        return;
      }

      // Check if the image is a GIF
      String contentType = urlConnection.getHeaderField("Content-Type");
      if(contentType!=null){
        isGif = contentType.equals(GIF_MIME);
      }

      // Grab the length of the image
      int length = 0;
      try{
        String fileLength = urlConnection.getHeaderField("Content-Length");
        if(fileLength!=null){
          length = Integer.parseInt(fileLength);
        }
      }catch(NumberFormatException e){
        if(ImageManager.DEBUG){
          e.printStackTrace();
        }
      }

      // Write the input stream to disk
      fileOutputStream = new FileOutputStream(file, true);
      int byteRead = 0;
      int totalRead = 0;
      final byte[] buffer = new byte[8192];
      int frameCount = 0;

      // Download the image
      while ((byteRead = inputStream.read(buffer)) != -1) {

        // If the image is a gif, count the start of frames
        if(isGif){
          for(int i=0;i<byteRead-3;i++){
            if( buffer[i] == 33 && buffer[i+1] == -7 && buffer[i+2] == 4 ){
              frameCount++;

              // Once we have at least one frame, stop the download
              if(frameCount>1){
                fileOutputStream.write(buffer, 0, i);
                fileOutputStream.close();

                imageCallback.sendProgressUpdate(imageCallback.mUrl, 100);
                imageCallback.sendCachedCallback(imageCallback.mUrl, true);

                urlConnection.disconnect();
                return;
              }
            }
          }
        }

        // Write the buffer to the file and update the total number of bytes
        // read so far (used for the callback)
        fileOutputStream.write(buffer, 0, byteRead);
        totalRead+=byteRead;

        // Update the callback with the current progress
        if(length>0){
          imageCallback.sendProgressUpdate(imageCallback.mUrl, 
              (int) (((float)totalRead/(float)length)*100) );
        }
      }

      // Tidy up after the download
      if (fileOutputStream != null){
        fileOutputStream.close();
      }

      // Sent the callback that the image has been downloaded
      imageCallback.sendCachedCallback(imageCallback.mUrl, true);

      if (inputStream != null){
        inputStream.close();
      }

      // Disconnect the connection
      urlConnection.disconnect();
    } catch (final MalformedURLException e) {
      if (ImageManager.DEBUG){
        e.printStackTrace();
      }

      // If the file exists and an error occurred, delete the file
      if (file != null){
        file.delete();
      }

    } catch (final IOException e) {
      if (ImageManager.DEBUG){
        e.printStackTrace();
      }

      // If the file exists and an error occurred, delete the file
      if (file != null){
        file.delete();
      }

    }

  }

  /**
   * Decode a Bitmap with the default max width and height
   * @param file The Bitmap file
   * @return The Bitmap image
   */
  public static Bitmap decodeBitmap(final File file){
    return decodeBitmap(file, ImageManager.MAX_WIDTH, ImageManager.MAX_WIDTH);
  }

  /**
   * Decode a Bitmap with a given max width and height
   * @param file The Bitmap file
   * @param reqWidth The requested width of the resulting bitmap
   * @param reqHeight The requested height of the resulting bitmap
   * @return The Bitmap image
   */
  @SuppressLint("NewApi")
  public static Bitmap decodeBitmap(final File file, final int reqWidth, 
      final int reqHeight) {

    // Serialize all decode on a global lock to reduce concurrent heap usage.
    synchronized (DECODE_LOCK) {

      // Check if the file doesn't exist or has no content
      if(!file.exists() || file.exists() && file.length()==0 ){
        return null;
      }

      // Load a scaled version of the bitmap
      Options opts = null;
      opts = getOptions(file,reqWidth,reqHeight);

      // Set a few additional options for the bitmap opts
      opts.inPurgeable = true;
      opts.inInputShareable = true;
      opts.inDither = true;

      // Grab the bitmap
      Bitmap bitmap = BitmapFactory.decodeFile(file.getPath(), opts);

      // If on JellyBean attempt to draw with mipmaps enabled
      if(bitmap!=null && 
          Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){
        bitmap.setHasMipMap(true);
      }

      // return the decoded bitmap
      return bitmap;
    }
  }

  /**
   * Grab the Bitmap options for a given max width and height 
   * https://code.google.com/p/iosched
   * 
   * @param file The file to load options for
   * @param reqWidth The requested width of the resulting bitmap
   * @param reqHeight The requested height of the resulting bitmap
   * @return The options to be used for loading bitmaps
   */
  public static Options getOptions(final File file, final int reqWidth, 
      final int reqHeight) {
    BitmapFactory.Options options = new Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(file.getPath(), options);
    options.inJustDecodeBounds = false;

    int inSampleSize = 1;
    int height = options.outHeight;
    int width = options.outWidth;

    if (height > reqHeight || width > reqWidth) {
      // Calculate ratios of height and width to requested height and width
      final int heightRatio = Math.round((float) height / (float) reqHeight);
      final int widthRatio = Math.round((float) width / (float) reqWidth);

      // Choose the smallest ratio as inSampleSize value, this will guarantee
      // a final image with both dimensions larger than or equal to the
      // requested height and width.
      inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;

      // 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++;
      }
    }

    options.inSampleSize = inSampleSize;
    return options;
  }

  /**
   * Grab the full cache name for an image
   * 
   * Based upon the following project
   * https://code.google.com/p/iosched
   * @param context The calling {@link Context}
   * @param url The URL of the image
   */
  public static String getFullCacheFileName(Context context, String url) {
    return getCacheDir(context) + "/" + getCacheFileName(url);
  }

  /**
   * A hashing method that changes a string (like a URL) into a hash suitable 
   * for using as a disk filename.
   * @param url The URL of the image 
   * @return A filename
   */
  public static String getCacheFileName(final String url){
    String cacheKey;
    try {
      final MessageDigest mDigest = MessageDigest.getInstance("MD5");
      mDigest.update(url.getBytes());
      cacheKey = bytesToHexString(mDigest.digest());
    } catch (NoSuchAlgorithmException e) {
      cacheKey = String.valueOf(url.hashCode());
    }

    return cacheKey;
  }

  /**
   * Based upon the following project
   * https://code.google.com/p/iosched
   * @param bytes A byte array
   * @return A {@link String} to use for a filename
   */
  private static String bytesToHexString(byte[] bytes) {
    final StringBuilder stringBuilder = new StringBuilder();
    for (int i = 0; i < bytes.length; i++) {
      final String hex = Integer.toHexString(0xFF & bytes[i]);
      if (hex.length() == 1) {
        stringBuilder.append('0');
      }
      stringBuilder.append(hex);
    }
    return stringBuilder.toString();
  }

  /**
   * Get the cache directory to store images in
   * @param context The calling {@link Context}
   * @return a {@link File} to save images in
   */
  private static File getCacheDir( final Context context ){
    if(context!=null){
      // Try to grab a reference to the external cache dir
      final File directory = context.getExternalCacheDir();

      // If the file is OK, return it
      if(directory!=null){
        return directory;
      } 

      // If that fails, try to get a reference to the internal cache
      // This is a rare edge case but occasionally users don't have external
      // storage / their SDCard is removed / the folder is corrupt
      else{
        return context.getCacheDir();
      }
    }
    return null;
  }

  /**
   * Clear the local image cache of images over n days old
   * @param context The calling {@link Context}
   * @param int Max age of the images in days
   */
  public static void clearCache( Context context, final int maxDays ){

    // Grab the time now
    long time = new Date().getTime();

    // If we can access the external cache, empty that first
    if( context.getExternalCacheDir() != null ){
      String[] children = context.getExternalCacheDir().list();
      for (int i = children.length-1; i >= 0; i--){
        final File file = new File(context.getExternalCacheDir(), children[i]);
        final Date lastModified = new Date( file.lastModified() );
        final long difference = time - lastModified.getTime();
        final int days = (int) (difference / (24 * 60 * 60 * 1000));

        if(days>=maxDays){
          file.delete();
        }
      }
    }

    // If we can access the internal cache, empty that too
    if(context.getCacheDir() != null ){
      String[] children = context.getCacheDir().list();
      for (int i = children.length-1; i >= 0; i--){
        final File file = new File(context.getCacheDir(), children[i]);
        final Date lastModified = new Date( file.lastModified() );
        final long difference =  time - lastModified.getTime();
        final int days = (int) (difference / (24 * 60 * 60 * 1000));

        if(days>=maxDays){
          file.delete();
        }
      }
    }
  }
}

/**
 * A simple comparator which favours ImageDownloadThreads with higher 
 * priorities (such as UI requests over background requests)
 * @author Laurence Dawson
 *
 */
class ImageThreadComparator implements Comparator<Runnable>{

  @Override
  public int compare(final Runnable lhs, final Runnable rhs) {

    if(lhs instanceof ImageDownloadThread && rhs instanceof ImageDownloadThread){
      // Favour a higher priority
      if(((ImageDownloadThread)lhs).getPriority()>((ImageDownloadThread)rhs).getPriority()){
        return -1;
      } else if(((ImageDownloadThread)lhs).getPriority()<((ImageDownloadThread)rhs).getPriority()){
        return 1;
      } 

      // Favour a lower creation time
      else if(((ImageDownloadThread)lhs).getCreated()>((ImageDownloadThread)rhs).getCreated()){
        return 1;
      }else if(((ImageDownloadThread)lhs).getCreated()<((ImageDownloadThread)rhs).getCreated()){
        return -1;
      }
    }

    return 0;
  }  

}

/**
 * A simple Runnable object that can be given a priority, to be used with
 * ImageThreadComparator and PriorityBlockingQueue
 * @author Laurence Dawson
 *
 */
class ImageDownloadThread implements Runnable{      
  private int priority;       
  private String url;
  private long created;


  @Override
  public void run() {
    // To be overridden
  }

  public int getPriority() {
    return priority;
  }

  public void setPriority(final int priority) {
    this.priority = priority;
  }

  public String getUrl(){
    return url;
  }

  public void setUrl(String url){
    this.url = url;
  }

  public long getCreated(){
    return created;
  }

  public void setCreated(long created){
    this.created = created;
  }

  @Override
  public boolean equals(final Object o) {
    if(getUrl()==null){
      return false;
    }

    if(o==null){
      return false;
    }

    if(o instanceof ImageDownloadThread &&
        ((ImageDownloadThread)o).getUrl().equals(getUrl())){
      return true;
    }
    return super.equals(o);
  }

  @Override
  public int hashCode() {
    return getUrl().hashCode();
  }
}




Java Source Code List

com.laurencedawson.image_management.ImageListener.java
com.laurencedawson.image_management.ImageManager.java
com.laurencedawson.image_management.ImageRequest.java
com.laurencedawson.image_management.extras.SimpleImageView.java
com.laurencedawson.image_management_sample.CustomApplication.java
com.laurencedawson.image_management_sample.MainActivity.java