/**
* Copyright 2009 Joe LaPenna
*/
package com.joelapenna.foursquared;
import com.joelapenna.foursquare.error.FoursquareException;
import com.joelapenna.foursquare.types.Checkin;
import com.joelapenna.foursquare.types.Group;
import com.joelapenna.foursquared.app.LoadableListActivityWithViewAndHeader;
import com.joelapenna.foursquared.location.LocationUtils;
import com.joelapenna.foursquared.util.CheckinTimestampSort;
import com.joelapenna.foursquared.util.Comparators;
import com.joelapenna.foursquared.util.MenuUtils;
import com.joelapenna.foursquared.util.NotificationsUtil;
import com.joelapenna.foursquared.util.UiUtil;
import com.joelapenna.foursquared.widget.CheckinListAdapter;
import com.joelapenna.foursquared.widget.SegmentedButton;
import com.joelapenna.foursquared.widget.SegmentedButton.OnClickListenerSegmentedButton;
import com.joelapenna.foursquared.widget.SeparatedListAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.location.Location;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ScrollView;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
/**
* @author Joe LaPenna (joe@joelapenna.com)
* @author Mark Wyszomierski (markww@gmail.com)
* -Added dummy location observer, new menu icon logic,
* links to new user activity (3/10/2010).
* -Sorting checkins by distance/time. (3/18/2010).
* -Added option to sort by server response, or by distance. (6/10/2010).
* -Reformatted/refactored. (9/22/2010).
*/
public class FriendsActivity extends LoadableListActivityWithViewAndHeader {
static final String TAG = "FriendsActivity";
static final boolean DEBUG = FoursquaredSettings.DEBUG;
public static final int CITY_RADIUS_IN_METERS = 20 * 1000; // 20km
private static final long SLEEP_TIME_IF_NO_LOCATION = 3000L;
private static final int MENU_GROUP_SEARCH = 0;
private static final int MENU_REFRESH = 1;
private static final int MENU_SHOUT = 2;
private static final int MENU_MORE = 3;
private static final int MENU_MORE_MAP = 20;
private static final int MENU_MORE_LEADERBOARD = 21;
private static final int SORT_METHOD_RECENT = 0;
private static final int SORT_METHOD_NEARBY = 1;
private StateHolder mStateHolder;
private SearchLocationObserver mSearchLocationObserver = new SearchLocationObserver();
private LinkedHashMap<Integer, String> mMenuMoreSubitems;
private SeparatedListAdapter mListAdapter;
private ViewGroup mLayoutEmpty;
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);
registerReceiver(mLoggedOutReceiver, new IntentFilter(Foursquared.INTENT_ACTION_LOGGED_OUT));
if (getLastNonConfigurationInstance() != null) {
mStateHolder = (StateHolder) getLastNonConfigurationInstance();
mStateHolder.setActivity(this);
} else {
mStateHolder = new StateHolder();
mStateHolder.setSortMethod(SORT_METHOD_RECENT);
}
ensureUi();
Foursquared foursquared = (Foursquared)getApplication();
if (foursquared.isReady()) {
if (!mStateHolder.getRanOnce()) {
mStateHolder.startTask(this);
}
}
}
@Override
public void onResume() {
super.onResume();
((Foursquared) getApplication()).requestLocationUpdates(mSearchLocationObserver);
}
@Override
public void onPause() {
super.onPause();
((Foursquared) getApplication()).removeLocationUpdates(mSearchLocationObserver);
if (isFinishing()) {
mListAdapter.removeObserver();
unregisterReceiver(mLoggedOutReceiver);
mStateHolder.cancel();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
menu.add(MENU_GROUP_SEARCH, MENU_REFRESH, Menu.NONE, R.string.refresh)
.setIcon(R.drawable.ic_menu_refresh);
menu.add(Menu.NONE, MENU_SHOUT, Menu.NONE, R.string.shout_action_label)
.setIcon(R.drawable.ic_menu_shout);
SubMenu menuMore = menu.addSubMenu(Menu.NONE, MENU_MORE, Menu.NONE, "More");
menuMore.setIcon(android.R.drawable.ic_menu_more);
for (Map.Entry<Integer, String> it : mMenuMoreSubitems.entrySet()) {
menuMore.add(it.getValue());
}
MenuUtils.addPreferencesToMenu(this, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_REFRESH:
mStateHolder.startTask(this);
return true;
case MENU_SHOUT:
Intent intent = new Intent(this, CheckinOrShoutGatherInfoActivity.class);
intent.putExtra(CheckinOrShoutGatherInfoActivity.INTENT_EXTRA_IS_SHOUT, true);
startActivity(intent);
return true;
case MENU_MORE:
// Submenu items generate id zero, but we check on item title below.
return true;
default:
if (item.getTitle().equals("Map")) {
Checkin[] checkins = (Checkin[])mStateHolder.getCheckins().toArray(
new Checkin[mStateHolder.getCheckins().size()]);
Intent intentMap = new Intent(FriendsActivity.this, FriendsMapActivity.class);
intentMap.putExtra(FriendsMapActivity.EXTRA_CHECKIN_PARCELS, checkins);
startActivity(intentMap);
return true;
} else if (item.getTitle().equals(mMenuMoreSubitems.get(MENU_MORE_LEADERBOARD))) {
startActivity(new Intent(FriendsActivity.this, StatsActivity.class));
return true;
}
break;
}
return super.onOptionsItemSelected(item);
}
@Override
public Object onRetainNonConfigurationInstance() {
mStateHolder.setActivity(null);
return mStateHolder;
}
@Override
public int getNoSearchResultsStringId() {
return R.string.no_friend_checkins;
}
private void ensureUi() {
SegmentedButton buttons = getHeaderButton();
buttons.clearButtons();
buttons.addButtons(
getString(R.string.friendsactivity_btn_recent),
getString(R.string.friendsactivity_btn_nearby));
if (mStateHolder.getSortMethod() == SORT_METHOD_RECENT) {
buttons.setPushedButtonIndex(0);
} else {
buttons.setPushedButtonIndex(1);
}
buttons.setOnClickListener(new OnClickListenerSegmentedButton() {
@Override
public void onClick(int index) {
if (index == 0) {
mStateHolder.setSortMethod(SORT_METHOD_RECENT);
} else {
mStateHolder.setSortMethod(SORT_METHOD_NEARBY);
}
ensureUiListView();
}
});
mMenuMoreSubitems = new LinkedHashMap<Integer, String>();
mMenuMoreSubitems.put(MENU_MORE_MAP, getResources().getString(
R.string.friendsactivity_menu_map));
mMenuMoreSubitems.put(MENU_MORE_LEADERBOARD, getResources().getString(
R.string.friendsactivity_menu_leaderboard));
ensureUiListView();
}
private void ensureUiListView() {
mListAdapter = new SeparatedListAdapter(this);
if (mStateHolder.getSortMethod() == SORT_METHOD_RECENT) {
sortCheckinsRecent(mStateHolder.getCheckins(), mListAdapter);
} else {
sortCheckinsDistance(mStateHolder.getCheckins(), mListAdapter);
}
ListView listView = getListView();
listView.setAdapter(mListAdapter);
listView.setDividerHeight(0);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Checkin checkin = (Checkin) parent.getAdapter().getItem(position);
if (checkin.getUser() != null) {
Intent intent = new Intent(FriendsActivity.this, UserDetailsActivity.class);
intent.putExtra(UserDetailsActivity.EXTRA_USER_PARCEL, checkin.getUser());
intent.putExtra(UserDetailsActivity.EXTRA_SHOW_ADD_FRIEND_OPTIONS, true);
startActivity(intent);
}
}
});
// Prepare our no-results view. Something odd is going on with the layout parameters though.
// If we don't explicitly set the layout to be fill/fill after inflating, the layout jumps
// to a wrap/wrap layout. Furthermore, sdk 3 crashes with the original layout using two
// buttons in a horizontal LinearLayout.
LayoutInflater inflater = LayoutInflater.from(this);
if (UiUtil.sdkVersion() > 3) {
mLayoutEmpty = (ScrollView)inflater.inflate(
R.layout.friends_activity_empty, null);
Button btnAddFriends = (Button)mLayoutEmpty.findViewById(
R.id.friendsActivityEmptyBtnAddFriends);
btnAddFriends.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FriendsActivity.this, AddFriendsActivity.class);
startActivity(intent);
}
});
Button btnFriendRequests = (Button)mLayoutEmpty.findViewById(
R.id.friendsActivityEmptyBtnFriendRequests);
btnFriendRequests.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FriendsActivity.this, FriendRequestsActivity.class);
startActivity(intent);
}
});
} else {
// Inflation on 1.5 is causing a lot of issues, dropping full layout.
mLayoutEmpty = (ScrollView)inflater.inflate(
R.layout.friends_activity_empty_sdk3, null);
}
mLayoutEmpty.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.FILL_PARENT));
if (mListAdapter.getCount() == 0) {
setEmptyView(mLayoutEmpty);
}
if (mStateHolder.getIsRunningTask()) {
setProgressBarIndeterminateVisibility(true);
if (!mStateHolder.getRanOnce()) {
setLoadingView();
}
} else {
setProgressBarIndeterminateVisibility(false);
}
}
private void sortCheckinsRecent(Group<Checkin> checkins, SeparatedListAdapter listAdapter) {
// Sort all by timestamp first.
Collections.sort(checkins, Comparators.getCheckinRecencyComparator());
// We'll group in different section adapters based on some time thresholds.
Group<Checkin> recent = new Group<Checkin>();
Group<Checkin> today = new Group<Checkin>();
Group<Checkin> yesterday = new Group<Checkin>();
Group<Checkin> older = new Group<Checkin>();
Group<Checkin> other = new Group<Checkin>();
CheckinTimestampSort timestamps = new CheckinTimestampSort();
for (Checkin it : checkins) {
// If we can't parse the distance value, it's possible that we
// did not have a geolocation for the device at the time the
// search was run. In this case just assume this friend is nearby
// to sort them in the time buckets.
int meters = 0;
try {
meters = Integer.parseInt(it.getDistance());
} catch (NumberFormatException ex) {
if (DEBUG) Log.d(TAG, "Couldn't parse distance for checkin during friend search.");
meters = 0;
}
if (meters > CITY_RADIUS_IN_METERS) {
other.add(it);
} else {
try {
Date date = new Date(it.getCreated());
if (date.after(timestamps.getBoundaryRecent())) {
recent.add(it);
} else if (date.after(timestamps.getBoundaryToday())) {
today.add(it);
} else if (date.after(timestamps.getBoundaryYesterday())) {
yesterday.add(it);
} else {
older.add(it);
}
} catch (Exception ex) {
older.add(it);
}
}
}
if (recent.size() > 0) {
CheckinListAdapter adapter = new CheckinListAdapter(this,
((Foursquared) getApplication()).getRemoteResourceManager());
adapter.setGroup(recent);
listAdapter.addSection(getResources().getString(
R.string.friendsactivity_title_sort_recent), adapter);
}
if (today.size() > 0) {
CheckinListAdapter adapter = new CheckinListAdapter(this,
((Foursquared) getApplication()).getRemoteResourceManager());
adapter.setGroup(today);
listAdapter.addSection(getResources().getString(
R.string.friendsactivity_title_sort_today), adapter);
}
if (yesterday.size() > 0) {
CheckinListAdapter adapter = new CheckinListAdapter(this,
((Foursquared) getApplication()).getRemoteResourceManager());
adapter.setGroup(yesterday);
listAdapter.addSection(getResources().getString(
R.string.friendsactivity_title_sort_yesterday), adapter);
}
if (older.size() > 0) {
CheckinListAdapter adapter = new CheckinListAdapter(this,
((Foursquared) getApplication()).getRemoteResourceManager());
adapter.setGroup(older);
listAdapter.addSection(getResources().getString(
R.string.friendsactivity_title_sort_older), adapter);
}
if (other.size() > 0) {
CheckinListAdapter adapter = new CheckinListAdapter(this,
((Foursquared) getApplication()).getRemoteResourceManager());
adapter.setGroup(other);
listAdapter.addSection(getResources().getString(
R.string.friendsactivity_title_sort_other_city), adapter);
}
}
private void sortCheckinsDistance(Group<Checkin> checkins, SeparatedListAdapter listAdapter) {
Collections.sort(checkins, Comparators.getCheckinDistanceComparator());
Group<Checkin> nearby = new Group<Checkin>();
CheckinListAdapter adapter = new CheckinListAdapter(this,
((Foursquared) getApplication()).getRemoteResourceManager());
for (Checkin it : checkins) {
int meters = 0;
try {
meters = Integer.parseInt(it.getDistance());
} catch (NumberFormatException ex) {
if (DEBUG) Log.d(TAG, "Couldn't parse distance for checkin during friend search.");
meters = 0;
}
if (meters < CITY_RADIUS_IN_METERS) {
nearby.add(it);
}
}
if (nearby.size() > 0) {
adapter.setGroup(nearby);
listAdapter.addSection(getResources().getString(
R.string.friendsactivity_title_sort_distance), adapter);
}
}
private void onTaskStart() {
setProgressBarIndeterminateVisibility(true);
setLoadingView();
}
private void onTaskComplete(Group<Checkin> checkins, Exception ex) {
mStateHolder.setRanOnce(true);
mStateHolder.setIsRunningTask(false);
setProgressBarIndeterminateVisibility(false);
// Clear list for new batch.
mListAdapter.removeObserver();
mListAdapter.clear();
mListAdapter = new SeparatedListAdapter(this);
// User can sort by default (which is by checkin time), or just by distance.
if (checkins != null) {
mStateHolder.setCheckins(checkins);
if (mStateHolder.getSortMethod() == SORT_METHOD_RECENT) {
sortCheckinsRecent(checkins, mListAdapter);
} else {
sortCheckinsDistance(checkins, mListAdapter);
}
} else if (ex != null) {
mStateHolder.setCheckins(new Group<Checkin>());
NotificationsUtil.ToastReasonForFailure(this, ex);
}
if (mStateHolder.getCheckins().size() == 0) {
setEmptyView(mLayoutEmpty);
}
getListView().setAdapter(mListAdapter);
}
private static class TaskCheckins extends AsyncTask<Void, Void, Group<Checkin>> {
private Foursquared mFoursquared;
private FriendsActivity mActivity;
private Exception mException;
public TaskCheckins(FriendsActivity activity) {
mFoursquared = ((Foursquared) activity.getApplication());
mActivity = activity;
}
public void setActivity(FriendsActivity activity) {
mActivity = activity;
}
@Override
public Group<Checkin> doInBackground(Void... params) {
Group<Checkin> checkins = null;
try {
checkins = checkins();
} catch (Exception ex) {
mException = ex;
}
return checkins;
}
@Override
protected void onPreExecute() {
mActivity.onTaskStart();
}
@Override
public void onPostExecute(Group<Checkin> checkins) {
if (mActivity != null) {
mActivity.onTaskComplete(checkins, mException);
}
}
private Group<Checkin> checkins() throws FoursquareException, IOException {
// If we're the startup tab, it's likely that we won't have a geo location
// immediately. For now we can use this ugly method of sleeping for N
// seconds to at least let network location get a lock. We're only trying
// to discern between same-city, so we can even use LocationManager's
// getLastKnownLocation() method because we don't care if we're even a few
// miles off. The api endpoint doesn't require location, so still go ahead
// even if we can't find a location.
Location loc = mFoursquared.getLastKnownLocation();
if (loc == null) {
try { Thread.sleep(SLEEP_TIME_IF_NO_LOCATION); } catch (InterruptedException ex) {}
loc = mFoursquared.getLastKnownLocation();
}
Group<Checkin> checkins = mFoursquared.getFoursquare().checkins(LocationUtils
.createFoursquareLocation(loc));
Collections.sort(checkins, Comparators.getCheckinRecencyComparator());
return checkins;
}
}
private static class StateHolder {
private Group<Checkin> mCheckins;
private int mSortMethod;
private boolean mRanOnce;
private boolean mIsRunningTask;
private TaskCheckins mTaskCheckins;
public StateHolder() {
mRanOnce = false;
mIsRunningTask = false;
mCheckins = new Group<Checkin>();
}
public int getSortMethod() {
return mSortMethod;
}
public void setSortMethod(int sortMethod) {
mSortMethod = sortMethod;
}
public Group<Checkin> getCheckins() {
return mCheckins;
}
public void setCheckins(Group<Checkin> checkins) {
mCheckins = checkins;
}
public boolean getRanOnce() {
return mRanOnce;
}
public void setRanOnce(boolean ranOnce) {
mRanOnce = ranOnce;
}
public boolean getIsRunningTask() {
return mIsRunningTask;
}
public void setIsRunningTask(boolean isRunning) {
mIsRunningTask = isRunning;
}
public void setActivity(FriendsActivity activity) {
if (mIsRunningTask) {
mTaskCheckins.setActivity(activity);
}
}
public void startTask(FriendsActivity activity) {
if (!mIsRunningTask) {
mTaskCheckins = new TaskCheckins(activity);
mTaskCheckins.execute();
mIsRunningTask = true;
}
}
public void cancel() {
if (mIsRunningTask) {
mTaskCheckins.cancel(true);
mIsRunningTask = false;
}
}
}
/**
* This is really just a dummy observer to get the GPS running
* since this is the new splash page. After getting a fix, we
* might want to stop registering this observer thereafter so
* it doesn't annoy the user too much.
*/
private class SearchLocationObserver implements Observer {
@Override
public void update(Observable observable, Object data) {
}
}
}