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 PositionMetadata metadata = getUnflattenedPos(flatListPos);
    376         final ExpandableListPosition pos = metadata.position;
    377 
    378         boolean retValue;
    379         if (pos.type == ExpandableListPosition.CHILD) {
    380             retValue = mExpandableListAdapter.isChildSelectable(pos.groupPos, pos.childPos);
    381         } else {
    382             // Groups are always selectable
    383             retValue = true;
    384         }
    385 
    386         metadata.recycle();
    387 
    388         return retValue;
    389     }
    390 
    391     public int getCount() {
    392         /*
    393          * Total count for the list view is the number groups plus the
    394          * number of children from currently expanded groups (a value we keep
    395          * cached in this class)
    396          */
    397         return mExpandableListAdapter.getGroupCount() + mTotalExpChildrenCount;
    398     }
    399 
    400     public Object getItem(int flatListPos) {
    401         final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
    402 
    403         Object retValue;
    404         if (posMetadata.position.type == ExpandableListPosition.GROUP) {
    405             retValue = mExpandableListAdapter
    406                     .getGroup(posMetadata.position.groupPos);
    407         } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
    408             retValue = mExpandableListAdapter.getChild(posMetadata.position.groupPos,
    409                     posMetadata.position.childPos);
    410         } else {
    411             // TODO: clean exit
    412             throw new RuntimeException("Flat list position is of unknown type");
    413         }
    414 
    415         posMetadata.recycle();
    416 
    417         return retValue;
    418     }
    419 
    420     public long getItemId(int flatListPos) {
    421         final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
    422         final long groupId = mExpandableListAdapter.getGroupId(posMetadata.position.groupPos);
    423 
    424         long retValue;
    425         if (posMetadata.position.type == ExpandableListPosition.GROUP) {
    426             retValue = mExpandableListAdapter.getCombinedGroupId(groupId);
    427         } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
    428             final long childId = mExpandableListAdapter.getChildId(posMetadata.position.groupPos,
    429                     posMetadata.position.childPos);
    430             retValue = mExpandableListAdapter.getCombinedChildId(groupId, childId);
    431         } else {
    432             // TODO: clean exit
    433             throw new RuntimeException("Flat list position is of unknown type");
    434         }
    435 
    436         posMetadata.recycle();
    437 
    438         return retValue;
    439     }
    440 
    441     public View getView(int flatListPos, View convertView, ViewGroup parent) {
    442         final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
    443 
    444         View retValue;
    445         if (posMetadata.position.type == ExpandableListPosition.GROUP) {
    446             retValue = mExpandableListAdapter.getGroupView(posMetadata.position.groupPos,
    447                     posMetadata.isExpanded(), convertView, parent);
    448         } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
    449             final boolean isLastChild = posMetadata.groupMetadata.lastChildFlPos == flatListPos;
    450 
    451             retValue = mExpandableListAdapter.getChildView(posMetadata.position.groupPos,
    452                     posMetadata.position.childPos, isLastChild, convertView, parent);
    453         } else {
    454             // TODO: clean exit
    455             throw new RuntimeException("Flat list position is of unknown type");
    456         }
    457 
    458         posMetadata.recycle();
    459 
    460         return retValue;
    461     }
    462 
    463     @Override
    464     public int getItemViewType(int flatListPos) {
    465         final PositionMetadata metadata = getUnflattenedPos(flatListPos);
    466         final ExpandableListPosition pos = metadata.position;
    467 
    468         int retValue;
    469         if (mExpandableListAdapter instanceof HeterogeneousExpandableList) {
    470             HeterogeneousExpandableList adapter =
    471                     (HeterogeneousExpandableList) mExpandableListAdapter;
    472             if (pos.type == ExpandableListPosition.GROUP) {
    473                 retValue = adapter.getGroupType(pos.groupPos);
    474             } else {
    475                 final int childType = adapter.getChildType(pos.groupPos, pos.childPos);
    476                 retValue = adapter.getGroupTypeCount() + childType;
    477             }
    478         } else {
    479             if (pos.type == ExpandableListPosition.GROUP) {
    480                 retValue = 0;
    481             } else {
    482                 retValue = 1;
    483             }
    484         }
    485 
    486         metadata.recycle();
    487 
    488         return retValue;
    489     }
    490 
    491     @Override
    492     public int getViewTypeCount() {
    493         if (mExpandableListAdapter instanceof HeterogeneousExpandableList) {
    494             HeterogeneousExpandableList adapter =
    495                     (HeterogeneousExpandableList) mExpandableListAdapter;
    496             return adapter.getGroupTypeCount() + adapter.getChildTypeCount();
    497         } else {
    498             return 2;
    499         }
    500     }
    501 
    502     @Override
    503     public boolean hasStableIds() {
    504         return mExpandableListAdapter.hasStableIds();
    505     }
    506 
    507     /**
    508      * Traverses the expanded group metadata list and fills in the flat list
    509      * positions.
    510      *
    511      * @param forceChildrenCountRefresh Forces refreshing of the children count
    512      *        for all expanded groups.
    513      * @param syncGroupPositions Whether to search for the group positions
    514      *         based on the group IDs. This should only be needed when calling
    515      *         this from an onChanged callback.
    516      */
    517     @SuppressWarnings("unchecked")
    518     private void refreshExpGroupMetadataList(boolean forceChildrenCountRefresh,
    519             boolean syncGroupPositions) {
    520         final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
    521         int egmlSize = egml.size();
    522         int curFlPos = 0;
    523 
    524         /* Update child count as we go through */
    525         mTotalExpChildrenCount = 0;
    526 
    527         if (syncGroupPositions) {
    528             // We need to check whether any groups have moved positions
    529             boolean positionsChanged = false;
    530 
    531             for (int i = egmlSize - 1; i >= 0; i--) {
    532                 GroupMetadata curGm = egml.get(i);
    533                 int newGPos = findGroupPosition(curGm.gId, curGm.gPos);
    534                 if (newGPos != curGm.gPos) {
    535                     if (newGPos == AdapterView.INVALID_POSITION) {
    536                         // Doh, just remove it from the list of expanded groups
    537                         egml.remove(i);
    538                         egmlSize--;
    539                     }
    540 
    541                     curGm.gPos = newGPos;
    542                     if (!positionsChanged) positionsChanged = true;
    543                 }
    544             }
    545 
    546             if (positionsChanged) {
    547                 // At least one group changed positions, so re-sort
    548                 Collections.sort(egml);
    549             }
    550         }
    551 
    552         int gChildrenCount;
    553         int lastGPos = 0;
    554         for (int i = 0; i < egmlSize; i++) {
    555             /* Store in local variable since we'll access freq */
    556             GroupMetadata curGm = egml.get(i);
    557 
    558             /*
    559              * Get the number of children, try to refrain from calling
    560              * another class's method unless we have to (so do a subtraction)
    561              */
    562             if ((curGm.lastChildFlPos == GroupMetadata.REFRESH) || forceChildrenCountRefresh) {
    563                 gChildrenCount = mExpandableListAdapter.getChildrenCount(curGm.gPos);
    564             } else {
    565                 /* Num children for this group is its last child's fl pos minus
    566                  * the group's fl pos
    567                  */
    568                 gChildrenCount = curGm.lastChildFlPos - curGm.flPos;
    569             }
    570 
    571             /* Update */
    572             mTotalExpChildrenCount += gChildrenCount;
    573 
    574             /*
    575              * This skips the collapsed groups and increments the flat list
    576              * position (for subsequent exp groups) by accounting for the collapsed
    577              * groups
    578              */
    579             curFlPos += (curGm.gPos - lastGPos);
    580             lastGPos = curGm.gPos;
    581 
    582             /* Update the flat list positions, and the current flat list pos */
    583             curGm.flPos = curFlPos;
    584             curFlPos += gChildrenCount;
    585             curGm.lastChildFlPos = curFlPos;
    586         }
    587     }
    588 
    589     /**
    590      * Collapse a group in the grouped list view
    591      *
    592      * @param groupPos position of the group to collapse
    593      */
    594     boolean collapseGroup(int groupPos) {
    595         ExpandableListPosition elGroupPos = ExpandableListPosition.obtain(
    596                 ExpandableListPosition.GROUP, groupPos, -1, -1);
    597         PositionMetadata pm = getFlattenedPos(elGroupPos);
    598         elGroupPos.recycle();
    599         if (pm == null) return false;
    600 
    601         boolean retValue = collapseGroup(pm);
    602         pm.recycle();
    603         return retValue;
    604     }
    605 
    606     boolean collapseGroup(PositionMetadata posMetadata) {
    607         /*
    608          * Collapsing requires removal from mExpGroupMetadataList
    609          */
    610 
    611         /*
    612          * If it is null, it must be already collapsed. This group metadata
    613          * object should have been set from the search that returned the
    614          * position metadata object.
    615          */
    616         if (posMetadata.groupMetadata == null) return false;
    617 
    618         // Remove the group from the list of expanded groups
    619         mExpGroupMetadataList.remove(posMetadata.groupMetadata);
    620 
    621         // Refresh the metadata
    622         refreshExpGroupMetadataList(false, false);
    623 
    624         // Notify of change
    625         notifyDataSetChanged();
    626 
    627         // Give the callback
    628         mExpandableListAdapter.onGroupCollapsed(posMetadata.groupMetadata.gPos);
    629 
    630         return true;
    631     }
    632 
    633     /**
    634      * Expand a group in the grouped list view
    635      * @param groupPos the group to be expanded
    636      */
    637     boolean expandGroup(int groupPos) {
    638         ExpandableListPosition elGroupPos = ExpandableListPosition.obtain(
    639                 ExpandableListPosition.GROUP, groupPos, -1, -1);
    640         PositionMetadata pm = getFlattenedPos(elGroupPos);
    641         elGroupPos.recycle();
    642         boolean retValue = expandGroup(pm);
    643         pm.recycle();
    644         return retValue;
    645     }
    646 
    647     boolean expandGroup(PositionMetadata posMetadata) {
    648         /*
    649          * Expanding requires insertion into the mExpGroupMetadataList
    650          */
    651 
    652         if (posMetadata.position.groupPos < 0) {
    653             // TODO clean exit
    654             throw new RuntimeException("Need group");
    655         }
    656 
    657         if (mMaxExpGroupCount == 0) return false;
    658 
    659         // Check to see if it's already expanded
    660         if (posMetadata.groupMetadata != null) return false;
    661 
    662         /* Restrict number of expanded groups to mMaxExpGroupCount */
    663         if (mExpGroupMetadataList.size() >= mMaxExpGroupCount) {
    664             /* Collapse a group */
    665             // TODO: Collapse something not on the screen instead of the first one?
    666             // TODO: Could write overloaded function to take GroupMetadata to collapse
    667             GroupMetadata collapsedGm = mExpGroupMetadataList.get(0);
    668 
    669             int collapsedIndex = mExpGroupMetadataList.indexOf(collapsedGm);
    670 
    671             collapseGroup(collapsedGm.gPos);
    672 
    673             /* Decrement index if it is after the group we removed */
    674             if (posMetadata.groupInsertIndex > collapsedIndex) {
    675                 posMetadata.groupInsertIndex--;
    676             }
    677         }
    678 
    679         GroupMetadata expandedGm = GroupMetadata.obtain(
    680                 GroupMetadata.REFRESH,
    681                 GroupMetadata.REFRESH,
    682                 posMetadata.position.groupPos,
    683                 mExpandableListAdapter.getGroupId(posMetadata.position.groupPos));
    684 
    685         mExpGroupMetadataList.add(posMetadata.groupInsertIndex, expandedGm);
    686 
    687         // Refresh the metadata
    688         refreshExpGroupMetadataList(false, false);
    689 
    690         // Notify of change
    691         notifyDataSetChanged();
    692 
    693         // Give the callback
    694         mExpandableListAdapter.onGroupExpanded(expandedGm.gPos);
    695 
    696         return true;
    697     }
    698 
    699     /**
    700      * Whether the given group is currently expanded.
    701      * @param groupPosition The group to check.
    702      * @return Whether the group is currently expanded.
    703      */
    704     public boolean isGroupExpanded(int groupPosition) {
    705         GroupMetadata groupMetadata;
    706         for (int i = mExpGroupMetadataList.size() - 1; i >= 0; i--) {
    707             groupMetadata = mExpGroupMetadataList.get(i);
    708 
    709             if (groupMetadata.gPos == groupPosition) {
    710                 return true;
    711             }
    712         }
    713 
    714         return false;
    715     }
    716 
    717     /**
    718      * Set the maximum number of groups that can be expanded at any given time
    719      */
    720     public void setMaxExpGroupCount(int maxExpGroupCount) {
    721         mMaxExpGroupCount = maxExpGroupCount;
    722     }
    723 
    724     ExpandableListAdapter getAdapter() {
    725         return mExpandableListAdapter;
    726     }
    727 
    728     public Filter getFilter() {
    729         ExpandableListAdapter adapter = getAdapter();
    730         if (adapter instanceof Filterable) {
    731             return ((Filterable) adapter).getFilter();
    732         } else {
    733             return null;
    734         }
    735     }
    736 
    737     ArrayList<GroupMetadata> getExpandedGroupMetadataList() {
    738         return mExpGroupMetadataList;
    739     }
    740 
    741     void setExpandedGroupMetadataList(ArrayList<GroupMetadata> expandedGroupMetadataList) {
    742 
    743         if ((expandedGroupMetadataList == null) || (mExpandableListAdapter == null)) {
    744             return;
    745         }
    746 
    747         // Make sure our current data set is big enough for the previously
    748         // expanded groups, if not, ignore this request
    749         int numGroups = mExpandableListAdapter.getGroupCount();
    750         for (int i = expandedGroupMetadataList.size() - 1; i >= 0; i--) {
    751             if (expandedGroupMetadataList.get(i).gPos >= numGroups) {
    752                 // Doh, for some reason the client doesn't have some of the groups
    753                 return;
    754             }
    755         }
    756 
    757         mExpGroupMetadataList = expandedGroupMetadataList;
    758         refreshExpGroupMetadataList(true, false);
    759     }
    760 
    761     @Override
    762     public boolean isEmpty() {
    763         ExpandableListAdapter adapter = getAdapter();
    764         return adapter != null ? adapter.isEmpty() : true;
    765     }
    766 
    767     /**
    768      * Searches the expandable list adapter for a group position matching the
    769      * given group ID. The search starts at the given seed position and then
    770      * alternates between moving up and moving down until 1) we find the right
    771      * position, or 2) we run out of time, or 3) we have looked at every
    772      * position
    773      *
    774      * @return Position of the row that matches the given row ID, or
    775      *         {@link AdapterView#INVALID_POSITION} if it can't be found
    776      * @see AdapterView#findSyncPosition()
    777      */
    778     int findGroupPosition(long groupIdToMatch, int seedGroupPosition) {
    779         int count = mExpandableListAdapter.getGroupCount();
    780 
    781         if (count == 0) {
    782             return AdapterView.INVALID_POSITION;
    783         }
    784 
    785         // If there isn't a selection don't hunt for it
    786         if (groupIdToMatch == AdapterView.INVALID_ROW_ID) {
    787             return AdapterView.INVALID_POSITION;
    788         }
    789 
    790         // Pin seed to reasonable values
    791         seedGroupPosition = Math.max(0, seedGroupPosition);
    792         seedGroupPosition = Math.min(count - 1, seedGroupPosition);
    793 
    794         long endTime = SystemClock.uptimeMillis() + AdapterView.SYNC_MAX_DURATION_MILLIS;
    795 
    796         long rowId;
    797 
    798         // first position scanned so far
    799         int first = seedGroupPosition;
    800 
    801         // last position scanned so far
    802         int last = seedGroupPosition;
    803 
    804         // True if we should move down on the next iteration
    805         boolean next = false;
    806 
    807         // True when we have looked at the first item in the data
    808         boolean hitFirst;
    809 
    810         // True when we have looked at the last item in the data
    811         boolean hitLast;
    812 
    813         // Get the item ID locally (instead of getItemIdAtPosition), so
    814         // we need the adapter
    815         ExpandableListAdapter adapter = getAdapter();
    816         if (adapter == null) {
    817             return AdapterView.INVALID_POSITION;
    818         }
    819 
    820         while (SystemClock.uptimeMillis() <= endTime) {
    821             rowId = adapter.getGroupId(seedGroupPosition);
    822             if (rowId == groupIdToMatch) {
    823                 // Found it!
    824                 return seedGroupPosition;
    825             }
    826 
    827             hitLast = last == count - 1;
    828             hitFirst = first == 0;
    829 
    830             if (hitLast && hitFirst) {
    831                 // Looked at everything
    832                 break;
    833             }
    834 
    835             if (hitFirst || (next && !hitLast)) {
    836                 // Either we hit the top, or we are trying to move down
    837                 last++;
    838                 seedGroupPosition = last;
    839                 // Try going up next time
    840                 next = false;
    841             } else if (hitLast || (!next && !hitFirst)) {
    842                 // Either we hit the bottom, or we are trying to move up
    843                 first--;
    844                 seedGroupPosition = first;
    845                 // Try going down next time
    846                 next = true;
    847             }
    848 
    849         }
    850 
    851         return AdapterView.INVALID_POSITION;
    852     }
    853 
    854     protected class MyDataSetObserver extends DataSetObserver {
    855         @Override
    856         public void onChanged() {
    857             refreshExpGroupMetadataList(true, true);
    858 
    859             notifyDataSetChanged();
    860         }
    861 
    862         @Override
    863         public void onInvalidated() {
    864             refreshExpGroupMetadataList(true, true);
    865 
    866             notifyDataSetInvalidated();
    867         }
    868     }
    869 
    870     /**
    871      * Metadata about an expanded group to help convert from a flat list
    872      * position to either a) group position for groups, or b) child position for
    873      * children
    874      */
    875     static class GroupMetadata implements Parcelable, Comparable<GroupMetadata> {
    876         final static int REFRESH = -1;
    877 
    878         /** This group's flat list position */
    879         int flPos;
    880 
    881         /* firstChildFlPos isn't needed since it's (flPos + 1) */
    882 
    883         /**
    884          * This group's last child's flat list position, so basically
    885          * the range of this group in the flat list
    886          */
    887         int lastChildFlPos;
    888 
    889         /**
    890          * This group's group position
    891          */
    892         int gPos;
    893 
    894         /**
    895          * This group's id
    896          */
    897         long gId;
    898 
    899         private GroupMetadata() {
    900         }
    901 
    902         static GroupMetadata obtain(int flPos, int lastChildFlPos, int gPos, long gId) {
    903             GroupMetadata gm = new GroupMetadata();
    904             gm.flPos = flPos;
    905             gm.lastChildFlPos = lastChildFlPos;
    906             gm.gPos = gPos;
    907             gm.gId = gId;
    908             return gm;
    909         }
    910 
    911         public int compareTo(GroupMetadata another) {
    912             if (another == null) {
    913                 throw new IllegalArgumentException();
    914             }
    915 
    916             return gPos - another.gPos;
    917         }
    918 
    919         public int describeContents() {
    920             return 0;
    921         }
    922 
    923         public void writeToParcel(Parcel dest, int flags) {
    924             dest.writeInt(flPos);
    925             dest.writeInt(lastChildFlPos);
    926             dest.writeInt(gPos);
    927             dest.writeLong(gId);
    928         }
    929 
    930         public static final Parcelable.Creator<GroupMetadata> CREATOR =
    931                 new Parcelable.Creator<GroupMetadata>() {
    932 
    933             public GroupMetadata createFromParcel(Parcel in) {
    934                 GroupMetadata gm = GroupMetadata.obtain(
    935                         in.readInt(),
    936                         in.readInt(),
    937                         in.readInt(),
    938                         in.readLong());
    939                 return gm;
    940             }
    941 
    942             public GroupMetadata[] newArray(int size) {
    943                 return new GroupMetadata[size];
    944             }
    945         };
    946 
    947     }
    948 
    949     /**
    950      * Data type that contains an expandable list position (can refer to either a group
    951      * or child) and some extra information regarding referred item (such as
    952      * where to insert into the flat list, etc.)
    953      */
    954     static public class PositionMetadata {
    955 
    956         private static final int MAX_POOL_SIZE = 5;
    957         private static ArrayList<PositionMetadata> sPool =
    958                 new ArrayList<PositionMetadata>(MAX_POOL_SIZE);
    959 
    960         /** Data type to hold the position and its type (child/group) */
    961         public ExpandableListPosition position;
    962 
    963         /**
    964          * Link back to the expanded GroupMetadata for this group. Useful for
    965          * removing the group from the list of expanded groups inside the
    966          * connector when we collapse the group, and also as a check to see if
    967          * the group was expanded or collapsed (this will be null if the group
    968          * is collapsed since we don't keep that group's metadata)
    969          */
    970         public GroupMetadata groupMetadata;
    971 
    972         /**
    973          * For groups that are collapsed, we use this as the index (in
    974          * mExpGroupMetadataList) to insert this group when we are expanding
    975          * this group.
    976          */
    977         public int groupInsertIndex;
    978 
    979         private void resetState() {
    980             if (position != null) {
    981                 position.recycle();
    982                 position = null;
    983             }
    984             groupMetadata = null;
    985             groupInsertIndex = 0;
    986         }
    987 
    988         /**
    989          * Use {@link #obtain(int, int, int, int, GroupMetadata, int)}
    990          */
    991         private PositionMetadata() {
    992         }
    993 
    994         static PositionMetadata obtain(int flatListPos, int type, int groupPos,
    995                 int childPos, GroupMetadata groupMetadata, int groupInsertIndex) {
    996             PositionMetadata pm = getRecycledOrCreate();
    997             pm.position = ExpandableListPosition.obtain(type, groupPos, childPos, flatListPos);
    998             pm.groupMetadata = groupMetadata;
    999             pm.groupInsertIndex = groupInsertIndex;
   1000             return pm;
   1001         }
   1002 
   1003         private static PositionMetadata getRecycledOrCreate() {
   1004             PositionMetadata pm;
   1005             synchronized (sPool) {
   1006                 if (sPool.size() > 0) {
   1007                     pm = sPool.remove(0);
   1008                 } else {
   1009                     return new PositionMetadata();
   1010                 }
   1011             }
   1012             pm.resetState();
   1013             return pm;
   1014         }
   1015 
   1016         public void recycle() {
   1017             resetState();
   1018             synchronized (sPool) {
   1019                 if (sPool.size() < MAX_POOL_SIZE) {
   1020                     sPool.add(this);
   1021                 }
   1022             }
   1023         }
   1024 
   1025         /**
   1026          * Checks whether the group referred to in this object is expanded,
   1027          * or not (at the time this object was created)
   1028          *
   1029          * @return whether the group at groupPos is expanded or not
   1030          */
   1031         public boolean isExpanded() {
   1032             return groupMetadata != null;
   1033         }
   1034     }
   1035 }
   1036