package in.srain.cube.image; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import in.srain.cube.app.CubeFragment; import in.srain.cube.app.lifecycle.LifeCycleComponent; import in.srain.cube.app.lifecycle.LifeCycleComponentManager; import in.srain.cube.concurrent.SimpleTask; import in.srain.cube.image.iface.*; import in.srain.cube.util.CLog; import in.srain.cube.util.CubeDebug; import java.util.Iterator; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; /** * @author http://www.liaohuqiu.net */ public class ImageLoader implements LifeCycleComponent { // for LoadImageTask private static final Object sPoolSync = new Object(); private static LoadImageTask sTopLoadImageTask; private static int sPoolSize = 0; private static final int MAX_POOL_SIZE = 0; private static final String MSG_ATTACK_TO_RUNNING_TASK = "%s attach to running: %s"; private static final String MSG_TASK_DO_IN_BACKGROUND = "%s, %s LoadImageTask.doInBackground"; private static final String MSG_TASK_WAITING = "%s, %s LoadImageTask.waiting"; private static final String MSG_TASK_FINISH = "%s, %s LoadImageTask.onFinish, mExitTasksEarly? %s"; private static final String MSG_TASK_AFTER_fetchBitmapData = "%s, %s LoadImageTask.afterFetchBitmapData, canceled? %s"; private static final String MSG_TASK_CANCEL = "%s, %s LoadImageTask.onCancel"; private static final String MSG_TASK_RECYCLE = "%s, %s LoadImageTask.removeAndRecycle"; private static final String MSG_HIT_CACHE = "%s hit cache %s %s"; protected static final boolean DEBUG = CubeDebug.DEBUG_IMAGE; protected static final String LOG_TAG = CubeDebug.DEBUG_IMAGE_LOG_TAG; protected ImageTaskExecutor mImageTaskExecutor; protected ImageReSizer mImageReSizer; protected ImageProvider mImageProvider; protected ImageLoadHandler mImageLoadHandler; protected ImageLoadProgressHandler mLoadImageLoadProgressHandler; protected ImageDownloader mImageDownloader; protected boolean mPauseWork = false; protected boolean mExitTasksEarly = false; private final Object mPauseWorkLock = new Object(); private ConcurrentHashMap<String, LoadImageTask> mLoadWorkList; protected Context mContext; protected Resources mResources; protected boolean mHasBeenAddedToComponentManager = false; public static final int TASK_ORDER_FIRST_IN_FIRST_OUT = 1; public static final int TASK_ORDER_LAST_IN_FIRST_OUT = 2; public ImageLoader(Context context, ImageProvider imageProvider, ImageTaskExecutor imageTaskExecutor, ImageReSizer imageReSizer, ImageLoadHandler imageLoadHandler) { mContext = context; mResources = context.getResources(); mImageProvider = imageProvider; mImageTaskExecutor = imageTaskExecutor; mImageReSizer = imageReSizer; mImageLoadHandler = imageLoadHandler; mLoadWorkList = new ConcurrentHashMap<String, LoadImageTask>(); } public void setImageLoadHandler(ImageLoadHandler imageLoadHandler) { mImageLoadHandler = imageLoadHandler; } public ImageLoadHandler getImageLoadHandler() { return mImageLoadHandler; } public ImageDownloader getImageDownloader() { return mImageDownloader; } public void setImageDownloader(ImageDownloader imageDownloader) { mImageDownloader = imageDownloader; } public void setImageReSizer(ImageReSizer reSizer) { mImageReSizer = reSizer; } public ImageReSizer getImageReSizer() { return mImageReSizer; } public ImageProvider getImageProvider() { return mImageProvider; } /** * Load the image in advance. */ public void preLoadImages(String[] urls) { int len = urls.length; for (int i = 0; i < len; i++) { ImageLoadRequest request = new ImageLoadRequest(urls[i]); final ImageTask imageTask = createImageTask(request); imageTask.setIsPreLoad(); addImageTask(imageTask, null); } } /** * Create an ImageTask. * You can override this method to return a customized {@link in.srain.cube.image.ImageTask}. * * @param url * @param requestWidth * @param requestHeight * @param imageReuseInfo * @return */ @Deprecated public ImageTask createImageTask(String url, int requestWidth, int requestHeight, ImageReuseInfo imageReuseInfo) { ImageTask imageTask = ImageTask.obtain(); if (imageTask == null) { imageTask = new ImageTask(); } ImageLoadRequest imageLoadRequest = new ImageLoadRequest(url, requestWidth, requestHeight, -1, imageReuseInfo); imageTask.renewForRequest(imageLoadRequest); return imageTask; } public ImageTask createImageTask(ImageLoadRequest request) { ImageTask imageTask = ImageTask.obtain(); if (imageTask == null) { imageTask = new ImageTask(); } imageTask.renewForRequest(request); return imageTask; } /** * Detach the ImageView from the ImageTask. * * @param imageTask * @param imageView */ public void detachImageViewFromImageTask(ImageTask imageTask, CubeImageView imageView) { imageTask.removeImageView(imageView); if (imageTask.isLoading()) { if (!imageTask.isPreLoad() && !imageTask.stillHasRelatedImageView()) { LoadImageTask task = mLoadWorkList.get(imageTask.getIdentityKey()); if (task != null) { task.cancel(); } if (DEBUG) { CLog.d(LOG_TAG, "%s previous work is cancelled.", imageTask); } } } if (!imageTask.stillHasRelatedImageView()) { imageTask.tryToRecycle(); } } /** * Add the ImageTask into loading list. * * @param imageTask * @param imageView */ public void addImageTask(ImageTask imageTask, CubeImageView imageView) { if (!mHasBeenAddedToComponentManager) { CLog.w(LOG_TAG, "ImageLoader has not been add to a Component Manager.", this); } LoadImageTask runningTask = mLoadWorkList.get(imageTask.getIdentityKey()); if (runningTask != null) { if (imageView != null) { if (DEBUG) { CLog.d(LOG_TAG, MSG_ATTACK_TO_RUNNING_TASK, imageTask, runningTask.getImageTask()); } runningTask.getImageTask().addImageView(imageView); runningTask.getImageTask().notifyLoading(mImageLoadHandler, imageView); } return; } else { imageTask.addImageView(imageView); } imageTask.onLoading(mImageLoadHandler); LoadImageTask loadImageTask = createLoadImageTask(imageTask); mLoadWorkList.put(imageTask.getIdentityKey(), loadImageTask); mImageTaskExecutor.execute(loadImageTask); } /** * Check weather this imageTask has cache Drawable data. */ public boolean queryCache(ImageTask imageTask, CubeImageView imageView) { if (null == mImageProvider) { return false; } BitmapDrawable drawable = mImageProvider.getBitmapFromMemCache(imageTask); if (imageTask.getStatistics() != null) { imageTask.getStatistics().s0_afterCheckMemoryCache(drawable != null); } if (drawable == null) { return false; } if (DEBUG) { CLog.d(LOG_TAG, MSG_HIT_CACHE, imageTask, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); } imageTask.addImageView(imageView); imageTask.onLoadTaskFinish(drawable, mImageLoadHandler); return true; } /** * set task executed order: {@link @TASK_ORDER_LAST_IN_FIRST_OUT} or {@link #TASK_ORDER_FIRST_IN_FIRST_OUT} * * @param order */ @SuppressWarnings({"unused"}) public void setTaskOrder(int order) { if (null != mImageTaskExecutor) { mImageTaskExecutor.setTaskOrder(order); } } /** * flush un-cached image to disk */ public void flushFileCache() { if (mImageProvider != null) { mImageProvider.flushFileCache(); } } private LoadImageTask createLoadImageTask(ImageTask imageTask) { // pop top, make top.next as top synchronized (sPoolSync) { if (sTopLoadImageTask != null) { LoadImageTask m = sTopLoadImageTask; m.mNextImageTask = null; m.renew(this, imageTask); sTopLoadImageTask = m.mNextImageTask; sPoolSize--; return m; } } return new LoadImageTask().renew(this, imageTask); } /** * Inner class to process the image loading task in background threads. * <p/> * Memory required: * Shadow heap size: 24(Parent class, {@link SimpleTask}) + 4 * 3 = 36. Align to 40 bytes. * Retained heap size: 40 + 24(AtomicInteger introduced by {@link SimpleTask} = 64 bytes. * * @author http://www.liaohuqiu.net */ public static class LoadImageTask extends SimpleTask { private ImageTask mImageTask; private BitmapDrawable mDrawable; private LoadImageTask mNextImageTask; private ImageLoader mImageLoader; public ImageTask getImageTask() { return mImageTask; } public LoadImageTask renew(ImageLoader imageLoader, ImageTask imageTask) { mImageLoader = imageLoader; mImageTask = imageTask; restart(); return this; } @Override public void doInBackground() { if (DEBUG) { CLog.d(LOG_TAG, MSG_TASK_DO_IN_BACKGROUND, this, mImageTask); } if (mImageTask.getStatistics() != null) { mImageTask.getStatistics().s1_beginLoad(); } Bitmap bitmap = null; // Wait here if work is paused and the task is not cancelled synchronized (mImageLoader.mPauseWorkLock) { while (mImageLoader.mPauseWork && !isCancelled()) { try { if (DEBUG) { CLog.d(LOG_TAG, MSG_TASK_WAITING, this, mImageTask); } mImageLoader.mPauseWorkLock.wait(); } catch (InterruptedException e) { } } } // If this task has not been cancelled by another // thread and the ImageView that was originally bound to this task is still bound back // to this task and our "exit early" flag is not set then try and fetch the bitmap from // the cache if (!isCancelled() && !mImageLoader.mExitTasksEarly && (mImageTask.isPreLoad() || mImageTask.stillHasRelatedImageView())) { try { bitmap = mImageLoader.mImageProvider.fetchBitmapData(mImageLoader, mImageTask, mImageLoader.mImageReSizer); if (DEBUG) { CLog.d(LOG_TAG, MSG_TASK_AFTER_fetchBitmapData, this, mImageTask, isCancelled()); } mDrawable = mImageLoader.mImageProvider.createBitmapDrawable(mImageLoader.mResources, bitmap); mImageLoader.mImageProvider.addBitmapToMemCache(mImageTask.getIdentityKey(), mDrawable); } catch (Exception e) { e.printStackTrace(); } catch (OutOfMemoryError e) { e.printStackTrace(); } } } @Override public void onFinish(boolean canceled) { if (DEBUG) { CLog.d(LOG_TAG, MSG_TASK_FINISH, this, mImageTask, mImageLoader.mExitTasksEarly); } if (mImageLoader.mExitTasksEarly) { return; } if (!isCancelled() && !mImageLoader.mExitTasksEarly) { mImageTask.onLoadTaskFinish(mDrawable, mImageLoader.mImageLoadHandler); } mImageLoader.mLoadWorkList.remove(mImageTask.getIdentityKey()); } @Override public void onCancel() { if (DEBUG) { CLog.d(LOG_TAG, MSG_TASK_CANCEL, this, mImageTask); } mImageLoader.getImageProvider().cancelTask(mImageTask); mImageTask.onLoadTaskCancel(); mImageLoader.mLoadWorkList.remove(mImageTask.getIdentityKey()); } private void removeAndRecycle() { if (DEBUG) { CLog.d(LOG_TAG, MSG_TASK_RECYCLE, this, mImageTask); } // unlink mImageLoader = null; mImageTask = null; mDrawable = null; // mark top as the next of current, then push current as pop synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { mNextImageTask = sTopLoadImageTask; sTopLoadImageTask = this; sPoolSize++; } } } @Override public String toString() { return "[LoadImageTask" + '@' + Integer.toHexString(hashCode()) + ']'; } } private void setPause(boolean pause) { synchronized (mPauseWorkLock) { mPauseWork = pause; if (!pause) { mPauseWorkLock.notifyAll(); } } } /** * Temporarily hand up work, you can call this when the view is scrolling. */ public void pauseWork() { mExitTasksEarly = false; setPause(true); if (DEBUG) { CLog.d(LOG_TAG, "work_status: pauseWork %s", this); } } /** * Resume the work */ public void resumeWork() { mExitTasksEarly = false; setPause(false); if (DEBUG) { CLog.d(LOG_TAG, "work_status: resumeWork %s", this); } } /** * Recover the from the work list */ public void recoverWork() { if (DEBUG) { CLog.d(LOG_TAG, "work_status: recoverWork %s", this); } mExitTasksEarly = false; setPause(false); Iterator<Entry<String, LoadImageTask>> it = (Iterator<Entry<String, LoadImageTask>>) mLoadWorkList.entrySet().iterator(); while (it.hasNext()) { Entry<String, LoadImageTask> item = it.next(); LoadImageTask task = item.getValue(); task.restart(); mImageTaskExecutor.execute(task); } } /** * Drop all the work, and leave it in the work list. */ public void stopWork() { if (DEBUG) { CLog.d(LOG_TAG, "work_status: stopWork %s", this); } mExitTasksEarly = true; setPause(false); flushFileCache(); } /** * Drop all the work, clear the work list. */ public void destroy() { if (DEBUG) { CLog.d(LOG_TAG, "work_status: destroy %s", this); } mExitTasksEarly = true; setPause(false); Iterator<Entry<String, LoadImageTask>> it = (Iterator<Entry<String, LoadImageTask>>) mLoadWorkList.entrySet().iterator(); while (it.hasNext()) { Entry<String, LoadImageTask> item = it.next(); final LoadImageTask task = item.getValue(); it.remove(); if (task != null) { task.cancel(); } } mLoadWorkList.clear(); } /** * The UI becomes partially invisible. * like {@link android.app.Activity#onPause} */ @Override public void onBecomesPartiallyInvisible() { pauseWork(); } /** * The UI becomes visible from partially invisible. * like {@link android.app.Activity#onResume} */ @Override public void onBecomesVisible() { resumeWork(); } /** * The UI becomes totally invisible. * like {@link android.app.Activity#onStop} */ @Override public void onBecomesTotallyInvisible() { stopWork(); } /** * The UI becomes visible from totally invisible. * like {@link android.app.Activity#onRestart} */ @Override public void onBecomesVisibleFromTotallyInvisible() { recoverWork(); } /** * like {@link android.app.Activity#onDestroy} */ @Override public void onDestroy() { destroy(); } /** * try to attach to {@link in.srain.cube.app.lifecycle.IComponentContainer} */ public ImageLoader tryToAttachToContainer(Object object) { tryToAttachToContainer(object, true); return this; } /** * try to attach to {@link in.srain.cube.app.lifecycle.IComponentContainer} */ public ImageLoader tryToAttachToContainer(Object object, boolean throwEx) { if (LifeCycleComponentManager.tryAddComponentToContainer(this, object, throwEx)) { mHasBeenAddedToComponentManager = true; } return this; } /** * LiefCycle phase will be same to CubeFragment, an will be processed automatically. * * @param fragment * @return */ public ImageLoader attachToCubeFragment(CubeFragment fragment) { if (fragment != null) { if (LifeCycleComponentManager.tryAddComponentToContainer(this, fragment, true)) { mHasBeenAddedToComponentManager = true; } } return this; } }