package in.srain.cube.image; import android.graphics.Point; import android.graphics.drawable.BitmapDrawable; import in.srain.cube.image.iface.ImageLoadHandler; import in.srain.cube.util.CLog; import in.srain.cube.util.CubeDebug; import in.srain.cube.util.Encrypt; import java.lang.ref.WeakReference; /** * A wrapper of the related information used in loading a bitmap * * @author http://www.liaohuqiu.net */ public class ImageTask { protected static final String LOG_TAG = CubeDebug.DEBUG_IMAGE_LOG_TAG_TASK; private static final Object sPoolSync = new Object(); private static ImageTask sTop; private static int sPoolSize = 0; private static final int MAX_POOL_SIZE = 20; private static boolean USE_POOL = false; private static int sId = 0; private final static String SIZE_SP = "_"; // 0000 0111 private static final int ERROR_CODE_MASK = 0x07; // error code, max 0x07 public final static int ERROR_NETWORK = 0x01; public final static int ERROR_BAD_FORMAT = 0x02; /** * bits: * 1 error-code * 2 error-code * 3 error-code * 4 loading * 5 pre-load */ private final static int STATUS_LOADING = 0x01 << 3; private final static int STATUS_PRE_LOAD = 0x02 << 3; private int mFlag = 0; protected int mId = 0; // the origin request url for the image. protected String mOriginUrl; // In some situations, we may store the same image in some different servers. So the same image will related to some different urls. private String mIdentityUrl; // The key related to the image this ImageTask is requesting. private String mIdentityKey; // cache for toString(); private String mStr; protected Point mRequestSize = new Point(); protected Point mBitmapOriginSize = new Point(); protected ImageLoadRequest mRequest; protected ImageViewHolder mFirstImageViewHolder; protected ImageTaskStatistics mImageTaskStatistics; ImageTask next; private boolean mHasRecycled = false; protected void clearForRecycle() { mHasRecycled = true; mFlag = 0; mOriginUrl = null; mIdentityUrl = null; mIdentityKey = null; mStr = null; mRequestSize.set(0, 0); mBitmapOriginSize.set(0, 0); mRequest = null; mFirstImageViewHolder = null; mImageTaskStatistics = null; } public static ImageTask obtain() { if (!USE_POOL) { return null; } // pop top, make top.next as top synchronized (sPoolSync) { if (sTop != null) { ImageTask m = sTop; sTop = m.next; m.next = null; sPoolSize--; m.mHasRecycled = false; if (CubeDebug.DEBUG_IMAGE) { CLog.d(LOG_TAG, "%s, obtain reused, pool remain: %d", m, sPoolSize); } return m; } } return null; } public void tryToRecycle() { if (!USE_POOL) { return; } clearForRecycle(); // mark top as the next of current, then push current as pop synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { next = sTop; sTop = this; sPoolSize++; if (CubeDebug.DEBUG_IMAGE) { CLog.d(LOG_TAG, "%s is put to recycle poll, pool size: %d", this, sPoolSize); } else { if (CubeDebug.DEBUG_IMAGE) { CLog.d(LOG_TAG, "%s is not recycled, the poll is full: %d", this, sPoolSize); } } } } } public ImageTask renewForRequest(ImageLoadRequest request) { if (CubeDebug.DEBUG_IMAGE) { int lastId = mId; mId = ++sId; CLog.d(LOG_TAG, "%s, renew: %s => %s", this, lastId, mId); } else { mId = ++sId; } mStr = null; if (ImagePerformanceStatistics.sample(mId)) { mImageTaskStatistics = new ImageTaskStatistics(); } mOriginUrl = request.getUrl(); mRequestSize.set(request.getRequestWidth(), request.getRequestHeight()); mRequest = request; return this; } public ImageTask setOriginUrl(String originUrl) { mOriginUrl = originUrl; return this; } public ImageTask setRequestSize(int requestWidth, int requestHeight) { mRequestSize.set(requestWidth, requestHeight); return this; } /** * For accessing the identity url * * @return */ public String getIdentityUrl() { if (null == mIdentityUrl) { mIdentityUrl = generateIdentityUrl(mOriginUrl); } return mIdentityUrl; } /** * In some situations, we may store the same image in some different servers. So the same image will related to some different urls. * Generate the identity url according your situation. * {@link #mIdentityUrl} * * @return * @deprecated Do not overwrite this method, use {@link in.srain.cube.image.iface.NameGenerator} instead. * should be used. */ @Deprecated protected String generateIdentityUrl(String originUrl) { return ImageLoaderFactory.getNameGenerator().generateIdentityUrlFor(mRequest); } /** * Generate the identity key. * <p/> * This key should be related to the unique image this task is requesting: the size, the remote url. * * @return */ protected String generateIdentityKey() { if (mRequest.getImageReuseInfo() == null) { return getIdentityUrl(); } else { return joinSizeTagToKey(getIdentityUrl(), mRequest.getImageReuseInfo().getIdentitySize()); } } public boolean isPreLoad() { return (mFlag & STATUS_PRE_LOAD) == STATUS_PRE_LOAD; } public void setIsPreLoad() { mFlag = mFlag | STATUS_PRE_LOAD; } public boolean isLoading() { return (mFlag & STATUS_LOADING) != 0; } /** * Check the given {@link in.srain.cube.image.ImageLoadRequest} is loading. * * @param request * @return Identify the given url, if same to {@link #mIdentityUrl} return true. */ public boolean isLoadingThisUrl(ImageLoadRequest request) { String url2 = ImageLoaderFactory.getNameGenerator().generateIdentityUrlFor(request); return getIdentityUrl().equals(url2); } /** * Bind ImageView with ImageTask * * @param imageView */ public void addImageView(CubeImageView imageView) { if (null == imageView) { return; } if (null == mFirstImageViewHolder) { mFirstImageViewHolder = new ImageViewHolder(imageView); return; } ImageViewHolder holder = mFirstImageViewHolder; for (; ; holder = holder.mNext) { if (holder.contains(imageView)) { return; } if (holder.mNext == null) { break; } } ImageViewHolder newHolder = new ImageViewHolder(imageView); newHolder.mPrev = holder; holder.mNext = newHolder; } /** * Remove the ImageView from ImageTask * * @param imageView */ public void removeImageView(CubeImageView imageView) { if (null == imageView || null == mFirstImageViewHolder) { return; } ImageViewHolder holder = mFirstImageViewHolder; do { if (holder.contains(imageView)) { // Make sure entry is right. if (holder == mFirstImageViewHolder) { mFirstImageViewHolder = holder.mNext; } if (null != holder.mNext) { holder.mNext.mPrev = holder.mPrev; } if (null != holder.mPrev) { holder.mPrev.mNext = holder.mNext; } } } while ((holder = holder.mNext) != null); } /** * Check if this ImageTask has any related ImageViews. * * @return */ public boolean stillHasRelatedImageView() { if (null == mFirstImageViewHolder || mFirstImageViewHolder.getImageView() == null) { return false; } else { return true; } } /** * When loading from network * * @param handler */ public void onLoading(ImageLoadHandler handler) { mFlag = mFlag | STATUS_LOADING; if (null == handler) { return; } if (mFirstImageViewHolder == null) { handler.onLoading(this, null); } else { ImageViewHolder holder = mFirstImageViewHolder; do { final CubeImageView imageView = holder.getImageView(); if (null != imageView) { handler.onLoading(this, imageView); } } while ((holder = holder.mNext) != null); } } /** * notify loading * * @param handler * @param imageView */ public void notifyLoading(ImageLoadHandler handler, CubeImageView imageView) { if (handler == null || imageView == null) { return; } handler.onLoading(this, imageView); } /** * Will be called when begin load image data from dish or network * * @param drawable */ public void onLoadTaskFinish(BitmapDrawable drawable, ImageLoadHandler handler) { mFlag &= ~STATUS_LOADING; if (null == handler) { return; } int errorCode = mFlag & ERROR_CODE_MASK; if (errorCode > 0) { onLoadError(errorCode, handler); return; } if (null != mImageTaskStatistics) { mImageTaskStatistics.s5_beforeShow(); } if (mFirstImageViewHolder == null) { handler.onLoadFinish(this, null, drawable); } else { ImageViewHolder holder = mFirstImageViewHolder; do { final CubeImageView imageView = holder.getImageView(); if (null != imageView) { imageView.onLoadFinish(); handler.onLoadFinish(this, imageView, drawable); } } while ((holder = holder.mNext) != null); } if (null != mImageTaskStatistics) { mImageTaskStatistics.s6_afterShow(ImageProvider.getBitmapSize(drawable)); ImagePerformanceStatistics.onImageLoaded(this, mImageTaskStatistics); } } public void onLoadTaskCancel() { } public void setError(int errorCode) { if (errorCode > ERROR_CODE_MASK) { throw new IllegalArgumentException("error code undefined."); } // clear old error flag mFlag = (mFlag & ~ERROR_CODE_MASK); // set current error flag mFlag |= errorCode; } private void onLoadError(int reason, ImageLoadHandler handler) { if (mFirstImageViewHolder == null) { handler.onLoadError(this, null, reason); } else { ImageViewHolder holder = mFirstImageViewHolder; do { final CubeImageView imageView = holder.getImageView(); if (null != imageView) { imageView.onLoadFinish(); handler.onLoadError(this, imageView, reason); } } while ((holder = holder.mNext) != null); } } /** * If you have a thumbnail web service which can return multiple size image according the url, * <p/> * you can implements this method to return the specified url according the request size. * * @return */ public String getRemoteUrl() { return mOriginUrl; } /** * Return the origin request url * * @return */ public String getOriginUrl() { return mOriginUrl; } public void setBitmapOriginSize(int width, int height) { mBitmapOriginSize.set(width, height); } public Point getBitmapOriginSize() { return mBitmapOriginSize; } public Point getRequestSize() { return mRequestSize; } /** * Return the key which identifies this Image Wrapper object. */ public String getIdentityKey() { if (mIdentityKey == null) { mIdentityKey = generateIdentityKey(); } return mIdentityKey; } /** * Join the key and the size information. * * @param key * @param w * @param h * @return "$key" + "_" + "$w" + "_" + "$h" */ public static String joinSizeInfoToKey(String key, int w, int h) { if (w > 0 && h != Integer.MAX_VALUE && h > 0 && h != Integer.MAX_VALUE) { return new StringBuilder(key).append(SIZE_SP).append(w).append(SIZE_SP).append(h).toString(); } return key; } /** * Join the tag with the key. * * @param key * @param tag * @return "$key" + "_" + "$tag" */ public static String joinSizeTagToKey(String key, String tag) { return new StringBuilder(key).append(SIZE_SP).append(tag).toString(); } /** * Return the cache key for file cache. * * @return the cache key for file cache. */ public String getFileCacheKey() { return Encrypt.md5(getIdentityKey()); } /** * @param sizeKey * @return */ public String generateFileCacheKeyForReuse(String sizeKey) { return Encrypt.md5(joinSizeTagToKey(getIdentityUrl(), sizeKey)); } @Override public boolean equals(Object object) { if (object != null && object instanceof ImageTask) { return ((ImageTask) object).getIdentityKey().equals(getIdentityKey()); } return false; } @Override public String toString() { if (mStr == null) { mStr = String.format("[ImageTask@%s %s %sx%s %s]", Integer.toHexString(hashCode()), mId, mRequestSize.x, mRequestSize.y, mHasRecycled); } return mStr; } public ImageTaskStatistics getStatistics() { return mImageTaskStatistics; } /** * A tiny and light linked-list like container to hold all the ImageViews related to the ImageTask. */ private static class ImageViewHolder { private WeakReference<CubeImageView> mImageViewRef; private ImageViewHolder mNext; private ImageViewHolder mPrev; public ImageViewHolder(CubeImageView imageView) { mImageViewRef = new WeakReference<CubeImageView>(imageView); } boolean contains(CubeImageView imageView) { return mImageViewRef != null && imageView == mImageViewRef.get(); } CubeImageView getImageView() { if (null == mImageViewRef) { return null; } return mImageViewRef.get(); } } public ImageLoadRequest getRequest() { return mRequest; } }