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