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;
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 */
50class 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