package ca.josephroque.bowlingcompanion.fragment; import android.app.AlertDialog; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.HorizontalScrollView; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; import ca.josephroque.bowlingcompanion.Constants; import ca.josephroque.bowlingcompanion.MainActivity; import ca.josephroque.bowlingcompanion.R; import ca.josephroque.bowlingcompanion.database.Contract.FrameEntry; import ca.josephroque.bowlingcompanion.database.Contract.GameEntry; import ca.josephroque.bowlingcompanion.database.DatabaseHelper; import ca.josephroque.bowlingcompanion.dialog.ManualScoreDialog; import ca.josephroque.bowlingcompanion.utilities.DisplayUtils; import ca.josephroque.bowlingcompanion.utilities.NavigationUtils; import ca.josephroque.bowlingcompanion.utilities.Score; import ca.josephroque.bowlingcompanion.theme.Theme; import ca.josephroque.bowlingcompanion.utilities.ShareUtils; import ca.josephroque.bowlingcompanion.view.AnimatedFloatingActionButton; import ca.josephroque.bowlingcompanion.view.PinLayout; /** * Created by Joseph Roque on 15-03-18. Manages the UI to display information about the games being tracked by the * application, and offers a callback interface {@code GameFragment.GameFragmentCallback} for handling interactions. */ @SuppressWarnings({"Convert2Lambda", "CheckStyle"}) public class GameFragment extends Fragment implements Theme.ChangeableTheme, ManualScoreDialog.ManualScoreDialogListener { /** Identifies output from this class in Logcat. */ @SuppressWarnings("unused") private static final String TAG = "GameFragment"; /** Represents the OnClickListener for the frame TextView objects. */ private static final byte LISTENER_TEXT_FRAMES = 0; /** Represents the OnClickListener for any other objects. */ private static final byte LISTENER_OTHER = 1; /** Represents the OnClickListener for the ball TextView objects. */ private static final byte LISTENER_TEXT_BALLS = 2; /** Number of milliseconds to delay auto locking a game for. */ private static final int AUTO_LOCK_DELAY = 5 * 1000; /** TextView which displays score on a certain ball in a certain frame. */ private TextView[][] mTextViewBallScores; /** TextView which displays whether a foul was invoked on a certain ball. */ private TextView[][] mTextViewFouls; /** TextView which displays total score (not considering fouls) in a certain frame. */ private TextView[] mTextViewFrames; /** TextView which displays final score of the game, with fouls considered. */ private TextView mTextViewFinalScore; /** Offer interaction methods, indicate state of pins in a frame. */ private ImageView[] mImageViewPins; /** * Displays TextView objects in a layout which user can interact with to access specific frame. */ private HorizontalScrollView mHorizontalScrollViewFrames; /** Displays text to user of option to lock a game. */ private ImageView mImageViewLock; /** Used to offer user method to knock down all pins in a frame. */ private ImageView mImageViewClear; /** Displays image to user of option to enabled or disable a foul. */ private ImageView mImageViewFoul; /** Displays image to user of option to reset a frame. */ private ImageView mImageViewResetFrame; /** Displays image to user of option to set match play results. */ private ImageView mImageViewMatchPlay; /** Displays manually set score. */ private TextView mTextViewManualScore; /** Displays text to indicate navigation to next ball. */ private TextView mTextViewNextBall; /** Displays text to indicate navigation to previous ball. */ private TextView mTextViewPrevBall; /** Displays image to indicate navigation to next ball. */ private ImageView mImageViewNextBall; /** Displays image to indicate navigation to previous ball. */ private ImageView mImageViewPrevBall; /** Displays the current game number being viewed. */ private TextView mTextViewGameNumber; /** View which displays auto advance information. */ private TextView mTextViewAutoAdvance; /** Floating action button to advance to next ball. */ private AnimatedFloatingActionButton mNextFab; /** Floating action button to return to previous ball. */ private AnimatedFloatingActionButton mPrevFab; /** Instance of callback interface for handling user events. */ private GameFragmentCallback mGameCallback; /** Ids which represent current games that are available to be edited. */ private long[] mGameIds; /** Ids which represent frames of current games. */ private long[] mFrameIds; /** Indicates which frames in the game have been accessed. */ private boolean[] mHasFrameBeenAccessed; /** Indicate whether a certain pin was knocked down after a certain ball in a certain frame. */ private boolean[][][] mPinState; /** Indicate whether a foul was invoked on a certain ball in a certain frame. */ private boolean[][] mFouls; /** Scores of the current games being edited. */ private short[] mGameScores; /** Scores of the current games being edited, with fouls considered. */ private short[] mGameScoresMinusFouls; /** Indicates if a game is locked or not. */ private boolean[] mGameLocked; /** Indicates if a game has a manual score set or not. */ private boolean[] mManualScoreSet; /** Indicates the results of match play for the game. */ private byte mMatchPlay; /** Integer which represents the background color of views in the activity. */ private int mColorBackground; /** Integer which represents the highlighted color of views in the activity. */ private int mColorHighlight; /** The current game being edited. */ private int mCurrentGame = 0; /** The current frame being edited. */ private byte mCurrentFrame = 0; /** The current ball being edited. */ private byte mCurrentBall = 0; /** Indicates if the app should not save games - set to true in case of errors. */ private final AtomicBoolean mDoNotSave = new AtomicBoolean(false); /** Linear layout which contains button pins. */ private PinLayout mLinearLayoutPins; /** Handles events when pin buttons are touched. */ private final PinLayout.PinInterceptListener mPinTouchListener = new PinLayout.PinInterceptListener() { /** Indicates if a pin has been altered in a touch event. */ private final boolean[] mPinAltered = new boolean[5]; /** State of first pin touched. */ private boolean mInitialState; /** Indicates if an initial valid pin has been selected. */ private boolean mInitialStateSet; /** Total width of pin container. */ private int mLayoutWidth; /** Number of pins which were altered. */ private int mPinAlteredCount; /** Indicates if the layout is being interacted with. */ private boolean mIsDragging; /** Indicates if events to start dragging can begin. */ private boolean mCanStartDragging = true; private void setFirstPinTouched(MotionEvent event) { final int pinTouched = (int) ((event.getX() / mLayoutWidth) * 5); if (!mImageViewPins[pinTouched].isEnabled()) { mInitialStateSet = false; return; } mInitialStateSet = true; mInitialState = mPinState[mCurrentFrame][mCurrentBall][pinTouched]; mPinAltered[pinTouched] = true; mPinAlteredCount = 1; // Flip pin image while user is dragging mImageViewPins[pinTouched].post(new Runnable() { @Override public void run() { if (!mPinState[mCurrentFrame][mCurrentBall][pinTouched]) mImageViewPins[pinTouched].setImageResource( R.drawable.pin_disabled); else mImageViewPins[pinTouched].setImageResource( R.drawable.pin_enabled); } }); } @Override public void disablePinTouches() { mCanStartDragging = false; } @Override public void onPinTouch(MotionEvent event) { if (NavigationUtils.getDrawerOffset() > 0 || mGameLocked[mCurrentGame] || mManualScoreSet[mCurrentGame]) return; final int pinTouched; switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: if (!mCanStartDragging) return; mIsDragging = true; for (int i = 0; i < mPinAltered.length; i++) mPinAltered[i] = false; mLayoutWidth = mLinearLayoutPins.getWidth(); if (event.getX() > mLayoutWidth || event.getX() < 0) return; setFirstPinTouched(event); break; case MotionEvent.ACTION_MOVE: if (!mCanStartDragging || !mInitialStateSet || mLayoutWidth == 0 || event.getX() > mLayoutWidth || event.getX() < 0) return; // Flip pin image while user is dragging pinTouched = (int) ((event.getX() / mLayoutWidth) * 5); if (mImageViewPins[pinTouched].isEnabled() && mPinState[mCurrentFrame][mCurrentBall][pinTouched] == mInitialState && !mPinAltered[pinTouched]) { mPinAlteredCount++; mPinAltered[pinTouched] = true; mImageViewPins[pinTouched].post(new Runnable() { @Override public void run() { if (!mPinState[mCurrentFrame][mCurrentBall][pinTouched]) mImageViewPins[pinTouched].setImageResource(R.drawable.pin_disabled); else mImageViewPins[pinTouched].setImageResource(R.drawable.pin_enabled); } }); } break; case MotionEvent.ACTION_CANCEL: mCanStartDragging = true; case MotionEvent.ACTION_UP: if (!mIsDragging) return; mIsDragging = false; int pinsUpdated = 0; mInitialStateSet = false; // Finalize flipped pins, calculate score for (int i = 0; i < mPinAltered.length; i++) { if (mPinAltered[i]) { pinsUpdated++; if (pinsUpdated == mPinAlteredCount) alterPinState((byte) i, true); else alterPinState((byte) i, false); } } if (!(mCurrentFrame == Constants.LAST_FRAME && mCurrentBall == 2)) { if (mGameCallback != null) { mGameCallback.resetAutoAdvanceTimer(); } } else { startAutoLockTimer(); } break; default: // does nothing } } }; /** Indicates if auto lock functionality is enabled. */ private boolean autoLockEnabled = false; private final Handler autoLockHandler = new Handler(); /** Runnable which locks the game. */ private final Runnable autoLockGame = new Runnable() { @Override public void run() { setGameLocked(true); } }; @Override public void onAttach(Context context) { super.onAttach(context); /* * This makes sure the container Activity has implemented * the callback interface. If not, an exception is thrown */ try { mGameCallback = (GameFragmentCallback) context; } catch (ClassCastException ex) { throw new ClassCastException(context.toString() + " must implement GameFragmentCallbacks"); } } @Override public void onDetach() { super.onDetach(); mGameCallback = null; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); // Loads member variables from saved instance state, if one exists if (savedInstanceState != null) { mCurrentGame = savedInstanceState.getInt(Constants.EXTRA_CURRENT_GAME); mGameIds = savedInstanceState.getLongArray(Constants.EXTRA_ARRAY_GAME_IDS); mFrameIds = savedInstanceState.getLongArray(Constants.EXTRA_ARRAY_FRAME_IDS); mGameLocked = savedInstanceState.getBooleanArray(Constants.EXTRA_ARRAY_GAME_LOCKED); mManualScoreSet = savedInstanceState.getBooleanArray(Constants.EXTRA_ARRAY_MANUAL_SCORE_SET); } } @SuppressWarnings("deprecation") // uses newer APIs where available @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_games, container, false); // Density of screen to set proper width/height of views final float screenDensity = getResources().getDisplayMetrics().density; mHorizontalScrollViewFrames = (HorizontalScrollView) rootView.findViewById(R.id.hsv_frames); RelativeLayout relativeLayout = new RelativeLayout(getActivity()); RelativeLayout.LayoutParams layoutParams; mTextViewBallScores = new TextView[Constants.NUMBER_OF_FRAMES][3]; mTextViewFouls = new TextView[Constants.NUMBER_OF_FRAMES][3]; mTextViewFrames = new TextView[Constants.NUMBER_OF_FRAMES]; mPinState = new boolean[Constants.NUMBER_OF_FRAMES][3][5]; mFouls = new boolean[Constants.NUMBER_OF_FRAMES][3]; mHasFrameBeenAccessed = new boolean[Constants.NUMBER_OF_FRAMES]; final View.OnClickListener[] onClickListeners = getOnClickListeners(); /* * Following creates TextView objects to display information about state of game and * stores references in member variables */ // Calculates most static view sizes beforehand so they don't have to be recalculated final int dp128 = DisplayUtils.getPixelsFromDP(screenDensity, 128); final int dp120 = DisplayUtils.getPixelsFromDP(screenDensity, 120); final int dp88 = DisplayUtils.getPixelsFromDP(screenDensity, 88); final int dp40 = DisplayUtils.getPixelsFromDP(screenDensity, 40); final int dp41 = DisplayUtils.getPixelsFromDP(screenDensity, 41); final int dp20 = DisplayUtils.getPixelsFromDP(screenDensity, 20); final int dp36 = DisplayUtils.getPixelsFromDP(screenDensity, 36); for (int i = 0; i < Constants.NUMBER_OF_FRAMES; i++) { // TextView to display score of a frame TextView frameText = new TextView(getActivity()); switch (i) { // Id is set so when view is clicked, it can be identified case 0: frameText.setId(R.id.text_frame_0); break; case 1: frameText.setId(R.id.text_frame_1); break; case 2: frameText.setId(R.id.text_frame_2); break; case 3: frameText.setId(R.id.text_frame_3); break; case 4: frameText.setId(R.id.text_frame_4); break; case 5: frameText.setId(R.id.text_frame_5); break; case 6: frameText.setId(R.id.text_frame_6); break; case 7: frameText.setId(R.id.text_frame_7); break; case 8: frameText.setId(R.id.text_frame_8); break; case 9: frameText.setId(R.id.text_frame_9); break; default: // do nothing } frameText.setTextColor(0xff000000); frameText.setBackgroundResource(R.drawable.background_frame_text); frameText.setGravity(Gravity.CENTER); frameText.setOnClickListener(onClickListeners[LISTENER_TEXT_FRAMES]); layoutParams = new RelativeLayout.LayoutParams(dp120, dp88); layoutParams.leftMargin = DisplayUtils.getPixelsFromDP(screenDensity, 120 * i); layoutParams.topMargin = dp40; relativeLayout.addView(frameText, layoutParams); mTextViewFrames[i] = frameText; for (int j = 0; j < 3; j++) { // TextView to display value scored on a certain ball TextView text = new TextView(getActivity()); text.setTextColor(0xff000000); text.setBackgroundResource(R.drawable.background_frame_text); text.setGravity(Gravity.CENTER); layoutParams = new RelativeLayout.LayoutParams(dp40, dp41); layoutParams.leftMargin = DisplayUtils.getPixelsFromDP(screenDensity, 120 * i + j * 40); relativeLayout.addView(text, layoutParams); mTextViewBallScores[i][j] = text; text.setOnClickListener(onClickListeners[LISTENER_TEXT_BALLS]); // TextView to display fouls invoked on a certain ball text = new TextView(getActivity()); text.setGravity(Gravity.CENTER); text.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); layoutParams = new RelativeLayout.LayoutParams(dp40, dp20); layoutParams.leftMargin = DisplayUtils.getPixelsFromDP(screenDensity, 120 * i + j * 40); layoutParams.topMargin = dp40; relativeLayout.addView(text, layoutParams); mTextViewFouls[i][j] = text; } // TextView to display frame number under related frame information frameText = new TextView(getActivity()); frameText.setText(String.valueOf(i + 1)); frameText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); frameText.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL); frameText.setTextColor(DisplayUtils.getColorResource(getResources(), android.R.color.white)); layoutParams = new RelativeLayout.LayoutParams(dp120, dp36); layoutParams.leftMargin = DisplayUtils.getPixelsFromDP(screenDensity, 120 * i); layoutParams.addRule(RelativeLayout.BELOW, mTextViewFrames[i].getId()); relativeLayout.addView(frameText, layoutParams); } // TextView to display final score of game mTextViewFinalScore = new TextView(getActivity()); mTextViewFinalScore.setGravity(Gravity.CENTER); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) mTextViewFinalScore.setTextAppearance(android.R.style.TextAppearance_Large); else mTextViewFinalScore.setTextAppearance(getActivity(), android.R.style.TextAppearance_Large); mTextViewFinalScore.setBackgroundResource(R.drawable.background_frame_text); layoutParams = new RelativeLayout.LayoutParams(dp120, dp128); layoutParams.leftMargin = DisplayUtils.getPixelsFromDP(screenDensity, Constants.NUMBER_OF_FRAMES * 120); relativeLayout.addView(mTextViewFinalScore, layoutParams); mHorizontalScrollViewFrames.addView(relativeLayout); // Buttons which indicate state of pins in a frame, provide user interaction methods mImageViewPins = new ImageView[5]; mImageViewPins[0] = (ImageView) rootView.findViewById(R.id.button_pin_1); mImageViewPins[1] = (ImageView) rootView.findViewById(R.id.button_pin_2); mImageViewPins[2] = (ImageView) rootView.findViewById(R.id.button_pin_3); mImageViewPins[3] = (ImageView) rootView.findViewById(R.id.button_pin_4); mImageViewPins[4] = (ImageView) rootView.findViewById(R.id.button_pin_5); mLinearLayoutPins = (PinLayout) rootView.findViewById(R.id.ll_pins); mLinearLayoutPins.setInterceptListener(mPinTouchListener); // Loading other views into member variables, setting OnClickListeners mImageViewNextBall = (ImageView) rootView.findViewById(R.id.iv_next_ball); mImageViewNextBall.setOnClickListener(onClickListeners[LISTENER_OTHER]); mImageViewPrevBall = (ImageView) rootView.findViewById(R.id.iv_prev_ball); mImageViewPrevBall.setOnClickListener(onClickListeners[LISTENER_OTHER]); mTextViewNextBall = (TextView) rootView.findViewById(R.id.tv_next_ball); mTextViewNextBall.setOnClickListener(onClickListeners[LISTENER_OTHER]); mTextViewPrevBall = (TextView) rootView.findViewById(R.id.tv_prev_ball); mTextViewPrevBall.setOnClickListener(onClickListeners[LISTENER_OTHER]); mImageViewClear = (ImageView) rootView.findViewById(R.id.iv_clear); mImageViewClear.setOnClickListener(onClickListeners[LISTENER_OTHER]); mTextViewGameNumber = (TextView) rootView.findViewById(R.id.tv_game_number); layoutParams = (RelativeLayout.LayoutParams) mTextViewGameNumber.getLayoutParams(); layoutParams.addRule(RelativeLayout.ALIGN_TOP, mTextViewNextBall.getId()); layoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, mTextViewNextBall.getId()); mImageViewFoul = (ImageView) rootView.findViewById(R.id.iv_foul); mImageViewFoul.setOnClickListener(onClickListeners[LISTENER_OTHER]); mImageViewResetFrame = (ImageView) rootView.findViewById(R.id.iv_reset_frame); mImageViewResetFrame.setOnClickListener(onClickListeners[LISTENER_OTHER]); mImageViewLock = (ImageView) rootView.findViewById(R.id.iv_lock); mImageViewLock.setOnClickListener(onClickListeners[LISTENER_OTHER]); mImageViewMatchPlay = (ImageView) rootView.findViewById(R.id.iv_match); mImageViewMatchPlay.setOnClickListener(onClickListeners[LISTENER_OTHER]); mTextViewManualScore = (TextView) rootView.findViewById(R.id.tv_manual_score); mTextViewAutoAdvance = (TextView) rootView.findViewById(R.id.tv_auto_advance_status); mNextFab = (AnimatedFloatingActionButton) rootView.findViewById(R.id.fab_next_ball); mNextFab.setVisibility(View.GONE); DisplayUtils.fixFloatingActionButtonMargins(getResources(), mNextFab); mNextFab.setOnClickListener(onClickListeners[LISTENER_OTHER]); mPrevFab = (AnimatedFloatingActionButton) rootView.findViewById(R.id.fab_prev_ball); mPrevFab.setVisibility(View.GONE); DisplayUtils.fixFloatingActionButtonMargins(getResources(), mPrevFab); mPrevFab.setOnClickListener(onClickListeners[LISTENER_OTHER]); return rootView; } @Override public void onResume() { super.onResume(); if (getActivity() != null) { MainActivity mainActivity = (MainActivity) getActivity(); mainActivity.setActionBarTitle(R.string.title_fragment_game, true); mainActivity.setFloatingActionButtonState(0, 0); mainActivity.createGameNavigationDrawer(); mainActivity.setDrawerState(true); } // Loads colors for frame view backgrounds mColorBackground = DisplayUtils.getColorResource(getResources(), R.color.primary_background); mColorHighlight = DisplayUtils.getColorResource(getResources(), R.color.secondary_background); // If values were not loaded from saved instance state, they are loaded here if (mGameIds == null) { Bundle args = getArguments(); mGameIds = args.getLongArray(Constants.EXTRA_ARRAY_GAME_IDS); mFrameIds = args.getLongArray(Constants.EXTRA_ARRAY_FRAME_IDS); mGameLocked = args.getBooleanArray(Constants.EXTRA_ARRAY_GAME_LOCKED); mManualScoreSet = args.getBooleanArray(Constants.EXTRA_ARRAY_MANUAL_SCORE_SET); } mGameScores = new short[((MainActivity) getActivity()).getNumberOfGamesForSeries()]; mGameScoresMinusFouls = new short[((MainActivity) getActivity()).getNumberOfGamesForSeries()]; updateTheme(); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); boolean autoAdvanceEnabled = preferences.getBoolean(Constants.KEY_ENABLE_AUTO_ADVANCE, false); autoLockEnabled = preferences.getBoolean(Constants.KEY_ENABLE_AUTO_LOCK, true); String strDelay = preferences.getString(Constants.KEY_AUTO_ADVANCE_TIME, "15 seconds"); int autoAdvanceDelay = Integer.valueOf(strDelay.substring(0, strDelay.indexOf(" "))); if (mGameCallback != null) { mGameCallback.setAutoAdvanceConditions(mImageViewNextBall, mTextViewAutoAdvance, autoAdvanceEnabled, autoAdvanceDelay); mGameCallback.stopAutoAdvanceTimer(); // Loads scores of games being edited from database loadInitialScores(); // Loads first game to edit loadGameFromDatabase(mCurrentGame); mGameCallback.loadGameScoresForDrawer(mGameIds); } DisplayUtils.hideKeyboard(getActivity()); new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { if (mCurrentFrame > 0 || mCurrentBall > 0) setFloatingActionButtonState(false, R.drawable.ic_chevron_left_black_24dp); else setFloatingActionButtonState(false, 0); if (mCurrentFrame < Constants.LAST_FRAME || mCurrentBall < 2) setFloatingActionButtonState(true, R.drawable.ic_chevron_right_black_24dp); else setFloatingActionButtonState(true, 0); } }, 500); } @Override public void onPause() { super.onPause(); autoLockHandler.removeCallbacks(autoLockGame); if (mNextFab != null) setFloatingActionButtonState(true, 0); if (mPrevFab != null) setFloatingActionButtonState(false, 0); mGameCallback.setAutoAdvanceConditions(mImageViewNextBall, mTextViewAutoAdvance, false, Integer.MAX_VALUE); // Clears color changes to frames and saves the game being edited clearFrameColor(); saveGame(false); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // Puts member variables into outState so they can be loaded back outState.putLongArray(Constants.EXTRA_ARRAY_GAME_IDS, mGameIds); outState.putLongArray(Constants.EXTRA_ARRAY_FRAME_IDS, mFrameIds); outState.putBooleanArray(Constants.EXTRA_ARRAY_GAME_LOCKED, mGameLocked); outState.putBooleanArray(Constants.EXTRA_ARRAY_MANUAL_SCORE_SET, mManualScoreSet); outState.putInt(Constants.EXTRA_CURRENT_GAME, mCurrentGame); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.menu_game, menu); } @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); // Sets names/visibility of menu items menu.findItem(R.id.action_event_stats).setVisible(((MainActivity) getActivity()).isEventMode()); menu.findItem(R.id.action_set_score) .setTitle((mManualScoreSet[mCurrentGame]) ? R.string.action_clear_score : R.string.action_set_score); boolean drawerOpen = ((MainActivity) getActivity()).isDrawerOpen(); MenuItem menuItem = menu.findItem(R.id.action_stats).setVisible(!drawerOpen); Drawable drawable = menuItem.getIcon(); if (drawable != null) drawable.setAlpha(DisplayUtils.BLACK_ICON_ALPHA); menuItem = menu.findItem(R.id.action_share).setVisible(!drawerOpen); drawable = menuItem.getIcon(); if (drawable != null) drawable.setAlpha(DisplayUtils.BLACK_ICON_ALPHA); menu.findItem(R.id.action_event_stats).setVisible(!drawerOpen); menu.findItem(R.id.action_reset_game).setVisible(!drawerOpen); menu.findItem(R.id.action_set_score).setVisible(!drawerOpen); menu.findItem(R.id.action_best_possible).setVisible( !mManualScoreSet[mCurrentGame] && !drawerOpen); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_share: // Prompts user to share or save an image of current series saveGame(false); MainActivity mainActivity = (MainActivity) getActivity(); ShareUtils.showShareDialog(mainActivity, mainActivity.getSeriesId()); return true; case R.id.action_set_score: // If a manual score is set, clear it // Otherwise, prompt for a new score from the user if (mManualScoreSet[mCurrentGame]) showClearManualScoreDialog(); else showManualScoreDialog(); return true; case R.id.action_event_stats: // Displays all stats related to series of games if (mGameCallback != null) mGameCallback.onSeriesStatsOpened(); return true; case R.id.action_reset_game: // If the game is locked, it cannot be reset // Otherwise, prompts user to reset if (mGameLocked[mCurrentGame] && !mManualScoreSet[mCurrentGame]) showGameLockedDialog(); else showResetGameDialog(); if (mGameCallback != null) mGameCallback.stopAutoAdvanceTimer(); return true; case R.id.action_best_possible: // Calculates possible score and displays showBestPossibleScoreDialog(); return true; case R.id.action_stats: // Displays all stats related to current game if (mGameCallback != null) mGameCallback.onGameStatsOpened(mGameIds[mCurrentGame], (byte) (mCurrentGame + 1)); return true; default: return super.onOptionsItemSelected(item); } } @Override public void updateTheme() { View rootView = getView(); if (rootView != null) { rootView.findViewById(R.id.rl_game_toolbar).setBackgroundColor( Theme.getSecondaryThemeColor()); } if (mNextFab != null) { DisplayUtils.setFloatingActionButtonColors(mNextFab, Theme.getPrimaryThemeColor(), Theme.getTertiaryThemeColor()); mNextFab.setEnabled(false); mNextFab.performClick(); mNextFab.setEnabled(true); } if (mPrevFab != null) { DisplayUtils.setFloatingActionButtonColors(mPrevFab, Theme.getPrimaryThemeColor(), Theme.getTertiaryThemeColor()); mPrevFab.setEnabled(false); mPrevFab.performClick(); mPrevFab.setEnabled(true); } } @Override public void onSetScore(short scoreToSet) { if (scoreToSet < 0 || scoreToSet > Constants.GAME_MAX_SCORE) { // If an invalid score is given, user is informed and method exists AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setTitle("Invalid score!") .setMessage(R.string.dialog_bad_score) .setPositiveButton(R.string.dialog_okay, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }) .create() .show(); return; } // Clears state of game and sets the manual score of the game resetGame(); setGameLocked(true); mManualScoreSet[mCurrentGame] = true; mGameScores[mCurrentGame] = scoreToSet; mGameScoresMinusFouls[mCurrentGame] = scoreToSet; clearAllText(false); getActivity().supportInvalidateOptionsMenu(); if (mGameCallback != null) mGameCallback.updateGameScore((byte) (mCurrentGame + 1), mGameScoresMinusFouls[mCurrentGame]); saveGame(true); } /** * Creates instances of OnClickListener to listen to events created by views in this activity. * * @return OnClickListener instances which are applied to views in this activity */ private View.OnClickListener[] getOnClickListeners() { View.OnClickListener[] listeners = new View.OnClickListener[3]; listeners[LISTENER_TEXT_FRAMES] = new View.OnClickListener() { @Override public void onClick(View v) { byte frameToSet = 0; switch (v.getId()) { case R.id.text_frame_9: frameToSet++; case R.id.text_frame_8: frameToSet++; case R.id.text_frame_7: frameToSet++; case R.id.text_frame_6: frameToSet++; case R.id.text_frame_5: frameToSet++; case R.id.text_frame_4: frameToSet++; case R.id.text_frame_3: frameToSet++; case R.id.text_frame_2: frameToSet++; case R.id.text_frame_1: frameToSet++; case R.id.text_frame_0: // Changes the current frame and updates the GUI clearFrameColor(); mCurrentFrame = frameToSet; mCurrentBall = 0; byte frameUpdateCount = 0; for (int i = mCurrentFrame; i >= 0; i--) { if (mHasFrameBeenAccessed[i]) break; mHasFrameBeenAccessed[i] = true; frameUpdateCount++; } setVisibilityOfNextAndPrevItems(); if (frameUpdateCount > 3) updateAllBalls(); else updateBalls(mCurrentFrame, (byte) 0); updateScore(); updateFrameColor(false); if (mGameCallback != null) mGameCallback.stopAutoAdvanceTimer(); break; default: throw new RuntimeException("Invalid frame id"); } } }; listeners[LISTENER_OTHER] = new View.OnClickListener() { @Override public void onClick(View v) { int viewId = v.getId(); switch (viewId) { case R.id.iv_match: if (mGameLocked[mCurrentGame]) showSetMatchPlayLockedDialog(); else showSetMatchPlayScreen(); if (mGameCallback != null) mGameCallback.stopAutoAdvanceTimer(); break; case R.id.iv_lock: // Locks the game if it is unlocked, // unlocks if locked if (mManualScoreSet[mCurrentGame]) return; setGameLocked(!mGameLocked[mCurrentGame]); if (mGameCallback != null) mGameCallback.stopAutoAdvanceTimer(); break; case R.id.iv_foul: // Sets or removes a foul and updates scores if (mGameLocked[mCurrentGame] || mManualScoreSet[mCurrentGame]) return; mFouls[mCurrentFrame][mCurrentBall] = !mFouls[mCurrentFrame][mCurrentBall]; updateFouls(); if (!(mCurrentFrame == Constants.LAST_FRAME && mCurrentBall == 2)) { if (mGameCallback != null) { mGameCallback.resetAutoAdvanceTimer(); } } else { startAutoLockTimer(); } break; case R.id.iv_reset_frame: // Resets the current frame to ball 0, no fouls, no pins knocked if (mGameLocked[mCurrentGame] || mManualScoreSet[mCurrentGame]) return; clearFrameColor(); mCurrentBall = 0; for (int i = 0; i < 3; i++) { mFouls[mCurrentFrame][i] = false; for (int j = 0; j < 5; j++) mPinState[mCurrentFrame][i][j] = false; } setVisibilityOfNextAndPrevItems(); updateFrameColor(false); updateBalls(mCurrentFrame, (byte) 0); updateScore(); if (mGameCallback != null) mGameCallback.stopAutoAdvanceTimer(); break; case R.id.iv_clear: clearPins(); if (!(mCurrentFrame == Constants.LAST_FRAME && mCurrentBall == 2)) { if (mGameCallback != null) { mGameCallback.resetAutoAdvanceTimer(); } } else { startAutoLockTimer(); } break; case R.id.iv_next_ball: case R.id.tv_next_ball: case R.id.fab_next_ball: // Changes the current frame and updates the GUI if (mCurrentFrame == Constants.LAST_FRAME && mCurrentBall == 2) return; clearFrameColor(); if (Arrays.equals(mPinState[mCurrentFrame][mCurrentBall], Constants.FRAME_PINS_DOWN)) { if (mCurrentFrame < Constants.LAST_FRAME) { mCurrentBall = 0; mCurrentFrame++; } else if (mCurrentBall < 2) { mCurrentBall++; } } else if (++mCurrentBall == 3) { mCurrentBall = 0; ++mCurrentFrame; } mHasFrameBeenAccessed[mCurrentFrame] = true; setVisibilityOfNextAndPrevItems(); updateFrameColor(false); if (mGameCallback != null) mGameCallback.stopAutoAdvanceTimer(); break; case R.id.iv_prev_ball: case R.id.tv_prev_ball: case R.id.fab_prev_ball: // Changes the current frame and updates the GUI if (mCurrentFrame == 0 && mCurrentBall == 0) return; clearFrameColor(); if (--mCurrentBall == -1) { mCurrentBall = 0; --mCurrentFrame; while (!Arrays.equals(mPinState[mCurrentFrame][mCurrentBall], Constants.FRAME_PINS_DOWN) && mCurrentBall < 2) { mCurrentBall++; } } setVisibilityOfNextAndPrevItems(); updateFrameColor(false); if (mGameCallback != null) mGameCallback.stopAutoAdvanceTimer(); break; default: throw new RuntimeException("Unknown other button id"); } } }; listeners[LISTENER_TEXT_BALLS] = new View.OnClickListener() { @Override public void onClick(View v) { boolean viewFound = false; for (byte i = 0; i < mTextViewBallScores.length && !viewFound; i++) { for (byte j = 0; j < mTextViewBallScores[i].length; j++) { if (v == mTextViewBallScores[i][j]) { viewFound = true; // Changes the current frame and updates the GUI clearFrameColor(); mCurrentFrame = i; mCurrentBall = 0; byte frameUpdateCount = 0; for (int k = mCurrentFrame; k >= 0; k--) { if (mHasFrameBeenAccessed[k]) break; mHasFrameBeenAccessed[k] = true; frameUpdateCount++; } while (!Arrays.equals(mPinState[mCurrentFrame][mCurrentBall], Constants.FRAME_PINS_DOWN) && mCurrentBall < j) { mCurrentBall++; } setVisibilityOfNextAndPrevItems(); if (frameUpdateCount > 3) updateAllBalls(); else updateBalls(mCurrentFrame, (byte) 0); updateScore(); updateFrameColor(false); if (mGameCallback != null) mGameCallback.stopAutoAdvanceTimer(); break; } } } } }; return listeners; } /** * Displays prompt to user to inform them the game is locked. Included to prevent accidental changes of match play * results, but still allow changes if game is locked (such as by a manual score being set) */ private void showSetMatchPlayLockedDialog() { new AlertDialog.Builder(getContext()) .setTitle("Game is locked") .setMessage(R.string.dialog_match_locked) .setPositiveButton(R.string.dialog_okay, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { showSetMatchPlayScreen(); dialog.dismiss(); } }) .setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }) .create() .show(); } /** * Prompts user to set results of match play for a game. */ private void showSetMatchPlayScreen() { MainActivity mainActivity = (MainActivity) getActivity(); if (mainActivity != null) mainActivity.openMatchPlayStats(mGameIds[mCurrentGame]); } /** * Prompts user to reset the current game and set a manual score */ private void showManualScoreDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setTitle("Set manual score?") .setMessage(R.string.dialog_set_score) .setPositiveButton(R.string.dialog_okay, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); DialogFragment dialogFragment = ManualScoreDialog.newInstance(GameFragment.this); dialogFragment.show(getFragmentManager(), "ManualScoreDialog"); } }) .setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }) .create() .show(); } /** * Prompts user to reset the current game and remove a manual score. */ private void showClearManualScoreDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setTitle("Clear the set score?") .setMessage(R.string.dialog_clear_score) .setPositiveButton(R.string.dialog_okay, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Unlocks and resets game setGameLocked(false); mManualScoreSet[mCurrentGame] = false; resetGame(); clearAllText(true); updateScore(); updateAllBalls(); setVisibilityOfNextAndPrevItems(); updateFrameColor(false); dialog.dismiss(); } }) .setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }) .create() .show(); } /** * Hides or shows TextView objects in the app that display individual score elements. * * @param enabled if true, TextView objects related to scores will be shown */ private void clearAllText(boolean enabled) { View rootView = getView(); if (rootView == null) return; SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); boolean fabsEnabled = preferences.getBoolean(Constants.KEY_ENABLE_FAB, true); if (enabled) { mImageViewNextBall.setVisibility(View.VISIBLE); mImageViewPrevBall.setVisibility(View.VISIBLE); mTextViewNextBall.setVisibility(View.VISIBLE); mTextViewPrevBall.setVisibility(View.VISIBLE); mHorizontalScrollViewFrames.setVisibility(View.VISIBLE); for (ImageView imageView : mImageViewPins) imageView.setVisibility(View.VISIBLE); mTextViewManualScore.setText(null); mTextViewManualScore.setVisibility(View.INVISIBLE); mLinearLayoutPins.setEnabled(true); View pinsLayout = rootView.findViewById(R.id.ll_pins); RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) pinsLayout.getLayoutParams(); if (fabsEnabled && preferences.getBoolean(Constants.KEY_PINS_BEHIND_FABS, false)) layoutParams.addRule(RelativeLayout.ABOVE, R.id.iv_lock); else layoutParams.addRule(RelativeLayout.ABOVE, R.id.fab_container); pinsLayout.setLayoutParams(layoutParams); } else { mImageViewNextBall.setVisibility(View.INVISIBLE); mImageViewPrevBall.setVisibility(View.INVISIBLE); mTextViewNextBall.setVisibility(View.INVISIBLE); mTextViewPrevBall.setVisibility(View.INVISIBLE); mHorizontalScrollViewFrames.setVisibility(View.INVISIBLE); for (ImageView imageView : mImageViewPins) imageView.setVisibility(View.INVISIBLE); mTextViewManualScore.setText(String.valueOf(mGameScoresMinusFouls[mCurrentGame])); mTextViewManualScore.setVisibility(View.VISIBLE); mLinearLayoutPins.setEnabled(false); } rootView.findViewById(R.id.fab_container).setVisibility((fabsEnabled && enabled) ? View.VISIBLE : View.GONE); mImageViewLock.setImageResource((mGameLocked[mCurrentGame]) ? R.drawable.ic_lock : R.drawable.ic_lock_open); } private void setToolbarEnabled(boolean enabled) { if (enabled) { mImageViewClear.setVisibility(View.VISIBLE); mImageViewFoul.setVisibility(View.VISIBLE); mImageViewResetFrame.setVisibility(View.VISIBLE); } else { mImageViewClear.setVisibility(View.INVISIBLE); mImageViewFoul.setVisibility(View.INVISIBLE); mImageViewResetFrame.setVisibility(View.INVISIBLE); } } /** * Informs user with prompt that the game is current locked. */ private void showGameLockedDialog() { new AlertDialog.Builder(getContext()) .setTitle("Invalid action!") .setMessage(R.string.dialog_game_locked) .setPositiveButton(R.string.dialog_okay, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }) .create() .show(); } /** * Prompts user to reset the current game. */ private void showResetGameDialog() { new AlertDialog.Builder(getContext()) .setTitle("Reset Game?") .setMessage(R.string.dialog_reset_game) .setPositiveButton(R.string.dialog_okay, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Resets and saves the current game resetGame(); saveGame(true); setGameLocked(false); mMatchPlay = 0; mManualScoreSet[mCurrentGame] = false; clearAllText(true); updateScore(); updateAllBalls(); setMatchPlay(); getActivity().supportInvalidateOptionsMenu(); setVisibilityOfNextAndPrevItems(); updateFrameColor(false); dialog.dismiss(); } }) .setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }) .create() .show(); } /** * Sets mImageViewMatchPlay image resource depending on match play results set by user. */ private void setMatchPlay() { getActivity().runOnUiThread(new Runnable() { @Override public void run() { switch (mMatchPlay) { case Constants.MATCH_PLAY_NONE: mImageViewMatchPlay.setImageResource(R.drawable.ic_match_none); break; case Constants.MATCH_PLAY_WON: mImageViewMatchPlay.setImageResource(R.drawable.ic_match_win); break; case Constants.MATCH_PLAY_LOST: mImageViewMatchPlay.setImageResource(R.drawable.ic_match_lose); break; case Constants.MATCH_PLAY_TIED: mImageViewMatchPlay.setImageResource(R.drawable.ic_match_tie); break; default: // do nothing } } }); } /** * Starts a timer that will lock the current game when the timer runs out. */ private void startAutoLockTimer() { autoLockHandler.removeCallbacks(autoLockGame); if (autoLockEnabled) { autoLockHandler.postDelayed(autoLockGame, AUTO_LOCK_DELAY); } } /** * Locks or unlocks a game and hides/shows settings which are only necessary if a game is unlocked. * * @param lock if true, settings will be hidden and game locked */ private void setGameLocked(boolean lock) { mGameLocked[mCurrentGame] = lock; getActivity().runOnUiThread(new Runnable() { @Override public void run() { setToolbarEnabled(!mGameLocked[mCurrentGame]); mImageViewLock.setImageResource((mGameLocked[mCurrentGame]) ? R.drawable.ic_lock : R.drawable.ic_lock_open); } }); } /** * Copies data of current game to variables and saves game to the database on a new thread. * * @param ignoreManualScore if false, game will only be saved if a manual score is not set. Otherwise, will save * regardless. */ private void saveGame(boolean ignoreManualScore) { if ((!ignoreManualScore && mManualScoreSet[mCurrentGame]) || mDoNotSave.get()) return; long[] framesToSave = new long[Constants.NUMBER_OF_FRAMES]; boolean[] accessToSave = new boolean[Constants.NUMBER_OF_FRAMES]; boolean[][][] pinStateToSave = new boolean[Constants.NUMBER_OF_FRAMES][3][5]; boolean[][] foulsToSave = new boolean[Constants.NUMBER_OF_FRAMES][3]; System.arraycopy( mFrameIds, mCurrentGame * Constants.NUMBER_OF_FRAMES, framesToSave, 0, Constants.NUMBER_OF_FRAMES); System.arraycopy( mHasFrameBeenAccessed, 0, accessToSave, 0, Constants.NUMBER_OF_FRAMES); for (byte i = 0; i < Constants.NUMBER_OF_FRAMES; i++) { for (byte j = 0; j < mPinState[i].length; j++) { System.arraycopy( mPinState[i][j], 0, pinStateToSave[i][j], 0, mPinState[i][j].length); } System.arraycopy(mFouls[i], 0, foulsToSave[i], 0, mFouls[i].length); } ((MainActivity) getActivity()).addSavingThread( saveGameToDatabase((MainActivity) getActivity(), mGameIds[mCurrentGame], framesToSave, accessToSave, pinStateToSave, foulsToSave, mGameScoresMinusFouls[mCurrentGame], mGameLocked[mCurrentGame], mManualScoreSet[mCurrentGame], mMatchPlay)); } /** * Resets a game to its original state with 0 score, 0 fouls. */ private void resetGame() { clearFrameColor(); mCurrentBall = 0; mCurrentFrame = 0; for (int i = 0; i < Constants.NUMBER_OF_FRAMES; i++) { mHasFrameBeenAccessed[i] = false; for (int j = 0; j < 3; j++) { mFouls[i][j] = false; for (int k = 0; k < 5; k++) mPinState[i][j][k] = false; } } mHasFrameBeenAccessed[0] = true; mGameScores[mCurrentGame] = 0; mGameScoresMinusFouls[mCurrentGame] = 0; } /** * Calculates the highest score possible from the current state of the game and displays it in a dialog to the * user. */ private void showBestPossibleScoreDialog() { byte initialFrame = mCurrentFrame; while (initialFrame > 0 && TextUtils.isEmpty(mTextViewFrames[initialFrame].getText().toString())) initialFrame--; StringBuilder alertMessageBuilder = new StringBuilder("If you get"); short possibleScore; try { possibleScore = Short.parseShort(mTextViewFrames[initialFrame].getText().toString()); } catch (NumberFormatException ex) { possibleScore = 0; } if (mCurrentFrame < Constants.LAST_FRAME) { if (Arrays.equals(mPinState[mCurrentFrame][0], Constants.FRAME_PINS_DOWN)) { int firstBallNextFrame = Score.getValueOfFrame(mPinState[mCurrentFrame + 1][0], true); possibleScore -= firstBallNextFrame; if (firstBallNextFrame == 15) { if (mCurrentFrame < Constants.LAST_FRAME - 1) possibleScore -= Score.getValueOfFrame(mPinState[mCurrentFrame + 2][0], true); else possibleScore -= Score.getValueOfFrame(mPinState[mCurrentFrame + 1][1], true); } else possibleScore -= Score.getValueOfFrameDifference(mPinState[mCurrentFrame][0], mPinState[mCurrentFrame][1]); } else if (Arrays.equals(mPinState[mCurrentFrame][1], Constants.FRAME_PINS_DOWN)) { int firstBallNextFrame = Score.getValueOfFrame(mPinState[mCurrentFrame + 1][0], true); possibleScore -= firstBallNextFrame; } } else { for (int i = mCurrentBall + 1; i < 3; i++) possibleScore -= Score.getValueOfFrameDifference(mPinState[mCurrentFrame][i - 1], mPinState[mCurrentFrame][i]); } int pinsLeftStanding = Score.getValueOfFrame(mPinState[mCurrentFrame][mCurrentBall], false); boolean strikeLastFrame = false; boolean strikeTwoFramesAgo = false; boolean spareLastFrame = false; if (mCurrentFrame > 0) { if (Arrays.equals(mPinState[mCurrentFrame - 1][0], Constants.FRAME_PINS_DOWN)) { strikeLastFrame = true; if (mCurrentFrame > 1 && Arrays.equals( mPinState[mCurrentFrame - 2][0], Constants.FRAME_PINS_DOWN)) strikeTwoFramesAgo = true; } else { if (Arrays.equals(mPinState[mCurrentFrame - 1][1], Constants.FRAME_PINS_DOWN)) spareLastFrame = true; } } if (mCurrentBall == 0) { alertMessageBuilder.append(" a strike"); possibleScore += pinsLeftStanding + 30; int secondBall = Score.getValueOfFrameDifference(mPinState[mCurrentFrame][0], mPinState[mCurrentFrame][1]); int thirdBall = Score.getValueOfFrameDifference(mPinState[mCurrentFrame][1], mPinState[mCurrentFrame][2]); if (mCurrentFrame < Constants.LAST_FRAME) possibleScore -= secondBall + thirdBall; if (strikeLastFrame) { possibleScore -= secondBall; possibleScore += pinsLeftStanding + 15; if (strikeTwoFramesAgo) possibleScore += pinsLeftStanding; } else if (spareLastFrame) possibleScore += pinsLeftStanding; } else if (mCurrentBall == 1) { int thirdBall = Score.getValueOfFrameDifference(mPinState[mCurrentFrame][1], mPinState[mCurrentFrame][2]); if (mCurrentFrame < Constants.LAST_FRAME) possibleScore -= thirdBall; if (mCurrentFrame == Constants.LAST_FRAME && Arrays.equals(mPinState[mCurrentFrame][0], Constants.FRAME_PINS_DOWN)) alertMessageBuilder.append(" a strike"); else alertMessageBuilder.append(" a spare"); possibleScore += pinsLeftStanding + 15; if (strikeLastFrame) possibleScore += pinsLeftStanding; } else { if (mCurrentFrame == Constants.LAST_FRAME && Arrays.equals(mPinState[mCurrentFrame][1], Constants.FRAME_PINS_DOWN)) alertMessageBuilder.append(" a strike"); else alertMessageBuilder.append(" fifteen"); possibleScore += pinsLeftStanding; } possibleScore += 45 * (Constants.LAST_FRAME - mCurrentFrame); for (int i = 0; i <= mCurrentFrame; i++) { for (int j = 0; j < 3 && !(i == mCurrentFrame && j >= mCurrentBall); j++) { if (mFouls[i][j]) possibleScore -= 15; } } if (possibleScore < 0) possibleScore = 0; alertMessageBuilder.append(" this ball, and strikes onwards, your final score will be "); alertMessageBuilder.append(possibleScore); AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setMessage(alertMessageBuilder.toString()) .setPositiveButton(R.string.dialog_okay, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); builder.create() .show(); } /** * Sets the visibility of text and image views which indicate available navigation options in the fragment, * depending on whether they are available at the time. */ private void setVisibilityOfNextAndPrevItems() { getActivity().runOnUiThread(new Runnable() { @Override public void run() { if (mCurrentFrame == 0 && mCurrentBall == 0) { mTextViewPrevBall.setVisibility(View.GONE); mImageViewPrevBall.setVisibility(View.GONE); setFloatingActionButtonState(false, 0); RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) mTextViewGameNumber.getLayoutParams(); layoutParams.addRule(RelativeLayout.ALIGN_TOP, mTextViewNextBall.getId()); layoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, mTextViewNextBall.getId()); } else { mTextViewPrevBall.setVisibility(View.VISIBLE); mImageViewPrevBall.setVisibility(View.VISIBLE); setFloatingActionButtonState(false, R.drawable.ic_chevron_left_black_24dp); } if (mCurrentFrame == Constants.LAST_FRAME && mCurrentBall == 2) { mTextViewNextBall.setVisibility(View.GONE); mImageViewNextBall.setVisibility(View.GONE); setFloatingActionButtonState(true, 0); RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) mTextViewGameNumber.getLayoutParams(); layoutParams.addRule(RelativeLayout.ALIGN_TOP, mTextViewPrevBall.getId()); layoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, mTextViewPrevBall.getId()); } else { mTextViewNextBall.setVisibility(View.VISIBLE); mImageViewNextBall.setVisibility(View.VISIBLE); setFloatingActionButtonState(true, R.drawable.ic_chevron_right_black_24dp); } } }); } /** * Sets text of all TextView instances which display individual ball values. */ private void updateAllBalls() { for (byte i = Constants.LAST_FRAME; i >= 0; i -= 3) updateBalls(i, (byte) 0); } /** * Sets the text of the three TextView instances which correspond to frameToUpdate. * * @param frameToUpdate frame of which text should be updated * @param leftToUpdate number of frames to the left to update */ private void updateBalls(final byte frameToUpdate, final byte leftToUpdate) { if (frameToUpdate < 0 || frameToUpdate > Constants.LAST_FRAME) return; new Thread(new Runnable() { @Override public void run() { // Sets text depending on state of pins in the frame final String[] ballString = new String[3]; if (mHasFrameBeenAccessed[frameToUpdate]) { if (frameToUpdate == Constants.LAST_FRAME) // Treat last frame differently { if (Arrays.equals(mPinState[frameToUpdate][0], Constants.FRAME_PINS_DOWN)) { // If first ball is a strike, next two can be strikes/spares ballString[0] = Constants.BALL_STRIKE; if (Arrays.equals(mPinState[frameToUpdate][1], Constants.FRAME_PINS_DOWN)) { ballString[1] = Constants.BALL_STRIKE; ballString[2] = Score.getValueOfBall( mPinState[frameToUpdate][2], 2, true, false); } else { ballString[1] = Score.getValueOfBall( mPinState[frameToUpdate][1], 1, false, false); if (Arrays.equals(mPinState[frameToUpdate][2], Constants.FRAME_PINS_DOWN)) ballString[2] = Constants.BALL_SPARE; else ballString[2] = Score.getValueOfBallDifference( mPinState[frameToUpdate], 2, false, false); } } else { // If first ball is not a strike, score is calculated normally ballString[0] = Score.getValueOfBall( mPinState[frameToUpdate][0], 0, false, false); if (Arrays.equals(mPinState[frameToUpdate][1], Constants.FRAME_PINS_DOWN)) { ballString[1] = Constants.BALL_SPARE; ballString[2] = Score.getValueOfBall( mPinState[frameToUpdate][2], 2, true, true); } else { ballString[1] = Score.getValueOfBallDifference( mPinState[frameToUpdate], 1, false, false); ballString[2] = Score.getValueOfBallDifference( mPinState[frameToUpdate], 2, false, false); } } } else { ballString[0] = Score.getValueOfBallDifference( mPinState[frameToUpdate], 0, false, false); if (!Arrays.equals(mPinState[frameToUpdate][0], Constants.FRAME_PINS_DOWN)) { if (Arrays.equals(mPinState[frameToUpdate][1], Constants.FRAME_PINS_DOWN)) { ballString[1] = Constants.BALL_SPARE; ballString[2] = (mHasFrameBeenAccessed[frameToUpdate + 1]) ? Score.getValueOfBallDifference( mPinState[frameToUpdate + 1], 0, false, true) : Constants.BALL_EMPTY; } else { ballString[1] = Score.getValueOfBallDifference( mPinState[frameToUpdate], 1, false, false); ballString[2] = Score.getValueOfBallDifference( mPinState[frameToUpdate], 2, false, false); } } else { // Either displays pins knocked down in next frames // or shows empty frames if (mHasFrameBeenAccessed[frameToUpdate + 1]) { ballString[1] = Score.getValueOfBallDifference( mPinState[frameToUpdate + 1], 0, false, true); if (Arrays.equals(mPinState[frameToUpdate + 1][0], Constants.FRAME_PINS_DOWN)) { if (frameToUpdate < Constants.LAST_FRAME - 1) { ballString[2] = (mHasFrameBeenAccessed[frameToUpdate + 2]) ? Score.getValueOfBallDifference( mPinState[frameToUpdate + 2], 0, false, true) : Constants.BALL_EMPTY; } else { ballString[2] = Score.getValueOfBall( mPinState[frameToUpdate + 1][1], 1, false, true); } } else { ballString[2] = Score.getValueOfBallDifference( mPinState[frameToUpdate + 1], 1, false, true); } } else { ballString[1] = Constants.BALL_EMPTY; ballString[2] = Constants.BALL_EMPTY; } } } } else { ballString[0] = ""; ballString[1] = ""; ballString[2] = ""; } getActivity().runOnUiThread(new Runnable() { @Override public void run() { boolean highlightStrikesAndSpares = PreferenceManager.getDefaultSharedPreferences(getContext()) .getBoolean(Constants.KEY_ENABLE_STRIKE_HIGHLIGHTS, true); for (byte i = 0; i < 3; i++) { mTextViewBallScores[frameToUpdate][i].setText(ballString[i]); if (highlightStrikesAndSpares) { if (frameToUpdate == Constants.LAST_FRAME && Arrays.equals(mPinState[frameToUpdate][i], Constants.FRAME_PINS_DOWN) && !(i == 2 && !Arrays.equals(mPinState[frameToUpdate][0], Constants.FRAME_PINS_DOWN) && !Arrays.equals(mPinState[frameToUpdate][1], Constants.FRAME_PINS_DOWN))) mTextViewBallScores[frameToUpdate][i].setTextColor(Theme.getTertiaryThemeColor()); else if ((i == 0 && Arrays.equals(mPinState[frameToUpdate][i], Constants.FRAME_PINS_DOWN)) || (i == 1 && Arrays.equals(mPinState[frameToUpdate][i], Constants.FRAME_PINS_DOWN) && !Arrays.equals(mPinState[frameToUpdate][0], Constants.FRAME_PINS_DOWN))) mTextViewBallScores[frameToUpdate][i].setTextColor(Theme.getTertiaryThemeColor()); else mTextViewBallScores[frameToUpdate][i].setTextColor(DisplayUtils.COLOR_BLACK); } else { mTextViewBallScores[frameToUpdate][i].setTextColor(DisplayUtils.COLOR_BLACK); } mTextViewFouls[frameToUpdate][i].setText( (mFouls[frameToUpdate][i]) ? "F" : null); } } }); // Updates previous frames as well, to display balls after strikes switch (leftToUpdate) { case 0: updateBalls((byte) (frameToUpdate - 1), (byte) (leftToUpdate + 1)); break; case 1: updateBalls((byte) (frameToUpdate - 1), (byte) (leftToUpdate + 1)); break; default: // do nothing } } }).start(); } /** * Sets text of the TextView instances which display the score up to the frame to the user. */ private void updateScore() { new Thread(new Runnable() { @Override public void run() { // Calculates and keeps running total of scores of each frame final short[] frameScores = new short[Constants.NUMBER_OF_FRAMES]; for (byte f = Constants.LAST_FRAME; f >= 0; f--) { if (f == Constants.LAST_FRAME) { for (byte b = 2; b >= 0; b--) { switch (b) { case 2: frameScores[f] += Score.getValueOfFrame(mPinState[f][b], true); break; case 1: case 0: if (Arrays.equals(mPinState[f][b], Constants.FRAME_PINS_DOWN)) { frameScores[f] += Score.getValueOfFrame(mPinState[f][b], true); } break; default: // do nothing } } } else { for (byte b = 0; b < 3; b++) { if (b < 2 && Arrays.equals(mPinState[f][b], Constants.FRAME_PINS_DOWN)) { frameScores[f] += Score.getValueOfFrame(mPinState[f][b], true); frameScores[f] += Score.getValueOfFrame(mPinState[f + 1][0], true); if (b == 0) { if (f == Constants.LAST_FRAME - 1) { if (frameScores[f] == 30) { frameScores[f] += Score.getValueOfFrame(mPinState[f + 1][1], true); } else { frameScores[f] += Score.getValueOfFrameDifference( mPinState[f + 1][0], mPinState[f + 1][1]); } } else if (frameScores[f] < 30) { frameScores[f] += Score.getValueOfFrameDifference( mPinState[f + 1][0], mPinState[f + 1][1]); } else { frameScores[f] += Score.getValueOfFrame(mPinState[f + 2][0], true); } } break; } else if (b == 2) { frameScores[f] += Score.getValueOfFrame(mPinState[f][b], true); } } } } short totalScore = 0; for (byte i = 0; i < frameScores.length; i++) { totalScore += frameScores[i]; frameScores[i] = totalScore; } mGameScores[mCurrentGame] = totalScore; getActivity().runOnUiThread(new Runnable() { @Override public void run() { // Sets scores calculated from running total as text of TextViews for (byte i = 0; i < frameScores.length; i++) { mTextViewFrames[i].setText((mHasFrameBeenAccessed[i] ? String.valueOf(frameScores[i]) : "")); } } }); updateFouls(); } }).start(); } /** * Counts fouls of the frames and calculates scores minus 15 points for each foul, then sets score in last * TextView. */ private void updateFouls() { byte foulCount = 0; for (byte i = 0; i < Constants.NUMBER_OF_FRAMES; i++) { for (int j = 0; j < 3; j++) { if (mFouls[i][j]) foulCount++; } } short scoreWithFouls = (short) (mGameScores[mCurrentGame] - 15 * foulCount); if (scoreWithFouls < 0) scoreWithFouls = 0; mGameScoresMinusFouls[mCurrentGame] = scoreWithFouls; getActivity().runOnUiThread(new Runnable() { @Override public void run() { mTextViewFinalScore.setText(String.valueOf(mGameScoresMinusFouls[mCurrentGame])); mImageViewFoul.setImageResource( !mFouls[mCurrentFrame][mCurrentBall] ? R.drawable.ic_foul_remove : R.drawable.ic_foul); mTextViewFouls[mCurrentFrame][mCurrentBall] .setText(mFouls[mCurrentFrame][mCurrentBall] ? "F" : ""); if (mGameCallback != null) mGameCallback.updateGameScore((byte) (mCurrentGame + 1), mGameScoresMinusFouls[mCurrentGame]); } }); } /** * Sets background color of current ball and frame TextView instances to mColorBackground. */ private void clearFrameColor() { getActivity().runOnUiThread(new Runnable() { @Override public void run() { GradientDrawable drawable = (GradientDrawable) mTextViewBallScores[mCurrentFrame][mCurrentBall].getBackground(); drawable.setColor(mColorBackground); drawable = (GradientDrawable) mTextViewFrames[mCurrentFrame].getBackground(); drawable.setColor(mColorBackground); } }); } /** * Sets background color of current ball and frame TextView instances to mColorHighlight and sets color of pin and * whether its enabled or not depending on its state. * * @param initialLoad indicates if this method was called when a game was first loaded */ private void updateFrameColor(final boolean initialLoad) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { GradientDrawable drawable = (GradientDrawable) mTextViewBallScores[mCurrentFrame][mCurrentBall].getBackground(); drawable.setColor(mColorHighlight); drawable = (GradientDrawable) mTextViewFrames[mCurrentFrame].getBackground(); drawable.setColor(mColorHighlight); // Sets images of pins to enabled/disabled depending on // if they were knocked down in current frame or a previous one int numberOfPinsStanding = 0; for (byte i = 0; i < 5; i++) { if (mPinState[mCurrentFrame][mCurrentBall][i]) mImageViewPins[i].setImageResource(R.drawable.pin_disabled); else { mImageViewPins[i].setImageResource(R.drawable.pin_enabled); numberOfPinsStanding++; } if (mCurrentBall > 0 && (mPinState[mCurrentFrame][mCurrentBall - 1][i]) && !(mCurrentFrame == Constants.LAST_FRAME && Arrays.equals(mPinState[mCurrentFrame][mCurrentBall - 1], Constants.FRAME_PINS_DOWN))) mImageViewPins[i].setEnabled(false); else mImageViewPins[i].setEnabled(true); } if (mCurrentFrame == Constants.LAST_FRAME) { switch (mCurrentBall) { case 0: mImageViewClear.setImageResource(R.drawable.ic_strike); break; case 1: if (Arrays.equals(mPinState[mCurrentFrame][0], Constants.FRAME_PINS_DOWN)) mImageViewClear.setImageResource(R.drawable.ic_strike); else mImageViewClear.setImageResource(R.drawable.ic_spare); break; case 2: if (Arrays.equals(mPinState[mCurrentFrame][1], Constants.FRAME_PINS_DOWN)) mImageViewClear.setImageResource(R.drawable.ic_strike); else if (Arrays.equals(mPinState[mCurrentFrame][0], Constants.FRAME_PINS_DOWN)) mImageViewClear.setImageResource(R.drawable.ic_spare); else mImageViewClear.setImageResource(R.drawable.ic_fifteen); break; default: // do nothing } } else { switch (mCurrentBall) { case 0: mImageViewClear.setImageResource(R.drawable.ic_strike); break; case 1: mImageViewClear.setImageResource(R.drawable.ic_spare); break; case 2: mImageViewClear.setImageResource(R.drawable.ic_fifteen); break; default: // do nothing } } mImageViewClear.setEnabled(numberOfPinsStanding > 0); mImageViewFoul.setImageResource( !mFouls[mCurrentFrame][mCurrentBall] ? R.drawable.ic_foul_remove : R.drawable.ic_foul); focusOnFrame(initialLoad); } }); } /** * Either sets a pin to be standing or knocked down, and updates the score accordingly. * * @param pinToSet the pin which was altered * @param updateScore indicates if score and text views should be updated. If altering more than one pin at a time, * this should only be true for the last call. */ private void alterPinState(final byte pinToSet, final boolean updateScore) { final boolean isPinKnockedOver = mPinState[mCurrentFrame][mCurrentBall][pinToSet]; final boolean allPinsKnockedOver; if (!isPinKnockedOver) { for (int i = mCurrentBall; i < 3; i++) mPinState[mCurrentFrame][i][pinToSet] = true; if (Arrays.equals(mPinState[mCurrentFrame][mCurrentBall], Constants.FRAME_PINS_DOWN)) { for (int i = mCurrentBall + 1; i < 3; i++) mFouls[mCurrentFrame][i] = false; if (mCurrentFrame == Constants.LAST_FRAME) { if (mCurrentBall < 2) { for (int j = mCurrentBall + 1; j < 3; j++) { for (int i = 0; i < 5; i++) mPinState[mCurrentFrame][j][i] = false; } } } allPinsKnockedOver = true; } else allPinsKnockedOver = false; } else { allPinsKnockedOver = false; for (int i = mCurrentBall; i < 3; i++) mPinState[mCurrentFrame][i][pinToSet] = false; if (mCurrentFrame == Constants.LAST_FRAME) { if (mCurrentBall == 1) { System.arraycopy(mPinState[mCurrentFrame][1], 0, mPinState[mCurrentFrame][2], 0, mPinState[mCurrentFrame][1].length); } else if (mCurrentBall == 0 && Score.countStandingPins(mPinState[mCurrentFrame][mCurrentBall]) == 4) { System.arraycopy(mPinState[mCurrentFrame][0], 0, mPinState[mCurrentFrame][1], 0, mPinState[mCurrentFrame][0].length); System.arraycopy(mPinState[mCurrentFrame][0], 0, mPinState[mCurrentFrame][2], 0, mPinState[mCurrentFrame][0].length); } } } mImageViewPins[pinToSet].post(new Runnable() { @Override public void run() { if (mPinState[mCurrentFrame][mCurrentBall][pinToSet]) mImageViewPins[pinToSet].setImageResource(R.drawable.pin_disabled); else mImageViewPins[pinToSet].setImageResource(R.drawable.pin_enabled); mImageViewClear.setEnabled(!allPinsKnockedOver); } }); if (updateScore) { updateBalls(mCurrentFrame, (byte) 0); updateScore(); } } /** * Clears all the pins which are currently standing in the frame and updates the TextViews with new score. */ private void clearPins() { if (mGameLocked[mCurrentGame] || mManualScoreSet[mCurrentGame]) return; if (!Arrays.equals(mPinState[mCurrentFrame][mCurrentBall], Constants.FRAME_PINS_DOWN)) { for (int j = mCurrentBall; j < 3; j++) { for (int i = 0; i < 5; i++) { mPinState[mCurrentFrame][j][i] = (mCurrentFrame != Constants.LAST_FRAME || (j == mCurrentBall)); if (j > mCurrentBall) mFouls[mCurrentFrame][j] = false; } } updateBalls(mCurrentFrame, (byte) 0); updateScore(); updateFrameColor(false); } } /** * Scrolls the position of hsvFrames so the current frame is centred. * * @param initialLoad indicates if this method was called when a game was first loaded */ private void focusOnFrame(final boolean initialLoad) { mHorizontalScrollViewFrames.post(new Runnable() { @Override public void run() { if (initialLoad && mCurrentFrame == Constants.LAST_FRAME) mHorizontalScrollViewFrames .smoothScrollTo(mTextViewFrames[mCurrentFrame].getLeft(), 0); else if (mCurrentFrame >= 1) mHorizontalScrollViewFrames .smoothScrollTo(mTextViewFrames[mCurrentFrame - 1].getLeft(), 0); else mHorizontalScrollViewFrames .smoothScrollTo(mTextViewFrames[mCurrentFrame].getLeft(), 0); } }); } /** * Saves a games score and individual frames to the database on a separate thread. * * @param srcActivity activity which called the method to get instance of database * @param gameId id of the game to be updated * @param frameIds ids of the frames to be updated * @param hasFrameBeenAccessed state of whether frames have been accessed or not * @param pinState state of pins after each ball * @param fouls indicates whether a foul was invoked on each ball * @param finalScore final score of the game, considering fouls * @param gameLocked indicates if the game is locked * @param manualScoreSet indicates if a manual score for the game is set * @param matchPlay indicates the result of match play for the game * @return a new thread which is saving a game. Thread is not started */ private static Thread saveGameToDatabase( final MainActivity srcActivity, final long gameId, final long[] frameIds, final boolean[] hasFrameBeenAccessed, final boolean[][][] pinState, final boolean[][] fouls, final short finalScore, final boolean gameLocked, final boolean manualScoreSet, final byte matchPlay) { return new Thread(new Runnable() { @Override public void run() { SQLiteDatabase database = DatabaseHelper.getInstance(srcActivity).getWritableDatabase(); ContentValues values; database.beginTransaction(); try { values = new ContentValues(); values.put(GameEntry.COLUMN_SCORE, finalScore); values.put(GameEntry.COLUMN_IS_LOCKED, (gameLocked ? 1 : 0)); values.put(GameEntry.COLUMN_IS_MANUAL, (manualScoreSet) ? 1 : 0); values.put(GameEntry.COLUMN_MATCH_PLAY, matchPlay); database.update(GameEntry.TABLE_NAME, values, GameEntry._ID + "=?", new String[]{String.valueOf(gameId)}); for (byte i = 0; i < Constants.NUMBER_OF_FRAMES; i++) { StringBuilder foulsOfFrame = new StringBuilder(); for (int ballCount = 0; ballCount < 3; ballCount++) { if (fouls[i][ballCount]) foulsOfFrame.append(ballCount + 1); } if (foulsOfFrame.length() == 0) foulsOfFrame.append(0); values = new ContentValues(); values.put(FrameEntry.COLUMN_PIN_STATE[0], Score.booleanFrameToInt(pinState[i][0])); values.put(FrameEntry.COLUMN_PIN_STATE[1], Score.booleanFrameToInt(pinState[i][1])); values.put(FrameEntry.COLUMN_PIN_STATE[2], Score.booleanFrameToInt(pinState[i][2])); values.put(FrameEntry.COLUMN_IS_ACCESSED, (hasFrameBeenAccessed[i]) ? 1 : 0); values.put(FrameEntry.COLUMN_FOULS, Score.foulStringToInt(foulsOfFrame.toString())); database.update(FrameEntry.TABLE_NAME, values, FrameEntry._ID + "=?", new String[]{String.valueOf(frameIds[i])}); } database.setTransactionSuccessful(); } catch (Exception ex) { throw new RuntimeException("Fatal error: Game could not save. " + ex.getMessage()); } finally { database.endTransaction(); } } }); } /** * Loads a game from the database to member variables. * * @param newGame index of id in mGameIds to load */ private void loadGameFromDatabase(final int newGame) { clearFrameColor(); new Thread(new Runnable() { @Override public void run() { MainActivity.waitForSaveThreads(new WeakReference<>((MainActivity) getActivity())); mCurrentGame = newGame; if (mGameCallback != null) mGameCallback.onGameChanged(mCurrentGame); SQLiteDatabase database = DatabaseHelper.getInstance(getContext()).getReadableDatabase(); Cursor cursor = null; try { cursor = database.query(GameEntry.TABLE_NAME, new String[]{GameEntry.COLUMN_MATCH_PLAY}, GameEntry._ID + "=?", new String[]{String.valueOf(mGameIds[mCurrentGame])}, null, null, null); if (cursor.moveToFirst()) { mMatchPlay = (byte) cursor.getInt(cursor.getColumnIndex(GameEntry.COLUMN_MATCH_PLAY)); } cursor = database.query(FrameEntry.TABLE_NAME, new String[]{ FrameEntry.COLUMN_IS_ACCESSED, FrameEntry.COLUMN_PIN_STATE[0], FrameEntry.COLUMN_PIN_STATE[1], FrameEntry.COLUMN_PIN_STATE[2], FrameEntry.COLUMN_FOULS }, FrameEntry.COLUMN_GAME_ID + "=?", new String[]{String.valueOf(mGameIds[mCurrentGame])}, null, null, FrameEntry.COLUMN_FRAME_NUMBER); mFouls = new boolean[Constants.NUMBER_OF_FRAMES][3]; byte currentFrameIterator = 0; if (cursor.moveToFirst()) { while (!cursor.isAfterLast()) { byte frameAccessed = (byte) cursor.getInt( cursor.getColumnIndex(FrameEntry.COLUMN_IS_ACCESSED)); mHasFrameBeenAccessed[currentFrameIterator] = (frameAccessed == 1); for (int i = 0; i < 3; i++) { int ball = cursor.getInt(cursor.getColumnIndex( FrameEntry.COLUMN_PIN_STATE[i])); boolean[] ballBoolean = Score.ballIntToBoolean(ball); mPinState[currentFrameIterator][i] = ballBoolean; } String fouls = Score.foulIntToString(cursor.getInt( cursor.getColumnIndex(FrameEntry.COLUMN_FOULS))); for (int ballCount = 0; ballCount < 3; ballCount++) { mFouls[currentFrameIterator][ballCount] = fouls.contains(String.valueOf(ballCount + 1)); } currentFrameIterator++; cursor.moveToNext(); } } } finally { if (cursor != null && !cursor.isClosed()) cursor.close(); } getActivity().runOnUiThread(new Runnable() { @Override public void run() { clearAllText(!mManualScoreSet[mCurrentGame]); getActivity().supportInvalidateOptionsMenu(); mTextViewGameNumber.setText( String.format(Locale.CANADA, getResources().getString(R.string.text_game_placeholder), mCurrentGame + 1)); } }); setMatchPlay(); setGameLocked(mGameLocked[mCurrentGame]); mCurrentFrame = 0; mCurrentBall = 0; if (mManualScoreSet[mCurrentGame]) return; updateScore(); updateAllBalls(); mHasFrameBeenAccessed[0] = true; while (mCurrentFrame < Constants.LAST_FRAME && mHasFrameBeenAccessed[mCurrentFrame + 1]) mCurrentFrame++; setVisibilityOfNextAndPrevItems(); updateFrameColor(true); } }).start(); } /** * Sets the icon of one of the floating action buttons. * * @param nextFab if true, the icon of the "next ball" fab is set. If false, the "previous ball" fab icon is set. * @param drawableId icon for fab */ private void setFloatingActionButtonState(boolean nextFab, int drawableId) { if (nextFab && mNextFab != null) mNextFab.animate(drawableId, Theme.getPrimaryThemeColor(), Theme.getTertiaryThemeColor()); else if (!nextFab && mPrevFab != null) mPrevFab.animate(drawableId, Theme.getPrimaryThemeColor(), Theme.getTertiaryThemeColor()); } /** * Saves current game and loads new game from database. * * @param gameNumber index in mGameIds to load from database */ public void switchGame(byte gameNumber) { if (gameNumber == mCurrentGame) return; saveGame(false); loadGameFromDatabase(gameNumber); } /** * Loads the initial scores for the games being displayed from the database so they can be shown and updated. */ private void loadInitialScores() { MainActivity.waitForSaveThreads(new WeakReference<>((MainActivity) getActivity())); int numberOfGames = ((MainActivity) getActivity()).getNumberOfGamesForSeries(); SQLiteDatabase database = DatabaseHelper.getInstance(getContext()).getReadableDatabase(); StringBuilder whereBuilder = new StringBuilder(GameEntry._ID + "=?"); String[] whereArgs = new String[numberOfGames]; whereArgs[0] = String.valueOf(mGameIds[0]); for (byte i = 1; i < numberOfGames; i++) { whereBuilder.append(" OR "); whereBuilder.append(GameEntry._ID); whereBuilder.append("=?"); whereArgs[i] = String.valueOf(mGameIds[i]); } Cursor cursor = database.query(GameEntry.TABLE_NAME, new String[]{GameEntry.COLUMN_SCORE}, whereBuilder.toString(), whereArgs, null, null, GameEntry._ID); byte currentGamePosition = 0; if (cursor.moveToFirst()) { while (!cursor.isAfterLast()) { short gameScore = cursor.getShort(cursor.getColumnIndex(GameEntry.COLUMN_SCORE)); mGameScoresMinusFouls[currentGamePosition++] = gameScore; cursor.moveToNext(); } } else { mDoNotSave.set(true); throw new RuntimeException("Fatal error: could not load initial scores."); } cursor.close(); } public void externalStoragePermissionGranted() { MainActivity mainActivity = (MainActivity) getActivity(); if (mainActivity != null) ShareUtils.showShareDialog(mainActivity, mainActivity.getSeriesId()); } /** * Gets the current game being edited. * * @return value of mCurrentGame */ public int getCurrentGame() { return mCurrentGame; } /** * Callback interface offers methods upon user interaction. */ public interface GameFragmentCallback { /** * Tells activity to open new StatsFragment with current game id and game number. * * @param gameId id of the game to display * @param gameNumber number in a series of the game to display */ void onGameStatsOpened(long gameId, byte gameNumber); /** * Tells activity to open new StatsFragment with current series. */ void onSeriesStatsOpened(); /** * Tells activity that the game has been changed. * * @param newGameNumber number of the new game, starting at index 0 */ void onGameChanged(int newGameNumber); /** * Sets auto advance values in activity. * * @param clickToAdvance view to click to advance ball * @param textViewStatus text view to display updates * @param enabled indicates if the timer should be enabled * @param delay seconds of inactivity before auto advancing */ void setAutoAdvanceConditions(View clickToAdvance, TextView textViewStatus, boolean enabled, int delay); /** * Starts the auto advance timer. */ void resetAutoAdvanceTimer(); /** * Stops the auto advance timer. */ void stopAutoAdvanceTimer(); /** * Invoked when a score is updated. * * @param gameNumber number of the game * @param gameScore score of the game */ void updateGameScore(byte gameNumber, short gameScore); /** * Loads the game scores from the database and sets them in the navigation drawer. * * @param gameIds ids of games to load */ void loadGameScoresForDrawer(long[] gameIds); } /** * Creates a new instance and sets parameters as arguments for the instance. * * @param gameIds ids of the games being displayed * @param frameIds ids of frames belonging to gameIds * @param gameLocked whether the games being displayed are locked or not * @param manualScore whether the games being displayed have manual scores set * @return the newly created instance */ public static GameFragment newInstance(long[] gameIds, long[] frameIds, boolean[] gameLocked, boolean[] manualScore) { GameFragment gameFragment = new GameFragment(); Bundle args = new Bundle(); args.putLongArray(Constants.EXTRA_ARRAY_GAME_IDS, gameIds); args.putLongArray(Constants.EXTRA_ARRAY_FRAME_IDS, frameIds); args.putBooleanArray(Constants.EXTRA_ARRAY_GAME_LOCKED, gameLocked); args.putBooleanArray(Constants.EXTRA_ARRAY_MANUAL_SCORE_SET, manualScore); gameFragment.setArguments(args); return gameFragment; } }