2 * Copyright (C) 2007 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package android.widget;
19 import android.app.Activity;
20 import android.content.Context;
21 import android.database.ContentObserver;
22 import android.database.Cursor;
23 import android.database.DataSetObserver;
24 import android.os.Handler;
25 import android.util.Config;
26 import android.util.Log;
27 import android.util.SparseArray;
28 import android.view.View;
29 import android.view.ViewGroup;
32 * An adapter that exposes data from a series of {@link Cursor}s to an
33 * {@link ExpandableListView} widget. The top-level {@link Cursor} (that is
34 * given in the constructor) exposes the groups, while subsequent {@link Cursor}s
35 * returned from {@link #getChildrenCursor(Cursor)} expose children within a
36 * particular group. The Cursors must include a column named "_id" or this class
39 public abstract class CursorTreeAdapter extends BaseExpandableListAdapter implements Filterable,
40 CursorFilter.CursorFilterClient {
41 private Context mContext;
42 private Handler mHandler;
43 private boolean mAutoRequery;
45 /** The cursor helper that is used to get the groups */
46 MyCursorHelper mGroupCursorHelper;
49 * The map of a group position to the group's children cursor helper (the
50 * cursor helper that is used to get the children for that group)
52 SparseArray<MyCursorHelper> mChildrenCursorHelpers;
55 CursorFilter mCursorFilter;
56 FilterQueryProvider mFilterQueryProvider;
59 * Constructor. The adapter will call {@link Cursor#requery()} on the cursor whenever
60 * it changes so that the most recent data is always displayed.
62 * @param cursor The cursor from which to get the data for the groups.
64 public CursorTreeAdapter(Cursor cursor, Context context) {
65 init(cursor, context, true);
71 * @param cursor The cursor from which to get the data for the groups.
72 * @param context The context
73 * @param autoRequery If true the adapter will call {@link Cursor#requery()}
74 * on the cursor whenever it changes so the most recent data is
77 public CursorTreeAdapter(Cursor cursor, Context context, boolean autoRequery) {
78 init(cursor, context, autoRequery);
81 private void init(Cursor cursor, Context context, boolean autoRequery) {
83 mHandler = new Handler();
84 mAutoRequery = autoRequery;
86 mGroupCursorHelper = new MyCursorHelper(cursor);
87 mChildrenCursorHelpers = new SparseArray<MyCursorHelper>();
91 * Gets the cursor helper for the children in the given group.
93 * @param groupPosition The group whose children will be returned
94 * @param requestCursor Whether to request a Cursor via
95 * {@link #getChildrenCursor(Cursor)} (true), or to assume a call
96 * to {@link #setChildrenCursor(int, Cursor)} will happen shortly
98 * @return The cursor helper for the children of the given group
100 synchronized MyCursorHelper getChildrenCursorHelper(int groupPosition, boolean requestCursor) {
101 MyCursorHelper cursorHelper = mChildrenCursorHelpers.get(groupPosition);
103 if (cursorHelper == null) {
104 if (mGroupCursorHelper.moveTo(groupPosition) == null) return null;
106 final Cursor cursor = getChildrenCursor(mGroupCursorHelper.getCursor());
107 cursorHelper = new MyCursorHelper(cursor);
108 mChildrenCursorHelpers.put(groupPosition, cursorHelper);
115 * Gets the Cursor for the children at the given group. Subclasses must
116 * implement this method to return the children data for a particular group.
118 * If you want to asynchronously query a provider to prevent blocking the
119 * UI, it is possible to return null and at a later time call
120 * {@link #setChildrenCursor(int, Cursor)}.
122 * It is your responsibility to manage this Cursor through the Activity
123 * lifecycle. It is a good idea to use {@link Activity#managedQuery} which
124 * will handle this for you. In some situations, the adapter will deactivate
125 * the Cursor on its own, but this will not always be the case, so please
126 * ensure the Cursor is properly managed.
128 * @param groupCursor The cursor pointing to the group whose children cursor
130 * @return The cursor for the children of a particular group, or null.
132 abstract protected Cursor getChildrenCursor(Cursor groupCursor);
135 * Sets the group Cursor.
137 * @param cursor The Cursor to set for the group. If there is an existing cursor
140 public void setGroupCursor(Cursor cursor) {
141 mGroupCursorHelper.changeCursor(cursor, false);
145 * Sets the children Cursor for a particular group. If there is an existing cursor
148 * This is useful when asynchronously querying to prevent blocking the UI.
150 * @param groupPosition The group whose children are being set via this Cursor.
151 * @param childrenCursor The Cursor that contains the children of the group.
153 public void setChildrenCursor(int groupPosition, Cursor childrenCursor) {
156 * Don't request a cursor from the subclass, instead we will be setting
157 * the cursor ourselves.
159 MyCursorHelper childrenCursorHelper = getChildrenCursorHelper(groupPosition, false);
162 * Don't release any cursor since we know exactly what data is changing
163 * (this cursor, which is still valid).
165 childrenCursorHelper.changeCursor(childrenCursor, false);
168 public Cursor getChild(int groupPosition, int childPosition) {
169 // Return this group's children Cursor pointing to the particular child
170 return getChildrenCursorHelper(groupPosition, true).moveTo(childPosition);
173 public long getChildId(int groupPosition, int childPosition) {
174 return getChildrenCursorHelper(groupPosition, true).getId(childPosition);
177 public int getChildrenCount(int groupPosition) {
178 MyCursorHelper helper = getChildrenCursorHelper(groupPosition, true);
179 return (mGroupCursorHelper.isValid() && helper != null) ? helper.getCount() : 0;
182 public Cursor getGroup(int groupPosition) {
183 // Return the group Cursor pointing to the given group
184 return mGroupCursorHelper.moveTo(groupPosition);
187 public int getGroupCount() {
188 return mGroupCursorHelper.getCount();
191 public long getGroupId(int groupPosition) {
192 return mGroupCursorHelper.getId(groupPosition);
195 public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
197 Cursor cursor = mGroupCursorHelper.moveTo(groupPosition);
198 if (cursor == null) {
199 throw new IllegalStateException("this should only be called when the cursor is valid");
203 if (convertView == null) {
204 v = newGroupView(mContext, cursor, isExpanded, parent);
208 bindGroupView(v, mContext, cursor, isExpanded);
213 * Makes a new group view to hold the group data pointed to by cursor.
215 * @param context Interface to application's global information
216 * @param cursor The group cursor from which to get the data. The cursor is
217 * already moved to the correct position.
218 * @param isExpanded Whether the group is expanded.
219 * @param parent The parent to which the new view is attached to
220 * @return The newly created view.
222 protected abstract View newGroupView(Context context, Cursor cursor, boolean isExpanded,
226 * Bind an existing view to the group data pointed to by cursor.
228 * @param view Existing view, returned earlier by newGroupView.
229 * @param context Interface to application's global information
230 * @param cursor The cursor from which to get the data. The cursor is
231 * already moved to the correct position.
232 * @param isExpanded Whether the group is expanded.
234 protected abstract void bindGroupView(View view, Context context, Cursor cursor,
237 public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
238 View convertView, ViewGroup parent) {
239 MyCursorHelper cursorHelper = getChildrenCursorHelper(groupPosition, true);
241 Cursor cursor = cursorHelper.moveTo(childPosition);
242 if (cursor == null) {
243 throw new IllegalStateException("this should only be called when the cursor is valid");
247 if (convertView == null) {
248 v = newChildView(mContext, cursor, isLastChild, parent);
252 bindChildView(v, mContext, cursor, isLastChild);
257 * Makes a new child view to hold the data pointed to by cursor.
259 * @param context Interface to application's global information
260 * @param cursor The cursor from which to get the data. The cursor is
261 * already moved to the correct position.
262 * @param isLastChild Whether the child is the last child within its group.
263 * @param parent The parent to which the new view is attached to
264 * @return the newly created view.
266 protected abstract View newChildView(Context context, Cursor cursor, boolean isLastChild,
270 * Bind an existing view to the child data pointed to by cursor
272 * @param view Existing view, returned earlier by newChildView
273 * @param context Interface to application's global information
274 * @param cursor The cursor from which to get the data. The cursor is
275 * already moved to the correct position.
276 * @param isLastChild Whether the child is the last child within its group.
278 protected abstract void bindChildView(View view, Context context, Cursor cursor,
279 boolean isLastChild);
281 public boolean isChildSelectable(int groupPosition, int childPosition) {
285 public boolean hasStableIds() {
289 private synchronized void releaseCursorHelpers() {
290 for (int pos = mChildrenCursorHelpers.size() - 1; pos >= 0; pos--) {
291 mChildrenCursorHelpers.valueAt(pos).deactivate();
294 mChildrenCursorHelpers.clear();
298 public void notifyDataSetChanged() {
299 notifyDataSetChanged(true);
303 * Notifies a data set change, but with the option of not releasing any
306 * @param releaseCursors Whether to release and deactivate any cached
309 public void notifyDataSetChanged(boolean releaseCursors) {
311 if (releaseCursors) {
312 releaseCursorHelpers();
315 super.notifyDataSetChanged();
319 public void notifyDataSetInvalidated() {
320 releaseCursorHelpers();
321 super.notifyDataSetInvalidated();
325 public void onGroupCollapsed(int groupPosition) {
326 deactivateChildrenCursorHelper(groupPosition);
330 * Deactivates the Cursor and removes the helper from cache.
332 * @param groupPosition The group whose children Cursor and helper should be
335 synchronized void deactivateChildrenCursorHelper(int groupPosition) {
336 MyCursorHelper cursorHelper = getChildrenCursorHelper(groupPosition, true);
337 mChildrenCursorHelpers.remove(groupPosition);
338 cursorHelper.deactivate();
342 * @see CursorAdapter#convertToString(Cursor)
344 public String convertToString(Cursor cursor) {
345 return cursor == null ? "" : cursor.toString();
349 * @see CursorAdapter#runQueryOnBackgroundThread(CharSequence)
351 public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
352 if (mFilterQueryProvider != null) {
353 return mFilterQueryProvider.runQuery(constraint);
356 return mGroupCursorHelper.getCursor();
359 public Filter getFilter() {
360 if (mCursorFilter == null) {
361 mCursorFilter = new CursorFilter(this);
363 return mCursorFilter;
367 * @see CursorAdapter#getFilterQueryProvider()
369 public FilterQueryProvider getFilterQueryProvider() {
370 return mFilterQueryProvider;
374 * @see CursorAdapter#setFilterQueryProvider(FilterQueryProvider)
376 public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
377 mFilterQueryProvider = filterQueryProvider;
381 * @see CursorAdapter#changeCursor(Cursor)
383 public void changeCursor(Cursor cursor) {
384 mGroupCursorHelper.changeCursor(cursor, true);
388 * @see CursorAdapter#getCursor()
390 public Cursor getCursor() {
391 return mGroupCursorHelper.getCursor();
395 * Helper class for Cursor management:
397 * <li> Funneling the content and data set observers from a Cursor to a
398 * single data set observer for widgets
399 * <li> ID from the Cursor for use in adapter IDs
400 * <li> Swapping cursors but maintaining other metadata
402 class MyCursorHelper {
403 private Cursor mCursor;
404 private boolean mDataValid;
405 private int mRowIDColumn;
406 private MyContentObserver mContentObserver;
407 private MyDataSetObserver mDataSetObserver;
409 MyCursorHelper(Cursor cursor) {
410 final boolean cursorPresent = cursor != null;
412 mDataValid = cursorPresent;
413 mRowIDColumn = cursorPresent ? cursor.getColumnIndex("_id") : -1;
414 mContentObserver = new MyContentObserver();
415 mDataSetObserver = new MyDataSetObserver();
417 cursor.registerContentObserver(mContentObserver);
418 cursor.registerDataSetObserver(mDataSetObserver);
427 if (mDataValid && mCursor != null) {
428 return mCursor.getCount();
434 long getId(int position) {
435 if (mDataValid && mCursor != null) {
436 if (mCursor.moveToPosition(position)) {
437 return mCursor.getLong(mRowIDColumn);
446 Cursor moveTo(int position) {
447 if (mDataValid && (mCursor != null) && mCursor.moveToPosition(position)) {
454 void changeCursor(Cursor cursor, boolean releaseCursors) {
455 if (cursor == mCursor) return;
459 if (cursor != null) {
460 cursor.registerContentObserver(mContentObserver);
461 cursor.registerDataSetObserver(mDataSetObserver);
462 mRowIDColumn = cursor.getColumnIndex("_id");
464 // notify the observers about the new cursor
465 notifyDataSetChanged(releaseCursors);
469 // notify the observers about the lack of a data set
470 notifyDataSetInvalidated();
475 if (mCursor == null) {
479 mCursor.unregisterContentObserver(mContentObserver);
480 mCursor.unregisterDataSetObserver(mDataSetObserver);
486 return mDataValid && mCursor != null;
489 private class MyContentObserver extends ContentObserver {
490 public MyContentObserver() {
495 public boolean deliverSelfNotifications() {
500 public void onChange(boolean selfChange) {
501 if (mAutoRequery && mCursor != null) {
502 if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor +
504 mDataValid = mCursor.requery();
509 private class MyDataSetObserver extends DataSetObserver {
511 public void onChanged() {
513 notifyDataSetChanged();
517 public void onInvalidated() {
519 notifyDataSetInvalidated();