1/*
2 * Copyright (C) 2006 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 com.android.internal.R;
20
21import android.content.Context;
22import android.content.res.TypedArray;
23import android.graphics.Canvas;
24import android.graphics.Rect;
25import android.graphics.drawable.Drawable;
26import android.os.Parcel;
27import android.os.Parcelable;
28import android.util.AttributeSet;
29import android.view.ContextMenu;
30import android.view.SoundEffectConstants;
31import android.view.View;
32import android.view.ContextMenu.ContextMenuInfo;
33import android.view.accessibility.AccessibilityEvent;
34import android.view.accessibility.AccessibilityNodeInfo;
35import android.widget.ExpandableListConnector.PositionMetadata;
36
37import java.util.ArrayList;
38
39import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
40
41/**
42 * A view that shows items in a vertically scrolling two-level list. This
43 * differs from the {@link ListView} by allowing two levels: groups which can
44 * individually be expanded to show its children. The items come from the
45 * {@link ExpandableListAdapter} associated with this view.
46 * <p>
47 * Expandable lists are able to show an indicator beside each item to display
48 * the item's current state (the states are usually one of expanded group,
49 * collapsed group, child, or last child). Use
50 * {@link #setChildIndicator(Drawable)} or {@link #setGroupIndicator(Drawable)}
51 * (or the corresponding XML attributes) to set these indicators (see the docs
52 * for each method to see additional state that each Drawable can have). The
53 * default style for an {@link ExpandableListView} provides indicators which
54 * will be shown next to Views given to the {@link ExpandableListView}. The
55 * layouts android.R.layout.simple_expandable_list_item_1 and
56 * android.R.layout.simple_expandable_list_item_2 (which should be used with
57 * {@link SimpleCursorTreeAdapter}) contain the preferred position information
58 * for indicators.
59 * <p>
60 * The context menu information set by an {@link ExpandableListView} will be a
61 * {@link ExpandableListContextMenuInfo} object with
62 * {@link ExpandableListContextMenuInfo#packedPosition} being a packed position
63 * that can be used with {@link #getPackedPositionType(long)} and the other
64 * similar methods.
65 * <p>
66 * <em><b>Note:</b></em> You cannot use the value <code>wrap_content</code>
67 * for the <code>android:layout_height</code> attribute of a
68 * ExpandableListView in XML if the parent's size is also not strictly specified
69 * (for example, if the parent were ScrollView you could not specify
70 * wrap_content since it also can be any length. However, you can use
71 * wrap_content if the ExpandableListView parent has a specific size, such as
72 * 100 pixels.
73 *
74 * @attr ref android.R.styleable#ExpandableListView_groupIndicator
75 * @attr ref android.R.styleable#ExpandableListView_indicatorLeft
76 * @attr ref android.R.styleable#ExpandableListView_indicatorRight
77 * @attr ref android.R.styleable#ExpandableListView_childIndicator
78 * @attr ref android.R.styleable#ExpandableListView_childIndicatorLeft
79 * @attr ref android.R.styleable#ExpandableListView_childIndicatorRight
80 * @attr ref android.R.styleable#ExpandableListView_childDivider
81 * @attr ref android.R.styleable#ExpandableListView_indicatorStart
82 * @attr ref android.R.styleable#ExpandableListView_indicatorEnd
83 * @attr ref android.R.styleable#ExpandableListView_childIndicatorStart
84 * @attr ref android.R.styleable#ExpandableListView_childIndicatorEnd
85 */
86public class ExpandableListView extends ListView {
87
88    /**
89     * The packed position represents a group.
90     */
91    public static final int PACKED_POSITION_TYPE_GROUP = 0;
92
93    /**
94     * The packed position represents a child.
95     */
96    public static final int PACKED_POSITION_TYPE_CHILD = 1;
97
98    /**
99     * The packed position represents a neither/null/no preference.
100     */
101    public static final int PACKED_POSITION_TYPE_NULL = 2;
102
103    /**
104     * The value for a packed position that represents neither/null/no
105     * preference. This value is not otherwise possible since a group type
106     * (first bit 0) should not have a child position filled.
107     */
108    public static final long PACKED_POSITION_VALUE_NULL = 0x00000000FFFFFFFFL;
109
110    /** The mask (in packed position representation) for the child */
111    private static final long PACKED_POSITION_MASK_CHILD = 0x00000000FFFFFFFFL;
112
113    /** The mask (in packed position representation) for the group */
114    private static final long PACKED_POSITION_MASK_GROUP = 0x7FFFFFFF00000000L;
115
116    /** The mask (in packed position representation) for the type */
117    private static final long PACKED_POSITION_MASK_TYPE  = 0x8000000000000000L;
118
119    /** The shift amount (in packed position representation) for the group */
120    private static final long PACKED_POSITION_SHIFT_GROUP = 32;
121
122    /** The shift amount (in packed position representation) for the type */
123    private static final long PACKED_POSITION_SHIFT_TYPE  = 63;
124
125    /** The mask (in integer child position representation) for the child */
126    private static final long PACKED_POSITION_INT_MASK_CHILD = 0xFFFFFFFF;
127
128    /** The mask (in integer group position representation) for the group */
129    private static final long PACKED_POSITION_INT_MASK_GROUP = 0x7FFFFFFF;
130
131    /** Serves as the glue/translator between a ListView and an ExpandableListView */
132    private ExpandableListConnector mConnector;
133
134    /** Gives us Views through group+child positions */
135    private ExpandableListAdapter mAdapter;
136
137    /** Left bound for drawing the indicator. */
138    private int mIndicatorLeft;
139
140    /** Right bound for drawing the indicator. */
141    private int mIndicatorRight;
142
143    /** Start bound for drawing the indicator. */
144    private int mIndicatorStart;
145
146    /** End bound for drawing the indicator. */
147    private int mIndicatorEnd;
148
149    /**
150     * Left bound for drawing the indicator of a child. Value of
151     * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorLeft.
152     */
153    private int mChildIndicatorLeft;
154
155    /**
156     * Right bound for drawing the indicator of a child. Value of
157     * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorRight.
158     */
159    private int mChildIndicatorRight;
160
161    /**
162     * Start bound for drawing the indicator of a child. Value of
163     * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorStart.
164     */
165    private int mChildIndicatorStart;
166
167    /**
168     * End bound for drawing the indicator of a child. Value of
169     * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorEnd.
170     */
171    private int mChildIndicatorEnd;
172
173    /**
174     * Denotes when a child indicator should inherit this bound from the generic
175     * indicator bounds
176     */
177    public static final int CHILD_INDICATOR_INHERIT = -1;
178
179    /**
180     * Denotes an undefined value for an indicator
181     */
182    private static final int INDICATOR_UNDEFINED = -2;
183
184    /** The indicator drawn next to a group. */
185    private Drawable mGroupIndicator;
186
187    /** The indicator drawn next to a child. */
188    private Drawable mChildIndicator;
189
190    private static final int[] EMPTY_STATE_SET = {};
191
192    /** State indicating the group is expanded. */
193    private static final int[] GROUP_EXPANDED_STATE_SET =
194            {R.attr.state_expanded};
195
196    /** State indicating the group is empty (has no children). */
197    private static final int[] GROUP_EMPTY_STATE_SET =
198            {R.attr.state_empty};
199
200    /** State indicating the group is expanded and empty (has no children). */
201    private static final int[] GROUP_EXPANDED_EMPTY_STATE_SET =
202            {R.attr.state_expanded, R.attr.state_empty};
203
204    /** States for the group where the 0th bit is expanded and 1st bit is empty. */
205    private static final int[][] GROUP_STATE_SETS = {
206         EMPTY_STATE_SET, // 00
207         GROUP_EXPANDED_STATE_SET, // 01
208         GROUP_EMPTY_STATE_SET, // 10
209         GROUP_EXPANDED_EMPTY_STATE_SET // 11
210    };
211
212    /** State indicating the child is the last within its group. */
213    private static final int[] CHILD_LAST_STATE_SET =
214            {R.attr.state_last};
215
216    /** Drawable to be used as a divider when it is adjacent to any children */
217    private Drawable mChildDivider;
218
219    // Bounds of the indicator to be drawn
220    private final Rect mIndicatorRect = new Rect();
221
222    public ExpandableListView(Context context) {
223        this(context, null);
224    }
225
226    public ExpandableListView(Context context, AttributeSet attrs) {
227        this(context, attrs, com.android.internal.R.attr.expandableListViewStyle);
228    }
229
230    public ExpandableListView(Context context, AttributeSet attrs, int defStyleAttr) {
231        this(context, attrs, defStyleAttr, 0);
232    }
233
234    public ExpandableListView(
235            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
236        super(context, attrs, defStyleAttr, defStyleRes);
237
238        final TypedArray a = context.obtainStyledAttributes(attrs,
239                com.android.internal.R.styleable.ExpandableListView, defStyleAttr, defStyleRes);
240
241        mGroupIndicator = a.getDrawable(
242                com.android.internal.R.styleable.ExpandableListView_groupIndicator);
243        mChildIndicator = a.getDrawable(
244                com.android.internal.R.styleable.ExpandableListView_childIndicator);
245        mIndicatorLeft = a.getDimensionPixelSize(
246                com.android.internal.R.styleable.ExpandableListView_indicatorLeft, 0);
247        mIndicatorRight = a.getDimensionPixelSize(
248                com.android.internal.R.styleable.ExpandableListView_indicatorRight, 0);
249        if (mIndicatorRight == 0 && mGroupIndicator != null) {
250            mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth();
251        }
252        mChildIndicatorLeft = a.getDimensionPixelSize(
253                com.android.internal.R.styleable.ExpandableListView_childIndicatorLeft,
254                CHILD_INDICATOR_INHERIT);
255        mChildIndicatorRight = a.getDimensionPixelSize(
256                com.android.internal.R.styleable.ExpandableListView_childIndicatorRight,
257                CHILD_INDICATOR_INHERIT);
258        mChildDivider = a.getDrawable(
259                com.android.internal.R.styleable.ExpandableListView_childDivider);
260
261        if (!isRtlCompatibilityMode()) {
262            mIndicatorStart = a.getDimensionPixelSize(
263                    com.android.internal.R.styleable.ExpandableListView_indicatorStart,
264                    INDICATOR_UNDEFINED);
265            mIndicatorEnd = a.getDimensionPixelSize(
266                    com.android.internal.R.styleable.ExpandableListView_indicatorEnd,
267                    INDICATOR_UNDEFINED);
268
269            mChildIndicatorStart = a.getDimensionPixelSize(
270                    com.android.internal.R.styleable.ExpandableListView_childIndicatorStart,
271                    CHILD_INDICATOR_INHERIT);
272            mChildIndicatorEnd = a.getDimensionPixelSize(
273                    com.android.internal.R.styleable.ExpandableListView_childIndicatorEnd,
274                    CHILD_INDICATOR_INHERIT);
275        }
276
277        a.recycle();
278    }
279
280    /**
281     * Return true if we are in RTL compatibility mode (either before Jelly Bean MR1 or
282     * RTL not supported)
283     */
284    private boolean isRtlCompatibilityMode() {
285        final int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
286        return targetSdkVersion < JELLY_BEAN_MR1 || !hasRtlSupport();
287    }
288
289    /**
290     * Return true if the application tag in the AndroidManifest has set "supportRtl" to true
291     */
292    private boolean hasRtlSupport() {
293        return mContext.getApplicationInfo().hasRtlSupport();
294    }
295
296    public void onRtlPropertiesChanged(int layoutDirection) {
297        resolveIndicator();
298        resolveChildIndicator();
299    }
300
301    /**
302     * Resolve start/end indicator. start/end indicator always takes precedence over left/right
303     * indicator when defined.
304     */
305    private void resolveIndicator() {
306        final boolean isLayoutRtl = isLayoutRtl();
307        if (isLayoutRtl) {
308            if (mIndicatorStart >= 0) {
309                mIndicatorRight = mIndicatorStart;
310            }
311            if (mIndicatorEnd >= 0) {
312                mIndicatorLeft = mIndicatorEnd;
313            }
314        } else {
315            if (mIndicatorStart >= 0) {
316                mIndicatorLeft = mIndicatorStart;
317            }
318            if (mIndicatorEnd >= 0) {
319                mIndicatorRight = mIndicatorEnd;
320            }
321        }
322        if (mIndicatorRight == 0 && mGroupIndicator != null) {
323            mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth();
324        }
325    }
326
327    /**
328     * Resolve start/end child indicator. start/end child indicator always takes precedence over
329     * left/right child indicator when defined.
330     */
331    private void resolveChildIndicator() {
332        final boolean isLayoutRtl = isLayoutRtl();
333        if (isLayoutRtl) {
334            if (mChildIndicatorStart >= CHILD_INDICATOR_INHERIT) {
335                mChildIndicatorRight = mChildIndicatorStart;
336            }
337            if (mChildIndicatorEnd >= CHILD_INDICATOR_INHERIT) {
338                mChildIndicatorLeft = mChildIndicatorEnd;
339            }
340        } else {
341            if (mChildIndicatorStart >= CHILD_INDICATOR_INHERIT) {
342                mChildIndicatorLeft = mChildIndicatorStart;
343            }
344            if (mChildIndicatorEnd >= CHILD_INDICATOR_INHERIT) {
345                mChildIndicatorRight = mChildIndicatorEnd;
346            }
347        }
348    }
349
350    @Override
351    protected void dispatchDraw(Canvas canvas) {
352        // Draw children, etc.
353        super.dispatchDraw(canvas);
354
355        // If we have any indicators to draw, we do it here
356        if ((mChildIndicator == null) && (mGroupIndicator == null)) {
357            return;
358        }
359
360        int saveCount = 0;
361        final boolean clipToPadding = (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
362        if (clipToPadding) {
363            saveCount = canvas.save();
364            final int scrollX = mScrollX;
365            final int scrollY = mScrollY;
366            canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
367                    scrollX + mRight - mLeft - mPaddingRight,
368                    scrollY + mBottom - mTop - mPaddingBottom);
369        }
370
371        final int headerViewsCount = getHeaderViewsCount();
372
373        final int lastChildFlPos = mItemCount - getFooterViewsCount() - headerViewsCount - 1;
374
375        final int myB = mBottom;
376
377        PositionMetadata pos;
378        View item;
379        Drawable indicator;
380        int t, b;
381
382        // Start at a value that is neither child nor group
383        int lastItemType = ~(ExpandableListPosition.CHILD | ExpandableListPosition.GROUP);
384
385        final Rect indicatorRect = mIndicatorRect;
386
387        // The "child" mentioned in the following two lines is this
388        // View's child, not referring to an expandable list's
389        // notion of a child (as opposed to a group)
390        final int childCount = getChildCount();
391        for (int i = 0, childFlPos = mFirstPosition - headerViewsCount; i < childCount;
392             i++, childFlPos++) {
393
394            if (childFlPos < 0) {
395                // This child is header
396                continue;
397            } else if (childFlPos > lastChildFlPos) {
398                // This child is footer, so are all subsequent children
399                break;
400            }
401
402            item = getChildAt(i);
403            t = item.getTop();
404            b = item.getBottom();
405
406            // This item isn't on the screen
407            if ((b < 0) || (t > myB)) continue;
408
409            // Get more expandable list-related info for this item
410            pos = mConnector.getUnflattenedPos(childFlPos);
411
412            final boolean isLayoutRtl = isLayoutRtl();
413            final int width = getWidth();
414
415            // If this item type and the previous item type are different, then we need to change
416            // the left & right bounds
417            if (pos.position.type != lastItemType) {
418                if (pos.position.type == ExpandableListPosition.CHILD) {
419                    indicatorRect.left = (mChildIndicatorLeft == CHILD_INDICATOR_INHERIT) ?
420                            mIndicatorLeft : mChildIndicatorLeft;
421                    indicatorRect.right = (mChildIndicatorRight == CHILD_INDICATOR_INHERIT) ?
422                            mIndicatorRight : mChildIndicatorRight;
423                } else {
424                    indicatorRect.left = mIndicatorLeft;
425                    indicatorRect.right = mIndicatorRight;
426                }
427
428                if (isLayoutRtl) {
429                    final int temp = indicatorRect.left;
430                    indicatorRect.left = width - indicatorRect.right;
431                    indicatorRect.right = width - temp;
432
433                    indicatorRect.left -= mPaddingRight;
434                    indicatorRect.right -= mPaddingRight;
435                } else {
436                    indicatorRect.left += mPaddingLeft;
437                    indicatorRect.right += mPaddingLeft;
438                }
439
440                lastItemType = pos.position.type;
441            }
442
443            if (indicatorRect.left != indicatorRect.right) {
444                // Use item's full height + the divider height
445                if (mStackFromBottom) {
446                    // See ListView#dispatchDraw
447                    indicatorRect.top = t;// - mDividerHeight;
448                    indicatorRect.bottom = b;
449                } else {
450                    indicatorRect.top = t;
451                    indicatorRect.bottom = b;// + mDividerHeight;
452                }
453
454                // Get the indicator (with its state set to the item's state)
455                indicator = getIndicator(pos);
456                if (indicator != null) {
457                    // Draw the indicator
458                    indicator.setBounds(indicatorRect);
459                    indicator.draw(canvas);
460                }
461            }
462            pos.recycle();
463        }
464
465        if (clipToPadding) {
466            canvas.restoreToCount(saveCount);
467        }
468    }
469
470    /**
471     * Gets the indicator for the item at the given position. If the indicator
472     * is stateful, the state will be given to the indicator.
473     *
474     * @param pos The flat list position of the item whose indicator
475     *            should be returned.
476     * @return The indicator in the proper state.
477     */
478    private Drawable getIndicator(PositionMetadata pos) {
479        Drawable indicator;
480
481        if (pos.position.type == ExpandableListPosition.GROUP) {
482            indicator = mGroupIndicator;
483
484            if (indicator != null && indicator.isStateful()) {
485                // Empty check based on availability of data.  If the groupMetadata isn't null,
486                // we do a check on it. Otherwise, the group is collapsed so we consider it
487                // empty for performance reasons.
488                boolean isEmpty = (pos.groupMetadata == null) ||
489                        (pos.groupMetadata.lastChildFlPos == pos.groupMetadata.flPos);
490
491                final int stateSetIndex =
492                    (pos.isExpanded() ? 1 : 0) | // Expanded?
493                    (isEmpty ? 2 : 0); // Empty?
494                indicator.setState(GROUP_STATE_SETS[stateSetIndex]);
495            }
496        } else {
497            indicator = mChildIndicator;
498
499            if (indicator != null && indicator.isStateful()) {
500                // No need for a state sets array for the child since it only has two states
501                final int stateSet[] = pos.position.flatListPos == pos.groupMetadata.lastChildFlPos
502                        ? CHILD_LAST_STATE_SET
503                        : EMPTY_STATE_SET;
504                indicator.setState(stateSet);
505            }
506        }
507
508        return indicator;
509    }
510
511    /**
512     * Sets the drawable that will be drawn adjacent to every child in the list. This will
513     * be drawn using the same height as the normal divider ({@link #setDivider(Drawable)}) or
514     * if it does not have an intrinsic height, the height set by {@link #setDividerHeight(int)}.
515     *
516     * @param childDivider The drawable to use.
517     */
518    public void setChildDivider(Drawable childDivider) {
519        mChildDivider = childDivider;
520    }
521
522    @Override
523    void drawDivider(Canvas canvas, Rect bounds, int childIndex) {
524        int flatListPosition = childIndex + mFirstPosition;
525
526        // Only proceed as possible child if the divider isn't above all items (if it is above
527        // all items, then the item below it has to be a group)
528        if (flatListPosition >= 0) {
529            final int adjustedPosition = getFlatPositionForConnector(flatListPosition);
530            PositionMetadata pos = mConnector.getUnflattenedPos(adjustedPosition);
531            // If this item is a child, or it is a non-empty group that is expanded
532            if ((pos.position.type == ExpandableListPosition.CHILD) || (pos.isExpanded() &&
533                    pos.groupMetadata.lastChildFlPos != pos.groupMetadata.flPos)) {
534                // These are the cases where we draw the child divider
535                final Drawable divider = mChildDivider;
536                divider.setBounds(bounds);
537                divider.draw(canvas);
538                pos.recycle();
539                return;
540            }
541            pos.recycle();
542        }
543
544        // Otherwise draw the default divider
545        super.drawDivider(canvas, bounds, flatListPosition);
546    }
547
548    /**
549     * This overloaded method should not be used, instead use
550     * {@link #setAdapter(ExpandableListAdapter)}.
551     * <p>
552     * {@inheritDoc}
553     */
554    @Override
555    public void setAdapter(ListAdapter adapter) {
556        throw new RuntimeException(
557                "For ExpandableListView, use setAdapter(ExpandableListAdapter) instead of " +
558                "setAdapter(ListAdapter)");
559    }
560
561    /**
562     * This method should not be used, use {@link #getExpandableListAdapter()}.
563     */
564    @Override
565    public ListAdapter getAdapter() {
566        /*
567         * The developer should never really call this method on an
568         * ExpandableListView, so it would be nice to throw a RuntimeException,
569         * but AdapterView calls this
570         */
571        return super.getAdapter();
572    }
573
574    /**
575     * Register a callback to be invoked when an item has been clicked and the
576     * caller prefers to receive a ListView-style position instead of a group
577     * and/or child position. In most cases, the caller should use
578     * {@link #setOnGroupClickListener} and/or {@link #setOnChildClickListener}.
579     * <p />
580     * {@inheritDoc}
581     */
582    @Override
583    public void setOnItemClickListener(OnItemClickListener l) {
584        super.setOnItemClickListener(l);
585    }
586
587    /**
588     * Sets the adapter that provides data to this view.
589     * @param adapter The adapter that provides data to this view.
590     */
591    public void setAdapter(ExpandableListAdapter adapter) {
592        // Set member variable
593        mAdapter = adapter;
594
595        if (adapter != null) {
596            // Create the connector
597            mConnector = new ExpandableListConnector(adapter);
598        } else {
599            mConnector = null;
600        }
601
602        // Link the ListView (superclass) to the expandable list data through the connector
603        super.setAdapter(mConnector);
604    }
605
606    /**
607     * Gets the adapter that provides data to this view.
608     * @return The adapter that provides data to this view.
609     */
610    public ExpandableListAdapter getExpandableListAdapter() {
611        return mAdapter;
612    }
613
614    /**
615     * @param position An absolute (including header and footer) flat list position.
616     * @return true if the position corresponds to a header or a footer item.
617     */
618    private boolean isHeaderOrFooterPosition(int position) {
619        final int footerViewsStart = mItemCount - getFooterViewsCount();
620        return (position < getHeaderViewsCount() || position >= footerViewsStart);
621    }
622
623    /**
624     * Converts an absolute item flat position into a group/child flat position, shifting according
625     * to the number of header items.
626     *
627     * @param flatListPosition The absolute flat position
628     * @return A group/child flat position as expected by the connector.
629     */
630    private int getFlatPositionForConnector(int flatListPosition) {
631        return flatListPosition - getHeaderViewsCount();
632    }
633
634    /**
635     * Converts a group/child flat position into an absolute flat position, that takes into account
636     * the possible headers.
637     *
638     * @param flatListPosition The child/group flat position
639     * @return An absolute flat position.
640     */
641    private int getAbsoluteFlatPosition(int flatListPosition) {
642        return flatListPosition + getHeaderViewsCount();
643    }
644
645    @Override
646    public boolean performItemClick(View v, int position, long id) {
647        // Ignore clicks in header/footers
648        if (isHeaderOrFooterPosition(position)) {
649            // Clicked on a header/footer, so ignore pass it on to super
650            return super.performItemClick(v, position, id);
651        }
652
653        // Internally handle the item click
654        final int adjustedPosition = getFlatPositionForConnector(position);
655        return handleItemClick(v, adjustedPosition, id);
656    }
657
658    /**
659     * This will either expand/collapse groups (if a group was clicked) or pass
660     * on the click to the proper child (if a child was clicked)
661     *
662     * @param position The flat list position. This has already been factored to
663     *            remove the header/footer.
664     * @param id The ListAdapter ID, not the group or child ID.
665     */
666    boolean handleItemClick(View v, int position, long id) {
667        final PositionMetadata posMetadata = mConnector.getUnflattenedPos(position);
668
669        id = getChildOrGroupId(posMetadata.position);
670
671        boolean returnValue;
672        if (posMetadata.position.type == ExpandableListPosition.GROUP) {
673            /* It's a group, so handle collapsing/expanding */
674
675            /* It's a group click, so pass on event */
676            if (mOnGroupClickListener != null) {
677                if (mOnGroupClickListener.onGroupClick(this, v,
678                        posMetadata.position.groupPos, id)) {
679                    posMetadata.recycle();
680                    return true;
681                }
682            }
683
684            if (posMetadata.isExpanded()) {
685                /* Collapse it */
686                mConnector.collapseGroup(posMetadata);
687
688                playSoundEffect(SoundEffectConstants.CLICK);
689
690                if (mOnGroupCollapseListener != null) {
691                    mOnGroupCollapseListener.onGroupCollapse(posMetadata.position.groupPos);
692                }
693            } else {
694                /* Expand it */
695                mConnector.expandGroup(posMetadata);
696
697                playSoundEffect(SoundEffectConstants.CLICK);
698
699                if (mOnGroupExpandListener != null) {
700                    mOnGroupExpandListener.onGroupExpand(posMetadata.position.groupPos);
701                }
702
703                final int groupPos = posMetadata.position.groupPos;
704                final int groupFlatPos = posMetadata.position.flatListPos;
705
706                final int shiftedGroupPosition = groupFlatPos + getHeaderViewsCount();
707                smoothScrollToPosition(shiftedGroupPosition + mAdapter.getChildrenCount(groupPos),
708                        shiftedGroupPosition);
709            }
710
711            returnValue = true;
712        } else {
713            /* It's a child, so pass on event */
714            if (mOnChildClickListener != null) {
715                playSoundEffect(SoundEffectConstants.CLICK);
716                return mOnChildClickListener.onChildClick(this, v, posMetadata.position.groupPos,
717                        posMetadata.position.childPos, id);
718            }
719
720            returnValue = false;
721        }
722
723        posMetadata.recycle();
724
725        return returnValue;
726    }
727
728    /**
729     * Expand a group in the grouped list view
730     *
731     * @param groupPos the group to be expanded
732     * @return True if the group was expanded, false otherwise (if the group
733     *         was already expanded, this will return false)
734     */
735    public boolean expandGroup(int groupPos) {
736       return expandGroup(groupPos, false);
737    }
738
739    /**
740     * Expand a group in the grouped list view
741     *
742     * @param groupPos the group to be expanded
743     * @param animate true if the expanding group should be animated in
744     * @return True if the group was expanded, false otherwise (if the group
745     *         was already expanded, this will return false)
746     */
747    public boolean expandGroup(int groupPos, boolean animate) {
748        ExpandableListPosition elGroupPos = ExpandableListPosition.obtain(
749                ExpandableListPosition.GROUP, groupPos, -1, -1);
750        PositionMetadata pm = mConnector.getFlattenedPos(elGroupPos);
751        elGroupPos.recycle();
752        boolean retValue = mConnector.expandGroup(pm);
753
754        if (mOnGroupExpandListener != null) {
755            mOnGroupExpandListener.onGroupExpand(groupPos);
756        }
757
758        if (animate) {
759            final int groupFlatPos = pm.position.flatListPos;
760
761            final int shiftedGroupPosition = groupFlatPos + getHeaderViewsCount();
762            smoothScrollToPosition(shiftedGroupPosition + mAdapter.getChildrenCount(groupPos),
763                    shiftedGroupPosition);
764        }
765        pm.recycle();
766
767        return retValue;
768    }
769
770    /**
771     * Collapse a group in the grouped list view
772     *
773     * @param groupPos position of the group to collapse
774     * @return True if the group was collapsed, false otherwise (if the group
775     *         was already collapsed, this will return false)
776     */
777    public boolean collapseGroup(int groupPos) {
778        boolean retValue = mConnector.collapseGroup(groupPos);
779
780        if (mOnGroupCollapseListener != null) {
781            mOnGroupCollapseListener.onGroupCollapse(groupPos);
782        }
783
784        return retValue;
785    }
786
787    /** Used for being notified when a group is collapsed */
788    public interface OnGroupCollapseListener {
789        /**
790         * Callback method to be invoked when a group in this expandable list has
791         * been collapsed.
792         *
793         * @param groupPosition The group position that was collapsed
794         */
795        void onGroupCollapse(int groupPosition);
796    }
797
798    private OnGroupCollapseListener mOnGroupCollapseListener;
799
800    public void setOnGroupCollapseListener(
801            OnGroupCollapseListener onGroupCollapseListener) {
802        mOnGroupCollapseListener = onGroupCollapseListener;
803    }
804
805    /** Used for being notified when a group is expanded */
806    public interface OnGroupExpandListener {
807        /**
808         * Callback method to be invoked when a group in this expandable list has
809         * been expanded.
810         *
811         * @param groupPosition The group position that was expanded
812         */
813        void onGroupExpand(int groupPosition);
814    }
815
816    private OnGroupExpandListener mOnGroupExpandListener;
817
818    public void setOnGroupExpandListener(
819            OnGroupExpandListener onGroupExpandListener) {
820        mOnGroupExpandListener = onGroupExpandListener;
821    }
822
823    /**
824     * Interface definition for a callback to be invoked when a group in this
825     * expandable list has been clicked.
826     */
827    public interface OnGroupClickListener {
828        /**
829         * Callback method to be invoked when a group in this expandable list has
830         * been clicked.
831         *
832         * @param parent The ExpandableListConnector where the click happened
833         * @param v The view within the expandable list/ListView that was clicked
834         * @param groupPosition The group position that was clicked
835         * @param id The row id of the group that was clicked
836         * @return True if the click was handled
837         */
838        boolean onGroupClick(ExpandableListView parent, View v, int groupPosition,
839                long id);
840    }
841
842    private OnGroupClickListener mOnGroupClickListener;
843
844    public void setOnGroupClickListener(OnGroupClickListener onGroupClickListener) {
845        mOnGroupClickListener = onGroupClickListener;
846    }
847
848    /**
849     * Interface definition for a callback to be invoked when a child in this
850     * expandable list has been clicked.
851     */
852    public interface OnChildClickListener {
853        /**
854         * Callback method to be invoked when a child in this expandable list has
855         * been clicked.
856         *
857         * @param parent The ExpandableListView where the click happened
858         * @param v The view within the expandable list/ListView that was clicked
859         * @param groupPosition The group position that contains the child that
860         *        was clicked
861         * @param childPosition The child position within the group
862         * @param id The row id of the child that was clicked
863         * @return True if the click was handled
864         */
865        boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
866                int childPosition, long id);
867    }
868
869    private OnChildClickListener mOnChildClickListener;
870
871    public void setOnChildClickListener(OnChildClickListener onChildClickListener) {
872        mOnChildClickListener = onChildClickListener;
873    }
874
875    /**
876     * Converts a flat list position (the raw position of an item (child or group)
877     * in the list) to a group and/or child position (represented in a
878     * packed position). This is useful in situations where the caller needs to
879     * use the underlying {@link ListView}'s methods. Use
880     * {@link ExpandableListView#getPackedPositionType} ,
881     * {@link ExpandableListView#getPackedPositionChild},
882     * {@link ExpandableListView#getPackedPositionGroup} to unpack.
883     *
884     * @param flatListPosition The flat list position to be converted.
885     * @return The group and/or child position for the given flat list position
886     *         in packed position representation. #PACKED_POSITION_VALUE_NULL if
887     *         the position corresponds to a header or a footer item.
888     */
889    public long getExpandableListPosition(int flatListPosition) {
890        if (isHeaderOrFooterPosition(flatListPosition)) {
891            return PACKED_POSITION_VALUE_NULL;
892        }
893
894        final int adjustedPosition = getFlatPositionForConnector(flatListPosition);
895        PositionMetadata pm = mConnector.getUnflattenedPos(adjustedPosition);
896        long packedPos = pm.position.getPackedPosition();
897        pm.recycle();
898        return packedPos;
899    }
900
901    /**
902     * Converts a group and/or child position to a flat list position. This is
903     * useful in situations where the caller needs to use the underlying
904     * {@link ListView}'s methods.
905     *
906     * @param packedPosition The group and/or child positions to be converted in
907     *            packed position representation. Use
908     *            {@link #getPackedPositionForChild(int, int)} or
909     *            {@link #getPackedPositionForGroup(int)}.
910     * @return The flat list position for the given child or group.
911     */
912    public int getFlatListPosition(long packedPosition) {
913        ExpandableListPosition elPackedPos = ExpandableListPosition
914                .obtainPosition(packedPosition);
915        PositionMetadata pm = mConnector.getFlattenedPos(elPackedPos);
916        elPackedPos.recycle();
917        final int flatListPosition = pm.position.flatListPos;
918        pm.recycle();
919        return getAbsoluteFlatPosition(flatListPosition);
920    }
921
922    /**
923     * Gets the position of the currently selected group or child (along with
924     * its type). Can return {@link #PACKED_POSITION_VALUE_NULL} if no selection.
925     *
926     * @return A packed position containing the currently selected group or
927     *         child's position and type. #PACKED_POSITION_VALUE_NULL if no selection
928     *         or if selection is on a header or a footer item.
929     */
930    public long getSelectedPosition() {
931        final int selectedPos = getSelectedItemPosition();
932
933        // The case where there is no selection (selectedPos == -1) is also handled here.
934        return getExpandableListPosition(selectedPos);
935    }
936
937    /**
938     * Gets the ID of the currently selected group or child. Can return -1 if no
939     * selection.
940     *
941     * @return The ID of the currently selected group or child. -1 if no
942     *         selection.
943     */
944    public long getSelectedId() {
945        long packedPos = getSelectedPosition();
946        if (packedPos == PACKED_POSITION_VALUE_NULL) return -1;
947
948        int groupPos = getPackedPositionGroup(packedPos);
949
950        if (getPackedPositionType(packedPos) == PACKED_POSITION_TYPE_GROUP) {
951            // It's a group
952            return mAdapter.getGroupId(groupPos);
953        } else {
954            // It's a child
955            return mAdapter.getChildId(groupPos, getPackedPositionChild(packedPos));
956        }
957    }
958
959    /**
960     * Sets the selection to the specified group.
961     * @param groupPosition The position of the group that should be selected.
962     */
963    public void setSelectedGroup(int groupPosition) {
964        ExpandableListPosition elGroupPos = ExpandableListPosition
965                .obtainGroupPosition(groupPosition);
966        PositionMetadata pm = mConnector.getFlattenedPos(elGroupPos);
967        elGroupPos.recycle();
968        final int absoluteFlatPosition = getAbsoluteFlatPosition(pm.position.flatListPos);
969        super.setSelection(absoluteFlatPosition);
970        pm.recycle();
971    }
972
973    /**
974     * Sets the selection to the specified child. If the child is in a collapsed
975     * group, the group will only be expanded and child subsequently selected if
976     * shouldExpandGroup is set to true, otherwise the method will return false.
977     *
978     * @param groupPosition The position of the group that contains the child.
979     * @param childPosition The position of the child within the group.
980     * @param shouldExpandGroup Whether the child's group should be expanded if
981     *            it is collapsed.
982     * @return Whether the selection was successfully set on the child.
983     */
984    public boolean setSelectedChild(int groupPosition, int childPosition, boolean shouldExpandGroup) {
985        ExpandableListPosition elChildPos = ExpandableListPosition.obtainChildPosition(
986                groupPosition, childPosition);
987        PositionMetadata flatChildPos = mConnector.getFlattenedPos(elChildPos);
988
989        if (flatChildPos == null) {
990            // The child's group isn't expanded
991
992            // Shouldn't expand the group, so return false for we didn't set the selection
993            if (!shouldExpandGroup) return false;
994
995            expandGroup(groupPosition);
996
997            flatChildPos = mConnector.getFlattenedPos(elChildPos);
998
999            // Sanity check
1000            if (flatChildPos == null) {
1001                throw new IllegalStateException("Could not find child");
1002            }
1003        }
1004
1005        int absoluteFlatPosition = getAbsoluteFlatPosition(flatChildPos.position.flatListPos);
1006        super.setSelection(absoluteFlatPosition);
1007
1008        elChildPos.recycle();
1009        flatChildPos.recycle();
1010
1011        return true;
1012    }
1013
1014    /**
1015     * Whether the given group is currently expanded.
1016     *
1017     * @param groupPosition The group to check.
1018     * @return Whether the group is currently expanded.
1019     */
1020    public boolean isGroupExpanded(int groupPosition) {
1021        return mConnector.isGroupExpanded(groupPosition);
1022    }
1023
1024    /**
1025     * Gets the type of a packed position. See
1026     * {@link #getPackedPositionForChild(int, int)}.
1027     *
1028     * @param packedPosition The packed position for which to return the type.
1029     * @return The type of the position contained within the packed position,
1030     *         either {@link #PACKED_POSITION_TYPE_CHILD}, {@link #PACKED_POSITION_TYPE_GROUP}, or
1031     *         {@link #PACKED_POSITION_TYPE_NULL}.
1032     */
1033    public static int getPackedPositionType(long packedPosition) {
1034        if (packedPosition == PACKED_POSITION_VALUE_NULL) {
1035            return PACKED_POSITION_TYPE_NULL;
1036        }
1037
1038        return (packedPosition & PACKED_POSITION_MASK_TYPE) == PACKED_POSITION_MASK_TYPE
1039                ? PACKED_POSITION_TYPE_CHILD
1040                : PACKED_POSITION_TYPE_GROUP;
1041    }
1042
1043    /**
1044     * Gets the group position from a packed position. See
1045     * {@link #getPackedPositionForChild(int, int)}.
1046     *
1047     * @param packedPosition The packed position from which the group position
1048     *            will be returned.
1049     * @return The group position portion of the packed position. If this does
1050     *         not contain a group, returns -1.
1051     */
1052    public static int getPackedPositionGroup(long packedPosition) {
1053        // Null
1054        if (packedPosition == PACKED_POSITION_VALUE_NULL) return -1;
1055
1056        return (int) ((packedPosition & PACKED_POSITION_MASK_GROUP) >> PACKED_POSITION_SHIFT_GROUP);
1057    }
1058
1059    /**
1060     * Gets the child position from a packed position that is of
1061     * {@link #PACKED_POSITION_TYPE_CHILD} type (use {@link #getPackedPositionType(long)}).
1062     * To get the group that this child belongs to, use
1063     * {@link #getPackedPositionGroup(long)}. See
1064     * {@link #getPackedPositionForChild(int, int)}.
1065     *
1066     * @param packedPosition The packed position from which the child position
1067     *            will be returned.
1068     * @return The child position portion of the packed position. If this does
1069     *         not contain a child, returns -1.
1070     */
1071    public static int getPackedPositionChild(long packedPosition) {
1072        // Null
1073        if (packedPosition == PACKED_POSITION_VALUE_NULL) return -1;
1074
1075        // Group since a group type clears this bit
1076        if ((packedPosition & PACKED_POSITION_MASK_TYPE) != PACKED_POSITION_MASK_TYPE) return -1;
1077
1078        return (int) (packedPosition & PACKED_POSITION_MASK_CHILD);
1079    }
1080
1081    /**
1082     * Returns the packed position representation of a child's position.
1083     * <p>
1084     * In general, a packed position should be used in
1085     * situations where the position given to/returned from an
1086     * {@link ExpandableListAdapter} or {@link ExpandableListView} method can
1087     * either be a child or group. The two positions are packed into a single
1088     * long which can be unpacked using
1089     * {@link #getPackedPositionChild(long)},
1090     * {@link #getPackedPositionGroup(long)}, and
1091     * {@link #getPackedPositionType(long)}.
1092     *
1093     * @param groupPosition The child's parent group's position.
1094     * @param childPosition The child position within the group.
1095     * @return The packed position representation of the child (and parent group).
1096     */
1097    public static long getPackedPositionForChild(int groupPosition, int childPosition) {
1098        return (((long)PACKED_POSITION_TYPE_CHILD) << PACKED_POSITION_SHIFT_TYPE)
1099                | ((((long)groupPosition) & PACKED_POSITION_INT_MASK_GROUP)
1100                        << PACKED_POSITION_SHIFT_GROUP)
1101                | (childPosition & PACKED_POSITION_INT_MASK_CHILD);
1102    }
1103
1104    /**
1105     * Returns the packed position representation of a group's position. See
1106     * {@link #getPackedPositionForChild(int, int)}.
1107     *
1108     * @param groupPosition The child's parent group's position.
1109     * @return The packed position representation of the group.
1110     */
1111    public static long getPackedPositionForGroup(int groupPosition) {
1112        // No need to OR a type in because PACKED_POSITION_GROUP == 0
1113        return ((((long)groupPosition) & PACKED_POSITION_INT_MASK_GROUP)
1114                        << PACKED_POSITION_SHIFT_GROUP);
1115    }
1116
1117    @Override
1118    ContextMenuInfo createContextMenuInfo(View view, int flatListPosition, long id) {
1119        if (isHeaderOrFooterPosition(flatListPosition)) {
1120            // Return normal info for header/footer view context menus
1121            return new AdapterContextMenuInfo(view, flatListPosition, id);
1122        }
1123
1124        final int adjustedPosition = getFlatPositionForConnector(flatListPosition);
1125        PositionMetadata pm = mConnector.getUnflattenedPos(adjustedPosition);
1126        ExpandableListPosition pos = pm.position;
1127
1128        id = getChildOrGroupId(pos);
1129        long packedPosition = pos.getPackedPosition();
1130
1131        pm.recycle();
1132
1133        return new ExpandableListContextMenuInfo(view, packedPosition, id);
1134    }
1135
1136    /**
1137     * Gets the ID of the group or child at the given <code>position</code>.
1138     * This is useful since there is no ListAdapter ID -> ExpandableListAdapter
1139     * ID conversion mechanism (in some cases, it isn't possible).
1140     *
1141     * @param position The position of the child or group whose ID should be
1142     *            returned.
1143     */
1144    private long getChildOrGroupId(ExpandableListPosition position) {
1145        if (position.type == ExpandableListPosition.CHILD) {
1146            return mAdapter.getChildId(position.groupPos, position.childPos);
1147        } else {
1148            return mAdapter.getGroupId(position.groupPos);
1149        }
1150    }
1151
1152    /**
1153     * Sets the indicator to be drawn next to a child.
1154     *
1155     * @param childIndicator The drawable to be used as an indicator. If the
1156     *            child is the last child for a group, the state
1157     *            {@link android.R.attr#state_last} will be set.
1158     */
1159    public void setChildIndicator(Drawable childIndicator) {
1160        mChildIndicator = childIndicator;
1161    }
1162
1163    /**
1164     * Sets the drawing bounds for the child indicator. For either, you can
1165     * specify {@link #CHILD_INDICATOR_INHERIT} to use inherit from the general
1166     * indicator's bounds.
1167     *
1168     * @see #setIndicatorBounds(int, int)
1169     * @param left The left position (relative to the left bounds of this View)
1170     *            to start drawing the indicator.
1171     * @param right The right position (relative to the left bounds of this
1172     *            View) to end the drawing of the indicator.
1173     */
1174    public void setChildIndicatorBounds(int left, int right) {
1175        mChildIndicatorLeft = left;
1176        mChildIndicatorRight = right;
1177        resolveChildIndicator();
1178    }
1179
1180    /**
1181     * Sets the relative drawing bounds for the child indicator. For either, you can
1182     * specify {@link #CHILD_INDICATOR_INHERIT} to use inherit from the general
1183     * indicator's bounds.
1184     *
1185     * @see #setIndicatorBounds(int, int)
1186     * @param start The start position (relative to the start bounds of this View)
1187     *            to start drawing the indicator.
1188     * @param end The end position (relative to the end bounds of this
1189     *            View) to end the drawing of the indicator.
1190     */
1191    public void setChildIndicatorBoundsRelative(int start, int end) {
1192        mChildIndicatorStart = start;
1193        mChildIndicatorEnd = end;
1194        resolveChildIndicator();
1195    }
1196
1197    /**
1198     * Sets the indicator to be drawn next to a group.
1199     *
1200     * @param groupIndicator The drawable to be used as an indicator. If the
1201     *            group is empty, the state {@link android.R.attr#state_empty} will be
1202     *            set. If the group is expanded, the state
1203     *            {@link android.R.attr#state_expanded} will be set.
1204     */
1205    public void setGroupIndicator(Drawable groupIndicator) {
1206        mGroupIndicator = groupIndicator;
1207        if (mIndicatorRight == 0 && mGroupIndicator != null) {
1208            mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth();
1209        }
1210    }
1211
1212    /**
1213     * Sets the drawing bounds for the indicators (at minimum, the group indicator
1214     * is affected by this; the child indicator is affected by this if the
1215     * child indicator bounds are set to inherit).
1216     *
1217     * @see #setChildIndicatorBounds(int, int)
1218     * @param left The left position (relative to the left bounds of this View)
1219     *            to start drawing the indicator.
1220     * @param right The right position (relative to the left bounds of this
1221     *            View) to end the drawing of the indicator.
1222     */
1223    public void setIndicatorBounds(int left, int right) {
1224        mIndicatorLeft = left;
1225        mIndicatorRight = right;
1226        resolveIndicator();
1227    }
1228
1229    /**
1230     * Sets the relative drawing bounds for the indicators (at minimum, the group indicator
1231     * is affected by this; the child indicator is affected by this if the
1232     * child indicator bounds are set to inherit).
1233     *
1234     * @see #setChildIndicatorBounds(int, int)
1235     * @param start The start position (relative to the start bounds of this View)
1236     *            to start drawing the indicator.
1237     * @param end The end position (relative to the end bounds of this
1238     *            View) to end the drawing of the indicator.
1239     */
1240    public void setIndicatorBoundsRelative(int start, int end) {
1241        mIndicatorStart = start;
1242        mIndicatorEnd = end;
1243        resolveIndicator();
1244    }
1245
1246    /**
1247     * Extra menu information specific to an {@link ExpandableListView} provided
1248     * to the
1249     * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
1250     * callback when a context menu is brought up for this AdapterView.
1251     */
1252    public static class ExpandableListContextMenuInfo implements ContextMenu.ContextMenuInfo {
1253
1254        public ExpandableListContextMenuInfo(View targetView, long packedPosition, long id) {
1255            this.targetView = targetView;
1256            this.packedPosition = packedPosition;
1257            this.id = id;
1258        }
1259
1260        /**
1261         * The view for which the context menu is being displayed. This
1262         * will be one of the children Views of this {@link ExpandableListView}.
1263         */
1264        public View targetView;
1265
1266        /**
1267         * The packed position in the list represented by the adapter for which
1268         * the context menu is being displayed. Use the methods
1269         * {@link ExpandableListView#getPackedPositionType},
1270         * {@link ExpandableListView#getPackedPositionChild}, and
1271         * {@link ExpandableListView#getPackedPositionGroup} to unpack this.
1272         */
1273        public long packedPosition;
1274
1275        /**
1276         * The ID of the item (group or child) for which the context menu is
1277         * being displayed.
1278         */
1279        public long id;
1280    }
1281
1282    static class SavedState extends BaseSavedState {
1283        ArrayList<ExpandableListConnector.GroupMetadata> expandedGroupMetadataList;
1284
1285        /**
1286         * Constructor called from {@link ExpandableListView#onSaveInstanceState()}
1287         */
1288        SavedState(
1289                Parcelable superState,
1290                ArrayList<ExpandableListConnector.GroupMetadata> expandedGroupMetadataList) {
1291            super(superState);
1292            this.expandedGroupMetadataList = expandedGroupMetadataList;
1293        }
1294
1295        /**
1296         * Constructor called from {@link #CREATOR}
1297         */
1298        private SavedState(Parcel in) {
1299            super(in);
1300            expandedGroupMetadataList = new ArrayList<ExpandableListConnector.GroupMetadata>();
1301            in.readList(expandedGroupMetadataList, ExpandableListConnector.class.getClassLoader());
1302        }
1303
1304        @Override
1305        public void writeToParcel(Parcel out, int flags) {
1306            super.writeToParcel(out, flags);
1307            out.writeList(expandedGroupMetadataList);
1308        }
1309
1310        public static final Parcelable.Creator<SavedState> CREATOR
1311                = new Parcelable.Creator<SavedState>() {
1312            public SavedState createFromParcel(Parcel in) {
1313                return new SavedState(in);
1314            }
1315
1316            public SavedState[] newArray(int size) {
1317                return new SavedState[size];
1318            }
1319        };
1320    }
1321
1322    @Override
1323    public Parcelable onSaveInstanceState() {
1324        Parcelable superState = super.onSaveInstanceState();
1325        return new SavedState(superState,
1326                mConnector != null ? mConnector.getExpandedGroupMetadataList() : null);
1327    }
1328
1329    @Override
1330    public void onRestoreInstanceState(Parcelable state) {
1331        if (!(state instanceof SavedState)) {
1332            super.onRestoreInstanceState(state);
1333            return;
1334        }
1335
1336        SavedState ss = (SavedState) state;
1337        super.onRestoreInstanceState(ss.getSuperState());
1338
1339        if (mConnector != null && ss.expandedGroupMetadataList != null) {
1340            mConnector.setExpandedGroupMetadataList(ss.expandedGroupMetadataList);
1341        }
1342    }
1343
1344    @Override
1345    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1346        super.onInitializeAccessibilityEvent(event);
1347        event.setClassName(ExpandableListView.class.getName());
1348    }
1349
1350    @Override
1351    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1352        super.onInitializeAccessibilityNodeInfo(info);
1353        info.setClassName(ExpandableListView.class.getName());
1354    }
1355}
1356