526 lines
18 KiB
Java
526 lines
18 KiB
Java
![]() |
/*
|
||
|
* 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.compat.annotation.UnsupportedAppUsage;
|
||
|
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}
|
||
|
*/
|
||
|
@UnsupportedAppUsage
|
||
|
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}
|
||
|
*/
|
||
|
@UnsupportedAppUsage
|
||
|
protected Cursor mCursor;
|
||
|
/**
|
||
|
* This field should be made private, so it is hidden from the SDK.
|
||
|
* {@hide}
|
||
|
*/
|
||
|
@UnsupportedAppUsage
|
||
|
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}
|
||
|
*/
|
||
|
@UnsupportedAppUsage
|
||
|
protected int mRowIDColumn;
|
||
|
/**
|
||
|
* This field should be made private, so it is hidden from the SDK.
|
||
|
* {@hide}
|
||
|
*/
|
||
|
@UnsupportedAppUsage
|
||
|
protected ChangeObserver mChangeObserver;
|
||
|
/**
|
||
|
* This field should be made private, so it is hidden from the SDK.
|
||
|
* {@hide}
|
||
|
*/
|
||
|
@UnsupportedAppUsage
|
||
|
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 was 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();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|