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
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.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     *
136     * @param cursor The Cursor to set for the group. If there is an existing cursor
137     * it will be closed.
138     */
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     */
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);
480            mCursor.close();
481            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 && !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
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}
523