1 /*
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 
17 package android.widget;
18 
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.Log;
26 import android.util.SparseArray;
27 import android.view.View;
28 import 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  */
38 public 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      */
CursorTreeAdapter(Cursor cursor, Context context)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      */
CursorTreeAdapter(Cursor cursor, Context context, boolean autoRequery)76     public CursorTreeAdapter(Cursor cursor, Context context, boolean autoRequery) {
77         init(cursor, context, autoRequery);
78     }
79 
init(Cursor cursor, Context context, boolean autoRequery)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      */
getChildrenCursorHelper(int groupPosition, boolean requestCursor)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      */
getChildrenCursor(Cursor groupCursor)131     abstract protected Cursor getChildrenCursor(Cursor groupCursor);
132 
133     /**
134      * Sets the group Cursor.
135      *
136      * @param cursor The Cursor to set for the group. If there is an existing cursor
137      * it will be closed.
138      */
setGroupCursor(Cursor cursor)139     public void setGroupCursor(Cursor cursor) {
140         mGroupCursorHelper.changeCursor(cursor, false);
141     }
142 
143     /**
144      * Sets the children Cursor for a particular group. If there is an existing cursor
145      * it will be closed.
146      * <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      */
setChildrenCursor(int groupPosition, Cursor childrenCursor)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 
getChild(int groupPosition, int childPosition)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 
getChildId(int groupPosition, int childPosition)172     public long getChildId(int groupPosition, int childPosition) {
173         return getChildrenCursorHelper(groupPosition, true).getId(childPosition);
174     }
175 
getChildrenCount(int groupPosition)176     public int getChildrenCount(int groupPosition) {
177         MyCursorHelper helper = getChildrenCursorHelper(groupPosition, true);
178         return (mGroupCursorHelper.isValid() && helper != null) ? helper.getCount() : 0;
179     }
180 
getGroup(int groupPosition)181     public Cursor getGroup(int groupPosition) {
182         // Return the group Cursor pointing to the given group
183         return mGroupCursorHelper.moveTo(groupPosition);
184     }
185 
getGroupCount()186     public int getGroupCount() {
187         return mGroupCursorHelper.getCount();
188     }
189 
getGroupId(int groupPosition)190     public long getGroupId(int groupPosition) {
191         return mGroupCursorHelper.getId(groupPosition);
192     }
193 
getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent)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      */
newGroupView(Context context, Cursor cursor, boolean isExpanded, ViewGroup parent)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      */
bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded)233     protected abstract void bindGroupView(View view, Context context, Cursor cursor,
234             boolean isExpanded);
235 
getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent)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      */
newChildView(Context context, Cursor cursor, boolean isLastChild, ViewGroup parent)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      */
bindChildView(View view, Context context, Cursor cursor, boolean isLastChild)277     protected abstract void bindChildView(View view, Context context, Cursor cursor,
278             boolean isLastChild);
279 
isChildSelectable(int groupPosition, int childPosition)280     public boolean isChildSelectable(int groupPosition, int childPosition) {
281         return true;
282     }
283 
hasStableIds()284     public boolean hasStableIds() {
285         return true;
286     }
287 
releaseCursorHelpers()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
notifyDataSetChanged()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      */
notifyDataSetChanged(boolean releaseCursors)308     public void notifyDataSetChanged(boolean releaseCursors) {
309 
310         if (releaseCursors) {
311             releaseCursorHelpers();
312         }
313 
314         super.notifyDataSetChanged();
315     }
316 
317     @Override
notifyDataSetInvalidated()318     public void notifyDataSetInvalidated() {
319         releaseCursorHelpers();
320         super.notifyDataSetInvalidated();
321     }
322 
323     @Override
onGroupCollapsed(int groupPosition)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      */
deactivateChildrenCursorHelper(int groupPosition)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      */
convertToString(Cursor cursor)343     public String convertToString(Cursor cursor) {
344         return cursor == null ? "" : cursor.toString();
345     }
346 
347     /**
348      * @see CursorAdapter#runQueryOnBackgroundThread(CharSequence)
349      */
runQueryOnBackgroundThread(CharSequence constraint)350     public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
351         if (mFilterQueryProvider != null) {
352             return mFilterQueryProvider.runQuery(constraint);
353         }
354 
355         return mGroupCursorHelper.getCursor();
356     }
357 
getFilter()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      */
getFilterQueryProvider()368     public FilterQueryProvider getFilterQueryProvider() {
369         return mFilterQueryProvider;
370     }
371 
372     /**
373      * @see CursorAdapter#setFilterQueryProvider(FilterQueryProvider)
374      */
setFilterQueryProvider(FilterQueryProvider filterQueryProvider)375     public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
376         mFilterQueryProvider = filterQueryProvider;
377     }
378 
379     /**
380      * @see CursorAdapter#changeCursor(Cursor)
381      */
changeCursor(Cursor cursor)382     public void changeCursor(Cursor cursor) {
383         mGroupCursorHelper.changeCursor(cursor, true);
384     }
385 
386     /**
387      * @see CursorAdapter#getCursor()
388      */
getCursor()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 
MyCursorHelper(Cursor cursor)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 
getCursor()421         Cursor getCursor() {
422             return mCursor;
423         }
424 
getCount()425         int getCount() {
426             if (mDataValid && mCursor != null) {
427                 return mCursor.getCount();
428             } else {
429                 return 0;
430             }
431         }
432 
getId(int position)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 
moveTo(int position)445         Cursor moveTo(int position) {
446             if (mDataValid && (mCursor != null) && mCursor.moveToPosition(position)) {
447                 return mCursor;
448             } else {
449                 return null;
450             }
451         }
452 
changeCursor(Cursor cursor, boolean releaseCursors)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 
deactivate()473         void deactivate() {
474             if (mCursor == null) {
475                 return;
476             }
477 
478             mCursor.unregisterContentObserver(mContentObserver);
479             mCursor.unregisterDataSetObserver(mDataSetObserver);
480             mCursor.close();
481             mCursor = null;
482         }
483 
isValid()484         boolean isValid() {
485             return mDataValid && mCursor != null;
486         }
487 
488         private class MyContentObserver extends ContentObserver {
MyContentObserver()489             public MyContentObserver() {
490                 super(mHandler);
491             }
492 
493             @Override
deliverSelfNotifications()494             public boolean deliverSelfNotifications() {
495                 return true;
496             }
497 
498             @Override
onChange(boolean selfChange)499             public void onChange(boolean selfChange) {
500                 if (mAutoRequery && mCursor != null && !mCursor.isClosed()) {
501                     if (false) Log.v("Cursor", "Auto requerying " + mCursor +
502                             " due to update");
503                     mDataValid = mCursor.requery();
504                 }
505             }
506         }
507 
508         private class MyDataSetObserver extends DataSetObserver {
509             @Override
onChanged()510             public void onChanged() {
511                 mDataValid = true;
512                 notifyDataSetChanged();
513             }
514 
515             @Override
onInvalidated()516             public void onInvalidated() {
517                 mDataValid = false;
518                 notifyDataSetInvalidated();
519             }
520         }
521     }
522 }
523