blob: 3fadf4c54075a7671d95dcbe751a7c5d054e27fa [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;
25import android.util.Config;
26import android.util.Log;
27import android.util.SparseArray;
28import android.view.View;
29import android.view.ViewGroup;
30
31/**
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
37 * will not work.
38 */
39public abstract class CursorTreeAdapter extends BaseExpandableListAdapter implements Filterable,
40 CursorFilter.CursorFilterClient {
41 private Context mContext;
42 private Handler mHandler;
43 private boolean mAutoRequery;
44
45 /** The cursor helper that is used to get the groups */
46 MyCursorHelper mGroupCursorHelper;
47
48 /**
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)
51 */
52 SparseArray<MyCursorHelper> mChildrenCursorHelpers;
53
54 // Filter related
55 CursorFilter mCursorFilter;
56 FilterQueryProvider mFilterQueryProvider;
57
58 /**
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.
61 *
62 * @param cursor The cursor from which to get the data for the groups.
63 */
64 public CursorTreeAdapter(Cursor cursor, Context context) {
65 init(cursor, context, true);
66 }
67
68 /**
69 * Constructor.
70 *
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
75 * always displayed.
76 */
77 public CursorTreeAdapter(Cursor cursor, Context context, boolean autoRequery) {
78 init(cursor, context, autoRequery);
79 }
80
81 private void init(Cursor cursor, Context context, boolean autoRequery) {
82 mContext = context;
83 mHandler = new Handler();
84 mAutoRequery = autoRequery;
85
86 mGroupCursorHelper = new MyCursorHelper(cursor);
87 mChildrenCursorHelpers = new SparseArray<MyCursorHelper>();
88 }
89
90 /**
91 * Gets the cursor helper for the children in the given group.
92 *
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
97 * (false).
98 * @return The cursor helper for the children of the given group
99 */
100 synchronized MyCursorHelper getChildrenCursorHelper(int groupPosition, boolean requestCursor) {
101 MyCursorHelper cursorHelper = mChildrenCursorHelpers.get(groupPosition);
102
103 if (cursorHelper == null) {
104 if (mGroupCursorHelper.moveTo(groupPosition) == null) return null;
105
106 final Cursor cursor = getChildrenCursor(mGroupCursorHelper.getCursor());
107 cursorHelper = new MyCursorHelper(cursor);
108 mChildrenCursorHelpers.put(groupPosition, cursorHelper);
109 }
110
111 return cursorHelper;
112 }
113
114 /**
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.
117 * <p>
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)}.
121 * <p>
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.
127 *
128 * @param groupCursor The cursor pointing to the group whose children cursor
129 * should be returned
130 * @return The cursor for the children of a particular group, or null.
131 */
132 abstract protected Cursor getChildrenCursor(Cursor groupCursor);
133
134 /**
135 * Sets the group Cursor.
136 *
Jason Parks4b08d3e2010-07-01 14:38:02 -0500137 * @param cursor The Cursor to set for the group. If there is an existing cursor
138 * it will be closed.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800139 */
140 public void setGroupCursor(Cursor cursor) {
141 mGroupCursorHelper.changeCursor(cursor, false);
142 }
143
144 /**
Jason Parks4b08d3e2010-07-01 14:38:02 -0500145 * Sets the children Cursor for a particular group. If there is an existing cursor
146 * it will be closed.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800147 * <p>
148 * This is useful when asynchronously querying to prevent blocking the UI.
149 *
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.
152 */
153 public void setChildrenCursor(int groupPosition, Cursor childrenCursor) {
154
155 /*
156 * Don't request a cursor from the subclass, instead we will be setting
157 * the cursor ourselves.
158 */
159 MyCursorHelper childrenCursorHelper = getChildrenCursorHelper(groupPosition, false);
160
161 /*
162 * Don't release any cursor since we know exactly what data is changing
163 * (this cursor, which is still valid).
164 */
165 childrenCursorHelper.changeCursor(childrenCursor, false);
166 }
167
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);
171 }
172
173 public long getChildId(int groupPosition, int childPosition) {
174 return getChildrenCursorHelper(groupPosition, true).getId(childPosition);
175 }
176
177 public int getChildrenCount(int groupPosition) {
178 MyCursorHelper helper = getChildrenCursorHelper(groupPosition, true);
179 return (mGroupCursorHelper.isValid() && helper != null) ? helper.getCount() : 0;
180 }
181
182 public Cursor getGroup(int groupPosition) {
183 // Return the group Cursor pointing to the given group
184 return mGroupCursorHelper.moveTo(groupPosition);
185 }
186
187 public int getGroupCount() {
188 return mGroupCursorHelper.getCount();
189 }
190
191 public long getGroupId(int groupPosition) {
192 return mGroupCursorHelper.getId(groupPosition);
193 }
194
195 public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
196 ViewGroup parent) {
197 Cursor cursor = mGroupCursorHelper.moveTo(groupPosition);
198 if (cursor == null) {
199 throw new IllegalStateException("this should only be called when the cursor is valid");
200 }
201
202 View v;
203 if (convertView == null) {
204 v = newGroupView(mContext, cursor, isExpanded, parent);
205 } else {
206 v = convertView;
207 }
208 bindGroupView(v, mContext, cursor, isExpanded);
209 return v;
210 }
211
212 /**
213 * Makes a new group view to hold the group data pointed to by cursor.
214 *
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.
221 */
222 protected abstract View newGroupView(Context context, Cursor cursor, boolean isExpanded,
223 ViewGroup parent);
224
225 /**
226 * Bind an existing view to the group data pointed to by cursor.
227 *
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.
233 */
234 protected abstract void bindGroupView(View view, Context context, Cursor cursor,
235 boolean isExpanded);
236
237 public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
238 View convertView, ViewGroup parent) {
239 MyCursorHelper cursorHelper = getChildrenCursorHelper(groupPosition, true);
240
241 Cursor cursor = cursorHelper.moveTo(childPosition);
242 if (cursor == null) {
243 throw new IllegalStateException("this should only be called when the cursor is valid");
244 }
245
246 View v;
247 if (convertView == null) {
248 v = newChildView(mContext, cursor, isLastChild, parent);
249 } else {
250 v = convertView;
251 }
252 bindChildView(v, mContext, cursor, isLastChild);
253 return v;
254 }
255
256 /**
257 * Makes a new child view to hold the data pointed to by cursor.
258 *
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.
265 */
266 protected abstract View newChildView(Context context, Cursor cursor, boolean isLastChild,
267 ViewGroup parent);
268
269 /**
270 * Bind an existing view to the child data pointed to by cursor
271 *
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.
277 */
278 protected abstract void bindChildView(View view, Context context, Cursor cursor,
279 boolean isLastChild);
280
281 public boolean isChildSelectable(int groupPosition, int childPosition) {
282 return true;
283 }
284
285 public boolean hasStableIds() {
286 return true;
287 }
288
289 private synchronized void releaseCursorHelpers() {
290 for (int pos = mChildrenCursorHelpers.size() - 1; pos >= 0; pos--) {
291 mChildrenCursorHelpers.valueAt(pos).deactivate();
292 }
293
294 mChildrenCursorHelpers.clear();
295 }
296
297 @Override
298 public void notifyDataSetChanged() {
299 notifyDataSetChanged(true);
300 }
301
302 /**
303 * Notifies a data set change, but with the option of not releasing any
304 * cached cursors.
305 *
306 * @param releaseCursors Whether to release and deactivate any cached
307 * cursors.
308 */
309 public void notifyDataSetChanged(boolean releaseCursors) {
310
311 if (releaseCursors) {
312 releaseCursorHelpers();
313 }
314
315 super.notifyDataSetChanged();
316 }
317
318 @Override
319 public void notifyDataSetInvalidated() {
320 releaseCursorHelpers();
321 super.notifyDataSetInvalidated();
322 }
323
324 @Override
325 public void onGroupCollapsed(int groupPosition) {
326 deactivateChildrenCursorHelper(groupPosition);
327 }
328
329 /**
330 * Deactivates the Cursor and removes the helper from cache.
331 *
332 * @param groupPosition The group whose children Cursor and helper should be
333 * deactivated.
334 */
335 synchronized void deactivateChildrenCursorHelper(int groupPosition) {
336 MyCursorHelper cursorHelper = getChildrenCursorHelper(groupPosition, true);
337 mChildrenCursorHelpers.remove(groupPosition);
338 cursorHelper.deactivate();
339 }
340
341 /**
342 * @see CursorAdapter#convertToString(Cursor)
343 */
344 public String convertToString(Cursor cursor) {
345 return cursor == null ? "" : cursor.toString();
346 }
347
348 /**
349 * @see CursorAdapter#runQueryOnBackgroundThread(CharSequence)
350 */
351 public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
352 if (mFilterQueryProvider != null) {
353 return mFilterQueryProvider.runQuery(constraint);
354 }
355
356 return mGroupCursorHelper.getCursor();
357 }
358
359 public Filter getFilter() {
360 if (mCursorFilter == null) {
361 mCursorFilter = new CursorFilter(this);
362 }
363 return mCursorFilter;
364 }
365
366 /**
367 * @see CursorAdapter#getFilterQueryProvider()
368 */
369 public FilterQueryProvider getFilterQueryProvider() {
370 return mFilterQueryProvider;
371 }
372
373 /**
374 * @see CursorAdapter#setFilterQueryProvider(FilterQueryProvider)
375 */
376 public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
377 mFilterQueryProvider = filterQueryProvider;
378 }
379
380 /**
381 * @see CursorAdapter#changeCursor(Cursor)
382 */
383 public void changeCursor(Cursor cursor) {
384 mGroupCursorHelper.changeCursor(cursor, true);
385 }
386
387 /**
388 * @see CursorAdapter#getCursor()
389 */
390 public Cursor getCursor() {
391 return mGroupCursorHelper.getCursor();
392 }
393
394 /**
395 * Helper class for Cursor management:
396 * <li> Data validity
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
401 */
402 class MyCursorHelper {
403 private Cursor mCursor;
404 private boolean mDataValid;
405 private int mRowIDColumn;
406 private MyContentObserver mContentObserver;
407 private MyDataSetObserver mDataSetObserver;
408
409 MyCursorHelper(Cursor cursor) {
410 final boolean cursorPresent = cursor != null;
411 mCursor = cursor;
412 mDataValid = cursorPresent;
413 mRowIDColumn = cursorPresent ? cursor.getColumnIndex("_id") : -1;
414 mContentObserver = new MyContentObserver();
415 mDataSetObserver = new MyDataSetObserver();
416 if (cursorPresent) {
417 cursor.registerContentObserver(mContentObserver);
418 cursor.registerDataSetObserver(mDataSetObserver);
419 }
420 }
421
422 Cursor getCursor() {
423 return mCursor;
424 }
425
426 int getCount() {
427 if (mDataValid && mCursor != null) {
428 return mCursor.getCount();
429 } else {
430 return 0;
431 }
432 }
433
434 long getId(int position) {
435 if (mDataValid && mCursor != null) {
436 if (mCursor.moveToPosition(position)) {
437 return mCursor.getLong(mRowIDColumn);
438 } else {
439 return 0;
440 }
441 } else {
442 return 0;
443 }
444 }
445
446 Cursor moveTo(int position) {
447 if (mDataValid && (mCursor != null) && mCursor.moveToPosition(position)) {
448 return mCursor;
449 } else {
450 return null;
451 }
452 }
453
454 void changeCursor(Cursor cursor, boolean releaseCursors) {
455 if (cursor == mCursor) return;
456
457 deactivate();
458 mCursor = cursor;
459 if (cursor != null) {
460 cursor.registerContentObserver(mContentObserver);
461 cursor.registerDataSetObserver(mDataSetObserver);
462 mRowIDColumn = cursor.getColumnIndex("_id");
463 mDataValid = true;
464 // notify the observers about the new cursor
465 notifyDataSetChanged(releaseCursors);
466 } else {
467 mRowIDColumn = -1;
468 mDataValid = false;
469 // notify the observers about the lack of a data set
470 notifyDataSetInvalidated();
471 }
472 }
473
474 void deactivate() {
475 if (mCursor == null) {
476 return;
477 }
478
479 mCursor.unregisterContentObserver(mContentObserver);
480 mCursor.unregisterDataSetObserver(mDataSetObserver);
Jason Parks4b08d3e2010-07-01 14:38:02 -0500481 mCursor.close();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800482 mCursor = null;
483 }
484
485 boolean isValid() {
486 return mDataValid && mCursor != null;
487 }
488
489 private class MyContentObserver extends ContentObserver {
490 public MyContentObserver() {
491 super(mHandler);
492 }
493
494 @Override
495 public boolean deliverSelfNotifications() {
496 return true;
497 }
498
499 @Override
500 public void onChange(boolean selfChange) {
501 if (mAutoRequery && mCursor != null) {
502 if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor +
503 " due to update");
504 mDataValid = mCursor.requery();
505 }
506 }
507 }
508
509 private class MyDataSetObserver extends DataSetObserver {
510 @Override
511 public void onChanged() {
512 mDataValid = true;
513 notifyDataSetChanged();
514 }
515
516 @Override
517 public void onInvalidated() {
518 mDataValid = false;
519 notifyDataSetInvalidated();
520 }
521 }
522 }
523}