package com.charlesmadere.android.classygames;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.app.SherlockListFragment;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
import com.actionbarsherlock.widget.SearchView;
import com.charlesmadere.android.classygames.models.ListItem;
import com.charlesmadere.android.classygames.models.Person;
import com.charlesmadere.android.classygames.utilities.FacebookUtilities;
import com.charlesmadere.android.classygames.utilities.Utilities;
import com.facebook.Request;
import com.facebook.Request.GraphUserListCallback;
import com.facebook.Response;
import com.facebook.Session;
import com.facebook.model.GraphUser;
import java.util.*;
public final class FriendsListFragment extends SherlockListFragment implements
AdapterView.OnItemClickListener
{
private final static String PREFERENCES_NAME = "FriendsListFragment_Preferences";
private ListView list;
private TextView empty;
private LinearLayout loading;
private TextView noInternetConnection;
/**
* Boolean that marks if this is the first time that the onResume() method
* was hit. We do this because we don't want to refresh the friends list
* if it is not in need of refreshing.
* In other words, it's not in need of a refreshment. It's not thirsty. Har
* har.
*/
private boolean isFirstOnResume = true;
/**
* Holds a handle to the currently running (if it's currently running)
* AsyncRefreshFriendsList AsyncTask.
*/
private AsyncRefreshFriendsList asyncRefreshFriendsList;
/**
* If the user has selected a friend in their friends list, then this will
* be a handle to that Friend object. If no friend is currently selected,
* then this will be null.
*/
private ListItem<Person> selectedFriend;
private SharedPreferences sPreferences;
/**
* Object that allows us to run any of the methods that are defined in the
* Listeners interface.
*/
private Listeners listeners;
/**
* A bunch of listener methods for this Fragment.
*/
public interface Listeners
{
/**
* This is fired whenever the user has selected a friend in their
* friends list.
*
* @param friend
* The Facebook friend that the user selected.
*/
public void onFriendSelected(final Person friend);
/**
* This is fired whenever the user has selected the Refresh button on
* the action bar.
*/
public void onRefreshSelected();
}
@Override
public void onCreate(final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState)
{
return inflater.inflate(R.layout.friends_list_fragment, container, false);
}
@Override
public void onActivityCreated(final Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
final View view = getView();
list = getListView();
list.setOnItemClickListener(this);
empty = (TextView) view.findViewById(android.R.id.empty);
loading = (LinearLayout) view.findViewById(R.id.friends_list_fragment_loading);
noInternetConnection = (TextView) view.findViewById(R.id.fragment_no_internet_connection);
}
@Override
public void onAttach(final Activity activity)
// This makes sure that the Activity containing this Fragment has
// implemented the callback interface. If the callback interface has not
// been implemented, an exception is thrown.
{
super.onAttach(activity);
listeners = (Listeners) activity;
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater)
{
inflater.inflate(R.menu.friends_list_fragment, menu);
if (!isAnAsyncTaskRunning())
{
final MenuItem refreshMenuItem = menu.findItem(R.id.friends_list_fragment_menu_refresh);
refreshMenuItem.setVisible(true);
final MenuItem searchMenuItem = menu.findItem(R.id.friends_list_fragment_menu_search);
searchMenuItem.setVisible(true);
final SearchView searchView = (SearchView) searchMenuItem.getActionView();
searchView.setQueryHint(getString(R.string.search_friends));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener()
{
@Override
public boolean onQueryTextChange(final String newText)
{
if (list != null)
{
final FriendsListAdapter adapter = (FriendsListAdapter) list.getAdapter();
if (adapter != null)
{
adapter.getFilter().filter(newText);
}
}
return true;
}
@Override
public boolean onQueryTextSubmit(final String query)
{
return true;
}
});
searchMenuItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener()
{
@Override
public boolean onMenuItemActionCollapse(final MenuItem item)
{
searchView.setQuery(null, true);
return true;
}
@Override
public boolean onMenuItemActionExpand(final MenuItem item)
{
return true;
}
});
}
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public void onDestroyView()
{
cancelRunningAnyAsyncTask();
super.onDestroyView();
}
@Override
public void onItemClick(final AdapterView<?> parent, final View view, final int position, final long id)
{
@SuppressWarnings("unchecked")
final ListItem<Person> friend = (ListItem<Person>) parent.getItemAtPosition(position);
if (!friend.isSelected())
{
listeners.onFriendSelected(friend.get());
selectedFriend = friend;
selectedFriend.select();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
{
view.setActivated(true);
}
}
}
@Override
public boolean onOptionsItemSelected(final MenuItem item)
{
switch (item.getItemId())
{
case R.id.friends_list_fragment_menu_refresh:
if (!isAnAsyncTaskRunning())
{
empty.setText(R.string.friends_list_fragment_no_friends);
getPreferences().edit().clear().commit();
listeners.onRefreshSelected();
refreshFriendsList();
}
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
@Override
public void onResume()
{
super.onResume();
if (isFirstOnResume)
{
isFirstOnResume = false;
refreshFriendsList();
}
}
/**
* Cancels the currently running AsyncTask (if any).
*/
private void cancelRunningAnyAsyncTask()
{
if (isAnAsyncTaskRunning())
{
asyncRefreshFriendsList.cancel(true);
}
}
private void deselectFriend()
{
if (selectedFriend != null)
{
selectedFriend.unselect();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
{
for (int i = 0; i < list.getChildCount(); ++i)
{
final View view = list.getChildAt(i);
view.setActivated(false);
}
}
}
private SharedPreferences getPreferences()
{
if (sPreferences == null)
{
sPreferences = getSherlockActivity().getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
}
return sPreferences;
}
/**
* @return
* Returns true if the asyncRefreshFriendsList AsyncTask is currently
* running.
*/
private boolean isAnAsyncTaskRunning()
{
return asyncRefreshFriendsList != null;
}
public boolean onBackPressed()
{
if (isAnAsyncTaskRunning())
{
cancelRunningAnyAsyncTask();
}
else
{
deselectFriend();
}
return false;
}
/**
* Refreshes the friends list if a refresh is not already running.
*/
public void refreshFriendsList()
{
if (!isAnAsyncTaskRunning())
{
asyncRefreshFriendsList = new AsyncRefreshFriendsList();
asyncRefreshFriendsList.execute();
}
}
private final class AsyncRefreshFriendsList extends AsyncTask<Void, Void, LinkedList<ListItem<Person>>>
implements Comparator<ListItem<Person>>
{
private final static byte RUN_STATUS_NORMAL = 1;
private final static byte RUN_STATUS_NO_NETWORK_CONNECTION = 2;
private byte runStatus;
private SherlockFragmentActivity fragmentActivity;
private AsyncRefreshFriendsList()
{
fragmentActivity = getSherlockActivity();
runStatus = RUN_STATUS_NORMAL;
}
@Override
protected LinkedList<ListItem<Person>> doInBackground(final Void... params)
{
final LinkedList<ListItem<Person>> friends = new LinkedList<ListItem<Person>>();
if (!isCancelled())
{
@SuppressWarnings("unchecked")
final Map<String, String> map = (Map<String, String>) getPreferences().getAll();
if (map == null || map.isEmpty())
{
if (Utilities.checkForNetworkConnectivity(fragmentActivity))
{
final Session session = Session.getActiveSession();
Request.newMyFriendsRequest(session, new GraphUserListCallback()
{
@Override
public void onCompleted(final List<GraphUser> users, final Response response)
{
if (users != null && !users.isEmpty())
{
for (int i = 0; i < users.size() && !isCancelled(); ++i)
{
final GraphUser user = users.get(i);
final String id = user.getId();
final String name = user.getName();
final Person friend = addFriend(id, name);
if (friend != null)
{
final ListItem<Person> listItem = new ListItem<Person>(friend);
friends.add(listItem);
}
}
}
final SharedPreferences.Editor editor = getPreferences().edit();
editor.clear();
for (int i = 0; i < friends.size() && !isCancelled(); ++i)
{
final Person friend = friends.get(i).get();
editor.putString(String.valueOf(friend.getId()), friend.getName());
}
// http://stackoverflow.com/questions/5960678/whats-the-difference-between-commit-and-apply
// Basically, commit() is slower than apply() because it synchronously writes these
// SharedPreferences changes to storage. Because we're potentially writing a bunch of data
// to SharedPreferences right now (the user's whole list of Facebook friends!) we can get a
// potentially pretty decent speed boost out of this.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD)
{
editor.apply();
}
else
{
editor.commit();
}
}
}).executeAndWait();
}
else
{
runStatus = RUN_STATUS_NO_NETWORK_CONNECTION;
}
}
else
{
final Set<String> set = map.keySet();
for (final Iterator<String> i = set.iterator(); i.hasNext() && !isCancelled(); )
{
final String id = i.next();
final String name = map.get(id);
final Person friend = addFriend(id, name);
if (friend != null)
{
final ListItem<Person> listItem = new ListItem<Person>(friend);
friends.add(listItem);
}
}
}
}
// sorts the list of friends using the Comparator method found in
// this class
Collections.sort(friends, this);
return friends;
}
private void cancelled()
{
setRunningState(false);
final SharedPreferences.Editor editor = getPreferences().edit();
editor.clear();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD)
{
editor.apply();
}
else
{
editor.commit();
}
}
@Override
protected void onCancelled()
{
cancelled();
}
@Override
protected void onCancelled(final LinkedList<ListItem<Person>> friends)
{
cancelled();
}
@Override
public int compare(final ListItem<Person> geo, final ListItem<Person> jarrad)
{
return geo.get().getName().compareToIgnoreCase(jarrad.get().getName());
}
@Override
protected void onPostExecute(final LinkedList<ListItem<Person>> friends)
{
if (runStatus == RUN_STATUS_NORMAL && friends != null && !friends.isEmpty())
{
final FriendsListAdapter adapter = new FriendsListAdapter(friends);
list.setAdapter(adapter);
list.setVisibility(View.VISIBLE);
empty.setVisibility(View.GONE);
loading.setVisibility(View.GONE);
noInternetConnection.setVisibility(View.GONE);
}
else if (runStatus == RUN_STATUS_NO_NETWORK_CONNECTION)
{
list.setVisibility(View.GONE);
empty.setVisibility(View.GONE);
loading.setVisibility(View.GONE);
noInternetConnection.setVisibility(View.VISIBLE);
}
else
{
list.setVisibility(View.GONE);
empty.setVisibility(View.VISIBLE);
loading.setVisibility(View.GONE);
noInternetConnection.setVisibility(View.GONE);
}
setRunningState(false);
}
@Override
protected void onPreExecute()
{
setRunningState(true);
list.setVisibility(View.GONE);
empty.setVisibility(View.GONE);
loading.setVisibility(View.VISIBLE);
noInternetConnection.setVisibility(View.GONE);
}
/**
* Creates a Person object out of the given data (if the given data is
* valid).
*
* @param id
* The friend's Facebook ID.
*
* @param name
* The friend's Facebook name.
*
* @return
* Returns a Person object representing the given Facebook friend. Has
* the possibility of returning null if the given data is invalid.
*/
private Person addFriend(final String id, final String name)
{
Person friend = null;
if (Person.isIdValid(id) && Person.isNameValid(name))
{
friend = new Person(id, name);
}
return friend;
}
/**
* Use this method to reset the options menu. This should only be used when
* an AsyncTask has either just begun or has just ended.
*
* @param isRunning
* True if the AsyncTask is just starting to run, false if it's just
* finished.
*/
private void setRunningState(final boolean isRunning)
{
if (!isRunning)
{
asyncRefreshFriendsList = null;
}
fragmentActivity.supportInvalidateOptionsMenu();
}
}
private final class FriendsListAdapter extends BaseAdapter implements Filterable
{
private Activity activity;
private Drawable emptyProfilePicture;
private Filter filter;
private LayoutInflater inflater;
private LinkedList<ListItem<Person>> friends;
private FriendsListAdapter(final LinkedList<ListItem<Person>> friends)
{
this.friends = friends;
activity = getSherlockActivity();
inflater = activity.getLayoutInflater();
emptyProfilePicture = getResources().getDrawable(R.drawable.empty_profile_picture_small);
filter = new FriendsListFilter(friends);
}
@Override
public int getCount()
{
return friends.size();
}
@Override
public Filter getFilter()
{
return filter;
}
@Override
public ListItem getItem(int position)
{
return friends.get(position);
}
@Override
public long getItemId(final int position)
{
return position;
}
@Override
public View getView(final int position, View convertView, final ViewGroup parent)
{
final ListItem<Person> listItem = friends.get(position);
final Person friend = listItem.get();
if (convertView == null)
{
convertView = inflater.inflate(R.layout.friends_list_fragment_listview_item, null);
final ViewHolder viewHolder = new ViewHolder(convertView);
convertView.setTag(viewHolder);
}
final ViewHolder viewHolder = (ViewHolder) convertView.getTag();
viewHolder.name.setText(friend.getName());
viewHolder.picture.setImageDrawable(emptyProfilePicture);
final String friendsPictureURL = FacebookUtilities.getFriendsPictureSquare(activity, friend.getId());
Utilities.getImageLoader().displayImage(friendsPictureURL, viewHolder.picture);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
{
if (listItem.isSelected())
{
convertView.setActivated(true);
}
else
{
convertView.setActivated(false);
}
}
return convertView;
}
/**
* This class performs the actual filtering of the friends in the
* friends list.
*/
private final class FriendsListFilter extends Filter
{
private LinkedList<ListItem<Person>> friendsCopy;
private String previousQuery;
private FriendsListFilter(final LinkedList<ListItem<Person>> friends)
{
friendsCopy = new LinkedList<ListItem<Person>>(friends);
}
@Override
protected FilterResults performFiltering(final CharSequence constraint)
// The CharSequence constraint variable is the actual text that the
// user is searching for.
{
final FilterResults filterResults = new FilterResults();
if (constraint == null || constraint.length() < 1)
// Check to see if the text that the user searched for is null
// or empty. If either of these checks prove true, then we know
// that the user wants to clear their search, which means that
// they want to see their entire, unfiltered, friends list.
{
if (friends.size() != friendsCopy.size())
// The friendsCopy object contains the original unsorted
// list of friends. This check compares the sizes of the
// two lists, if they are the same size then we know that
// the friends list has not been altered / filtered and
// this means that we don't need to waste the performance
// power needed to create a copy of the original list.
{
// Creates a copy of the original, unaltered friends
// list.
friends = new LinkedList<ListItem<Person>>(friendsCopy);
}
filterResults.count = friendsCopy.size();
filterResults.values = friendsCopy;
previousQuery = null;
}
else
{
// The friends that are found to contain the text that the
// user has searched for will be placed in this LinkedList.
final LinkedList<ListItem<Person>> filteredFriends = new LinkedList<ListItem<Person>>();
// Grab a String version of the user's search text and
// convert it to lower case form.
final String query = constraint.toString().toLowerCase();
if (previousQuery != null && previousQuery.length() > query.length())
// Check the previous query and see if it was a bigger
// String than the current query. This is needed because if
// the user entered "Ka", but then deleted it down to just
// "K", the friends list would only continue to filter
// based on the results found from the "Ka" search.
{
friends = new LinkedList<ListItem<Person>>(friendsCopy);
}
// store the user's current search query
previousQuery = query;
for (final ListItem<Person> friend : friends)
// search through every friend in the list of friends
{
final String name = friend.get().getName().toLowerCase();
if (name.contains(query))
{
// This friend's name was found to contain the text
// that the user is searching for. Add this friend
// to the list of filtered friends.
filteredFriends.add(friend);
}
}
filterResults.count = filteredFriends.size();
filterResults.values = filteredFriends;
}
return filterResults;
}
@Override
protected void publishResults(final CharSequence constraint, final FilterResults results)
{
// Clear the list of friends that is currently being shown on
// the screen.
friends.clear();
@SuppressWarnings("unchecked")
final LinkedList<ListItem<Person>> values = (LinkedList<ListItem<Person>>) results.values;
// Add the list of filtered friends to the newly cleared list.
friends.addAll(values);
if (friends.isEmpty())
{
empty.setText(R.string.no_friends_found);
}
else
{
empty.setText(R.string.friends_list_fragment_no_friends);
}
// This refreshes the friends list as shown on the device's
// screen.
notifyDataSetChanged();
}
}
private final class ViewHolder
{
private ImageView picture;
private TextView name;
private ViewHolder(final View view)
{
picture = (ImageView) view.findViewById(R.id.friends_list_fragment_listview_item_picture);
name = (TextView) view.findViewById(R.id.friends_list_fragment_listview_item_name);
}
}
}
}