package com.charlesmadere.android.classygames; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.res.Resources; import android.graphics.Shader; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.util.Log; 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.charlesmadere.android.classygames.gcm.GCMIntentService; import com.charlesmadere.android.classygames.models.Game; import com.charlesmadere.android.classygames.models.ListItem; import com.charlesmadere.android.classygames.models.Person; import com.charlesmadere.android.classygames.server.*; import com.charlesmadere.android.classygames.utilities.FacebookUtilities; import com.charlesmadere.android.classygames.utilities.Utilities; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; public final class GamesListFragment extends SherlockListFragment implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener { private final static String LOG_TAG = Utilities.LOG_TAG + " - GamesListFragment"; private final static String KEY_GAMES_LIST_JSON = "KEY_GAMES_LIST_JSON"; private ListView list; private TextView empty; private LinearLayout loading; private TextView noInternetConnection; /** * JSONObject downloaded from the server that represents the games list. */ private JSONObject gamesListJSON; /** * Boolean that marks if this is the first time that the onResume() method * was hit. */ private boolean isFirstOnResume = true; /** * Used to perform a refresh of the Games List. */ private AsyncRefreshGamesList asyncRefreshGamesList; /** * If the user has selected a game in their games list, then this will be a * handle to that Game object. If no game is currently selected, then this * will be null. */ private ListItem<Game> selectedGame; /** * Holds a handle to a currently running (if it's currently running) * ServerApi object. */ private ServerApi serverApiTask; /** * Callback interface for the ServerApi class. */ private ServerApi.Listeners serverApiListeners; /** * 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 when the user selects a game in their games list. * * @param game * The Game object that the user selected. */ public void onGameSelected(final Game game); /** * 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.games_list_fragment, container, false); } @Override @SuppressWarnings("deprecation") public void onActivityCreated(final Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); final View view = getView(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { final BitmapDrawable background = (BitmapDrawable) getResources().getDrawable(R.drawable.bg_bright); background.setAntiAlias(true); background.setDither(true); background.setFilterBitmap(true); background.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); view.setBackgroundDrawable(background); } serverApiListeners = new ServerApi.Listeners() { @Override public void onCancel() { serverApiTask = null; } @Override public void onComplete(final String serverResponse) { serverApiTask = null; listeners.onRefreshSelected(); } @Override public void onDismiss() { serverApiTask = null; } }; list = getListView(); list.setOnItemClickListener(this); list.setOnItemLongClickListener(this); empty = (TextView) view.findViewById(android.R.id.empty); loading = (LinearLayout) view.findViewById(R.id.games_list_fragment_loading); noInternetConnection = (TextView) view.findViewById(R.id.fragment_no_internet_connection); if (savedInstanceState != null && savedInstanceState.containsKey(KEY_GAMES_LIST_JSON)) { final String gamesListJSONString = savedInstanceState.getString(KEY_GAMES_LIST_JSON); if (Utilities.validString(gamesListJSONString)) { try { gamesListJSON = new JSONObject(gamesListJSONString); } catch (final JSONException e) { Log.e(LOG_TAG, "JSONException in onActivityCreated()!", e); } } } } @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.games_list_fragment, menu); 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<Game> game = (ListItem<Game>) parent.getItemAtPosition(position); if (!game.isSelected() && game.get().isTypeGame() && game.get().isTurnYours()) { listeners.onGameSelected(game.get()); selectedGame = game; selectedGame.select(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { view.setActivated(true); } } } @Override public boolean onItemLongClick(final AdapterView<?> parent, final View view, int position, final long id) { if (!isAnAsyncTaskRunning()) { @SuppressWarnings("unchecked") final ListItem<Game> game = (ListItem<Game>) parent.getItemAtPosition(position); if (game.get().isTypeGame()) { final Context context = getSherlockActivity(); String [] items; if (game.get().isTurnYours()) { items = getResources().getStringArray(R.array.games_list_fragment_context_menu_entries_turn_yours); } else { items = getResources().getStringArray(R.array.games_list_fragment_context_menu_entries_turn_theirs); } new AlertDialog.Builder(context) .setItems(items, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { dialog.dismiss(); switch (which) { case 0: serverApiTask = new ServerApiForfeitGame(context, serverApiListeners, game.get()); break; case 1: serverApiTask = new ServerApiSkipMove(context, serverApiListeners, game.get()); break; } if (serverApiTask != null) { serverApiTask.execute(); } } }) .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { dialog.dismiss(); } }) .setTitle(getString(R.string.select_an_action_for_this_game_against_x, game.get().getPerson().getName())) .show(); return true; } return false; } else { return false; } } @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.games_list_fragment_menu_refresh: if (!isAnAsyncTaskRunning()) { listeners.onRefreshSelected(); } break; default: return super.onOptionsItemSelected(item); } return true; } @Override public void onResume() { super.onResume(); if (isFirstOnResume) { isFirstOnResume = false; final boolean restoreExistingList = gamesListJSON != null; refreshGamesList(restoreExistingList); } } @Override public void onSaveInstanceState(final Bundle outState) { if (gamesListJSON != null) { final String gamesListJSONString = gamesListJSON.toString(); if (Utilities.validString(gamesListJSONString)) { outState.putString(KEY_GAMES_LIST_JSON, gamesListJSONString); } } super.onSaveInstanceState(outState); } /** * Cancels the AsyncRefreshGamesList AsyncTask if it is currently * running. */ private void cancelRunningAnyAsyncTask() { if (isAsyncRefreshGamesListRunning()) { asyncRefreshGamesList.cancel(true); } else if (serverApiTask != null) { serverApiTask.cancel(); } } private void deselectGame() { if (selectedGame != null) { selectedGame.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); } } } /** * @return * Returns true if the asyncRefreshGamesList AsyncTask is currently * running. */ private boolean isAsyncRefreshGamesListRunning() { return asyncRefreshGamesList != null; } /** * @return * Returns true if an AsyncTask is running. */ private boolean isAnAsyncTaskRunning() { return isAsyncRefreshGamesListRunning() || serverApiTask != null; } public boolean onBackPressed() { if (isAnAsyncTaskRunning()) { cancelRunningAnyAsyncTask(); } else { deselectGame(); } return false; } public void refreshGamesList() { refreshGamesList(false); } /** * Refreshes the Games List if a refresh is not already running. * * @param restoreExistingList * Set this to true if you want to restore the games list from the existing * stored games list. Set this to false to force the app to download a new * games list from the server. */ private void refreshGamesList(final boolean restoreExistingList) { if (!isAnAsyncTaskRunning()) { asyncRefreshGamesList = new AsyncRefreshGamesList(restoreExistingList); asyncRefreshGamesList.execute(); } } private final class AsyncRefreshGamesList extends AsyncTask<Void, Void, LinkedList<ListItem<Game>>> implements Comparator<ListItem<Game>> { private byte runStatus; private final static byte RUN_STATUS_NORMAL = 1; private final static byte RUN_STATUS_IOEXCEPTION = 2; private final static byte RUN_STATUS_NO_NETWORK_CONNECTION = 3; private boolean restoreExistingList; private SherlockFragmentActivity fragmentActivity; private AsyncRefreshGamesList(final boolean restoreExistingList) { this.restoreExistingList = restoreExistingList; fragmentActivity = getSherlockActivity(); runStatus = RUN_STATUS_NORMAL; } @Override protected LinkedList<ListItem<Game>> doInBackground(final Void... params) { LinkedList<ListItem<Game>> games = null; if (restoreExistingList && gamesListJSON != null) { games = parseServerResponse(null); } else if (!isCancelled()) { restoreExistingList = false; final Person whoAmI = Utilities.getWhoAmI(fragmentActivity); GCMIntentService.clearNotifications(fragmentActivity); GenericGameFragment.clearCachedBoards(fragmentActivity); MyStatsDialogFragment.clearCachedStats(fragmentActivity); if (!isCancelled() && Utilities.checkForNetworkConnectivity(fragmentActivity)) { try { Thread.sleep(Server.WAIT_FOR_SERVER_DELAY); } catch (final InterruptedException e) { Log.w(LOG_TAG, "AsyncRefreshGamesList thread sleep interrupted!", e); } try { // create the data that will be posted to the server final ApiData data = new ApiData() .addKeyValuePair(Server.POST_DATA_ID, whoAmI.getId()); // Make a call to the Classy Games server API and store // the JSON response. Note that we're also sending it // the nameValuePairs variable that we just created. // The server requires we send it some information in // order for us to get a meaningful response back. final String serverResponse = Server.postToServerGetGames(data); // This line does a lot. Check the parseServerResponse() // method below to get detailed information. This will // parse the JSON response that we got from the server // and create a bunch of individual Game objects out of // that data. games = parseServerResponse(serverResponse); } catch (final IOException e) { Log.e(LOG_TAG, "IOException error in AsyncPopulateGamesList - doInBackground()!", e); } } else { runStatus = RUN_STATUS_NO_NETWORK_CONNECTION; } } return games; } private void cancelled() { setRunningState(false); } @Override public int compare(final ListItem<Game> gameOne, final ListItem<Game> gameTwo) { return (int) (gameTwo.get().getTimestamp() - gameOne.get().getTimestamp()); } @Override protected void onCancelled() { cancelled(); } @Override protected void onCancelled(final LinkedList<ListItem<Game>> games) { cancelled(); } @Override protected void onPostExecute(final LinkedList<ListItem<Game>> games) { if (runStatus == RUN_STATUS_NORMAL && games != null && !games.isEmpty()) { final GamesListAdapter gamesListAdapter = new GamesListAdapter(games); list.setAdapter(gamesListAdapter); list.setVisibility(View.VISIBLE); empty.setVisibility(View.GONE); loading.setVisibility(View.GONE); noInternetConnection.setVisibility(View.GONE); } else if (runStatus == RUN_STATUS_IOEXCEPTION || 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); } /** * Parses the JSON response from the server and makes a bunch of Game * objects about it. * * @param serverResponse * The JSON response acquired from the Classy Games server. This method * <strong>does</strong> check to make sure that this String is both * not null and not empty. If that scenario happens then this method * will return an empty LinkedList of Game objects. * * @return * Returns an LinkedList of Game objects. This LinkedList has a * possibility of being empty. */ private LinkedList<ListItem<Game>> parseServerResponse(final String serverResponse) { final LinkedList<ListItem<Game>> games = new LinkedList<ListItem<Game>>(); if (!isCancelled()) { if (restoreExistingList || Utilities.validString(serverResponse)) { try { if (!restoreExistingList) // Check to see if this boolean is set to false. If it // is set to false, then we're restoring an existing // games list. { gamesListJSON = new JSONObject(serverResponse); } final JSONObject jsonResult = gamesListJSON.getJSONObject(Server.POST_DATA_RESULT); final JSONObject jsonGameData = jsonResult.optJSONObject(Server.POST_DATA_SUCCESS); if (jsonGameData == null) { final String successMessage = jsonResult.optString(Server.POST_DATA_SUCCESS); if (Utilities.validString(successMessage)) { Log.d(LOG_TAG, "Server returned success message: " + successMessage); } else { final String errorMessage = jsonResult.getString(Server.POST_DATA_ERROR); Log.e(LOG_TAG, "Server returned error message: " + errorMessage); } } else { LinkedList<ListItem<Game>> turn = parseTurn(jsonGameData, Server.POST_DATA_TURN_YOURS, Game.TURN_YOURS); if (turn != null && !turn.isEmpty()) { games.addAll(turn); } turn = parseTurn(jsonGameData, Server.POST_DATA_TURN_THEIRS, Game.TURN_THEIRS); if (turn != null && !turn.isEmpty()) { games.addAll(turn); } } } catch (final JSONException e) { Log.e(LOG_TAG, "JSON String is massively malformed.", e); } } else { Log.e(LOG_TAG, "Empty or null String received from server on get games!"); } } return games; } /** * Creates and returns a LinkedList of Game objects that are of the * turn as specified in the whichTurn parameter. * * @param jsonGameData * The JSON game data as received from the server. * * @param postDataTurn * Which turn to pull from the JSON game data. This variable's value * should be one of the Server.POST_DATA_TURN_* variables. * * @param whichTurn * Who's turn is this? This variable's value should be one of the * GAME.TURN_* variables. * * @return * Returns all of the games of the specified turn. Has the possibility * of being null. Check for that! */ private LinkedList<ListItem<Game>> parseTurn(final JSONObject jsonGameData, final String postDataTurn, final boolean whichTurn) { LinkedList<ListItem<Game>> games = null; try { final JSONArray turn = jsonGameData.getJSONArray(postDataTurn); final int turnLength = turn.length(); if (turnLength >= 1) // ensure that we have at least one element in the JSONArray { games = new LinkedList<ListItem<Game>>(); final Game separator = new Game(whichTurn); games.add(new ListItem<Game>(separator)); for (int i = 0; i < turnLength && !isCancelled(); ++i) // loop through all of the games in this turn { try { final JSONObject jsonGame = turn.getJSONObject(i); final Game game = new Game(jsonGame, whichTurn); games.add(new ListItem<Game>(game)); } catch (final JSONException e) { Log.e(LOG_TAG, "Error parsing a turn's game data! (" + i + ") whichTurn: " + whichTurn); } } Collections.sort(games, this); } else { throw new JSONException("Player has no games for this turn."); } } catch (final JSONException e) { if (whichTurn == Game.TURN_YOURS) { Log.d(LOG_TAG, "Player has no games that are his own turn."); } else { Log.d(LOG_TAG, "Player has no games that are the other people's turn."); } } return games; } /** * 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) { asyncRefreshGamesList = null; } } } private final class GamesListAdapter extends BaseAdapter { private Activity activity; private Drawable checkersIcon; private Drawable chessIcon; private Drawable emptyProfilePicture; private LayoutInflater inflater; private LinkedList<ListItem<Game>> games; private Resources resources; private GamesListAdapter(final LinkedList<ListItem<Game>> games) { this.games = games; activity = getSherlockActivity(); inflater = activity.getLayoutInflater(); resources = getResources(); emptyProfilePicture = resources.getDrawable(R.drawable.empty_profile_picture_small); checkersIcon = resources.getDrawable(R.drawable.game_icon_checkers_small); chessIcon = resources.getDrawable(R.drawable.game_icon_chess_small); } @Override public int getCount() { return games.size(); } @Override public ListItem<Game> getItem(final int position) { return games.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<Game> listItem = games.get(position); final Game game = listItem.get(); if (game.isTypeGame()) { if (convertView == null || convertView.getTag() == null) { convertView = inflater.inflate(R.layout.games_list_fragment_listview_item, null); final ViewHolder viewHolder = new ViewHolder(convertView); convertView.setTag(viewHolder); } final ViewHolder viewHolder = (ViewHolder) convertView.getTag(); viewHolder.picture.setImageDrawable(emptyProfilePicture); final String friendsPictureURL = FacebookUtilities.getFriendsPictureSquare(activity, game.getPerson().getId()); Utilities.getImageLoader().displayImage(friendsPictureURL, viewHolder.picture); viewHolder.name.setText(game.getPerson().getName()); viewHolder.time.setText(game.getTimestampFormatted(resources)); if (game.isGameCheckers()) { viewHolder.gameIcon.setImageDrawable(checkersIcon); } else if (game.isGameChess()) { viewHolder.gameIcon.setImageDrawable(chessIcon); } else { viewHolder.gameIcon.setImageDrawable(null); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { if (listItem.isSelected()) { convertView.setActivated(true); } else { convertView.setActivated(false); } } } else { if (game.isTurnYours()) { convertView = inflater.inflate(R.layout.games_list_fragment_listview_turn_yours, null); } else { convertView = inflater.inflate(R.layout.games_list_fragment_listview_turn_theirs, null); } convertView.setOnClickListener(null); convertView.setOnLongClickListener(null); convertView.setTag(null); } return convertView; } } private final class ViewHolder { private ImageView gameIcon; private ImageView picture; private TextView name; private TextView time; private ViewHolder(final View view) { gameIcon = (ImageView) view.findViewById(R.id.games_list_fragment_listview_item_game_icon); picture = (ImageView) view.findViewById(R.id.games_list_fragment_listview_item_picture); name = (TextView) view.findViewById(R.id.games_list_fragment_listview_item_name); time = (TextView) view.findViewById(R.id.games_list_fragment_listview_item_time); } } }