/*
* Copyright (C) 2007 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.app.Activity;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.os.Handler;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
/**
* An adapter that exposes data from a series of {@link Cursor}s to an
* {@link ExpandableListView} widget. The top-level {@link Cursor} (that is
* given in the constructor) exposes the groups, while subsequent {@link Cursor}s
* returned from {@link #getChildrenCursor(Cursor)} expose children within a
* particular group. The Cursors must include a column named "_id" or this class
* will not work.
*/
public abstract class CursorTreeAdapter extends BaseExpandableListAdapter implements Filterable,
CursorFilter.CursorFilterClient {
private Context mContext;
private Handler mHandler;
private boolean mAutoRequery;
/** The cursor helper that is used to get the groups */
MyCursorHelper mGroupCursorHelper;
/**
* The map of a group position to the group's children cursor helper (the
* cursor helper that is used to get the children for that group)
*/
SparseArray<MyCursorHelper> mChildrenCursorHelpers;
// Filter related
CursorFilter mCursorFilter;
FilterQueryProvider mFilterQueryProvider;
/**
* Constructor. The adapter will call {@link Cursor#requery()} on the cursor whenever
* it changes so that the most recent data is always displayed.
*
* @param cursor The cursor from which to get the data for the groups.
*/
public CursorTreeAdapter(Cursor cursor, Context context) {
init(cursor, context, true);
}
/**
* Constructor.
*
* @param cursor The cursor from which to get the data for the groups.
* @param context The context
* @param autoRequery If true the adapter will call {@link Cursor#requery()}
* on the cursor whenever it changes so the most recent data is
* always displayed.
*/
public CursorTreeAdapter(Cursor cursor, Context context, boolean autoRequery) {
init(cursor, context, autoRequery);
}
private void init(Cursor cursor, Context context, boolean autoRequery) {
mContext = context;
mHandler = new Handler();
mAutoRequery = autoRequery;
mGroupCursorHelper = new MyCursorHelper(cursor);
mChildrenCursorHelpers = new SparseArray<MyCursorHelper>();
}
/**
* Gets the cursor helper for the children in the given group.
*
* @param groupPosition The group whose children will be returned
* @param requestCursor Whether to request a Cursor via
* {@link #getChildrenCursor(Cursor)} (true), or to assume a call
* to {@link #setChildrenCursor(int, Cursor)} will happen shortly
* (false).
* @return The cursor helper for the children of the given group
*/
synchronized MyCursorHelper getChildrenCursorHelper(int groupPosition, boolean requestCursor) {
MyCursorHelper cursorHelper = mChildrenCursorHelpers.get(groupPosition);
if (cursorHelper == null) {
if (mGroupCursorHelper.moveTo(groupPosition) == null) return null;
final Cursor cursor = getChildrenCursor(mGroupCursorHelper.getCursor());
cursorHelper = new MyCursorHelper(cursor);
mChildrenCursorHelpers.put(groupPosition, cursorHelper);
}
return cursorHelper;
}
/**
* Gets the Cursor for the children at the given group. Subclasses must
* implement this method to return the children data for a particular group.
* <p>
* If you want to asynchronously query a provider to prevent blocking the
* UI, it is possible to return null and at a later time call
* {@link #setChildrenCursor(int, Cursor)}.
* <p>
* It is your responsibility to manage this Cursor through the Activity
* lifecycle. It is a good idea to use {@link Activity#managedQuery} which
* will handle this for you. In some situations, the adapter will deactivate
* the Cursor on its own, but this will not always be the case, so please
* ensure the Cursor is properly managed.
*
* @param groupCursor The cursor pointing to the group whose children cursor
* should be returned
* @return The cursor for the children of a particular group, or null.
*/
abstract protected Cursor getChildrenCursor(Cursor groupCursor);
/**
* Sets the group Cursor.
*
* @param cursor The Cursor to set for the group. If there is an existing cursor
* it will be closed.
*/
public void setGroupCursor(Cursor cursor) {
mGroupCursorHelper.changeCursor(cursor, false);
}
/**
* Sets the children Cursor for a particular group. If there is an existing cursor
* it will be closed.
* <p>
* This is useful when asynchronously querying to prevent blocking the UI.
*
* @param groupPosition The group whose children are being set via this Cursor.
* @param childrenCursor The Cursor that contains the children of the group.
*/
public void setChildrenCursor(int groupPosition, Cursor childrenCursor) {
/*
* Don't request a cursor from the subclass, instead we will be setting
* the cursor ourselves.
*/
MyCursorHelper childrenCursorHelper = getChildrenCursorHelper(groupPosition, false);
/*
* Don't release any cursor since we know exactly what data is changing
* (this cursor, which is still valid).
*/
childrenCursorHelper.changeCursor(childrenCursor, false);
}
public Cursor getChild(int groupPosition, int childPosition) {
// Return this group's children Cursor pointing to the particular child
return getChildrenCursorHelper(groupPosition, true).moveTo(childPosition);
}
public long getChildId(int groupPosition, int childPosition) {
return getChildrenCursorHelper(groupPosition, true).getId(childPosition);
}
public int getChildrenCount(int groupPosition) {
MyCursorHelper helper = getChildrenCursorHelper(groupPosition, true);
return (mGroupCursorHelper.isValid() && helper != null) ? helper.getCount() : 0;
}
public Cursor getGroup(int groupPosition) {
// Return the group Cursor pointing to the given group
return mGroupCursorHelper.moveTo(groupPosition);
}
public int getGroupCount() {
return mGroupCursorHelper.getCount();
}
public long getGroupId(int groupPosition) {
return mGroupCursorHelper.getId(groupPosition);
}
public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
ViewGroup parent) {
Cursor cursor = mGroupCursorHelper.moveTo(groupPosition);
if (cursor == null) {
throw new IllegalStateException("this should only be called when the cursor is valid");
}
View v;
if (convertView == null) {
v = newGroupView(mContext, cursor, isExpanded, parent);
} else {
v = convertView;
}
bindGroupView(v, mContext, cursor, isExpanded);
return v;
}
/**
* Makes a new group view to hold the group data pointed to by cursor.
*
* @param context Interface to application's global information
* @param cursor The group cursor from which to get the data. The cursor is
* already moved to the correct position.
* @param isExpanded Whether the group is expanded.
* @param parent The parent to which the new view is attached to
* @return The newly created view.
*/
protected abstract View newGroupView(Context context, Cursor cursor, boolean isExpanded,
ViewGroup parent);
/**
* Bind an existing view to the group data pointed to by cursor.
*
* @param view Existing view, returned earlier by newGroupView.
* @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 isExpanded Whether the group is expanded.
*/
protected abstract void bindGroupView(View view, Context context, Cursor cursor,
boolean isExpanded);
public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
View convertView, ViewGroup parent) {
MyCursorHelper cursorHelper = getChildrenCursorHelper(groupPosition, true);
Cursor cursor = cursorHelper.moveTo(childPosition);
if (cursor == null) {
throw new IllegalStateException("this should only be called when the cursor is valid");
}
View v;
if (convertView == null) {
v = newChildView(mContext, cursor, isLastChild, parent);
} else {
v = convertView;
}
bindChildView(v, mContext, cursor, isLastChild);
return v;
}
/**
* Makes a new child 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 isLastChild Whether the child is the last child within its group.
* @param parent The parent to which the new view is attached to
* @return the newly created view.
*/
protected abstract View newChildView(Context context, Cursor cursor, boolean isLastChild,
ViewGroup parent);
/**
* Bind an existing view to the child data pointed to by cursor
*
* @param view Existing view, returned earlier by newChildView
* @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 isLastChild Whether the child is the last child within its group.
*/
protected abstract void bindChildView(View view, Context context, Cursor cursor,
boolean isLastChild);
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
public boolean hasStableIds() {
return true;
}
private synchronized void releaseCursorHelpers() {
for (int pos = mChildrenCursorHelpers.size() - 1; pos >= 0; pos--) {
mChildrenCursorHelpers.valueAt(pos).deactivate();
}
mChildrenCursorHelpers.clear();
}
@Override
public void notifyDataSetChanged() {
notifyDataSetChanged(true);
}
/**
* Notifies a data set change, but with the option of not releasing any
* cached cursors.
*
* @param releaseCursors Whether to release and deactivate any cached
* cursors.
*/
public void notifyDataSetChanged(boolean releaseCursors) {
if (releaseCursors) {
releaseCursorHelpers();
}
super.notifyDataSetChanged();
}
@Override
public void notifyDataSetInvalidated() {
releaseCursorHelpers();
super.notifyDataSetInvalidated();
}
@Override
public void onGroupCollapsed(int groupPosition) {
deactivateChildrenCursorHelper(groupPosition);
}
/**
* Deactivates the Cursor and removes the helper from cache.
*
* @param groupPosition The group whose children Cursor and helper should be
* deactivated.
*/
synchronized void deactivateChildrenCursorHelper(int groupPosition) {
MyCursorHelper cursorHelper = getChildrenCursorHelper(groupPosition, true);
mChildrenCursorHelpers.remove(groupPosition);
cursorHelper.deactivate();
}
/**
* @see CursorAdapter#convertToString(Cursor)
*/
public String convertToString(Cursor cursor) {
return cursor == null ? "" : cursor.toString();
}
/**
* @see CursorAdapter#runQueryOnBackgroundThread(CharSequence)
*/
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
if (mFilterQueryProvider != null) {
return mFilterQueryProvider.runQuery(constraint);
}
return mGroupCursorHelper.getCursor();
}
public Filter getFilter() {
if (mCursorFilter == null) {
mCursorFilter = new CursorFilter(this);
}
return mCursorFilter;
}
/**
* @see CursorAdapter#getFilterQueryProvider()
*/
public FilterQueryProvider getFilterQueryProvider() {
return mFilterQueryProvider;
}
/**
* @see CursorAdapter#setFilterQueryProvider(FilterQueryProvider)
*/
public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
mFilterQueryProvider = filterQueryProvider;
}
/**
* @see CursorAdapter#changeCursor(Cursor)
*/
public void changeCursor(Cursor cursor) {
mGroupCursorHelper.changeCursor(cursor, true);
}
/**
* @see CursorAdapter#getCursor()
*/
public Cursor getCursor() {
return mGroupCursorHelper.getCursor();
}
/**
* Helper class for Cursor management:
* <li> Data validity
* <li> Funneling the content and data set observers from a Cursor to a
* single data set observer for widgets
* <li> ID from the Cursor for use in adapter IDs
* <li> Swapping cursors but maintaining other metadata
*/
class MyCursorHelper {
private Cursor mCursor;
private boolean mDataValid;
private int mRowIDColumn;
private MyContentObserver mContentObserver;
private MyDataSetObserver mDataSetObserver;
MyCursorHelper(Cursor cursor) {
final boolean cursorPresent = cursor != null;
mCursor = cursor;
mDataValid = cursorPresent;
mRowIDColumn = cursorPresent ? cursor.getColumnIndex("_id") : -1;
mContentObserver = new MyContentObserver();
mDataSetObserver = new MyDataSetObserver();
if (cursorPresent) {
cursor.registerContentObserver(mContentObserver);
cursor.registerDataSetObserver(mDataSetObserver);
}
}
Cursor getCursor() {
return mCursor;
}
int getCount() {
if (mDataValid && mCursor != null) {
return mCursor.getCount();
} else {
return 0;
}
}
long getId(int position) {
if (mDataValid && mCursor != null) {
if (mCursor.moveToPosition(position)) {
return mCursor.getLong(mRowIDColumn);
} else {
return 0;
}
} else {
return 0;
}
}
Cursor moveTo(int position) {
if (mDataValid && (mCursor != null) && mCursor.moveToPosition(position)) {
return mCursor;
} else {
return null;
}
}
void changeCursor(Cursor cursor, boolean releaseCursors) {
if (cursor == mCursor) return;
deactivate();
mCursor = cursor;
if (cursor != null) {
cursor.registerContentObserver(mContentObserver);
cursor.registerDataSetObserver(mDataSetObserver);
mRowIDColumn = cursor.getColumnIndex("_id");
mDataValid = true;
// notify the observers about the new cursor
notifyDataSetChanged(releaseCursors);
} else {
mRowIDColumn = -1;
mDataValid = false;
// notify the observers about the lack of a data set
notifyDataSetInvalidated();
}
}
void deactivate() {
if (mCursor == null) {
return;
}
mCursor.unregisterContentObserver(mContentObserver);
mCursor.unregisterDataSetObserver(mDataSetObserver);
mCursor.close();
mCursor = null;
}
boolean isValid() {
return mDataValid && mCursor != null;
}
private class MyContentObserver extends ContentObserver {
public MyContentObserver() {
super(mHandler);
}
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public void onChange(boolean selfChange) {
if (mAutoRequery && mCursor != null && !mCursor.isClosed()) {
if (false) Log.v("Cursor", "Auto requerying " + mCursor +
" due to update");
mDataValid = mCursor.requery();
}
}
}
private class MyDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
mDataValid = true;
notifyDataSetChanged();
}
@Override
public void onInvalidated() {
mDataValid = false;
notifyDataSetInvalidated();
}
}
}
}