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