/*
* Copyright (C) 2006 The Android Open Source Project
*
* 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 android.widget;
import android.annotation.WorkerThread;
import android.content.Context;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.os.Handler;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.View;
import android.view.ViewGroup;
/**
* Adapter that exposes data from a {@link android.database.Cursor Cursor} to a
* {@link android.widget.ListView ListView} widget.
* <p>
* The Cursor must include a column named "_id" or this class will not work.
* Additionally, using {@link android.database.MergeCursor} with this class will
* not work if the merged Cursors have overlapping values in their "_id"
* columns.
*/
public abstract class CursorAdapter extends BaseAdapter implements Filterable,
CursorFilter.CursorFilterClient, ThemedSpinnerAdapter {
/**
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected boolean mDataValid;
/**
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected boolean mAutoRequery;
/**
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected Cursor mCursor;
/**
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected Context mContext;
/**
* Context used for {@link #getDropDownView(int, View, ViewGroup)}.
* {@hide}
*/
protected Context mDropDownContext;
/**
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected int mRowIDColumn;
/**
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected ChangeObserver mChangeObserver;
/**
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected DataSetObserver mDataSetObserver;
/**
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected CursorFilter mCursorFilter;
/**
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected FilterQueryProvider mFilterQueryProvider;
/**
* If set the adapter will call requery() on the cursor whenever a content change
* notification is delivered. Implies {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
*
* @deprecated This option is discouraged, as it results in Cursor queries
* being performed on the application's UI thread and thus can cause poor
* responsiveness or even Application Not Responding errors. As an alternative,
* use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
*/
@Deprecated
public static final int FLAG_AUTO_REQUERY = 0x01;
/**
* If set the adapter will register a content observer on the cursor and will call
* {@link #onContentChanged()} when a notification comes in. Be careful when
* using this flag: you will need to unset the current Cursor from the adapter
* to avoid leaks due to its registered observers. This flag is not needed
* when using a CursorAdapter with a
* {@link android.content.CursorLoader}.
*/
public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02;
/**
* Constructor that always enables auto-requery.
*
* @deprecated This option is discouraged, as it results in Cursor queries
* being performed on the application's UI thread and thus can cause poor
* responsiveness or even Application Not Responding errors. As an alternative,
* use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
*
* @param c The cursor from which to get the data.
* @param context The context
*/
@Deprecated
public CursorAdapter(Context context, Cursor c) {
init(context, c, FLAG_AUTO_REQUERY);
}
/**
* Constructor that allows control over auto-requery. It is recommended
* you not use this, but instead {@link #CursorAdapter(Context, Cursor, int)}.
* When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER}
* will always be set.
*
* @param c The cursor from which to get the data.
* @param context The context
* @param autoRequery If true the adapter will call requery() on the
* cursor whenever it changes so the most recent
* data is always displayed. Using true here is discouraged.
*/
public CursorAdapter(Context context, Cursor c, boolean autoRequery) {
init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
}
/**
* Recommended constructor.
*
* @param c The cursor from which to get the data.
* @param context The context
* @param flags Flags used to determine the behavior of the adapter; may
* be any combination of {@link #FLAG_AUTO_REQUERY} and
* {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
*/
public CursorAdapter(Context context, Cursor c, int flags) {
init(context, c, flags);
}
/**
* @deprecated Don't use this, use the normal constructor. This will
* be removed in the future.
*/
@Deprecated
protected void init(Context context, Cursor c, boolean autoRequery) {
init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
}
void init(Context context, Cursor c, int flags) {
if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) {
flags |= FLAG_REGISTER_CONTENT_OBSERVER;
mAutoRequery = true;
} else {
mAutoRequery = false;
}
boolean cursorPresent = c != null;
mCursor = c;
mDataValid = cursorPresent;
mContext = context;
mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
mChangeObserver = new ChangeObserver();
mDataSetObserver = new MyDataSetObserver();
} else {
mChangeObserver = null;
mDataSetObserver = null;
}
if (cursorPresent) {
if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);
}
}
/**
* Sets the {@link Resources.Theme} against which drop-down views are
* inflated.
* <p>
* By default, drop-down views are inflated against the theme of the
* {@link Context} passed to the adapter's constructor.
*
* @param theme the theme against which to inflate drop-down views or
* {@code null} to use the theme from the adapter's context
* @see #newDropDownView(Context, Cursor, ViewGroup)
*/
@Override
public void setDropDownViewTheme(Resources.Theme theme) {
if (theme == null) {
mDropDownContext = null;
} else if (theme == mContext.getTheme()) {
mDropDownContext = mContext;
} else {
mDropDownContext = new ContextThemeWrapper(mContext, theme);
}
}
@Override
public Resources.Theme getDropDownViewTheme() {
return mDropDownContext == null ? null : mDropDownContext.getTheme();
}
/**
* Returns the cursor.
* @return the cursor.
*/
public Cursor getCursor() {
return mCursor;
}
/**
* @see android.widget.ListAdapter#getCount()
*/
public int getCount() {
if (mDataValid && mCursor != null) {
return mCursor.getCount();
} else {
return 0;
}
}
/**
* @see android.widget.ListAdapter#getItem(int)
*/
public Object getItem(int position) {
if (mDataValid && mCursor != null) {
mCursor.moveToPosition(position);
return mCursor;
} else {
return null;
}
}
/**
* @see android.widget.ListAdapter#getItemId(int)
*/
public long getItemId(int position) {
if (mDataValid && mCursor != null) {
if (mCursor.moveToPosition(position)) {
return mCursor.getLong(mRowIDColumn);
} else {
return 0;
}
} else {
return 0;
}
}
@Override
public boolean hasStableIds() {
return true;
}
/**
* @see android.widget.ListAdapter#getView(int, View, ViewGroup)
*/
public View getView(int position, View convertView, ViewGroup parent) {
if (!mDataValid) {
throw new IllegalStateException("this should only be called when the cursor is valid");
}
if (!mCursor.moveToPosition(position)) {
throw new IllegalStateException("couldn't move cursor to position " + position);
}
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
@Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
if (mDataValid) {
final Context context = mDropDownContext == null ? mContext : mDropDownContext;
mCursor.moveToPosition(position);
final View v;
if (convertView == null) {
v = newDropDownView(context, mCursor, parent);
} else {
v = convertView;
}
bindView(v, context, mCursor);
return v;
} else {
return null;
}
}
/**
* Makes a new view to hold the data pointed to by cursor.
* @param context Interface to application's global information
* @param cursor The cursor from which to get the data. The cursor is already
* moved to the correct position.
* @param parent The parent to which the new view is attached to
* @return the newly created view.
*/
public abstract View newView(Context context, Cursor cursor, ViewGroup parent);
/**
* Makes a new drop down view to hold the data pointed to by cursor.
* @param context Interface to application's global information
* @param cursor The cursor from which to get the data. The cursor is already
* moved to the correct position.
* @param parent The parent to which the new view is attached to
* @return the newly created view.
*/
public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) {
return newView(context, cursor, parent);
}
/**
* Bind an existing view to the data pointed to by cursor
* @param view Existing view, returned earlier by newView
* @param context Interface to application's global information
* @param cursor The cursor from which to get the data. The cursor is already
* moved to the correct position.
*/
public abstract void bindView(View view, Context context, Cursor cursor);
/**
* 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();
}
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)
*/
@WorkerThread
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.
* The default implementation provides the auto-requery logic, but may be overridden by
* sub classes.
*
* @see ContentObserver#onChange(boolean)
*/
protected void onContentChanged() {
if (mAutoRequery && mCursor != null && !mCursor.isClosed()) {
if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update");
mDataValid = mCursor.requery();
}
}
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();
}
}
}