/*
 * 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 it.sephiroth.android.library.widget;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.database.DataSetObserver;
import android.os.Parcelable;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
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.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Adapter;

/**
 * An AdapterView is a view whose children are determined by an {@link Adapter}.
 */
public abstract class AdapterView<T extends Adapter> extends ViewGroup {
	
	public static final String LOG_TAG = "AdapterView";

	public static final boolean LOG_ENABLED = false;

	/**
	 * 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")
	protected int mFirstPosition = 0;

	/**
	 * The offset in pixels from the left of the AdapterView to the left of the view to select during the next layout.
	 */
	protected int mSpecificLeft;

	/**
	 * Position from which to start looking for mSyncRowId
	 */
	protected int mSyncPosition;

	/**
	 * Col id to look for when data has changed
	 */
	protected long mSyncColId = INVALID_COL_ID;

	/**
	 * Width of the view when mSyncPosition and mSyncColId where set
	 */
	protected long mSyncWidth;

	/**
	 * True if we need to sync to mSyncColId
	 */
	protected 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 width after the last layout
	 */
	private int mLayoutWidth;

	/**
	 * 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.
	 */
	protected 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
	 * @hide
	 */
	public boolean mDataChanged;

	/**
	 * The position within the adapter's data set of the item to select during the next layout.
	 */
	@ViewDebug.ExportedProperty(category = "list")
	protected int mNextSelectedPosition = INVALID_POSITION;

	/**
	 * The item id of the item to select during the next layout.
	 */
	protected long mNextSelectedColId = INVALID_COL_ID;

	/**
	 * The position within the adapter's data set of the currently selected item.
	 */
	@ViewDebug.ExportedProperty(category = "list")
	protected int mSelectedPosition = INVALID_POSITION;

	/**
	 * The item id of the currently selected item.
	 */
	protected long mSelectedColId = INVALID_COL_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")
	protected int mItemCount;

	/**
	 * The number of items in the adapter before a data changed event occurred.
	 */
	protected int mOldItemCount;

	AccessibilityManager mAccessibilityManager;

	/**
	 * 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 col id
	 */
	public static final long INVALID_COL_ID = Long.MIN_VALUE;

	/**
	 * The last selected position we used when notifying
	 */
	protected int mOldSelectedPosition = INVALID_POSITION;

	/**
	 * The id of the last selected position we used when notifying
	 */
	protected long mOldSelectedColId = INVALID_COL_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.
	 */
	protected boolean mBlockLayoutRequests = false;

	public AdapterView( Context context ) {
		super( context );
	}

	public AdapterView( Context context, AttributeSet attrs ) {
		super( context, attrs );
	}

	@TargetApi(16)
	public AdapterView( Context context, AttributeSet attrs, int defStyle ) {
		super( context, attrs, defStyle );

		if( android.os.Build.VERSION.SDK_INT >= 16 ) {
			// If not explicitly specified this view is important for accessibility.
			if ( getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO ) {
				setImportantForAccessibility( IMPORTANT_FOR_ACCESSIBILITY_YES );
			}
		}

		if( ! isInEditMode() ) {
			mAccessibilityManager = (AccessibilityManager) getContext().getSystemService( Context.ACCESSIBILITY_SERVICE );
		}
	}

	/**
	 * Interface definition for a callback to be invoked when an item in this AdapterView has been clicked.
	 */
	public interface OnItemClickListener {

		/**
		 * Callback method to be invoked when an item in this AdapterView has been clicked.
		 * <p>
		 * Implementers can call getItemAtPosition(position) if they need to access the data associated with the selected item.
		 * 
		 * @param parent
		 *           The AdapterView where the click happened.
		 * @param view
		 *           The view within the AdapterView that was clicked (this will be a view provided by the adapter)
		 * @param position
		 *           The position of the view in the adapter.
		 * @param id
		 *           The col id of the item that was clicked.
		 */
		void onItemClick( AdapterView<?> parent, View view, int position, long id );
	}

	/**
	 * 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 col 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, 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 col id of the item that was clicked
		 * 
		 * @return true if the callback consumed the long click, false otherwise
		 */
		boolean onItemLongClick( AdapterView<?> 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 col id of the item that is selected
		 */
		void onItemSelected( AdapterView<?> 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( AdapterView<?> 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 col 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 ) {
		mLayoutWidth = getWidth();
	}

	/**
	 * 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 INVALID_ROW_ID if nothing is selected.
	 */
	@ViewDebug.CapturedViewProperty
	public long getSelectedItemId() {
		return mNextSelectedColId;
	}

	/**
	 * @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
	 */
	@TargetApi(16)
	public void setEmptyView( View emptyView ) {
		mEmptyView = emptyView;

		if( android.os.Build.VERSION.SDK_INT >= 16 ) {
			// If not explicitly specified this view is important for accessibility.
			if ( emptyView != null && emptyView.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO ) {
				emptyView.setImportantForAccessibility( IMPORTANT_FOR_ACCESSIBILITY_YES );
			}
		}

		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() ) );
	}

	protected 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).
	 */
	@SuppressLint("WrongCall")
	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_COL_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() {
			if( LOG_ENABLED ) {
				Log.i( LOG_TAG, "AdapterDataSetObserver::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 ( AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
					&& mOldItemCount == 0 && mItemCount > 0 ) {
				if( LOG_ENABLED ) {
					Log.d( LOG_TAG, "calling onRestoreInstanceState");
				}
				AdapterView.this.onRestoreInstanceState( mInstanceState );
				mInstanceState = null;
			} else {
				if( LOG_ENABLED ) {
					Log.d( LOG_TAG, "else calling rememberSyncState" );
				}
				rememberSyncState();
			}
			checkFocus();
			requestLayout();
		}

		@Override
		public void onInvalidated() {
			if( LOG_ENABLED ) {
				Log.i( LOG_TAG, "AdapterDataSetObserver::onInvalidated" );
			}
			mDataChanged = true;

			if ( AdapterView.this.getAdapter().hasStableIds() ) {
				// Remember the current state for the case where our hosting activity is being
				// stopped and later restarted
				mInstanceState = AdapterView.this.onSaveInstanceState();
			}

			// Data is invalid so we should reset our state
			mOldItemCount = mItemCount;
			mItemCount = 0;
			mSelectedPosition = INVALID_POSITION;
			mSelectedColId = INVALID_COL_ID;
			mNextSelectedPosition = INVALID_POSITION;
			mNextSelectedColId = INVALID_COL_ID;
			mNeedSync = false;

			checkFocus();
			requestLayout();
		}

		public void clearSavedState() {
			mInstanceState = null;
		}
	}

	@Override
	protected void onDetachedFromWindow() {
		super.onDetachedFromWindow();
		removeCallbacks( mSelectionNotifier );
	}

	private class SelectionNotifier implements Runnable {

		@Override
		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();
				performAccessibilityActionsOnSelected();
			}
		}
	}

	void selectionChanged() {
		if( LOG_ENABLED ) {
			Log.i( LOG_TAG, "selectionChanged" );
		}
		if ( mOnItemSelectedListener != null
				|| mAccessibilityManager.isEnabled() ) {
			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();
				performAccessibilityActionsOnSelected();
			}
		}
	}

	private void fireOnSelected() {
		if ( mOnItemSelectedListener == null ) {
			return;
		}
		final int selection = getSelectedItemPosition();
		if ( selection >= 0 ) {
			View v = getSelectedView();
			mOnItemSelectedListener.onItemSelected( this, v, selection,
					getAdapter().getItemId( selection ) );
		} else {
			mOnItemSelectedListener.onNothingSelected( this );
		}
	}

	private void performAccessibilityActionsOnSelected() {
		if ( !mAccessibilityManager.isEnabled() ) {
			return;
		}
		final int position = getSelectedItemPosition();
		if ( position >= 0 ) {
			// we fire selection events here not in View
			sendAccessibilityEvent( AccessibilityEvent.TYPE_VIEW_SELECTED );
		}
	}

	@Override
	public boolean dispatchPopulateAccessibilityEvent( AccessibilityEvent event ) {
		View selectedView = getSelectedView();
		if ( selectedView != null && selectedView.getVisibility() == VISIBLE
				&& selectedView.dispatchPopulateAccessibilityEvent( event ) ) {
			return true;
		}
		return false;
	}

	@TargetApi(14)
	@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;
	}

	@TargetApi(14)
	@Override
	public void onInitializeAccessibilityNodeInfo( AccessibilityNodeInfo info ) {
		super.onInitializeAccessibilityNodeInfo( info );
		info.setClassName( AdapterView.class.getName() );
		info.setScrollable( isScrollableForAccessibility() );
		View selectedView = getSelectedView();
		if ( selectedView != null ) {
			info.setEnabled( selectedView.isEnabled() );
		}
	}

	@TargetApi(14)
	@Override
	public void onInitializeAccessibilityEvent( AccessibilityEvent event ) {
		super.onInitializeAccessibilityEvent( event );
		event.setClassName( AdapterView.class.getName() );
		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() {
		if( LOG_ENABLED ) {
			Log.i( LOG_TAG, "handleDataChanged" );
		}
		final int count = mItemCount;
		boolean found = false;

		if ( count > 0 ) {

			int newPos;

			// Find the col 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 col 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;
			mSelectedColId = INVALID_COL_ID;
			mNextSelectedPosition = INVALID_POSITION;
			mNextSelectedColId = INVALID_COL_ID;
			mNeedSync = false;
			checkSelectionChanged();
		}

		// TODO: Hmm, we do not know the old state so this is sub-optimal
		
		// TODO: implement this ( WTF Google, why you use the @hide tag?? )
		// notifyAccessibilityStateChanged();
	}

	protected void checkSelectionChanged() {
		if( LOG_ENABLED ) {
			Log.i( LOG_TAG, "checkSelectionChanged" );
		}
		if ( ( mSelectedPosition != mOldSelectedPosition ) || ( mSelectedColId != mOldSelectedColId ) ) {
			selectionChanged();
			mOldSelectedPosition = mSelectedPosition;
			mOldSelectedColId = mSelectedColId;
		}
	}

	/**
	 * Searches the adapter for a position matching mSyncColId. 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 col that matches mSyncColId, or {@link #INVALID_POSITION} if it can't be found
	 */
	int findSyncPosition() {
		int count = mItemCount;

		if ( count == 0 ) {
			return INVALID_POSITION;
		}

		long idToMatch = mSyncColId;
		int seed = mSyncPosition;

		// If there isn't a selection don't hunt for it
		if ( idToMatch == INVALID_COL_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 colId;

		// 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 ) {
			colId = adapter.getItemId( seed );
			if ( colId == 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.
	 */
	protected int lookForSelectablePosition( int position, boolean lookDown ) {
		return position;
	}

	/**
	 * Utility to keep mSelectedPosition and mSelectedColId in sync
	 * 
	 * @param position
	 *           Our current position
	 */
	protected void setSelectedPositionInt( int position ) {
		mSelectedPosition = position;
		mSelectedColId = getItemIdAtPosition( position );
	}

	/**
	 * Utility to keep mNextSelectedPosition and mNextSelectedColId in sync
	 * 
	 * @param position
	 *           Intended value for mSelectedPosition the next time we go through layout
	 */
	protected void setNextSelectedPositionInt( int position ) {
		mNextSelectedPosition = position;
		mNextSelectedColId = getItemIdAtPosition( position );
		// If we are trying to sync to the selection, update that too
		if ( mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0 ) {
			mSyncPosition = position;
			mSyncColId = mNextSelectedColId;
		}
	}

	/**
	 * Remember enough information to restore the screen state when the data has changed.
	 * @hide
	 */
	public void rememberSyncState() {
		if( LOG_ENABLED ) {
			Log.i( LOG_TAG, "rememberSyncState" );
		}
		if ( getChildCount() > 0 ) {
			mNeedSync = true;
			mSyncWidth = mLayoutWidth;
			if ( mSelectedPosition >= 0 ) {
				// Sync the selection state
				View v = getChildAt( mSelectedPosition - mFirstPosition );
				mSyncColId = mNextSelectedColId;
				mSyncPosition = mNextSelectedPosition;
				if ( v != null ) {
					mSpecificLeft = v.getLeft();
				}
				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() ) {
					mSyncColId = adapter.getItemId( mFirstPosition );
				} else {
					mSyncColId = NO_ID;
				}
				mSyncPosition = mFirstPosition;
				if ( v != null ) {
					mSpecificLeft = v.getLeft();
				}
				mSyncMode = SYNC_FIRST_POSITION;
			}
		}
	}
}
