diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/AddPlayerActivity.java b/app/src/main/java/com/aldo/apps/ochecompanion/AddPlayerActivity.java index 1a0a068..5a4e87a 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/AddPlayerActivity.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/AddPlayerActivity.java @@ -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() { + Log.d(TAG, "Back button pressed"); if (mIsStatsViewShown) { + Log.d(TAG, "Hiding stats view instead of exiting"); mPlayerStatsView.setVisibility(View.GONE); mIsStatsViewShown = false; 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) { new AlertDialog.Builder(this) .setTitle(R.string.txt_permission_hint_title) diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/BaseActivity.java b/app/src/main/java/com/aldo/apps/ochecompanion/BaseActivity.java index 1b35748..033d9d5 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/BaseActivity.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/BaseActivity.java @@ -16,6 +16,7 @@ import androidx.preference.PreferenceManager; */ public abstract class BaseActivity extends AppCompatActivity { + /** Tag for debug logging. */ private static final String TAG = "BaseActivity"; /** diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/GameActivity.java b/app/src/main/java/com/aldo/apps/ochecompanion/GameActivity.java index 40adae6..201794b 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/GameActivity.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/GameActivity.java @@ -50,36 +50,29 @@ import nl.dionsegijn.konfetti.xml.KonfettiView; /** * Main game activity for playing X01 darts games (501, 301, etc.). - * Provides numeric keyboard, real-time checkout suggestions, Double Out enforcement, - * and bust detection. Enforces standard darts rules including finishing on doubles. + * Provides numeric keyboard, real-time checkout suggestions, Double Out + * enforcement, + * and bust detection. Enforces standard darts rules including finishing on + * doubles. *

- * This activity now serves as a lightweight UI controller that delegates all business logic - * to the GameManager singleton and responds to game state changes via GameStateCallback. + * This activity now serves as a lightweight UI controller that delegates all + * business logic + * to the GameManager singleton and responds to game state changes via + * GameStateCallback. */ public class GameActivity extends BaseActivity implements GameManager.GameStateCallback { 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"; - - /** - * 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"; - + private boolean mAreStatsShown = false; // ======================================================================================== // Game Manager (Singleton Business Logic Handler) // ======================================================================================== - + /** * Singleton instance managing all game business logic. * Replaces all previous game state fields and logic. @@ -94,27 +87,27 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC // ======================================================================================== // UI References // ======================================================================================== - + /** * TextView displaying the active player's remaining score. */ private TextView tvScorePrimary; - + /** * TextView displaying the active player's name (uppercase). */ private TextView tvPlayerName; - + /** * TextView displaying the active player's three-dart average. */ private TextView tvLegAvg; - + /** * TextView displaying the suggested checkout route. */ private TextView tvCheckout; - + /** * Container layout for checkout suggestion display. * 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. */ private LinearLayout mScoreContainer; - + /** * Button for selecting single (1×) multiplier. */ private View btnSingle; - + /** * Button for selecting double (2×) multiplier. */ private View btnDouble; - + /** * 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. */ private PlayerStatsView mStatsView; - + /** * Array of three TextViews showing darts thrown in current turn. */ private final TextView[] tvDartPills = new TextView[3]; - + /** * GridLayout container holding numeric keyboard buttons (1-20). */ @@ -189,30 +182,39 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC 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; /** * 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 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) { final GameManager gameManager = GameManager.getInstance(context); - gameManager.initializeMatch(matchId, startScore,null); + gameManager.initializeMatch(matchId, startScore, null); Intent intent = new Intent(context, GameActivity.class); 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 players, final int startScore) { 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); } @@ -226,7 +228,8 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC super.onCreate(savedInstanceState); 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) -> { Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); 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); } - + /** + * 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() { - // TODO: Handle Statistics view in here + if (mAreStatsShown) { + mStatsView.setVisibility(View.GONE); + mShowStatsBtn.setVisibility(View.VISIBLE); + mAreStatsShown = false; + return; + } finish(); } @@ -299,12 +311,12 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC mShowStatsBtn = findViewById(R.id.show_stats_btn); mStatsView = findViewById(R.id.player_stats_view); - mShowStatsBtn.setOnClickListener(v -> { + mShowStatsBtn.setOnClickListener(v -> { mStatsView.setVisibility(View.VISIBLE); mShowStatsBtn.setVisibility(View.GONE); + mAreStatsShown = true; }); - mSubmitTurnBtn.setOnClickListener(v -> submitTurn()); findViewById(R.id.btnUndoDart).setOnClickListener(v -> undoLastDart()); } @@ -316,17 +328,17 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC Log.d(TAG, "setupKeyboard() called"); glKeyboard.removeAllViews(); mKeyboardButtons.clear(); - + // Create buttons for numbers 1-20 for (int i = 1; i <= 20; i++) { // Inflate button from template layout MaterialButton btn = (MaterialButton) getLayoutInflater().inflate( - R.layout.view_keyboard_button, glKeyboard, false); - + R.layout.view_keyboard_button, glKeyboard, false); + final int val = i; btn.setText(String.valueOf(val)); btn.setOnClickListener(v -> onNumberTap(val)); - + glKeyboard.addView(btn); mKeyboardButtons.add(btn); // Cache for styling updates } @@ -342,6 +354,10 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC mGameManager.onNumberTap(baseValue); } + /** + * Triggers the bust sequence with visual and haptic feedback. + * Called by GameManager when a bust is detected. + */ private void triggerBustSequence() { // Visual feedback: Shake scoreboard Animation shake = AnimationUtils.loadAnimation(this, R.anim.shake); @@ -363,6 +379,10 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC updateTurnIndicators(); } + /** + * Resets all visual elements to their default state after a bust or at the + * start of a turn. + */ private void resetVisuals() { mScoreContainer.setBackgroundColor(ContextCompat.getColor(this, R.color.surface_primary)); 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); } - /** * 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. * * @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() { GameManager.PlayerState active = mGameManager.getActivePlayer(); - if (active == null) return; - + if (active == null) + return; + // Update score display tvScorePrimary.setText(String.valueOf(active.remainingScore)); - + // Update player name (uppercase for emphasis) tvPlayerName.setText(active.name.toUpperCase()); // Calculate and display three-dart average - double avg = active.dartsThrown == 0 ? 0.0 : - ((double)(mGameManager.getStartingScore() - active.remainingScore) / active.dartsThrown) * 3; + double avg = active.dartsThrown == 0 ? 0.0 + : ((double) (mGameManager.getStartingScore() - active.remainingScore) / active.dartsThrown) * 3; tvLegAvg.setText(String.format("AVG: %.1f", avg)); // 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. * 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) */ 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 */ private String getDartLabel(final int score) { - if (score == DartsConstants.DOUBLE_BULL_VALUE) return DartsConstants.LABEL_DOUBLE_BULL; // Double Bull / Bullseye - if (score == DartsConstants.BULL_VALUE) return DartsConstants.LABEL_BULL; // Single Bull + if (score == DartsConstants.DOUBLE_BULL_VALUE) + 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 String.valueOf(score); } @@ -499,7 +522,7 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC * Displays win notification and celebration animation. * 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 */ private void handleWin(final GameManager.PlayerState winner, final int checkoutValue) { @@ -515,12 +538,18 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC 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() { new Thread(() -> { try { final GameManager.PlayerState activePlayer = mGameManager.getActivePlayer(); - if (activePlayer == null) return; - + if (activePlayer == null) + return; + final Player player = activePlayer.player; final Statistics statistics = mDatabaseHelper.getStatisticsForPlayer(player.id); runOnUiThread(() -> mStatsView.bind(player, statistics)); @@ -549,8 +578,7 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC .spread(360) .position(0.5, 0.5) // Center of screen .colors(Arrays.asList(0xfce18a, 0xff726d, 0xf4306d, 0xb48def)) // Gold/Festive colors - .build() - ); + .build()); winnerText.setText(winnerName + " WINS!"); winnerText.setVisibility(View.VISIBLE); @@ -584,13 +612,19 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC @Override public void onMultiplierChanged(final int multiplier) { runOnUiThread(() -> { - btnSingle.setAlpha(multiplier == DartsConstants.MULTIPLIER_SINGLE ? 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.setAlpha(multiplier == DartsConstants.MULTIPLIER_SINGLE ? 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); - btnDouble.setBackgroundResource(multiplier == DartsConstants.MULTIPLIER_DOUBLE ? R.drawable.shape_multiplier_red : 0); - btnTriple.setBackgroundResource(multiplier == DartsConstants.MULTIPLIER_TRIPLE ? R.drawable.shape_multiplier_blue : 0); + btnSingle.setBackgroundResource( + multiplier == DartsConstants.MULTIPLIER_SINGLE ? R.drawable.shape_multiplier_active : 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; 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 View main = findViewById(R.id.main); main.startAnimation(shakeAnimation); - + if (mIsVibrationEnabled) { final Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE); if (vibrator != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Log.d(TAG, "onOneEightyScored: Pattern vibration"); // 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)); } else if (vibrator != null) { 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"); } } - + if (mIsAudioEnabled) { mSoundEngine.playOneHundredAndEightySound(); } diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/MainMenuActivity.java b/app/src/main/java/com/aldo/apps/ochecompanion/MainMenuActivity.java index 68e6b82..8dc0104 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/MainMenuActivity.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/MainMenuActivity.java @@ -60,6 +60,7 @@ public class MainMenuActivity extends BaseActivity { */ private DatabaseHelper mDatabaseHelper; + /** The ongoing match retrieved from the database, if any. Used for quick start functionality. */ private Match mOngoingMatch; /** diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/SettingsActivity.java b/app/src/main/java/com/aldo/apps/ochecompanion/SettingsActivity.java index f94eabe..c3f3c07 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/SettingsActivity.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/SettingsActivity.java @@ -38,7 +38,7 @@ public class SettingsActivity extends BaseActivity { .replace(R.id.settings, new MainMenuPreferencesFragment()) .commit(); } - ActionBar actionBar = getSupportActionBar(); + final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/database/DatabaseHelper.java b/app/src/main/java/com/aldo/apps/ochecompanion/database/DatabaseHelper.java index a4096dd..4487ca8 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/database/DatabaseHelper.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/database/DatabaseHelper.java @@ -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 players) { // Parse starting score from gameMode string 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 getOngoingMatches() { try { 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() { Log.d(TAG, "printAllMatches() called"); 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() { try { 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) { try { return mExecutor.submit(() -> { diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/MatchDao.java b/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/MatchDao.java index de1cd5d..df5e2f7 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/MatchDao.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/MatchDao.java @@ -49,6 +49,13 @@ public interface MatchDao { @Query("SELECT * FROM matches ORDER BY timestamp DESC LIMIT 1") 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") Match getMatchById(final int matchId); diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/database/objects/Match.java b/app/src/main/java/com/aldo/apps/ochecompanion/database/objects/Match.java index 764ef84..4a8f485 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/database/objects/Match.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/database/objects/Match.java @@ -19,9 +19,12 @@ import java.util.Map; /** * Represents a darts match in the Oche Companion application. - * Room entity storing match information including game mode, timestamp, player count, - * state (ongoing/completed/canceled), and detailed performance data for all participants. - * Implements Serializable for passing between Android components. Provides helper methods + * Room entity storing match information including game mode, timestamp, player + * count, + * 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. * * @see com.aldo.apps.ochecompanion.database.dao.MatchDao @@ -40,17 +43,18 @@ public class Match implements Serializable { public enum MatchState { /** Match is currently in progress */ ONGOING, - + /** Match has been completed successfully */ COMPLETED, - + /** Match was canceled before completion */ CANCELED } /** * 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 */ @@ -59,7 +63,8 @@ public class Match implements Serializable { /** * 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() */ @@ -98,14 +103,19 @@ public class Match implements Serializable { * Constructs a new Match entity ready for database 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 gameMode Identifier for the darts game variant (e.g., "501", "Cricket") - * @param playerCount Number of players who participated (must be at least 1) + * @param timestamp Unix epoch timestamp in milliseconds when match was + * created/completed + * @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 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) */ - 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.gameMode = gameMode; this.playerCount = playerCount; @@ -115,11 +125,13 @@ public class Match implements Serializable { /** * 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. * - * @param gameMode Identifier for the darts game variant (e.g., "501", "Cricket") - * @param players List of Player objects to include in this match + * @param gameMode Identifier for the darts game variant (e.g., "501", + * "Cricket") + * @param players List of Player objects to include in this match */ @Ignore public Match(final String gameMode, final List players) { @@ -132,12 +144,14 @@ public class Match implements Serializable { /** * 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. * - * @param gameMode Identifier for the darts game variant (e.g., "501", "Cricket") - * @param players List of Player objects to include in this match - * @param scores Map of player IDs to their final scores + * @param gameMode Identifier for the darts game variant (e.g., "501", + * "Cricket") + * @param players List of Player objects to include in this match + * @param scores Map of player IDs to their final scores */ @Ignore public Match(final String gameMode, final List players, final Map scores) { @@ -221,6 +235,7 @@ public class Match implements Serializable { } } catch (JSONException e) { // Return empty map if JSON parsing fails + Log.e(TAG, "getPlayerScores: Failed to parse participant data", e); } return scores; } @@ -243,8 +258,9 @@ public class Match implements Serializable { player.careerAverage = participant.optDouble("careerAverage", 0.0); players.add(player); } - } catch (JSONException e) { + } catch (final JSONException e) { // Return empty list if JSON parsing fails + Log.e(TAG, "getAllPlayers: Failed to parse participant data", e); } return players; } @@ -253,7 +269,7 @@ public class Match implements Serializable { * Generates JSON string from a list of Player objects with optional scores. * * @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 */ private String generateParticipantJson(final List players, final Map scores) { @@ -265,12 +281,14 @@ public class Match implements Serializable { participant.put("username", player.username); participant.put("photoUri", player.profilePictureUri); 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); participants.put(participant); } - } catch (JSONException e) { + } catch (final JSONException e) { // Return empty array if JSON generation fails + Log.e(TAG, "generateParticipantJson: Failed to generate participant JSON", e); } return participants.toString(); } @@ -281,7 +299,7 @@ public class Match implements Serializable { public static class ParticipantData { /** The Player object */ public final Player player; - + /** The player's score in this match */ public final int score; @@ -312,6 +330,7 @@ public class Match implements Serializable { } } catch (JSONException e) { // Return empty list if JSON parsing fails + Log.e(TAG, "getAllParticipants: Failed to parse participant data", e); } return participants; } diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/game/GameManager.java b/app/src/main/java/com/aldo/apps/ochecompanion/game/GameManager.java index ca58d05..ec71321 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/game/GameManager.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/game/GameManager.java @@ -27,25 +27,96 @@ import java.util.List; * that only handles UI updates via the GameStateCallback interface. */ public class GameManager { + /** + * Tag for logging purposes. + */ 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 + // ======================================================================================== + + /** + * Reference to the centralized database helper for all database operations. + * Initialized once in constructor and never changed. + */ 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; + // ======================================================================================== // Game State + // ======================================================================================== + + /** + * The database ID of the current match. + * -1 indicates no match is loaded. + */ private int mMatchId = -1; + + /** + * The starting score for this X01 game (typically 501, 301, or 701). + */ 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; + + /** + * 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; + + /** + * List of all player states in this match. + * Order determines turn order. + * Thread-safe operations through GameManager's single-threaded nature. + */ private final List 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 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 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; + + /** + * Flag indicating the current turn resulted in a bust. + * Used to prevent UI from subtracting bust darts from score display. + */ 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; /** @@ -93,11 +164,25 @@ public class GameManager { /** * Represents a single dart hit with its base value and multiplier. + * Immutable data class used for tracking individual dart throws. */ public static class DartHit { + /** + * The dartboard number hit (1-20 or 25 for bull). + */ public final int baseValue; + + /** + * The multiplier applied to the base value (1=single, 2=double, 3=triple). + */ 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) { this.baseValue = baseValue; this.multiplier = multiplier; @@ -106,14 +191,45 @@ public class GameManager { /** * State holder for a single player's X01 game progress. + * Tracks current match state for an individual player. */ public static class PlayerState { + /** + * Reference to the Player entity from the database. + * Used for statistics updates and player information. + */ 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; + + /** + * Player's display name cached from the Player entity. + * Stored for convenience to avoid repeated lookups. + */ 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; + + /** + * Total number of darts thrown by this player in the current match. + * Used for calculating averages and statistics. + */ 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) { this.player = player; this.playerId = player.id; @@ -124,6 +240,9 @@ public class GameManager { /** * 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) { mDatabaseHelper = DatabaseHelper.getInstance(context); @@ -144,16 +263,26 @@ public class GameManager { /** * 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) { mCallback = callback; - //Send one initial callback + //Send one initial callback to sync UI with current state notifyGameStateChanged(); } - public void initializeMatch(final int matchId,final int startingScore, final List 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 players, final Runnable onComplete) { mStartingScore = startingScore; mMatchId = matchId; @@ -234,6 +363,11 @@ public class GameManager { /** * 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 players) { mPlayerStates.clear(); @@ -250,6 +384,10 @@ public class GameManager { /** * 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) { 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. + * 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) { final int dartsThrown = mCurrentTurnDarts.size(); @@ -451,6 +593,9 @@ public class GameManager { /** * 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() { final MatchProgress progress = new MatchProgress(); @@ -490,6 +635,10 @@ public class GameManager { /** * 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) { 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, final boolean wasBust) { @@ -538,6 +693,14 @@ public class GameManager { /** * 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, final boolean wasBust, final int checkoutValue) { @@ -555,13 +718,20 @@ public class GameManager { /** * 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) { 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() { final List 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 dartHits) { if (dartHits.isEmpty()) return; @@ -605,54 +781,115 @@ public class GameManager { // 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() { return mMatchId; } + /** + * Gets the starting score for this X01 game. + * + * @return The starting score (e.g., 501, 301, 701) + */ public int getStartingScore() { return mStartingScore; } + /** + * Gets the index of the currently active player. + * + * @return The active player index (0 to playerCount-1) + */ public int getActivePlayerIndex() { return mActivePlayerIndex; } + /** + * Gets the current dart multiplier setting. + * + * @return The multiplier (1=Single, 2=Double, 3=Triple) + */ public int getMultiplier() { 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 getPlayerStates() { 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() { if (mPlayerStates.isEmpty()) return null; 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 getCurrentTurnDarts() { 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 getCurrentTurnDartHits() { 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() { 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() { 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() { return mIsMatchCompleted; } /** - * Calculates the current target score (remaining - current turn darts). - * If turn is busted, returns the remaining score without subtracting bust darts. + * Calculates the current target score (remaining score minus current turn 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() { PlayerState active = getActivePlayer(); @@ -669,6 +906,9 @@ public class GameManager { /** * 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() { return 3 - mCurrentTurnDarts.size(); @@ -678,30 +918,55 @@ public class GameManager { // 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() { if (mCallback != null) { 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() { if (mCallback != null) { 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() { if (mCallback != null) { 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() { if (mCallback != null) { 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() { if (mCallback != null) { mCallback.onResetVisuals(); diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/HeatmapView.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/HeatmapView.java index 7ff4c19..421f6ce 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/HeatmapView.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/HeatmapView.java @@ -21,29 +21,108 @@ import java.util.Map; /** * HeatmapView: A custom high-performance rendering component that draws a * dartboard and overlays player performance data as a color-coded heatmap. + *

+ * 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. + *

* Optimized Palette: - * - Zero hits: Subtle semi-transparent "ghost" segments. - * - Low frequency: Volt Green (Starting "cool" color). - * - High frequency: Double Red (Intense "hot" color). + * - Zero hits: Subtle semi-transparent "ghost" segments (#1AFFFFFF) + * - Low frequency: Volt Green (Starting "cool" color from resources) + * - High frequency: Double Red (Intense "hot" color from resources) + *

+ * 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 { + /** + * 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); + + /** + * 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(); - // 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 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; - // 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 = { 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() { mBasePaint.setStyle(Paint.Style.FILL); } @@ -56,46 +135,85 @@ public class HeatmapView extends View { 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 protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) { super.onSizeChanged(w, h, oldw, oldh); mCenterX = w / 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(); } /** - * 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. + *

+ * 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) + *

+ * 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() { mSegmentPaths.clear(); - final float angleStep = 360f / 20f; - final float startOffset = -90f - (angleStep / 2f); // Center 20 at the top + final float angleStep = 360f / 20f; // 18 degrees per segment + final float startOffset = -90f - (angleStep / 2f); // Center 20 at the top (-99 degrees) for (int i = 0; i < BOARD_NUMBERS.length; i++) { final int num = BOARD_NUMBERS[i]; final float startAngle = startOffset + (i * angleStep); // Define concentric ring boundaries as percentages of radius - mSegmentPaths.put("d" + num, createArcPath(0.90f, 1.00f, startAngle, angleStep)); // Double - 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("s" + num + "_inner", createArcPath(0.15f, 0.50f, startAngle, angleStep)); // Inner Single + 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("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 } - // Bulls are simple circles + // Bulls are simple circles (no angular segments) final Path sbPath = new Path(); 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(); 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. + *

+ * 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. + *

+ * 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) { final Path path = new Path(); final RectF outerRect = new RectF( @@ -113,28 +231,50 @@ public class HeatmapView extends View { return path; } + /** + * Renders the dartboard heatmap visualization to the canvas. + *

+ * 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 + *

+ * 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 protected void onDraw(@NonNull final Canvas canvas) { super.onDraw(canvas); if (mStats == null) return; // Resolve branding colors from resources - int coldColor = ContextCompat.getColor(getContext(), R.color.volt_green); - int hotColor = ContextCompat.getColor(getContext(), R.color.double_red); - int emptyColor = Color.parseColor("#1AFFFFFF"); // Subtle ghost segments for zero data + final int coldColor = ContextCompat.getColor(getContext(), R.color.volt_green); + final int hotColor = ContextCompat.getColor(getContext(), R.color.double_red); + final int emptyColor = Color.parseColor("#1AFFFFFF"); // Subtle ghost segments for zero data for (final Map.Entry entry : mSegmentPaths.entrySet()) { 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; // Check if there are any hits recorded for this segment final Integer hitCount = mStats.getHitDistribution().get(statsKey); - int color; + final int color; if (hitCount == null || hitCount == 0) { color = emptyColor; } 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); color = (int) mColorEvaluator.evaluate(weight, coldColor, hotColor); } @@ -143,13 +283,13 @@ public class HeatmapView extends View { 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.setStrokeWidth(1.2f); - mBasePaint.setColor(Color.parseColor("#26FFFFFF")); + mBasePaint.setColor(Color.parseColor("#26FFFFFF")); // 15% white for subtle outlines for (final Path p : mSegmentPaths.values()) { canvas.drawPath(p, mBasePaint); } - mBasePaint.setStyle(Paint.Style.FILL); + mBasePaint.setStyle(Paint.Style.FILL); // Reset to fill mode for next draw cycle } } \ No newline at end of file diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/MainMenuPreferencesFragment.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/MainMenuPreferencesFragment.java index 4a78708..a8f503f 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/MainMenuPreferencesFragment.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/MainMenuPreferencesFragment.java @@ -12,57 +12,94 @@ import com.aldo.apps.ochecompanion.R; /** * Preference fragment for the main menu settings screen. - * Displays app-wide preferences including day/night mode and standard game mode selection. + *

+ * 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 + *

* 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 + *

+ * Theme changes trigger activity recreation to ensure proper system theme detection + * and UI consistency. */ 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"; /** * Initializes the preference screen from the main_menu_preferences XML resource. - * Called automatically by the fragment lifecycle. + *

+ * Called automatically by the fragment lifecycle. Sets up: + *

+ * 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 + *

+ * 2. Audio and Vibration Toggles: + * - Custom button-style preferences with dynamic icon updates + * - Icons change based on enabled/disabled state + *

+ * 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 rootKey Optional preference hierarchy root key + * @param rootKey Optional preference hierarchy root key for nested preference screens */ @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { setPreferencesFromResource(R.xml.main_menu_preferences, rootKey); + // --- 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)); - // Use your string resource key here final SwitchPreference manualPref = findPreference(getString(R.string.pref_key_day_night_mode)); 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()); + // Listen for auto mode changes autoPref.setOnPreferenceChangeListener((preference, newValue) -> { - boolean isAutoEnabled = (Boolean) newValue; + final boolean isAutoEnabled = (Boolean) newValue; Log.d(TAG, "Auto mode changed to: " + isAutoEnabled); + + // Enable/disable manual toggle based on auto mode state manualPref.setEnabled(!isAutoEnabled); - // Apply theme immediately and recreate activity to ensure system theme is detected + // Apply theme immediately if (isAutoEnabled) { + // Switch to system-follow mode and recreate activity Log.d(TAG, "Switching to MODE_NIGHT_FOLLOW_SYSTEM and recreating"); 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(); } else { - // Use current manual preference - boolean isDarkMode = manualPref.isChecked(); + // Use current manual preference setting + final boolean isDarkMode = manualPref.isChecked(); Log.d(TAG, "Switching to manual mode, dark mode = " + isDarkMode); AppCompatDelegate.setDefaultNightMode( 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) -> { - boolean isDarkMode = (Boolean) newValue; + final boolean isDarkMode = (Boolean) newValue; Log.d(TAG, "Manual dark mode changed to: " + isDarkMode); + // Only apply if auto mode is disabled if (!autoPref.isChecked()) { Log.d(TAG, "Applying manual theme change"); @@ -72,32 +109,52 @@ public class MainMenuPreferencesFragment extends PreferenceFragmentCompat { } else { 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 --- + // 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_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. + *

+ * 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 + *

+ * 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); 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); pref.setIcon(isEnabled ? iconOn : iconOff); + // Handle toggle on click pref.setOnPreferenceClickListener(p -> { - boolean currentState = getPreferenceManager().getSharedPreferences().getBoolean(key, true); - boolean newState = !currentState; + // Read current state + 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(); - // Update the icon visually + // Update the icon visually to reflect the new state p.setIcon(newState ? iconOn : iconOff); - return true; + return true; // Consume the click event }); } } diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerItemView.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerItemView.java index 731e152..2997903 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerItemView.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerItemView.java @@ -50,7 +50,10 @@ public class PlayerItemView extends MaterialCardView { 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) { mTvUsername.setText(player.username); 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) { mTvUsername.setText(player.username); diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerSelectionDialogFragment.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerSelectionDialogFragment.java index d05cc0c..e2e4cfe 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerSelectionDialogFragment.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerSelectionDialogFragment.java @@ -28,33 +28,112 @@ import java.util.Set; /** * PlayerSelectionDialogFragment: A modern bottom-sheet selector for match participants. - * Automatically pre-selects players from the most recent match for speed. + *

+ * This {@link BottomSheetDialogFragment} provides a user interface for selecting players + * from the database before starting a new match. It features: + *

+ *

+ * 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 { /** * Tag for debugging and logging purposes. + * Used for identifying this fragment in log output. */ 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. + *

+ * This list is populated by {@link #loadData()} and displayed in the + * {@link RecyclerView} via the {@link PlayerSelectionAdapter}. */ private final List mAllPlayers = new ArrayList<>(); + + /** + * The set of currently selected player IDs. + *

+ * 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 mSelectedIds = new HashSet<>(); + /** + * The {@link RecyclerView} that displays the list of selectable players. + * Populated with data from {@link #mAllPlayers}. + */ private RecyclerView mRvSquad; + + /** + * The confirmation button that initiates the match with selected players. + *

+ * Displays the current selection count and is disabled when no players + * are selected. Handled by {@link #updateButtonState()}. + */ private MaterialButton mBtnStart; + + /** + * The adapter that manages the player selection list in the {@link RecyclerView}. + *

+ * Handles player selection logic, visual feedback, and communicates + * selection changes via {@link #onSelectionChanged()}. + */ private PlayerSelectionAdapter mAdapter; + /** + * Creates and returns the view hierarchy associated with this fragment. + *

+ * Inflates the player selection bottom sheet layout, which includes: + *

+ * + * @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 @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); } + /** + * Called immediately after {@link #onCreateView} has returned, but before any saved + * state has been restored into the view. + *

+ * This method performs the following initialization steps: + *

    + *
  1. Binds UI components ({@link RecyclerView}, {@link MaterialButton})
  2. + *
  3. Configures the {@link RecyclerView} with a {@link LinearLayoutManager}
  4. + *
  5. Initializes the {@link PlayerSelectionAdapter} with selection callback
  6. + *
  7. Sets up the confirmation button click listener
  8. + *
  9. Triggers asynchronous data loading via {@link #loadData()}
  10. + *
+ * + * @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 - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mRvSquad = view.findViewById(R.id.rvSquadSelection); @@ -69,20 +148,37 @@ public class PlayerSelectionDialogFragment extends BottomSheetDialogFragment { loadData(); } + /** + * Loads player data from the database asynchronously. + *

+ * This method performs database operations on a background thread to avoid + * blocking the UI. The loading process includes: + *

    + *
  1. Fetching all players from the database
  2. + *
  3. Retrieving the last match's participant data for pre-selection
  4. + *
  5. Parsing the last match data using {@link MatchProgressConverter}
  6. + *
  7. Extracting player IDs from the previous match
  8. + *
  9. Updating the UI on the main thread with loaded data
  10. + *
  11. Pre-selecting players who participated in the last match
  12. + *
+ *

+ * This pre-selection behavior significantly improves user experience when + * starting consecutive matches with the same group of players. + */ private void loadData() { new Thread(() -> { - AppDatabase db = AppDatabase.getDatabase(requireContext()); + final AppDatabase db = AppDatabase.getDatabase(requireContext()); // 1. Get All Players - List players = db.playerDao().getAllPlayers(); + final List players = db.playerDao().getAllPlayers(); // 2. Get Last Participants for Pre-selection - String lastJson = db.matchDao().getLastMatchParticipantData(); - MatchProgress lastProgress = MatchProgressConverter.fromString(lastJson); + final String lastJson = db.matchDao().getLastMatchParticipantData(); + final MatchProgress lastProgress = MatchProgressConverter.fromString(lastJson); final Set lastPlayerIds = new HashSet<>(); if (lastProgress != null && lastProgress.players != null) { - for (MatchProgress.PlayerStateSnapshot p : lastProgress.players) { + for (final MatchProgress.PlayerStateSnapshot p : lastProgress.players) { lastPlayerIds.add((int) p.playerId); } } @@ -94,7 +190,7 @@ public class PlayerSelectionDialogFragment extends BottomSheetDialogFragment { // Auto-select players from the previous session mSelectedIds.clear(); - for (Player p : mAllPlayers) { + for (final Player p : mAllPlayers) { if (lastPlayerIds.contains((int) p.id)) { mSelectedIds.add((int) p.id); } @@ -107,19 +203,59 @@ public class PlayerSelectionDialogFragment extends BottomSheetDialogFragment { }).start(); } + /** + * Callback method invoked by {@link PlayerSelectionAdapter} when the selection state changes. + *

+ * 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. + *

+ * This callback pattern allows the adapter to communicate selection changes + * back to the fragment without tight coupling. + */ private void onSelectionChanged() { updateButtonState(); } + /** + * Updates the confirmation button's state based on the current selection. + *

+ * This method performs two UI updates: + *

+ *

+ * This provides clear visual feedback about the current selection state and + * guides the user through the selection process. + */ private void updateButtonState() { - int count = mSelectedIds.size(); + final int count = mSelectedIds.size(); mBtnStart.setEnabled(count > 0); mBtnStart.setText(count > 0 ? "START MATCH (" + count + ")" : "SELECT PLAYERS"); } + /** + * Initiates a new match with the currently selected players. + *

+ * This method performs the following operations: + *

    + *
  1. Constructs a list of selected {@link Player} objects by filtering + * {@link #mAllPlayers} based on {@link #mSelectedIds}
  2. + *
  3. Validates that at least one player is selected (early return if empty)
  4. + *
  5. Launches {@link GameActivity} with the selected players and default score + * ({@link DartsConstants#DEFAULT_GAME_SCORE})
  6. + *
  7. Dismisses this dialog fragment after successfully starting the match
  8. + *
+ *

+ * This method is triggered by the confirmation button click listener set up + * in {@link #onViewCreated}. + */ private void initiateMatch() { - ArrayList selectedList = new ArrayList<>(); - for (Player p : mAllPlayers) { + final ArrayList selectedList = new ArrayList<>(); + for (final Player p : mAllPlayers) { if (mSelectedIds.contains((int) p.id)) { selectedList.add(p); } diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerStatsView.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerStatsView.java index e07e7f9..422a582 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerStatsView.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerStatsView.java @@ -17,27 +17,137 @@ import com.google.android.material.imageview.ShapeableImageView; /** * PlayerStatsView: A complete dashboard component that visualizes a player's * career performance, including a heatmap and detailed metrics. + *

+ * This custom {@link ScrollView} component provides a comprehensive player statistics + * display that includes: + *

+ *

+ * 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 { + /** + * Tag for debugging and logging purposes. + * Used for identifying this view in log output. + */ 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; + + /** + * 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 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) { this(context, null); } + /** + * Constructs a new {@link PlayerStatsView} with the specified context and attributes. + *

+ * This constructor inflates the player stats layout and initializes all UI components. + * It performs the following operations: + *

    + *
  1. Calls the superclass constructor with context and attributes
  2. + *
  3. Inflates the {@code R.layout.player_stats_layout} resource into this view
  4. + *
  5. Initializes all child views via {@link #initViews()}
  6. + *
+ *

+ * 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) { super(context, attrs); LayoutInflater.from(context).inflate(R.layout.player_stats_layout, this, true); initViews(); } + /** + * Initializes all UI component references by finding views in the inflated layout. + *

+ * This method binds the following UI components: + *

+ *

+ * This method is called once during construction and establishes references + * that are used throughout the view's lifecycle for data binding. + */ private void initViews() { mHeatmap = findViewById(R.id.statsHeatmap); 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. + *

+ * This method populates all UI components with the provided player and statistics data. + * The binding process is organized into four logical sections: + *

    + *
  1. Identity: Displays the player's username in uppercase and loads their + * profile picture using Glide (or shows a default icon if no picture is set)
  2. + *
  3. High-Level Metrics: Formats and displays career average, first-9 average, + * checkout percentage (with % symbol), and highest checkout value
  4. + *
  5. Threshold Totals: Displays counts for 60+, 100+, 140+, and 180 point turns
  6. + *
  7. Heatmap Rendering: Passes statistics to the {@link HeatmapView} for + * visual dartboard representation
  8. + *
+ *

+ * 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) { Log.e(TAG, "bind: Cannot bind, return"); return; diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/PlayerSelectionAdapter.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/PlayerSelectionAdapter.java index 69e86d7..f633fe4 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/PlayerSelectionAdapter.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/PlayerSelectionAdapter.java @@ -18,62 +18,159 @@ import java.util.List; import java.util.Set; /** - * PlayerSelectionAdapter: Optimized for the "Squad Selector" Bottom Sheet. - * Features specialized selection visual states and haptic feedback. + * PlayerSelectionAdapter: Optimized RecyclerView adapter for the "Squad Selector" Bottom Sheet. + *

+ * 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 + *

+ * 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 { + /** + * List of all available players to display in the selection interface. + * This list is immutable after adapter construction. + */ private final List 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 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; - public PlayerSelectionAdapter(List players, Set 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 players, final Set selected, final Runnable onChanged) { this.mPlayers = players; this.mSelectedIds = selected; 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 @Override - public SelectionHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_player_selection, parent, false); + public SelectionHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { + final View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_player_selection, parent, false); return new SelectionHolder(v); } + /** + * Binds player data to the ViewHolder and sets up click handling. + *

+ * 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 - public void onBindViewHolder(@NonNull SelectionHolder holder, int position) { - Player p = mPlayers.get(position); + public void onBindViewHolder(@NonNull final SelectionHolder holder, final int position) { + final Player p = mPlayers.get(position); holder.bind(p, mSelectedIds.contains((int) p.id)); holder.itemView.setOnClickListener(v -> { - // Haptic Feedback - Vibrator vib = (Vibrator) v.getContext().getSystemService(Context.VIBRATOR_SERVICE); + // Haptic Feedback (20ms vibration for tactile response) + final Vibrator vib = (Vibrator) v.getContext().getSystemService(Context.VIBRATOR_SERVICE); if (vib != null) vib.vibrate(20); if (mSelectedIds.contains((int) p.id)) { + // Deselect player mSelectedIds.remove((int) p.id); } else { + // Select player (with maximum limit enforcement) if (mSelectedIds.size() < 8) { // PDC/Standard limit mSelectedIds.add((int) p.id); } } 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 public int getItemCount() { return mPlayers.size(); } + /** + * ViewHolder for player selection items. + *

+ * Manages the visual representation of a single player in the selection list, + * including avatar image, name display, card styling, and selection indicator overlay. + *

+ * Visual states: + * - Selected: Green stroke (4dp), visible selection overlay + * - Unselected: Subtle border (1dp), hidden selection overlay + */ 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; + + /** + * TextView displaying the player's username. + */ 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; + + /** + * Visual overlay indicator shown when the player is selected. + * Typically a checkmark or tinted overlay for visual feedback. + */ 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); ivAvatar = itemView.findViewById(R.id.ivPlayerProfile); tvName = itemView.findViewById(R.id.tvPlayerName); @@ -81,16 +178,34 @@ public class PlayerSelectionAdapter extends RecyclerView.Adapter + * Sets the player's name and avatar, then applies visual styling based on + * whether the player is currently selected: + *

+ * Selected state: + * - Card stroke: volt_green color, 4dp width + * - Selection indicator: visible + *

+ * 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); + // Load profile picture or default icon if (p.profilePictureUri != null) { Glide.with(itemView.getContext()).load(p.profilePictureUri).into(ivAvatar); } else { ivAvatar.setImageResource(R.drawable.ic_users); } - // Visual toggle + // Apply visual selection state if (isSelected) { card.setStrokeColor(ContextCompat.getColor(itemView.getContext(), R.color.volt_green)); card.setStrokeWidth(4); diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/utils/MatchProgress.java b/app/src/main/java/com/aldo/apps/ochecompanion/utils/MatchProgress.java index f6d46b8..f7c1efe 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/utils/MatchProgress.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/utils/MatchProgress.java @@ -8,21 +8,37 @@ import java.util.List; * to allow resuming matches from the database. */ public class MatchProgress { + /** Index of the currently active player in the match. */ public int activePlayerIndex; + /** The starting score for the match (e.g., 301, 501). */ public int startingScore; + /** List of player state snapshots representing each player's current status. */ public List players; /** * Represents the state of an individual player at a point in time. */ public static class PlayerStateSnapshot { + /** The unique ID of the player (0 for guests). */ public long playerId; // 0 for guests + /** The display name of the player. */ public String name; + /** The player's current remaining score in the match. */ public int remainingScore; + /** The total number of darts thrown by the player so far. */ public int dartsThrown; + /** Default constructor required for serialization/deserialization. */ 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) { this.playerId = playerId; this.name = name; diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/utils/converters/HitDistributionConverter.java b/app/src/main/java/com/aldo/apps/ochecompanion/utils/converters/HitDistributionConverter.java index d7bc5f2..833c4a6 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/utils/converters/HitDistributionConverter.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/utils/converters/HitDistributionConverter.java @@ -9,8 +9,15 @@ import java.lang.reflect.Type; import java.util.HashMap; 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 { + /** Gson instance for JSON serialization and deserialization. */ private static final Gson gson = new Gson(); /** diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/utils/converters/MatchProgressConverter.java b/app/src/main/java/com/aldo/apps/ochecompanion/utils/converters/MatchProgressConverter.java index 4a61870..e94d5e8 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/utils/converters/MatchProgressConverter.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/utils/converters/MatchProgressConverter.java @@ -14,7 +14,12 @@ import com.google.gson.JsonSyntaxException; */ public class MatchProgressConverter { + /** + * Tag for logging purposes. + */ private static final String TAG = "MatchProgressConverter"; + + /** Gson instance for JSON serialization and deserialization. */ private static final Gson gson = new Gson(); @TypeConverter diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index b4e7989..26e9e78 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -6,7 +6,8 @@ 701 501 301 - Cricket + + @@ -14,6 +15,7 @@ @string/pref_game_mode_701_value @string/pref_game_mode_501_value @string/pref_game_mode_301_value - @string/pref_game_mode_cricket_value + + \ No newline at end of file