blob: 44d16564e5ce783afa5f42134f8280814630306c [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
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
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16
17package android.widget;
18
19import android.app.Activity;
20import android.content.Context;
21import android.database.ContentObserver;
22import android.database.Cursor;
23import android.database.DataSetObserver;
24import android.os.Handler;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080025import android.util.Log;
26import android.util.SparseArray;
27import android.view.View;
28import android.view.ViewGroup;
29
30/**
31 * An adapter that exposes data from a series of {@link Cursor}s to an
32 * {@link ExpandableListView} widget. The top-level {@link Cursor} (that is
33 * given in the constructor) exposes the groups, while subsequent {@link Cursor}s
34 * returned from {@link #getChildrenCursor(Cursor)} expose children within a
35 * particular group. The Cursors must include a column named "_id" or this class
36 * will not work.
37 */
38public abstract class CursorTreeAdapter extends BaseExpandableListAdapter implements Filterable,
39 CursorFilter.CursorFilterClient {
40 private Context mContext;
41 private Handler mHandler;
42 private boolean mAutoRequery;
43
44 /** The cursor helper that is used to get the groups */
45 MyCursorHelper mGroupCursorHelper;
46
47 /**
48 * The map of a group position to the group's children cursor helper (the
49 * cursor helper that is used to get the children for that group)
50 */
51 SparseArray<MyCursorHelper> mChildrenCursorHelpers;
52
53 // Filter related
54 CursorFilter mCursorFilter;
55 FilterQueryProvider mFilterQueryProvider;
56
57 /**
58 * Constructor. The adapter will call {@link Cursor#requery()} on the cursor whenever
59 * it changes so that the most recent data is always displayed.
60 *
61 * @param cursor The cursor from which to get the data for the groups.
62 */
63 public CursorTreeAdapter(Cursor cursor, Context context) {
64 init(cursor, context, true);
65 }
66
67 /**
68 * Constructor.
69 *
70 * @param cursor The cursor from which to get the data for the groups.
71 * @param context The context
72 * @param autoRequery If true the adapter will call {@link Cursor#requery()}
73 * on the cursor whenever it changes so the most recent data is
74 * always displayed.
75 */
76 public CursorTreeAdapter(Cursor cursor, Context context, boolean autoRequery) {
77 init(cursor, context, autoRequery);
78 }
79
80 private void init(Cursor cursor, Context context, boolean autoRequery) {
81 mContext = context;
82 mHandler = new Handler();
83 mAutoRequery = autoRequery;
84
85 mGroupCursorHelper = new MyCursorHelper(cursor);
86 mChildrenCursorHelpers = new SparseArray<MyCursorHelper>();
87 }
88
89 /**
90 * Gets the cursor helper for the children in the given group.
91 *
92 * @param groupPosition The group whose children will be returned
93 * @param requestCursor Whether to request a Cursor via
94 * {@link #getChildrenCursor(Cursor)} (true), or to assume a call
95 * to {@link #setChildrenCursor(int, Cursor)} will happen shortly
96 * (false).
97 * @return The cursor helper for the children of the given group
98 */
99 synchronized MyCursorHelper getChildrenCursorHelper(int groupPosition, boolean requestCursor) {
100 MyCursorHelper cursorHelper = mChildrenCursorHelpers.get(groupPosition);
101
102 if (cursorHelper == null) {
103 if (mGroupCursorHelper.moveTo(groupPosition) == null) return null;
104
105 final Cursor cursor = getChildrenCursor(mGroupCursorHelper.getCursor());
106 cursorHelper = new MyCursorHelper(cursor);
107 mChildrenCursorHelpers.put(groupPosition, cursorHelper);
108 }
109
110 return cursorHelper;
111 }
112
113 /**
114 * Gets the Cursor for the children at the given group. Subclasses must
115 * implement this method to return the children data for a particular group.
116 * <p>
117 * If you want to asynchronously query a provider to prevent blocking the
118 * UI, it is possible to return null and at a later time call
119 * {@link #setChildrenCursor(int, Cursor)}.
120 * <p>
121 * It is your responsibility to manage this Cursor through the Activity
122 * lifecycle. It is a good idea to use {@link Activity#managedQuery} which
123 * will handle this for you. In some situations, the adapter will deactivate
124 * the Cursor on its own, but this will not always be the case, so please
125 * ensure the Cursor is properly managed.
126 *
127 * @param groupCursor The cursor pointing to the group whose children cursor
128 * should be returned
129 * @return The cursor for the children of a particular group, or null.
130 */
131 abstract protected Cursor getChildrenCursor(Cursor groupCursor);
132
133 /**
134 * Sets the group Cursor.
135 *
Jason Parks4b08d3e2010-07-01 14:38:02 -0500136 * @param cursor The Cursor to set for the group. If there is an existing cursor
137 * it will be closed.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800138 */
139 public void setGroupCursor(Cursor cursor) {
140 mGroupCursorHelper.changeCursor(cursor, false);
141 }
142
143 /**
Jason Parks4b08d3e2010-07-01 14:38:02 -0500144 * Sets the children Cursor for a particular group. If there is an existing cursor
145 * it will be closed.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800146 * <p>
147 * This is useful when asynchronously querying to prevent blocking the UI.
148 *
149 * @param groupPosition The group whose children are being set via this Cursor.
150 * @param childrenCursor The Cursor that contains the children of the group.
151 */
152 public void setChildrenCursor(int groupPosition, Cursor childrenCursor) {
153
154 /*
155 * Don't request a cursor from the subclass, instead we will be setting
156 * the cursor ourselves.
157 */
158 MyCursorHelper childrenCursorHelper = getChildrenCursorHelper(groupPosition, false);
159
160 /*
161 * Don't release any cursor since we know exactly what data is changing
162 * (this cursor, which is still valid).
163 */
164 childrenCursorHelper.changeCursor(childrenCursor, false);
165 }
166
167 public Cursor getChild(int groupPosition, int childPosition) {
168 // Return this group's children Cursor pointing to the particular child
169 return getChildrenCursorHelper(groupPosition, true).moveTo(childPosition);
170 }
171
172 public long getChildId(int groupPosition, int childPosition) {
173 return getChildrenCursorHelper(groupPosition, true).getId(childPosition);
174 }
175
176 public int getChildrenCount(int groupPosition) {
177 MyCursorHelper helper = getChildrenCursorHelper(groupPosition, true);
178 return (mGroupCursorHelper.isValid() && helper != null) ? helper.getCount() : 0;
179 }
180
181 public Cursor getGroup(int groupPosition) {
182 // Return the group Cursor pointing to the given group
183 return mGroupCursorHelper.moveTo(groupPosition);
184 }
185
186 public int getGroupCount() {
187 return mGroupCursorHelper.getCount();
188 }
189
190 public long getGroupId(int groupPosition) {
191 return mGroupCursorHelper.getId(groupPosition);
192 }
193
194 public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
195 ViewGroup parent) {
196 Cursor cursor = mGroupCursorHelper.moveTo(groupPosition);
197 if (cursor == null) {
198 throw new IllegalStateException("this should only be called when the cursor is valid");
199 }
200
201 View v;
202 if (convertView == null) {
203 v = newGroupView(mContext, cursor, isExpanded, parent);
204 } else {
205 v = convertView;
206 }
207 bindGroupView(v, mContext, cursor, isExpanded);
208 return v;
209 }
210
211 /**
212 * Makes a new group view to hold the group data pointed to by cursor.
213 *
214 * @param context Interface to application's global information
215 * @param cursor The group cursor from which to get the data. The cursor is
216 * already moved to the correct position.
217 * @param isExpanded Whether the group is expanded.
218 * @param parent The parent to which the new view is attached to
219 * @return The newly created view.
220 */
221 protected abstract View newGroupView(Context context, Cursor cursor, boolean isExpanded,
222 ViewGroup parent);
223
224 /**
225 * Bind an existing view to the group data pointed to by cursor.
226 *
227 * @param view Existing view, returned earlier by newGroupView.
228 * @param context Interface to application's global information
229 * @param cursor The cursor from which to get the data. The cursor is
230 * already moved to the correct position.
231 * @param isExpanded Whether the group is expanded.
232 */
233 protected abstract void bindGroupView(View view, Context context, Cursor cursor,
234 boolean isExpanded);
235
236 public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
237 View convertView, ViewGroup parent) {
238 MyCursorHelper cursorHelper = getChildrenCursorHelper(groupPosition, true);
239
240 Cursor cursor = cursorHelper.moveTo(childPosition);
241 if (cursor == null) {
242 throw new IllegalStateException("this should only be called when the cursor is valid");
243 }
244
245 View v;
246 if (convertView == null) {
247 v = newChildView(mContext, cursor, isLastChild, parent);
248 } else {
249 v = convertView;
250 }
251 bindChildView(v, mContext, cursor, isLastChild);
252 return v;
253 }
254
255 /**
256 * Makes a new child view to hold the data pointed to by cursor.
257 *
258 * @param context Interface to application's global information
259 * @param cursor The cursor from which to get the data. The cursor is
260 * already moved to the correct position.
261 * @param isLastChild Whether the child is the last child within its group.
262 * @param parent The parent to which the new view is attached to
263 * @return the newly created view.
264 */
265 protected abstract View newChildView(Context context, Cursor cursor, boolean isLastChild,
266 ViewGroup parent);
267
268 /**
269 * Bind an existing view to the child data pointed to by cursor
270 *
271 * @param view Existing view, returned earlier by newChildView
272 * @param context Interface to application's global information
273 * @param cursor The cursor from which to get the data. The cursor is
274 * already moved to the correct position.
275 * @param isLastChild Whether the child is the last child within its group.
276 */
277 protected abstract void bindChildView(View view, Context context, Cursor cursor,
278 boolean isLastChild);
279
280 public boolean isChildSelectable(int groupPosition, int childPosition) {
281 return true;
282 }
283
284 public boolean hasStableIds() {
285 return true;
286 }
287
288 private synchronized void releaseCursorHelpers() {
289 for (int pos = mChildrenCursorHelpers.size() - 1; pos >= 0; pos--) {
290 mChildrenCursorHelpers.valueAt(pos).deactivate();
291 }
292
293 mChildrenCursorHelpers.clear();
294 }
295
296 @Override
297 public void notifyDataSetChanged() {
298 notifyDataSetChanged(true);
299 }
300
301 /**
302 * Notifies a data set change, but with the option of not releasing any
303 * cached cursors.
304 *
305 * @param releaseCursors Whether to release and deactivate any cached
306 * cursors.
307 */
308 public void notifyDataSetChanged(boolean releaseCursors) {
309
310 if (releaseCursors) {
311 releaseCursorHelpers();
312 }
313
314 super.notifyDataSetChanged();
315 }
316
317 @Override
318 public void notifyDataSetInvalidated() {
319 releaseCursorHelpers();
320 super.notifyDataSetInvalidated();
321 }
322
323 @Override
324 public void onGroupCollapsed(int groupPosition) {
325 deactivateChildrenCursorHelper(groupPosition);
326 }
327
328 /**
329 * Deactivates the Cursor and removes the helper from cache.
330 *
331 * @param groupPosition The group whose children Cursor and helper should be
332 * deactivated.
333 */
334 synchronized void deactivateChildrenCursorHelper(int groupPosition) {
335 MyCursorHelper cursorHelper = getChildrenCursorHelper(groupPosition, true);
336 mChildrenCursorHelpers.remove(groupPosition);
337 cursorHelper.deactivate();
338 }
339
340 /**
341 * @see CursorAdapter#convertToString(Cursor)
342 */
343 public String convertToString(Cursor cursor) {
344 return cursor == null ? "" : cursor.toString();
345 }
346
347 /**
348 * @see CursorAdapter#runQueryOnBackgroundThread(CharSequence)
349 */
350 public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
351 if (mFilterQueryProvider != null) {
352 return mFilterQueryProvider.runQuery(constraint);
353 }
354
355 return mGroupCursorHelper.getCursor();
356 }
357
358 public Filter getFilter() {
359 if (mCursorFilter == null) {
360 mCursorFilter = new CursorFilter(this);
361 }
362 return mCursorFilter;
363 }
364
365 /**
366 * @see CursorAdapter#getFilterQueryProvider()
367 */
368 public FilterQueryProvider getFilterQueryProvider() {
369 return mFilterQueryProvider;
370 }
371
372 /**
373 * @see CursorAdapter#setFilterQueryProvider(FilterQueryProvider)
374 */
375 public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
376 mFilterQueryProvider = filterQueryProvider;
377 }
378
379 /**
380 * @see CursorAdapter#changeCursor(Cursor)
381 */
382 public void changeCursor(Cursor cursor) {
383 mGroupCursorHelper.changeCursor(cursor, true);
384 }
385
386 /**
387 * @see CursorAdapter#getCursor()
388 */
389 public Cursor getCursor() {
390 return mGroupCursorHelper.getCursor();
391 }
392
393 /**
394 * Helper class for Cursor management:
395 * <li> Data validity
396 * <li> Funneling the content and data set observers from a Cursor to a
397 * single data set observer for widgets
398 * <li> ID from the Cursor for use in adapter IDs
399 * <li> Swapping cursors but maintaining other metadata
400 */
401 class MyCursorHelper {
402 private Cursor mCursor;
403 private boolean mDataValid;
404 private int mRowIDColumn;
405 private MyContentObserver mContentObserver;
406 private MyDataSetObserver mDataSetObserver;
407
408 MyCursorHelper(Cursor cursor) {
409 final boolean cursorPresent = cursor != null;
410 mCursor = cursor;
411 mDataValid = cursorPresent;
412 mRowIDColumn = cursorPresent ? cursor.getColumnIndex("_id") : -1;
413 mContentObserver = new MyContentObserver();
414 mDataSetObserver = new MyDataSetObserver();
415 if (cursorPresent) {
416 cursor.registerContentObserver(mContentObserver);
417 cursor.registerDataSetObserver(mDataSetObserver);
418 }
419 }
420
421 Cursor getCursor() {
422 return mCursor;
423 }
424
425 int getCount() {
426 if (mDataValid && mCursor != null) {
427 return mCursor.getCount();
428 } else {
429 return 0;
430 }
431 }
432
433 long getId(int position) {
434 if (mDataValid && mCursor != null) {
435 if (mCursor.moveToPosition(position)) {
436 return mCursor.getLong(mRowIDColumn);
437 } else {
438 return 0;
439 }
440 } else {
441 return 0;
442 }
443 }
444
445 Cursor moveTo(int position) {
446 if (mDataValid && (mCursor != null) && mCursor.moveToPosition(position)) {
447 return mCursor;
448 } else {
449 return null;
450 }
451 }
452
453 void changeCursor(Cursor cursor, boolean releaseCursors) {
454 if (cursor == mCursor) return;
455
456 deactivate();
457 mCursor = cursor;
458 if (cursor != null) {
459 cursor.registerContentObserver(mContentObserver);
460 cursor.registerDataSetObserver(mDataSetObserver);
461 mRowIDColumn = cursor.getColumnIndex("_id");
462 mDataValid = true;
463 // notify the observers about the new cursor
464 notifyDataSetChanged(releaseCursors);
465 } else {
466 mRowIDColumn = -1;
467 mDataValid = false;
468 // notify the observers about the lack of a data set
469 notifyDataSetInvalidated();
470 }
471 }
472
473 void deactivate() {
474 if (mCursor == null) {
475 return;
476 }
477
478 mCursor.unregisterContentObserver(mContentObserver);
479 mCursor.unregisterDataSetObserver(mDataSetObserver);
Jason Parks4b08d3e2010-07-01 14:38:02 -0500480 mCursor.close();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800481 mCursor = null;
482 }
483
484 boolean isValid() {
485 return mDataValid && mCursor != null;
486 }
487
488 private class MyContentObserver extends ContentObserver {
489 public MyContentObserver() {
490 super(mHandler);
491 }
492
493 @Override
494 public boolean deliverSelfNotifications() {
495 return true;
496 }
497
498 @Override
499 public void onChange(boolean selfChange) {
500 if (mAutoRequery && mCursor != null) {
Joe Onorato43a17652011-04-06 19:22:23 -0700501 if (false) Log.v("Cursor", "Auto requerying " + mCursor +
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800502 " due to update");
503 mDataValid = mCursor.requery();
504 }
505 }
506 }
507
508 private class MyDataSetObserver extends DataSetObserver {
509 @Override
510 public void onChanged() {
511 mDataValid = true;
512 notifyDataSetChanged();
513 }
514
515 @Override
516 public void onInvalidated() {
517 mDataValid = false;
518 notifyDataSetInvalidated();
519 }
520 }
521 }
522}