morristech
11/26/2014 - 12:59 PM

CursorRecyclerAdapter.java

package fr.shywim.tools.adapter;

import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.os.Handler;
import android.support.v7.widget.RecyclerView;
import android.widget.Filter;
import android.widget.FilterQueryProvider;
import android.widget.Filterable;

/**
 * Provide a {@link android.support.v7.widget.RecyclerView.Adapter} implementation with cursor
 * support.
 *
 * Child classes only need to implement {@link #onCreateViewHolder(android.view.ViewGroup, int)} and
 * {@link #onBindViewHolderCursor(android.support.v7.widget.RecyclerView.ViewHolder, android.database.Cursor)}.
 *
 * This class does not implement deprecated fields and methods from CursorAdapter! Incidentally,
 * only {@link android.widget.CursorAdapter#FLAG_REGISTER_CONTENT_OBSERVER} is available, so the
 * flag is implied, and only the Adapter behavior using this flag has been ported.
 *
 * @param <VH> {@inheritDoc}
 *
 * @see android.support.v7.widget.RecyclerView.Adapter
 * @see android.widget.CursorAdapter
 * @see android.widget.Filterable
 * @see fr.shywim.tools.adapter.CursorFilter.CursorFilterClient
 */
public abstract class CursorRecyclerAdapter<VH
		extends android.support.v7.widget.RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH>
implements Filterable, CursorFilter.CursorFilterClient {
	private boolean mDataValid;
	private int mRowIDColumn;
	private Cursor mCursor;
	private ChangeObserver mChangeObserver;
	private DataSetObserver mDataSetObserver;
	private CursorFilter mCursorFilter;
	private FilterQueryProvider mFilterQueryProvider;

	public CursorRecyclerAdapter( Cursor cursor) {
		init(cursor);
	}

	void init(Cursor c) {
		boolean cursorPresent = c != null;
		mCursor = c;
		mDataValid = cursorPresent;
		mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;

		mChangeObserver = new ChangeObserver();
		mDataSetObserver = new MyDataSetObserver();

		if (cursorPresent) {
			if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
			if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);
		}
	}

	/**
	 * This method will move the Cursor to the correct position and call
	 * {@link #onBindViewHolderCursor(android.support.v7.widget.RecyclerView.ViewHolder,
	 * android.database.Cursor)}.
	 * 
	 * @param holder {@inheritDoc}
	 * @param i {@inheritDoc}
	 */
	@Override
	public void onBindViewHolder(VH holder, int i){
		if (!mDataValid) {
			throw new IllegalStateException("this should only be called when the cursor is valid");
		}
		if (!mCursor.moveToPosition(i)) {
			throw new IllegalStateException("couldn't move cursor to position " + i);
		}
		onBindViewHolderCursor(holder, mCursor);
	}

	/**
	 * See {@link android.widget.CursorAdapter#bindView(android.view.View, android.content.Context,
	 * android.database.Cursor)},
	 * {@link #onBindViewHolder(android.support.v7.widget.RecyclerView.ViewHolder, int)}
	 *
	 * @param holder View holder.
	 * @param cursor The cursor from which to get the data. The cursor is already
	 * moved to the correct position.
	 */
	public abstract void onBindViewHolderCursor(VH holder, Cursor cursor);

	@Override
	public int getItemCount() {
		if (mDataValid && mCursor != null) {
			return mCursor.getCount();
		} else {
			return 0;
		}
	}

	/**
	 * @see android.widget.ListAdapter#getItemId(int)
	 */
	@Override
	public long getItemId(int position) {
		if (mDataValid && mCursor != null) {
			if (mCursor.moveToPosition(position)) {
				return mCursor.getLong(mRowIDColumn);
			} else {
				return 0;
			}
		} else {
			return 0;
		}
	}

	public Cursor getCursor(){
		return mCursor;
	}

	/**
	 * Change the underlying cursor to a new cursor. If there is an existing cursor it will be
	 * closed.
	 *
	 * @param cursor The new cursor to be used
	 */
	public void changeCursor(Cursor cursor) {
		Cursor old = swapCursor(cursor);
		if (old != null) {
			old.close();
		}
	}

	/**
	 * Swap in a new Cursor, returning the old Cursor.  Unlike
	 * {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
	 * closed.
	 *
	 * @param newCursor The new cursor to be used.
	 * @return Returns the previously set Cursor, or null if there wasa not one.
	 * If the given new Cursor is the same instance is the previously set
	 * Cursor, null is also returned.
	 */
	public Cursor swapCursor(Cursor newCursor) {
		if (newCursor == mCursor) {
			return null;
		}
		Cursor oldCursor = mCursor;
		if (oldCursor != null) {
			if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
			if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
		}
		mCursor = newCursor;
		if (newCursor != null) {
			if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
			if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
			mRowIDColumn = newCursor.getColumnIndexOrThrow("_id");
			mDataValid = true;
			// notify the observers about the new cursor
			notifyDataSetChanged();
		} else {
			mRowIDColumn = -1;
			mDataValid = false;
			// notify the observers about the lack of a data set
			// notifyDataSetInvalidated();
			notifyItemRangeRemoved(0, getItemCount());
		}
		return oldCursor;
	}

	/**
	 * <p>Converts the cursor into a CharSequence. Subclasses should override this
	 * method to convert their results. The default implementation returns an
	 * empty String for null values or the default String representation of
	 * the value.</p>
	 *
	 * @param cursor the cursor to convert to a CharSequence
	 * @return a CharSequence representing the value
	 */
	public CharSequence convertToString(Cursor cursor) {
		return cursor == null ? "" : cursor.toString();
	}

	/**
	 * Runs a query with the specified constraint. This query is requested
	 * by the filter attached to this adapter.
	 *
	 * The query is provided by a
	 * {@link android.widget.FilterQueryProvider}.
	 * If no provider is specified, the current cursor is not filtered and returned.
	 *
	 * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)}
	 * and the previous cursor is closed.
	 *
	 * This method is always executed on a background thread, not on the
	 * application's main thread (or UI thread.)
	 *
	 * Contract: when constraint is null or empty, the original results,
	 * prior to any filtering, must be returned.
	 *
	 * @param constraint the constraint with which the query must be filtered
	 *
	 * @return a Cursor representing the results of the new query
	 *
	 * @see #getFilter()
	 * @see #getFilterQueryProvider()
	 * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
	 */
	public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
		if (mFilterQueryProvider != null) {
			return mFilterQueryProvider.runQuery(constraint);
		}

		return mCursor;
	}

	public Filter getFilter() {
		if (mCursorFilter == null) {
			mCursorFilter = new CursorFilter(this);
		}
		return mCursorFilter;
	}

	/**
	 * Returns the query filter provider used for filtering. When the
	 * provider is null, no filtering occurs.
	 *
	 * @return the current filter query provider or null if it does not exist
	 *
	 * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
	 * @see #runQueryOnBackgroundThread(CharSequence)
	 */
	public FilterQueryProvider getFilterQueryProvider() {
		return mFilterQueryProvider;
	}

	/**
	 * Sets the query filter provider used to filter the current Cursor.
	 * The provider's
	 * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)}
	 * method is invoked when filtering is requested by a client of
	 * this adapter.
	 *
	 * @param filterQueryProvider the filter query provider or null to remove it
	 *
	 * @see #getFilterQueryProvider()
	 * @see #runQueryOnBackgroundThread(CharSequence)
	 */
	public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
		mFilterQueryProvider = filterQueryProvider;
	}

	/**
	 * Called when the {@link ContentObserver} on the cursor receives a change notification.
	 * Can be implemented by sub-class.
	 *
	 * @see ContentObserver#onChange(boolean)
	 */
	protected void onContentChanged() {

	}

	private class ChangeObserver extends ContentObserver {
		public ChangeObserver() {
			super(new Handler());
		}

		@Override
		public boolean deliverSelfNotifications() {
			return true;
		}

		@Override
		public void onChange(boolean selfChange) {
			onContentChanged();
		}
	}

	private class MyDataSetObserver extends DataSetObserver {
		@Override
		public void onChanged() {
			mDataValid = true;
			notifyDataSetChanged();
		}

		@Override
		public void onInvalidated() {
			mDataValid = false;
			// notifyDataSetInvalidated();
			notifyItemRangeRemoved(0, getItemCount());
		}
	}

	/**
	 * <p>The CursorFilter delegates most of the work to the CursorAdapter.
	 * Subclasses should override these delegate methods to run the queries
	 * and convert the results into String that can be used by auto-completion
	 * widgets.</p>
	 */

}

class CursorFilter extends Filter {

	CursorFilterClient mClient;

	interface CursorFilterClient {
		CharSequence convertToString(Cursor cursor);
		Cursor runQueryOnBackgroundThread(CharSequence constraint);
		Cursor getCursor();
		void changeCursor(Cursor cursor);
	}

	CursorFilter(CursorFilterClient client) {
		mClient = client;
	}

	@Override
	public CharSequence convertResultToString(Object resultValue) {
		return mClient.convertToString((Cursor) resultValue);
	}

	@Override
	protected FilterResults performFiltering(CharSequence constraint) {
		Cursor cursor = mClient.runQueryOnBackgroundThread(constraint);

		FilterResults results = new FilterResults();
		if (cursor != null) {
			results.count = cursor.getCount();
			results.values = cursor;
		} else {
			results.count = 0;
			results.values = null;
		}
		return results;
	}

	@Override
	protected void publishResults(CharSequence constraint, FilterResults results) {
		Cursor oldCursor = mClient.getCursor();

		if (results.values != null && results.values != oldCursor) {
			mClient.changeCursor((Cursor) results.values);
		}
	}
}