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.database.DataSetObserver;
     20 import android.os.Parcel;
     21 import android.os.Parcelable;
     22 import android.os.SystemClock;
     23 import android.view.View;
     24 import android.view.ViewGroup;
     25 
     26 import java.util.ArrayList;
     27 import java.util.Collections;
     28 
     29 /*
     30  * Implementation notes:
     31  *
     32  * <p>
     33  * Terminology:
     34  * <li> flPos - Flat list position, the position used by ListView
     35  * <li> gPos - Group position, the position of a group among all the groups
     36  * <li> cPos - Child position, the position of a child among all the children
     37  * in a group
     38  */
     39 
     40 /**
     41  * A {@link BaseAdapter} that provides data/Views in an expandable list (offers
     42  * features such as collapsing/expanding groups containing children). By
     43  * itself, this adapter has no data and is a connector to a
     44  * {@link ExpandableListAdapter} which provides the data.
     45  * <p>
     46  * Internally, this connector translates the flat list position that the
     47  * ListAdapter expects to/from group and child positions that the ExpandableListAdapter
     48  * expects.
     49  */
     50 class ExpandableListConnector extends BaseAdapter implements Filterable {
     51     /**
     52      * The ExpandableListAdapter to fetch the data/Views for this expandable list
     53      */
     54     private ExpandableListAdapter mExpandableListAdapter;
     55 
     56     /**
     57      * List of metadata for the currently expanded groups. The metadata consists
     58      * of data essential for efficiently translating between flat list positions
     59      * and group/child positions. See {@link GroupMetadata}.
     60      */
     61     private ArrayList<GroupMetadata> mExpGroupMetadataList;
     62 
     63     /** The number of children from all currently expanded groups */
     64     private int mTotalExpChildrenCount;
     65 
     66     /** The maximum number of allowable expanded groups. Defaults to 'no limit' */
     67     private int mMaxExpGroupCount = Integer.MAX_VALUE;
     68 
     69     /** Change observer used to have ExpandableListAdapter changes pushed to us */
     70     private final DataSetObserver mDataSetObserver = new MyDataSetObserver();
     71 
     72     /**
     73      * Constructs the connector
     74      */
     75     public ExpandableListConnector(ExpandableListAdapter expandableListAdapter) {
     76         mExpGroupMetadataList = new ArrayList<GroupMetadata>();
     77 
     78         setExpandableListAdapter(expandableListAdapter);
     79     }
     80 
     81     /**
     82      * Point to the {@link ExpandableListAdapter} that will give us data/Views
     83      *
     84      * @param expandableListAdapter the adapter that supplies us with data/Views
     85      */
     86     public void setExpandableListAdapter(ExpandableListAdapter expandableListAdapter) {
     87         if (mExpandableListAdapter != null) {
     88             mExpandableListAdapter.unregisterDataSetObserver(mDataSetObserver);
     89         }
     90 
     91         mExpandableListAdapter = expandableListAdapter;
     92         expandableListAdapter.registerDataSetObserver(mDataSetObserver);
     93     }
     94 
     95     /**
     96      * Translates a flat list position to either a) group pos if the specified
     97      * flat list position corresponds to a group, or b) child pos if it
     98      * corresponds to a child.  Performs a binary search on the expanded
     99      * groups list to find the flat list pos if it is an exp group, otherwise
    100      * finds where the flat list pos fits in between the exp groups.
    101      *
    102      * @param flPos the flat list position to be translated
    103      * @return the group position or child position of the specified flat list
    104      *         position encompassed in a {@link PositionMetadata} object
    105      *         that contains additional useful info for insertion, etc.
    106      */
    107     PositionMetadata getUnflattenedPos(final int flPos) {
    108         /* Keep locally since frequent use */
    109         final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
    110         final int numExpGroups = egml.size();
    111 
    112         /* Binary search variables */
    113         int leftExpGroupIndex = 0;
    114         int rightExpGroupIndex = numExpGroups - 1;
    115         int midExpGroupIndex = 0;
    116         GroupMetadata midExpGm;
    117 
    118         if (numExpGroups == 0) {
    119             /*
    120              * There aren't any expanded groups (hence no visible children
    121              * either), so flPos must be a group and its group pos will be the
    122              * same as its flPos
    123              */
    124             return PositionMetadata.obtain(flPos, ExpandableListPosition.GROUP, flPos,
    125                     -1, null, 0);
    126         }
    127 
    128         /*
    129          * Binary search over the expanded groups to find either the exact
    130          * expanded group (if we're looking for a group) or the group that
    131          * contains the child we're looking for. If we are looking for a
    132          * collapsed group, we will not have a direct match here, but we will
    133          * find the expanded group just before the group we're searching for (so
    134          * then we can calculate the group position of the group we're searching
    135          * for). If there isn't an expanded group prior to the group being
    136          * searched for, then the group being searched for's group position is
    137          * the same as the flat list position (since there are no children before
    138          * it, and all groups before it are collapsed).
    139          */
    140         while (leftExpGroupIndex <= rightExpGroupIndex) {
    141             midExpGroupIndex =
    142                     (rightExpGroupIndex - leftExpGroupIndex) / 2
    143                             + leftExpGroupIndex;
    144             midExpGm = egml.get(midExpGroupIndex);
    145 
    146             if (flPos > midExpGm.lastChildFlPos) {
    147                 /*
    148                  * The flat list position is after the current middle group's
    149                  * last child's flat list position, so search right
    150                  */
    151                 leftExpGroupIndex = midExpGroupIndex + 1;
    152             } else if (flPos < midExpGm.flPos) {
    153                 /*
    154                  * The flat list position is before the current middle group's
    155                  * flat list position, so search left
    156                  */
    157                 rightExpGroupIndex = midExpGroupIndex - 1;
    158             } else if (flPos == midExpGm.flPos) {
    159                 /*
    160                  * The flat list position is this middle group's flat list
    161                  * position, so we've found an exact hit
    162                  */
    163                 return PositionMetadata.obtain(flPos, ExpandableListPosition.GROUP,
    164                         midExpGm.gPos, -1, midExpGm, midExpGroupIndex);
    165             } else if (flPos <= midExpGm.lastChildFlPos
    166                     /* && flPos > midGm.flPos as deduced from previous
    167                      * conditions */) {
    168                 /* The flat list position is a child of the middle group */
    169 
    170                 /*
    171                  * Subtract the first child's flat list position from the
    172                  * specified flat list pos to get the child's position within
    173                  * the group
    174                  */
    175                 final int childPos = flPos - (midExpGm.flPos + 1);
    176                 return PositionMetadata.obtain(flPos, ExpandableListPosition.CHILD,
    177                         midExpGm.gPos, childPos, midExpGm, midExpGroupIndex);
    178             }
    179         }
    180 
    181         /*
    182          * If we've reached here, it means the flat list position must be a
    183          * group that is not expanded, since otherwise we would have hit it
    184          * in the above search.
    185          */
    186 
    187 
    188         /**
    189          * If we are to expand this group later, where would it go in the
    190          * mExpGroupMetadataList ?
    191          */
    192         int insertPosition = 0;
    193 
    194         /** What is its group position in the list of all groups? */
    195         int groupPos = 0;
    196 
    197         /*
    198          * To figure out exact insertion and prior group positions, we need to
    199          * determine how we broke out of the binary search.  We backtrack
    200          * to see this.
    201          */
    202         if (leftExpGroupIndex > midExpGroupIndex) {
    203 
    204             /*
    205              * This would occur in the first conditional, so the flat list
    206              * insertion position is after the left group. Also, the
    207              * leftGroupPos is one more than it should be (since that broke out
    208              * of our binary search), so we decrement it.
    209              */
    210             final GroupMetadata leftExpGm = egml.get(leftExpGroupIndex-1);
    211 
    212             insertPosition = leftExpGroupIndex;
    213 
    214             /*
    215              * Sums the number of groups between the prior exp group and this
    216              * one, and then adds it to the prior group's group pos
    217              */
    218             groupPos =
    219                 (flPos - leftExpGm.lastChildFlPos) + leftExpGm.gPos;
    220         } else if (rightExpGroupIndex < midExpGroupIndex) {
    221 
    222             /*
    223              * This would occur in the second conditional, so the flat list
    224              * insertion position is before the right group. Also, the
    225              * rightGroupPos is one less than it should be, so increment it.
    226              */
    227             final GroupMetadata rightExpGm = egml.get(++rightExpGroupIndex);
    228 
    229             insertPosition = rightExpGroupIndex;
    230 
    231             /*
    232              * Subtracts this group's flat list pos from the group after's flat
    233              * list position to find out how many groups are in between the two
    234              * groups. Then, subtracts that number from the group after's group
    235              * pos to get this group's pos.
    236              */
    237             groupPos = rightExpGm.gPos - (rightExpGm.flPos - flPos);
    238         } else {
    239             // TODO: clean exit
    240             throw new RuntimeException("Unknown state");
    241         }
    242 
    243         return PositionMetadata.obtain(flPos, ExpandableListPosition.GROUP, groupPos, -1,
    244                 null, insertPosition);
    245     }
    246 
    247     /**
    248      * Translates either a group pos or a child pos (+ group it belongs to) to a
    249      * flat list position.  If searching for a child and its group is not expanded, this will
    250      * return null since the child isn't being shown in the ListView, and hence it has no
    251      * position.
    252      *
    253      * @param pos a {@link ExpandableListPosition} representing either a group position
    254      *        or child position
    255      * @return the flat list position encompassed in a {@link PositionMetadata}
    256      *         object that contains additional useful info for insertion, etc., or null.
    257      */
    258     PositionMetadata getFlattenedPos(final ExpandableListPosition pos) {
    259         final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
    260         final int numExpGroups = egml.size();
    261 
    262         /* Binary search variables */
    263         int leftExpGroupIndex = 0;
    264         int rightExpGroupIndex = numExpGroups - 1;
    265         int midExpGroupIndex = 0;
    266         GroupMetadata midExpGm;
    267 
    268         if (numExpGroups == 0) {
    269             /*
    270              * There aren't any expanded groups, so flPos must be a group and
    271              * its flPos will be the same as its group pos.  The
    272              * insert position is 0 (since the list is empty).
    273              */
    274             return PositionMetadata.obtain(pos.groupPos, pos.type,
    275                     pos.groupPos, pos.childPos, null, 0);
    276         }
    277 
    278         /*
    279          * Binary search over the expanded groups to find either the exact
    280          * expanded group (if we're looking for a group) or the group that
    281          * contains the child we're looking for.
    282          */
    283         while (leftExpGroupIndex <= rightExpGroupIndex) {
    284             midExpGroupIndex = (rightExpGroupIndex - leftExpGroupIndex)/2 + leftExpGroupIndex;
    285             midExpGm = egml.get(midExpGroupIndex);
    286 
    287             if (pos.groupPos > midExpGm.gPos) {
    288                 /*
    289                  * It's after the current middle group, so search right
    290                  */
    291                 leftExpGroupIndex = midExpGroupIndex + 1;
    292             } else if (pos.groupPos < midExpGm.gPos) {
    293                 /*
    294                  * It's before the current middle group, so search left
    295                  */
    296                 rightExpGroupIndex = midExpGroupIndex - 1;
    297             } else if (pos.groupPos == midExpGm.gPos) {
    298                 /*
    299                  * It's this middle group, exact hit
    300                  */
    301 
    302                 if (pos.type == ExpandableListPosition.GROUP) {
    303                     /* If it's a group, give them this matched group's flPos */
    304                     return PositionMetadata.obtain(midExpGm.flPos, pos.type,
    305                             pos.groupPos, pos.childPos, midExpGm, midExpGroupIndex);
    306                 } else if (pos.type == ExpandableListPosition.CHILD) {
    307                     /* If it's a child, calculate the flat list pos */
    308                     return PositionMetadata.obtain(midExpGm.flPos + pos.childPos
    309                             + 1, pos.type, pos.groupPos, pos.childPos,
    310                             midExpGm, midExpGroupIndex);
    311                 } else {
    312                     return null;
    313                 }
    314             }
    315         }
    316 
    317         /*
    318          * If we've reached here, it means there was no match in the expanded
    319          * groups, so it must be a collapsed group that they're search for
    320          */
    321         if (pos.type != ExpandableListPosition.GROUP) {
    322             /* If it isn't a group, return null */
    323             return null;
    324         }
    325 
    326         /*
    327          * To figure out exact insertion and prior group positions, we need to
    328          * determine how we broke out of the binary search. We backtrack to see
    329          * this.
    330          */
    331         if (leftExpGroupIndex > midExpGroupIndex) {
    332 
    333             /*
    334              * This would occur in the first conditional, so the flat list
    335              * insertion position is after the left group.
    336              *
    337              * The leftGroupPos is one more than it should be (from the binary
    338              * search loop) so we subtract 1 to get the actual left group.  Since
    339              * the insertion point is AFTER the left group, we keep this +1
    340              * value as the insertion point
    341              */
    342             final GroupMetadata leftExpGm = egml.get(leftExpGroupIndex-1);
    343             final int flPos =
    344                     leftExpGm.lastChildFlPos
    345                             + (pos.groupPos - leftExpGm.gPos);
    346 
    347             return PositionMetadata.obtain(flPos, pos.type, pos.groupPos,
    348                     pos.childPos, null, leftExpGroupIndex);
    349         } else if (rightExpGroupIndex < midExpGroupIndex) {
    350 
    351             /*
    352              * This would occur in the second conditional, so the flat list
    353              * insertion position is before the right group. Also, the
    354              * rightGroupPos is one less than it should be (from binary search
    355              * loop), so we increment to it.
    356              */
    357             final GroupMetadata rightExpGm = egml.get(++rightExpGroupIndex);
    358             final int flPos =
    359                     rightExpGm.flPos
    360                             - (rightExpGm.gPos - pos.groupPos);
    361             return PositionMetadata.obtain(flPos, pos.type, pos.groupPos,
    362                     pos.childPos, null, rightExpGroupIndex);
    363         } else {
    364             return null;
    365         }
    366     }
    367 
    368     @Override
    369     public boolean areAllItemsEnabled() {
    370         return mExpandableListAdapter.areAllItemsEnabled();
    371     }
    372 
    373     @Override
    374     public boolean isEnabled(int flatListPos) {
    375         final ExpandableListPosition pos = getUnflattenedPos(flatListPos).position;
    376 
    377         boolean retValue;
    378         if (pos.type == ExpandableListPosition.CHILD) {
    379             retValue = mExpandableListAdapter.isChildSelectable(pos.groupPos, pos.childPos);
    380         } else {
    381             // Groups are always selectable
    382             retValue = true;
    383         }
    384 
    385         pos.recycle();
    386 
    387         return retValue;
    388     }
    389 
    390     public int getCount() {
    391         /*
    392          * Total count for the list view is the number groups plus the
    393          * number of children from currently expanded groups (a value we keep
    394          * cached in this class)
    395          */
    396         return mExpandableListAdapter.getGroupCount() + mTotalExpChildrenCount;
    397     }
    398 
    399     public Object getItem(int flatListPos) {
    400         final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
    401 
    402         Object retValue;
    403         if (posMetadata.position.type == ExpandableListPosition.GROUP) {
    404             retValue = mExpandableListAdapter
    405                     .getGroup(posMetadata.position.groupPos);
    406         } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
    407             retValue = mExpandableListAdapter.getChild(posMetadata.position.groupPos,
    408                     posMetadata.position.childPos);
    409         } else {
    410             // TODO: clean exit
    411             throw new RuntimeException("Flat list position is of unknown type");
    412         }
    413 
    414         posMetadata.recycle();
    415 
    416         return retValue;
    417     }
    418 
    419     public long getItemId(int flatListPos) {
    420         final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
    421         final long groupId = mExpandableListAdapter.getGroupId(posMetadata.position.groupPos);
    422 
    423         long retValue;
    424         if (posMetadata.position.type == ExpandableListPosition.GROUP) {
    425             retValue = mExpandableListAdapter.getCombinedGroupId(groupId);
    426         } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
    427             final long childId = mExpandableListAdapter.getChildId(posMetadata.position.groupPos,
    428                     posMetadata.position.childPos);
    429             retValue = mExpandableListAdapter.getCombinedChildId(groupId, childId);
    430         } else {
    431             // TODO: clean exit
    432             throw new RuntimeException("Flat list position is of unknown type");
    433         }
    434 
    435         posMetadata.recycle();
    436 
    437         return retValue;
    438     }
    439 
    440     public View getView(int flatListPos, View convertView, ViewGroup parent) {
    441         final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
    442 
    443         View retValue;
    444         if (posMetadata.position.type == ExpandableListPosition.GROUP) {
    445             retValue = mExpandableListAdapter.getGroupView(posMetadata.position.groupPos,
    446                     posMetadata.isExpanded(), convertView, parent);
    447         } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
    448             final boolean isLastChild = posMetadata.groupMetadata.lastChildFlPos == flatListPos;
    449 
    450             retValue = mExpandableListAdapter.getChildView(posMetadata.position.groupPos,
    451                     posMetadata.position.childPos, isLastChild, convertView, parent);
    452         } else {
    453             // TODO: clean exit
    454             throw new RuntimeException("Flat list position is of unknown type");
    455         }
    456 
    457         posMetadata.recycle();
    458 
    459         return retValue;
    460     }
    461 
    462     @Override
    463     public int getItemViewType(int flatListPos) {
    464         final ExpandableListPosition pos = getUnflattenedPos(flatListPos).position;
    465 
    466         int retValue;
    467         if (mExpandableListAdapter instanceof HeterogeneousExpandableList) {
    468             HeterogeneousExpandableList adapter =
    469                     (HeterogeneousExpandableList) mExpandableListAdapter;
    470             if (pos.type == ExpandableListPosition.GROUP) {
    471                 retValue = adapter.getGroupType(pos.groupPos);
    472             } else {
    473                 final int childType = adapter.getChildType(pos.groupPos, pos.childPos);
    474                 retValue = adapter.getGroupTypeCount() + childType;
    475             }
    476         } else {
    477             if (pos.type == ExpandableListPosition.GROUP) {
    478                 retValue = 0;
    479             } else {
    480                 retValue = 1;
    481             }
    482         }
    483 
    484         pos.recycle();
    485 
    486         return retValue;
    487     }
    488 
    489     @Override
    490     public int getViewTypeCount() {
    491         if (mExpandableListAdapter instanceof HeterogeneousExpandableList) {
    492             HeterogeneousExpandableList adapter =
    493                     (HeterogeneousExpandableList) mExpandableListAdapter;
    494             return adapter.getGroupTypeCount() + adapter.getChildTypeCount();
    495         } else {
    496             return 2;
    497         }
    498     }
    499 
    500     @Override
    501     public boolean hasStableIds() {
    502         return mExpandableListAdapter.hasStableIds();
    503     }
    504 
    505     /**
    506      * Traverses the expanded group metadata list and fills in the flat list
    507      * positions.
    508      *
    509      * @param forceChildrenCountRefresh Forces refreshing of the children count
    510      *        for all expanded groups.
    511      * @param syncGroupPositions Whether to search for the group positions
    512      *         based on the group IDs. This should only be needed when calling
    513      *         this from an onChanged callback.
    514      */
    515     @SuppressWarnings("unchecked")
    516     private void refreshExpGroupMetadataList(boolean forceChildrenCountRefresh,
    517             boolean syncGroupPositions) {
    518         final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
    519         int egmlSize = egml.size();
    520         int curFlPos = 0;
    521 
    522         /* Update child count as we go through */
    523         mTotalExpChildrenCount = 0;
    524 
    525         if (syncGroupPositions) {
    526             // We need to check whether any groups have moved positions
    527             boolean positionsChanged = false;
    528 
    529             for (int i = egmlSize - 1; i >= 0; i--) {
    530                 GroupMetadata curGm = egml.get(i);
    531                 int newGPos = findGroupPosition(curGm.gId, curGm.gPos);
    532                 if (newGPos != curGm.gPos) {
    533                     if (newGPos == AdapterView.INVALID_POSITION) {
    534                         // Doh, just remove it from the list of expanded groups
    535                         egml.remove(i);
    536                         egmlSize--;
    537                     }
    538 
    539                     curGm.gPos = newGPos;
    540                     if (!positionsChanged) positionsChanged = true;
    541                 }
    542             }
    543 
    544             if (positionsChanged) {
    545                 // At least one group changed positions, so re-sort
    546                 Collections.sort(egml);
    547             }
    548         }
    549 
    550         int gChildrenCount;
    551         int lastGPos = 0;
    552         for (int i = 0; i < egmlSize; i++) {
    553             /* Store in local variable since we'll access freq */
    554             GroupMetadata curGm = egml.get(i);
    555 
    556             /*
    557              * Get the number of children, try to refrain from calling
    558              * another class's method unless we have to (so do a subtraction)
    559              */
    560             if ((curGm.lastChildFlPos == GroupMetadata.REFRESH) || forceChildrenCountRefresh) {
    561                 gChildrenCount = mExpandableListAdapter.getChildrenCount(curGm.gPos);
    562             } else {
    563                 /* Num children for this group is its last child's fl pos minus
    564                  * the group's fl pos
    565                  */
    566                 gChildrenCount = curGm.lastChildFlPos - curGm.flPos;
    567             }
    568 
    569             /* Update */
    570             mTotalExpChildrenCount += gChildrenCount;
    571 
    572             /*
    573              * This skips the collapsed groups and increments the flat list
    574              * position (for subsequent exp groups) by accounting for the collapsed
    575              * groups
    576              */
    577             curFlPos += (curGm.gPos - lastGPos);
    578             lastGPos = curGm.gPos;
    579 
    580             /* Update the flat list positions, and the current flat list pos */
    581             curGm.flPos = curFlPos;
    582             curFlPos += gChildrenCount;
    583             curGm.lastChildFlPos = curFlPos;
    584         }
    585     }
    586 
    587     /**
    588      * Collapse a group in the grouped list view
    589      *
    590      * @param groupPos position of the group to collapse
    591      */
    592     boolean collapseGroup(int groupPos) {
    593         PositionMetadata pm = getFlattenedPos(ExpandableListPosition.obtain(
    594                 ExpandableListPosition.GROUP, groupPos, -1, -1));
    595         if (pm == null) return false;
    596 
    597         boolean retValue = collapseGroup(pm);
    598         pm.recycle();
    599         return retValue;
    600     }
    601 
    602     boolean collapseGroup(PositionMetadata posMetadata) {
    603         /*
    604          * Collapsing requires removal from mExpGroupMetadataList
    605          */
    606 
    607         /*
    608          * If it is null, it must be already collapsed. This group metadata
    609          * object should have been set from the search that returned the
    610          * position metadata object.
    611          */
    612         if (posMetadata.groupMetadata == null) return false;
    613 
    614         // Remove the group from the list of expanded groups
    615         mExpGroupMetadataList.remove(posMetadata.groupMetadata);
    616 
    617         // Refresh the metadata
    618         refreshExpGroupMetadataList(false, false);
    619 
    620         // Notify of change
    621         notifyDataSetChanged();
    622 
    623         // Give the callback
    624         mExpandableListAdapter.onGroupCollapsed(posMetadata.groupMetadata.gPos);
    625 
    626         return true;
    627     }
    628 
    629     /**
    630      * Expand a group in the grouped list view
    631      * @param groupPos the group to be expanded
    632      */
    633     boolean expandGroup(int groupPos) {
    634         PositionMetadata pm = getFlattenedPos(ExpandableListPosition.obtain(
    635                 ExpandableListPosition.GROUP, groupPos, -1, -1));
    636         boolean retValue = expandGroup(pm);
    637         pm.recycle();
    638         return retValue;
    639     }
    640 
    641     boolean expandGroup(PositionMetadata posMetadata) {
    642         /*
    643          * Expanding requires insertion into the mExpGroupMetadataList
    644          */
    645 
    646         if (posMetadata.position.groupPos < 0) {
    647             // TODO clean exit
    648             throw new RuntimeException("Need group");
    649         }
    650 
    651         if (mMaxExpGroupCount == 0) return false;
    652 
    653         // Check to see if it's already expanded
    654         if (posMetadata.groupMetadata != null) return false;
    655 
    656         /* Restrict number of expanded groups to mMaxExpGroupCount */
    657         if (mExpGroupMetadataList.size() >= mMaxExpGroupCount) {
    658             /* Collapse a group */
    659             // TODO: Collapse something not on the screen instead of the first one?
    660             // TODO: Could write overloaded function to take GroupMetadata to collapse
    661             GroupMetadata collapsedGm = mExpGroupMetadataList.get(0);
    662 
    663             int collapsedIndex = mExpGroupMetadataList.indexOf(collapsedGm);
    664 
    665             collapseGroup(collapsedGm.gPos);
    666 
    667             /* Decrement index if it is after the group we removed */
    668             if (posMetadata.groupInsertIndex > collapsedIndex) {
    669                 posMetadata.groupInsertIndex--;
    670             }
    671         }
    672 
    673         GroupMetadata expandedGm = GroupMetadata.obtain(
    674                 GroupMetadata.REFRESH,
    675                 GroupMetadata.REFRESH,
    676                 posMetadata.position.groupPos,
    677                 mExpandableListAdapter.getGroupId(posMetadata.position.groupPos));
    678 
    679         mExpGroupMetadataList.add(posMetadata.groupInsertIndex, expandedGm);
    680 
    681         // Refresh the metadata
    682         refreshExpGroupMetadataList(false, false);
    683 
    684         // Notify of change
    685         notifyDataSetChanged();
    686 
    687         // Give the callback
    688         mExpandableListAdapter.onGroupExpanded(expandedGm.gPos);
    689 
    690         return true;
    691     }
    692 
    693     /**
    694      * Whether the given group is currently expanded.
    695      * @param groupPosition The group to check.
    696      * @return Whether the group is currently expanded.
    697      */
    698     public boolean isGroupExpanded(int groupPosition) {
    699         GroupMetadata groupMetadata;
    700         for (int i = mExpGroupMetadataList.size() - 1; i >= 0; i--) {
    701             groupMetadata = mExpGroupMetadataList.get(i);
    702 
    703             if (groupMetadata.gPos == groupPosition) {
    704                 return true;
    705             }
    706         }
    707 
    708         return false;
    709     }
    710 
    711     /**
    712      * Set the maximum number of groups that can be expanded at any given time
    713      */
    714     public void setMaxExpGroupCount(int maxExpGroupCount) {
    715         mMaxExpGroupCount = maxExpGroupCount;
    716     }
    717 
    718     ExpandableListAdapter getAdapter() {
    719         return mExpandableListAdapter;
    720     }
    721 
    722     public Filter getFilter() {
    723         ExpandableListAdapter adapter = getAdapter();
    724         if (adapter instanceof Filterable) {
    725             return ((Filterable) adapter).getFilter();
    726         } else {
    727             return null;
    728         }
    729     }
    730 
    731     ArrayList<GroupMetadata> getExpandedGroupMetadataList() {
    732         return mExpGroupMetadataList;
    733     }
    734 
    735     void setExpandedGroupMetadataList(ArrayList<GroupMetadata> expandedGroupMetadataList) {
    736 
    737         if ((expandedGroupMetadataList == null) || (mExpandableListAdapter == null)) {
    738             return;
    739         }
    740 
    741         // Make sure our current data set is big enough for the previously
    742         // expanded groups, if not, ignore this request
    743         int numGroups = mExpandableListAdapter.getGroupCount();
    744         for (int i = expandedGroupMetadataList.size() - 1; i >= 0; i--) {
    745             if (expandedGroupMetadataList.get(i).gPos >= numGroups) {
    746                 // Doh, for some reason the client doesn't have some of the groups
    747                 return;
    748             }
    749         }
    750 
    751         mExpGroupMetadataList = expandedGroupMetadataList;
    752         refreshExpGroupMetadataList(true, false);
    753     }
    754 
    755     @Override
    756     public boolean isEmpty() {
    757         ExpandableListAdapter adapter = getAdapter();
    758         return adapter != null ? adapter.isEmpty() : true;
    759     }
    760 
    761     /**
    762      * Searches the expandable list adapter for a group position matching the
    763      * given group ID. The search starts at the given seed position and then
    764      * alternates between moving up and moving down until 1) we find the right
    765      * position, or 2) we run out of time, or 3) we have looked at every
    766      * position
    767      *
    768      * @return Position of the row that matches the given row ID, or
    769      *         {@link AdapterView#INVALID_POSITION} if it can't be found
    770      * @see AdapterView#findSyncPosition()
    771      */
    772     int findGroupPosition(long groupIdToMatch, int seedGroupPosition) {
    773         int count = mExpandableListAdapter.getGroupCount();
    774 
    775         if (count == 0) {
    776             return AdapterView.INVALID_POSITION;
    777         }
    778 
    779         // If there isn't a selection don't hunt for it
    780         if (groupIdToMatch == AdapterView.INVALID_ROW_ID) {
    781             return AdapterView.INVALID_POSITION;
    782         }
    783 
    784         // Pin seed to reasonable values
    785         seedGroupPosition = Math.max(0, seedGroupPosition);
    786         seedGroupPosition = Math.min(count - 1, seedGroupPosition);
    787 
    788         long endTime = SystemClock.uptimeMillis() + AdapterView.SYNC_MAX_DURATION_MILLIS;
    789 
    790         long rowId;
    791 
    792         // first position scanned so far
    793         int first = seedGroupPosition;
    794 
    795         // last position scanned so far
    796         int last = seedGroupPosition;
    797 
    798         // True if we should move down on the next iteration
    799         boolean next = false;
    800 
    801         // True when we have looked at the first item in the data
    802         boolean hitFirst;
    803 
    804         // True when we have looked at the last item in the data
    805         boolean hitLast;
    806 
    807         // Get the item ID locally (instead of getItemIdAtPosition), so
    808         // we need the adapter
    809         ExpandableListAdapter adapter = getAdapter();
    810         if (adapter == null) {
    811             return AdapterView.INVALID_POSITION;
    812         }
    813 
    814         while (SystemClock.uptimeMillis() <= endTime) {
    815             rowId = adapter.getGroupId(seedGroupPosition);
    816             if (rowId == groupIdToMatch) {
    817                 // Found it!
    818                 return seedGroupPosition;
    819             }
    820 
    821             hitLast = last == count - 1;
    822             hitFirst = first == 0;
    823 
    824             if (hitLast && hitFirst) {
    825                 // Looked at everything
    826                 break;
    827             }
    828 
    829             if (hitFirst || (next && !hitLast)) {
    830                 // Either we hit the top, or we are trying to move down
    831                 last++;
    832                 seedGroupPosition = last;
    833                 // Try going up next time
    834                 next = false;
    835             } else if (hitLast || (!next && !hitFirst)) {
    836                 // Either we hit the bottom, or we are trying to move up
    837                 first--;
    838                 seedGroupPosition = first;
    839                 // Try going down next time
    840                 next = true;
    841             }
    842 
    843         }
    844 
    845         return AdapterView.INVALID_POSITION;
    846     }
    847 
    848     protected class MyDataSetObserver extends DataSetObserver {
    849         @Override
    850         public void onChanged() {
    851             refreshExpGroupMetadataList(true, true);
    852 
    853             notifyDataSetChanged();
    854         }
    855 
    856         @Override
    857         public void onInvalidated() {
    858             refreshExpGroupMetadataList(true, true);
    859 
    860             notifyDataSetInvalidated();
    861         }
    862     }
    863 
    864     /**
    865      * Metadata about an expanded group to help convert from a flat list
    866      * position to either a) group position for groups, or b) child position for
    867      * children
    868      */
    869     static class GroupMetadata implements Parcelable, Comparable<GroupMetadata> {
    870         final static int REFRESH = -1;
    871 
    872         /** This group's flat list position */
    873         int flPos;
    874 
    875         /* firstChildFlPos isn't needed since it's (flPos + 1) */
    876 
    877         /**
    878          * This group's last child's flat list position, so basically
    879          * the range of this group in the flat list
    880          */
    881         int lastChildFlPos;
    882 
    883         /**
    884          * This group's group position
    885          */
    886         int gPos;
    887 
    888         /**
    889          * This group's id
    890          */
    891         long gId;
    892 
    893         private GroupMetadata() {
    894         }
    895 
    896         static GroupMetadata obtain(int flPos, int lastChildFlPos, int gPos, long gId) {
    897             GroupMetadata gm = new GroupMetadata();
    898             gm.flPos = flPos;
    899             gm.lastChildFlPos = lastChildFlPos;
    900             gm.gPos = gPos;
    901             gm.gId = gId;
    902             return gm;
    903         }
    904 
    905         public int compareTo(GroupMetadata another) {
    906             if (another == null) {
    907                 throw new IllegalArgumentException();
    908             }
    909 
    910             return gPos - another.gPos;
    911         }
    912 
    913         public int describeContents() {
    914             return 0;
    915         }
    916 
    917         public void writeToParcel(Parcel dest, int flags) {
    918             dest.writeInt(flPos);
    919             dest.writeInt(lastChildFlPos);
    920             dest.writeInt(gPos);
    921             dest.writeLong(gId);
    922         }
    923 
    924         public static final Parcelable.Creator<GroupMetadata> CREATOR =
    925                 new Parcelable.Creator<GroupMetadata>() {
    926 
    927             public GroupMetadata createFromParcel(Parcel in) {
    928                 GroupMetadata gm = GroupMetadata.obtain(
    929                         in.readInt(),
    930                         in.readInt(),
    931                         in.readInt(),
    932                         in.readLong());
    933                 return gm;
    934             }
    935 
    936             public GroupMetadata[] newArray(int size) {
    937                 return new GroupMetadata[size];
    938             }
    939         };
    940 
    941     }
    942 
    943     /**
    944      * Data type that contains an expandable list position (can refer to either a group
    945      * or child) and some extra information regarding referred item (such as
    946      * where to insert into the flat list, etc.)
    947      */
    948     static public class PositionMetadata {
    949 
    950         private static final int MAX_POOL_SIZE = 5;
    951         private static ArrayList<PositionMetadata> sPool =
    952                 new ArrayList<PositionMetadata>(MAX_POOL_SIZE);
    953 
    954         /** Data type to hold the position and its type (child/group) */
    955         public ExpandableListPosition position;
    956 
    957         /**
    958          * Link back to the expanded GroupMetadata for this group. Useful for
    959          * removing the group from the list of expanded groups inside the
    960          * connector when we collapse the group, and also as a check to see if
    961          * the group was expanded or collapsed (this will be null if the group
    962          * is collapsed since we don't keep that group's metadata)
    963          */
    964         public GroupMetadata groupMetadata;
    965 
    966         /**
    967          * For groups that are collapsed, we use this as the index (in
    968          * mExpGroupMetadataList) to insert this group when we are expanding
    969          * this group.
    970          */
    971         public int groupInsertIndex;
    972 
    973         private void resetState() {
    974             position = null;
    975             groupMetadata = null;
    976             groupInsertIndex = 0;
    977         }
    978 
    979         /**
    980          * Use {@link #obtain(int, int, int, int, GroupMetadata, int)}
    981          */
    982         private PositionMetadata() {
    983         }
    984 
    985         static PositionMetadata obtain(int flatListPos, int type, int groupPos,
    986                 int childPos, GroupMetadata groupMetadata, int groupInsertIndex) {
    987             PositionMetadata pm = getRecycledOrCreate();
    988             pm.position = ExpandableListPosition.obtain(type, groupPos, childPos, flatListPos);
    989             pm.groupMetadata = groupMetadata;
    990             pm.groupInsertIndex = groupInsertIndex;
    991             return pm;
    992         }
    993 
    994         private static PositionMetadata getRecycledOrCreate() {
    995             PositionMetadata pm;
    996             synchronized (sPool) {
    997                 if (sPool.size() > 0) {
    998                     pm = sPool.remove(0);
    999                 } else {
   1000                     return new PositionMetadata();
   1001                 }
   1002             }
   1003             pm.resetState();
   1004             return pm;
   1005         }
   1006 
   1007         public void recycle() {
   1008             synchronized (sPool) {
   1009                 if (sPool.size() < MAX_POOL_SIZE) {
   1010                     sPool.add(this);
   1011                 }
   1012             }
   1013         }
   1014 
   1015         /**
   1016          * Checks whether the group referred to in this object is expanded,
   1017          * or not (at the time this object was created)
   1018          *
   1019          * @return whether the group at groupPos is expanded or not
   1020          */
   1021         public boolean isExpanded() {
   1022             return groupMetadata != null;
   1023         }
   1024     }
   1025 }
   1026