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      */
ExpandableListConnector(ExpandableListAdapter expandableListAdapter)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      */
setExpandableListAdapter(ExpandableListAdapter expandableListAdapter)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      */
getUnflattenedPos(final int flPos)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      */
getFlattenedPos(final ExpandableListPosition pos)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
areAllItemsEnabled()369     public boolean areAllItemsEnabled() {
370         return mExpandableListAdapter.areAllItemsEnabled();
371     }
372 
373     @Override
isEnabled(int flatListPos)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 
getCount()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 
getItem(int flatListPos)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 
getItemId(int flatListPos)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 
getView(int flatListPos, View convertView, ViewGroup parent)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
getItemViewType(int flatListPos)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
getViewTypeCount()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
hasStableIds()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")
refreshExpGroupMetadataList(boolean forceChildrenCountRefresh, boolean syncGroupPositions)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      */
collapseGroup(int groupPos)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 
collapseGroup(PositionMetadata posMetadata)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      */
expandGroup(int groupPos)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 
expandGroup(PositionMetadata posMetadata)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      */
isGroupExpanded(int groupPosition)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      */
setMaxExpGroupCount(int maxExpGroupCount)720     public void setMaxExpGroupCount(int maxExpGroupCount) {
721         mMaxExpGroupCount = maxExpGroupCount;
722     }
723 
getAdapter()724     ExpandableListAdapter getAdapter() {
725         return mExpandableListAdapter;
726     }
727 
getFilter()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 
getExpandedGroupMetadataList()737     ArrayList<GroupMetadata> getExpandedGroupMetadataList() {
738         return mExpGroupMetadataList;
739     }
740 
setExpandedGroupMetadataList(ArrayList<GroupMetadata> expandedGroupMetadataList)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
isEmpty()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      */
findGroupPosition(long groupIdToMatch, int seedGroupPosition)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
onChanged()856         public void onChanged() {
857             refreshExpGroupMetadataList(true, true);
858 
859             notifyDataSetChanged();
860         }
861 
862         @Override
onInvalidated()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 
GroupMetadata()899         private GroupMetadata() {
900         }
901 
obtain(int flPos, int lastChildFlPos, int gPos, long gId)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 
compareTo(GroupMetadata another)911         public int compareTo(GroupMetadata another) {
912             if (another == null) {
913                 throw new IllegalArgumentException();
914             }
915 
916             return gPos - another.gPos;
917         }
918 
describeContents()919         public int describeContents() {
920             return 0;
921         }
922 
writeToParcel(Parcel dest, int flags)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 @android.annotation.NonNull 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 
resetState()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          */
PositionMetadata()991         private PositionMetadata() {
992         }
993 
obtain(int flatListPos, int type, int groupPos, int childPos, GroupMetadata groupMetadata, int groupInsertIndex)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 
getRecycledOrCreate()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 
recycle()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          */
isExpanded()1031         public boolean isExpanded() {
1032             return groupMetadata != null;
1033         }
1034     }
1035 }
1036