Home | History | Annotate | Download | only in widget
      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      */
     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