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.location.Location;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.Looper;
  import android.text.Editable;
  import android.text.TextUtils;
  import android.text.TextWatcher;
  import android.util.AttributeSet;
  import android.view.View;
  import android.view.ViewGroup;
  import android.view.inputmethod.InputMethodManager;
  import android.widget.EditText;
  import android.widget.ListView;
  import com.facebook.*;
  import com.facebook.android.R;
  import com.facebook.internal.AnalyticsEvents;
  import com.facebook.model.GraphPlace;
  import com.facebook.internal.Logger;
  import com.facebook.internal.Utility;
  
  import java.util.*;
  
  public class PlacePickerFragment extends PickerFragment<GraphPlace> {
      /**
       * The key for an int parameter in the fragment's Intent bundle to indicate the radius in meters around
       * the center point to search. The default is 1000 meters.
       */
      public static final String RADIUS_IN_METERS_BUNDLE_KEY = "com.facebook.widget.PlacePickerFragment.RadiusInMeters";
      /**
       * The key for an int parameter in the fragment's Intent bundle to indicate what how many results to
       * return at a time. The default is 100 results.
       */
      public static final String RESULTS_LIMIT_BUNDLE_KEY = "com.facebook.widget.PlacePickerFragment.ResultsLimit";
      /**
       * The key for a String parameter in the fragment's Intent bundle to indicate what search text should
       * be sent to the service. The default is to have no search text.
       */
      public static final String SEARCH_TEXT_BUNDLE_KEY = "com.facebook.widget.PlacePickerFragment.SearchText";
      /**
       * The key for a Location parameter in the fragment's Intent bundle to indicate what geographical
       * location should be the center of the search.
       */
      public static final String LOCATION_BUNDLE_KEY = "com.facebook.widget.PlacePickerFragment.Location";
      /**
       * The key for a boolean parameter in the fragment's Intent bundle to indicate that the fragment
       * should display a search box and automatically update the search text as it changes.
       */
      public static final String SHOW_SEARCH_BOX_BUNDLE_KEY = "com.facebook.widget.PlacePickerFragment.ShowSearchBox";
  
      /**
       * The default radius around the center point to search.
       */
      public static final int DEFAULT_RADIUS_IN_METERS = 1000;
      /**
       * The default number of results to retrieve.
       */
      public static final int DEFAULT_RESULTS_LIMIT = 100;
  
      private static final int searchTextTimerDelayInMilliseconds = 2 * 1000;
  
      private static final String ID = "id";
      private static final String NAME = "name";
      private static final String LOCATION = "location";
      private static final String CATEGORY = "category";
      private static final String WERE_HERE_COUNT = "were_here_count";
      private static final String TAG = "PlacePickerFragment";
  
      private Location location;
      private int radiusInMeters = DEFAULT_RADIUS_IN_METERS;
      private int resultsLimit = DEFAULT_RESULTS_LIMIT;
      private String searchText;
      private Timer searchTextTimer;
      private boolean hasSearchTextChangedSinceLastQuery;
      private boolean showSearchBox = true;
      private EditText searchBox;
  
      /**
       * Default constructor. Creates a Fragment with all default properties.
       */
      public PlacePickerFragment() {
          this(null);
      }
  
      /**
       * Constructor.
       *
       * @param args a Bundle that optionally contains one or more values containing additional
       *             configuration information for the Fragment.
       */
      public PlacePickerFragment(Bundle args) {
          super(GraphPlace.class, R.layout.com_facebook_placepickerfragment, args);
          setPlacePickerSettingsFromBundle(args);
      }
  
      /**
       * Gets the location to search around. Either the location or the search text (or both) must be specified.
       *
       * @return the Location to search around
       */
      public Location getLocation() {
          return location;
      }
  
      /**
       * Sets the location to search around. Either the location or the search text (or both) must be specified.
       *
       * @param location the Location to search around
       */
      public void setLocation(Location location) {
          this.location = location;
      }
  
      /**
       * Gets the radius in meters around the location to search.
       *
       * @return the radius in meters
       */
      public int getRadiusInMeters() {
          return radiusInMeters;
      }
  
      /**
       * Sets the radius in meters around the location to search.
       *
       * @param radiusInMeters the radius in meters
       */
      public void setRadiusInMeters(int radiusInMeters) {
          this.radiusInMeters = radiusInMeters;
      }
  
      /**
       * Gets the number of results to retrieve.
       *
       * @return the number of results to retrieve
       */
      public int getResultsLimit() {
          return resultsLimit;
      }
  
      /**
       * Sets the number of results to retrieve.
       *
       * @param resultsLimit the number of results to retrieve
       */
      public void setResultsLimit(int resultsLimit) {
          this.resultsLimit = resultsLimit;
      }
  
      /**
       * Gets the search text (e.g., category, name) to search for. Either the location or the search
       * text (or both) must be specified.
       *
       * @return the search text
       */
      public String getSearchText() {
          return searchText;
      }
  
      /**
       * Sets the search text (e.g., category, name) to search for. Either the location or the search
       * text (or both) must be specified. If a search box is displayed, this will update its contents
       * to the specified text.
       *
       * @param searchText the search text
       */
      public void setSearchText(String searchText) {
          if (TextUtils.isEmpty(searchText)) {
              searchText = null;
          }
          this.searchText = searchText;
          if (this.searchBox != null) {
              this.searchBox.setText(searchText);
          }
      }
  
      /**
       * Sets the search text and reloads the data in the control. This is used to provide search-box
       * functionality where the user may be typing or editing text rapidly. It uses a timer to avoid repeated
       * requerying, preferring to wait until the user pauses typing to refresh the data. Note that this
       * method will NOT update the text in the search box, if any, as it is intended to be called as a result
       * of changes to the search box (and is public to enable applications to provide their own search box
       * UI instead of the default one).
       *
       * @param searchText                 the search text
       * @param forceReloadEventIfSameText if true, will reload even if the search text has not changed; if false,
       *                                   identical search text will not force a reload
       */
      public void onSearchBoxTextChanged(String searchText, boolean forceReloadEventIfSameText) {
          if (!forceReloadEventIfSameText && Utility.stringsEqualOrEmpty(this.searchText, searchText)) {
              return;
          }
  
          if (TextUtils.isEmpty(searchText)) {
              searchText = null;
          }
          this.searchText = searchText;
  
          // If search text is being set in response to user input, it is wasteful to send a new request
          // with every keystroke. Send a request the first time the search text is set, then set up a 2-second timer
          // and send whatever changes the user has made since then. (If nothing has changed
          // in 2 seconds, we reset so the next change will cause an immediate re-query.)
          hasSearchTextChangedSinceLastQuery = true;
          if (searchTextTimer == null) {
              searchTextTimer = createSearchTextTimer();
          }
      }
  
      /**
       * Gets the currently-selected place.
       *
       * @return the currently-selected place, or null if there is none
       */
      public GraphPlace getSelection() {
          Collection<GraphPlace> selection = getSelectedGraphObjects();
          return (selection != null && !selection.isEmpty()) ? selection.iterator().next() : null;
      }
  
      public void setSettingsFromBundle(Bundle inState) {
          super.setSettingsFromBundle(inState);
          setPlacePickerSettingsFromBundle(inState);
      }
  
      @Override
      public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) {
          super.onInflate(activity, attrs, savedInstanceState);
          TypedArray a = activity.obtainStyledAttributes(attrs, R.styleable.com_facebook_place_picker_fragment);
  
          setRadiusInMeters(a.getInt(R.styleable.com_facebook_place_picker_fragment_radius_in_meters, radiusInMeters));
          setResultsLimit(a.getInt(R.styleable.com_facebook_place_picker_fragment_results_limit, resultsLimit));
          if (a.hasValue(R.styleable.com_facebook_place_picker_fragment_results_limit)) {
              setSearchText(a.getString(R.styleable.com_facebook_place_picker_fragment_search_text));
          }
          showSearchBox = a.getBoolean(R.styleable.com_facebook_place_picker_fragment_show_search_box, showSearchBox);
  
          a.recycle();
      }
  
      @Override
      void setupViews(ViewGroup view) {
          if (showSearchBox) {
              ListView listView = (ListView) view.findViewById(R.id.com_facebook_picker_list_view);
  
              View searchHeaderView = getActivity().getLayoutInflater().inflate(
                      R.layout.com_facebook_picker_search_box, listView, false);
  
              listView.addHeaderView(searchHeaderView, null, false);
  
              searchBox = (EditText) view.findViewById(R.id.com_facebook_picker_search_text);
  
              searchBox.addTextChangedListener(new SearchTextWatcher());
              if (!TextUtils.isEmpty(searchText)) {
                  searchBox.setText(searchText);
              }
          }
      }
  
      @Override
      public void onAttach(Activity activity) {
          super.onAttach(activity);
  
          if (searchBox != null) {
              InputMethodManager imm = (InputMethodManager)getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
              imm.showSoftInput(searchBox, InputMethodManager.SHOW_IMPLICIT);
          }
      }
  
      @Override
      public void onDetach() {
          super.onDetach();
  
          if (searchBox != null) {
              InputMethodManager imm = (InputMethodManager)getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
              imm.hideSoftInputFromWindow(searchBox.getWindowToken(), 0);
          }
      }
  
      void saveSettingsToBundle(Bundle outState) {
          super.saveSettingsToBundle(outState);
  
          outState.putInt(RADIUS_IN_METERS_BUNDLE_KEY, radiusInMeters);
          outState.putInt(RESULTS_LIMIT_BUNDLE_KEY, resultsLimit);
          outState.putString(SEARCH_TEXT_BUNDLE_KEY, searchText);
          outState.putParcelable(LOCATION_BUNDLE_KEY, location);
          outState.putBoolean(SHOW_SEARCH_BOX_BUNDLE_KEY, showSearchBox);
      }
  
      @Override
      void onLoadingData() {
          hasSearchTextChangedSinceLastQuery = false;
      }
  
      @Override
      Request getRequestForLoadData(Session session) {
          return createRequest(location, radiusInMeters, resultsLimit, searchText, extraFields, session);
      }
  
      @Override
      String getDefaultTitleText() {
          return getString(R.string.com_facebook_nearby);
      }
  
      @Override
      void logAppEvents(boolean doneButtonClicked) {
          AppEventsLogger logger = AppEventsLogger.newLogger(this.getActivity(), getSession());
          Bundle parameters = new Bundle();
  
          // If Done was clicked, we know this completed successfully. If not, we don't know (caller might have
          // dismissed us in response to selection changing, or user might have hit back button). Either way
          // we'll log the number of selections.
          String outcome = doneButtonClicked ? AnalyticsEvents.PARAMETER_DIALOG_OUTCOME_VALUE_COMPLETED :
                  AnalyticsEvents.PARAMETER_DIALOG_OUTCOME_VALUE_UNKNOWN;
          parameters.putString(AnalyticsEvents.PARAMETER_DIALOG_OUTCOME, outcome);
          parameters.putInt("num_places_picked", (getSelection() != null) ? 1 : 0);
  
          logger.logSdkEvent(AnalyticsEvents.EVENT_PLACE_PICKER_USAGE, null, parameters);
      }
  
      @Override
      PickerFragmentAdapter<GraphPlace> createAdapter() {
          PickerFragmentAdapter<GraphPlace> adapter = new PickerFragmentAdapter<GraphPlace>(
                  this.getActivity()) {
              @Override
              protected CharSequence getSubTitleOfGraphObject(GraphPlace graphObject) {
                  String category = graphObject.getCategory();
                  Integer wereHereCount = (Integer) graphObject.getProperty(WERE_HERE_COUNT);
  
                  String result = null;
                  if (category != null && wereHereCount != null) {
                      result = getString(R.string.com_facebook_placepicker_subtitle_format, category, wereHereCount);
                  } else if (category == null && wereHereCount != null) {
                      result = getString(R.string.com_facebook_placepicker_subtitle_were_here_only_format, wereHereCount);
                  } else if (category != null && wereHereCount == null) {
                      result = getString(R.string.com_facebook_placepicker_subtitle_catetory_only_format, category);
                  }
                  return result;
              }
  
              @Override
              protected int getGraphObjectRowLayoutId(GraphPlace graphObject) {
                  return R.layout.com_facebook_placepickerfragment_list_row;
              }
  
              @Override
              protected int getDefaultPicture() {
                  return R.drawable.com_facebook_place_default_icon;
              }
  
          };
          adapter.setShowCheckbox(false);
          adapter.setShowPicture(getShowPictures());
          return adapter;
      }
  
      @Override
      LoadingStrategy createLoadingStrategy() {
          return new AsNeededLoadingStrategy();
      }
  
      @Override
      SelectionStrategy createSelectionStrategy() {
          return new SingleSelectionStrategy();
      }
  
      private Request createRequest(Location location, int radiusInMeters, int resultsLimit, String searchText,
              Set<String> extraFields,
              Session session) {
          Request request = Request.newPlacesSearchRequest(session, location, radiusInMeters, resultsLimit, searchText,
                  null);
  
          Set<String> fields = new HashSet<String>(extraFields);
          String[] requiredFields = new String[]{
                  ID,
                  NAME,
                  LOCATION,
                  CATEGORY,
                  WERE_HERE_COUNT
          };
          fields.addAll(Arrays.asList(requiredFields));
  
          String pictureField = adapter.getPictureFieldSpecifier();
          if (pictureField != null) {
              fields.add(pictureField);
          }
  
          Bundle parameters = request.getParameters();
          parameters.putString("fields", TextUtils.join(",", fields));
          request.setParameters(parameters);
  
          return request;
      }
  
      private void setPlacePickerSettingsFromBundle(Bundle inState) {
          // We do this in a separate non-overridable method so it is safe to call from the constructor.
          if (inState != null) {
              setRadiusInMeters(inState.getInt(RADIUS_IN_METERS_BUNDLE_KEY, radiusInMeters));
              setResultsLimit(inState.getInt(RESULTS_LIMIT_BUNDLE_KEY, resultsLimit));
              if (inState.containsKey(SEARCH_TEXT_BUNDLE_KEY)) {
                  setSearchText(inState.getString(SEARCH_TEXT_BUNDLE_KEY));
              }
              if (inState.containsKey(LOCATION_BUNDLE_KEY)) {
                  Location location = inState.getParcelable(LOCATION_BUNDLE_KEY);
                  setLocation(location);
              }
              showSearchBox = inState.getBoolean(SHOW_SEARCH_BOX_BUNDLE_KEY, showSearchBox);
          }
      }
  
      private Timer createSearchTextTimer() {
          Timer timer = new Timer();
          timer.schedule(new TimerTask() {
              @Override
              public void run() {
                  onSearchTextTimerTriggered();
              }
          }, 0, searchTextTimerDelayInMilliseconds);
  
          return timer;
      }
  
      private void onSearchTextTimerTriggered() {
          if (hasSearchTextChangedSinceLastQuery) {
              Handler handler = new Handler(Looper.getMainLooper());
              handler.post(new Runnable() {
                  @Override
                  public void run() {
                      FacebookException error = null;
                      try {
                          loadData(true);
                      } catch (FacebookException fe) {
                          error = fe;
                      } catch (Exception e) {
                          error = new FacebookException(e);
                      } finally {
                          if (error != null) {
                              OnErrorListener onErrorListener = getOnErrorListener();
                              if (onErrorListener != null) {
                                  onErrorListener.onError(PlacePickerFragment.this, error);
                              } else {
                                  Logger.log(LoggingBehavior.REQUESTS, TAG, "Error loading data : %s", error);
                              }
                          }
                      }
                  }
              });
          } else {
              // Nothing has changed in 2 seconds. Invalidate and forget about this timer.
              // Next time the user types, we will fire a query immediately again.
              searchTextTimer.cancel();
              searchTextTimer = null;
          }
      }
  
      private class AsNeededLoadingStrategy extends LoadingStrategy {
          @Override
          public void attach(GraphObjectAdapter<GraphPlace> adapter) {
              super.attach(adapter);
  
              this.adapter.setDataNeededListener(new GraphObjectAdapter.DataNeededListener() {
                  @Override
                  public void onDataNeeded() {
                      // Do nothing if we are currently loading data . We will get notified again when that load finishes if the adapter still
                      // needs more data. Otherwise, follow the next link.
                      if (!loader.isLoading()) {
                          loader.followNextLink();
                      }
                  }
              });
          }
  
          @Override
          protected void onLoadFinished(GraphObjectPagingLoader<GraphPlace> loader,
                  SimpleGraphObjectCursor<GraphPlace> data) {
              super.onLoadFinished(loader, data);
  
              // We could be called in this state if we are clearing data or if we are being re-attached
              // in the middle of a query.
              if (data == null || loader.isLoading()) {
                  return;
              }
  
              hideActivityCircle();
  
              if (data.isFromCache()) {
                  // Only the first page can be cached, since all subsequent pages will be round-tripped. Force
                  // a refresh of the first page before we allow paging to begin. If the first page produced
                  // no data, launch the refresh immediately, otherwise schedule it for later.
                  loader.refreshOriginalRequest(data.areMoreObjectsAvailable() ? CACHED_RESULT_REFRESH_DELAY : 0);
              }
          }
      }
  
      private class SearchTextWatcher implements TextWatcher {
  
          @Override
          public void beforeTextChanged(CharSequence s, int start, int count, int after) {
          }
  
          @Override
          public void onTextChanged(CharSequence s, int start, int before, int count) {
              onSearchBoxTextChanged(s.toString(), false);
          }
  
          @Override
          public void afterTextChanged(Editable s) {
          }
      }
  }
  