Project: ActionBarSherlock
/*
 * Copyright (C) 2006 The Android Open Source Project 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */
 
package com.actionbarsherlock.internal.widget; 
 
import android.content.Context; 
import android.database.DataSetObserver; 
import android.os.Parcelable; 
import android.os.SystemClock; 
import android.util.AttributeSet; 
import android.util.SparseArray; 
import android.view.ContextMenu; 
import android.view.SoundEffectConstants; 
import android.view.View; 
import android.view.ViewDebug; 
import android.view.ViewGroup; 
import android.view.accessibility.AccessibilityEvent; 
import android.view.accessibility.AccessibilityNodeInfo; 
import android.widget.Adapter; 
import android.widget.AdapterView.OnItemClickListener; 
import android.widget.ListView; 
 
 
/**
 * An AdapterView is a view whose children are determined by an {@link Adapter}. 
 * 
 * <p> 
 * See {@link ListView}, {@link GridView}, {@link Spinner} and 
 *      {@link Gallery} for commonly used subclasses of AdapterView. 
 * 
 * <div class="special reference"> 
 * <h3>Developer Guides</h3> 
 * <p>For more information about using AdapterView, read the 
 * <a href="{@docRoot}guide/topics/ui/binding.html">Binding to Data with AdapterView</a> 
 * developer guide.</p></div> 
 */
 
public abstract class IcsAdapterView<T extends Adapter> extends ViewGroup { 
 
    /**
     * The item view type returned by {@link Adapter#getItemViewType(int)} when 
     * the adapter does not want the item's view recycled. 
     */
 
    public static final int ITEM_VIEW_TYPE_IGNORE = -1
 
    /**
     * The item view type returned by {@link Adapter#getItemViewType(int)} when 
     * the item is a header or footer. 
     */
 
    public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2
 
    /**
     * The position of the first child displayed 
     */
 
    @ViewDebug.ExportedProperty(category = "scrolling"
    int mFirstPosition = 0
 
    /**
     * The offset in pixels from the top of the AdapterView to the top 
     * of the view to select during the next layout. 
     */
 
    int mSpecificTop; 
 
    /**
     * Position from which to start looking for mSyncRowId 
     */
 
    int mSyncPosition; 
 
    /**
     * Row id to look for when data has changed 
     */
 
    long mSyncRowId = INVALID_ROW_ID; 
 
    /**
     * Height of the view when mSyncPosition and mSyncRowId where set 
     */
 
    long mSyncHeight; 
 
    /**
     * True if we need to sync to mSyncRowId 
     */
 
    boolean mNeedSync = false
 
    /**
     * Indicates whether to sync based on the selection or position. Possible 
     * values are {@link #SYNC_SELECTED_POSITION} or 
     * {@link #SYNC_FIRST_POSITION}. 
     */
 
    int mSyncMode; 
 
    /**
     * Our height after the last layout 
     */
 
    private int mLayoutHeight; 
 
    /**
     * Sync based on the selected child 
     */
 
    static final int SYNC_SELECTED_POSITION = 0
 
    /**
     * Sync based on the first child displayed 
     */
 
    static final int SYNC_FIRST_POSITION = 1
 
    /**
     * Maximum amount of time to spend in {@link #findSyncPosition()} 
     */
 
    static final int SYNC_MAX_DURATION_MILLIS = 100
 
    /**
     * Indicates that this view is currently being laid out. 
     */
 
    boolean mInLayout = false
 
    /**
     * The listener that receives notifications when an item is selected. 
     */
 
    OnItemSelectedListener mOnItemSelectedListener; 
 
    /**
     * The listener that receives notifications when an item is clicked. 
     */
 
    OnItemClickListener mOnItemClickListener; 
 
    /**
     * The listener that receives notifications when an item is long clicked. 
     */
 
    OnItemLongClickListener mOnItemLongClickListener; 
 
    /**
     * True if the data has changed since the last layout 
     */
 
    boolean mDataChanged; 
 
    /**
     * The position within the adapter's data set of the item to select 
     * during the next layout. 
     */
 
    @ViewDebug.ExportedProperty(category = "list"
    int mNextSelectedPosition = INVALID_POSITION; 
 
    /**
     * The item id of the item to select during the next layout. 
     */
 
    long mNextSelectedRowId = INVALID_ROW_ID; 
 
    /**
     * The position within the adapter's data set of the currently selected item. 
     */
 
    @ViewDebug.ExportedProperty(category = "list"
    int mSelectedPosition = INVALID_POSITION; 
 
    /**
     * The item id of the currently selected item. 
     */
 
    long mSelectedRowId = INVALID_ROW_ID; 
 
    /**
     * View to show if there are no items to show. 
     */
 
    private View mEmptyView; 
 
    /**
     * The number of items in the current adapter. 
     */
 
    @ViewDebug.ExportedProperty(category = "list"
    int mItemCount; 
 
    /**
     * The number of items in the adapter before a data changed event occurred. 
     */
 
    int mOldItemCount; 
 
    /**
     * Represents an invalid position. All valid positions are in the range 0 to 1 less than the 
     * number of items in the current adapter. 
     */
 
    public static final int INVALID_POSITION = -1
 
    /**
     * Represents an empty or invalid row id 
     */
 
    public static final long INVALID_ROW_ID = Long.MIN_VALUE; 
 
    /**
     * The last selected position we used when notifying 
     */
 
    int mOldSelectedPosition = INVALID_POSITION; 
 
    /**
     * The id of the last selected position we used when notifying 
     */
 
    long mOldSelectedRowId = INVALID_ROW_ID; 
 
    /**
     * Indicates what focusable state is requested when calling setFocusable(). 
     * In addition to this, this view has other criteria for actually 
     * determining the focusable state (such as whether its empty or the text 
     * filter is shown). 
     * 
     * @see #setFocusable(boolean) 
     * @see #checkFocus() 
     */
 
    private boolean mDesiredFocusableState; 
    private boolean mDesiredFocusableInTouchModeState; 
 
    private SelectionNotifier mSelectionNotifier; 
    /**
     * When set to true, calls to requestLayout() will not propagate up the parent hierarchy. 
     * This is used to layout the children during a layout pass. 
     */
 
    boolean mBlockLayoutRequests = false
 
    public IcsAdapterView(Context context) { 
        super(context); 
    } 
 
    public IcsAdapterView(Context context, AttributeSet attrs) { 
        super(context, attrs); 
    } 
 
    public IcsAdapterView(Context context, AttributeSet attrs, int defStyle) { 
        super(context, attrs, defStyle); 
    } 
 
    /**
     * Register a callback to be invoked when an item in this AdapterView has 
     * been clicked. 
     * 
     * @param listener The callback that will be invoked. 
     */
 
    public void setOnItemClickListener(OnItemClickListener listener) { 
        mOnItemClickListener = listener; 
    } 
 
    /**
     * @return The callback to be invoked with an item in this AdapterView has 
     *         been clicked, or null id no callback has been set. 
     */
 
    public final OnItemClickListener getOnItemClickListener() { 
        return mOnItemClickListener; 
    } 
 
    /**
     * Call the OnItemClickListener, if it is defined. 
     * 
     * @param view The view within the AdapterView that was clicked. 
     * @param position The position of the view in the adapter. 
     * @param id The row id of the item that was clicked. 
     * @return True if there was an assigned OnItemClickListener that was 
     *         called, false otherwise is returned. 
     */
 
    public boolean performItemClick(View view, int position, long id) { 
        if (mOnItemClickListener != null) { 
            playSoundEffect(SoundEffectConstants.CLICK); 
            if (view != null) { 
                view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 
            } 
            mOnItemClickListener.onItemClick(/*this*/null, view, position, id); 
            return true
        } 
 
        return false
    } 
 
    /**
     * Interface definition for a callback to be invoked when an item in this 
     * view has been clicked and held. 
     */
 
    public interface OnItemLongClickListener { 
        /**
         * Callback method to be invoked when an item in this view has been 
         * clicked and held. 
         * 
         * Implementers can call getItemAtPosition(position) if they need to access 
         * the data associated with the selected item. 
         * 
         * @param parent The AbsListView where the click happened 
         * @param view The view within the AbsListView that was clicked 
         * @param position The position of the view in the list 
         * @param id The row id of the item that was clicked 
         * 
         * @return true if the callback consumed the long click, false otherwise 
         */
 
        boolean onItemLongClick(IcsAdapterView<?> parent, View view, int position, long id); 
    } 
 
 
    /**
     * Register a callback to be invoked when an item in this AdapterView has 
     * been clicked and held 
     * 
     * @param listener The callback that will run 
     */
 
    public void setOnItemLongClickListener(OnItemLongClickListener listener) { 
        if (!isLongClickable()) { 
            setLongClickable(true); 
        } 
        mOnItemLongClickListener = listener; 
    } 
 
    /**
     * @return The callback to be invoked with an item in this AdapterView has 
     *         been clicked and held, or null id no callback as been set. 
     */
 
    public final OnItemLongClickListener getOnItemLongClickListener() { 
        return mOnItemLongClickListener; 
    } 
 
    /**
     * Interface definition for a callback to be invoked when 
     * an item in this view has been selected. 
     */
 
    public interface OnItemSelectedListener { 
        /**
         * <p>Callback method to be invoked when an item in this view has been 
         * selected. This callback is invoked only when the newly selected 
         * position is different from the previously selected position or if 
         * there was no selected item.</p> 
         * 
         * Impelmenters can call getItemAtPosition(position) if they need to access the 
         * data associated with the selected item. 
         * 
         * @param parent The AdapterView where the selection happened 
         * @param view The view within the AdapterView that was clicked 
         * @param position The position of the view in the adapter 
         * @param id The row id of the item that is selected 
         */
 
        void onItemSelected(IcsAdapterView<?> parent, View view, int position, long id); 
 
        /**
         * Callback method to be invoked when the selection disappears from this 
         * view. The selection can disappear for instance when touch is activated 
         * or when the adapter becomes empty. 
         * 
         * @param parent The AdapterView that now contains no selected item. 
         */
 
        void onNothingSelected(IcsAdapterView<?> parent); 
    } 
 
 
    /**
     * Register a callback to be invoked when an item in this AdapterView has 
     * been selected. 
     * 
     * @param listener The callback that will run 
     */
 
    public void setOnItemSelectedListener(OnItemSelectedListener listener) { 
        mOnItemSelectedListener = listener; 
    } 
 
    public final OnItemSelectedListener getOnItemSelectedListener() { 
        return mOnItemSelectedListener; 
    } 
 
    /**
     * Extra menu information provided to the 
     * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) } 
     * callback when a context menu is brought up for this AdapterView. 
     * 
     */
 
    public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo { 
 
        public AdapterContextMenuInfo(View targetView, int position, long id) { 
            this.targetView = targetView; 
            this.position = position; 
            this.id = id; 
        } 
 
        /**
         * The child view for which the context menu is being displayed. This 
         * will be one of the children of this AdapterView. 
         */
 
        public View targetView; 
 
        /**
         * The position in the adapter for which the context menu is being 
         * displayed. 
         */
 
        public int position; 
 
        /**
         * The row id of the item for which the context menu is being displayed. 
         */
 
        public long id; 
    } 
 
    /**
     * Returns the adapter currently associated with this widget. 
     * 
     * @return The adapter used to provide this view's content. 
     */
 
    public abstract T getAdapter(); 
 
    /**
     * Sets the adapter that provides the data and the views to represent the data 
     * in this widget. 
     * 
     * @param adapter The adapter to use to create this view's content. 
     */
 
    public abstract void setAdapter(T adapter); 
 
    /**
     * This method is not supported and throws an UnsupportedOperationException when called. 
     * 
     * @param child Ignored. 
     * 
     * @throws UnsupportedOperationException Every time this method is invoked. 
     */
 
    @Override 
    public void addView(View child) { 
        throw new UnsupportedOperationException("addView(View) is not supported in AdapterView"); 
    } 
 
    /**
     * This method is not supported and throws an UnsupportedOperationException when called. 
     * 
     * @param child Ignored. 
     * @param index Ignored. 
     * 
     * @throws UnsupportedOperationException Every time this method is invoked. 
     */
 
    @Override 
    public void addView(View child, int index) { 
        throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView"); 
    } 
 
    /**
     * This method is not supported and throws an UnsupportedOperationException when called. 
     * 
     * @param child Ignored. 
     * @param params Ignored. 
     * 
     * @throws UnsupportedOperationException Every time this method is invoked. 
     */
 
    @Override 
    public void addView(View child, LayoutParams params) { 
        throw new UnsupportedOperationException("addView(View, LayoutParams) " 
                + "is not supported in AdapterView"); 
    } 
 
    /**
     * This method is not supported and throws an UnsupportedOperationException when called. 
     * 
     * @param child Ignored. 
     * @param index Ignored. 
     * @param params Ignored. 
     * 
     * @throws UnsupportedOperationException Every time this method is invoked. 
     */
 
    @Override 
    public void addView(View child, int index, LayoutParams params) { 
        throw new UnsupportedOperationException("addView(View, int, LayoutParams) " 
                + "is not supported in AdapterView"); 
    } 
 
    /**
     * This method is not supported and throws an UnsupportedOperationException when called. 
     * 
     * @param child Ignored. 
     * 
     * @throws UnsupportedOperationException Every time this method is invoked. 
     */
 
    @Override 
    public void removeView(View child) { 
        throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView"); 
    } 
 
    /**
     * This method is not supported and throws an UnsupportedOperationException when called. 
     * 
     * @param index Ignored. 
     * 
     * @throws UnsupportedOperationException Every time this method is invoked. 
     */
 
    @Override 
    public void removeViewAt(int index) { 
        throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView"); 
    } 
 
    /**
     * This method is not supported and throws an UnsupportedOperationException when called. 
     * 
     * @throws UnsupportedOperationException Every time this method is invoked. 
     */
 
    @Override 
    public void removeAllViews() { 
        throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView"); 
    } 
 
    @Override 
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 
        mLayoutHeight = getHeight(); 
    } 
 
    /**
     * Return the position of the currently selected item within the adapter's data set 
     * 
     * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected. 
     */
 
    @ViewDebug.CapturedViewProperty 
    public int getSelectedItemPosition() { 
        return mNextSelectedPosition; 
    } 
 
    /**
     * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID} 
     * if nothing is selected. 
     */
 
    @ViewDebug.CapturedViewProperty 
    public long getSelectedItemId() { 
        return mNextSelectedRowId; 
    } 
 
    /**
     * @return The view corresponding to the currently selected item, or null 
     * if nothing is selected 
     */
 
    public abstract View getSelectedView(); 
 
    /**
     * @return The data corresponding to the currently selected item, or 
     * null if there is nothing selected. 
     */
 
    public Object getSelectedItem() { 
        T adapter = getAdapter(); 
        int selection = getSelectedItemPosition(); 
        if (adapter != null && adapter.getCount() > 0 && selection >= 0) { 
            return adapter.getItem(selection); 
        } else { 
            return null
        } 
    } 
 
    /**
     * @return The number of items owned by the Adapter associated with this 
     *         AdapterView. (This is the number of data items, which may be 
     *         larger than the number of visible views.) 
     */
 
    @ViewDebug.CapturedViewProperty 
    public int getCount() { 
        return mItemCount; 
    } 
 
    /**
     * Get the position within the adapter's data set for the view, where view is a an adapter item 
     * or a descendant of an adapter item. 
     * 
     * @param view an adapter item, or a descendant of an adapter item. This must be visible in this 
     *        AdapterView at the time of the call. 
     * @return the position within the adapter's data set of the view, or {@link #INVALID_POSITION} 
     *         if the view does not correspond to a list item (or it is not currently visible). 
     */
 
    public int getPositionForView(View view) { 
        View listItem = view; 
        try { 
            View v; 
            while (!(v = (View) listItem.getParent()).equals(this)) { 
                listItem = v; 
            } 
        } catch (ClassCastException e) { 
            // We made it up to the window without find this list view 
            return INVALID_POSITION; 
        } 
 
        // Search the children for the list item 
        final int childCount = getChildCount(); 
        for (int i = 0; i < childCount; i++) { 
            if (getChildAt(i).equals(listItem)) { 
                return mFirstPosition + i; 
            } 
        } 
 
        // Child not found! 
        return INVALID_POSITION; 
    } 
 
    /**
     * Returns the position within the adapter's data set for the first item 
     * displayed on screen. 
     * 
     * @return The position within the adapter's data set 
     */
 
    public int getFirstVisiblePosition() { 
        return mFirstPosition; 
    } 
 
    /**
     * Returns the position within the adapter's data set for the last item 
     * displayed on screen. 
     * 
     * @return The position within the adapter's data set 
     */
 
    public int getLastVisiblePosition() { 
        return mFirstPosition + getChildCount() - 1
    } 
 
    /**
     * Sets the currently selected item. To support accessibility subclasses that 
     * override this method must invoke the overriden super method first. 
     * 
     * @param position Index (starting at 0) of the data item to be selected. 
     */
 
    public abstract void setSelection(int position); 
 
    /**
     * Sets the view to show if the adapter is empty 
     */
 
    public void setEmptyView(View emptyView) { 
        mEmptyView = emptyView; 
 
        final T adapter = getAdapter(); 
        final boolean empty = ((adapter == null) || adapter.isEmpty()); 
        updateEmptyStatus(empty); 
    } 
 
    /**
     * When the current adapter is empty, the AdapterView can display a special view 
     * call the empty view. The empty view is used to provide feedback to the user 
     * that no data is available in this AdapterView. 
     * 
     * @return The view to show if the adapter is empty. 
     */
 
    public View getEmptyView() { 
        return mEmptyView; 
    } 
 
    /**
     * Indicates whether this view is in filter mode. Filter mode can for instance 
     * be enabled by a user when typing on the keyboard. 
     * 
     * @return True if the view is in filter mode, false otherwise. 
     */
 
    boolean isInFilterMode() { 
        return false
    } 
 
    @Override 
    public void setFocusable(boolean focusable) { 
        final T adapter = getAdapter(); 
        final boolean empty = adapter == null || adapter.getCount() == 0
 
        mDesiredFocusableState = focusable; 
        if (!focusable) { 
            mDesiredFocusableInTouchModeState = false
        } 
 
        super.setFocusable(focusable && (!empty || isInFilterMode())); 
    } 
 
    @Override 
    public void setFocusableInTouchMode(boolean focusable) { 
        final T adapter = getAdapter(); 
        final boolean empty = adapter == null || adapter.getCount() == 0
 
        mDesiredFocusableInTouchModeState = focusable; 
        if (focusable) { 
            mDesiredFocusableState = true
        } 
 
        super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode())); 
    } 
 
    void checkFocus() { 
        final T adapter = getAdapter(); 
        final boolean empty = adapter == null || adapter.getCount() == 0
        final boolean focusable = !empty || isInFilterMode(); 
        // The order in which we set focusable in touch mode/focusable may matter 
        // for the client, see View.setFocusableInTouchMode() comments for more 
        // details 
        super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState); 
        super.setFocusable(focusable && mDesiredFocusableState); 
        if (mEmptyView != null) { 
            updateEmptyStatus((adapter == null) || adapter.isEmpty()); 
        } 
    } 
 
    /**
     * Update the status of the list based on the empty parameter.  If empty is true and 
     * we have an empty view, display it.  In all the other cases, make sure that the listview 
     * is VISIBLE and that the empty view is GONE (if it's not null). 
     */
 
    private void updateEmptyStatus(boolean empty) { 
        if (isInFilterMode()) { 
            empty = false
        } 
 
        if (empty) { 
            if (mEmptyView != null) { 
                mEmptyView.setVisibility(View.VISIBLE); 
                setVisibility(View.GONE); 
            } else { 
                // If the caller just removed our empty view, make sure the list view is visible 
                setVisibility(View.VISIBLE); 
            } 
 
            // We are now GONE, so pending layouts will not be dispatched. 
            // Force one here to make sure that the state of the list matches 
            // the state of the adapter. 
            if (mDataChanged) { 
                this.onLayout(false, getLeft(), getTop(), getRight(), getBottom()); 
            } 
        } else { 
            if (mEmptyView != null) mEmptyView.setVisibility(View.GONE); 
            setVisibility(View.VISIBLE); 
        } 
    } 
 
    /**
     * Gets the data associated with the specified position in the list. 
     * 
     * @param position Which data to get 
     * @return The data associated with the specified position in the list 
     */
 
    public Object getItemAtPosition(int position) { 
        T adapter = getAdapter(); 
        return (adapter == null || position < 0) ? null : adapter.getItem(position); 
    } 
 
    public long getItemIdAtPosition(int position) { 
        T adapter = getAdapter(); 
        return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position); 
    } 
 
    @Override 
    public void setOnClickListener(OnClickListener l) { 
        throw new RuntimeException("Don't call setOnClickListener for an AdapterView. " 
                + "You probably want setOnItemClickListener instead"); 
    } 
 
    /**
     * Override to prevent freezing of any views created by the adapter. 
     */
 
    @Override 
    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { 
        dispatchFreezeSelfOnly(container); 
    } 
 
    /**
     * Override to prevent thawing of any views created by the adapter. 
     */
 
    @Override 
    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { 
        dispatchThawSelfOnly(container); 
    } 
 
    class AdapterDataSetObserver extends DataSetObserver { 
 
        private Parcelable mInstanceState = null
 
        @Override 
        public void onChanged() { 
            mDataChanged = true
            mOldItemCount = mItemCount; 
            mItemCount = getAdapter().getCount(); 
 
            // Detect the case where a cursor that was previously invalidated has 
            // been repopulated with new data. 
            if (IcsAdapterView.this.getAdapter().hasStableIds() && mInstanceState != null 
                    && mOldItemCount == 0 && mItemCount > 0) { 
                IcsAdapterView.this.onRestoreInstanceState(mInstanceState); 
                mInstanceState = null
            } else { 
                rememberSyncState(); 
            } 
            checkFocus(); 
            requestLayout(); 
        } 
 
        @Override 
        public void onInvalidated() { 
            mDataChanged = true
 
            if (IcsAdapterView.this.getAdapter().hasStableIds()) { 
                // Remember the current state for the case where our hosting activity is being 
                // stopped and later restarted 
                mInstanceState = IcsAdapterView.this.onSaveInstanceState(); 
            } 
 
            // Data is invalid so we should reset our state 
            mOldItemCount = mItemCount; 
            mItemCount = 0
            mSelectedPosition = INVALID_POSITION; 
            mSelectedRowId = INVALID_ROW_ID; 
            mNextSelectedPosition = INVALID_POSITION; 
            mNextSelectedRowId = INVALID_ROW_ID; 
            mNeedSync = false
 
            checkFocus(); 
            requestLayout(); 
        } 
 
        public void clearSavedState() { 
            mInstanceState = null
        } 
    } 
 
    @Override 
    protected void onDetachedFromWindow() { 
        super.onDetachedFromWindow(); 
        removeCallbacks(mSelectionNotifier); 
    } 
 
    private class SelectionNotifier implements Runnable { 
        public void run() { 
            if (mDataChanged) { 
                // Data has changed between when this SelectionNotifier 
                // was posted and now. We need to wait until the AdapterView 
                // has been synched to the new data. 
                if (getAdapter() != null) { 
                    post(this); 
                } 
            } else { 
                fireOnSelected(); 
            } 
        } 
    } 
 
    void selectionChanged() { 
        if (mOnItemSelectedListener != null) { 
            if (mInLayout || mBlockLayoutRequests) { 
                // If we are in a layout traversal, defer notification 
                // by posting. This ensures that the view tree is 
                // in a consistent state and is able to accomodate 
                // new layout or invalidate requests. 
                if (mSelectionNotifier == null) { 
                    mSelectionNotifier = new SelectionNotifier(); 
                } 
                post(mSelectionNotifier); 
            } else { 
                fireOnSelected(); 
            } 
        } 
 
        // we fire selection events here not in View 
        if (mSelectedPosition != ListView.INVALID_POSITION && isShown() && !isInTouchMode()) { 
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 
        } 
    } 
 
    private void fireOnSelected() { 
        if (mOnItemSelectedListener == null
            return
 
        int selection = this.getSelectedItemPosition(); 
        if (selection >= 0) { 
            View v = getSelectedView(); 
            mOnItemSelectedListener.onItemSelected(this, v, selection, 
                    getAdapter().getItemId(selection)); 
        } else { 
            mOnItemSelectedListener.onNothingSelected(this); 
        } 
    } 
 
    @Override 
    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 
        View selectedView = getSelectedView(); 
        if (selectedView != null && selectedView.getVisibility() == VISIBLE 
                && selectedView.dispatchPopulateAccessibilityEvent(event)) { 
            return true
        } 
        return false
    } 
 
    @Override 
    public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { 
        if (super.onRequestSendAccessibilityEvent(child, event)) { 
            // Add a record for ourselves as well. 
            AccessibilityEvent record = AccessibilityEvent.obtain(); 
            onInitializeAccessibilityEvent(record); 
            // Populate with the text of the requesting child. 
            child.dispatchPopulateAccessibilityEvent(record); 
            event.appendRecord(record); 
            return true
        } 
        return false
    } 
 
    @Override 
    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 
        super.onInitializeAccessibilityNodeInfo(info); 
        info.setScrollable(isScrollableForAccessibility()); 
        View selectedView = getSelectedView(); 
        if (selectedView != null) { 
            info.setEnabled(selectedView.isEnabled()); 
        } 
    } 
 
    @Override 
    public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 
        super.onInitializeAccessibilityEvent(event); 
        event.setScrollable(isScrollableForAccessibility()); 
        View selectedView = getSelectedView(); 
        if (selectedView != null) { 
            event.setEnabled(selectedView.isEnabled()); 
        } 
        event.setCurrentItemIndex(getSelectedItemPosition()); 
        event.setFromIndex(getFirstVisiblePosition()); 
        event.setToIndex(getLastVisiblePosition()); 
        event.setItemCount(getCount()); 
    } 
 
    private boolean isScrollableForAccessibility() { 
        T adapter = getAdapter(); 
        if (adapter != null) { 
            final int itemCount = adapter.getCount(); 
            return itemCount > 0 
                && (getFirstVisiblePosition() > 0 || getLastVisiblePosition() < itemCount - 1); 
        } 
        return false
    } 
 
    @Override 
    protected boolean canAnimate() { 
        return super.canAnimate() && mItemCount > 0
    } 
 
    void handleDataChanged() { 
        final int count = mItemCount; 
        boolean found = false
 
        if (count > 0) { 
 
            int newPos; 
 
            // Find the row we are supposed to sync to 
            if (mNeedSync) { 
                // Update this first, since setNextSelectedPositionInt inspects 
                // it 
                mNeedSync = false
 
                // See if we can find a position in the new data with the same 
                // id as the old selection 
                newPos = findSyncPosition(); 
                if (newPos >= 0) { 
                    // Verify that new selection is selectable 
                    int selectablePos = lookForSelectablePosition(newPos, true); 
                    if (selectablePos == newPos) { 
                        // Same row id is selected 
                        setNextSelectedPositionInt(newPos); 
                        found = true
                    } 
                } 
            } 
            if (!found) { 
                // Try to use the same position if we can't find matching data 
                newPos = getSelectedItemPosition(); 
 
                // Pin position to the available range 
                if (newPos >= count) { 
                    newPos = count - 1
                } 
                if (newPos < 0) { 
                    newPos = 0
                } 
 
                // Make sure we select something selectable -- first look down 
                int selectablePos = lookForSelectablePosition(newPos, true); 
                if (selectablePos < 0) { 
                    // Looking down didn't work -- try looking up 
                    selectablePos = lookForSelectablePosition(newPos, false); 
                } 
                if (selectablePos >= 0) { 
                    setNextSelectedPositionInt(selectablePos); 
                    checkSelectionChanged(); 
                    found = true
                } 
            } 
        } 
        if (!found) { 
            // Nothing is selected 
            mSelectedPosition = INVALID_POSITION; 
            mSelectedRowId = INVALID_ROW_ID; 
            mNextSelectedPosition = INVALID_POSITION; 
            mNextSelectedRowId = INVALID_ROW_ID; 
            mNeedSync = false
            checkSelectionChanged(); 
        } 
    } 
 
    void checkSelectionChanged() { 
        if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) { 
            selectionChanged(); 
            mOldSelectedPosition = mSelectedPosition; 
            mOldSelectedRowId = mSelectedRowId; 
        } 
    } 
 
    /**
     * Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition 
     * and then alternates between moving up and moving down until 1) we find the right position, or 
     * 2) we run out of time, or 3) we have looked at every position 
     * 
     * @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't 
     *         be found 
     */
 
    int findSyncPosition() { 
        int count = mItemCount; 
 
        if (count == 0) { 
            return INVALID_POSITION; 
        } 
 
        long idToMatch = mSyncRowId; 
        int seed = mSyncPosition; 
 
        // If there isn't a selection don't hunt for it 
        if (idToMatch == INVALID_ROW_ID) { 
            return INVALID_POSITION; 
        } 
 
        // Pin seed to reasonable values 
        seed = Math.max(0, seed); 
        seed = Math.min(count - 1, seed); 
 
        long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS; 
 
        long rowId; 
 
        // first position scanned so far 
        int first = seed; 
 
        // last position scanned so far 
        int last = seed; 
 
        // True if we should move down on the next iteration 
        boolean next = false
 
        // True when we have looked at the first item in the data 
        boolean hitFirst; 
 
        // True when we have looked at the last item in the data 
        boolean hitLast; 
 
        // Get the item ID locally (instead of getItemIdAtPosition), so 
        // we need the adapter 
        T adapter = getAdapter(); 
        if (adapter == null) { 
            return INVALID_POSITION; 
        } 
 
        while (SystemClock.uptimeMillis() <= endTime) { 
            rowId = adapter.getItemId(seed); 
            if (rowId == idToMatch) { 
                // Found it! 
                return seed; 
            } 
 
            hitLast = last == count - 1
            hitFirst = first == 0
 
            if (hitLast && hitFirst) { 
                // Looked at everything 
                break
            } 
 
            if (hitFirst || (next && !hitLast)) { 
                // Either we hit the top, or we are trying to move down 
                last++; 
                seed = last; 
                // Try going up next time 
                next = false
            } else if (hitLast || (!next && !hitFirst)) { 
                // Either we hit the bottom, or we are trying to move up 
                first--; 
                seed = first; 
                // Try going down next time 
                next = true
            } 
 
        } 
 
        return INVALID_POSITION; 
    } 
 
    /**
     * Find a position that can be selected (i.e., is not a separator). 
     * 
     * @param position The starting position to look at. 
     * @param lookDown Whether to look down for other positions. 
     * @return The next selectable position starting at position and then searching either up or 
     *         down. Returns {@link #INVALID_POSITION} if nothing can be found. 
     */
 
    int lookForSelectablePosition(int position, boolean lookDown) { 
        return position; 
    } 
 
    /**
     * Utility to keep mSelectedPosition and mSelectedRowId in sync 
     * @param position Our current position 
     */
 
    void setSelectedPositionInt(int position) { 
        mSelectedPosition = position; 
        mSelectedRowId = getItemIdAtPosition(position); 
    } 
 
    /**
     * Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync 
     * @param position Intended value for mSelectedPosition the next time we go 
     * through layout 
     */
 
    void setNextSelectedPositionInt(int position) { 
        mNextSelectedPosition = position; 
        mNextSelectedRowId = getItemIdAtPosition(position); 
        // If we are trying to sync to the selection, update that too 
        if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) { 
            mSyncPosition = position; 
            mSyncRowId = mNextSelectedRowId; 
        } 
    } 
 
    /**
     * Remember enough information to restore the screen state when the data has 
     * changed. 
     * 
     */
 
    void rememberSyncState() { 
        if (getChildCount() > 0) { 
            mNeedSync = true
            mSyncHeight = mLayoutHeight; 
            if (mSelectedPosition >= 0) { 
                // Sync the selection state 
                View v = getChildAt(mSelectedPosition - mFirstPosition); 
                mSyncRowId = mNextSelectedRowId; 
                mSyncPosition = mNextSelectedPosition; 
                if (v != null) { 
                    mSpecificTop = v.getTop(); 
                } 
                mSyncMode = SYNC_SELECTED_POSITION; 
            } else { 
                // Sync the based on the offset of the first view 
                View v = getChildAt(0); 
                T adapter = getAdapter(); 
                if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) { 
                    mSyncRowId = adapter.getItemId(mFirstPosition); 
                } else { 
                    mSyncRowId = NO_ID; 
                } 
                mSyncPosition = mFirstPosition; 
                if (v != null) { 
                    mSpecificTop = v.getTop(); 
                } 
                mSyncMode = SYNC_FIRST_POSITION; 
            } 
        } 
    } 
}