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