/** * Copyright 2009 Joe LaPenna */ package com.joelapenna.foursquared; import com.joelapenna.foursquare.Foursquare; import com.joelapenna.foursquare.error.FoursquareException; import com.joelapenna.foursquare.types.Checkin; import com.joelapenna.foursquare.types.Group; import com.joelapenna.foursquared.app.LoadableListActivityWithView; import com.joelapenna.foursquared.location.LocationUtils; import com.joelapenna.foursquared.preferences.Preferences; import com.joelapenna.foursquared.util.*; import com.joelapenna.foursquared.widget.CheckinListAdapter; import com.joelapenna.foursquared.widget.SeparatedListAdapter; import android.app.AlertDialog; import android.app.Dialog; import android.app.SearchManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.location.Location; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; 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.ViewGroup; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.ScrollView; import android.widget.Toast; import android.widget.AdapterView.OnItemClickListener; 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). */ public class FriendsActivity extends LoadableListActivityWithView { static final String TAG = "FriendsActivity"; static final boolean DEBUG = FoursquaredSettings.DEBUG; public static final String QUERY_NEARBY = null; 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_MYINFO = 4; private static final int MENU_MORE_SORT_METHOD = 20; private static final int MENU_MORE_MAP = 21; private static final int MENU_MORE_LEADERBOARD = 22; private static final int MENU_MORE_ADD_FRIENDS = 23; private static final int MENU_MORE_FRIEND_REQUESTS = 24; private static final int SORT_METHOD_DEFAULT = 0; private static final int SORT_METHOD_DISTANCE = 1; private static final int DIALOG_SORT_METHOD = 20; private SearchTask mSearchTask; private SearchHolder mSearchHolder = new SearchHolder(); private SearchLocationObserver mSearchLocationObserver = new SearchLocationObserver(); public static SearchResultsObservable searchResultsObservable; private ViewGroup mLayoutEmpty; private LinkedHashMap<Integer, String> mMenuMoreSubitems; private SeparatedListAdapter mListAdapter; 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)); searchResultsObservable = new SearchResultsObservable(); initListViewAdapter(); if (getLastNonConfigurationInstance() != null) { if (DEBUG) Log.d(TAG, "Restoring state."); SearchHolder holder = (SearchHolder) getLastNonConfigurationInstance(); if (holder.results == null) { executeSearchTask(holder.query); } else { mSearchHolder.query = holder.query; setSearchResults(holder.results); putSearchResultsInAdapter(holder.results, holder.sortMethod); if (holder.results.size() < 1) { setEmptyView(mLayoutEmpty); } } } else { onNewIntent(getIntent()); } mMenuMoreSubitems = new LinkedHashMap<Integer, String>(); mMenuMoreSubitems.put(MENU_MORE_SORT_METHOD, getResources().getString( R.string.friendsactivity_menu_sort_method)); mMenuMoreSubitems.put(MENU_MORE_MAP, getResources().getString( R.string.friendsactivity_menu_map)); mMenuMoreSubitems.put(MENU_MORE_LEADERBOARD, getResources().getString( R.string.friendsactivity_menu_leaderboard)); mMenuMoreSubitems.put(MENU_MORE_ADD_FRIENDS, getResources().getString( R.string.friendsactivity_menu_add_friends)); mMenuMoreSubitems.put(MENU_MORE_FRIEND_REQUESTS, getResources().getString( R.string.friendsactivity_menu_friend_requests)); } @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); } } @Override public void onStop() { super.onStop(); if (mSearchTask != null) { mSearchTask.cancel(true); } } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); menu.add(MENU_GROUP_SEARCH, MENU_REFRESH, Menu.NONE, R.string.refresh_label) // .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); 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); } 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: executeSearchTask(null); 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_MYINFO: Intent intentUser = new Intent(FriendsActivity.this, UserDetailsActivity.class); intentUser.putExtra(UserDetailsActivity.EXTRA_USER_ID, ((Foursquared) getApplication()).getUserId()); startActivity(intentUser); return true; case MENU_MORE: // Submenu items generate id zero, but we check on item title below. return true; default: if (item.getTitle().equals(mMenuMoreSubitems.get(MENU_MORE_SORT_METHOD))) { showDialog(DIALOG_SORT_METHOD); return true; } else if (item.getTitle().equals("Map")) { startActivity(new Intent(FriendsActivity.this, FriendsMapActivity.class)); return true; } else if (item.getTitle().equals(mMenuMoreSubitems.get(MENU_MORE_LEADERBOARD))) { startActivity(new Intent(FriendsActivity.this, StatsActivity.class)); return true; } else if (item.getTitle().equals(mMenuMoreSubitems.get(MENU_MORE_ADD_FRIENDS))) { startActivity(new Intent(FriendsActivity.this, AddFriendsActivity.class)); return true; } else if (item.getTitle().equals(mMenuMoreSubitems.get(MENU_MORE_FRIEND_REQUESTS))) { startActivity(new Intent(FriendsActivity.this, FriendRequestsActivity.class)); return true; } break; } return super.onOptionsItemSelected(item); } @Override public void onNewIntent(Intent intent) { if (DEBUG) Log.d(TAG, "New Intent: " + intent); if (intent == null) { if (DEBUG) Log.d(TAG, "No intent to search, querying default."); } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) { if (DEBUG) Log.d(TAG, "onNewIntent received search intent and saving."); } executeSearchTask(intent.getStringExtra(SearchManager.QUERY)); } @Override public Object onRetainNonConfigurationInstance() { return mSearchHolder; } @Override protected Dialog onCreateDialog(int id) { switch (id) { case DIALOG_SORT_METHOD: ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1); adapter.add(getResources().getString(R.string.friendsactivity_menu_sort_time)); adapter.add(getResources().getString(R.string.friendsactivity_menu_sort_distance)); AlertDialog dlgSortMethod = new AlertDialog.Builder(this) .setTitle(getResources().getString(R.string.friendsactivity_menu_sort_method)) .setIcon(0) .setAdapter(adapter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { switch (which) { case 0: mSearchHolder.sortMethod = SORT_METHOD_DEFAULT; putSearchResultsInAdapter(mSearchHolder.results, mSearchHolder.sortMethod); break; case 1: mSearchHolder.sortMethod = SORT_METHOD_DISTANCE; putSearchResultsInAdapter(mSearchHolder.results, mSearchHolder.sortMethod); break; } } }) .create(); return dlgSortMethod; } return null; } @Override public int getNoSearchResultsStringId() { return R.string.no_friend_checkins; } private void initListViewAdapter() { mListAdapter = new SeparatedListAdapter(this); ListView listView = getListView(); listView.setAdapter(mListAdapter); 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); } } }); listView.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View arg0) { return false; } }); // 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. int sdk = new Integer(Build.VERSION.SDK).intValue(); if (sdk > 3) { mLayoutEmpty = (ScrollView)LayoutInflater.from(this).inflate( R.layout.friends_activity_empty, null); } else { mLayoutEmpty = (ScrollView)LayoutInflater.from(this).inflate( R.layout.friends_activity_empty_sdk3, null); } mLayoutEmpty.setLayoutParams(new LinearLayout.LayoutParams( LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.FILL_PARENT)); 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); } }); } private void setSearchResults(Group<Checkin> searchResults) { if (DEBUG) Log.d(TAG, "Setting search results."); mSearchHolder.results = searchResults; searchResultsObservable.notifyObservers(); } private void executeSearchTask(String query) { if (DEBUG) Log.d(TAG, "sendQuery()"); mSearchHolder.query = query; // not going through set* because we don't want to notify search result // observers. mSearchHolder.results = null; // If a task is already running, don't start a new one. if (mSearchTask != null && mSearchTask.getStatus() != AsyncTask.Status.FINISHED) { if (DEBUG) Log.d(TAG, "Query already running attempting to cancel: " + mSearchTask); if (!mSearchTask.cancel(true) && !mSearchTask.isCancelled()) { if (DEBUG) Log.d(TAG, "Unable to cancel search? Notifying the user."); Toast.makeText(this, getResources().getString(R.string.friendsactivity_search_in_progress), Toast.LENGTH_SHORT); return; } } mSearchTask = (SearchTask) new SearchTask().execute(); } private void ensureTitle(boolean finished) { if (finished) { setTitle(R.string.friendsactivity_title_finished); } else { setTitle(R.string.friendsactivity_title_searching); } } private class SearchTask extends AsyncTask<Void, Void, Group<Checkin>> { private Exception mReason = null; @Override public void onPreExecute() { if (DEBUG) Log.d(TAG, "SearchTask: onPreExecute()"); setProgressBarIndeterminateVisibility(true); ensureTitle(false); setLoadingView(); } @Override public Group<Checkin> doInBackground(Void... params) { try { return search(); } catch (Exception e) { mReason = e; } return null; } @Override public void onPostExecute(Group<Checkin> checkins) { try { if (checkins == null) { NotificationsUtil.ToastReasonForFailure(FriendsActivity.this, mReason); } else { boolean syncPref = PreferenceManager.getDefaultSharedPreferences(FriendsActivity.this).getBoolean(Preferences.PREFERENCE_SYNC_CONTACTS, false); Log.i(TAG, "sync preference is " + syncPref); if ( syncPref ) { Log.i(TAG, "starting task to sync contacts"); Foursquared.get(FriendsActivity.this).getSync().createSyncTask().execute(); } } setSearchResults(checkins); putSearchResultsInAdapter(checkins, mSearchHolder.sortMethod); } finally { setProgressBarIndeterminateVisibility(false); ensureTitle(true); if (checkins == null || checkins.size() < 1) { setEmptyView(mLayoutEmpty); } } } Group<Checkin> search() throws FoursquareException, IOException { Foursquare foursquare = ((Foursquared) getApplication()).getFoursquare(); // 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 three // 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. Location loc = ((Foursquared) getApplication()) .getLastKnownLocation(); if (loc == null) { try { Thread.sleep(SLEEP_TIME_IF_NO_LOCATION); } catch (InterruptedException ex) {} loc = ((Foursquared) getApplication()) .getLastKnownLocation(); } Group<Checkin> checkins = foursquare.checkins(LocationUtils .createFoursquareLocation(loc)); Collections.sort(checkins, Comparators.getCheckinRecencyComparator()); return checkins; } } private void sortCheckinsDefault(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); } } } RemoteResourceManager rRm = ((Foursquared)getApplication()).getRemoteResourceManager(); Sync sync = ((Foursquared)getApplication()).getSync(); if (recent.size() > 0) { CheckinListAdapter adapter = new CheckinListAdapter(this, rRm, sync); adapter.setGroup(recent); listAdapter.addSection(getResources().getString( R.string.friendsactivity_title_sort_recent), adapter); } if (today.size() > 0) { CheckinListAdapter adapter = new CheckinListAdapter(this, rRm, sync); adapter.setGroup(today); listAdapter.addSection(getResources().getString( R.string.friendsactivity_title_sort_today), adapter); } if (yesterday.size() > 0) { CheckinListAdapter adapter = new CheckinListAdapter(this, rRm, sync); adapter.setGroup(yesterday); listAdapter.addSection(getResources().getString( R.string.friendsactivity_title_sort_yesterday), adapter); } if (older.size() > 0) { CheckinListAdapter adapter = new CheckinListAdapter(this, rRm, sync); adapter.setGroup(older); listAdapter.addSection(getResources().getString( R.string.friendsactivity_title_sort_older), adapter); } if (other.size() > 0) { CheckinListAdapter adapter = new CheckinListAdapter(this, rRm, sync); 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(), ((Foursquared)getApplication()).getSync()); 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); } } adapter.setGroup(nearby); listAdapter.addSection(getResources().getString( R.string.friendsactivity_title_sort_distance), adapter); } /** * Sort checkin results first by distance [same city | different city], * then within the [same city] bucket, sort by last three hours, today, * and yesterday. If we had no geoloation at the time of the search, we * won't have any distance parameter to do the first level of sorting, * in this case we just place all our friends in the [same city] bucket. */ private void putSearchResultsInAdapter(Group<Checkin> checkins, int sortMethod) { // 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 && checkins.size() > 0) { if (sortMethod == SORT_METHOD_DISTANCE) { sortCheckinsDistance(checkins, mListAdapter); } else { sortCheckinsDefault(checkins, mListAdapter); } } getListView().setAdapter(mListAdapter); } private static class SearchHolder { Group<Checkin> results; String query; int sortMethod; public SearchHolder() { sortMethod = SORT_METHOD_DEFAULT; } } class SearchResultsObservable extends Observable { @Override public void notifyObservers(Object data) { setChanged(); super.notifyObservers(data); } public Group<Checkin> getSearchResults() { return mSearchHolder.results; } public String getQuery() { return mSearchHolder.query; } }; /** * 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) { } } }