Android Open Source - singly-android Remote Image Cache






From Project

Back to project page singly-android.

License

The source code is released under:

MIT License

If you think the Android project singly-android 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

package com.singly.android.util;
/*from  w w w.  j  a va 2 s.c  om*/
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.commons.io.FileUtils;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.util.LruCache;

import com.loopj.android.http.AsyncHttpClient;
import com.loopj.android.http.BinaryHttpResponseHandler;

/**
 * A class that downloads and caches remote images to the local filesystem.
 * 
 * This class handles downloading images in background threads, limiting the
 * number of concurrent downloads, sampling the images to the current sample 
 * rate for the size at which the image will be displayed, writing the sampled
 * image to local disk, and running callbacks in the main UI thread when the
 * entire process is complete or has errored.
 * 
 * The class consists of two layers of caching, in memory and on disk.  If an
 * image is already stored on the local disk it will be pulled into memory upon
 * use.  If it hasn't been downloaded, it will be downloaded in a background
 * thread, stored on local disk, and then brought into memory.  If an image has
 * been recently used it will most likely be in memory and pulled from the in
 * memory cache for quick access.
 * 
 * Developers should be able to drop this class in and not have to worry about
 * if the image is remote, has already been cached locally, or is in memory.
 * There are options for specifying max concurrent downloads and max number of
 * images to cache in memory.
 */
public class RemoteImageCache {

  public static final String DEFAULT_CACHE_DIR = "_image_cache_";
  
  private static final String POISON_PILL = "_shutdown_";

  private File storageDir;

  private AtomicBoolean active = new AtomicBoolean(false);
  private final Semaphore throttle;
  private BlockingQueue<ImageInfo> queue = new LinkedBlockingQueue<ImageInfo>();

  private LruCache<String, Bitmap> imageCache;
  private Set<String> bad = Collections.synchronizedSet(new HashSet<String>());
  private Set<String> down = Collections.synchronizedSet(new HashSet<String>());

  private AsyncHttpClient httpClient = new AsyncHttpClient();
  private Handler handler = new Handler(Looper.getMainLooper());

  /**
   * Downloader thread that reads from a queue and downloads remote images to
   * local storage.  Runs continuously while the 
   */
  private class DownloaderThread
    extends Thread {

    public void run() {

      // continue downloading while not shutdown
      while (active.get()) {

        ImageInfo nextImage = null;
        try {
          
          // wait until an image to download appears in the queue
          nextImage = queue.take();
          
          // poison pill for clean shutdown of blocking queue
          if (nextImage.id.equals(POISON_PILL)) {
            continue;
          }
        }
        catch (InterruptedException ie) {
          continue;
        }
        
        // acquire the semaphore to start downloading
        try {
          throttle.acquire();
        }
        catch (InterruptedException ie) {
          continue;
        }

        // download the image
        final ImageInfo imageInfo = nextImage;
        final File imageFile = new File(storageDir, imageInfo.id + ".img");

        httpClient.get(imageInfo.imageUrl, null,
          new BinaryHttpResponseHandler() {

            @Override
            public void onSuccess(byte[] bytes) {

              // image downloaded, release semaphore, let the next one go
              throttle.release();

              byte[] imageBytes = null;
              Bitmap bitmap = null;

              try {

                // if we are sampling, then sample the image and turn it into a
                // Bitmap, if not just turn it into a Bitmap
                if (imageInfo.sample) {

                  bitmap = BitmapUtils.decodeAndScaleImage(bytes,
                    imageInfo.width, imageInfo.height);
                  ByteArrayOutputStream baos = new ByteArrayOutputStream();
                  bitmap.compress(imageInfo.format, imageInfo.quality, baos);
                  imageBytes = baos.toByteArray();
                }
                else {

                  imageBytes = bytes;
                  bitmap = BitmapFactory.decodeByteArray(imageBytes, 0,
                    imageBytes.length);
                }

                // write the Bitmap bytes to local storage
                if (bitmap != null && imageBytes != null) {
                  FileUtils.writeByteArrayToFile(imageFile, imageBytes);
                  imageCache.put(imageInfo.id, bitmap);
                }
              }
              catch (Exception e) {
                // error converting bytes to image
              }
              finally {                
                // remove image from the downloading state
                down.remove(imageInfo.id);
              }

              // create a handler to ensure the callback listener runs in the
              // main UI thread, pass in the Bitmap and the original image info
              final Bitmap image = bitmap;
              handler.post(new Runnable() {

                @Override
                public void run() {
                  
                  // run the ImageCacheListner callback for success
                  if (imageInfo.listener != null) {
                    imageInfo.listener.onSuccess(imageInfo, image);
                  }
                }
              });
            }

            @Override
            public void onFailure(final Throwable error, byte[] bytes) {

              // image download failed, release semaphore, let the next one go
              throttle.release();
              
              // remove from the downloading state, add to bad images so we 
              // won't try to download again
              bad.add(imageInfo.id);
              down.remove(imageInfo.id);
              
              // create a handler to ensure the callback listener runs in the
              // main UI thread, pass in the error and the original image info
              handler.post(new Runnable() {

                @Override
                public void run() {
                  
                  // run the ImageCacheListner callback for failure
                  if (imageInfo.listener != null) {
                    imageInfo.listener.onFailure(error, imageInfo);
                  }
                }
              });
            }
          });
      }
    }
  }

  /**
   * Default constructor.
   * 
   * @param context The current Android context.
   * @param maxParallelDown The maximum number of downloads that can
   * happen concurrently.
   * @param cacheDir The local directory to store cached images. This is just
   * a directory name.  The directory will always be inside the applications 
   * data/files directory.
   * @param cacheSize The number of images to cache in memory.
   */
  public RemoteImageCache(Context context, int maxParallelDown,
    String cacheDir, int cacheSize) {

    // Context and LRUCache for in memory Bitmaps
    Context appContext = context.getApplicationContext();
    File dataDir = appContext.getFilesDir();
    this.storageDir = new File(dataDir, cacheDir != null ? cacheDir
      : DEFAULT_CACHE_DIR);
    this.imageCache = new LruCache<String, Bitmap>(cacheSize);

    // semaphore to limit parallel downloads
    this.throttle = new Semaphore(maxParallelDown, true);
    this.active.set(true);

    // downloader thread that pulls from a queue
    DownloaderThread downloaderThread = new DownloaderThread();
    downloaderThread.setDaemon(true);
    downloaderThread.start();
  }

  /**
   * Return the Bitmap of the image or null if the image is not available, 
   * either because it could not be downloaded or because it is currently
   * downloading.
   * 
   * @param imageInfo The image information.
   * 
   * @return A Bitmap of the image.
   */
  public Bitmap getImage(ImageInfo imageInfo) {

    // no image info or image url or bad image don't bother further
    if (imageInfo == null || imageInfo.imageUrl == null
      || bad.contains(imageInfo.id)) {
      return null;
    }

    // try and get from the cache
    Bitmap image = imageCache.get(imageInfo.id);
    if (image != null) {
      return image;
    }

    // if the file exists then read the file from internal storage and turn
    // it into an Bitmap
    File imageFile = new File(storageDir, imageInfo.id + ".img");
    if (imageFile.exists()) {

      // try and first get from local storage
      try {

        byte[] imageBytes = FileUtils.readFileToByteArray(imageFile);
        Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0,
          imageBytes.length);
        imageCache.put(imageInfo.id, bitmap);

        return bitmap;
      }
      catch (IOException e) {
      }
    }

    // two step synchronized check and add for downloading state
    synchronized (down) {
      if (down.contains(imageInfo.id)) {
        return null;
      }
      down.add(imageInfo.id);
    }

    // drop into the download queue
    try {
      queue.put(imageInfo);
    }
    catch (InterruptedException ie) {
      // shouldn't happen, the queue is non blocking
    }

    return null;
  }

  /**
   * Shutdown the image cache, evicts all in memory Bitmaps, stops downloader
   * from running any future downloads.  Anything on the download queue or 
   * anything added to the download queue after shutdown is called is lost.
   */
  public void shutdown() {
    
    active.set(false);
    imageCache.evictAll();
    
    try {
      ImageInfo poisonPill = new ImageInfo();
      poisonPill.id = POISON_PILL;
      queue.put(poisonPill);
    }
    catch (InterruptedException ie) {
      // shouldn't happen, the queue is non blocking
    }
  }
}




Java Source Code List

com.singly.android.client.AsyncApiResponseHandler.java
com.singly.android.client.AuthenticationActivity.java
com.singly.android.client.AuthenticationWebViewListener.java
com.singly.android.client.BaseAuthenticationWebViewClient.java
com.singly.android.client.FacebookAuthenticationActivity.java
com.singly.android.client.SinglyClient.java
com.singly.android.component.AbstractCachingBlockLoadedListAdapter.java
com.singly.android.component.AuthenticatedServicesActivity.java
com.singly.android.component.AuthenticatedServicesAdapter.java
com.singly.android.component.AuthenticatedServicesFragment.java
com.singly.android.component.DeviceOwnerActivity.java
com.singly.android.component.Friend.java
com.singly.android.component.FriendsListActivity.java
com.singly.android.component.FriendsListAdapter.java
com.singly.android.component.FriendsListFragment.java
com.singly.android.component.FriendsListRowClickListener.java
com.singly.android.component.SinglyService.java
com.singly.android.component.TableOfContentsFragment.java
com.singly.android.component.TableOfContentsTouchListener.java
com.singly.android.examples.MainActivity.java
com.singly.android.util.BitmapUtils.java
com.singly.android.util.ImageCacheListener.java
com.singly.android.util.ImageInfo.java
com.singly.android.util.JSON.java
com.singly.android.util.RemoteImageCache.java
com.singly.android.util.SinglyUtils.java
com.singly.android.util.URLUtils.java