package ca.josephroque.bowlingcompanion.fragment;
import android.app.AlertDialog;
import android.content.ContentValues;
import android.content.DialogInterface;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import java.lang.ref.WeakReference;
import java.util.Locale;
import ca.josephroque.bowlingcompanion.Constants;
import ca.josephroque.bowlingcompanion.MainActivity;
import ca.josephroque.bowlingcompanion.R;
import ca.josephroque.bowlingcompanion.database.Contract.GameEntry;
import ca.josephroque.bowlingcompanion.database.Contract.MatchPlayEntry;
import ca.josephroque.bowlingcompanion.database.DatabaseHelper;
import ca.josephroque.bowlingcompanion.theme.Theme;
import ca.josephroque.bowlingcompanion.utilities.DisplayUtils;
/**
* Displays options for setting match play results for a game.
*/
public class MatchPlayFragment
extends Fragment
implements Theme.ChangeableTheme {
/** Identifies output from this class in Logcat. */
@SuppressWarnings("unused")
private static final String TAG = "MatchPlayFragment";
/** Represents the opponent's name. */
private static final String ARG_OPPONENT_NAME = "arg_opp_name";
/** Represents the opponent's score. */
private static final String ARG_OPPONENT_SCORE = "arg_opp_score";
/** Represents the user's selected match play result. */
private static final String ARG_SELECTED_RESULT = "arg_selected_result";
/** TextView to display the bowler's name. */
private TextView mTextViewBowler;
/** TextView to display the league or event's name. */
private TextView mTextViewLeagueEvent;
/** TextView to display the date of the series or event. */
private TextView mTextViewDate;
/** TextView to display the number of game. */
private TextView mTextViewGameNumber;
/** TextView to display the user's score. */
private TextView mTextViewScore;
/** Input field for opponent's name. */
private EditText mEditTextOpponentName;
/** Input field for opponent's score. */
private EditText mEditTextOpponentScore;
/** Radio buttons indicating the results of the match. */
private RadioGroup mRadioGroupMatchResult;
/** Game for which match play statistics are being shown. */
private long mGameId;
/** Indicates if the fragment was loaded from a saved instance state. */
private boolean mFromSavedInstanceState;
/** Indicates if the match play results have finished loading from the database. */
private boolean mFinishedLoadingResults;
/** The selected radio button. Used to restore instance state. */
private int mSelectedRadioButtonId;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View rootView = inflater.inflate(R.layout.fragment_match_play, container, false);
mTextViewBowler = (TextView) rootView.findViewById(R.id.tv_bowler_name);
mTextViewLeagueEvent = (TextView) rootView.findViewById(R.id.tv_league_name);
mTextViewDate = (TextView) rootView.findViewById(R.id.tv_series_name);
mTextViewGameNumber = (TextView) rootView.findViewById(R.id.tv_game_number);
mTextViewScore = (TextView) rootView.findViewById(R.id.tv_score);
mEditTextOpponentName = (EditText) rootView.findViewById(R.id.et_opponent_name);
mEditTextOpponentScore = (EditText) rootView.findViewById(R.id.et_opponent_score);
mRadioGroupMatchResult = (RadioGroup) rootView.findViewById(R.id.rg_match_results);
if (savedInstanceState != null) {
mFromSavedInstanceState = true;
mGameId = savedInstanceState.getLong(Constants.EXTRA_ID_GAME);
String opponentName = savedInstanceState.getString(ARG_OPPONENT_NAME);
String opponentScore = savedInstanceState.getString(ARG_OPPONENT_SCORE);
mSelectedRadioButtonId = savedInstanceState.getInt(ARG_SELECTED_RESULT);
mEditTextOpponentName.setText(opponentName);
mEditTextOpponentScore.setText(opponentScore);
} else {
mGameId = getArguments().getLong(Constants.EXTRA_ID_GAME);
}
setupToolbar(rootView);
return rootView;
}
@Override
public void onResume() {
super.onResume();
if (getActivity() != null) {
MainActivity mainActivity = (MainActivity) getActivity();
mainActivity.setFloatingActionButtonState(0, 0);
mainActivity.setDrawerState(false);
if (mFromSavedInstanceState && mSelectedRadioButtonId != -1)
mRadioGroupMatchResult.check(mSelectedRadioButtonId);
mainActivity.setActionBarTitle(R.string.title_fragment_match_play, true);
mFinishedLoadingResults = false;
new LoadMatchPlayTask(this).execute(mGameId);
}
updateTheme();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putLong(Constants.EXTRA_ID_GAME, mGameId);
outState.putString(ARG_OPPONENT_NAME, mEditTextOpponentName.getText().toString());
outState.putString(ARG_OPPONENT_SCORE, mEditTextOpponentScore.getText().toString());
outState.putInt(ARG_SELECTED_RESULT, mRadioGroupMatchResult.getCheckedRadioButtonId());
}
@Override
public void updateTheme() {
View rootView = getView();
if (rootView != null) {
rootView.findViewById(R.id.toolbar_bottom).setBackgroundColor(Theme.getPrimaryThemeColor());
}
}
/**
* Sets up the toolbar for the fragment.
*
* @param rootView root view of the fragment
*/
private void setupToolbar(View rootView) {
Toolbar toolbarBottom = (Toolbar) rootView.findViewById(R.id.toolbar_bottom);
toolbarBottom.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_save:
mEditTextOpponentName.clearFocus();
mEditTextOpponentScore.clearFocus();
DisplayUtils.hideKeyboard(getActivity());
if (mFinishedLoadingResults)
saveMatchPlayResults();
else
Toast.makeText(getContext(), R.string.text_not_loaded, Toast.LENGTH_SHORT).show();
break;
case R.id.action_cancel:
getActivity().onBackPressed();
break;
default:
// does nothing
}
return true;
}
});
// Inflate a menu to be displayed in the toolbar
toolbarBottom.inflateMenu(R.menu.menu_match_play);
}
/**
* Checks if the user's input for a name and score is valid.
*
* @param opponentName name input by user for opponent
* @param opponentScore score input by user for opponent
* @return {@code true} if the score and name are valid, {@code false} otherwise
*/
private boolean isInputValid(String opponentName, String opponentScore) {
int invalidInputMessage = -1;
short scoreConverted;
if (!TextUtils.isEmpty(opponentScore)) {
try {
scoreConverted = Short.parseShort(opponentScore);
if (scoreConverted < 0 || scoreConverted > Constants.GAME_MAX_SCORE)
throw new NumberFormatException("Not a valid 5 pin score.");
} catch (NumberFormatException ex) {
invalidInputMessage = R.string.dialog_score_invalid;
}
}
if (!TextUtils.isEmpty(opponentName) && !opponentName.matches(Constants.REGEX_NAME)) {
// Name is not made up of letters and spaces
invalidInputMessage = R.string.dialog_name_letters_spaces;
}
/*
* If the input was invalid for any reason, a dialog is shown
* to the user and the method does not continue
*/
if (invalidInputMessage != -1) {
new AlertDialog.Builder(getContext())
.setTitle(R.string.text_save_failure)
.setMessage(invalidInputMessage)
.setPositiveButton(R.string.dialog_okay, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.create()
.show();
return false;
}
return true;
}
/**
* Saves the user's input for the match play results.
*/
private void saveMatchPlayResults() {
if (!isInputValid(mEditTextOpponentName.getText().toString(), mEditTextOpponentScore.getText().toString()))
return;
final MainActivity mainActivity = (MainActivity) getActivity();
mainActivity.addSavingThread(new Thread(new Runnable() {
@Override
public void run() {
SQLiteDatabase database = DatabaseHelper.getInstance(mainActivity).getWritableDatabase();
boolean saveSuccessful = true;
database.beginTransaction();
try {
String[] whereArgs = {String.valueOf(mGameId)};
ContentValues values = new ContentValues();
values.put(GameEntry.COLUMN_MATCH_PLAY, getSelectedMatchPlayRadioButton());
database.update(GameEntry.TABLE_NAME, values, GameEntry._ID + "=?", whereArgs);
String opponentName = mEditTextOpponentName.getText().toString();
String opponentScore = mEditTextOpponentScore.getText().toString();
values = new ContentValues();
values.put(MatchPlayEntry.COLUMN_GAME_ID, mGameId);
if (!TextUtils.isEmpty(opponentName))
values.put(MatchPlayEntry.COLUMN_OPPONENT_NAME, opponentName);
if (!TextUtils.isEmpty(opponentScore))
values.put(MatchPlayEntry.COLUMN_OPPONENT_SCORE, Short.parseShort(opponentScore));
else
values.put(MatchPlayEntry.COLUMN_OPPONENT_SCORE, 0);
/*
* Due to the way this method was originally implemented, when match play results were updated,
* often the wrong row in the table was altered. This bug prevented users from saving match play
* results under certain circumstances. This has been fixed, but the old data cannot be safely
* removed all at once, without potentially deleting some of the user's real data. As a fix, when
* a user now saves match play results, any old results for *only that game* are deleted, and the
* new results are inserted, as seen below.
*/
database.delete(MatchPlayEntry.TABLE_NAME, MatchPlayEntry.COLUMN_GAME_ID + "=?", whereArgs);
long result = database.insert(MatchPlayEntry.TABLE_NAME, null, values);
if (result == -1)
throw new Exception("Failed to insert values into database.");
database.setTransactionSuccessful();
} catch (Exception ex) {
Log.e(TAG, "Error saving match results.", ex);
saveSuccessful = false;
} finally {
database.endTransaction();
}
final int toastMessage = (saveSuccessful)
? R.string.text_results_saved
: R.string.text_save_failure;
mainActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(mainActivity, toastMessage, Toast.LENGTH_SHORT).show();
}
});
}
}));
}
/**
* Gets the match play results set by the user depending on the radio button they selected in {@code
* mRadioGroupMatchResult}.
*
* @return one of {@code Constants.MATCH_PLAY_WON}, {@code Constants.MATCH_PLAY_LOST}, {@code
* Constants.MATCH_PLAY_TIED}, or {@code Constants.MATCH_PLAY_NONE}
*/
private int getSelectedMatchPlayRadioButton() {
if (mRadioGroupMatchResult != null) {
switch (mRadioGroupMatchResult.getCheckedRadioButtonId()) {
case R.id.rb_result_won:
return Constants.MATCH_PLAY_WON;
case R.id.rb_result_lost:
return Constants.MATCH_PLAY_LOST;
case R.id.rb_result_tied:
return Constants.MATCH_PLAY_TIED;
default:
return Constants.MATCH_PLAY_NONE;
}
}
return Constants.MATCH_PLAY_NONE;
}
/**
* Creates a new instance of this fragment and passes the parameters as arguments.
*
* @param gameId game id to set match play results of
* @return a new {@code MatchPlayFragment} instance
*/
public static MatchPlayFragment newInstance(long gameId) {
MatchPlayFragment fragment = new MatchPlayFragment();
Bundle args = new Bundle();
args.putLong(Constants.EXTRA_ID_GAME, gameId);
fragment.setArguments(args);
return fragment;
}
/**
* Loads the match play statistics for the game so they can be viewed and altered.
*/
private static final class LoadMatchPlayTask
extends AsyncTask<Long, Void, SparseArray<Object>> {
/** Weak reference to the parent fragment. */
private final WeakReference<MatchPlayFragment> mFragment;
/**
* Assigns a weak reference to the parent fragment.
*
* @param fragment parent fragment
*/
private LoadMatchPlayTask(MatchPlayFragment fragment) {
mFragment = new WeakReference<>(fragment);
}
@Override
protected SparseArray<Object> doInBackground(Long... gameId) {
MatchPlayFragment fragment = mFragment.get();
if (fragment == null || !fragment.isAdded() || fragment.getActivity() == null)
return null;
MainActivity.waitForSaveThreads(new WeakReference<>((MainActivity) fragment.getActivity()));
SparseArray<Object> result = new SparseArray<>();
int gameNumber = -1;
int matchResult = -1;
short gameScore = -1;
String[] rawArgs = {String.valueOf(gameId[0])};
String rawMatchQuery = "SELECT "
+ GameEntry.COLUMN_GAME_NUMBER + ", "
+ GameEntry.COLUMN_MATCH_PLAY + ", "
+ GameEntry.COLUMN_SCORE
+ " FROM " + GameEntry.TABLE_NAME
+ " WHERE " + GameEntry._ID + "=?";
SQLiteDatabase database = DatabaseHelper.getInstance(fragment.getContext()).getReadableDatabase();
Cursor cursor = database.rawQuery(rawMatchQuery, rawArgs);
if (cursor.moveToFirst()) {
gameNumber = cursor.getInt(cursor.getColumnIndex(GameEntry.COLUMN_GAME_NUMBER));
gameScore = cursor.getShort(cursor.getColumnIndex(GameEntry.COLUMN_SCORE));
matchResult = cursor.getInt(cursor.getColumnIndex(GameEntry.COLUMN_MATCH_PLAY));
}
cursor.close();
String opponentName = null;
short opponentScore;
rawMatchQuery = "SELECT "
+ MatchPlayEntry.COLUMN_OPPONENT_NAME + ", "
+ MatchPlayEntry.COLUMN_OPPONENT_SCORE
+ " FROM " + MatchPlayEntry.TABLE_NAME
+ " WHERE " + MatchPlayEntry.COLUMN_GAME_ID + "=?";
cursor = database.rawQuery(rawMatchQuery, rawArgs);
if (cursor.moveToFirst()) {
if (!cursor.isNull(cursor.getColumnIndex(MatchPlayEntry.COLUMN_OPPONENT_NAME)))
opponentName = cursor.getString(cursor.getColumnIndex(MatchPlayEntry.COLUMN_OPPONENT_NAME));
opponentScore = cursor.getShort(cursor.getColumnIndex(MatchPlayEntry.COLUMN_OPPONENT_SCORE));
} else {
opponentName = null;
opponentScore = 0;
}
if (gameNumber == -1 || matchResult == -1)
return null;
if (opponentName != null)
result.put(MatchPlayData.OpponentName.ordinal(), opponentName);
result.put(MatchPlayData.Score.ordinal(), gameScore);
result.put(MatchPlayData.OpponentScore.ordinal(), opponentScore);
result.put(MatchPlayData.GameNumber.ordinal(), gameNumber);
result.put(MatchPlayData.Result.ordinal(), matchResult);
return result;
}
@Override
protected void onPostExecute(SparseArray<Object> result) {
MatchPlayFragment fragment = mFragment.get();
if (result == null || fragment == null)
return;
MainActivity mainActivity = (MainActivity) fragment.getActivity();
if (mainActivity == null)
return;
fragment.mTextViewBowler.setText(mainActivity.getBowlerName());
fragment.mTextViewLeagueEvent.setText(mainActivity.getLeagueName());
fragment.mTextViewDate.setText(mainActivity.getSeriesDate());
int gameNumber = (int) result.get(MatchPlayData.GameNumber.ordinal());
short gameScore = (short) result.get(MatchPlayData.Score.ordinal());
fragment.mTextViewGameNumber.setText(String.format(Locale.CANADA, "%d", gameNumber));
fragment.mTextViewScore.setText(String.format(Locale.CANADA, "%d", gameScore));
if (!fragment.mFromSavedInstanceState) {
String opponentName = result.get(MatchPlayData.OpponentName.ordinal(), "").toString();
short opponentScore = (short) result.get(MatchPlayData.OpponentScore.ordinal());
if (opponentName.length() > 0)
fragment.mEditTextOpponentName.setText(opponentName);
if (opponentScore > 0)
fragment.mEditTextOpponentScore.setText(String.format(Locale.CANADA, "%d", opponentScore));
switch ((int) result.get(MatchPlayData.Result.ordinal())) {
case Constants.MATCH_PLAY_NONE:
fragment.mRadioGroupMatchResult.check(R.id.rb_result_none);
break;
case Constants.MATCH_PLAY_WON:
fragment.mRadioGroupMatchResult.check(R.id.rb_result_won);
break;
case Constants.MATCH_PLAY_LOST:
fragment.mRadioGroupMatchResult.check(R.id.rb_result_lost);
break;
case Constants.MATCH_PLAY_TIED:
fragment.mRadioGroupMatchResult.check(R.id.rb_result_tied);
break;
default:
throw new IllegalArgumentException(
"Invalid match play results: " + (int) result.get(MatchPlayData.Result.ordinal()));
}
}
fragment.mFinishedLoadingResults = true;
}
}
/**
* Data of the results of a game's match play.
*/
private enum MatchPlayData {
/** Number of the game in the series. */
GameNumber,
/** User's score. */
Score,
/** Name of the opponent. */
OpponentName,
/** Score of the opponent. */
OpponentScore,
/** Match play result. */
Result
}
}