package cn.trinea.android.common.service.impl; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import android.content.Context; import android.graphics.Bitmap; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; import android.view.View; import cn.trinea.android.common.entity.CacheObject; import cn.trinea.android.common.entity.FailedReason; import cn.trinea.android.common.entity.FailedReason.FailedType; import cn.trinea.android.common.service.CacheFullRemoveType; import cn.trinea.android.common.util.ImageUtils; import cn.trinea.android.common.util.SizeUtils; import cn.trinea.android.common.util.StringUtils; import cn.trinea.android.common.util.SystemUtils; /** * <strong>Image Memory Cache</strong><br/> * <br/> * It applies to images those uesd frequently, like users avatar of twitter or sina weibo. Cache of big image you can * consider of {@link ImageSDCardCache}.<br/> * <ul> * <strong>Setting and Usage</strong> * <li>Use one of constructors in sections II to init cache</li> * <li>{@link #setOnImageCallbackListener(cn.trinea.android.common.service.impl.ImageMemoryCache.OnImageCallbackListener)} set callback interface when getting image</li> * <li>{@link #get(String, java.util.List, android.view.View)} get image asynchronous and preload other images asynchronous according to * urlList</li> * <li>{@link #get(String, android.view.View)} get image asynchronous</li> * <li>{@link #setHttpReadTimeOut(int)} set http read image time out, if less than 0, not set. default is not set</li> * <li>{@link PreloadDataCache#setContext(android.content.Context)} and {@link PreloadDataCache#setAllowedNetworkTypes(int)} restrict * the types of networks over which this data can get.</li> * <li>{@link #setOpenWaitingQueue(boolean)} set whether open waiting queue, default is true. If true, save all view * waiting for image loaded, else only save the newest one</li> * <li>{@link PreloadDataCache#setOnGetDataListener(OnGetDataListener)} set how to get image, this cache will get image * and preload images by it</li> * <li>{@link cn.trinea.android.common.service.impl.SimpleCache#setCacheFullRemoveType(CacheFullRemoveType)} set remove type when cache is full</li> * <li>other see {@link PreloadDataCache} and {@link cn.trinea.android.common.service.impl.SimpleCache}</li> * </ul> * <ul> * <strong>Constructor</strong> * <li>{@link #ImageMemoryCache()}</li> * <li>{@link #ImageMemoryCache(int)}</li> * <li>{@link #ImageMemoryCache(int, int)}</li> * </ul> * <ul> * <strong>Attentions</strong> * <li>You should add <strong>android.permission.ACCESS_NETWORK_STATE</strong> in manifest if you get image from * network.</li> * </ul> * * @author <a href="http://www.trinea.cn" target="_blank">Trinea</a> 2012-4-5 */ public class ImageMemoryCache extends PreloadDataCache<String, Bitmap> { private static final long serialVersionUID = 1L; private static final String TAG = "ImageCache"; /** callback interface when getting image **/ private OnImageCallbackListener onImageCallbackListener; /** http read image time out, if less than 0, not set. default is not set **/ private int httpReadTimeOut = -1; /** * whether open waiting queue, default is true. If true, save all view waiting for image loaded, else only save the * newest one **/ private boolean isOpenWaitingQueue = true; /** http request properties **/ private Map<String, String> requestProperties = null; /** recommend default max cache size according to dalvik max memory **/ public static final int DEFAULT_MAX_SIZE = getDefaultMaxSize(); /** message what for get image successfully **/ private static final int WHAT_GET_IMAGE_SUCCESS = 1; /** message what for get image failed **/ private static final int WHAT_GET_IMAGE_FAILED = 2; /** thread pool whose wait for data got, attention, not the get data thread pool **/ private transient ExecutorService threadPool = Executors .newFixedThreadPool(SystemUtils.DEFAULT_THREAD_POOL_SIZE); /** * key is image url, value is the newest view which waiting for image loaded, used when {@link #isOpenWaitingQueue} * is false **/ private transient Map<String, View> viewMap; /** * key is image url, value is view set those waiting for image loaded, used when {@link #isOpenWaitingQueue} is true **/ private transient Map<String, HashSet<View>> viewSetMap; private transient Handler handler; /** * get image asynchronous. when get image success, it will pass to * {@link cn.trinea.android.common.service.impl.ImageMemoryCache.OnImageCallbackListener#onGetSuccess(String, android.graphics.Bitmap, android.view.View, boolean)} * * @param imageUrl * @param view * @return whether image already in cache or not */ public boolean get(String imageUrl, View view) { return get(imageUrl, null, view); } /** * get image asynchronous and preload other images asynchronous according to urlList * * @param imageUrl * @param urlList url list, if is null, not preload, else preload forward by * {@link PreloadDataCache#preloadDataForward(Object, java.util.List, int)}, preload backward by * {@link PreloadDataCache#preloadDataBackward(Object, java.util.List, int)} * @param view * @return whether image already in cache or not */ public boolean get(final String imageUrl, final List<String> urlList, final View view) { if (onImageCallbackListener != null) { onImageCallbackListener.onPreGet(imageUrl, view); } if (StringUtils.isEmpty(imageUrl)) { if (onImageCallbackListener != null) { onImageCallbackListener.onGetNotInCache(imageUrl, view); } return false; } /** * if already in cache, call onImageSDCallbackListener, else new thread to wait for it */ CacheObject<Bitmap> object = getFromCache(imageUrl, urlList); if (object != null) { Bitmap bitmap = object.getData(); if (bitmap != null) { onGetSuccess(imageUrl, bitmap, view, true); return true; } else { remove(imageUrl); } } if (isOpenWaitingQueue) { synchronized (viewSetMap) { HashSet<View> viewSet = viewSetMap.get(imageUrl); if (viewSet == null) { viewSet = new HashSet<View>(); viewSetMap.put(imageUrl, viewSet); } viewSet.add(view); } } else { viewMap.put(imageUrl, view); } if (onImageCallbackListener != null) { onImageCallbackListener.onGetNotInCache(imageUrl, view); } if (isExistGettingDataThread(imageUrl)) { return false; } startGetImageThread(imageUrl, urlList); return false; } /** * get callback interface when getting image * * @return the onImageCallbackListener */ public OnImageCallbackListener getOnImageCallbackListener() { return onImageCallbackListener; } /** * set callback interface when getting image * * @param onImageCallbackListener */ public void setOnImageCallbackListener(OnImageCallbackListener onImageCallbackListener) { this.onImageCallbackListener = onImageCallbackListener; } /** * get http read image time out, if less than 0, not set. default is not set * * @return the httpReadTimeOut */ public int getHttpReadTimeOut() { return httpReadTimeOut; } /** * set http read image time out, if less than 0, not set. default is not set, in mills * * @param readTimeOutMillis */ public void setHttpReadTimeOut(int readTimeOutMillis) { this.httpReadTimeOut = readTimeOutMillis; } /** * get whether open waiting queue, default is true. If true, save all view waiting for image loaded, else only save * the newest one * * @return */ public boolean isOpenWaitingQueue() { return isOpenWaitingQueue; } /** * set whether open waiting queue, default is true. If true, save all view waiting for image loaded, else only save * the newest one * * @param isOpenWaitingQueue */ public void setOpenWaitingQueue(boolean isOpenWaitingQueue) { this.isOpenWaitingQueue = isOpenWaitingQueue; } /** * set http request properties * <ul> * <li>If image is from the different server, setRequestProperty("Connection", "false") is recommended. If image is * from the same server, true is recommended, and this is the default value</li> * </ul> * * @param requestProperties */ public void setRequestProperties(Map<String, String> requestProperties) { this.requestProperties = requestProperties; } /** * get http request properties * * @return */ public Map<String, String> getRequestProperties() { return requestProperties; } /** * Sets the value of the http request header field * * @param field the request header field to be set * @param newValue the new value of the specified property * @see {@link #setRequestProperties(java.util.Map)} */ public void setRequestProperty(String field, String newValue) { if (StringUtils.isEmpty(field)) { return; } if (requestProperties == null) { requestProperties = new HashMap<String, String>(); } requestProperties.put(field, newValue); } /** * <ul> * <li>Get data listener is {@link #getDefaultOnGetImageListener()}</li> * <li>callback interface when getting image is null, can set by * {@link #setOnImageCallbackListener(cn.trinea.android.common.service.impl.ImageMemoryCache.OnImageCallbackListener)}</li> * <li>Maximum size of the cache is {@link #DEFAULT_MAX_SIZE}</li> * <li>Elements of the cache will not invalid</li> * <li>Remove type is {@link RemoveTypeUsedCountSmall} when cache is full</li> * </ul> * * @see PreloadDataCache#PreloadDataCache() */ public ImageMemoryCache() { this(DEFAULT_MAX_SIZE, PreloadDataCache.DEFAULT_THREAD_POOL_SIZE); } /** * <ul> * <li>Get data listener is {@link #getDefaultOnGetImageListener()}</li> * <li>callback interface when getting image is null, can set by * {@link #setOnImageCallbackListener(cn.trinea.android.common.service.impl.ImageMemoryCache.OnImageCallbackListener)}</li> * <li>Elements of the cache will not invalid</li> * <li>Remove type is {@link RemoveTypeUsedCountSmall} when cache is full</li> * </ul> * * @param maxSize maximum size of the cache * @see PreloadDataCache#PreloadDataCache(int) */ public ImageMemoryCache(int maxSize) { this(maxSize, PreloadDataCache.DEFAULT_THREAD_POOL_SIZE); } /** * <ul> * <li>Get data listener is {@link #getDefaultOnGetImageListener()}</li> * <li>callback interface when getting image is null, can set by * {@link #setOnImageCallbackListener(cn.trinea.android.common.service.impl.ImageMemoryCache.OnImageCallbackListener)}</li> * <li>Elements of the cache will not invalid</li> * <li>Remove type is {@link RemoveTypeUsedCountSmall} when cache is full</li> * </ul> * * @param maxSize maximum size of the cache * @param threadPoolSize getting data thread pool size * @see PreloadDataCache#PreloadDataCache(int, int) */ public ImageMemoryCache(int maxSize, int threadPoolSize) { super(maxSize, threadPoolSize); super.setOnGetDataListener(getDefaultOnGetImageListener()); super.setCacheFullRemoveType(new RemoveTypeUsedCountSmall<Bitmap>()); this.viewMap = new ConcurrentHashMap<String, View>(); this.viewSetMap = new HashMap<String, HashSet<View>>(); this.handler = new MyHandler(); if (Looper.myLooper() == null) { Looper.prepare(); } } /** * callback interface when getting image * * @author <a href="http://www.trinea.cn" target="_blank">Trinea</a> 2012-4-5 */ public interface OnImageCallbackListener { /** * callback function before get image, run on ui thread * * @param imageUrl imageUrl * @param view view need the image */ public void onPreGet(String imageUrl, View view); /** * callback function when get image but image not in cache, run on ui thread.<br/> * Will be called after {@link #onPreGet(String, android.view.View)}, before * {@link #onGetSuccess(String, String, android.view.View, boolean)} and * {@link #onGetFailed(String, String, android.view.View, cn.trinea.android.common.entity.FailedReason)} * * @param imageUrl imageUrl * @param view view need the image */ public void onGetNotInCache(String imageUrl, View view); /** * callback function after get image successfully, run on ui thread * * @param imageUrl imageUrl * @param loadedImage loaded image bitmap * @param view view need the image * @param isInCache whether already in cache or got realtime */ public void onGetSuccess(String imageUrl, Bitmap loadedImage, View view, boolean isInCache); /** * callback function after get image failed, run on ui thread * * @param imageUrl imageUrl * @param loadedImage loaded image bitmap * @param view view need the image * @param failedReason failed reason for get image */ public void onGetFailed(String imageUrl, Bitmap loadedImage, View view, FailedReason failedReason); } /** * @see java.util.concurrent.ExecutorService#shutdown() */ protected void shutdown() { threadPool.shutdown(); super.shutdown(); } /** * @see java.util.concurrent.ExecutorService#shutdownNow() */ public List<Runnable> shutdownNow() { threadPool.shutdownNow(); return super.shutdownNow(); } /** * My handler * * @author <a href="http://www.trinea.cn" target="_blank">Trinea</a> 2012-11-20 */ private class MyHandler extends Handler { public void handleMessage(Message message) { switch (message.what) { case WHAT_GET_IMAGE_SUCCESS: case WHAT_GET_IMAGE_FAILED: MessageObject object = (MessageObject)message.obj; if (object == null) { break; } String imageUrl = object.imageUrl; Bitmap bitmap = object.bitmap; if (onImageCallbackListener != null) { if (isOpenWaitingQueue) { synchronized (viewSetMap) { HashSet<View> viewSet = viewSetMap.get(imageUrl); if (viewSet != null) { for (View view : viewSet) { if (view != null) { if (WHAT_GET_IMAGE_SUCCESS == message.what) { onGetSuccess(imageUrl, bitmap, view, false); } else { onImageCallbackListener.onGetFailed(imageUrl, bitmap, view, object.failedReason); } } } } } } else { View view = viewMap.get(imageUrl); if (view != null) { if (WHAT_GET_IMAGE_SUCCESS == message.what) { onGetSuccess(imageUrl, bitmap, view, false); } else { onImageCallbackListener.onGetFailed(imageUrl, bitmap, view, object.failedReason); } } } } if (isOpenWaitingQueue) { synchronized (viewSetMap) { viewSetMap.remove(imageUrl); } } else { viewMap.remove(imageUrl); } break; } } }; private void onGetSuccess(String imageUrl, Bitmap loadedImage, View view, boolean isInCache) { if (onImageCallbackListener == null) { return; } try { onImageCallbackListener.onGetSuccess(imageUrl, loadedImage, view, isInCache); } catch (OutOfMemoryError e) { onImageCallbackListener.onGetFailed(imageUrl, loadedImage, view, new FailedReason( FailedType.ERROR_OUT_OF_MEMORY, e)); } } /** * message object * * @author <a href="http://www.trinea.cn" target="_blank">Trinea</a> 2013-1-14 */ private class MessageObject { String imageUrl; Bitmap bitmap; FailedReason failedReason; public MessageObject(String imageUrl, Bitmap bitmap) { this.imageUrl = imageUrl; this.bitmap = bitmap; } public MessageObject(String imageUrl, Bitmap bitmap, FailedReason failedReason) { this.imageUrl = imageUrl; this.bitmap = bitmap; this.failedReason = failedReason; } } /** * start thread to wait for image get * * @param imageUrl * @param urlList url list, if is null, not preload, else preload forward by * {@link PreloadDataCache#preloadDataForward(Object, java.util.List, int)}, preload backward by * {@link PreloadDataCache#preloadDataBackward(Object, java.util.List, int)} */ private void startGetImageThread(final String imageUrl, final List<String> urlList) { // wait for image be got success and send message threadPool.execute(new Runnable() { @Override public void run() { try { CacheObject<Bitmap> object = get(imageUrl, urlList); Bitmap bitmap = (object == null ? null : object.getData()); if (bitmap == null) { // if bitmap is null, remove it remove(imageUrl); String failedException = "get image from network or save image to sdcard error. please make sure you have added permission android.permission.WRITE_EXTERNAL_STORAGE and android.permission.ACCESS_NETWORK_STATE"; FailedReason failedReason = new FailedReason(FailedType.ERROR_IO, failedException); handler.sendMessage(handler.obtainMessage(WHAT_GET_IMAGE_FAILED, new MessageObject(imageUrl, bitmap, failedReason))); } else { handler.sendMessage(handler.obtainMessage(WHAT_GET_IMAGE_SUCCESS, new MessageObject(imageUrl, bitmap))); } } catch (OutOfMemoryError e) { MessageObject msg = new MessageObject(imageUrl, null, new FailedReason( FailedType.ERROR_OUT_OF_MEMORY, e)); handler.sendMessage(handler.obtainMessage(WHAT_GET_IMAGE_FAILED, msg)); } } }); } /** * default get image from network listener * * @return */ public OnGetDataListener<String, Bitmap> getDefaultOnGetImageListener() { return new OnGetDataListener<String, Bitmap>() { private static final long serialVersionUID = 1L; @Override public CacheObject<Bitmap> onGetData(String key) { Bitmap d = null; try { d = ImageUtils.getBitmapFromUrl(key, httpReadTimeOut, requestProperties); } catch (Exception e) { Log.e(TAG, "get image exception, imageUrl is:" + key, e); } return (d == null ? null : new CacheObject<Bitmap>(d)); } }; } /** * get recommend default max cache size according to dalvik max memory * * @return */ static int getDefaultMaxSize() { long maxMemory = Runtime.getRuntime().maxMemory(); if (maxMemory > SizeUtils.GB_2_BYTE) { return 512; } int mb = (int)(maxMemory / SizeUtils.MB_2_BYTE); return mb > 16 ? mb * 2 : 16; } }