package org.devtcg.five.widget;
import java.util.HashSet;
import org.devtcg.five.Constants;
import org.devtcg.five.R;
import org.devtcg.five.provider.Five;
import org.devtcg.five.provider.util.AbstractDAOItem;
import org.devtcg.five.util.AsyncBitmapHandler;
import org.devtcg.five.util.LogUtils;
import org.devtcg.five.util.MemCache;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.util.Pair;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AlphabetIndexer;
import android.widget.FilterQueryProvider;
import android.widget.SectionIndexer;
/**
* Base list adapter used by the primary artist and album list screens. Provides
* the animated emblem loading logic and an alphabet fast scroller.
* <p>
* This could be factored into two separate classes, one more of a controller
* model and the other an actual list adapter but since only two screens use
* this currently I don't think it's worth the extra effort.
*/
public abstract class AbstractMainItemAdapter
<ItemHolder extends MainItemHolder, ItemDAO extends AbstractDAOItem>
extends AbstractDAOItemAdapter<ItemDAO> implements SectionIndexer
{
/**
* Animation duration when an image is loaded and displayed for the first
* time.
*/
private static final int ON_LOAD_FADE_IN_DURATION = 175;
private static final boolean DEBUG_BITMAP_LOADS =
LogUtils.isLoggable("BitmapLoads", Log.VERBOSE);
private final Context mContext;
private final AlphabetIndexer mIndexer;
private static final MemCache<Long, Bitmap> sBitmapCache =
new MemCache<Long, Bitmap>();
private final HashSet<View> mViewsMissingImagery = new HashSet<View>();
private final FetchMissingImageryHandler mHandler = new FetchMissingImageryHandler();
private AsyncBadgeLoader mBitmapLoader;
private int mScrollState;
public AbstractMainItemAdapter(Context context, int layout, FilterQueryProvider provider)
{
super(context, layout, provider.runQuery(null));
mContext = context;
setFilterQueryProvider(provider);
mIndexer = new AlphabetIndexer(getCursor(),
getCursor().getColumnIndexOrThrow(Five.Music.Artists.NAME),
context.getResources().getString(R.string.alphabet));
}
private void cleanupBackgroundOperations()
{
mHandler.cancelFetchMissingRequest();
if (mBitmapLoader != null) {
mBitmapLoader.cancelOperations();
}
}
@Override
public void changeCursor(Cursor cursor)
{
cleanupBackgroundOperations();
super.changeCursor(cursor);
mIndexer.setCursor(cursor);
}
public void dispatchScrollStateChanged(AbsListView view, int scrollState)
{
mScrollState = scrollState;
if (scrollState == AbstractMainListActivity.SCROLL_STATE_FLING)
cleanupBackgroundOperations();
else
mHandler.sendFetchMissingRequest();
}
public void dispatchOnResume()
{
mScrollState = AbstractMainListActivity.SCROLL_STATE_IDLE;
}
public void dispatchOnPause()
{
cleanupBackgroundOperations();
}
@Override
public void bindView(View view, Context context, Cursor cursor)
{
MainItemHolder holder = getHolder(view);
Uri photoUri = getCurrentRowBadgeUri();
holder.bindTo(mItemDAO.getId(), cursor.getPosition(), photoUri);
if (photoUri == null)
holder.badgeView.setImageResource(holder.defaultBadgeResource);
else
{
Bitmap bmp = sBitmapCache.get(mItemDAO.getId());
if (bmp != null)
{
holder.badgeView.setImageBitmap(bmp);
mViewsMissingImagery.remove(view);
}
else
{
holder.badgeTransition.resetTransition();
holder.badgeView.setImageDrawable(holder.badgeTransition);
mViewsMissingImagery.add(view);
if (mScrollState != AbstractMainListActivity.SCROLL_STATE_FLING)
mHandler.sendFetchMissingRequest();
}
}
holder.badgeNeedsRevealing = false;
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent)
{
View view = super.newView(context, cursor, parent);
view.setTag(newHolder(view));
return view;
}
protected abstract ItemHolder newHolder(View view);
@SuppressWarnings("unchecked")
protected final ItemHolder getHolder(View view)
{
return (ItemHolder)view.getTag();
}
protected abstract Uri getCurrentRowBadgeUri();
public int getPositionForSection(int section)
{
return mIndexer.getPositionForSection(section);
}
public int getSectionForPosition(int position)
{
return mIndexer.getSectionForPosition(position);
}
public Object[] getSections()
{
return mIndexer.getSections();
}
private AsyncBadgeLoader getOrCreateBitmapLoader()
{
if (mBitmapLoader == null)
mBitmapLoader = new AsyncBadgeLoader();
return mBitmapLoader;
}
private class FetchMissingImageryHandler extends Handler
{
private static final int MSG_FETCH_MISSING = 0;
/**
* Slight delay imposed to handle the first-time loading case where not
* flinging but where every item is going to call
* {@link #sendFetchMissingRequest()}. With a slight delay, we can
* ensure that under normal load all items fade-in at the same time.
*/
private static final int SHORT_FETCH_DELAY = 200;
@Override
public void handleMessage(Message msg)
{
switch (msg.what)
{
case MSG_FETCH_MISSING:
AsyncBadgeLoader bitmapLoader = getOrCreateBitmapLoader();
for (View row: mViewsMissingImagery)
{
ItemHolder holder = getHolder(row);
if (holder.badgeUri != null)
{
if (DEBUG_BITMAP_LOADS) {
Log.d(Constants.TAG, "Reading badge: " + holder.badgeUri);
}
bitmapLoader.startDecode(holder.position,
Pair.create(holder.id, row), holder.badgeUri);
}
}
/*
* Send a sort of termination request that we'll use to
* reflect updates to the UI only when all requests are
* processed. Prevents kind of a weird looking tile-loading
* problem that exists in the contacts app (which doesn't
* use this trick).
*/
bitmapLoader.sendSentinel();
break;
}
}
public void cancelFetchMissingRequest()
{
removeMessages(MSG_FETCH_MISSING);
}
public void sendFetchMissingRequest()
{
removeMessages(MSG_FETCH_MISSING);
if (!mViewsMissingImagery.isEmpty())
sendMessageDelayed(obtainMessage(MSG_FETCH_MISSING), SHORT_FETCH_DELAY);
}
}
private class AsyncBadgeLoader extends AsyncBitmapHandler
{
private static final int SENTINEL_TOKEN = -1;
public AsyncBadgeLoader()
{
super(mContext.getContentResolver());
}
public void sendSentinel()
{
startDecode(SENTINEL_TOKEN, null, null);
}
@SuppressWarnings("unchecked")
@Override
public void onDecodeComplete(int token, Object cookie, Bitmap result)
{
if (token == SENTINEL_TOKEN)
{
/*
* Loop through all views that may need revealing. Some of these
* items may have changed or may no longer need revealing so
* it's important to check that condition inside the loop.
*/
for (View row: mViewsMissingImagery)
{
ItemHolder holder = getHolder(row);
if (holder.badgeNeedsRevealing)
{
Bitmap bitmap = sBitmapCache.get(holder.id);
if (bitmap != null)
{
BitmapDrawable drawable = new BitmapDrawable(mContext.getResources(),
bitmap);
drawable.setBounds(holder.badgeTransition.getBounds());
holder.badgeTransition.setDrawableByLayerId(MainItemHolder.SECOND_LAYER_ID,
drawable);
holder.badgeTransition.startTransition(ON_LOAD_FADE_IN_DURATION);
}
holder.badgeNeedsRevealing = false;
}
}
mViewsMissingImagery.clear();
}
else
{
Pair<Long, View> data = (Pair<Long, View>)cookie;
long artistId = data.first;
View row = data.second;
ItemHolder holder = getHolder(row);
sBitmapCache.put(artistId, result);
if (holder.id == artistId)
holder.badgeNeedsRevealing = true;
}
}
}
}