Code CleanUp and Checkstyle

This commit is contained in:
Alexander Doerflinger
2026-02-06 14:55:30 +01:00
parent 7e668b664b
commit e2ec4f478e
19 changed files with 1162 additions and 182 deletions

View File

@@ -226,8 +226,14 @@ public class AddPlayerActivity extends BaseActivity {
} }
} }
/**
* Handles the back button press. If the stats view is currently shown, it hides it instead of exiting.
* Otherwise, it finishes the activity as normal.
*/
private void handleBackPressed() { private void handleBackPressed() {
Log.d(TAG, "Back button pressed");
if (mIsStatsViewShown) { if (mIsStatsViewShown) {
Log.d(TAG, "Hiding stats view instead of exiting");
mPlayerStatsView.setVisibility(View.GONE); mPlayerStatsView.setVisibility(View.GONE);
mIsStatsViewShown = false; mIsStatsViewShown = false;
return; return;
@@ -283,6 +289,11 @@ public class AddPlayerActivity extends BaseActivity {
} }
} }
/**
* Displays a rationale dialog explaining why the app needs the specified permission.
*
* @param permission The permission for which to show the rationale
*/
private void showRationaleDialog(final String permission) { private void showRationaleDialog(final String permission) {
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setTitle(R.string.txt_permission_hint_title) .setTitle(R.string.txt_permission_hint_title)

View File

@@ -16,6 +16,7 @@ import androidx.preference.PreferenceManager;
*/ */
public abstract class BaseActivity extends AppCompatActivity { public abstract class BaseActivity extends AppCompatActivity {
/** Tag for debug logging. */
private static final String TAG = "BaseActivity"; private static final String TAG = "BaseActivity";
/** /**

View File

@@ -50,36 +50,29 @@ import nl.dionsegijn.konfetti.xml.KonfettiView;
/** /**
* Main game activity for playing X01 darts games (501, 301, etc.). * Main game activity for playing X01 darts games (501, 301, etc.).
* Provides numeric keyboard, real-time checkout suggestions, Double Out enforcement, * Provides numeric keyboard, real-time checkout suggestions, Double Out
* and bust detection. Enforces standard darts rules including finishing on doubles. * enforcement,
* and bust detection. Enforces standard darts rules including finishing on
* doubles.
* <p> * <p>
* This activity now serves as a lightweight UI controller that delegates all business logic * This activity now serves as a lightweight UI controller that delegates all
* to the GameManager singleton and responds to game state changes via GameStateCallback. * business logic
* to the GameManager singleton and responds to game state changes via
* GameStateCallback.
*/ */
public class GameActivity extends BaseActivity implements GameManager.GameStateCallback { public class GameActivity extends BaseActivity implements GameManager.GameStateCallback {
private static final String TAG = "GameActivity"; private static final String TAG = "GameActivity";
/**
* Intent extra key for starting score. Type: int (typically 501, 301, or 701)
*/
private static final String EXTRA_START_SCORE = "extra_start_score";
/** /**
* Intent extra for a match ID. Making it possible to load a match from the database. * Boolean flag indicating whether the stats are shown or not.
*/ */
private static final String EXTRA_MATCH_ID = "extra_match_uuid"; private boolean mAreStatsShown = false;
/**
* Intent extra for a player list. Making it possible to start a match with pre-defined players.
*/
private static final String EXTRA_PLAYERS = "extra_players";
// ======================================================================================== // ========================================================================================
// Game Manager (Singleton Business Logic Handler) // Game Manager (Singleton Business Logic Handler)
// ======================================================================================== // ========================================================================================
/** /**
* Singleton instance managing all game business logic. * Singleton instance managing all game business logic.
* Replaces all previous game state fields and logic. * Replaces all previous game state fields and logic.
@@ -94,27 +87,27 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
// ======================================================================================== // ========================================================================================
// UI References // UI References
// ======================================================================================== // ========================================================================================
/** /**
* TextView displaying the active player's remaining score. * TextView displaying the active player's remaining score.
*/ */
private TextView tvScorePrimary; private TextView tvScorePrimary;
/** /**
* TextView displaying the active player's name (uppercase). * TextView displaying the active player's name (uppercase).
*/ */
private TextView tvPlayerName; private TextView tvPlayerName;
/** /**
* TextView displaying the active player's three-dart average. * TextView displaying the active player's three-dart average.
*/ */
private TextView tvLegAvg; private TextView tvLegAvg;
/** /**
* TextView displaying the suggested checkout route. * TextView displaying the suggested checkout route.
*/ */
private TextView tvCheckout; private TextView tvCheckout;
/** /**
* Container layout for checkout suggestion display. * Container layout for checkout suggestion display.
* Visible when score ≤170 and route is available. * Visible when score ≤170 and route is available.
@@ -125,17 +118,17 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
* The Container for displaying the current score. * The Container for displaying the current score.
*/ */
private LinearLayout mScoreContainer; private LinearLayout mScoreContainer;
/** /**
* Button for selecting single (1×) multiplier. * Button for selecting single (1×) multiplier.
*/ */
private View btnSingle; private View btnSingle;
/** /**
* Button for selecting double (2×) multiplier. * Button for selecting double (2×) multiplier.
*/ */
private View btnDouble; private View btnDouble;
/** /**
* Button for selecting triple (3×) multiplier. * Button for selecting triple (3×) multiplier.
*/ */
@@ -160,12 +153,12 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
* The {@link PlayerStatsView} to display player statistics. * The {@link PlayerStatsView} to display player statistics.
*/ */
private PlayerStatsView mStatsView; private PlayerStatsView mStatsView;
/** /**
* Array of three TextViews showing darts thrown in current turn. * Array of three TextViews showing darts thrown in current turn.
*/ */
private final TextView[] tvDartPills = new TextView[3]; private final TextView[] tvDartPills = new TextView[3];
/** /**
* GridLayout container holding numeric keyboard buttons (1-20). * GridLayout container holding numeric keyboard buttons (1-20).
*/ */
@@ -189,30 +182,39 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
private boolean mIsAudioEnabled; private boolean mIsAudioEnabled;
/** /**
* Centralized database helper that manages all database operations with proper synchronization. * Centralized database helper that manages all database operations with proper
* synchronization.
*/ */
private DatabaseHelper mDatabaseHelper; private DatabaseHelper mDatabaseHelper;
/** /**
* Starts GameActivity with specified players and starting score. * Starts GameActivity with specified players and starting score.
* *
* @param context The context from which to start the activity * @param context The context from which to start the activity
* @param startScore The starting score (typically 501, 301, or 701) * @param startScore The starting score (typically 501, 301, or 701)
* @param matchId The ID of the match to be started/loaded. * @param matchId The ID of the match to be started/loaded.
*/ */
public static void start(final Context context, final int startScore, final int matchId) { public static void start(final Context context, final int startScore, final int matchId) {
final GameManager gameManager = GameManager.getInstance(context); final GameManager gameManager = GameManager.getInstance(context);
gameManager.initializeMatch(matchId, startScore,null); gameManager.initializeMatch(matchId, startScore, null);
Intent intent = new Intent(context, GameActivity.class); Intent intent = new Intent(context, GameActivity.class);
context.startActivity(intent); context.startActivity(intent);
} }
/**
* Overloaded start method for starting a new match with specified players and
* starting score.
*
* @param context The context from which to start the activity
* @param players List of Player objects participating in the match
* @param startScore The starting score (typically 501, 301, or 701)
*/
public static void start(final Context context, final List<Player> players, final int startScore) { public static void start(final Context context, final List<Player> players, final int startScore) {
final GameManager gameManager = GameManager.getInstance(context); final GameManager gameManager = GameManager.getInstance(context);
gameManager.initializeMatch(-1, startScore, players,null); gameManager.initializeMatch(-1, startScore, players, null);
Intent intent = new Intent(context, GameActivity.class); final Intent intent = new Intent(context, GameActivity.class);
context.startActivity(intent); context.startActivity(intent);
} }
@@ -226,7 +228,8 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_game); setContentView(R.layout.activity_game);
// Configure window insets to properly handle system bars (status bar, navigation bar) // Configure window insets to properly handle system bars (status bar,
// navigation bar)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
@@ -263,9 +266,18 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
mIsVibrationEnabled = settingPrefs.getBoolean(getString(R.string.pref_key_vibration_feedback), true); mIsVibrationEnabled = settingPrefs.getBoolean(getString(R.string.pref_key_vibration_feedback), true);
} }
/**
* Handles the back button press. If the stats view is currently shown, it hides
* it instead of exiting.
* Otherwise, it finishes the activity as normal.
*/
private void handleBackPressed() { private void handleBackPressed() {
// TODO: Handle Statistics view in here if (mAreStatsShown) {
mStatsView.setVisibility(View.GONE);
mShowStatsBtn.setVisibility(View.VISIBLE);
mAreStatsShown = false;
return;
}
finish(); finish();
} }
@@ -299,12 +311,12 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
mShowStatsBtn = findViewById(R.id.show_stats_btn); mShowStatsBtn = findViewById(R.id.show_stats_btn);
mStatsView = findViewById(R.id.player_stats_view); mStatsView = findViewById(R.id.player_stats_view);
mShowStatsBtn.setOnClickListener(v -> { mShowStatsBtn.setOnClickListener(v -> {
mStatsView.setVisibility(View.VISIBLE); mStatsView.setVisibility(View.VISIBLE);
mShowStatsBtn.setVisibility(View.GONE); mShowStatsBtn.setVisibility(View.GONE);
mAreStatsShown = true;
}); });
mSubmitTurnBtn.setOnClickListener(v -> submitTurn()); mSubmitTurnBtn.setOnClickListener(v -> submitTurn());
findViewById(R.id.btnUndoDart).setOnClickListener(v -> undoLastDart()); findViewById(R.id.btnUndoDart).setOnClickListener(v -> undoLastDart());
} }
@@ -316,17 +328,17 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
Log.d(TAG, "setupKeyboard() called"); Log.d(TAG, "setupKeyboard() called");
glKeyboard.removeAllViews(); glKeyboard.removeAllViews();
mKeyboardButtons.clear(); mKeyboardButtons.clear();
// Create buttons for numbers 1-20 // Create buttons for numbers 1-20
for (int i = 1; i <= 20; i++) { for (int i = 1; i <= 20; i++) {
// Inflate button from template layout // Inflate button from template layout
MaterialButton btn = (MaterialButton) getLayoutInflater().inflate( MaterialButton btn = (MaterialButton) getLayoutInflater().inflate(
R.layout.view_keyboard_button, glKeyboard, false); R.layout.view_keyboard_button, glKeyboard, false);
final int val = i; final int val = i;
btn.setText(String.valueOf(val)); btn.setText(String.valueOf(val));
btn.setOnClickListener(v -> onNumberTap(val)); btn.setOnClickListener(v -> onNumberTap(val));
glKeyboard.addView(btn); glKeyboard.addView(btn);
mKeyboardButtons.add(btn); // Cache for styling updates mKeyboardButtons.add(btn); // Cache for styling updates
} }
@@ -342,6 +354,10 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
mGameManager.onNumberTap(baseValue); mGameManager.onNumberTap(baseValue);
} }
/**
* Triggers the bust sequence with visual and haptic feedback.
* Called by GameManager when a bust is detected.
*/
private void triggerBustSequence() { private void triggerBustSequence() {
// Visual feedback: Shake scoreboard // Visual feedback: Shake scoreboard
Animation shake = AnimationUtils.loadAnimation(this, R.anim.shake); Animation shake = AnimationUtils.loadAnimation(this, R.anim.shake);
@@ -363,6 +379,10 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
updateTurnIndicators(); updateTurnIndicators();
} }
/**
* Resets all visual elements to their default state after a bust or at the
* start of a turn.
*/
private void resetVisuals() { private void resetVisuals() {
mScoreContainer.setBackgroundColor(ContextCompat.getColor(this, R.color.surface_primary)); mScoreContainer.setBackgroundColor(ContextCompat.getColor(this, R.color.surface_primary));
tvScorePrimary.setTextColor(ContextCompat.getColor(this, R.color.volt_green)); tvScorePrimary.setTextColor(ContextCompat.getColor(this, R.color.volt_green));
@@ -371,7 +391,6 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
mSubmitTurnBtn.setText(R.string.txt_game_btn_submit); mSubmitTurnBtn.setText(R.string.txt_game_btn_submit);
} }
/** /**
* Handler for Bull button tap. Delegates to onNumberTap with base value 25. * Handler for Bull button tap. Delegates to onNumberTap with base value 25.
* *
@@ -382,7 +401,8 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
} }
/** /**
* Sets the multiplier and updates UI elements (button appearances and keyboard colors). * Sets the multiplier and updates UI elements (button appearances and keyboard
* colors).
* Delegates to GameManager and updates UI via callback. * Delegates to GameManager and updates UI via callback.
* *
* @param m The multiplier value (1=Single, 2=Double, 3=Triple) * @param m The multiplier value (1=Single, 2=Double, 3=Triple)
@@ -413,17 +433,18 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
*/ */
private void updateUI() { private void updateUI() {
GameManager.PlayerState active = mGameManager.getActivePlayer(); GameManager.PlayerState active = mGameManager.getActivePlayer();
if (active == null) return; if (active == null)
return;
// Update score display // Update score display
tvScorePrimary.setText(String.valueOf(active.remainingScore)); tvScorePrimary.setText(String.valueOf(active.remainingScore));
// Update player name (uppercase for emphasis) // Update player name (uppercase for emphasis)
tvPlayerName.setText(active.name.toUpperCase()); tvPlayerName.setText(active.name.toUpperCase());
// Calculate and display three-dart average // Calculate and display three-dart average
double avg = active.dartsThrown == 0 ? 0.0 : double avg = active.dartsThrown == 0 ? 0.0
((double)(mGameManager.getStartingScore() - active.remainingScore) / active.dartsThrown) * 3; : ((double) (mGameManager.getStartingScore() - active.remainingScore) / active.dartsThrown) * 3;
tvLegAvg.setText(String.format("AVG: %.1f", avg)); tvLegAvg.setText(String.format("AVG: %.1f", avg));
// Get current target and darts remaining from GameManager // Get current target and darts remaining from GameManager
@@ -438,7 +459,7 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
* Updates checkout route suggestion display based on score and darts remaining. * Updates checkout route suggestion display based on score and darts remaining.
* Shows pulsing animation when route is available. * Shows pulsing animation when route is available.
* *
* @param score Target score to finish * @param score Target score to finish
* @param dartsLeft Number of darts remaining (0-3) * @param dartsLeft Number of darts remaining (0-3)
*/ */
private void updateCheckoutSuggestion(final int score, final int dartsLeft) { private void updateCheckoutSuggestion(final int score, final int dartsLeft) {
@@ -488,8 +509,10 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
* @return Display label for UI * @return Display label for UI
*/ */
private String getDartLabel(final int score) { private String getDartLabel(final int score) {
if (score == DartsConstants.DOUBLE_BULL_VALUE) return DartsConstants.LABEL_DOUBLE_BULL; // Double Bull / Bullseye if (score == DartsConstants.DOUBLE_BULL_VALUE)
if (score == DartsConstants.BULL_VALUE) return DartsConstants.LABEL_BULL; // Single Bull return DartsConstants.LABEL_DOUBLE_BULL; // Double Bull / Bullseye
if (score == DartsConstants.BULL_VALUE)
return DartsConstants.LABEL_BULL; // Single Bull
// Return numeric value for all other scores // Return numeric value for all other scores
return String.valueOf(score); return String.valueOf(score);
} }
@@ -499,7 +522,7 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
* Displays win notification and celebration animation. * Displays win notification and celebration animation.
* Called by GameManager through onPlayerWin callback. * Called by GameManager through onPlayerWin callback.
* *
* @param winner GameManager.PlayerState of the winning player * @param winner GameManager.PlayerState of the winning player
* @param checkoutValue The final dart value that won the game * @param checkoutValue The final dart value that won the game
*/ */
private void handleWin(final GameManager.PlayerState winner, final int checkoutValue) { private void handleWin(final GameManager.PlayerState winner, final int checkoutValue) {
@@ -515,12 +538,18 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
attachPlayerStats(); attachPlayerStats();
} }
/**
* Retrieves the winning player's statistics from the database and binds them to
* the stats view.
* Runs database query on a background thread to avoid blocking the UI.
*/
private void attachPlayerStats() { private void attachPlayerStats() {
new Thread(() -> { new Thread(() -> {
try { try {
final GameManager.PlayerState activePlayer = mGameManager.getActivePlayer(); final GameManager.PlayerState activePlayer = mGameManager.getActivePlayer();
if (activePlayer == null) return; if (activePlayer == null)
return;
final Player player = activePlayer.player; final Player player = activePlayer.player;
final Statistics statistics = mDatabaseHelper.getStatisticsForPlayer(player.id); final Statistics statistics = mDatabaseHelper.getStatisticsForPlayer(player.id);
runOnUiThread(() -> mStatsView.bind(player, statistics)); runOnUiThread(() -> mStatsView.bind(player, statistics));
@@ -549,8 +578,7 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
.spread(360) .spread(360)
.position(0.5, 0.5) // Center of screen .position(0.5, 0.5) // Center of screen
.colors(Arrays.asList(0xfce18a, 0xff726d, 0xf4306d, 0xb48def)) // Gold/Festive colors .colors(Arrays.asList(0xfce18a, 0xff726d, 0xf4306d, 0xb48def)) // Gold/Festive colors
.build() .build());
);
winnerText.setText(winnerName + " WINS!"); winnerText.setText(winnerName + " WINS!");
winnerText.setVisibility(View.VISIBLE); winnerText.setVisibility(View.VISIBLE);
@@ -584,13 +612,19 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
@Override @Override
public void onMultiplierChanged(final int multiplier) { public void onMultiplierChanged(final int multiplier) {
runOnUiThread(() -> { runOnUiThread(() -> {
btnSingle.setAlpha(multiplier == DartsConstants.MULTIPLIER_SINGLE ? UIConstants.ALPHA_FULL : UIConstants.ALPHA_INACTIVE); btnSingle.setAlpha(multiplier == DartsConstants.MULTIPLIER_SINGLE ? UIConstants.ALPHA_FULL
btnDouble.setAlpha(multiplier == DartsConstants.MULTIPLIER_DOUBLE ? UIConstants.ALPHA_FULL : UIConstants.ALPHA_INACTIVE); : UIConstants.ALPHA_INACTIVE);
btnTriple.setAlpha(multiplier == DartsConstants.MULTIPLIER_TRIPLE ? UIConstants.ALPHA_FULL : UIConstants.ALPHA_INACTIVE); btnDouble.setAlpha(multiplier == DartsConstants.MULTIPLIER_DOUBLE ? UIConstants.ALPHA_FULL
: UIConstants.ALPHA_INACTIVE);
btnTriple.setAlpha(multiplier == DartsConstants.MULTIPLIER_TRIPLE ? UIConstants.ALPHA_FULL
: UIConstants.ALPHA_INACTIVE);
btnSingle.setBackgroundResource(multiplier == DartsConstants.MULTIPLIER_SINGLE ? R.drawable.shape_multiplier_active : 0); btnSingle.setBackgroundResource(
btnDouble.setBackgroundResource(multiplier == DartsConstants.MULTIPLIER_DOUBLE ? R.drawable.shape_multiplier_red : 0); multiplier == DartsConstants.MULTIPLIER_SINGLE ? R.drawable.shape_multiplier_active : 0);
btnTriple.setBackgroundResource(multiplier == DartsConstants.MULTIPLIER_TRIPLE ? R.drawable.shape_multiplier_blue : 0); btnDouble.setBackgroundResource(
multiplier == DartsConstants.MULTIPLIER_DOUBLE ? R.drawable.shape_multiplier_red : 0);
btnTriple.setBackgroundResource(
multiplier == DartsConstants.MULTIPLIER_TRIPLE ? R.drawable.shape_multiplier_blue : 0);
int bgColor, textColor, strokeColor; int bgColor, textColor, strokeColor;
if (multiplier == DartsConstants.MULTIPLIER_TRIPLE) { if (multiplier == DartsConstants.MULTIPLIER_TRIPLE) {
@@ -636,13 +670,13 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
final Animation shakeAnimation = AnimationUtils.loadAnimation(this, R.anim.shake); final Animation shakeAnimation = AnimationUtils.loadAnimation(this, R.anim.shake);
final View main = findViewById(R.id.main); final View main = findViewById(R.id.main);
main.startAnimation(shakeAnimation); main.startAnimation(shakeAnimation);
if (mIsVibrationEnabled) { if (mIsVibrationEnabled) {
final Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE); final Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
if (vibrator != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (vibrator != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Log.d(TAG, "onOneEightyScored: Pattern vibration"); Log.d(TAG, "onOneEightyScored: Pattern vibration");
// Pattern that should match the 180 shout. // Pattern that should match the 180 shout.
long[] pattern = {0, 150, 100, 1650, 50, 150, 10, 500, 300, 200}; long[] pattern = { 0, 150, 100, 1650, 50, 150, 10, 500, 300, 200 };
vibrator.vibrate(VibrationEffect.createWaveform(pattern, -1)); vibrator.vibrate(VibrationEffect.createWaveform(pattern, -1));
} else if (vibrator != null) { } else if (vibrator != null) {
Log.d(TAG, "onOneEightyScored: Vibrating legacy mode"); Log.d(TAG, "onOneEightyScored: Vibrating legacy mode");
@@ -651,7 +685,7 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
Log.e(TAG, "onOneEightyScored: Vibrator not available"); Log.e(TAG, "onOneEightyScored: Vibrator not available");
} }
} }
if (mIsAudioEnabled) { if (mIsAudioEnabled) {
mSoundEngine.playOneHundredAndEightySound(); mSoundEngine.playOneHundredAndEightySound();
} }

View File

@@ -60,6 +60,7 @@ public class MainMenuActivity extends BaseActivity {
*/ */
private DatabaseHelper mDatabaseHelper; private DatabaseHelper mDatabaseHelper;
/** The ongoing match retrieved from the database, if any. Used for quick start functionality. */
private Match mOngoingMatch; private Match mOngoingMatch;
/** /**

View File

@@ -38,7 +38,7 @@ public class SettingsActivity extends BaseActivity {
.replace(R.id.settings, new MainMenuPreferencesFragment()) .replace(R.id.settings, new MainMenuPreferencesFragment())
.commit(); .commit();
} }
ActionBar actionBar = getSupportActionBar(); final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) { if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true);
} }

View File

@@ -303,6 +303,15 @@ public class DatabaseHelper {
} }
} }
/**
* Creates a new match record in the database with the specified game mode and players.
* Initializes the match progress with starting scores and player data.
* Blocks until the operation completes to return the new match ID.
*
* @param gameMode The game mode string (e.g., "501")
* @param players List of Player objects participating in the match
* @return The ID of the newly created match, or -1 if creation failed
*/
public long createNewMatch(final String gameMode, final List<Player> players) { public long createNewMatch(final String gameMode, final List<Player> players) {
// Parse starting score from gameMode string // Parse starting score from gameMode string
int startingScore = 501; // Default int startingScore = 501; // Default
@@ -373,6 +382,11 @@ public class DatabaseHelper {
}); });
} }
/**
* Retrieves all ongoing matches from the database synchronously.
* Blocks until the operation completes to ensure consistency with any pending writes.
* @return List of ongoing matches, or empty list if none exist
*/
public List<Match> getOngoingMatches() { public List<Match> getOngoingMatches() {
try { try {
return mExecutor.submit(() -> mDatabase.matchDao().getOngoingMatches()).get(); return mExecutor.submit(() -> mDatabase.matchDao().getOngoingMatches()).get();
@@ -382,6 +396,9 @@ public class DatabaseHelper {
} }
} }
/**
* Prints all matches in the database to the log for debugging purposes.
*/
public void printAllMatches() { public void printAllMatches() {
Log.d(TAG, "printAllMatches() called"); Log.d(TAG, "printAllMatches() called");
try { try {
@@ -400,6 +417,11 @@ public class DatabaseHelper {
} }
} }
/**
* Retrieves the most recently completed match from the database synchronously.
* Blocks until the operation completes to ensure consistency with any pending writes.
* @return The most recent completed match, or null if no completed matches exist
*/
public Match getLastCompletedMatch() { public Match getLastCompletedMatch() {
try { try {
return mExecutor.submit(() -> { return mExecutor.submit(() -> {
@@ -416,6 +438,12 @@ public class DatabaseHelper {
} }
} }
/**
* Retrieves a match by its unique ID from the database synchronously.
* Blocks until the operation completes to ensure consistency with any pending writes.
* @param matchId The unique identifier of the match
* @return The match with the specified ID, or null if no such match exists
*/
public Match getMatchById(final int matchId) { public Match getMatchById(final int matchId) {
try { try {
return mExecutor.submit(() -> { return mExecutor.submit(() -> {

View File

@@ -49,6 +49,13 @@ public interface MatchDao {
@Query("SELECT * FROM matches ORDER BY timestamp DESC LIMIT 1") @Query("SELECT * FROM matches ORDER BY timestamp DESC LIMIT 1")
Match getLastMatch(); Match getLastMatch();
/**
* Retrieves a match by its unique ID.
* Must be called on a background thread.
*
* @param matchId The unique identifier of the match
* @return The match with the specified ID, or null if no such match exists
*/
@Query("SELECT * FROM matches WHERE id = :matchId") @Query("SELECT * FROM matches WHERE id = :matchId")
Match getMatchById(final int matchId); Match getMatchById(final int matchId);

View File

@@ -19,9 +19,12 @@ import java.util.Map;
/** /**
* Represents a darts match in the Oche Companion application. * Represents a darts match in the Oche Companion application.
* Room entity storing match information including game mode, timestamp, player count, * Room entity storing match information including game mode, timestamp, player
* state (ongoing/completed/canceled), and detailed performance data for all participants. * count,
* Implements Serializable for passing between Android components. Provides helper methods * state (ongoing/completed/canceled), and detailed performance data for all
* participants.
* Implements Serializable for passing between Android components. Provides
* helper methods
* to parse participant JSON data and reconstruct Player objects. * to parse participant JSON data and reconstruct Player objects.
* *
* @see com.aldo.apps.ochecompanion.database.dao.MatchDao * @see com.aldo.apps.ochecompanion.database.dao.MatchDao
@@ -40,17 +43,18 @@ public class Match implements Serializable {
public enum MatchState { public enum MatchState {
/** Match is currently in progress */ /** Match is currently in progress */
ONGOING, ONGOING,
/** Match has been completed successfully */ /** Match has been completed successfully */
COMPLETED, COMPLETED,
/** Match was canceled before completion */ /** Match was canceled before completion */
CANCELED CANCELED
} }
/** /**
* Auto-generated unique primary key for this match. * Auto-generated unique primary key for this match.
* Value is 0 before insertion, then assigned by Room using SQLite AUTOINCREMENT. * Value is 0 before insertion, then assigned by Room using SQLite
* AUTOINCREMENT.
* *
* @see PrimaryKey * @see PrimaryKey
*/ */
@@ -59,7 +63,8 @@ public class Match implements Serializable {
/** /**
* Unix epoch timestamp (milliseconds) when match was completed. * Unix epoch timestamp (milliseconds) when match was completed.
* Used for chronological sorting and display. Obtained via System.currentTimeMillis(). * Used for chronological sorting and display. Obtained via
* System.currentTimeMillis().
* *
* @see System#currentTimeMillis() * @see System#currentTimeMillis()
*/ */
@@ -98,14 +103,19 @@ public class Match implements Serializable {
* Constructs a new Match entity ready for database insertion. * Constructs a new Match entity ready for database insertion.
* The match ID will be auto-generated by Room upon insertion. * The match ID will be auto-generated by Room upon insertion.
* *
* @param timestamp Unix epoch timestamp in milliseconds when match was created/completed * @param timestamp Unix epoch timestamp in milliseconds when match was
* @param gameMode Identifier for the darts game variant (e.g., "501", "Cricket") * created/completed
* @param playerCount Number of players who participated (must be at least 1) * @param gameMode Identifier for the darts game variant (e.g., "501",
* "Cricket")
* @param playerCount Number of players who participated (must be at least
* 1)
* @param participantData JSON string containing player performance data * @param participantData JSON string containing player performance data
* @param state Current state of the match (ONGOING, COMPLETED, or CANCELED) * @param state Current state of the match (ONGOING, COMPLETED, or
* CANCELED)
* @see com.aldo.apps.ochecompanion.database.dao.MatchDao#insert(Match) * @see com.aldo.apps.ochecompanion.database.dao.MatchDao#insert(Match)
*/ */
public Match(final long timestamp, final String gameMode, final int playerCount, final String participantData, final MatchState state) { public Match(final long timestamp, final String gameMode, final int playerCount, final String participantData,
final MatchState state) {
this.timestamp = timestamp; this.timestamp = timestamp;
this.gameMode = gameMode; this.gameMode = gameMode;
this.playerCount = playerCount; this.playerCount = playerCount;
@@ -115,11 +125,13 @@ public class Match implements Serializable {
/** /**
* Convenience constructor for creating a Match from a list of Player objects. * Convenience constructor for creating a Match from a list of Player objects.
* Automatically generates JSON participant data and sets timestamp to current time. * Automatically generates JSON participant data and sets timestamp to current
* time.
* All players will have a score of 0. Match state is set to ONGOING. * All players will have a score of 0. Match state is set to ONGOING.
* *
* @param gameMode Identifier for the darts game variant (e.g., "501", "Cricket") * @param gameMode Identifier for the darts game variant (e.g., "501",
* @param players List of Player objects to include in this match * "Cricket")
* @param players List of Player objects to include in this match
*/ */
@Ignore @Ignore
public Match(final String gameMode, final List<Player> players) { public Match(final String gameMode, final List<Player> players) {
@@ -132,12 +144,14 @@ public class Match implements Serializable {
/** /**
* Convenience constructor for creating a Match from players with their scores. * Convenience constructor for creating a Match from players with their scores.
* Automatically generates JSON participant data and sets timestamp to current time. * Automatically generates JSON participant data and sets timestamp to current
* time.
* Match state is set to ONGOING by default. * Match state is set to ONGOING by default.
* *
* @param gameMode Identifier for the darts game variant (e.g., "501", "Cricket") * @param gameMode Identifier for the darts game variant (e.g., "501",
* @param players List of Player objects to include in this match * "Cricket")
* @param scores Map of player IDs to their final scores * @param players List of Player objects to include in this match
* @param scores Map of player IDs to their final scores
*/ */
@Ignore @Ignore
public Match(final String gameMode, final List<Player> players, final Map<Integer, Integer> scores) { public Match(final String gameMode, final List<Player> players, final Map<Integer, Integer> scores) {
@@ -221,6 +235,7 @@ public class Match implements Serializable {
} }
} catch (JSONException e) { } catch (JSONException e) {
// Return empty map if JSON parsing fails // Return empty map if JSON parsing fails
Log.e(TAG, "getPlayerScores: Failed to parse participant data", e);
} }
return scores; return scores;
} }
@@ -243,8 +258,9 @@ public class Match implements Serializable {
player.careerAverage = participant.optDouble("careerAverage", 0.0); player.careerAverage = participant.optDouble("careerAverage", 0.0);
players.add(player); players.add(player);
} }
} catch (JSONException e) { } catch (final JSONException e) {
// Return empty list if JSON parsing fails // Return empty list if JSON parsing fails
Log.e(TAG, "getAllPlayers: Failed to parse participant data", e);
} }
return players; return players;
} }
@@ -253,7 +269,7 @@ public class Match implements Serializable {
* Generates JSON string from a list of Player objects with optional scores. * Generates JSON string from a list of Player objects with optional scores.
* *
* @param players List of Player objects to convert * @param players List of Player objects to convert
* @param scores Map of player IDs to their match scores (null for all zeros) * @param scores Map of player IDs to their match scores (null for all zeros)
* @return JSON string representation of player data * @return JSON string representation of player data
*/ */
private String generateParticipantJson(final List<Player> players, final Map<Integer, Integer> scores) { private String generateParticipantJson(final List<Player> players, final Map<Integer, Integer> scores) {
@@ -265,12 +281,14 @@ public class Match implements Serializable {
participant.put("username", player.username); participant.put("username", player.username);
participant.put("photoUri", player.profilePictureUri); participant.put("photoUri", player.profilePictureUri);
participant.put("careerAverage", player.careerAverage); participant.put("careerAverage", player.careerAverage);
final int score = (scores != null && scores.containsKey((int) player.id)) ? scores.get((int) player.id) : 0; final int score = (scores != null && scores.containsKey((int) player.id)) ? scores.get((int) player.id)
: 0;
participant.put("score", score); participant.put("score", score);
participants.put(participant); participants.put(participant);
} }
} catch (JSONException e) { } catch (final JSONException e) {
// Return empty array if JSON generation fails // Return empty array if JSON generation fails
Log.e(TAG, "generateParticipantJson: Failed to generate participant JSON", e);
} }
return participants.toString(); return participants.toString();
} }
@@ -281,7 +299,7 @@ public class Match implements Serializable {
public static class ParticipantData { public static class ParticipantData {
/** The Player object */ /** The Player object */
public final Player player; public final Player player;
/** The player's score in this match */ /** The player's score in this match */
public final int score; public final int score;
@@ -312,6 +330,7 @@ public class Match implements Serializable {
} }
} catch (JSONException e) { } catch (JSONException e) {
// Return empty list if JSON parsing fails // Return empty list if JSON parsing fails
Log.e(TAG, "getAllParticipants: Failed to parse participant data", e);
} }
return participants; return participants;
} }

View File

@@ -27,25 +27,96 @@ import java.util.List;
* that only handles UI updates via the GameStateCallback interface. * that only handles UI updates via the GameStateCallback interface.
*/ */
public class GameManager { public class GameManager {
/**
* Tag for logging purposes.
*/
private static final String TAG = "GameManager"; private static final String TAG = "GameManager";
// Singleton instance /**
private static GameManager sInstance; * Singleton instance of GameManager.
* Volatile to ensure thread-safe lazy initialization.
*/
private static volatile GameManager sInstance;
// ========================================================================================
// Dependencies // Dependencies
// ========================================================================================
/**
* Reference to the centralized database helper for all database operations.
* Initialized once in constructor and never changed.
*/
private final DatabaseHelper mDatabaseHelper; private final DatabaseHelper mDatabaseHelper;
/**
* Callback interface for notifying the UI layer of game state changes.
* Set by the UI controller (GameActivity) when it's ready to receive updates.
*/
private GameStateCallback mCallback; private GameStateCallback mCallback;
// ========================================================================================
// Game State // Game State
// ========================================================================================
/**
* The database ID of the current match.
* -1 indicates no match is loaded.
*/
private int mMatchId = -1; private int mMatchId = -1;
/**
* The starting score for this X01 game (typically 501, 301, or 701).
*/
private int mStartingScore = DartsConstants.DEFAULT_GAME_SCORE; private int mStartingScore = DartsConstants.DEFAULT_GAME_SCORE;
/**
* Index of the currently active player (0 to playerCount-1).
* Cycles through players as turns complete.
*/
private int mActivePlayerIndex = 0; private int mActivePlayerIndex = 0;
/**
* Current multiplier for the next dart (1=Single, 2=Double, 3=Triple).
* Resets to 1 after each dart throw for safety.
*/
private int mMultiplier = 1; private int mMultiplier = 1;
/**
* List of all player states in this match.
* Order determines turn order.
* Thread-safe operations through GameManager's single-threaded nature.
*/
private final List<PlayerState> mPlayerStates = new ArrayList<>(); private final List<PlayerState> mPlayerStates = new ArrayList<>();
/**
* Point values of darts thrown in the current turn (up to 3).
* Cleared when turn is submitted or reset.
*/
private final List<Integer> mCurrentTurnDarts = new ArrayList<>(); private final List<Integer> mCurrentTurnDarts = new ArrayList<>();
/**
* Detailed dart hit information (base value and multiplier) for current turn.
* Parallel to mCurrentTurnDarts, used for statistics and heat map tracking.
* Cleared when turn is submitted or reset.
*/
private final List<DartHit> mCurrentTurnDartHits = new ArrayList<>(); private final List<DartHit> mCurrentTurnDartHits = new ArrayList<>();
/**
* Flag indicating the current turn has ended (bust, win, or 3 darts thrown).
* Prevents additional dart entry until turn is submitted.
*/
private boolean mIsTurnOver = false; private boolean mIsTurnOver = false;
/**
* Flag indicating the current turn resulted in a bust.
* Used to prevent UI from subtracting bust darts from score display.
*/
private boolean mIsBustedTurn = false; private boolean mIsBustedTurn = false;
/**
* Flag indicating the match has been completed (a player has won).
* When true, game state is frozen and match is marked as COMPLETED in database.
*/
private boolean mIsMatchCompleted = false; private boolean mIsMatchCompleted = false;
/** /**
@@ -93,11 +164,25 @@ public class GameManager {
/** /**
* Represents a single dart hit with its base value and multiplier. * Represents a single dart hit with its base value and multiplier.
* Immutable data class used for tracking individual dart throws.
*/ */
public static class DartHit { public static class DartHit {
/**
* The dartboard number hit (1-20 or 25 for bull).
*/
public final int baseValue; public final int baseValue;
/**
* The multiplier applied to the base value (1=single, 2=double, 3=triple).
*/
public final int multiplier; public final int multiplier;
/**
* Constructs a DartHit with the specified base value and multiplier.
*
* @param baseValue The dartboard number (1-20 or 25 for bull)
* @param multiplier The multiplier (1=single, 2=double, 3=triple)
*/
public DartHit(final int baseValue, final int multiplier) { public DartHit(final int baseValue, final int multiplier) {
this.baseValue = baseValue; this.baseValue = baseValue;
this.multiplier = multiplier; this.multiplier = multiplier;
@@ -106,14 +191,45 @@ public class GameManager {
/** /**
* State holder for a single player's X01 game progress. * State holder for a single player's X01 game progress.
* Tracks current match state for an individual player.
*/ */
public static class PlayerState { public static class PlayerState {
/**
* Reference to the Player entity from the database.
* Used for statistics updates and player information.
*/
public final Player player; public final Player player;
/**
* Player's database ID for quick lookups.
* 0 for guest players who don't have database entries.
*/
public final long playerId; public final long playerId;
/**
* Player's display name cached from the Player entity.
* Stored for convenience to avoid repeated lookups.
*/
public final String name; public final String name;
/**
* Player's current remaining score in this match.
* Decreases with valid throws, resets to previous value on bust.
*/
public int remainingScore; public int remainingScore;
/**
* Total number of darts thrown by this player in the current match.
* Used for calculating averages and statistics.
*/
public int dartsThrown = 0; public int dartsThrown = 0;
/**
* Constructs a PlayerState for a player with the specified starting score.
*
* @param player The Player entity from the database
* @param startScore The starting score for this X01 game (e.g., 501)
*/
public PlayerState(final Player player, final int startScore) { public PlayerState(final Player player, final int startScore) {
this.player = player; this.player = player;
this.playerId = player.id; this.playerId = player.id;
@@ -124,6 +240,9 @@ public class GameManager {
/** /**
* Private constructor to enforce singleton pattern. * Private constructor to enforce singleton pattern.
* Initializes the database helper with application context.
*
* @param context Application or Activity context (will be converted to application context)
*/ */
private GameManager(final Context context) { private GameManager(final Context context) {
mDatabaseHelper = DatabaseHelper.getInstance(context); mDatabaseHelper = DatabaseHelper.getInstance(context);
@@ -144,16 +263,26 @@ public class GameManager {
/** /**
* Registers a callback to receive game state updates. * Registers a callback to receive game state updates.
* Immediately triggers an initial state change callback to synchronize the UI.
* *
* @param callback The callback to register * @param callback The callback to register (can be null to unregister)
*/ */
public void setCallback(final GameStateCallback callback) { public void setCallback(final GameStateCallback callback) {
mCallback = callback; mCallback = callback;
//Send one initial callback //Send one initial callback to sync UI with current state
notifyGameStateChanged(); notifyGameStateChanged();
} }
public void initializeMatch(final int matchId,final int startingScore, final List<Player> players, final Runnable onComplete) { /**
* Initializes a new game or loads an existing match from the database with explicit player list.
* This overload allows passing a pre-loaded player list instead of loading from database.
*
* @param matchId The match ID to load, or -1 to create a new match
* @param startingScore The starting score (501, 301, etc.)
* @param players Pre-loaded list of players to use for this match
* @param onComplete Callback executed on the calling thread when initialization is complete
*/
public void initializeMatch(final int matchId, final int startingScore, final List<Player> players, final Runnable onComplete) {
mStartingScore = startingScore; mStartingScore = startingScore;
mMatchId = matchId; mMatchId = matchId;
@@ -234,6 +363,11 @@ public class GameManager {
/** /**
* Initializes player states from the provided player list. * Initializes player states from the provided player list.
* Clears any existing player states and creates new PlayerState objects
* for each player with the current starting score.
* If no players are provided, creates a single guest player.
*
* @param players List of Player entities to initialize, or null/empty for guest player
*/ */
private void initializePlayerStates(final List<Player> players) { private void initializePlayerStates(final List<Player> players) {
mPlayerStates.clear(); mPlayerStates.clear();
@@ -250,6 +384,10 @@ public class GameManager {
/** /**
* Loads match progress from a saved state. * Loads match progress from a saved state.
* Restores player scores, darts thrown, and active player index from
* a previously saved MatchProgress snapshot.
*
* @param progress The MatchProgress snapshot to restore from
*/ */
private void loadMatchProgress(final MatchProgress progress) { private void loadMatchProgress(final MatchProgress progress) {
if (progress == null || mPlayerStates.isEmpty()) return; if (progress == null || mPlayerStates.isEmpty()) return;
@@ -336,6 +474,10 @@ public class GameManager {
/** /**
* Handles the win condition when a player finishes on zero with a double. * Handles the win condition when a player finishes on zero with a double.
* Updates statistics, records dart hits, increments match counters, saves the
* completed match to the database, and notifies the UI layer.
*
* @param winner The PlayerState of the player who won the match
*/ */
private void handleWin(final PlayerState winner) { private void handleWin(final PlayerState winner) {
final int dartsThrown = mCurrentTurnDarts.size(); final int dartsThrown = mCurrentTurnDarts.size();
@@ -451,6 +593,9 @@ public class GameManager {
/** /**
* Saves the current match progress to the database. * Saves the current match progress to the database.
* Creates a MatchProgress snapshot of the current game state (player scores,
* darts thrown, active player) and persists it to the database.
* Executes asynchronously on a background thread.
*/ */
private void saveMatchProgress() { private void saveMatchProgress() {
final MatchProgress progress = new MatchProgress(); final MatchProgress progress = new MatchProgress();
@@ -490,6 +635,10 @@ public class GameManager {
/** /**
* Saves the completed match to the database. * Saves the completed match to the database.
* Marks the match as COMPLETED, saves the final game state, and updates
* the timestamp. Executes asynchronously on a background thread.
*
* @param winner The PlayerState of the winning player (used for logging)
*/ */
private void saveCompletedMatch(final PlayerState winner) { private void saveCompletedMatch(final PlayerState winner) {
if (mMatchId <= 0) return; if (mMatchId <= 0) return;
@@ -529,7 +678,13 @@ public class GameManager {
} }
/** /**
* Updates player statistics in the database. * Updates player statistics in the database after a turn.
* Convenience method that delegates to the overloaded version with checkoutValue=0.
*
* @param active The PlayerState whose statistics should be updated
* @param dartsThrown Number of darts thrown in this turn
* @param pointsMade Total points scored in this turn
* @param wasBust Whether this turn resulted in a bust
*/ */
private void updatePlayerStats(final PlayerState active, final int dartsThrown, final int pointsMade, private void updatePlayerStats(final PlayerState active, final int dartsThrown, final int pointsMade,
final boolean wasBust) { final boolean wasBust) {
@@ -538,6 +693,14 @@ public class GameManager {
/** /**
* Updates player statistics in the database with optional checkout value. * Updates player statistics in the database with optional checkout value.
* Tracks darts thrown, points made, bust count, and successful checkouts.
* Executes asynchronously on a background thread.
*
* @param active The PlayerState whose statistics should be updated
* @param dartsThrown Number of darts thrown in this turn
* @param pointsMade Total points scored in this turn
* @param wasBust Whether this turn resulted in a bust
* @param checkoutValue The checkout score if this was a winning turn (0 if not)
*/ */
private void updatePlayerStats(final PlayerState active, final int dartsThrown, final int pointsMade, private void updatePlayerStats(final PlayerState active, final int dartsThrown, final int pointsMade,
final boolean wasBust, final int checkoutValue) { final boolean wasBust, final int checkoutValue) {
@@ -555,13 +718,20 @@ public class GameManager {
/** /**
* Tracks a double-out attempt in player statistics. * Tracks a double-out attempt in player statistics.
* Records whether a player attempted to finish on a double and whether
* they succeeded or missed. Executes asynchronously on a background thread.
*
* @param playerState The PlayerState of the player who attempted the double
* @param isMissed true if the double-out attempt was missed, false if successful
*/ */
private void trackDoubleAttempt(final PlayerState playerState, final boolean isMissed) { private void trackDoubleAttempt(final PlayerState playerState, final boolean isMissed) {
new Thread(() -> mDatabaseHelper.trackDoubleAttempt(playerState.playerId, isMissed)).start(); new Thread(() -> mDatabaseHelper.trackDoubleAttempt(playerState.playerId, isMissed)).start();
} }
/** /**
* Increments matches played counter for all players. * Increments matches played counter for all players in the current match.
* Called when a match is completed to update the match count for all
* participating players. Executes asynchronously on a background thread.
*/ */
private void incrementMatchesPlayed() { private void incrementMatchesPlayed() {
final List<Long> playerIds = new ArrayList<>(); final List<Long> playerIds = new ArrayList<>();
@@ -572,7 +742,13 @@ public class GameManager {
} }
/** /**
* Records dart hits to player statistics. * Records all dart hits from a confirmed turn to player statistics.
* Updates the hit distribution map for heat map visualization.
* Only called after turn is confirmed to avoid recording unconfirmed throws.
* Executes asynchronously on a background thread.
*
* @param playerState The PlayerState whose statistics should be updated
* @param dartHits List of DartHit objects representing the darts thrown in this turn
*/ */
private void recordTurnHitsToStatistics(final PlayerState playerState, final List<DartHit> dartHits) { private void recordTurnHitsToStatistics(final PlayerState playerState, final List<DartHit> dartHits) {
if (dartHits.isEmpty()) return; if (dartHits.isEmpty()) return;
@@ -605,54 +781,115 @@ public class GameManager {
// Getters for Game State // Getters for Game State
// ======================================================================================== // ========================================================================================
/**
* Gets the database ID of the current match.
*
* @return The match ID, or -1 if no match is loaded
*/
public int getMatchId() { public int getMatchId() {
return mMatchId; return mMatchId;
} }
/**
* Gets the starting score for this X01 game.
*
* @return The starting score (e.g., 501, 301, 701)
*/
public int getStartingScore() { public int getStartingScore() {
return mStartingScore; return mStartingScore;
} }
/**
* Gets the index of the currently active player.
*
* @return The active player index (0 to playerCount-1)
*/
public int getActivePlayerIndex() { public int getActivePlayerIndex() {
return mActivePlayerIndex; return mActivePlayerIndex;
} }
/**
* Gets the current dart multiplier setting.
*
* @return The multiplier (1=Single, 2=Double, 3=Triple)
*/
public int getMultiplier() { public int getMultiplier() {
return mMultiplier; return mMultiplier;
} }
/**
* Gets a copy of all player states in this match.
* Returns a new list to prevent external modification of the internal state.
*
* @return A new ArrayList containing all PlayerState objects
*/
public List<PlayerState> getPlayerStates() { public List<PlayerState> getPlayerStates() {
return new ArrayList<>(mPlayerStates); return new ArrayList<>(mPlayerStates);
} }
/**
* Gets the currently active player's state.
*
* @return The active PlayerState, or null if no players are loaded
*/
public PlayerState getActivePlayer() { public PlayerState getActivePlayer() {
if (mPlayerStates.isEmpty()) return null; if (mPlayerStates.isEmpty()) return null;
return mPlayerStates.get(mActivePlayerIndex); return mPlayerStates.get(mActivePlayerIndex);
} }
/**
* Gets a copy of the darts thrown in the current turn.
* Returns a new list to prevent external modification.
*
* @return A new ArrayList containing the point values of darts thrown
*/
public List<Integer> getCurrentTurnDarts() { public List<Integer> getCurrentTurnDarts() {
return new ArrayList<>(mCurrentTurnDarts); return new ArrayList<>(mCurrentTurnDarts);
} }
/**
* Gets a copy of the detailed dart hits in the current turn.
* Returns a new list to prevent external modification.
*
* @return A new ArrayList containing DartHit objects for this turn
*/
public List<DartHit> getCurrentTurnDartHits() { public List<DartHit> getCurrentTurnDartHits() {
return new ArrayList<>(mCurrentTurnDartHits); return new ArrayList<>(mCurrentTurnDartHits);
} }
/**
* Checks if the current turn is over (bust, win, or 3 darts thrown).
*
* @return true if the turn is complete and should be submitted
*/
public boolean isTurnOver() { public boolean isTurnOver() {
return mIsTurnOver; return mIsTurnOver;
} }
/**
* Checks if the current turn resulted in a bust.
*
* @return true if the current turn is a bust (score invalid)
*/
public boolean isBustedTurn() { public boolean isBustedTurn() {
return mIsBustedTurn; return mIsBustedTurn;
} }
/**
* Checks if the match has been completed (someone won).
*
* @return true if a player has won and the match is finished
*/
public boolean isMatchCompleted() { public boolean isMatchCompleted() {
return mIsMatchCompleted; return mIsMatchCompleted;
} }
/** /**
* Calculates the current target score (remaining - current turn darts). * Calculates the current target score (remaining score minus current turn darts).
* If turn is busted, returns the remaining score without subtracting bust darts. * If the turn is busted, returns the remaining score without subtracting bust darts
* since those darts don't count.
*
* @return The effective remaining score after considering current turn darts
*/ */
public int getCurrentTarget() { public int getCurrentTarget() {
PlayerState active = getActivePlayer(); PlayerState active = getActivePlayer();
@@ -669,6 +906,9 @@ public class GameManager {
/** /**
* Gets the number of darts remaining in the current turn. * Gets the number of darts remaining in the current turn.
* A turn consists of up to 3 darts.
*
* @return Number of darts remaining (0-3)
*/ */
public int getDartsRemainingInTurn() { public int getDartsRemainingInTurn() {
return 3 - mCurrentTurnDarts.size(); return 3 - mCurrentTurnDarts.size();
@@ -678,30 +918,55 @@ public class GameManager {
// Callback Notification Methods // Callback Notification Methods
// ======================================================================================== // ========================================================================================
/**
* Notifies the UI callback that the general game state has changed.
* Used for updating scores, player names, averages, and checkout suggestions.
* Null-safe - does nothing if no callback is registered.
*/
private void notifyGameStateChanged() { private void notifyGameStateChanged() {
if (mCallback != null) { if (mCallback != null) {
mCallback.onGameStateChanged(); mCallback.onGameStateChanged();
} }
} }
/**
* Notifies the UI callback that the turn indicators (dart pills) should be updated.
* Called whenever darts are thrown or undone.
* Null-safe - does nothing if no callback is registered.
*/
private void notifyTurnIndicatorsChanged() { private void notifyTurnIndicatorsChanged() {
if (mCallback != null) { if (mCallback != null) {
mCallback.onTurnIndicatorsChanged(); mCallback.onTurnIndicatorsChanged();
} }
} }
/**
* Notifies the UI callback that a bust has occurred.
* Triggers bust animations, sounds, and visual feedback.
* Null-safe - does nothing if no callback is registered.
*/
private void notifyBust() { private void notifyBust() {
if (mCallback != null) { if (mCallback != null) {
mCallback.onBust(); mCallback.onBust();
} }
} }
/**
* Notifies the UI callback that a perfect 180 was scored.
* Triggers celebration animations, sounds, and vibrations.
* Null-safe - does nothing if no callback is registered.
*/
private void notifyOneEighty() { private void notifyOneEighty() {
if (mCallback != null) { if (mCallback != null) {
mCallback.onOneEightyScored(); mCallback.onOneEightyScored();
} }
} }
/**
* Notifies the UI callback to reset visual effects (bust overlays, colors).
* Called when starting a new turn or undoing darts.
* Null-safe - does nothing if no callback is registered.
*/
private void notifyResetVisuals() { private void notifyResetVisuals() {
if (mCallback != null) { if (mCallback != null) {
mCallback.onResetVisuals(); mCallback.onResetVisuals();

View File

@@ -21,29 +21,108 @@ import java.util.Map;
/** /**
* HeatmapView: A custom high-performance rendering component that draws a * HeatmapView: A custom high-performance rendering component that draws a
* dartboard and overlays player performance data as a color-coded heatmap. * dartboard and overlays player performance data as a color-coded heatmap.
* <p>
* This custom View renders a professional dartboard visualization with color-coded
* performance data overlaid on each segment (singles, doubles, triples, and bulls).
* The heatmap uses a gradient from "cold" (low frequency) to "hot" (high frequency)
* based on how often a player hits each segment.
* <p>
* Optimized Palette: * Optimized Palette:
* - Zero hits: Subtle semi-transparent "ghost" segments. * - Zero hits: Subtle semi-transparent "ghost" segments (#1AFFFFFF)
* - Low frequency: Volt Green (Starting "cool" color). * - Low frequency: Volt Green (Starting "cool" color from resources)
* - High frequency: Double Red (Intense "hot" color). * - High frequency: Double Red (Intense "hot" color from resources)
* <p>
* Performance optimizations:
* - Segment paths calculated once on size change and cached
* - Uses ArgbEvaluator for smooth color interpolation
* - Anti-aliased rendering for professional appearance
* - Wireframe overlay for visual definition
*/ */
public class HeatmapView extends View { public class HeatmapView extends View {
/**
* Base paint object used for rendering all dartboard segments.
* Configured with anti-aliasing for smooth edges and curves.
* Reused across all drawing operations for efficiency.
*/
private final Paint mBasePaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint mBasePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/**
* Color evaluator for interpolating between cold (green) and hot (red) colors.
* Evaluates intermediate colors based on normalized hit frequency (0.0 to 1.0).
*/
private final ArgbEvaluator mColorEvaluator = new ArgbEvaluator(); private final ArgbEvaluator mColorEvaluator = new ArgbEvaluator();
// Geometry configuration /**
private float mCenterX, mCenterY, mRadius; * X-coordinate of the dartboard center in pixels.
* Calculated from view width in onSizeChanged.
*/
private float mCenterX;
/**
* Y-coordinate of the dartboard center in pixels.
* Calculated from view height in onSizeChanged.
*/
private float mCenterY;
/**
* Radius of the dartboard in pixels.
* Calculated as 95% of the minimum dimension (width or height) to leave margin.
* All segment boundaries are defined as factors of this radius.
*/
private float mRadius;
/**
* Cache of pre-calculated Path objects for all dartboard segments.
* Key format: "d" + number (doubles), "t" + number (triples),
* "s" + number + "_inner/outer" (singles), "sb" (single bull), "db" (double bull).
* Calculated once in calculatePaths() and reused for efficient rendering.
*/
private final Map<String, Path> mSegmentPaths = new HashMap<>(); private final Map<String, Path> mSegmentPaths = new HashMap<>();
/**
* Player statistics object containing hit distribution data.
* Used to determine heatmap colors for each segment.
* Can be null if no statistics are loaded.
*/
private Statistics mStats; private Statistics mStats;
// Standard Dartboard Segment Order (clockwise starting from 20 at the top) /**
* Standard dartboard segment order (clockwise starting from 20 at the top).
* This array defines the physical layout of numbers around the dartboard
* as per official dartboard specifications.
*/
private static final int[] BOARD_NUMBERS = { private static final int[] BOARD_NUMBERS = {
20, 1, 18, 4, 13, 6, 10, 15, 2, 17, 3, 19, 7, 16, 8, 11, 14, 9, 12, 5 20, 1, 18, 4, 13, 6, 10, 15, 2, 17, 3, 19, 7, 16, 8, 11, 14, 9, 12, 5
}; };
public HeatmapView(final Context context) { super(context); init(); } /**
public HeatmapView(final Context context, @Nullable final AttributeSet attrs) { super(context, attrs); init(); } * Constructs a new HeatmapView programmatically.
* Used when creating the view from code rather than XML inflation.
*
* @param context The Context the view is running in, through which it can access resources
*/
public HeatmapView(final Context context) {
super(context);
init();
}
/**
* Constructs a new HeatmapView from XML.
* Used when inflating the view from an XML layout.
*
* @param context The Context the view is running in, through which it can access resources
* @param attrs The attributes of the XML tag that is inflating the view
*/
public HeatmapView(final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* Initializes the view by configuring the base paint style.
* Sets the paint to FILL mode for rendering solid segment colors.
*/
private void init() { private void init() {
mBasePaint.setStyle(Paint.Style.FILL); mBasePaint.setStyle(Paint.Style.FILL);
} }
@@ -56,46 +135,85 @@ public class HeatmapView extends View {
invalidate(); invalidate();
} }
/**
* Called when the size of this view has changed.
* Recalculates the dartboard geometry (center point and radius) and regenerates
* all segment paths based on the new dimensions.
*
* @param w Current width of this view
* @param h Current height of this view
* @param oldw Old width of this view (before size change)
* @param oldh Old height of this view (before size change)
*/
@Override @Override
protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) { protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
super.onSizeChanged(w, h, oldw, oldh); super.onSizeChanged(w, h, oldw, oldh);
mCenterX = w / 2f; mCenterX = w / 2f;
mCenterY = h / 2f; mCenterY = h / 2f;
mRadius = Math.min(w, h) / 2.1f; // Leave a small margin mRadius = Math.min(w, h) / 2.1f; // Leave a small margin (~5%)
calculatePaths(); calculatePaths();
} }
/** /**
* Calculates the Path for every segment on the board based on the view size. * Calculates the Path objects for every segment on the dartboard based on current view size.
* <p>
* Clears the existing path cache and regenerates all paths for:
* - 20 double ring segments (90-100% radius)
* - 20 outer single ring segments (60-90% radius)
* - 20 triple ring segments (50-60% radius)
* - 20 inner single ring segments (15-50% radius)
* - Single bull (15% radius circle)
* - Double bull (7% radius circle)
* <p>
* The board is oriented with number 20 at the top (12 o'clock position).
* Segments are arranged clockwise according to the standard dartboard layout.
*/ */
private void calculatePaths() { private void calculatePaths() {
mSegmentPaths.clear(); mSegmentPaths.clear();
final float angleStep = 360f / 20f; final float angleStep = 360f / 20f; // 18 degrees per segment
final float startOffset = -90f - (angleStep / 2f); // Center 20 at the top final float startOffset = -90f - (angleStep / 2f); // Center 20 at the top (-99 degrees)
for (int i = 0; i < BOARD_NUMBERS.length; i++) { for (int i = 0; i < BOARD_NUMBERS.length; i++) {
final int num = BOARD_NUMBERS[i]; final int num = BOARD_NUMBERS[i];
final float startAngle = startOffset + (i * angleStep); final float startAngle = startOffset + (i * angleStep);
// Define concentric ring boundaries as percentages of radius // Define concentric ring boundaries as percentages of radius
mSegmentPaths.put("d" + num, createArcPath(0.90f, 1.00f, startAngle, angleStep)); // Double mSegmentPaths.put("d" + num, createArcPath(0.90f, 1.00f, startAngle, angleStep)); // Double ring
mSegmentPaths.put("s" + num + "_outer", createArcPath(0.60f, 0.90f, startAngle, angleStep)); // Outer Single mSegmentPaths.put("s" + num + "_outer", createArcPath(0.60f, 0.90f, startAngle, angleStep)); // Outer single
mSegmentPaths.put("t" + num, createArcPath(0.50f, 0.60f, startAngle, angleStep)); // Triple mSegmentPaths.put("t" + num, createArcPath(0.50f, 0.60f, startAngle, angleStep)); // Triple ring
mSegmentPaths.put("s" + num + "_inner", createArcPath(0.15f, 0.50f, startAngle, angleStep)); // Inner Single mSegmentPaths.put("s" + num + "_inner", createArcPath(0.15f, 0.50f, startAngle, angleStep)); // Inner single
} }
// Bulls are simple circles // Bulls are simple circles (no angular segments)
final Path sbPath = new Path(); final Path sbPath = new Path();
sbPath.addCircle(mCenterX, mCenterY, mRadius * 0.15f, Path.Direction.CW); sbPath.addCircle(mCenterX, mCenterY, mRadius * 0.15f, Path.Direction.CW);
mSegmentPaths.put("sb", sbPath); mSegmentPaths.put("sb", sbPath); // Single bull
final Path dbPath = new Path(); final Path dbPath = new Path();
dbPath.addCircle(mCenterX, mCenterY, mRadius * 0.07f, Path.Direction.CW); dbPath.addCircle(mCenterX, mCenterY, mRadius * 0.07f, Path.Direction.CW);
mSegmentPaths.put("db", dbPath); mSegmentPaths.put("db", dbPath); // Double bull (bullseye)
} }
/**
* Creates a closed Path representing a wedge-shaped arc segment of the dartboard.
* <p>
* Generates a path for a single dartboard segment (e.g., the triple 20 region)
* by drawing an arc along the outer boundary, then an arc along the inner boundary
* in reverse, creating a closed wedge shape.
* <p>
* The path is constructed by:
* 1. Drawing an outer arc from startAngle to startAngle + sweep
* 2. Drawing an inner arc from startAngle + sweep back to startAngle (negative sweep)
* 3. Closing the path to connect the endpoints
*
* @param innerFactor Ratio of inner radius to base radius (0.0 to 1.0). E.g., 0.50 = 50% of radius
* @param outerFactor Ratio of outer radius to base radius (0.0 to 1.0). E.g., 1.00 = 100% of radius
* @param startAngle Starting angle in degrees (0° = right, increasing clockwise)
* @param sweep Angular sweep in degrees (typically 18° for standard dartboard segments)
* @return A closed Path object representing the arc segment
*/
private Path createArcPath(final float innerFactor, final float outerFactor, final float startAngle, final float sweep) { private Path createArcPath(final float innerFactor, final float outerFactor, final float startAngle, final float sweep) {
final Path path = new Path(); final Path path = new Path();
final RectF outerRect = new RectF( final RectF outerRect = new RectF(
@@ -113,28 +231,50 @@ public class HeatmapView extends View {
return path; return path;
} }
/**
* Renders the dartboard heatmap visualization to the canvas.
* <p>
* Drawing process:
* 1. Early exit if no statistics are loaded
* 2. Resolve color palette from resources (cold/hot) and hardcoded (empty)
* 3. For each dartboard segment:
* - Determine hit count from statistics
* - If zero hits: use subtle ghost color
* - If hits exist: interpolate between cold and hot based on normalized frequency
* - Draw the filled path with the calculated color
* 4. Draw wireframe overlay on all segments for visual definition
* <p>
* Color mapping:
* - 0 hits → #1AFFFFFF (10% white, subtle ghost)
* - Low frequency → R.color.volt_green (cold, starting point)
* - High frequency → R.color.double_red (hot, end point)
* - Intermediate → Linear interpolation between cold and hot
*
* @param canvas The canvas on which the background will be drawn
*/
@Override @Override
protected void onDraw(@NonNull final Canvas canvas) { protected void onDraw(@NonNull final Canvas canvas) {
super.onDraw(canvas); super.onDraw(canvas);
if (mStats == null) return; if (mStats == null) return;
// Resolve branding colors from resources // Resolve branding colors from resources
int coldColor = ContextCompat.getColor(getContext(), R.color.volt_green); final int coldColor = ContextCompat.getColor(getContext(), R.color.volt_green);
int hotColor = ContextCompat.getColor(getContext(), R.color.double_red); final int hotColor = ContextCompat.getColor(getContext(), R.color.double_red);
int emptyColor = Color.parseColor("#1AFFFFFF"); // Subtle ghost segments for zero data final int emptyColor = Color.parseColor("#1AFFFFFF"); // Subtle ghost segments for zero data
for (final Map.Entry<String, Path> entry : mSegmentPaths.entrySet()) { for (final Map.Entry<String, Path> entry : mSegmentPaths.entrySet()) {
final String key = entry.getKey(); final String key = entry.getKey();
// Strip suffix for inner/outer singles (e.g., "s20_inner" -> "s20")
final String statsKey = key.contains("_") ? key.substring(0, key.indexOf("_")) : key; final String statsKey = key.contains("_") ? key.substring(0, key.indexOf("_")) : key;
// Check if there are any hits recorded for this segment // Check if there are any hits recorded for this segment
final Integer hitCount = mStats.getHitDistribution().get(statsKey); final Integer hitCount = mStats.getHitDistribution().get(statsKey);
int color; final int color;
if (hitCount == null || hitCount == 0) { if (hitCount == null || hitCount == 0) {
color = emptyColor; color = emptyColor;
} else { } else {
// Fetch the normalized heat (0.0 to 1.0) and evaluate against Green -> Red // Fetch the normalized heat (0.0 to 1.0) and evaluate against Green -> Red gradient
final float weight = mStats.getNormalizedWeight(statsKey); final float weight = mStats.getNormalizedWeight(statsKey);
color = (int) mColorEvaluator.evaluate(weight, coldColor, hotColor); color = (int) mColorEvaluator.evaluate(weight, coldColor, hotColor);
} }
@@ -143,13 +283,13 @@ public class HeatmapView extends View {
canvas.drawPath(entry.getValue(), mBasePaint); canvas.drawPath(entry.getValue(), mBasePaint);
} }
// Final wireframe overlay for professional aesthetics // Final wireframe overlay for professional aesthetics and segment definition
mBasePaint.setStyle(Paint.Style.STROKE); mBasePaint.setStyle(Paint.Style.STROKE);
mBasePaint.setStrokeWidth(1.2f); mBasePaint.setStrokeWidth(1.2f);
mBasePaint.setColor(Color.parseColor("#26FFFFFF")); mBasePaint.setColor(Color.parseColor("#26FFFFFF")); // 15% white for subtle outlines
for (final Path p : mSegmentPaths.values()) { for (final Path p : mSegmentPaths.values()) {
canvas.drawPath(p, mBasePaint); canvas.drawPath(p, mBasePaint);
} }
mBasePaint.setStyle(Paint.Style.FILL); mBasePaint.setStyle(Paint.Style.FILL); // Reset to fill mode for next draw cycle
} }
} }

View File

@@ -12,57 +12,94 @@ import com.aldo.apps.ochecompanion.R;
/** /**
* Preference fragment for the main menu settings screen. * Preference fragment for the main menu settings screen.
* Displays app-wide preferences including day/night mode and standard game mode selection. * <p>
* Displays app-wide preferences including:
* - Day/night mode configuration (auto system-follow or manual dark/light)
* - Standard game mode selection (501, 301, etc.)
* - Audio feedback toggle
* - Vibration feedback toggle
* <p>
* Preferences are automatically persisted to SharedPreferences by the AndroidX Preference library. * Preferences are automatically persisted to SharedPreferences by the AndroidX Preference library.
* This fragment implements custom behavior for:
* - Disabling manual day/night toggle when auto mode is enabled
* - Immediate theme application on preference changes
* - Icon updates for audio and vibration toggles
* <p>
* Theme changes trigger activity recreation to ensure proper system theme detection
* and UI consistency.
*/ */
public class MainMenuPreferencesFragment extends PreferenceFragmentCompat { public class MainMenuPreferencesFragment extends PreferenceFragmentCompat {
/**
* Tag for debug logging in this fragment.
* Used to identify log messages originating from preference interactions.
*/
private static final String TAG = "PreferencesFragment"; private static final String TAG = "PreferencesFragment";
/** /**
* Initializes the preference screen from the main_menu_preferences XML resource. * Initializes the preference screen from the main_menu_preferences XML resource.
* Called automatically by the fragment lifecycle. * <p>
* Called automatically by the fragment lifecycle. Sets up:
* <p>
* 1. Day/Night Mode Logic:
* - Auto mode (system-follow): Disables manual toggle
* - Manual mode: Enables light/dark toggle
* - Theme changes trigger immediate application and activity recreation
* <p>
* 2. Audio and Vibration Toggles:
* - Custom button-style preferences with dynamic icon updates
* - Icons change based on enabled/disabled state
* <p>
* The auto/manual mode relationship ensures users can't accidentally override
* system theme preferences when auto mode is enabled.
* *
* @param savedInstanceState Bundle containing saved state, or null if none exists * @param savedInstanceState Bundle containing saved state, or null if none exists
* @param rootKey Optional preference hierarchy root key * @param rootKey Optional preference hierarchy root key for nested preference screens
*/ */
@Override @Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
setPreferencesFromResource(R.xml.main_menu_preferences, rootKey); setPreferencesFromResource(R.xml.main_menu_preferences, rootKey);
// --- 1. Day/Night Auto-Disable Logic --- // --- 1. Day/Night Auto-Disable Logic ---
// Retrieve the auto (system-follow) and manual (dark mode toggle) preferences
final SwitchPreference autoPref = findPreference(getString(R.string.pref_key_day_night_mode_auto)); final SwitchPreference autoPref = findPreference(getString(R.string.pref_key_day_night_mode_auto));
// Use your string resource key here
final SwitchPreference manualPref = findPreference(getString(R.string.pref_key_day_night_mode)); final SwitchPreference manualPref = findPreference(getString(R.string.pref_key_day_night_mode));
if (autoPref != null && manualPref != null) { if (autoPref != null && manualPref != null) {
// Set initial state: If Auto is ON, Manual is DISABLED // Set initial state: If Auto is ON, Manual toggle is DISABLED
manualPref.setEnabled(!autoPref.isChecked()); manualPref.setEnabled(!autoPref.isChecked());
// Listen for auto mode changes
autoPref.setOnPreferenceChangeListener((preference, newValue) -> { autoPref.setOnPreferenceChangeListener((preference, newValue) -> {
boolean isAutoEnabled = (Boolean) newValue; final boolean isAutoEnabled = (Boolean) newValue;
Log.d(TAG, "Auto mode changed to: " + isAutoEnabled); Log.d(TAG, "Auto mode changed to: " + isAutoEnabled);
// Enable/disable manual toggle based on auto mode state
manualPref.setEnabled(!isAutoEnabled); manualPref.setEnabled(!isAutoEnabled);
// Apply theme immediately and recreate activity to ensure system theme is detected // Apply theme immediately
if (isAutoEnabled) { if (isAutoEnabled) {
// Switch to system-follow mode and recreate activity
Log.d(TAG, "Switching to MODE_NIGHT_FOLLOW_SYSTEM and recreating"); Log.d(TAG, "Switching to MODE_NIGHT_FOLLOW_SYSTEM and recreating");
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
// Recreate activity to properly apply system theme // Recreate activity to properly detect and apply system theme
requireActivity().recreate(); requireActivity().recreate();
} else { } else {
// Use current manual preference // Use current manual preference setting
boolean isDarkMode = manualPref.isChecked(); final boolean isDarkMode = manualPref.isChecked();
Log.d(TAG, "Switching to manual mode, dark mode = " + isDarkMode); Log.d(TAG, "Switching to manual mode, dark mode = " + isDarkMode);
AppCompatDelegate.setDefaultNightMode( AppCompatDelegate.setDefaultNightMode(
isDarkMode ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO isDarkMode ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO
); );
} }
return true; return true; // Accept the preference change
}); });
// Listen for manual mode changes
manualPref.setOnPreferenceChangeListener((preference, newValue) -> { manualPref.setOnPreferenceChangeListener((preference, newValue) -> {
boolean isDarkMode = (Boolean) newValue; final boolean isDarkMode = (Boolean) newValue;
Log.d(TAG, "Manual dark mode changed to: " + isDarkMode); Log.d(TAG, "Manual dark mode changed to: " + isDarkMode);
// Only apply if auto mode is disabled // Only apply if auto mode is disabled
if (!autoPref.isChecked()) { if (!autoPref.isChecked()) {
Log.d(TAG, "Applying manual theme change"); Log.d(TAG, "Applying manual theme change");
@@ -72,32 +109,52 @@ public class MainMenuPreferencesFragment extends PreferenceFragmentCompat {
} else { } else {
Log.d(TAG, "Ignoring manual change - auto mode is enabled"); Log.d(TAG, "Ignoring manual change - auto mode is enabled");
} }
return true; return true; // Accept the preference change
}); });
} }
// --- 2. Button Toggles for Audio and Vibration --- // --- 2. Button Toggles for Audio and Vibration ---
// Setup custom button-style toggles with icon updates
setupButtonToggle(getString(R.string.pref_key_audio_feedback), R.drawable.ic_audio_on, R.drawable.ic_audio_off); setupButtonToggle(getString(R.string.pref_key_audio_feedback), R.drawable.ic_audio_on, R.drawable.ic_audio_off);
setupButtonToggle(getString(R.string.pref_key_vibration_feedback), R.drawable.ic_vibration_on, R.drawable.ic_vibration_off); setupButtonToggle(getString(R.string.pref_key_vibration_feedback), R.drawable.ic_vibration_on, R.drawable.ic_vibration_off);
} }
private void setupButtonToggle(String key, int iconOn, int iconOff) { /**
* Sets up a custom button-style preference toggle with dynamic icon updates.
* <p>
* Creates a preference that behaves like a toggle button:
* - Initializes with the current saved state from SharedPreferences
* - Displays the appropriate icon (on/off) based on state
* - Toggles state on click
* - Updates icon immediately to reflect new state
* - Persists state to SharedPreferences
* <p>
* This provides a more visual and interactive alternative to standard SwitchPreference
* for binary settings like audio and vibration feedback.
*
* @param key The preference key used to store the boolean value in SharedPreferences
* @param iconOn Drawable resource ID for the "enabled" state icon
* @param iconOff Drawable resource ID for the "disabled" state icon
*/
private void setupButtonToggle(final String key, final int iconOn, final int iconOff) {
final Preference pref = findPreference(key); final Preference pref = findPreference(key);
if (pref != null) { if (pref != null) {
// Initialize icon based on current saved value // Initialize icon based on current saved value (default to true if not set)
final boolean isEnabled = getPreferenceManager().getSharedPreferences().getBoolean(key, true); final boolean isEnabled = getPreferenceManager().getSharedPreferences().getBoolean(key, true);
pref.setIcon(isEnabled ? iconOn : iconOff); pref.setIcon(isEnabled ? iconOn : iconOff);
// Handle toggle on click
pref.setOnPreferenceClickListener(p -> { pref.setOnPreferenceClickListener(p -> {
boolean currentState = getPreferenceManager().getSharedPreferences().getBoolean(key, true); // Read current state
boolean newState = !currentState; final boolean currentState = getPreferenceManager().getSharedPreferences().getBoolean(key, true);
final boolean newState = !currentState;
// Save the new state // Save the new state to SharedPreferences
getPreferenceManager().getSharedPreferences().edit().putBoolean(key, newState).apply(); getPreferenceManager().getSharedPreferences().edit().putBoolean(key, newState).apply();
// Update the icon visually // Update the icon visually to reflect the new state
p.setIcon(newState ? iconOn : iconOff); p.setIcon(newState ? iconOn : iconOff);
return true; return true; // Consume the click event
}); });
} }
} }

View File

@@ -50,7 +50,10 @@ public class PlayerItemView extends MaterialCardView {
mTvStats = findViewById(R.id.tvPlayerAvg); mTvStats = findViewById(R.id.tvPlayerAvg);
} }
/** Binds player data to view components (username, stats, avatar). */ /**
* Binds player data to view components (username, stats, avatar).
* @param player The Player object containing data to display
*/
public void bind(@NonNull final Player player) { public void bind(@NonNull final Player player) {
mTvUsername.setText(player.username); mTvUsername.setText(player.username);
mTvStats.setText(String.format( mTvStats.setText(String.format(
@@ -66,6 +69,11 @@ public class PlayerItemView extends MaterialCardView {
} }
} }
/**
* Binds player data along with a specific score (e.g., match score) instead of career average.
* @param player The Player object containing data to display
* @param score The specific score to display (e.g., current match score)
*/
public void bindWithScore(@NonNull final Player player, final int score) { public void bindWithScore(@NonNull final Player player, final int score) {
mTvUsername.setText(player.username); mTvUsername.setText(player.username);

View File

@@ -28,33 +28,112 @@ import java.util.Set;
/** /**
* PlayerSelectionDialogFragment: A modern bottom-sheet selector for match participants. * PlayerSelectionDialogFragment: A modern bottom-sheet selector for match participants.
* Automatically pre-selects players from the most recent match for speed. * <p>
* This {@link BottomSheetDialogFragment} provides a user interface for selecting players
* from the database before starting a new match. It features:
* <ul>
* <li>Automatic pre-selection of players from the most recent match for speed</li>
* <li>Dynamic button state that displays the current selection count</li>
* <li>Integration with {@link PlayerSelectionAdapter} for multi-select functionality</li>
* <li>Validation to ensure at least one player is selected before starting</li>
* </ul>
* <p>
* The dialog automatically loads all players from the database on creation and queries
* the last match's participant data to pre-populate selections, improving user experience
* for consecutive matches with the same players.
*
* @see PlayerSelectionAdapter
* @see GameActivity
* @see MatchProgress
*/ */
public class PlayerSelectionDialogFragment extends BottomSheetDialogFragment { public class PlayerSelectionDialogFragment extends BottomSheetDialogFragment {
/** /**
* Tag for debugging and logging purposes. * Tag for debugging and logging purposes.
* Used for identifying this fragment in log output.
*/ */
private static final String TAG = "PlayerSelectionDialogFragment"; private static final String TAG = "PlayerSelectionDialogFragment";
/** /**
* The {@link List} of the selected {@link Player} for the match. * The complete list of all players loaded from the database.
* <p>
* This list is populated by {@link #loadData()} and displayed in the
* {@link RecyclerView} via the {@link PlayerSelectionAdapter}.
*/ */
private final List<Player> mAllPlayers = new ArrayList<>(); private final List<Player> mAllPlayers = new ArrayList<>();
/**
* The set of currently selected player IDs.
* <p>
* Maintained as a {@link Set} to ensure uniqueness and efficient lookup.
* Updated by the {@link PlayerSelectionAdapter} when users toggle selections.
* Pre-populated with IDs from the previous match for convenience.
*/
private final Set<Integer> mSelectedIds = new HashSet<>(); private final Set<Integer> mSelectedIds = new HashSet<>();
/**
* The {@link RecyclerView} that displays the list of selectable players.
* Populated with data from {@link #mAllPlayers}.
*/
private RecyclerView mRvSquad; private RecyclerView mRvSquad;
/**
* The confirmation button that initiates the match with selected players.
* <p>
* Displays the current selection count and is disabled when no players
* are selected. Handled by {@link #updateButtonState()}.
*/
private MaterialButton mBtnStart; private MaterialButton mBtnStart;
/**
* The adapter that manages the player selection list in the {@link RecyclerView}.
* <p>
* Handles player selection logic, visual feedback, and communicates
* selection changes via {@link #onSelectionChanged()}.
*/
private PlayerSelectionAdapter mAdapter; private PlayerSelectionAdapter mAdapter;
/**
* Creates and returns the view hierarchy associated with this fragment.
* <p>
* Inflates the player selection bottom sheet layout, which includes:
* <ul>
* <li>A {@link RecyclerView} for displaying the player list</li>
* <li>A {@link MaterialButton} for confirming the selection</li>
* </ul>
*
* @param inflater The {@link LayoutInflater} used to inflate views in the fragment
* @param container The parent view that the fragment's UI should be attached to,
* or {@code null} if not attached
* @param savedInstanceState If non-null, this fragment is being re-constructed from a
* previous saved state
* @return The root {@link View} of the inflated layout
*/
@Nullable @Nullable
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.layout_player_selection_sheet, container, false); return inflater.inflate(R.layout.layout_player_selection_sheet, container, false);
} }
/**
* Called immediately after {@link #onCreateView} has returned, but before any saved
* state has been restored into the view.
* <p>
* This method performs the following initialization steps:
* <ol>
* <li>Binds UI components ({@link RecyclerView}, {@link MaterialButton})</li>
* <li>Configures the {@link RecyclerView} with a {@link LinearLayoutManager}</li>
* <li>Initializes the {@link PlayerSelectionAdapter} with selection callback</li>
* <li>Sets up the confirmation button click listener</li>
* <li>Triggers asynchronous data loading via {@link #loadData()}</li>
* </ol>
*
* @param view The {@link View} returned by {@link #onCreateView}
* @param savedInstanceState If non-null, this fragment is being re-constructed from a
* previous saved state
*/
@Override @Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
mRvSquad = view.findViewById(R.id.rvSquadSelection); mRvSquad = view.findViewById(R.id.rvSquadSelection);
@@ -69,20 +148,37 @@ public class PlayerSelectionDialogFragment extends BottomSheetDialogFragment {
loadData(); loadData();
} }
/**
* Loads player data from the database asynchronously.
* <p>
* This method performs database operations on a background thread to avoid
* blocking the UI. The loading process includes:
* <ol>
* <li>Fetching all players from the database</li>
* <li>Retrieving the last match's participant data for pre-selection</li>
* <li>Parsing the last match data using {@link MatchProgressConverter}</li>
* <li>Extracting player IDs from the previous match</li>
* <li>Updating the UI on the main thread with loaded data</li>
* <li>Pre-selecting players who participated in the last match</li>
* </ol>
* <p>
* This pre-selection behavior significantly improves user experience when
* starting consecutive matches with the same group of players.
*/
private void loadData() { private void loadData() {
new Thread(() -> { new Thread(() -> {
AppDatabase db = AppDatabase.getDatabase(requireContext()); final AppDatabase db = AppDatabase.getDatabase(requireContext());
// 1. Get All Players // 1. Get All Players
List<Player> players = db.playerDao().getAllPlayers(); final List<Player> players = db.playerDao().getAllPlayers();
// 2. Get Last Participants for Pre-selection // 2. Get Last Participants for Pre-selection
String lastJson = db.matchDao().getLastMatchParticipantData(); final String lastJson = db.matchDao().getLastMatchParticipantData();
MatchProgress lastProgress = MatchProgressConverter.fromString(lastJson); final MatchProgress lastProgress = MatchProgressConverter.fromString(lastJson);
final Set<Integer> lastPlayerIds = new HashSet<>(); final Set<Integer> lastPlayerIds = new HashSet<>();
if (lastProgress != null && lastProgress.players != null) { if (lastProgress != null && lastProgress.players != null) {
for (MatchProgress.PlayerStateSnapshot p : lastProgress.players) { for (final MatchProgress.PlayerStateSnapshot p : lastProgress.players) {
lastPlayerIds.add((int) p.playerId); lastPlayerIds.add((int) p.playerId);
} }
} }
@@ -94,7 +190,7 @@ public class PlayerSelectionDialogFragment extends BottomSheetDialogFragment {
// Auto-select players from the previous session // Auto-select players from the previous session
mSelectedIds.clear(); mSelectedIds.clear();
for (Player p : mAllPlayers) { for (final Player p : mAllPlayers) {
if (lastPlayerIds.contains((int) p.id)) { if (lastPlayerIds.contains((int) p.id)) {
mSelectedIds.add((int) p.id); mSelectedIds.add((int) p.id);
} }
@@ -107,19 +203,59 @@ public class PlayerSelectionDialogFragment extends BottomSheetDialogFragment {
}).start(); }).start();
} }
/**
* Callback method invoked by {@link PlayerSelectionAdapter} when the selection state changes.
* <p>
* This method is triggered whenever a player is selected or deselected in the
* {@link RecyclerView}. It delegates to {@link #updateButtonState()} to reflect
* the current selection count in the UI.
* <p>
* This callback pattern allows the adapter to communicate selection changes
* back to the fragment without tight coupling.
*/
private void onSelectionChanged() { private void onSelectionChanged() {
updateButtonState(); updateButtonState();
} }
/**
* Updates the confirmation button's state based on the current selection.
* <p>
* This method performs two UI updates:
* <ul>
* <li><b>Enabled State:</b> The button is enabled only when at least one player
* is selected (count > 0), preventing match initiation with empty selections</li>
* <li><b>Text Display:</b> Shows "START MATCH (count)" when players are selected,
* or "SELECT PLAYERS" as a prompt when no selections are made</li>
* </ul>
* <p>
* This provides clear visual feedback about the current selection state and
* guides the user through the selection process.
*/
private void updateButtonState() { private void updateButtonState() {
int count = mSelectedIds.size(); final int count = mSelectedIds.size();
mBtnStart.setEnabled(count > 0); mBtnStart.setEnabled(count > 0);
mBtnStart.setText(count > 0 ? "START MATCH (" + count + ")" : "SELECT PLAYERS"); mBtnStart.setText(count > 0 ? "START MATCH (" + count + ")" : "SELECT PLAYERS");
} }
/**
* Initiates a new match with the currently selected players.
* <p>
* This method performs the following operations:
* <ol>
* <li>Constructs a list of selected {@link Player} objects by filtering
* {@link #mAllPlayers} based on {@link #mSelectedIds}</li>
* <li>Validates that at least one player is selected (early return if empty)</li>
* <li>Launches {@link GameActivity} with the selected players and default score
* ({@link DartsConstants#DEFAULT_GAME_SCORE})</li>
* <li>Dismisses this dialog fragment after successfully starting the match</li>
* </ol>
* <p>
* This method is triggered by the confirmation button click listener set up
* in {@link #onViewCreated}.
*/
private void initiateMatch() { private void initiateMatch() {
ArrayList<Player> selectedList = new ArrayList<>(); final ArrayList<Player> selectedList = new ArrayList<>();
for (Player p : mAllPlayers) { for (final Player p : mAllPlayers) {
if (mSelectedIds.contains((int) p.id)) { if (mSelectedIds.contains((int) p.id)) {
selectedList.add(p); selectedList.add(p);
} }

View File

@@ -17,27 +17,137 @@ import com.google.android.material.imageview.ShapeableImageView;
/** /**
* PlayerStatsView: A complete dashboard component that visualizes a player's * PlayerStatsView: A complete dashboard component that visualizes a player's
* career performance, including a heatmap and detailed metrics. * career performance, including a heatmap and detailed metrics.
* <p>
* This custom {@link ScrollView} component provides a comprehensive player statistics
* display that includes:
* <ul>
* <li>Player identity information (username and profile picture)</li>
* <li>High-level performance metrics (career average, first-9 average, checkout percentage, best finish)</li>
* <li>Threshold score counters (60+, 100+, 140+, and 180 counts)</li>
* <li>Interactive dartboard heatmap visualization via {@link HeatmapView}</li>
* </ul>
* <p>
* The view is inflated from a layout resource and automatically initializes all
* UI components. Data binding is performed via the {@link #bind(Player, Statistics)}
* method, which populates all fields with formatted player statistics.
*
* @see HeatmapView
* @see Player
* @see Statistics
*/ */
public class PlayerStatsView extends ScrollView { public class PlayerStatsView extends ScrollView {
/**
* Tag for debugging and logging purposes.
* Used for identifying this view in log output.
*/
private static final String TAG = "PlayerStatsView"; private static final String TAG = "PlayerStatsView";
// UI References /**
* The {@link HeatmapView} component that renders the player's dartboard
* hit distribution with color-coded performance visualization.
*/
private HeatmapView mHeatmap; private HeatmapView mHeatmap;
/**
* The circular profile image view that displays the player's avatar.
* Shows either a custom profile picture or a default user icon.
*/
private ShapeableImageView mIvAvatar; private ShapeableImageView mIvAvatar;
private TextView mTvUsername, mTvCareerAvg, mTvFirst9, mTvCheckoutPct, mTvBestFinish;
private TextView mTvCount60, mTvCount100, mTvCount140, mTvCount180; /**
* The {@link TextView} displaying the player's username in uppercase.
*/
private TextView mTvUsername;
/**
* The {@link TextView} displaying the player's overall career average score.
* Formatted to one decimal place.
*/
private TextView mTvCareerAvg;
/**
* The {@link TextView} displaying the player's first-9 darts average.
* This metric measures opening throw accuracy. Formatted to one decimal place.
*/
private TextView mTvFirst9;
/**
* The {@link TextView} displaying the player's checkout success percentage.
* Shows the ratio of successful checkouts to checkout opportunities.
* Formatted to one decimal place with a percentage symbol.
*/
private TextView mTvCheckoutPct;
/**
* The {@link TextView} displaying the player's highest checkout value.
* Represents the largest score successfully checked out in a single turn.
*/
private TextView mTvBestFinish;
/**
* The {@link TextView} displaying the count of turns scoring 60 points or more.
*/
private TextView mTvCount60;
/**
* The {@link TextView} displaying the count of turns scoring 100 points or more (century).
*/
private TextView mTvCount100;
/**
* The {@link TextView} displaying the count of turns scoring 140 points or more.
*/
private TextView mTvCount140;
/**
* The {@link TextView} displaying the count of perfect 180-point turns (maximum score).
*/
private TextView mTvCount180;
public PlayerStatsView(@NonNull final Context context) { public PlayerStatsView(@NonNull final Context context) {
this(context, null); this(context, null);
} }
/**
* Constructs a new {@link PlayerStatsView} with the specified context and attributes.
* <p>
* This constructor inflates the player stats layout and initializes all UI components.
* It performs the following operations:
* <ol>
* <li>Calls the superclass constructor with context and attributes</li>
* <li>Inflates the {@code R.layout.player_stats_layout} resource into this view</li>
* <li>Initializes all child views via {@link #initViews()}</li>
* </ol>
* <p>
* This constructor is typically invoked when the view is inflated from XML.
*
* @param context The {@link Context} in which the view is running, used for
* accessing resources and inflating layouts
* @param attrs The attributes of the XML tag that is inflating the view,
* or {@code null} if not inflated from XML
*/
public PlayerStatsView(@NonNull final Context context, @Nullable final AttributeSet attrs) { public PlayerStatsView(@NonNull final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs); super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.player_stats_layout, this, true); LayoutInflater.from(context).inflate(R.layout.player_stats_layout, this, true);
initViews(); initViews();
} }
/**
* Initializes all UI component references by finding views in the inflated layout.
* <p>
* This method binds the following UI components:
* <ul>
* <li><b>Identity:</b> Profile image and username text view</li>
* <li><b>Performance Metrics:</b> Career average, first-9 average, checkout percentage,
* and best finish text views</li>
* <li><b>Threshold Counters:</b> Four text views for 60+, 100+, 140+, and 180 counts</li>
* <li><b>Visualization:</b> The {@link HeatmapView} for dartboard hit distribution</li>
* </ul>
* <p>
* This method is called once during construction and establishes references
* that are used throughout the view's lifecycle for data binding.
*/
private void initViews() { private void initViews() {
mHeatmap = findViewById(R.id.statsHeatmap); mHeatmap = findViewById(R.id.statsHeatmap);
mIvAvatar = findViewById(R.id.ivPlayerAvatar); mIvAvatar = findViewById(R.id.ivPlayerAvatar);
@@ -56,8 +166,26 @@ public class PlayerStatsView extends ScrollView {
/** /**
* Binds both the player identity and their accumulated stats to the UI. * Binds both the player identity and their accumulated stats to the UI.
* <p>
* This method populates all UI components with the provided player and statistics data.
* The binding process is organized into four logical sections:
* <ol>
* <li><b>Identity:</b> Displays the player's username in uppercase and loads their
* profile picture using Glide (or shows a default icon if no picture is set)</li>
* <li><b>High-Level Metrics:</b> Formats and displays career average, first-9 average,
* checkout percentage (with % symbol), and highest checkout value</li>
* <li><b>Threshold Totals:</b> Displays counts for 60+, 100+, 140+, and 180 point turns</li>
* <li><b>Heatmap Rendering:</b> Passes statistics to the {@link HeatmapView} for
* visual dartboard representation</li>
* </ol>
* <p>
* If either parameter is {@code null}, the method logs an error and returns early
* without modifying the UI.
*
* @param player The {@link Player} object containing identity information (username, profile picture)
* @param stats The {@link Statistics} object containing all accumulated performance metrics
*/ */
public void bind(@NonNull final Player player, final @NonNull Statistics stats) { public void bind(@NonNull final Player player, @NonNull final Statistics stats) {
if (player == null || stats == null) { if (player == null || stats == null) {
Log.e(TAG, "bind: Cannot bind, return"); Log.e(TAG, "bind: Cannot bind, return");
return; return;

View File

@@ -18,62 +18,159 @@ import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
* PlayerSelectionAdapter: Optimized for the "Squad Selector" Bottom Sheet. * PlayerSelectionAdapter: Optimized RecyclerView adapter for the "Squad Selector" Bottom Sheet.
* Features specialized selection visual states and haptic feedback. * <p>
* This adapter manages the display and selection of players in a multi-select interface.
* Features include:
* - Visual selection states (stroke color, width, overlay indicator)
* - Haptic feedback on selection toggle
* - Maximum selection limit enforcement (8 players - PDC/Standard limit)
* - Real-time UI updates via callback
* <p>
* The adapter uses a shared mutable Set to track selected player IDs, allowing
* external components to observe and modify the selection state.
*/ */
public class PlayerSelectionAdapter extends RecyclerView.Adapter<PlayerSelectionAdapter.SelectionHolder> { public class PlayerSelectionAdapter extends RecyclerView.Adapter<PlayerSelectionAdapter.SelectionHolder> {
/**
* List of all available players to display in the selection interface.
* This list is immutable after adapter construction.
*/
private final List<Player> mPlayers; private final List<Player> mPlayers;
/**
* Set of selected player IDs (as integers).
* This set is shared with external components and modified in-place when
* players are selected or deselected. Maximum size is enforced at 8 players.
*/
private final Set<Integer> mSelectedIds; private final Set<Integer> mSelectedIds;
/**
* Callback executed whenever the selection state changes.
* Used to notify parent components that they should update their UI
* or state based on the new selection.
*/
private final Runnable mOnChanged; private final Runnable mOnChanged;
public PlayerSelectionAdapter(List<Player> players, Set<Integer> selected, Runnable onChanged) { /**
* Constructs a new PlayerSelectionAdapter with the specified players and selection state.
*
* @param players List of Player objects to display. Must not be null.
* @param selected Mutable Set of selected player IDs. Modified in-place by the adapter.
* @param onChanged Callback executed when selection changes. Can be null if no callback needed.
*/
public PlayerSelectionAdapter(final List<Player> players, final Set<Integer> selected, final Runnable onChanged) {
this.mPlayers = players; this.mPlayers = players;
this.mSelectedIds = selected; this.mSelectedIds = selected;
this.mOnChanged = onChanged; this.mOnChanged = onChanged;
} }
/**
* Creates a new ViewHolder for displaying a player selection item.
* Inflates the item_player_selection layout and wraps it in a SelectionHolder.
*
* @param parent The ViewGroup into which the new View will be added
* @param viewType The view type of the new View (unused, all items use same layout)
* @return A new SelectionHolder that holds a View of the given view type
*/
@NonNull @NonNull
@Override @Override
public SelectionHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public SelectionHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_player_selection, parent, false); final View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_player_selection, parent, false);
return new SelectionHolder(v); return new SelectionHolder(v);
} }
/**
* Binds player data to the ViewHolder and sets up click handling.
* <p>
* Updates the visual state based on selection status and configures a click listener
* that:
* - Provides haptic feedback (20ms vibration)
* - Toggles selection state (with 8-player maximum enforcement)
* - Triggers UI update via notifyItemChanged
* - Invokes the onChanged callback
*
* @param holder The ViewHolder to bind data to
* @param position The position of the item within the adapter's data set
*/
@Override @Override
public void onBindViewHolder(@NonNull SelectionHolder holder, int position) { public void onBindViewHolder(@NonNull final SelectionHolder holder, final int position) {
Player p = mPlayers.get(position); final Player p = mPlayers.get(position);
holder.bind(p, mSelectedIds.contains((int) p.id)); holder.bind(p, mSelectedIds.contains((int) p.id));
holder.itemView.setOnClickListener(v -> { holder.itemView.setOnClickListener(v -> {
// Haptic Feedback // Haptic Feedback (20ms vibration for tactile response)
Vibrator vib = (Vibrator) v.getContext().getSystemService(Context.VIBRATOR_SERVICE); final Vibrator vib = (Vibrator) v.getContext().getSystemService(Context.VIBRATOR_SERVICE);
if (vib != null) vib.vibrate(20); if (vib != null) vib.vibrate(20);
if (mSelectedIds.contains((int) p.id)) { if (mSelectedIds.contains((int) p.id)) {
// Deselect player
mSelectedIds.remove((int) p.id); mSelectedIds.remove((int) p.id);
} else { } else {
// Select player (with maximum limit enforcement)
if (mSelectedIds.size() < 8) { // PDC/Standard limit if (mSelectedIds.size() < 8) { // PDC/Standard limit
mSelectedIds.add((int) p.id); mSelectedIds.add((int) p.id);
} }
} }
notifyItemChanged(position); notifyItemChanged(position);
mOnChanged.run();
// Notify parent component of selection change
if (mOnChanged != null) {
mOnChanged.run();
}
}); });
} }
/**
* Returns the total number of players in the adapter.
*
* @return The number of players available for selection
*/
@Override @Override
public int getItemCount() { public int getItemCount() {
return mPlayers.size(); return mPlayers.size();
} }
/**
* ViewHolder for player selection items.
* <p>
* Manages the visual representation of a single player in the selection list,
* including avatar image, name display, card styling, and selection indicator overlay.
* <p>
* Visual states:
* - Selected: Green stroke (4dp), visible selection overlay
* - Unselected: Subtle border (1dp), hidden selection overlay
*/
static class SelectionHolder extends RecyclerView.ViewHolder { static class SelectionHolder extends RecyclerView.ViewHolder {
/**
* Circular avatar image view displaying the player's profile picture.
* Shows default user icon if no profile picture is available.
*/
private final ShapeableImageView ivAvatar; private final ShapeableImageView ivAvatar;
/**
* TextView displaying the player's username.
*/
private final TextView tvName; private final TextView tvName;
/**
* Material card view that wraps the entire item.
* Used for applying selection-based stroke styling (color and width).
*/
private final MaterialCardView card; private final MaterialCardView card;
/**
* Visual overlay indicator shown when the player is selected.
* Typically a checkmark or tinted overlay for visual feedback.
*/
private final View selectionIndicator; private final View selectionIndicator;
public SelectionHolder(@NonNull View itemView) { /**
* Constructs a new SelectionHolder and initializes all view references.
*
* @param itemView The root view of the item layout
*/
public SelectionHolder(@NonNull final View itemView) {
super(itemView); super(itemView);
ivAvatar = itemView.findViewById(R.id.ivPlayerProfile); ivAvatar = itemView.findViewById(R.id.ivPlayerProfile);
tvName = itemView.findViewById(R.id.tvPlayerName); tvName = itemView.findViewById(R.id.tvPlayerName);
@@ -81,16 +178,34 @@ public class PlayerSelectionAdapter extends RecyclerView.Adapter<PlayerSelection
selectionIndicator = itemView.findViewById(R.id.selectionOverlay); selectionIndicator = itemView.findViewById(R.id.selectionOverlay);
} }
public void bind(Player p, boolean isSelected) { /**
* Binds player data to the ViewHolder and updates the visual selection state.
* <p>
* Sets the player's name and avatar, then applies visual styling based on
* whether the player is currently selected:
* <p>
* Selected state:
* - Card stroke: volt_green color, 4dp width
* - Selection indicator: visible
* <p>
* Unselected state:
* - Card stroke: border_subtle color, 1dp width
* - Selection indicator: gone
*
* @param p The Player object containing name and profile picture data
* @param isSelected true if this player is currently selected, false otherwise
*/
public void bind(final Player p, final boolean isSelected) {
tvName.setText(p.username); tvName.setText(p.username);
// Load profile picture or default icon
if (p.profilePictureUri != null) { if (p.profilePictureUri != null) {
Glide.with(itemView.getContext()).load(p.profilePictureUri).into(ivAvatar); Glide.with(itemView.getContext()).load(p.profilePictureUri).into(ivAvatar);
} else { } else {
ivAvatar.setImageResource(R.drawable.ic_users); ivAvatar.setImageResource(R.drawable.ic_users);
} }
// Visual toggle // Apply visual selection state
if (isSelected) { if (isSelected) {
card.setStrokeColor(ContextCompat.getColor(itemView.getContext(), R.color.volt_green)); card.setStrokeColor(ContextCompat.getColor(itemView.getContext(), R.color.volt_green));
card.setStrokeWidth(4); card.setStrokeWidth(4);

View File

@@ -8,21 +8,37 @@ import java.util.List;
* to allow resuming matches from the database. * to allow resuming matches from the database.
*/ */
public class MatchProgress { public class MatchProgress {
/** Index of the currently active player in the match. */
public int activePlayerIndex; public int activePlayerIndex;
/** The starting score for the match (e.g., 301, 501). */
public int startingScore; public int startingScore;
/** List of player state snapshots representing each player's current status. */
public List<PlayerStateSnapshot> players; public List<PlayerStateSnapshot> players;
/** /**
* Represents the state of an individual player at a point in time. * Represents the state of an individual player at a point in time.
*/ */
public static class PlayerStateSnapshot { public static class PlayerStateSnapshot {
/** The unique ID of the player (0 for guests). */
public long playerId; // 0 for guests public long playerId; // 0 for guests
/** The display name of the player. */
public String name; public String name;
/** The player's current remaining score in the match. */
public int remainingScore; public int remainingScore;
/** The total number of darts thrown by the player so far. */
public int dartsThrown; public int dartsThrown;
/** Default constructor required for serialization/deserialization. */
public PlayerStateSnapshot() {} public PlayerStateSnapshot() {}
/**
* Constructs a new PlayerStateSnapshot with the specified values.
*
* @param playerId The unique ID of the player (0 for guests)
* @param name The display name of the player
* @param remainingScore The player's current remaining score in the match
* @param dartsThrown The total number of darts thrown by the player so far
*/
public PlayerStateSnapshot(final long playerId, final String name, final int remainingScore, final int dartsThrown) { public PlayerStateSnapshot(final long playerId, final String name, final int remainingScore, final int dartsThrown) {
this.playerId = playerId; this.playerId = playerId;
this.name = name; this.name = name;

View File

@@ -9,8 +9,15 @@ import java.lang.reflect.Type;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
/**
* Simple Gson-based converter to serialize and deserialize the hit distribution map for Room database storage.
* The map uses standardized keys (e.g., "t20", "d16", "sb") to represent dartboard segments and their hit counts.
* This allows for flexible and efficient storage of the player's hit distribution in a single database column as a
* JSON string.
*/
public class HitDistributionConverter { public class HitDistributionConverter {
/** Gson instance for JSON serialization and deserialization. */
private static final Gson gson = new Gson(); private static final Gson gson = new Gson();
/** /**

View File

@@ -14,7 +14,12 @@ import com.google.gson.JsonSyntaxException;
*/ */
public class MatchProgressConverter { public class MatchProgressConverter {
/**
* Tag for logging purposes.
*/
private static final String TAG = "MatchProgressConverter"; private static final String TAG = "MatchProgressConverter";
/** Gson instance for JSON serialization and deserialization. */
private static final Gson gson = new Gson(); private static final Gson gson = new Gson();
@TypeConverter @TypeConverter

View File

@@ -6,7 +6,8 @@
<item>701</item> <item>701</item>
<item>501</item> <item>501</item>
<item>301</item> <item>301</item>
<item>Cricket</item> <!-- TODO: Cricket is not yet available, implement in future -->
<!--<item>Cricket</item>-->
</string-array> </string-array>
<!-- Values of the standard game mode preference --> <!-- Values of the standard game mode preference -->
@@ -14,6 +15,7 @@
<item>@string/pref_game_mode_701_value</item> <item>@string/pref_game_mode_701_value</item>
<item>@string/pref_game_mode_501_value</item> <item>@string/pref_game_mode_501_value</item>
<item>@string/pref_game_mode_301_value</item> <item>@string/pref_game_mode_301_value</item>
<item>@string/pref_game_mode_cricket_value</item> <!-- TODO: Cricket is not yet available, implement in future -->
<!--<item>@string/pref_game_mode_cricket_value</item>-->
</string-array> </string-array>
</resources> </resources>