| /* |
| * 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(); |
| } |
| } |
| } |
| } |