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