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 android.content.Context;
20import android.database.DataSetObserver;
21import android.os.Parcelable;
22import android.os.SystemClock;
23import android.util.AttributeSet;
24import android.util.SparseArray;
25import android.view.ContextMenu;
26import android.view.ContextMenu.ContextMenuInfo;
27import android.view.SoundEffectConstants;
28import android.view.View;
29import android.view.ViewDebug;
30import android.view.ViewGroup;
31import android.view.accessibility.AccessibilityEvent;
32import android.view.accessibility.AccessibilityManager;
33import android.view.accessibility.AccessibilityNodeInfo;
34
35/**
36 * An AdapterView is a view whose children are determined by an {@link Adapter}.
37 *
38 * <p>
39 * See {@link ListView}, {@link GridView}, {@link Spinner} and
40 *      {@link Gallery} for commonly used subclasses of AdapterView.
41 *
42 * <div class="special reference">
43 * <h3>Developer Guides</h3>
44 * <p>For more information about using AdapterView, read the
45 * <a href="{@docRoot}guide/topics/ui/binding.html">Binding to Data with AdapterView</a>
46 * developer guide.</p></div>
47 */
48public abstract class AdapterView<T extends Adapter> extends ViewGroup {
49
50    /**
51     * The item view type returned by {@link Adapter#getItemViewType(int)} when
52     * the adapter does not want the item's view recycled.
53     */
54    public static final int ITEM_VIEW_TYPE_IGNORE = -1;
55
56    /**
57     * The item view type returned by {@link Adapter#getItemViewType(int)} when
58     * the item is a header or footer.
59     */
60    public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2;
61
62    /**
63     * The position of the first child displayed
64     */
65    @ViewDebug.ExportedProperty(category = "scrolling")
66    int mFirstPosition = 0;
67
68    /**
69     * The offset in pixels from the top of the AdapterView to the top
70     * of the view to select during the next layout.
71     */
72    int mSpecificTop;
73
74    /**
75     * Position from which to start looking for mSyncRowId
76     */
77    int mSyncPosition;
78
79    /**
80     * Row id to look for when data has changed
81     */
82    long mSyncRowId = INVALID_ROW_ID;
83
84    /**
85     * Height of the view when mSyncPosition and mSyncRowId where set
86     */
87    long mSyncHeight;
88
89    /**
90     * True if we need to sync to mSyncRowId
91     */
92    boolean mNeedSync = false;
93
94    /**
95     * Indicates whether to sync based on the selection or position. Possible
96     * values are {@link #SYNC_SELECTED_POSITION} or
97     * {@link #SYNC_FIRST_POSITION}.
98     */
99    int mSyncMode;
100
101    /**
102     * Our height after the last layout
103     */
104    private int mLayoutHeight;
105
106    /**
107     * Sync based on the selected child
108     */
109    static final int SYNC_SELECTED_POSITION = 0;
110
111    /**
112     * Sync based on the first child displayed
113     */
114    static final int SYNC_FIRST_POSITION = 1;
115
116    /**
117     * Maximum amount of time to spend in {@link #findSyncPosition()}
118     */
119    static final int SYNC_MAX_DURATION_MILLIS = 100;
120
121    /**
122     * Indicates that this view is currently being laid out.
123     */
124    boolean mInLayout = false;
125
126    /**
127     * The listener that receives notifications when an item is selected.
128     */
129    OnItemSelectedListener mOnItemSelectedListener;
130
131    /**
132     * The listener that receives notifications when an item is clicked.
133     */
134    OnItemClickListener mOnItemClickListener;
135
136    /**
137     * The listener that receives notifications when an item is long clicked.
138     */
139    OnItemLongClickListener mOnItemLongClickListener;
140
141    /**
142     * True if the data has changed since the last layout
143     */
144    boolean mDataChanged;
145
146    /**
147     * The position within the adapter's data set of the item to select
148     * during the next layout.
149     */
150    @ViewDebug.ExportedProperty(category = "list")
151    int mNextSelectedPosition = INVALID_POSITION;
152
153    /**
154     * The item id of the item to select during the next layout.
155     */
156    long mNextSelectedRowId = INVALID_ROW_ID;
157
158    /**
159     * The position within the adapter's data set of the currently selected item.
160     */
161    @ViewDebug.ExportedProperty(category = "list")
162    int mSelectedPosition = INVALID_POSITION;
163
164    /**
165     * The item id of the currently selected item.
166     */
167    long mSelectedRowId = INVALID_ROW_ID;
168
169    /**
170     * View to show if there are no items to show.
171     */
172    private View mEmptyView;
173
174    /**
175     * The number of items in the current adapter.
176     */
177    @ViewDebug.ExportedProperty(category = "list")
178    int mItemCount;
179
180    /**
181     * The number of items in the adapter before a data changed event occurred.
182     */
183    int mOldItemCount;
184
185    /**
186     * Represents an invalid position. All valid positions are in the range 0 to 1 less than the
187     * number of items in the current adapter.
188     */
189    public static final int INVALID_POSITION = -1;
190
191    /**
192     * Represents an empty or invalid row id
193     */
194    public static final long INVALID_ROW_ID = Long.MIN_VALUE;
195
196    /**
197     * The last selected position we used when notifying
198     */
199    int mOldSelectedPosition = INVALID_POSITION;
200
201    /**
202     * The id of the last selected position we used when notifying
203     */
204    long mOldSelectedRowId = INVALID_ROW_ID;
205
206    /**
207     * Indicates what focusable state is requested when calling setFocusable().
208     * In addition to this, this view has other criteria for actually
209     * determining the focusable state (such as whether its empty or the text
210     * filter is shown).
211     *
212     * @see #setFocusable(boolean)
213     * @see #checkFocus()
214     */
215    private boolean mDesiredFocusableState;
216    private boolean mDesiredFocusableInTouchModeState;
217
218    private SelectionNotifier mSelectionNotifier;
219    /**
220     * When set to true, calls to requestLayout() will not propagate up the parent hierarchy.
221     * This is used to layout the children during a layout pass.
222     */
223    boolean mBlockLayoutRequests = false;
224
225    public AdapterView(Context context) {
226        this(context, null);
227    }
228
229    public AdapterView(Context context, AttributeSet attrs) {
230        this(context, attrs, 0);
231    }
232
233    public AdapterView(Context context, AttributeSet attrs, int defStyleAttr) {
234        this(context, attrs, defStyleAttr, 0);
235    }
236
237    public AdapterView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
238        super(context, attrs, defStyleAttr, defStyleRes);
239
240        // If not explicitly specified this view is important for accessibility.
241        if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
242            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
243        }
244    }
245
246    /**
247     * Interface definition for a callback to be invoked when an item in this
248     * AdapterView has been clicked.
249     */
250    public interface OnItemClickListener {
251
252        /**
253         * Callback method to be invoked when an item in this AdapterView has
254         * been clicked.
255         * <p>
256         * Implementers can call getItemAtPosition(position) if they need
257         * to access the data associated with the selected item.
258         *
259         * @param parent The AdapterView where the click happened.
260         * @param view The view within the AdapterView that was clicked (this
261         *            will be a view provided by the adapter)
262         * @param position The position of the view in the adapter.
263         * @param id The row id of the item that was clicked.
264         */
265        void onItemClick(AdapterView<?> parent, View view, int position, long id);
266    }
267
268    /**
269     * Register a callback to be invoked when an item in this AdapterView has
270     * been clicked.
271     *
272     * @param listener The callback that will be invoked.
273     */
274    public void setOnItemClickListener(OnItemClickListener listener) {
275        mOnItemClickListener = listener;
276    }
277
278    /**
279     * @return The callback to be invoked with an item in this AdapterView has
280     *         been clicked, or null id no callback has been set.
281     */
282    public final OnItemClickListener getOnItemClickListener() {
283        return mOnItemClickListener;
284    }
285
286    /**
287     * Call the OnItemClickListener, if it is defined. Performs all normal
288     * actions associated with clicking: reporting accessibility event, playing
289     * a sound, etc.
290     *
291     * @param view The view within the AdapterView that was clicked.
292     * @param position The position of the view in the adapter.
293     * @param id The row id of the item that was clicked.
294     * @return True if there was an assigned OnItemClickListener that was
295     *         called, false otherwise is returned.
296     */
297    public boolean performItemClick(View view, int position, long id) {
298        if (mOnItemClickListener != null) {
299            playSoundEffect(SoundEffectConstants.CLICK);
300            mOnItemClickListener.onItemClick(this, view, position, id);
301            if (view != null) {
302                view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
303            }
304            return true;
305        }
306
307        return false;
308    }
309
310    /**
311     * Interface definition for a callback to be invoked when an item in this
312     * view has been clicked and held.
313     */
314    public interface OnItemLongClickListener {
315        /**
316         * Callback method to be invoked when an item in this view has been
317         * clicked and held.
318         *
319         * Implementers can call getItemAtPosition(position) if they need to access
320         * the data associated with the selected item.
321         *
322         * @param parent The AbsListView where the click happened
323         * @param view The view within the AbsListView that was clicked
324         * @param position The position of the view in the list
325         * @param id The row id of the item that was clicked
326         *
327         * @return true if the callback consumed the long click, false otherwise
328         */
329        boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id);
330    }
331
332
333    /**
334     * Register a callback to be invoked when an item in this AdapterView has
335     * been clicked and held
336     *
337     * @param listener The callback that will run
338     */
339    public void setOnItemLongClickListener(OnItemLongClickListener listener) {
340        if (!isLongClickable()) {
341            setLongClickable(true);
342        }
343        mOnItemLongClickListener = listener;
344    }
345
346    /**
347     * @return The callback to be invoked with an item in this AdapterView has
348     *         been clicked and held, or null id no callback as been set.
349     */
350    public final OnItemLongClickListener getOnItemLongClickListener() {
351        return mOnItemLongClickListener;
352    }
353
354    /**
355     * Interface definition for a callback to be invoked when
356     * an item in this view has been selected.
357     */
358    public interface OnItemSelectedListener {
359        /**
360         * <p>Callback method to be invoked when an item in this view has been
361         * selected. This callback is invoked only when the newly selected
362         * position is different from the previously selected position or if
363         * there was no selected item.</p>
364         *
365         * Impelmenters can call getItemAtPosition(position) if they need to access the
366         * data associated with the selected item.
367         *
368         * @param parent The AdapterView where the selection happened
369         * @param view The view within the AdapterView that was clicked
370         * @param position The position of the view in the adapter
371         * @param id The row id of the item that is selected
372         */
373        void onItemSelected(AdapterView<?> parent, View view, int position, long id);
374
375        /**
376         * Callback method to be invoked when the selection disappears from this
377         * view. The selection can disappear for instance when touch is activated
378         * or when the adapter becomes empty.
379         *
380         * @param parent The AdapterView that now contains no selected item.
381         */
382        void onNothingSelected(AdapterView<?> parent);
383    }
384
385
386    /**
387     * Register a callback to be invoked when an item in this AdapterView has
388     * been selected.
389     *
390     * @param listener The callback that will run
391     */
392    public void setOnItemSelectedListener(OnItemSelectedListener listener) {
393        mOnItemSelectedListener = listener;
394    }
395
396    public final OnItemSelectedListener getOnItemSelectedListener() {
397        return mOnItemSelectedListener;
398    }
399
400    /**
401     * Extra menu information provided to the
402     * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
403     * callback when a context menu is brought up for this AdapterView.
404     *
405     */
406    public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo {
407
408        public AdapterContextMenuInfo(View targetView, int position, long id) {
409            this.targetView = targetView;
410            this.position = position;
411            this.id = id;
412        }
413
414        /**
415         * The child view for which the context menu is being displayed. This
416         * will be one of the children of this AdapterView.
417         */
418        public View targetView;
419
420        /**
421         * The position in the adapter for which the context menu is being
422         * displayed.
423         */
424        public int position;
425
426        /**
427         * The row id of the item for which the context menu is being displayed.
428         */
429        public long id;
430    }
431
432    /**
433     * Returns the adapter currently associated with this widget.
434     *
435     * @return The adapter used to provide this view's content.
436     */
437    public abstract T getAdapter();
438
439    /**
440     * Sets the adapter that provides the data and the views to represent the data
441     * in this widget.
442     *
443     * @param adapter The adapter to use to create this view's content.
444     */
445    public abstract void setAdapter(T adapter);
446
447    /**
448     * This method is not supported and throws an UnsupportedOperationException when called.
449     *
450     * @param child Ignored.
451     *
452     * @throws UnsupportedOperationException Every time this method is invoked.
453     */
454    @Override
455    public void addView(View child) {
456        throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
457    }
458
459    /**
460     * This method is not supported and throws an UnsupportedOperationException when called.
461     *
462     * @param child Ignored.
463     * @param index Ignored.
464     *
465     * @throws UnsupportedOperationException Every time this method is invoked.
466     */
467    @Override
468    public void addView(View child, int index) {
469        throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView");
470    }
471
472    /**
473     * This method is not supported and throws an UnsupportedOperationException when called.
474     *
475     * @param child Ignored.
476     * @param params Ignored.
477     *
478     * @throws UnsupportedOperationException Every time this method is invoked.
479     */
480    @Override
481    public void addView(View child, LayoutParams params) {
482        throw new UnsupportedOperationException("addView(View, LayoutParams) "
483                + "is not supported in AdapterView");
484    }
485
486    /**
487     * This method is not supported and throws an UnsupportedOperationException when called.
488     *
489     * @param child Ignored.
490     * @param index Ignored.
491     * @param params Ignored.
492     *
493     * @throws UnsupportedOperationException Every time this method is invoked.
494     */
495    @Override
496    public void addView(View child, int index, LayoutParams params) {
497        throw new UnsupportedOperationException("addView(View, int, LayoutParams) "
498                + "is not supported in AdapterView");
499    }
500
501    /**
502     * This method is not supported and throws an UnsupportedOperationException when called.
503     *
504     * @param child Ignored.
505     *
506     * @throws UnsupportedOperationException Every time this method is invoked.
507     */
508    @Override
509    public void removeView(View child) {
510        throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView");
511    }
512
513    /**
514     * This method is not supported and throws an UnsupportedOperationException when called.
515     *
516     * @param index Ignored.
517     *
518     * @throws UnsupportedOperationException Every time this method is invoked.
519     */
520    @Override
521    public void removeViewAt(int index) {
522        throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView");
523    }
524
525    /**
526     * This method is not supported and throws an UnsupportedOperationException when called.
527     *
528     * @throws UnsupportedOperationException Every time this method is invoked.
529     */
530    @Override
531    public void removeAllViews() {
532        throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView");
533    }
534
535    @Override
536    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
537        mLayoutHeight = getHeight();
538    }
539
540    /**
541     * Return the position of the currently selected item within the adapter's data set
542     *
543     * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected.
544     */
545    @ViewDebug.CapturedViewProperty
546    public int getSelectedItemPosition() {
547        return mNextSelectedPosition;
548    }
549
550    /**
551     * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID}
552     * if nothing is selected.
553     */
554    @ViewDebug.CapturedViewProperty
555    public long getSelectedItemId() {
556        return mNextSelectedRowId;
557    }
558
559    /**
560     * @return The view corresponding to the currently selected item, or null
561     * if nothing is selected
562     */
563    public abstract View getSelectedView();
564
565    /**
566     * @return The data corresponding to the currently selected item, or
567     * null if there is nothing selected.
568     */
569    public Object getSelectedItem() {
570        T adapter = getAdapter();
571        int selection = getSelectedItemPosition();
572        if (adapter != null && adapter.getCount() > 0 && selection >= 0) {
573            return adapter.getItem(selection);
574        } else {
575            return null;
576        }
577    }
578
579    /**
580     * @return The number of items owned by the Adapter associated with this
581     *         AdapterView. (This is the number of data items, which may be
582     *         larger than the number of visible views.)
583     */
584    @ViewDebug.CapturedViewProperty
585    public int getCount() {
586        return mItemCount;
587    }
588
589    /**
590     * Get the position within the adapter's data set for the view, where view is a an adapter item
591     * or a descendant of an adapter item.
592     *
593     * @param view an adapter item, or a descendant of an adapter item. This must be visible in this
594     *        AdapterView at the time of the call.
595     * @return the position within the adapter's data set of the view, or {@link #INVALID_POSITION}
596     *         if the view does not correspond to a list item (or it is not currently visible).
597     */
598    public int getPositionForView(View view) {
599        View listItem = view;
600        try {
601            View v;
602            while (!(v = (View) listItem.getParent()).equals(this)) {
603                listItem = v;
604            }
605        } catch (ClassCastException e) {
606            // We made it up to the window without find this list view
607            return INVALID_POSITION;
608        }
609
610        // Search the children for the list item
611        final int childCount = getChildCount();
612        for (int i = 0; i < childCount; i++) {
613            if (getChildAt(i).equals(listItem)) {
614                return mFirstPosition + i;
615            }
616        }
617
618        // Child not found!
619        return INVALID_POSITION;
620    }
621
622    /**
623     * Returns the position within the adapter's data set for the first item
624     * displayed on screen.
625     *
626     * @return The position within the adapter's data set
627     */
628    public int getFirstVisiblePosition() {
629        return mFirstPosition;
630    }
631
632    /**
633     * Returns the position within the adapter's data set for the last item
634     * displayed on screen.
635     *
636     * @return The position within the adapter's data set
637     */
638    public int getLastVisiblePosition() {
639        return mFirstPosition + getChildCount() - 1;
640    }
641
642    /**
643     * Sets the currently selected item. To support accessibility subclasses that
644     * override this method must invoke the overriden super method first.
645     *
646     * @param position Index (starting at 0) of the data item to be selected.
647     */
648    public abstract void setSelection(int position);
649
650    /**
651     * Sets the view to show if the adapter is empty
652     */
653    @android.view.RemotableViewMethod
654    public void setEmptyView(View emptyView) {
655        mEmptyView = emptyView;
656
657        // If not explicitly specified this view is important for accessibility.
658        if (emptyView != null
659                && emptyView.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
660            emptyView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
661        }
662
663        final T adapter = getAdapter();
664        final boolean empty = ((adapter == null) || adapter.isEmpty());
665        updateEmptyStatus(empty);
666    }
667
668    /**
669     * When the current adapter is empty, the AdapterView can display a special view
670     * called the empty view. The empty view is used to provide feedback to the user
671     * that no data is available in this AdapterView.
672     *
673     * @return The view to show if the adapter is empty.
674     */
675    public View getEmptyView() {
676        return mEmptyView;
677    }
678
679    /**
680     * Indicates whether this view is in filter mode. Filter mode can for instance
681     * be enabled by a user when typing on the keyboard.
682     *
683     * @return True if the view is in filter mode, false otherwise.
684     */
685    boolean isInFilterMode() {
686        return false;
687    }
688
689    @Override
690    public void setFocusable(boolean focusable) {
691        final T adapter = getAdapter();
692        final boolean empty = adapter == null || adapter.getCount() == 0;
693
694        mDesiredFocusableState = focusable;
695        if (!focusable) {
696            mDesiredFocusableInTouchModeState = false;
697        }
698
699        super.setFocusable(focusable && (!empty || isInFilterMode()));
700    }
701
702    @Override
703    public void setFocusableInTouchMode(boolean focusable) {
704        final T adapter = getAdapter();
705        final boolean empty = adapter == null || adapter.getCount() == 0;
706
707        mDesiredFocusableInTouchModeState = focusable;
708        if (focusable) {
709            mDesiredFocusableState = true;
710        }
711
712        super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode()));
713    }
714
715    void checkFocus() {
716        final T adapter = getAdapter();
717        final boolean empty = adapter == null || adapter.getCount() == 0;
718        final boolean focusable = !empty || isInFilterMode();
719        // The order in which we set focusable in touch mode/focusable may matter
720        // for the client, see View.setFocusableInTouchMode() comments for more
721        // details
722        super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
723        super.setFocusable(focusable && mDesiredFocusableState);
724        if (mEmptyView != null) {
725            updateEmptyStatus((adapter == null) || adapter.isEmpty());
726        }
727    }
728
729    /**
730     * Update the status of the list based on the empty parameter.  If empty is true and
731     * we have an empty view, display it.  In all the other cases, make sure that the listview
732     * is VISIBLE and that the empty view is GONE (if it's not null).
733     */
734    private void updateEmptyStatus(boolean empty) {
735        if (isInFilterMode()) {
736            empty = false;
737        }
738
739        if (empty) {
740            if (mEmptyView != null) {
741                mEmptyView.setVisibility(View.VISIBLE);
742                setVisibility(View.GONE);
743            } else {
744                // If the caller just removed our empty view, make sure the list view is visible
745                setVisibility(View.VISIBLE);
746            }
747
748            // We are now GONE, so pending layouts will not be dispatched.
749            // Force one here to make sure that the state of the list matches
750            // the state of the adapter.
751            if (mDataChanged) {
752                this.onLayout(false, mLeft, mTop, mRight, mBottom);
753            }
754        } else {
755            if (mEmptyView != null) mEmptyView.setVisibility(View.GONE);
756            setVisibility(View.VISIBLE);
757        }
758    }
759
760    /**
761     * Gets the data associated with the specified position in the list.
762     *
763     * @param position Which data to get
764     * @return The data associated with the specified position in the list
765     */
766    public Object getItemAtPosition(int position) {
767        T adapter = getAdapter();
768        return (adapter == null || position < 0) ? null : adapter.getItem(position);
769    }
770
771    public long getItemIdAtPosition(int position) {
772        T adapter = getAdapter();
773        return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position);
774    }
775
776    @Override
777    public void setOnClickListener(OnClickListener l) {
778        throw new RuntimeException("Don't call setOnClickListener for an AdapterView. "
779                + "You probably want setOnItemClickListener instead");
780    }
781
782    /**
783     * Override to prevent freezing of any views created by the adapter.
784     */
785    @Override
786    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
787        dispatchFreezeSelfOnly(container);
788    }
789
790    /**
791     * Override to prevent thawing of any views created by the adapter.
792     */
793    @Override
794    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
795        dispatchThawSelfOnly(container);
796    }
797
798    class AdapterDataSetObserver extends DataSetObserver {
799
800        private Parcelable mInstanceState = null;
801
802        @Override
803        public void onChanged() {
804            mDataChanged = true;
805            mOldItemCount = mItemCount;
806            mItemCount = getAdapter().getCount();
807
808            // Detect the case where a cursor that was previously invalidated has
809            // been repopulated with new data.
810            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
811                    && mOldItemCount == 0 && mItemCount > 0) {
812                AdapterView.this.onRestoreInstanceState(mInstanceState);
813                mInstanceState = null;
814            } else {
815                rememberSyncState();
816            }
817            checkFocus();
818            requestLayout();
819        }
820
821        @Override
822        public void onInvalidated() {
823            mDataChanged = true;
824
825            if (AdapterView.this.getAdapter().hasStableIds()) {
826                // Remember the current state for the case where our hosting activity is being
827                // stopped and later restarted
828                mInstanceState = AdapterView.this.onSaveInstanceState();
829            }
830
831            // Data is invalid so we should reset our state
832            mOldItemCount = mItemCount;
833            mItemCount = 0;
834            mSelectedPosition = INVALID_POSITION;
835            mSelectedRowId = INVALID_ROW_ID;
836            mNextSelectedPosition = INVALID_POSITION;
837            mNextSelectedRowId = INVALID_ROW_ID;
838            mNeedSync = false;
839
840            checkFocus();
841            requestLayout();
842        }
843
844        public void clearSavedState() {
845            mInstanceState = null;
846        }
847    }
848
849    @Override
850    protected void onDetachedFromWindow() {
851        super.onDetachedFromWindow();
852        removeCallbacks(mSelectionNotifier);
853    }
854
855    private class SelectionNotifier implements Runnable {
856        public void run() {
857            if (mDataChanged) {
858                // Data has changed between when this SelectionNotifier
859                // was posted and now. We need to wait until the AdapterView
860                // has been synched to the new data.
861                if (getAdapter() != null) {
862                    post(this);
863                }
864            } else {
865                fireOnSelected();
866                performAccessibilityActionsOnSelected();
867            }
868        }
869    }
870
871    void selectionChanged() {
872        if (mOnItemSelectedListener != null
873                || AccessibilityManager.getInstance(mContext).isEnabled()) {
874            if (mInLayout || mBlockLayoutRequests) {
875                // If we are in a layout traversal, defer notification
876                // by posting. This ensures that the view tree is
877                // in a consistent state and is able to accomodate
878                // new layout or invalidate requests.
879                if (mSelectionNotifier == null) {
880                    mSelectionNotifier = new SelectionNotifier();
881                }
882                post(mSelectionNotifier);
883            } else {
884                fireOnSelected();
885                performAccessibilityActionsOnSelected();
886            }
887        }
888    }
889
890    private void fireOnSelected() {
891        if (mOnItemSelectedListener == null) {
892            return;
893        }
894        final int selection = getSelectedItemPosition();
895        if (selection >= 0) {
896            View v = getSelectedView();
897            mOnItemSelectedListener.onItemSelected(this, v, selection,
898                    getAdapter().getItemId(selection));
899        } else {
900            mOnItemSelectedListener.onNothingSelected(this);
901        }
902    }
903
904    private void performAccessibilityActionsOnSelected() {
905        if (!AccessibilityManager.getInstance(mContext).isEnabled()) {
906            return;
907        }
908        final int position = getSelectedItemPosition();
909        if (position >= 0) {
910            // we fire selection events here not in View
911            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
912        }
913    }
914
915    @Override
916    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
917        View selectedView = getSelectedView();
918        if (selectedView != null && selectedView.getVisibility() == VISIBLE
919                && selectedView.dispatchPopulateAccessibilityEvent(event)) {
920            return true;
921        }
922        return false;
923    }
924
925    @Override
926    public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
927        if (super.onRequestSendAccessibilityEvent(child, event)) {
928            // Add a record for ourselves as well.
929            AccessibilityEvent record = AccessibilityEvent.obtain();
930            onInitializeAccessibilityEvent(record);
931            // Populate with the text of the requesting child.
932            child.dispatchPopulateAccessibilityEvent(record);
933            event.appendRecord(record);
934            return true;
935        }
936        return false;
937    }
938
939    @Override
940    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
941        super.onInitializeAccessibilityNodeInfo(info);
942        info.setClassName(AdapterView.class.getName());
943        info.setScrollable(isScrollableForAccessibility());
944        View selectedView = getSelectedView();
945        if (selectedView != null) {
946            info.setEnabled(selectedView.isEnabled());
947        }
948    }
949
950    @Override
951    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
952        super.onInitializeAccessibilityEvent(event);
953        event.setClassName(AdapterView.class.getName());
954        event.setScrollable(isScrollableForAccessibility());
955        View selectedView = getSelectedView();
956        if (selectedView != null) {
957            event.setEnabled(selectedView.isEnabled());
958        }
959        event.setCurrentItemIndex(getSelectedItemPosition());
960        event.setFromIndex(getFirstVisiblePosition());
961        event.setToIndex(getLastVisiblePosition());
962        event.setItemCount(getCount());
963    }
964
965    private boolean isScrollableForAccessibility() {
966        T adapter = getAdapter();
967        if (adapter != null) {
968            final int itemCount = adapter.getCount();
969            return itemCount > 0
970                && (getFirstVisiblePosition() > 0 || getLastVisiblePosition() < itemCount - 1);
971        }
972        return false;
973    }
974
975    @Override
976    protected boolean canAnimate() {
977        return super.canAnimate() && mItemCount > 0;
978    }
979
980    void handleDataChanged() {
981        final int count = mItemCount;
982        boolean found = false;
983
984        if (count > 0) {
985
986            int newPos;
987
988            // Find the row we are supposed to sync to
989            if (mNeedSync) {
990                // Update this first, since setNextSelectedPositionInt inspects
991                // it
992                mNeedSync = false;
993
994                // See if we can find a position in the new data with the same
995                // id as the old selection
996                newPos = findSyncPosition();
997                if (newPos >= 0) {
998                    // Verify that new selection is selectable
999                    int selectablePos = lookForSelectablePosition(newPos, true);
1000                    if (selectablePos == newPos) {
1001                        // Same row id is selected
1002                        setNextSelectedPositionInt(newPos);
1003                        found = true;
1004                    }
1005                }
1006            }
1007            if (!found) {
1008                // Try to use the same position if we can't find matching data
1009                newPos = getSelectedItemPosition();
1010
1011                // Pin position to the available range
1012                if (newPos >= count) {
1013                    newPos = count - 1;
1014                }
1015                if (newPos < 0) {
1016                    newPos = 0;
1017                }
1018
1019                // Make sure we select something selectable -- first look down
1020                int selectablePos = lookForSelectablePosition(newPos, true);
1021                if (selectablePos < 0) {
1022                    // Looking down didn't work -- try looking up
1023                    selectablePos = lookForSelectablePosition(newPos, false);
1024                }
1025                if (selectablePos >= 0) {
1026                    setNextSelectedPositionInt(selectablePos);
1027                    checkSelectionChanged();
1028                    found = true;
1029                }
1030            }
1031        }
1032        if (!found) {
1033            // Nothing is selected
1034            mSelectedPosition = INVALID_POSITION;
1035            mSelectedRowId = INVALID_ROW_ID;
1036            mNextSelectedPosition = INVALID_POSITION;
1037            mNextSelectedRowId = INVALID_ROW_ID;
1038            mNeedSync = false;
1039            checkSelectionChanged();
1040        }
1041
1042        notifySubtreeAccessibilityStateChangedIfNeeded();
1043    }
1044
1045    void checkSelectionChanged() {
1046        if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
1047            selectionChanged();
1048            mOldSelectedPosition = mSelectedPosition;
1049            mOldSelectedRowId = mSelectedRowId;
1050        }
1051    }
1052
1053    /**
1054     * Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition
1055     * and then alternates between moving up and moving down until 1) we find the right position, or
1056     * 2) we run out of time, or 3) we have looked at every position
1057     *
1058     * @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't
1059     *         be found
1060     */
1061    int findSyncPosition() {
1062        int count = mItemCount;
1063
1064        if (count == 0) {
1065            return INVALID_POSITION;
1066        }
1067
1068        long idToMatch = mSyncRowId;
1069        int seed = mSyncPosition;
1070
1071        // If there isn't a selection don't hunt for it
1072        if (idToMatch == INVALID_ROW_ID) {
1073            return INVALID_POSITION;
1074        }
1075
1076        // Pin seed to reasonable values
1077        seed = Math.max(0, seed);
1078        seed = Math.min(count - 1, seed);
1079
1080        long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS;
1081
1082        long rowId;
1083
1084        // first position scanned so far
1085        int first = seed;
1086
1087        // last position scanned so far
1088        int last = seed;
1089
1090        // True if we should move down on the next iteration
1091        boolean next = false;
1092
1093        // True when we have looked at the first item in the data
1094        boolean hitFirst;
1095
1096        // True when we have looked at the last item in the data
1097        boolean hitLast;
1098
1099        // Get the item ID locally (instead of getItemIdAtPosition), so
1100        // we need the adapter
1101        T adapter = getAdapter();
1102        if (adapter == null) {
1103            return INVALID_POSITION;
1104        }
1105
1106        while (SystemClock.uptimeMillis() <= endTime) {
1107            rowId = adapter.getItemId(seed);
1108            if (rowId == idToMatch) {
1109                // Found it!
1110                return seed;
1111            }
1112
1113            hitLast = last == count - 1;
1114            hitFirst = first == 0;
1115
1116            if (hitLast && hitFirst) {
1117                // Looked at everything
1118                break;
1119            }
1120
1121            if (hitFirst || (next && !hitLast)) {
1122                // Either we hit the top, or we are trying to move down
1123                last++;
1124                seed = last;
1125                // Try going up next time
1126                next = false;
1127            } else if (hitLast || (!next && !hitFirst)) {
1128                // Either we hit the bottom, or we are trying to move up
1129                first--;
1130                seed = first;
1131                // Try going down next time
1132                next = true;
1133            }
1134
1135        }
1136
1137        return INVALID_POSITION;
1138    }
1139
1140    /**
1141     * Find a position that can be selected (i.e., is not a separator).
1142     *
1143     * @param position The starting position to look at.
1144     * @param lookDown Whether to look down for other positions.
1145     * @return The next selectable position starting at position and then searching either up or
1146     *         down. Returns {@link #INVALID_POSITION} if nothing can be found.
1147     */
1148    int lookForSelectablePosition(int position, boolean lookDown) {
1149        return position;
1150    }
1151
1152    /**
1153     * Utility to keep mSelectedPosition and mSelectedRowId in sync
1154     * @param position Our current position
1155     */
1156    void setSelectedPositionInt(int position) {
1157        mSelectedPosition = position;
1158        mSelectedRowId = getItemIdAtPosition(position);
1159    }
1160
1161    /**
1162     * Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync
1163     * @param position Intended value for mSelectedPosition the next time we go
1164     * through layout
1165     */
1166    void setNextSelectedPositionInt(int position) {
1167        mNextSelectedPosition = position;
1168        mNextSelectedRowId = getItemIdAtPosition(position);
1169        // If we are trying to sync to the selection, update that too
1170        if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) {
1171            mSyncPosition = position;
1172            mSyncRowId = mNextSelectedRowId;
1173        }
1174    }
1175
1176    /**
1177     * Remember enough information to restore the screen state when the data has
1178     * changed.
1179     *
1180     */
1181    void rememberSyncState() {
1182        if (getChildCount() > 0) {
1183            mNeedSync = true;
1184            mSyncHeight = mLayoutHeight;
1185            if (mSelectedPosition >= 0) {
1186                // Sync the selection state
1187                View v = getChildAt(mSelectedPosition - mFirstPosition);
1188                mSyncRowId = mNextSelectedRowId;
1189                mSyncPosition = mNextSelectedPosition;
1190                if (v != null) {
1191                    mSpecificTop = v.getTop();
1192                }
1193                mSyncMode = SYNC_SELECTED_POSITION;
1194            } else {
1195                // Sync the based on the offset of the first view
1196                View v = getChildAt(0);
1197                T adapter = getAdapter();
1198                if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
1199                    mSyncRowId = adapter.getItemId(mFirstPosition);
1200                } else {
1201                    mSyncRowId = NO_ID;
1202                }
1203                mSyncPosition = mFirstPosition;
1204                if (v != null) {
1205                    mSpecificTop = v.getTop();
1206                }
1207                mSyncMode = SYNC_FIRST_POSITION;
1208            }
1209        }
1210    }
1211}
1212