content: /**
   * Copyright 2010-present Facebook.
   *
   * 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.facebook.widget;
  
  import android.app.Activity;
  import android.content.Context;
  import android.content.res.TypedArray;
  import android.graphics.drawable.Drawable;
  import android.os.Bundle;
  import android.support.v4.app.Fragment;
  import android.support.v4.app.LoaderManager;
  import android.support.v4.content.Loader;
  import android.text.TextUtils;
  import android.util.AttributeSet;
  import android.view.LayoutInflater;
  import android.view.View;
  import android.view.ViewGroup;
  import android.view.ViewStub;
  import android.view.animation.AlphaAnimation;
  import android.widget.*;
  import com.facebook.*;
  import com.facebook.android.R;
  import com.facebook.model.GraphObject;
  import com.facebook.internal.SessionTracker;
  
  import java.util.*;
  
  /**
   * Provides functionality common to SDK UI elements that allow the user to pick one or more
   * graph objects (e.g., places, friends) from a list of possibilities. The UI is exposed as a
   * Fragment to allow to it to be included in an Activity along with other Fragments. The Fragments
   * can be configured by passing parameters as part of their Intent bundle, or (for certain
   * properties) by specifying attributes in their XML layout files.
   * <br/>
   * PickerFragments support callbacks that will be called in the event of an error, when the
   * underlying data has been changed, or when the set of selected graph objects changes.
   */
  public abstract class PickerFragment<T extends GraphObject> extends Fragment {
      /**
       * The key for a boolean parameter in the fragment's Intent bundle to indicate whether the
       * picker should show pictures (if available) for the graph objects.
       */
      public static final String SHOW_PICTURES_BUNDLE_KEY = "com.facebook.widget.PickerFragment.ShowPictures";
      /**
       * The key for a String parameter in the fragment's Intent bundle to indicate which extra fields
       * beyond the default fields should be retrieved for any graph objects in the results.
       */
      public static final String EXTRA_FIELDS_BUNDLE_KEY = "com.facebook.widget.PickerFragment.ExtraFields";
      /**
       * The key for a boolean parameter in the fragment's Intent bundle to indicate whether the
       * picker should display a title bar with a Done button.
       */
      public static final String SHOW_TITLE_BAR_BUNDLE_KEY = "com.facebook.widget.PickerFragment.ShowTitleBar";
      /**
       * The key for a String parameter in the fragment's Intent bundle to indicate the text to
       * display in the title bar.
       */
      public static final String TITLE_TEXT_BUNDLE_KEY = "com.facebook.widget.PickerFragment.TitleText";
      /**
       * The key for a String parameter in the fragment's Intent bundle to indicate the text to
       * display in the Done btuton.
       */
      public static final String DONE_BUTTON_TEXT_BUNDLE_KEY = "com.facebook.widget.PickerFragment.DoneButtonText";
  
      private static final String SELECTION_BUNDLE_KEY = "com.facebook.android.PickerFragment.Selection";
      private static final String ACTIVITY_CIRCLE_SHOW_KEY = "com.facebook.android.PickerFragment.ActivityCircleShown";
      private static final int PROFILE_PICTURE_PREFETCH_BUFFER = 5;
  
      private final int layout;
      private OnErrorListener onErrorListener;
      private OnDataChangedListener onDataChangedListener;
      private OnSelectionChangedListener onSelectionChangedListener;
      private OnDoneButtonClickedListener onDoneButtonClickedListener;
      private GraphObjectFilter<T> filter;
      private boolean showPictures = true;
      private boolean showTitleBar = true;
      private ListView listView;
      HashSet<String> extraFields = new HashSet<String>();
      GraphObjectAdapter<T> adapter;
      private final Class<T> graphObjectClass;
      private LoadingStrategy loadingStrategy;
      private SelectionStrategy selectionStrategy;
      private ProgressBar activityCircle;
      private SessionTracker sessionTracker;
      private String titleText;
      private String doneButtonText;
      private TextView titleTextView;
      private Button doneButton;
      private Drawable titleBarBackground;
      private Drawable doneButtonBackground;
      private boolean appEventsLogged;
  
      PickerFragment(Class<T> graphObjectClass, int layout, Bundle args) {
          this.graphObjectClass = graphObjectClass;
          this.layout = layout;
  
          setPickerFragmentSettingsFromBundle(args);
      }
  
      @Override
      public void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
  
          adapter = createAdapter();
          adapter.setFilter(new GraphObjectAdapter.Filter<T>() {
              @Override
              public boolean includeItem(T graphObject) {
                  return filterIncludesItem(graphObject);
              }
          });
      }
  
      @Override
      public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) {
          super.onInflate(activity, attrs, savedInstanceState);
          TypedArray a = activity.obtainStyledAttributes(attrs, R.styleable.com_facebook_picker_fragment);
  
          setShowPictures(a.getBoolean(R.styleable.com_facebook_picker_fragment_show_pictures, showPictures));
          String extraFieldsString = a.getString(R.styleable.com_facebook_picker_fragment_extra_fields);
          if (extraFieldsString != null) {
              String[] strings = extraFieldsString.split(",");
              setExtraFields(Arrays.asList(strings));
          }
  
          showTitleBar = a.getBoolean(R.styleable.com_facebook_picker_fragment_show_title_bar, showTitleBar);
          titleText = a.getString(R.styleable.com_facebook_picker_fragment_title_text);
          doneButtonText = a.getString(R.styleable.com_facebook_picker_fragment_done_button_text);
          titleBarBackground = a.getDrawable(R.styleable.com_facebook_picker_fragment_title_bar_background);
          doneButtonBackground = a.getDrawable(R.styleable.com_facebook_picker_fragment_done_button_background);
  
          a.recycle();
      }
  
      @Override
      public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
          ViewGroup view = (ViewGroup) inflater.inflate(layout, container, false);
  
          listView = (ListView) view.findViewById(R.id.com_facebook_picker_list_view);
          listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
              @Override
              public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
                  onListItemClick((ListView) parent, v, position);
              }
          });
          listView.setOnLongClickListener(new View.OnLongClickListener() {
              @Override
              public boolean onLongClick(View v) {
                  // We don't actually do anything differently on long-clicks, but setting the listener
                  // enables the selector transition that we have for visual consistency with the
                  // Facebook app's pickers.
                  return false;
              }
          });
          listView.setOnScrollListener(onScrollListener);
  
          activityCircle = (ProgressBar) view.findViewById(R.id.com_facebook_picker_activity_circle);
  
          setupViews(view);
  
          listView.setAdapter(adapter);
  
          return view;
      }
  
      @Override
      public void onActivityCreated(final Bundle savedInstanceState) {
          super.onActivityCreated(savedInstanceState);
  
          sessionTracker = new SessionTracker(getActivity(), new Session.StatusCallback() {
              @Override
              public void call(Session session, SessionState state, Exception exception) {
                  if (!session.isOpened()) {
                      // When a session is closed, we want to clear out our data so it is not visible to subsequent users
                      clearResults();
                  }
              }
          });
  
          setSettingsFromBundle(savedInstanceState);
  
          loadingStrategy = createLoadingStrategy();
          loadingStrategy.attach(adapter);
  
          selectionStrategy = createSelectionStrategy();
          selectionStrategy.readSelectionFromBundle(savedInstanceState, SELECTION_BUNDLE_KEY);
  
          // Should we display a title bar? (We need to do this after we've retrieved our bundle settings.)
          if (showTitleBar) {
              inflateTitleBar((ViewGroup) getView());
          }
  
          if (activityCircle != null && savedInstanceState != null) {
              boolean shown = savedInstanceState.getBoolean(ACTIVITY_CIRCLE_SHOW_KEY, false);
              if (shown) {
                  displayActivityCircle();
              } else {
                  // Should be hidden already, but just to be sure.
                  hideActivityCircle();
              }
          }
      }
  
      @Override
      public void onDetach() {
          super.onDetach();
  
          listView.setOnScrollListener(null);
          listView.setAdapter(null);
  
          loadingStrategy.detach();
          sessionTracker.stopTracking();
      }
  
      @Override
      public void onSaveInstanceState(Bundle outState) {
          super.onSaveInstanceState(outState);
  
          saveSettingsToBundle(outState);
          selectionStrategy.saveSelectionToBundle(outState, SELECTION_BUNDLE_KEY);
          if (activityCircle != null) {
              outState.putBoolean(ACTIVITY_CIRCLE_SHOW_KEY, activityCircle.getVisibility() == View.VISIBLE);
          }
      }
  
      @Override
      public void onStop() {
          if (!appEventsLogged) {
              logAppEvents(false);
          }
          super.onStop();
      }
  
      @Override
      public void setArguments(Bundle args) {
          super.setArguments(args);
          setSettingsFromBundle(args);
      }
  
      /**
       * Gets the current OnDataChangedListener for this fragment, which will be called whenever
       * the underlying data being displaying in the picker has changed.
       *
       * @return the OnDataChangedListener, or null if there is none
       */
      public OnDataChangedListener getOnDataChangedListener() {
          return onDataChangedListener;
      }
  
      /**
       * Sets the current OnDataChangedListener for this fragment, which will be called whenever
       * the underlying data being displaying in the picker has changed.
       *
       * @param onDataChangedListener the OnDataChangedListener, or null if there is none
       */
      public void setOnDataChangedListener(OnDataChangedListener onDataChangedListener) {
          this.onDataChangedListener = onDataChangedListener;
      }
  
      /**
       * Gets the current OnSelectionChangedListener for this fragment, which will be called
       * whenever the user selects or unselects a graph object in the list.
       *
       * @return the OnSelectionChangedListener, or null if there is none
       */
      public OnSelectionChangedListener getOnSelectionChangedListener() {
          return onSelectionChangedListener;
      }
  
      /**
       * Sets the current OnSelectionChangedListener for this fragment, which will be called
       * whenever the user selects or unselects a graph object in the list.
       *
       * @param onSelectionChangedListener the OnSelectionChangedListener, or null if there is none
       */
      public void setOnSelectionChangedListener(
              OnSelectionChangedListener onSelectionChangedListener) {
          this.onSelectionChangedListener = onSelectionChangedListener;
      }
  
      /**
       * Gets the current OnDoneButtonClickedListener for this fragment, which will be called
       * when the user clicks the Done button.
       *
       * @return the OnDoneButtonClickedListener, or null if there is none
       */
      public OnDoneButtonClickedListener getOnDoneButtonClickedListener() {
          return onDoneButtonClickedListener;
      }
  
      /**
       * Sets the current OnDoneButtonClickedListener for this fragment, which will be called
       * when the user clicks the Done button. This will only be possible if the title bar is
       * being shown in this fragment.
       *
       * @param onDoneButtonClickedListener the OnDoneButtonClickedListener, or null if there is none
       */
      public void setOnDoneButtonClickedListener(OnDoneButtonClickedListener onDoneButtonClickedListener) {
          this.onDoneButtonClickedListener = onDoneButtonClickedListener;
      }
  
      /**
       * Gets the current OnErrorListener for this fragment, which will be called in the event
       * of network or other errors encountered while populating the graph objects in the list.
       *
       * @return the OnErrorListener, or null if there is none
       */
      public OnErrorListener getOnErrorListener() {
          return onErrorListener;
      }
  
      /**
       * Sets the current OnErrorListener for this fragment, which will be called in the event
       * of network or other errors encountered while populating the graph objects in the list.
       *
       * @param onErrorListener the OnErrorListener, or null if there is none
       */
      public void setOnErrorListener(OnErrorListener onErrorListener) {
          this.onErrorListener = onErrorListener;
      }
  
      /**
       * Gets the current filter for this fragment, which will be called for each graph object
       * returned from the service to determine if it should be displayed in the list.
       * If no filter is specified, all retrieved graph objects will be displayed.
       *
       * @return the GraphObjectFilter, or null if there is none
       */
      public GraphObjectFilter<T> getFilter() {
          return filter;
      }
  
      /**
       * Sets the current filter for this fragment, which will be called for each graph object
       * returned from the service to determine if it should be displayed in the list.
       * If no filter is specified, all retrieved graph objects will be displayed.
       *
       * @param filter the GraphObjectFilter, or null if there is none
       */
      public void setFilter(GraphObjectFilter<T> filter) {
          this.filter = filter;
      }
  
      /**
       * Gets the Session to use for any Facebook requests this fragment will make.
       *
       * @return the Session that will be used for any Facebook requests, or null if there is none
       */
      public Session getSession() {
          return sessionTracker.getSession();
      }
  
      /**
       * Sets the Session to use for any Facebook requests this fragment will make. If the
       * parameter is null, the fragment will use the current active session, if any.
       *
       * @param session the Session to use for Facebook requests, or null to use the active session
       */
      public void setSession(Session session) {
          sessionTracker.setSession(session);
      }
  
      /**
       * Gets whether to display pictures, if available, for displayed graph objects.
       *
       * @return true if pictures should be displayed, false if not
       */
      public boolean getShowPictures() {
          return showPictures;
      }
  
      /**
       * Sets whether to display pictures, if available, for displayed graph objects.
       *
       * @param showPictures true if pictures should be displayed, false if not
       */
      public void setShowPictures(boolean showPictures) {
          this.showPictures = showPictures;
      }
  
      /**
       * Gets the extra fields to request for the retrieved graph objects.
       *
       * @return the extra fields to request
       */
      public Set<String> getExtraFields() {
          return new HashSet<String>(extraFields);
      }
  
      /**
       * Sets the extra fields to request for the retrieved graph objects.
       *
       * @param fields the extra fields to request
       */
      public void setExtraFields(Collection<String> fields) {
          extraFields = new HashSet<String>();
          if (fields != null) {
              extraFields.addAll(fields);
          }
      }
  
      /**
       * Sets whether to show a title bar with a Done button. This must be
       * called prior to the Fragment going through its creation lifecycle to have an effect.
       *
       * @param showTitleBar true if a title bar should be displayed, false if not
       */
      public void setShowTitleBar(boolean showTitleBar) {
          this.showTitleBar = showTitleBar;
      }
  
      /**
       * Gets whether to show a title bar with a Done button. The default is true.
       *
       * @return true if a title bar will be shown, false if not.
       */
      public boolean getShowTitleBar() {
          return showTitleBar;
      }
  
      /**
       * Sets the text to show in the title bar, if a title bar is to be shown. This must be
       * called prior to the Fragment going through its creation lifecycle to have an effect, or
       * the default will be used.
       *
       * @param titleText the text to show in the title bar
       */
      public void setTitleText(String titleText) {
          this.titleText = titleText;
      }
  
      /**
       * Gets the text to show in the title bar, if a title bar is to be shown.
       *
       * @return the text to show in the title bar
       */
      public String getTitleText() {
          if (titleText == null) {
              titleText = getDefaultTitleText();
          }
          return titleText;
      }
  
      /**
       * Sets the text to show in the Done button, if a title bar is to be shown. This must be
       * called prior to the Fragment going through its creation lifecycle to have an effect, or
       * the default will be used.
       *
       * @param doneButtonText the text to show in the Done button
       */
      public void setDoneButtonText(String doneButtonText) {
          this.doneButtonText = doneButtonText;
      }
  
      /**
       * Gets the text to show in the Done button, if a title bar is to be shown.
       *
       * @return the text to show in the Done button
       */
      public String getDoneButtonText() {
          if (doneButtonText == null) {
              doneButtonText = getDefaultDoneButtonText();
          }
          return doneButtonText;
      }
  
      /**
       * Causes the picker to load data from the service and display it to the user.
       *
       * @param forceReload if true, data will be loaded even if there is already data being displayed (or loading);
       *                    if false, data will not be re-loaded if it is already displayed (or loading)
       */
      public void loadData(boolean forceReload) {
          if (!forceReload && loadingStrategy.isDataPresentOrLoading()) {
              return;
          }
          loadDataSkippingRoundTripIfCached();
      }
  
      /**
       * Updates the properties of the PickerFragment based on the contents of the supplied Bundle;
       * calling Activities may use this to pass additional configuration information to the
       * PickerFragment beyond what is specified in its XML layout.
       *
       * @param inState a Bundle containing keys corresponding to properties of the PickerFragment
       */
      public void setSettingsFromBundle(Bundle inState) {
          setPickerFragmentSettingsFromBundle(inState);
      }
  
      void setupViews(ViewGroup view) {
      }
  
      boolean filterIncludesItem(T graphObject) {
          if (filter != null) {
              return filter.includeItem(graphObject);
          }
          return true;
      }
  
      List<T> getSelectedGraphObjects() {
          return adapter.getGraphObjectsById(selectionStrategy.getSelectedIds());
      }
  
      void setSelectedGraphObjects(List<String> objectIds) {
          for(String objectId : objectIds) {
              if(!this.selectionStrategy.isSelected(objectId)) {
                  this.selectionStrategy.toggleSelection(objectId);
              }
          }
      }
  
      void saveSettingsToBundle(Bundle outState) {
          outState.putBoolean(SHOW_PICTURES_BUNDLE_KEY, showPictures);
          if (!extraFields.isEmpty()) {
              outState.putString(EXTRA_FIELDS_BUNDLE_KEY, TextUtils.join(",", extraFields));
          }
          outState.putBoolean(SHOW_TITLE_BAR_BUNDLE_KEY, showTitleBar);
          outState.putString(TITLE_TEXT_BUNDLE_KEY, titleText);
          outState.putString(DONE_BUTTON_TEXT_BUNDLE_KEY, doneButtonText);
      }
  
      abstract Request getRequestForLoadData(Session session);
  
      abstract PickerFragmentAdapter<T> createAdapter();
  
      abstract LoadingStrategy createLoadingStrategy();
  
      abstract SelectionStrategy createSelectionStrategy();
  
      void onLoadingData() {
      }
  
      String getDefaultTitleText() {
          return null;
      }
  
      String getDefaultDoneButtonText() {
          return getString(R.string.com_facebook_picker_done_button_text);
      }
  
      void displayActivityCircle() {
          if (activityCircle != null) {
              layoutActivityCircle();
              activityCircle.setVisibility(View.VISIBLE);
          }
      }
  
      void layoutActivityCircle() {
          // If we've got no data, make the activity circle full-opacity. Otherwise we'll dim it to avoid
          //  cluttering the UI.
          float alpha = (!adapter.isEmpty()) ? .25f : 1.0f;
          setAlpha(activityCircle, alpha);
      }
  
      void hideActivityCircle() {
          if (activityCircle != null) {
              // We use an animation to dim the activity circle; need to clear this or it will remain visible.
              activityCircle.clearAnimation();
              activityCircle.setVisibility(View.INVISIBLE);
          }
      }
  
      void setSelectionStrategy(SelectionStrategy selectionStrategy) {
          if (selectionStrategy != this.selectionStrategy) {
              this.selectionStrategy = selectionStrategy;
              if (adapter != null) {
                  // Adapter should cause a re-render.
                  adapter.notifyDataSetChanged();
              }
          }
      }
  
      void logAppEvents(boolean doneButtonClicked) {
      }
  
      private static void setAlpha(View view, float alpha) {
          // Set the alpha appropriately (setAlpha is API >= 11, this technique works on all API levels).
          AlphaAnimation alphaAnimation = new AlphaAnimation(alpha, alpha);
          alphaAnimation.setDuration(0);
          alphaAnimation.setFillAfter(true);
          view.startAnimation(alphaAnimation);
      }
  
  
      private void setPickerFragmentSettingsFromBundle(Bundle inState) {
          // We do this in a separate non-overridable method so it is safe to call from the constructor.
          if (inState != null) {
              showPictures = inState.getBoolean(SHOW_PICTURES_BUNDLE_KEY, showPictures);
              String extraFieldsString = inState.getString(EXTRA_FIELDS_BUNDLE_KEY);
              if (extraFieldsString != null) {
                  String[] strings = extraFieldsString.split(",");
                  setExtraFields(Arrays.asList(strings));
              }
              showTitleBar = inState.getBoolean(SHOW_TITLE_BAR_BUNDLE_KEY, showTitleBar);
              String titleTextString = inState.getString(TITLE_TEXT_BUNDLE_KEY);
              if (titleTextString != null) {
                  titleText = titleTextString;
                  if (titleTextView != null) {
                      titleTextView.setText(titleText);
                  }
              }
              String doneButtonTextString = inState.getString(DONE_BUTTON_TEXT_BUNDLE_KEY);
              if (doneButtonTextString != null) {
                  doneButtonText = doneButtonTextString;
                  if (doneButton != null) {
                      doneButton.setText(doneButtonText);
                  }
              }
          }
      }
  
      private void inflateTitleBar(ViewGroup view) {
          ViewStub stub = (ViewStub) view.findViewById(R.id.com_facebook_picker_title_bar_stub);
          if (stub != null) {
              View titleBar = stub.inflate();
  
              final RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
                      RelativeLayout.LayoutParams.MATCH_PARENT,
                      RelativeLayout.LayoutParams.MATCH_PARENT);
              layoutParams.addRule(RelativeLayout.BELOW, R.id.com_facebook_picker_title_bar);
              listView.setLayoutParams(layoutParams);
  
              if (titleBarBackground != null) {
                  titleBar.setBackgroundDrawable(titleBarBackground);
              }
  
              doneButton = (Button) view.findViewById(R.id.com_facebook_picker_done_button);
              if (doneButton != null) {
                  doneButton.setOnClickListener(new View.OnClickListener() {
                      @Override
                      public void onClick(View v) {
                          logAppEvents(true);
                          appEventsLogged = true;
  
                          if (onDoneButtonClickedListener != null) {
                              onDoneButtonClickedListener.onDoneButtonClicked(PickerFragment.this);
                          }
                      }
                  });
  
                  if (getDoneButtonText() != null) {
                      doneButton.setText(getDoneButtonText());
                  }
  
                  if (doneButtonBackground != null) {
                      doneButton.setBackgroundDrawable(doneButtonBackground);
                  }
              }
  
              titleTextView = (TextView) view.findViewById(R.id.com_facebook_picker_title);
              if (titleTextView != null) {
                  if (getTitleText() != null) {
                      titleTextView.setText(getTitleText());
                  }
              }
          }
      }
  
      private void onListItemClick(ListView listView, View v, int position) {
          @SuppressWarnings("unchecked")
          T graphObject = (T) listView.getItemAtPosition(position);
          String id = adapter.getIdOfGraphObject(graphObject);
          selectionStrategy.toggleSelection(id);
          adapter.notifyDataSetChanged();
  
          if (onSelectionChangedListener != null) {
              onSelectionChangedListener.onSelectionChanged(PickerFragment.this);
          }
      }
  
      private void loadDataSkippingRoundTripIfCached() {
          clearResults();
  
          Request request = getRequestForLoadData(getSession());
          if (request != null) {
              onLoadingData();
              loadingStrategy.startLoading(request);
          }
      }
  
      private void clearResults() {
          if (adapter != null) {
              boolean wasSelection = !selectionStrategy.isEmpty();
              boolean wasData = !adapter.isEmpty();
  
              loadingStrategy.clearResults();
              selectionStrategy.clear();
              adapter.notifyDataSetChanged();
  
              // Tell anyone who cares the data and selection has changed, if they have.
              if (wasData && onDataChangedListener != null) {
                  onDataChangedListener.onDataChanged(PickerFragment.this);
              }
              if (wasSelection && onSelectionChangedListener != null) {
                  onSelectionChangedListener.onSelectionChanged(PickerFragment.this);
              }
          }
      }
  
      void updateAdapter(SimpleGraphObjectCursor<T> data) {
          if (adapter != null) {
              // As we fetch additional results and add them to the table, we do not
              // want the items displayed jumping around seemingly at random, frustrating the user's
              // attempts at scrolling, etc. Since results may be added anywhere in
              // the table, we choose to try to keep the first visible row in a fixed
              // position (from the user's perspective). We try to keep it positioned at
              // the same offset from the top of the screen so adding new items seems
              // smoother, as opposed to having it "snap" to a multiple of row height
  
              // We use the second row, to give context above and below it and avoid
              // cases where the first row is only barely visible, thus providing little context.
              // The exception is where the very first row is visible, in which case we use that.
              View view = listView.getChildAt(1);
              int anchorPosition = listView.getFirstVisiblePosition();
              if (anchorPosition > 0) {
                  anchorPosition++;
              }
              GraphObjectAdapter.SectionAndItem<T> anchorItem = adapter.getSectionAndItem(anchorPosition);
              final int top = (view != null &&
                      anchorItem.getType() != GraphObjectAdapter.SectionAndItem.Type.ACTIVITY_CIRCLE) ?
                      view.getTop() : 0;
  
              // Now actually add the results.
              boolean dataChanged = adapter.changeCursor(data);
  
              if (view != null && anchorItem != null) {
                  // Put the item back in the same spot it was.
                  final int newPositionOfItem = adapter.getPosition(anchorItem.sectionKey, anchorItem.graphObject);
                  if (newPositionOfItem != -1) {
                      listView.setSelectionFromTop(newPositionOfItem, top);
                  }
              }
  
              if (dataChanged && onDataChangedListener != null) {
                  onDataChangedListener.onDataChanged(PickerFragment.this);
              }
          }
      }
  
      private void reprioritizeDownloads() {
          int lastVisibleItem = listView.getLastVisiblePosition();
          if (lastVisibleItem >= 0) {
              int firstVisibleItem = listView.getFirstVisiblePosition();
              adapter.prioritizeViewRange(firstVisibleItem, lastVisibleItem, PROFILE_PICTURE_PREFETCH_BUFFER);
          }
      }
  
      private ListView.OnScrollListener onScrollListener = new ListView.OnScrollListener() {
          @Override
          public void onScrollStateChanged(AbsListView view, int scrollState) {
          }
  
          @Override
          public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
              reprioritizeDownloads();
          }
      };
  
      /**
       * Callback interface that will be called when a network or other error is encountered
       * while retrieving graph objects.
       */
      public interface OnErrorListener {
          /**
           * Called when a network or other error is encountered.
           *
           * @param error a FacebookException representing the error that was encountered.
           */
          void onError(PickerFragment<?> fragment, FacebookException error);
      }
  
      /**
       * Callback interface that will be called when the underlying data being displayed in the
       * picker has been updated.
       */
      public interface OnDataChangedListener {
          /**
           * Called when the set of data being displayed in the picker has changed.
           */
          void onDataChanged(PickerFragment<?> fragment);
      }
  
      /**
       * Callback interface that will be called when the user selects or unselects graph objects
       * in the picker.
       */
      public interface OnSelectionChangedListener {
          /**
           * Called when the user selects or unselects graph objects in the picker.
           */
          void onSelectionChanged(PickerFragment<?> fragment);
      }
  
      /**
       * Callback interface that will be called when the user clicks the Done button on the
       * title bar.
       */
      public interface OnDoneButtonClickedListener {
          /**
           * Called when the user clicks the Done button.
           */
          void onDoneButtonClicked(PickerFragment<?> fragment);
      }
  
      /**
       * Callback interface that will be called to determine if a graph object should be displayed.
       *
       * @param <T>
       */
      public interface GraphObjectFilter<T> {
          /**
           * Called to determine if a graph object should be displayed.
           *
           * @param graphObject the graph object
           * @return true to display the graph object, false to hide it
           */
          boolean includeItem(T graphObject);
      }
  
      abstract class LoadingStrategy {
          protected final static int CACHED_RESULT_REFRESH_DELAY = 2 * 1000;
  
          protected GraphObjectPagingLoader<T> loader;
          protected GraphObjectAdapter<T> adapter;
  
          public void attach(GraphObjectAdapter<T> adapter) {
              loader = (GraphObjectPagingLoader<T>) getLoaderManager().initLoader(0, null,
                      new LoaderManager.LoaderCallbacks<SimpleGraphObjectCursor<T>>() {
                          @Override
                          public Loader<SimpleGraphObjectCursor<T>> onCreateLoader(int id, Bundle args) {
                              return LoadingStrategy.this.onCreateLoader();
                          }
  
                          @Override
                          public void onLoadFinished(Loader<SimpleGraphObjectCursor<T>> loader,
                                  SimpleGraphObjectCursor<T> data) {
                              if (loader != LoadingStrategy.this.loader) {
                                  throw new FacebookException("Received callback for unknown loader.");
                              }
                              LoadingStrategy.this.onLoadFinished((GraphObjectPagingLoader<T>) loader, data);
                          }
  
                          @Override
                          public void onLoaderReset(Loader<SimpleGraphObjectCursor<T>> loader) {
                              if (loader != LoadingStrategy.this.loader) {
                                  throw new FacebookException("Received callback for unknown loader.");
                              }
                              LoadingStrategy.this.onLoadReset((GraphObjectPagingLoader<T>) loader);
                          }
                      });
  
              loader.setOnErrorListener(new GraphObjectPagingLoader.OnErrorListener() {
                  @Override
                  public void onError(FacebookException error, GraphObjectPagingLoader<?> loader) {
                      hideActivityCircle();
                      if (onErrorListener != null) {
                          onErrorListener.onError(PickerFragment.this, error);
                      }
                  }
              });
  
              this.adapter = adapter;
              // Tell the adapter about any data we might already have.
              this.adapter.changeCursor(loader.getCursor());
              this.adapter.setOnErrorListener(new GraphObjectAdapter.OnErrorListener() {
                  @Override
                  public void onError(GraphObjectAdapter<?> adapter, FacebookException error) {
                      if (onErrorListener != null) {
                          onErrorListener.onError(PickerFragment.this, error);
                      }
                  }
              });
          }
  
          public void detach() {
              adapter.setDataNeededListener(null);
              adapter.setOnErrorListener(null);
              loader.setOnErrorListener(null);
  
              loader = null;
              adapter = null;
          }
  
          public void clearResults() {
              if (loader != null) {
                  loader.clearResults();
              }
          }
  
          public void startLoading(Request request) {
              if (loader != null) {
                  loader.startLoading(request, true);
                  onStartLoading(loader, request);
              }
          }
  
          public boolean isDataPresentOrLoading() {
              return !adapter.isEmpty() || loader.isLoading();
          }
  
          protected GraphObjectPagingLoader<T> onCreateLoader() {
              return new GraphObjectPagingLoader<T>(getActivity(), graphObjectClass);
          }
  
          protected void onStartLoading(GraphObjectPagingLoader<T> loader, Request request) {
              displayActivityCircle();
          }
  
          protected void onLoadReset(GraphObjectPagingLoader<T> loader) {
              adapter.changeCursor(null);
          }
  
          protected void onLoadFinished(GraphObjectPagingLoader<T> loader, SimpleGraphObjectCursor<T> data) {
              updateAdapter(data);
          }
      }
  
      abstract class SelectionStrategy {
          abstract boolean isSelected(String id);
  
          abstract void toggleSelection(String id);
  
          abstract Collection<String> getSelectedIds();
  
          abstract void clear();
  
          abstract boolean isEmpty();
  
          abstract boolean shouldShowCheckBoxIfUnselected();
  
          abstract void saveSelectionToBundle(Bundle outBundle, String key);
  
          abstract void readSelectionFromBundle(Bundle inBundle, String key);
      }
  
      class SingleSelectionStrategy extends SelectionStrategy {
          private String selectedId;
  
          public Collection<String> getSelectedIds() {
              return Arrays.asList(new String[]{selectedId});
          }
  
          @Override
          boolean isSelected(String id) {
              return selectedId != null && id != null && selectedId.equals(id);
          }
  
          @Override
          void toggleSelection(String id) {
              if (selectedId != null && selectedId.equals(id)) {
                  selectedId = null;
              } else {
                  selectedId = id;
              }
          }
  
          @Override
          void saveSelectionToBundle(Bundle outBundle, String key) {
              if (!TextUtils.isEmpty(selectedId)) {
                  outBundle.putString(key, selectedId);
              }
          }
  
          @Override
          void readSelectionFromBundle(Bundle inBundle, String key) {
              if (inBundle != null) {
                  selectedId = inBundle.getString(key);
              }
          }
  
          @Override
          public void clear() {
              selectedId = null;
          }
  
          @Override
          boolean isEmpty() {
              return selectedId == null;
          }
  
          @Override
          boolean shouldShowCheckBoxIfUnselected() {
              return false;
          }
      }
  
      class MultiSelectionStrategy extends SelectionStrategy {
          private Set<String> selectedIds = new HashSet<String>();
  
          public Collection<String> getSelectedIds() {
              return selectedIds;
          }
  
          @Override
          boolean isSelected(String id) {
              return id != null && selectedIds.contains(id);
          }
  
          @Override
          void toggleSelection(String id) {
              if (id != null) {
                  if (selectedIds.contains(id)) {
                      selectedIds.remove(id);
                  } else {
                      selectedIds.add(id);
                  }
              }
          }
  
          @Override
          void saveSelectionToBundle(Bundle outBundle, String key) {
              if (!selectedIds.isEmpty()) {
                  String ids = TextUtils.join(",", selectedIds);
                  outBundle.putString(key, ids);
              }
          }
  
          @Override
          void readSelectionFromBundle(Bundle inBundle, String key) {
              if (inBundle != null) {
                  String ids = inBundle.getString(key);
                  if (ids != null) {
                      String[] splitIds = TextUtils.split(ids, ",");
                      selectedIds.clear();
                      Collections.addAll(selectedIds, splitIds);
                  }
              }
          }
  
          @Override
          public void clear() {
              selectedIds.clear();
          }
  
          @Override
          boolean isEmpty() {
              return selectedIds.isEmpty();
          }
  
          @Override
          boolean shouldShowCheckBoxIfUnselected() {
              return true;
          }
      }
  
      abstract class PickerFragmentAdapter<U extends GraphObject> extends GraphObjectAdapter<T> {
          public PickerFragmentAdapter(Context context) {
              super(context);
          }
  
          @Override
          boolean isGraphObjectSelected(String graphObjectId) {
              return selectionStrategy.isSelected(graphObjectId);
          }
  
          @Override
          void updateCheckboxState(CheckBox checkBox, boolean graphObjectSelected) {
              checkBox.setChecked(graphObjectSelected);
              int visible = (graphObjectSelected || selectionStrategy
                      .shouldShowCheckBoxIfUnselected()) ? View.VISIBLE : View.GONE;
              checkBox.setVisibility(visible);
          }
      }
  }
  