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