/*
* Copyright (C) 2012 Lucas Rocha
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lucasr.smoothie;
import java.lang.ref.SoftReference;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import android.os.Handler;
import android.os.Process;
import android.os.SystemClock;
import android.util.Log;
import android.view.View;
import android.widget.Adapter;
/**
* ItemLoader is responsible for loading and displaying items
* in {@link AsyncListView} or {@link AsyncGridView}. This is the class
* you should subclass to implement your app-specific item loading
* and displaying logic.
*
* <h2>Usage</h2>
* <p>ItemLoader must be subclassed to be used. The subclass will override four
* methods: {@link #loadItem(Object)}, @{@link #loadItemFromMemory(Object)},
* {@link #displayItem(View, Object, boolean)}, and
* {@link #getItemParams(Adapter, int)}.</p>
*
* <p>Here is an example of subclassing:</p>
* <pre>
* public class YourItemLoader extends ItemLoader<Long, Bitmap> {
* private final Context mContext;
* private final LruCache<Long, Bitmap> mMemCache;
*
* public YourItemLoader(Context context) {
* mContext = context;
*
* mMemCache = new LruCache<Long, Bitmap>(1024) {
* @Override
* protected int sizeOf(Long id, Bitmap bitmap) {
* return bitmap.getRowBytes() * bitmap.getHeight();
* }
* };
* }
*
* @Override
* public Long getItemParams(Adapter adapter, int position) {
* Cursor c = (Cursor) adapter.getItem(position);
* return c.getLong(c.getColumnIndex(ImageColumns._ID));
* }
*
* @Override
* public Bitmap loadItem(Long id) {
* Uri uri = Uri.withAppendedPath(Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id));
* Bitmap b = MediaStore.Images.Media.getBitmap(mContext.getContentResolver(), uri);
* if (b != null) {
* mMemCache.put(id, b);
* }
*
* return b;
* }
*
* @Override
* public Bitmap loadItemFromMemory(Long id) {
* return mMemCache.get(id);
* }
*
* @Override
* public void displayItem(View itemView, Bitmap result, boolean fromMemory) {
* ImageView image = (ImageView) itemView.findViewById(R.id.image);
* image.setImageBitmap(result);
* }
* }
* </pre>
*
* <p>The ItemLoader should be passed to {@link ItemManager.Builder Builder} constructor:</p>
* <pre>
* ItemManager.Builder = new ItemManager.Builder(new YourItemLoader(context));
* </pre>
*
* <h2>ItemLoader's generic types</h2>
* <p>The two types used by an ItemLoader are the following:</p>
* <ol>
* <li><code>Params</code>, the type of the parameters sent to {@link #loadItem(Object)}.</li>
* <li><code>Result</code>, the type of the result returned by {@link #loadItem(Object)}
* which will be sent to {@link #displayItem(View, Object, boolean)}.</li>
* </ol>
*
* <h2>The 4 steps</h2>
* <p>When an ItemLoader is in action, each item will go through 4 steps:</p>
* <ol>
* <li>{@link #getItemParams(Adapter, int)}, invoked on the UI thread before the
* item is loaded. This step should return all the parameters necessary for
* loading the item. This is necessary to avoid touching the Adapter in a
* background thread.</li>
* <li>{@link #loadItemFromMemory(Object)}, invoked on the UI thread before actually
* loading the item. If the item is already in memory, skip the next step and
* display the item immediately in the last step.</li>
* <li>{@link #loadItem(Object)}, invoked on a background thread. This call
* should return the item data that needs to be loaded asynchronously such
* as images or other online data.</li>
* <li>{@link #displayItem(View, Object, boolean)}, invoked on the UI thread
* after the item finishes loading.</li>
* </ol>
*
* <h2>ItemLoader and Adapter</h2>
* <p>Once you have an {@link ItemManager} set in an {@link AsyncListView} or
* {@link AsyncGridView}, your Adapter will behave exactly the same. In your
* {@link android.widget.Adapter #getView(int, View, android.view.ViewGroup)},
* you should display all the elements that are directly available from the
* Adapter's backing data. e.g. the backing data structure or database
* Cursor.</p>
*
* <p>The ItemLoader should handle the item data that needs to be loaded
* asynchronously in a background thread. e.g. downloading images from
* the cloud or loading files from disk.</p>
*
* <p>It's assumed that your
* {@link android.widget.Adapter #getView(int, View, android.view.ViewGroup)}
* will reset the item view to placeholder state regarding the data that
* the ItemLoader will load. For example, if your item has images that will
* be loaded asynchronously, your adapter should set the placeholder state
* in the target ImageView that will be shown until the image is actually
* loaded.</p>
*
* <h2>Other implementation notes</h2>
* <p>It's assumed that your implementation of {@link #loadItem(Object)}
* will result in the item data being cached in memory on success. Which
* means that a subsequent {@link #loadItemFromMemory(Object)} call will
* return the previously loaded item. You can easily implement memory
* caching using the Android support library's {@code LruCache}</p>
*
* @param <Params> - The parameters for loading an item.
* @param <Result> - The result of the item loading operation.
*
* @author Lucas Rocha <lucasr@lucasr.org>
*/
public abstract class ItemLoader<Params, Result> {
private static final String LOGTAG = "SmoothieItemLoader";
private static final boolean ENABLE_LOGGING = false;
private Handler mHandler;
private Map<View, ItemState<Params>> mItemStates;
private Map<Params, ItemRequest<Params, Result>> mItemRequests;
private ThreadPoolExecutor mExecutorService;
static final class ItemState<Params> {
public boolean shouldLoadItem;
public Params itemParams;
}
void init(Handler handler, int threadPoolSize) {
mHandler = handler;
mItemStates = Collections.synchronizedMap(new WeakHashMap<View, ItemState<Params>>());
mItemRequests = new ConcurrentHashMap<Params, ItemRequest<Params, Result>>(8, 0.9f, 1);
mExecutorService = new ItemsThreadPoolExecutor<Params, Result>(threadPoolSize, threadPoolSize, 60,
TimeUnit.SECONDS, new PriorityBlockingQueue<Runnable>());
}
void performDisplayItem(View itemView, long timestamp) {
ItemState<Params> itemState = getItemState(itemView);
if (!itemState.shouldLoadItem) {
if (ENABLE_LOGGING) {
Log.d(LOGTAG, "Item should not load, bailing: " + itemState.itemParams);
}
return;
}
Params itemParams = itemState.itemParams;
if (itemParams == null) {
if (ENABLE_LOGGING) {
Log.d(LOGTAG, "No item params, bailing: " + itemParams);
}
return;
}
ItemRequest<Params, Result> request = mItemRequests.get(itemParams);
if (request == null) {
if (ENABLE_LOGGING) {
Log.d(LOGTAG, "(Display) No pending item request, creating new: " + itemParams);
}
// No existing item request, create a new one
request = new ItemRequest<Params, Result>(itemView, itemParams, timestamp);
mItemRequests.put(itemParams, request);
} else {
if (ENABLE_LOGGING) {
Log.d(LOGTAG, "(Display) There's a pending item request, reusing: " + itemParams);
}
// There's a pending item request for these parameters, promote the
// existing request with higher priority. See LoadItemFutureTask
// for details on request priorities.
request.timestamp = timestamp;
request.itemView = new SoftReference<View>(itemView);
}
// We're actually running this item request, make sure
// this item is not requested again.
itemState.shouldLoadItem = false;
Result result = loadItemFromMemory(itemParams);
if (result != null) {
if (ENABLE_LOGGING) {
Log.d(LOGTAG, "Item is preloaded, quickly displaying");
}
cancelItemRequest(itemParams);
// The item is in memory, no need to asynchronously load it
// Run the final item display routine straight away.
request.result = new SoftReference<Result>(result);
mHandler.post(new DisplayItemRunnable<Params, Result>(this, request, true));
return;
}
request.loadItemTask = mExecutorService.submit(new LoadItemRunnable<Params, Result>(this, request));
}
void performLoadItem(View itemView, Adapter adapter, int position, boolean shouldDisplayItem) {
// Loader returned no parameters for the item, just bail
Params itemParams = getItemParams(adapter, position);
if (itemParams == null) {
return;
}
ItemState<Params> itemState = getItemState(itemView);
itemState.itemParams = itemParams;
// Mark the view for loading
itemState.shouldLoadItem = true;
if (shouldDisplayItem || isItemInMemory(itemParams)) {
performDisplayItem(itemView, SystemClock.uptimeMillis());
}
}
void performPreloadItem(Adapter adapter, int position, long timestamp) {
Params itemParams = getItemParams(adapter, position);
if (itemParams == null) {
return;
}
// If item is memory, just cancel any pending requests for
// this item and return as the item has already been loaded.
if (isItemInMemory(itemParams)) {
if (ENABLE_LOGGING) {
Log.d(LOGTAG, "Item is in memory, bailing: " + itemParams);
}
cancelItemRequest(itemParams);
return;
}
ItemRequest<Params, Result> request = mItemRequests.get(itemParams);
if (request == null) {
if (ENABLE_LOGGING) {
Log.d(LOGTAG, "(Preload) No pending item request, creating new: " + itemParams);
}
// No pending item preload request, create a new one
request = new ItemRequest<Params, Result>(itemParams, timestamp);
mItemRequests.put(itemParams, request);
request.loadItemTask = mExecutorService.submit(new LoadItemRunnable<Params, Result>(this, request));
} else {
if (ENABLE_LOGGING) {
Log.d(LOGTAG, "(Preload) There's a pending item request, reusing: " + itemParams);
}
// There's a pending item request for these parameters, demote the
// existing request with loader priority as it's just a preloading
// request. See LoadItemFutureTask for details on request priorities.
request.timestamp = timestamp;
request.itemView = null;
}
}
boolean isItemInMemory(Params itemParams) {
return (loadItemFromMemory(itemParams) != null);
}
void cancelObsoleteRequests(long timestamp) {
for (Iterator<ItemRequest<Params, Result>> i = mItemRequests.values().iterator(); i.hasNext();) {
ItemRequest<Params, Result> request = i.next();
if (request.timestamp < timestamp) {
if (ENABLE_LOGGING) {
Log.d(LOGTAG, "Cancelling obsolete request: " + request.itemParams);
}
if (request.loadItemTask != null) {
request.loadItemTask.cancel(true);
}
i.remove();
}
}
// Actually remove any cancelled tasks from the queue
mExecutorService.purge();
}
private ItemState<Params> getItemState(View itemView) {
ItemState<Params> itemState = mItemStates.get(itemView);
if (itemState == null) {
itemState = new ItemState<Params>();
itemState.itemParams = null;
itemState.shouldLoadItem = false;
mItemStates.put(itemView, itemState);
}
return itemState;
}
private void cancelItemRequest(Params itemParams) {
ItemRequest<Params, Result> request = mItemRequests.get(itemParams);
if (request == null) {
return;
}
mItemRequests.remove(itemParams);
if (request.loadItemTask != null) {
request.loadItemTask.cancel(true);
}
}
private boolean itemViewReused(ItemRequest<Params, Result> request) {
// If itemView is null, this means this is a preload request
// with no target view to display. No view to be possibly recycled
// in this case.
if (request.itemView == null) {
return false;
}
// If the request's soft reference to the view is now null, this means
// the view has been disposed from memory. Just bail.
View itemView = request.itemView.get();
if (itemView == null) {
return true;
}
// If the parameters associated with the view doesn't match the ones
// in the matching request, this means the view has been recycled to
// display something else.
final Params itemParams = getItemState(itemView).itemParams;
if (itemParams == null || !request.itemParams.equals(itemParams)) {
return true;
}
return false;
}
/**
* Retrieves the necessary parameters to load the item's data. This
* method is called in the UI thread.
*
* @param adapter - The {@link Adapter} associated with the target
* {@link AsyncListView} or {@link AsyncGridView}.
* @param position - The position in the Adapter from which the
* parameters should be retrieved.
*
* @return The parameters necessary to load an item which will be
* passed to {@link #loadItem(Object)}.
*/
public abstract Params getItemParams(Adapter adapter, int position);
/**
* Loads the item data. This method is called in a background thread.
* Hence you can make blocking calls (I/O, heavy computing) in your
* implementation.
*
* @param itemParams - The parameters generated by
* {@link #getItemParams(Adapter, int)}.
*
* @return The loaded item data.
*/
public abstract Result loadItem(Params itemParams);
/**
* Attempts to load the item data from memory. This method is called
* in the UI thread. In most implementations, this method will simply
* query a memory cache using the item parameters as a key.
*
* @param itemParams - The parameters generated by
* {@link #getItemParams(Adapter, int)}
*
* @return The cached item data.
*/
public abstract Result loadItemFromMemory(Params itemParams);
/**
* Displays the loaded item data in the target view. This method is called
* in the UI thread.
*
* @param itemView - The target item view returned by your Adapter's
* {@link android.widget.Adapter #getView(int, View, android.view.ViewGroup)}
* implementation.
* @param result - The item data loaded from {@link #loadItem(Object)} or
* {@link #loadItemFromMemory(Object)}.
* @param fromMemory - {@code True} if the item data has been loaded from
* {@link #loadItemFromMemory(Object)}. {@code False} if it has been
* loaded from {@link #loadItem(Object)}. This argument is usually used
* to skip animations when displaying preloaded items.
*/
public abstract void displayItem(View itemView, Result result, boolean fromMemory);
private static final class ItemRequest<Params, Result> {
public SoftReference<View> itemView;
public Params itemParams;
public SoftReference<Result> result;
public Long timestamp;
public Future<?> loadItemTask;
public ItemRequest(Params itemParams, long timestamp) {
this.itemView = null;
this.itemParams = itemParams;
this.result = new SoftReference<Result>(null);
this.timestamp = timestamp;
this.loadItemTask = null;
}
public ItemRequest(View itemView, Params itemParams, long timestamp) {
this.itemView = new SoftReference<View>(itemView);
this.itemParams = itemParams;
this.result = new SoftReference<Result>(null);
this.timestamp = timestamp;
this.loadItemTask = null;
}
}
private static final class ItemsThreadPoolExecutor<Params, Result> extends ThreadPoolExecutor {
public ItemsThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public Future<?> submit(Runnable task) {
if (task == null) {
throw new NullPointerException();
}
@SuppressWarnings("unchecked")
LoadItemFutureTask<Params, Result> ftask =
new LoadItemFutureTask<Params, Result>((LoadItemRunnable<Params, Result>) task);
execute(ftask);
return ftask;
}
}
private static final class LoadItemFutureTask<Params, Result> extends FutureTask<LoadItemRunnable<Params, Result>>
implements Comparable<LoadItemFutureTask<Params, Result>> {
private final LoadItemRunnable<Params, Result> mRunnable;
public LoadItemFutureTask(LoadItemRunnable<Params, Result> runnable) {
super(runnable, null);
mRunnable = runnable;
}
@Override
public int compareTo(LoadItemFutureTask<Params, Result> another) {
ItemRequest<Params, Result> r1 = mRunnable.getItemRequest();
ItemRequest<Params, Result> r2 = another.mRunnable.getItemRequest();
// A null itemView here means that the requests has no target view
// to display the loaded content, which means it's a preload request.
// Preloading requests always have lower priority than requests for items
// that are visible on screen. Request priorities are dynamically updated
// as the user scroll the list view. See performDisplayItem() and
// performPreloadItem() for details.
if (r1.itemView != null && r2.itemView == null) {
return -1;
} else if (r1.itemView == null && r2.itemView != null) {
return 1;
} else {
return r1.timestamp.compareTo(r2.timestamp);
}
}
}
private static final class LoadItemRunnable<Params, Result> implements Runnable {
private final ItemLoader<Params, Result> mItemLoader;
private final ItemRequest<Params, Result> mRequest;
public LoadItemRunnable(ItemLoader<Params, Result> itemLoader, ItemRequest<Params, Result> request) {
mItemLoader = itemLoader;
mRequest = request;
}
public ItemRequest<Params, Result> getItemRequest() {
return mRequest;
}
@Override
public void run() {
if (ENABLE_LOGGING) {
Log.d(LOGTAG, "Running: " + mRequest.itemParams);
}
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
mItemLoader.mItemRequests.remove(mRequest.itemParams);
if (mItemLoader.itemViewReused(mRequest)) {
return;
}
Result result = mItemLoader.loadItem(mRequest.itemParams);
mRequest.result = new SoftReference<Result>(result);
// If itemView is not null, this is a requests for an item
// that is currently visible on screen.
if (mRequest.itemView != null) {
if (ENABLE_LOGGING) {
Log.d(LOGTAG, "Done loading image: " + mRequest.itemParams);
}
if (mItemLoader.itemViewReused(mRequest)) {
return;
}
// Item is now loaded, run the display routine
mItemLoader.mHandler.post(new DisplayItemRunnable<Params, Result>(mItemLoader, mRequest, false));
} else {
// This is just a preload request, we're done here
if (ENABLE_LOGGING) {
Log.d(LOGTAG, "Done preloading: " + mRequest.itemParams);
}
}
}
}
private static final class DisplayItemRunnable<Params, Result> implements Runnable {
private final ItemLoader<Params, Result> mItemLoader;
private final ItemRequest<Params, Result> mRequest;
private final boolean mFromMemory;
public DisplayItemRunnable(ItemLoader<Params, Result> itemLoader,
ItemRequest<Params, Result> request, boolean fromMemory) {
mItemLoader = itemLoader;
mRequest = request;
mFromMemory = fromMemory;
}
@Override
public void run() {
View itemView = mRequest.itemView.get();
if (mItemLoader.itemViewReused(mRequest)) {
if (itemView != null) {
mItemLoader.getItemState(itemView).itemParams = null;
}
return;
}
Result result = mRequest.result.get();
if (result != null) {
mItemLoader.displayItem(itemView, result, mFromMemory);
if (itemView != null) {
mItemLoader.getItemState(itemView).itemParams = null;
}
}
}
}
}