content: /*
   * CDDL HEADER START
   *
   * The contents of this file are subject to the terms of the Common Development
   * and Distribution License (the "License").
   * You may not use this file except in compliance with the License.
   *
   * You can obtain a copy of the license at
   * src/com/vodafone360/people/VODAFONE.LICENSE.txt or
   * http://github.com/360/360-Engine-for-Android
   * See the License for the specific language governing permissions and
   * limitations under the License.
   *
   * When distributing Covered Code, include this CDDL HEADER in each file and
   * include the License file at src/com/vodafone360/people/VODAFONE.LICENSE.txt.
   * If applicable, add the following below this CDDL HEADER, with the fields
   * enclosed by brackets "[]" replaced with your own identifying information:
   * Portions Copyright [yyyy] [name of copyright owner]
   *
   * CDDL HEADER END
   *
   * Copyright 2010 Vodafone Sales & Services Ltd.  All rights reserved.
   * Use is subject to license terms.
   */
  
  package com.vodafone360.people;
  
  import java.lang.ref.SoftReference;
  import java.lang.reflect.Field;
  import java.security.InvalidParameterException;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Set;
  
  import android.app.Activity;
  import android.graphics.Bitmap;
  import android.graphics.BitmapFactory;
  import android.view.View;
  import android.widget.ImageView;
  
  import com.vodafone360.people.utils.LRUHashMap;
  import com.vodafone360.people.utils.LogUtils;
  import com.vodafone360.people.utils.ThumbnailUtils;
  
  
  /***
   * Unified thumbnail cache with asynchronous file reading, intended to be
   * utilised by multiple activities.
   */
  public class ThumbnailCache {
  
      /** Thread name.  **/
      private static final String THREAD_NAME = "ThumbnailCacheThread";
      /** Request queue size (number of possible on screen items). **/
      private static final int REQUEST_QUEUE_SIZE = 9;
      /**
       * Thumbnails cache size (REQUEST_QUEUE_SIZE or greater, but larger values
       * are OK due to garbage collection).
       */
      private static final int THUMBNAILS_CACHE_SIZE = 20;
      /**
       * Keep the Thumbnail Cache thread in the background by making it wait a
       * tiny bit between loading files.
       */
      private static final long THREAD_WAIT = 5L;
  
      /** Thumbnail cache. **/
      private final LRUHashMap<Long, SoftReference<Bitmap>> mThumbnailCache;
      /** Request queue (must be synchronised). **/
      private final List<Item> mRequestQueue;
      /** List of invalid Thumbnails. **/
      private final List<Long> mInvalidatedThumbnails;
      /**
       * Instance of the background thread (must always be accessed by the main
       * thread only).
       */
      private BackgroundThread mBackgroundThread;
      /** Background Thread sync object. **/
      private Object mBackgroundThreadSync = new Object();
      /** Reference to activity which is currently using this item. **/
      private Activity mActivity;
      /** True if the background thread should not be doing any work.  **/
      private boolean mPaused = false;
  
      /** Item in the request queue. **/
      private class Item {
          /** Contact ID. **/
          private final long mContactId;
          /** ImageView to populate. **/
          private final ImageView mImageView;
  
          /***
           * Item constructor.
           *
           * @param contactId Contact ID.
           * @param imageView ImageView to populate.
           */
          public Item(final long contactId, final ImageView imageView) {
              mContactId = contactId;
              mImageView = imageView;
          }
  
          /***
           * Contact ID.
           *
           * @return Contact ID.
           */
          public long getContactId() {
              return mContactId;
          }
  
          /***
           * ImageView to populate.
           *
           * @return ImageView to populate.
           */
          public ImageView getImageView() {
              return mImageView;
          }
      }
  
      /***
       * Create the Cache.
       */
      public ThumbnailCache() {
          mThumbnailCache = new LRUHashMap<Long, SoftReference<Bitmap>>(THUMBNAILS_CACHE_SIZE);
          mRequestQueue = new ArrayList<Item>(REQUEST_QUEUE_SIZE);
          mInvalidatedThumbnails = new ArrayList<Long>();
      }
  
      /***
       * Subscribe this Activity to the cache.
       *
       * @param activity Current Activity for posting to a UI thread.
       */
      public final void subscribe(final Activity activity) {
          synchronized (mBackgroundThreadSync) {
              if (mBackgroundThread == null) {
                  mBackgroundThread = new BackgroundThread();
              }
          }
          mActivity = activity;
          pauseThread(false);
      }
  
      /***
       * Removed the background thread and clear all pending work.
       */
      public final void unsubscribe() {
          synchronized (mBackgroundThreadSync) {
              if (mBackgroundThread != null) {
                  mBackgroundThread.killThread();
              }
              mBackgroundThread = null;
          }
          mPaused = true;
          mActivity = null;
          synchronized (mRequestQueue) {
              mRequestQueue.clear();
          }
  
      }
  
      /***
       * Pause the Background Thread, while keeping the work queue.
       *
       * @param pause TRUE if the thread should be paused, FALSE will immediately
       *      resume the thread.
       */
      public final void pauseThread(final boolean pause) {
          mPaused = pause;
          synchronized (mBackgroundThreadSync) {
              if (!pause && mBackgroundThread != null) {
                  mBackgroundThread.doWork();
              }
          }
      }
  
      /***
       * Sets the given ImageView with the cached thumbnail for this contact ID,
       * using the default thumbnail if it is not cached.  If the thumbnail is
       * not cached and "queue" is set to TRUE, then a background thread will try
       * and populate the ImageView later.
       *
       * @param imageView ImageView to set now (from cache) or later (in
       *          background thread).
       * @param localContactId ID of the contact thumbnail.
       * @param defaultThumbnailId ID of default thumbnail resource.
       */
      public final void setThumbnail(final ImageView imageView,
              final long localContactId, final int defaultThumbnailId) {
          if (imageView == null) {
              throw new InvalidParameterException("ThumbnailCache.setThumbnail() "
                      + "ImageView should not be NULL");
          }
  
          boolean imageSet = false;
  
          /** Associate the ImageView with the contact ID. **/
          imageView.setTag(localContactId);
  
          /** Check the thumbnail cache. **/
          final SoftReference<Bitmap> bitmapRef
              = mThumbnailCache.get(localContactId);
          if (bitmapRef != null) {
              final Bitmap thumbnail = bitmapRef.get();
              if (thumbnail != null) {
                  /** Instantly return a cached reference. **/
                  imageView.setImageBitmap(thumbnail);
                  imageSet = true;
  
                  if (!mInvalidatedThumbnails.contains(localContactId)) {
                      /** This image is valid, so don't try and reload. **/
                      return;
                  }
  
              } else {
                  /** Remove any faulty references from the cache. **/
                  LogUtils.logW("ThumbnailCache.setThumbnail() "
                          + "Bad reference removed id[" + localContactId + "]");
                  mThumbnailCache.remove(localContactId);
              }
          }
  
          /** Add this thumbnail to the request queue. **/
          synchronized (mRequestQueue) {
              if (!mRequestQueue.contains(localContactId)) {
                  /** Remove oldest item in the list. **/
                  if (mRequestQueue.size() > REQUEST_QUEUE_SIZE) {
                      mRequestQueue.remove(0);
                  }
                  mRequestQueue.add(new Item(localContactId, imageView));
                  synchronized (mBackgroundThreadSync) {
                      if (mBackgroundThread != null) {
                          mBackgroundThread.doWork();
                      }
                  }
              }
          }
  
          if (!imageSet) {
              /** Thumbnail was not found, so use default. **/
              imageView.setImageResource(defaultThumbnailId);
          }
      }
  
      /***
       * Clear the entire thumbnail cache to save memory.
       */
      private final void clearThumbnailCache() {
          synchronized (mRequestQueue) {
              mRequestQueue.clear();
          }
          mThumbnailCache.clear();
          mInvalidatedThumbnails.clear();
          System.gc();
      }
  
      /***
       * Invalidate the entire thumbnail cache, that way a thumbnail that has
       * been changed in the file system will be updated the next time it is
       * requested by the UI.
       */
      public final void invalidateThumbnailCache() {
          mInvalidatedThumbnails.clear();
          synchronized (mThumbnailCache) {
              for (Long contactId : mThumbnailCache.keySet()) {
                  mInvalidatedThumbnails.add(contactId);
              }
          }
      }
  
      /***
       * Perform work in a background thread.
       */
      private class BackgroundThread extends Thread {
          /**
           * Object is used to indicate that this thread has started, and for
           * pausing and notifying the Thread.  Thread will wait on this object.
           */
          private Object mRunning;
          /** Set to TRUE to indicate that this thread should roll to a stop. **/
          private boolean mKillThread = false;
  
          /***
           * Start or bring the thread out of wait state to do some background
           * work.
           */
          public void doWork() {
              if (mKillThread) {
                  /** Exit now. **/
                  return;
  
              } else if (mRunning == null) {
                  /** First time start. **/
                  mRunning = new Object();
                  start();
  
              } else if (mRequestQueue.size() != 0 && !mPaused) {
                  /** Notify the running thread. **/
                  synchronized (mRunning) {
                      mRunning.notify();
                  }
              }
          }
  
          @Override
          public void run() {
              android.os.Process.setThreadPriority(
                      android.os.Process.THREAD_PRIORITY_BACKGROUND);
              Thread.currentThread().setName(THREAD_NAME);
  
              while (!mKillThread) {
                  loadThumbnails();
                  threadWait(null);
              }
          }
  
          /***
           * Load thumbnails from the file IO, posting a cache updated
           * notification to the handled as long as one file has been loaded.
           */
          private final void loadThumbnails() {
              while (!mKillThread) {
                  if (mPaused || mActivity == null) {
                      return;
                  }
  
                  final Item item;
                  synchronized (mRequestQueue) {
                      if (mRequestQueue.size() < 1) {
                          /** Loading done. **/
                          return;
                      }
                      item = mRequestQueue.remove(0);
                  }
  
                  if (item.getContactId()
                          != getLocalContactId(item.getImageView())) {
                      /** ImageView has been Recycled. **/
                      continue;
                  }
  
                  /**
                   * Do all expensive File IO and Bitmap decoding work.
                   */
                  final String path = ThumbnailUtils.thumbnailPath(
                          item.getContactId());
                  if (path != null) {
                      try {
                          /* Using reflection to set inPurgeable flag as it is not available on 1.5 */
                          Class bitmapFactoryOptionsClass = BitmapFactory.Options.class;
                          BitmapFactory.Options bitmapFactoryOptionsInstance = new BitmapFactory.Options();
                          Field field;
                          try {
                              field = bitmapFactoryOptionsClass.getField("inPurgeable");
                              field.setBoolean(bitmapFactoryOptionsInstance, true);
                          } catch (SecurityException e) {
                              LogUtils.logW("ThumbnailCache.loadThumbnails() "
                                      + "Security Exception");
                          } catch (NoSuchFieldException e) {
                              LogUtils.logW("ThumbnailCache.loadThumbnails() "
                                      + "Field not found");
                          } catch (IllegalArgumentException e) {
                              LogUtils.logW("ThumbnailCache.loadThumbnails() "
                                      + "Illegal Argument");
                          } catch (IllegalAccessException e) {
                              LogUtils.logW("ThumbnailCache.loadThumbnails() "
                                      + "Illegal Access");
                          }
  
                          final Bitmap bitmap = BitmapFactory.decodeFile(path, bitmapFactoryOptionsInstance);
                          mThumbnailCache.put(item.getContactId(),
                                  new SoftReference<Bitmap>(bitmap));
  
                          /** Thumbnail is now valid. **/
                          mInvalidatedThumbnails.remove(item.getContactId());
  
                          if (item.getContactId()
                                  != getLocalContactId(item.getImageView())) {
                              /** ImageView has been Recycled. **/
                              continue;
                          }
  
                          if (mActivity == null) {
                              return;
                          }
                          mActivity.runOnUiThread(new Runnable() {
                              @Override
                              public void run() {
                                  /**
                                   * Check if the view is still the same, and
                                   * hasn't been re-used by the ListView.
                                   */
                                  if (item.getContactId()
                                          == getLocalContactId(item.getImageView())) {
                                      item.getImageView().setImageBitmap(bitmap);
                                  }
                              }});
  
                      } catch (NullPointerException e) {
                          LogUtils.logE("ThumbnailCache.loadThumbnails() "
                                  + "Unexpected NullPointerException while "
                                  + "loading thumbnails, clearing Thumbnail "
                                  + "cache for safety.", e);
                          clearThumbnailCache();
  
                      } catch (OutOfMemoryError outOfMemoryError) {
                          LogUtils.logE("ThumbnailCache.loadThumbnails() "
                                  + "Low on memory while decoding thumbnails",
                                  outOfMemoryError);
                          clearThumbnailCache();
                      }
                  }
  
                  threadWait(THREAD_WAIT);
              }
          }
  
          /***
           * Stop the thread between cycles to stop it overloading the device and
           * possibly blocking the UI.
           *
           * @param wait Time to wait in milliseconds, or NULL to wait for a
           *      thread notify.
           */
          private void threadWait(final Long wait) {
              synchronized (mRunning) {
                  try {
                      if (mKillThread) {
                          /** Never wait while thread is being killed. **/
                          return;
                      } else if (wait == null) {
                          mRunning.wait();
                      } else {
                          mRunning.wait(wait);
                      }
  
                  } catch (InterruptedException e) {
                      // Do nothing.
                  }
              }
          }
  
          /***
           * Kills the running background thread.
           */
          public void killThread() {
              mKillThread = true;
              synchronized (mRunning) {
                  mRunning.notify();
              }
          }
      }
  
      /***
       * Return the local contact ID tag for the given View.
       *
       * @param view View to extract the list position information.
       * @return List position of the view.
       */
      private static long getLocalContactId(final View view) {
          final Object localContactIdObject = view.getTag();
          if (localContactIdObject == null) {
              LogUtils.logW("ThumbnailCache.getLocalContactId() "
                      + "ID for view should not be NULL");
              return -1L;
          } else {
              return (Long) localContactIdObject;
          }
      }
  
      /***
       * Return TRUE if the background thread is paused.
       *
       * @return TRUE if the background thread is paused.
       */
      public boolean isPaused() {
          return mPaused;
      }
  }
  