/**
* Copyright 2009 Joe LaPenna
*/
package com.joelapenna.foursquared;
import com.joelapenna.foursquare.Foursquare;
import com.joelapenna.foursquare.error.FoursquareException;
import com.joelapenna.foursquare.types.Group;
import com.joelapenna.foursquare.types.Venue;
import com.joelapenna.foursquared.app.LoadableListActivity;
import com.joelapenna.foursquared.location.BestLocationListener;
import com.joelapenna.foursquared.location.LocationUtils;
import com.joelapenna.foursquared.preferences.Preferences;
import com.joelapenna.foursquared.util.MenuUtils;
import com.joelapenna.foursquared.util.NotificationsUtil;
import com.joelapenna.foursquared.util.UserUtils;
import com.joelapenna.foursquared.widget.SeparatedListAdapter;
import com.joelapenna.foursquared.widget.VenueListAdapter;
import android.app.Activity;
import android.app.SearchManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.AdapterView.OnItemClickListener;
import java.util.*;
/**
* @author Joe LaPenna (joe@joelapenna.com)
* @author Mark Wyszomierski (markww@gmail.com)
* -Refactored to allow NearbyVenuesMapActivity to list to search results.
*/
public class NearbyVenuesActivity extends LoadableListActivity {
static final String TAG = "NearbyVenuesActivity";
static final boolean DEBUG = FoursquaredSettings.DEBUG;
public static final String INTENT_EXTRA_STARTUP_GEOLOC_DELAY = Foursquared.PACKAGE_NAME
+ ".NearbyVenuesActivity.INTENT_EXTRA_STARTUP_GEOLOC_DELAY";
private static final int MENU_REFRESH = 0;
private static final int MENU_ADD_VENUE = 1;
private static final int MENU_SEARCH = 2;
private static final int MENU_MYINFO = 3;
private static final int MENU_MAP = 4;
private StateHolder mStateHolder = new StateHolder();
private SearchLocationObserver mSearchLocationObserver = new SearchLocationObserver();
public static SearchResultsObservable searchResultsObservable;
private ListView mListView;
private SeparatedListAdapter mListAdapter;
private LinearLayout mFooterView;
private TextView mTextViewFooter;
private Handler mHandler;
private BroadcastReceiver mLoggedOutReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG) Log.d(TAG, "onReceive: " + intent);
finish();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setDefaultKeyMode(Activity.DEFAULT_KEYS_SEARCH_LOCAL);
registerReceiver(mLoggedOutReceiver, new IntentFilter(Foursquared.INTENT_ACTION_LOGGED_OUT));
searchResultsObservable = new SearchResultsObservable();
mHandler = new Handler();
mListView = getListView();
mListAdapter = new SeparatedListAdapter(this);
mListView.setAdapter(mListAdapter);
mListView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Venue venue = (Venue) parent.getAdapter().getItem(position);
startItemActivity(venue);
}
});
// We can dynamically add a footer to our loadable listview.
LayoutInflater inflater = LayoutInflater.from(this);
mFooterView = (LinearLayout)inflater.inflate(R.layout.geo_loc_address_view, null);
mTextViewFooter = (TextView)mFooterView.findViewById(R.id.footerTextView);
LinearLayout llMain = (LinearLayout)findViewById(R.id.main);
llMain.addView(mFooterView);
// Check if we're returning from a configuration change.
if (getLastNonConfigurationInstance() != null) {
if (DEBUG) Log.d(TAG, "Restoring state.");
mStateHolder = (StateHolder) getLastNonConfigurationInstance();
mStateHolder.setActivity(this);
} else {
mStateHolder = new StateHolder();
mStateHolder.setQuery("");
}
// Start a new search if one is not running or we have no results.
if (mStateHolder.getIsRunningTask()) {
setProgressBarIndeterminateVisibility(true);
putSearchResultsInAdapter(mStateHolder.getResults());
ensureTitle(false);
} else if (mStateHolder.getResults().size() == 0) {
long firstLocDelay = 0L;
if (getIntent().getExtras() != null) {
firstLocDelay = getIntent().getLongExtra(INTENT_EXTRA_STARTUP_GEOLOC_DELAY, 0L);
}
startTask(firstLocDelay);
} else {
onTaskComplete(mStateHolder.getResults(), mStateHolder.getReverseGeoLoc(), null);
}
populateFooter(mStateHolder.getReverseGeoLoc());
}
@Override
public Object onRetainNonConfigurationInstance() {
mStateHolder.setActivity(null);
return mStateHolder;
}
@Override
public void onDestroy() {
super.onDestroy();
unregisterReceiver(mLoggedOutReceiver);
}
@Override
public void onResume() {
super.onResume();
if (DEBUG) Log.d(TAG, "onResume");
((Foursquared) getApplication()).requestLocationUpdates(mSearchLocationObserver);
}
@Override
public void onPause() {
super.onPause();
((Foursquared) getApplication()).removeLocationUpdates(mSearchLocationObserver);
if (isFinishing()) {
mStateHolder.cancelAllTasks();
mListAdapter.removeObserver();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
menu.add(Menu.NONE, MENU_REFRESH, Menu.NONE, R.string.refresh_label) //
.setIcon(R.drawable.ic_menu_refresh);
menu.add(Menu.NONE, MENU_SEARCH, Menu.NONE, R.string.search_label) //
.setIcon(R.drawable.ic_menu_search) //
.setAlphabeticShortcut(SearchManager.MENU_KEY);
menu.add(Menu.NONE, MENU_ADD_VENUE, Menu.NONE, R.string.nearby_menu_add_venue) //
.setIcon(R.drawable.ic_menu_add);
int sdk = new Integer(Build.VERSION.SDK).intValue();
if (sdk < 4) {
int menuIcon = UserUtils.getDrawableForMeMenuItemByGender(
((Foursquared) getApplication()).getUserGender());
menu.add(Menu.NONE, MENU_MYINFO, Menu.NONE, R.string.myinfo_label) //
.setIcon(menuIcon);
}
// Shows a map of all nearby venues, works but not going into this version.
//menu.add(Menu.NONE, MENU_MAP, Menu.NONE, "Map")
// .setIcon(R.drawable.ic_menu_places);
MenuUtils.addPreferencesToMenu(this, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_REFRESH:
if (mStateHolder.getIsRunningTask() == false) {
startTask();
}
return true;
case MENU_SEARCH:
Intent intent = new Intent(NearbyVenuesActivity.this, SearchVenuesActivity.class);
intent.setAction(Intent.ACTION_SEARCH);
startActivity(intent);
return true;
case MENU_ADD_VENUE:
startActivity(new Intent(NearbyVenuesActivity.this, AddVenueActivity.class));
return true;
case MENU_MYINFO:
Intent intentUser = new Intent(NearbyVenuesActivity.this, UserDetailsActivity.class);
intentUser.putExtra(UserDetailsActivity.EXTRA_USER_ID,
((Foursquared) getApplication()).getUserId());
startActivity(intentUser);
return true;
case MENU_MAP:
startActivity(new Intent(NearbyVenuesActivity.this, NearbyVenuesMapActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public int getNoSearchResultsStringId() {
return R.string.no_nearby_venues;
}
public void putSearchResultsInAdapter(Group<Group<Venue>> searchResults) {
if (DEBUG) Log.d(TAG, "putSearchResultsInAdapter");
mListAdapter.removeObserver();
mListAdapter = new SeparatedListAdapter(this);
if (searchResults != null && searchResults.size() > 0) {
int groupCount = searchResults.size();
for (int groupsIndex = 0; groupsIndex < groupCount; groupsIndex++) {
Group<Venue> group = searchResults.get(groupsIndex);
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
String[] venuesSortValues = getResources().getStringArray(R.array.venues_sort_values);
String venuesSort = settings.getString(Preferences.PREFERENCE_VENUES_SORT, venuesSortValues[0]);
if ( venuesSort.equals(venuesSortValues[1])) {
Collections.sort(group, new Comparator<Venue>() {
@Override
public int compare(Venue a, Venue b) {
int da = 0;
int db = 0;
if ( a.getDistance() != null ) {
try {
da = Integer.valueOf(a.getDistance());
} catch (NumberFormatException e) {
}
}
if ( b.getDistance() != null ) {
try {
db = Integer.valueOf(b.getDistance());
} catch (NumberFormatException e) {
}
}
return da - db;
}
});
}
if (group.size() > 0) {
VenueListAdapter groupAdapter = new VenueListAdapter(this,
((Foursquared) getApplication()).getRemoteResourceManager());
groupAdapter.setGroup(group);
if (DEBUG) Log.d(TAG, "Adding Section: " + group.getType());
mListAdapter.addSection(group.getType(), groupAdapter);
}
}
} else {
setEmptyView();
}
mListView.setAdapter(mListAdapter);
}
void startItemActivity(Venue venue) {
Intent intent = new Intent(NearbyVenuesActivity.this, VenueActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Foursquared.EXTRA_VENUE_ID, venue.getId());
startActivity(intent);
}
private void ensureTitle(boolean finished) {
if (finished) {
setTitle(getString(R.string.title_search_finished_noquery));
} else {
setTitle(getString(R.string.title_search_inprogress_noquery));
}
}
private void populateFooter(String reverseGeoLoc) {
mFooterView.setVisibility(View.VISIBLE);
mTextViewFooter.setText(reverseGeoLoc);
}
private long getClearGeolocationOnSearch() {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
boolean cacheGeolocation = settings.getBoolean(Preferences.PREFERENCE_CACHE_GEOLOCATION_FOR_SEARCHES, true);
if (cacheGeolocation) {
return 0L;
} else {
Foursquared foursquared = ((Foursquared) getApplication());
foursquared.clearLastKnownLocation();
foursquared.removeLocationUpdates(mSearchLocationObserver);
foursquared.requestLocationUpdates(mSearchLocationObserver);
return 2000L;
}
}
class SearchResultsObservable extends Observable {
@Override
public void notifyObservers(Object data) {
setChanged();
super.notifyObservers(data);
}
public Group<Group<Venue>> getSearchResults() {
return mStateHolder.getResults();
}
}
/** If location changes, auto-start a nearby venues search. */
private class SearchLocationObserver implements Observer {
private boolean mRequestedFirstSearch = false;
@Override
public void update(Observable observable, Object data) {
Location location = (Location) data;
// Fire a search if we haven't done so yet.
if (!mRequestedFirstSearch
&& ((BestLocationListener) observable).isAccurateEnough(location)) {
mRequestedFirstSearch = true;
if (mStateHolder.getIsRunningTask() == false) {
// Since we were told by the system that location has changed, no need to make the
// task wait before grabbing the current location.
mHandler.post(new Runnable() {
public void run() {
startTask(0L);
}
});
}
}
}
}
private void startTask() {
startTask(getClearGeolocationOnSearch());
}
private void startTask(long geoLocDelayTimeInMs) {
if (mStateHolder.getIsRunningTask() == false) {
setProgressBarIndeterminateVisibility(true);
ensureTitle(false);
if (mStateHolder.getResults().size() == 0) {
setLoadingView();
}
mStateHolder.startTask(this, mStateHolder.getQuery(), geoLocDelayTimeInMs);
}
}
private void onTaskComplete(Group<Group<Venue>> result, String reverseGeoLoc, Exception ex) {
if (result != null) {
mStateHolder.setResults(result);
mStateHolder.setReverseGeoLoc(reverseGeoLoc);
} else {
mStateHolder.setResults(new Group<Group<Venue>>());
NotificationsUtil.ToastReasonForFailure(NearbyVenuesActivity.this, ex);
}
populateFooter(mStateHolder.getReverseGeoLoc());
putSearchResultsInAdapter(mStateHolder.getResults());
setProgressBarIndeterminateVisibility(false);
ensureTitle(true);
searchResultsObservable.notifyObservers();
mStateHolder.cancelAllTasks();
}
/** Handles the work of finding nearby venues. */
private static class SearchTask extends AsyncTask<Void, Void, Group<Group<Venue>>> {
private NearbyVenuesActivity mActivity;
private Exception mReason = null;
private String mQuery;
private long mSleepTimeInMs;
private Foursquare mFoursquare;
private String mReverseGeoLoc; // Filled in after execution.
private String mNoLocException;
private String mLabelNearby;
public SearchTask(NearbyVenuesActivity activity, String query, long sleepTimeInMs) {
super();
mActivity = activity;
mQuery = query;
mSleepTimeInMs = sleepTimeInMs;
mFoursquare = ((Foursquared)activity.getApplication()).getFoursquare();
mNoLocException = activity.getResources().getString(R.string.nearby_venues_no_location);
mLabelNearby = activity.getResources().getString(R.string.nearby_venues_label_nearby);
}
@Override
public void onPreExecute() {
}
@Override
public Group<Group<Venue>> doInBackground(Void... params) {
try {
// If the user has chosen to clear their geolocation on each search, wait briefly
// for a new fix to come in. The two-second wait time is arbitrary and can be
// changed to something more intelligent.
if (mSleepTimeInMs > 0L) {
Thread.sleep(mSleepTimeInMs);
}
// Get last known location.
Location location = ((Foursquared) mActivity.getApplication()).getLastKnownLocation();
if (location == null) {
throw new FoursquareException(mNoLocException);
}
// Get the venues.
Group<Group<Venue>> results = mFoursquare.venues(LocationUtils
.createFoursquareLocation(location), mQuery, 30);
// Try to get our reverse geolocation.
mReverseGeoLoc = getGeocode(mActivity, location);
return results;
} catch (Exception e) {
mReason = e;
}
return null;
}
@Override
public void onPostExecute(Group<Group<Venue>> groups) {
if (mActivity != null) {
mActivity.onTaskComplete(groups, mReverseGeoLoc, mReason);
}
}
private String getGeocode(Context context, Location location) {
Geocoder geocoded = new Geocoder(context, Locale.getDefault());
try {
List<Address> addresses = geocoded.getFromLocation(
location.getLatitude(), location.getLongitude(), 1);
if (addresses.size() > 0) {
Address address = addresses.get(0);
StringBuilder sb = new StringBuilder(128);
sb.append(mLabelNearby);
sb.append(" ");
sb.append(address.getAddressLine(0));
if (addresses.size() > 1) {
sb.append(", ");
sb.append(address.getAddressLine(1));
}
if (addresses.size() > 2) {
sb.append(", ");
sb.append(address.getAddressLine(2));
}
if (!TextUtils.isEmpty(address.getLocality())) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(address.getLocality());
}
return sb.toString();
}
} catch (Exception ex) {
if (DEBUG) Log.d(TAG, "SearchTask: error geocoding current location.", ex);
}
return null;
}
public void setActivity(NearbyVenuesActivity activity) {
mActivity = activity;
}
}
private static class StateHolder {
private Group<Group<Venue>> mResults;
private String mQuery;
private String mReverseGeoLoc;
private SearchTask mSearchTask;
public StateHolder() {
mResults = new Group<Group<Venue>>();
mSearchTask = null;
}
public String getQuery() {
return mQuery;
}
public void setQuery(String query) {
mQuery = query;
}
public String getReverseGeoLoc() {
return mReverseGeoLoc;
}
public void setReverseGeoLoc(String reverseGeoLoc) {
mReverseGeoLoc = reverseGeoLoc;
}
public Group<Group<Venue>> getResults() {
return mResults;
}
public void setResults(Group<Group<Venue>> results) {
mResults = results;
}
public void startTask(NearbyVenuesActivity activity, String query, long sleepTimeInMs) {
mSearchTask = new SearchTask(activity, query, sleepTimeInMs);
mSearchTask.execute();
}
public boolean getIsRunningTask() {
return mSearchTask != null;
}
public void cancelAllTasks() {
if (mSearchTask != null) {
mSearchTask.cancel(true);
mSearchTask = null;
}
}
public void setActivity(NearbyVenuesActivity activity) {
if (mSearchTask != null) {
mSearchTask.setActivity(activity);
}
}
}
}