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 0589e60..e3e3349 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/MainMenuActivity.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/MainMenuActivity.java @@ -23,13 +23,14 @@ import com.aldo.apps.ochecompanion.ui.MatchRecapView; import com.aldo.apps.ochecompanion.ui.QuickStartButton; import com.aldo.apps.ochecompanion.ui.adapter.MainMenuPlayerAdapter; import com.aldo.apps.ochecompanion.utils.DartsConstants; -import com.aldo.apps.ochecompanion.utils.UIConstants; +import java.util.ArrayList; import java.util.List; /** * Main entry point and home screen of the Oche Companion application. - * Displays the squad of players, allows adding new players, and shows match recap with test data. + * Displays the squad of players, allows adding new players, and shows match + * recap. */ public class MainMenuActivity extends BaseActivity { @@ -40,67 +41,61 @@ public class MainMenuActivity extends BaseActivity { /** * Custom view component that displays a match summary. - * Can be clicked to cycle through different test data states. */ private MatchRecapView mMatchRecap; - /** - * Counter for cycling through different test data scenarios. - * Increments on each click to cycle through null match, 1v1 match, and group match states. - */ - private int mTestCounter = 0; - /** * The {@link SharedPreferences} containing the currently selected settings. */ private SharedPreferences mSettingsPref; /** - * 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; - /** The ongoing match retrieved from the database, if any. Used for quick start functionality. */ + /** + * The ongoing match retrieved from the database, if any. Used for quick start + * functionality. + */ private Match mOngoingMatch; /** - * Initializes the activity: enables edge-to-edge display, configures window insets, + * Initializes the activity: enables edge-to-edge display, configures window + * insets, * and sets up the match recap view with test data click listener. * - * @param savedInstanceState Bundle containing saved state, or null if none exists. + * @param savedInstanceState Bundle containing saved state, or null if none + * exists. */ @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - + // Enable edge-to-edge display for immersive UI experience EdgeToEdge.enable(this); setContentView(R.layout.activity_main); mSettingsPref = PreferenceManager.getDefaultSharedPreferences(this); mDatabaseHelper = DatabaseHelper.getInstance(this); - // 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); return insets; }); - findViewById(R.id.btnSettings).setOnClickListener(v -> launchSettings()); - // Set up match recap view with test data functionality + // Set up match recap view mMatchRecap = findViewById(R.id.match_recap); - mMatchRecap.setOnClickListener(v -> { - // Cycle through test data scenarios on each click - applyTestData(mTestCounter); - mTestCounter++; - new Thread(() -> mDatabaseHelper.printAllMatches()).start(); - }); } /** - * Refreshes the squad view with latest player data from the database when activity resumes. + * Refreshes the squad view with latest player data from the database when + * activity resumes. */ @Override protected void onResume() { @@ -132,7 +127,8 @@ public class MainMenuActivity extends BaseActivity { startActivity(intent); } - /**game. + /** + * game. * Checks for any ongoing matches and resumes them if found, * otherwise starts a new game with the default score. */ @@ -159,20 +155,21 @@ public class MainMenuActivity extends BaseActivity { /** * Initializes the squad view: sets up the RecyclerView with adapter, - * configures the add player button, and loads all players from the database on a background thread. + * configures the add player button, and loads all players from the database on + * a background thread. */ private void initSquadView() { // Get references to UI components final TextView addPlayerBtn = findViewById(R.id.btnAddPlayer); final RecyclerView squadView = findViewById(R.id.rvSquad); - + // Configure RecyclerView with linear layout squadView.setLayoutManager(new LinearLayoutManager(MainMenuActivity.this)); - + // Create and attach adapter final MainMenuPlayerAdapter adapter = new MainMenuPlayerAdapter(); squadView.setAdapter(adapter); - + // Set up button to launch AddPlayerActivity addPlayerBtn.setOnClickListener(v -> { final Intent intent = new Intent(MainMenuActivity.this, AddPlayerActivity.class); @@ -181,73 +178,66 @@ public class MainMenuActivity extends BaseActivity { new Thread(() -> { final List allPlayers = mDatabaseHelper.getAllPlayers(); runOnUiThread(() -> adapter.updatePlayers(allPlayers)); - }).start(); } /** * Applies the last completed match from the database to the match recap view. + * Enriches participant data with profile pictures from the database using batch + * fetch. */ private void applyLastMatch() { new Thread(() -> { + final List participants = new ArrayList<>(); final Match lastCompleted = mDatabaseHelper.getLastCompletedMatch(); if (lastCompleted != null) { Log.d(TAG, "applyLastMatch: Applying last completed match [" + lastCompleted + "]"); - runOnUiThread(() -> mMatchRecap.setMatch(lastCompleted)); + + // Enrich participant data with profile pictures from database (batch fetch) + participants.addAll(lastCompleted.getAllParticipants()); + + // Collect all player IDs that need enrichment + final List playerIds = new java.util.ArrayList<>(); + for (final Match.ParticipantData participant : participants) { + if (participant.player.id > 0) { + playerIds.add(participant.player.id); + } + } + + // Batch fetch all players in one query + if (!playerIds.isEmpty()) { + Log.d(TAG, "applyLastMatch: Fetching " + playerIds.size() + " players: " + playerIds); + final List fullPlayers = mDatabaseHelper.getPlayersByIds(playerIds); + Log.d(TAG, "applyLastMatch: Retrieved " + fullPlayers.size() + " players from database"); + + // Create a map for quick lookup + final java.util.Map playerMap = new java.util.HashMap<>(); + for (final Player player : fullPlayers) { + playerMap.put(player.id, player); + Log.d(TAG, "applyLastMatch: Player " + player.id + " has profilePictureUri: " + + player.profilePictureUri); + } + + // Enrich each participant with full player data + for (final Match.ParticipantData participant : participants) { + final Player fullPlayer = playerMap.get(participant.player.id); + if (fullPlayer != null) { + Log.d(TAG, "applyLastMatch: Enriching participant " + participant.player.username + + " (id=" + participant.player.id + ") with profilePictureUri: " + + fullPlayer.profilePictureUri); + participant.player.profilePictureUri = fullPlayer.profilePictureUri; + participant.player.careerAverage = fullPlayer.careerAverage; + Log.d(TAG, "applyLastMatch: After enrichment, participant.player.profilePictureUri = " + + participant.player.profilePictureUri); + } else { + // Player was deleted - participant will show with name only (no profile + // picture) + Log.d(TAG, "applyLastMatch: Player " + participant.player.id + " not found (deleted?)"); + } + } + } } + runOnUiThread(() -> mMatchRecap.setMatchWithParticipants(lastCompleted, participants)); }).start(); } - - /** - * Applies test data to the match recap view for development and testing. - * Cycles through null match (counter % 3 == 0), 1v1 match (counter % 3 == 1), - * and group match (counter % 3 == 2) based on the counter value. - * - * @param counter Counter value used to determine which test scenario to display. - */ - private void applyTestData(final int counter) { - Log.d(TAG, "applyTestData: Applying Test Data [" + counter + "]"); - // Create test player objects - final Player playerOne = new Player(DartsConstants.TEST_PLAYER_1, null); - playerOne.id = 1; - final Player playerTwo = new Player(DartsConstants.TEST_PLAYER_2, null); - playerTwo.id = 2; - final Player playerThree = new Player(DartsConstants.TEST_PLAYER_3, null); - playerThree.id = 3; - final Player playerFour = new Player(DartsConstants.TEST_PLAYER_4, null); - playerFour.id = 4; - - // Create score maps for test matches - final java.util.Map scores1v1 = new java.util.HashMap<>(); - scores1v1.put(1, 0); // Player 1 won (reached 0 from 501) - scores1v1.put(2, 157); // Player 2 remaining score - - final java.util.Map scoresGroup = new java.util.HashMap<>(); - scoresGroup.put(1, 0); // Player 1 won (reached 0 from 501) - scoresGroup.put(2, 89); // Player 2 remaining score - scoresGroup.put(3, 234); // Player 3 remaining score - scoresGroup.put(4, 312); // Player 4 remaining score - - // Create test match objects with different player configurations and scores - final Match match1on1 = new Match("501", java.util.Arrays.asList(playerOne, playerTwo), scores1v1); - match1on1.markCompleted(); // Mark as completed for test data - - final Match matchGroup = new Match("501", java.util.Arrays.asList(playerOne, playerTwo, playerThree, playerFour), scoresGroup); - matchGroup.markCompleted(); // Mark as completed for test data - - // Cycle through different test scenarios based on counter value - if (counter % UIConstants.TEST_CYCLE_MODULO == 0) { - Log.d(TAG, "applyTestData: No recent match selected."); - // Scenario 1: No match (null state) - mMatchRecap.setMatch(null); - } else if (counter % UIConstants.TEST_CYCLE_MODULO == 1) { - Log.d(TAG, "applyTestData: 1 on 1 Match"); - // Scenario 2: 1v1 match (two players) - mMatchRecap.setMatch(match1on1); - } else { - Log.d(TAG, "applyTestData: Group Match."); - // Scenario 3: Group match (four players) - mMatchRecap.setMatch(matchGroup); - } - } } \ No newline at end of file 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 cd785fe..e8c314c 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 @@ -629,6 +629,28 @@ public class DatabaseHelper { } } + /** + * Retrieves multiple players by their IDs in a single batch query. + * More efficient than calling getPlayerById() multiple times. + * Blocks until the operation completes to ensure consistency with any pending + * writes. + * + * @param playerIds List of player IDs to retrieve + * @return List of players found (may be fewer than requested if some IDs don't + * exist) + */ + public List getPlayersByIds(final List playerIds) { + if (playerIds == null || playerIds.isEmpty()) { + return new ArrayList<>(); + } + try { + return mExecutor.submit(() -> mDatabase.playerDao().getPlayersByIds(playerIds)).get(); + } catch (final Exception e) { + Log.e(TAG, "getPlayersByIds: Failed to submit task", e); + return new ArrayList<>(); + } + } + /** * Inserts a PlayerStats entity into the database. * diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/PlayerDao.java b/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/PlayerDao.java index 7440b7b..b57d0f3 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/PlayerDao.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/PlayerDao.java @@ -76,6 +76,17 @@ public interface PlayerDao { @Query("SELECT * FROM players WHERE isPrimaryUser = 1 LIMIT 1") Player getPrimaryUser(); + /** + * Retrieves multiple players by their IDs in a single query. + * Must be called on a background thread. + * + * @param ids List of player IDs to retrieve + * @return List of players found (may be fewer than requested if some IDs don't + * exist) + */ + @Query("SELECT * FROM players WHERE id IN (:ids)") + List getPlayersByIds(final List ids); + /** * Clears the primary user flag from all players. * Used before setting a new primary user to ensure only one exists. 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 f952732..38e9914 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 @@ -295,7 +295,8 @@ public class Match implements Serializable { } /** - * Data class representing a participant in a match with their score. + * Data class representing a participant in a match with their score and game + * average. */ public static class ParticipantData { /** The Player object */ @@ -304,19 +305,48 @@ public class Match implements Serializable { /** The player's score in this match */ public final int score; - public ParticipantData(final Player player, final int score) { + /** The player's average for this specific game (points per 3 darts) */ + public final double gameAverage; + + public ParticipantData(final Player player, final int score, final double gameAverage) { this.player = player; this.score = score; + this.gameAverage = gameAverage; } } /** - * Returns all participants with their match scores. + * Returns all participants with their match scores and game averages. + * Handles both legacy JSONArray format and MatchProgress JSONObject format. * - * @return List of ParticipantData objects containing player info and scores + * @return List of ParticipantData objects containing player info, scores, and + * game averages */ public List getAllParticipants() { final List participants = new ArrayList<>(); + + // First, try to parse as MatchProgress format (JSONObject with "players" array) + final MatchProgress progress = MatchProgressConverter.fromString(participantData); + if (progress != null && progress.players != null) { + // New format: MatchProgress object + for (final MatchProgress.PlayerStateSnapshot playerProgress : progress.players) { + final Player player = new Player(playerProgress.name, null); + player.id = playerProgress.playerId; + + final int score = playerProgress.remainingScore; + + // Calculate game average: (scored points / darts thrown) * 3 + final int scoredPoints = progress.startingScore - playerProgress.remainingScore; + final double gameAverage = playerProgress.dartsThrown > 0 + ? (double) scoredPoints / playerProgress.dartsThrown * 3 + : 0.0; + + participants.add(new ParticipantData(player, score, gameAverage)); + } + return participants; + } + + // Fall back to legacy JSONArray format try { final JSONArray participantArray = new JSONArray(participantData); for (int i = 0; i < participantArray.length(); i++) { @@ -327,7 +357,8 @@ public class Match implements Serializable { player.id = participant.optInt("id", 0); player.careerAverage = participant.optDouble("careerAverage", 0.0); final int score = participant.optInt("score", 0); - participants.add(new ParticipantData(player, score)); + // Legacy format doesn't have game average data, use 0.0 + participants.add(new ParticipantData(player, score, 0.0)); } } catch (JSONException e) { // Return empty list if JSON parsing fails 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 ec71321..208713f 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 @@ -16,14 +16,16 @@ import java.util.List; /** * GameManager: Singleton manager for handling all X01 game business logic. *

- * This class serves as the central data pool and business logic handler for an active darts match. + * This class serves as the central data pool and business logic handler for an + * active darts match. * It manages: * - Match state (scores, active player, dart throws) * - Database operations (loading/saving match progress) * - Game rules (bust detection, double-out, win conditions) * - Statistics tracking *

- * The GameManager decouples business logic from UI, making GameActivity a simple view controller + * The GameManager decouples business logic from UI, making GameActivity a + * simple view controller * that only handles UI updates via the GameStateCallback interface. */ public class GameManager { @@ -31,94 +33,94 @@ public class GameManager { * Tag for logging purposes. */ private static final String TAG = "GameManager"; - + /** * 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; - + /** * Callback interface for communicating game state changes to the UI layer. */ @@ -127,41 +129,43 @@ public class GameManager { * Called when the game state has changed and UI should be refreshed. */ void onGameStateChanged(); - + /** * Called when the turn indicators (dart pills) should be updated. */ void onTurnIndicatorsChanged(); - + /** * Called when the multiplier has changed. + * * @param multiplier The new multiplier value (1=Single, 2=Double, 3=Triple) */ void onMultiplierChanged(int multiplier); - + /** * Called when a bust occurs. */ void onBust(); - + /** * Called when a player wins the match. - * @param winner The winning player's state + * + * @param winner The winning player's state * @param checkoutValue The final dart value that won the game */ void onPlayerWin(PlayerState winner, int checkoutValue); - + /** * Called when a perfect 180 is scored. */ void onOneEightyScored(); - + /** * Called to reset visual effects after a bust. */ void onResetVisuals(); } - + /** * Represents a single dart hit with its base value and multiplier. * Immutable data class used for tracking individual dart throws. @@ -171,16 +175,16 @@ public class GameManager { * 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 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) { @@ -188,7 +192,7 @@ public class GameManager { this.multiplier = multiplier; } } - + /** * State holder for a single player's X01 game progress. * Tracks current match state for an individual player. @@ -199,35 +203,35 @@ public class GameManager { * 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 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) { @@ -237,17 +241,18 @@ public class GameManager { this.remainingScore = startScore; } } - + /** * 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) + * @param context Application or Activity context (will be converted to + * application context) */ private GameManager(final Context context) { mDatabaseHelper = DatabaseHelper.getInstance(context); } - + /** * Gets the singleton instance of GameManager. * @@ -260,7 +265,7 @@ public class GameManager { } return sInstance; } - + /** * Registers a callback to receive game state updates. * Immediately triggers an initial state change callback to synchronize the UI. @@ -269,20 +274,24 @@ public class GameManager { */ public void setCallback(final GameStateCallback callback) { mCallback = callback; - //Send one initial callback to sync UI with current state + // Send one initial callback to sync UI with current state notifyGameStateChanged(); } /** - * 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. + * 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 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 + * @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) { + public void initializeMatch(final int matchId, final int startingScore, final List players, + final Runnable onComplete) { mStartingScore = startingScore; mMatchId = matchId; @@ -293,13 +302,13 @@ public class GameManager { match = mDatabaseHelper.getMatchById(matchId); Log.d(TAG, "initializeMatch: Loaded match from DB: " + match); - if (match != null && match.participantData != null && !match.participantData.isEmpty()) { // Load match progress from database try { final MatchProgress progress = MatchProgressConverter.fromString(match.participantData); if (progress != null) { - Log.d(TAG, "initializeMatch: Found saved progress with " + progress.players.size() + " players"); + Log.d(TAG, "initializeMatch: Found saved progress with " + progress.players.size() + + " players"); // Initialize player states initializePlayerStates(players); loadMatchProgress(progress); @@ -341,33 +350,33 @@ public class GameManager { }).start(); } - /** * Initializes a new game or loads an existing match from the database. * This should be called when starting/resuming a match. * - * @param matchId The match ID to load, or -1 to create a new match + * @param matchId The match ID to load, or -1 to create a new match * @param startingScore The starting score (501, 301, etc.) - * @param onComplete Callback when initialization is complete + * @param onComplete Callback when initialization is complete */ public void initializeMatch(final int matchId, final int startingScore, final Runnable onComplete) { mStartingScore = startingScore; mMatchId = matchId; - + new Thread(() -> { final List allPlayers = mDatabaseHelper.getAllPlayers(); Log.d(TAG, "initializeMatch: Loading players, count = " + (allPlayers != null ? allPlayers.size() : 0)); initializeMatch(matchId, startingScore, allPlayers, onComplete); }).start(); } - + /** * 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 + * @param players List of Player entities to initialize, or null/empty for guest + * player */ private void initializePlayerStates(final List players) { mPlayerStates.clear(); @@ -381,7 +390,7 @@ public class GameManager { mPlayerStates.add(new PlayerState(guest, mStartingScore)); } } - + /** * Loads match progress from a saved state. * Restores player scores, darts thrown, and active player index from @@ -390,13 +399,14 @@ public class GameManager { * @param progress The MatchProgress snapshot to restore from */ private void loadMatchProgress(final MatchProgress progress) { - if (progress == null || mPlayerStates.isEmpty()) return; - + if (progress == null || mPlayerStates.isEmpty()) + return; + Log.d(TAG, "loadMatchProgress: Loading saved match progress"); - + // Restore active player index mActivePlayerIndex = progress.activePlayerIndex; - + // Restore player scores and darts thrown for (int i = 0; i < progress.players.size() && i < mPlayerStates.size(); i++) { MatchProgress.PlayerStateSnapshot snapshot = progress.players.get(i); @@ -404,74 +414,77 @@ public class GameManager { state.remainingScore = snapshot.remainingScore; state.dartsThrown = snapshot.dartsThrown; } - + Log.d(TAG, "loadMatchProgress: Match progress loaded successfully"); } - + /** * Processes a dart throw when a keyboard number is tapped. * * @param baseValue Face value of the number hit (1-20 or 25 for Bull) */ public void onNumberTap(final int baseValue) { - if (mCurrentTurnDarts.size() >= 3 || mIsTurnOver) return; - + if (mCurrentTurnDarts.size() >= 3 || mIsTurnOver) + return; + int points = baseValue * mMultiplier; if (baseValue == DartsConstants.BULL_VALUE && mMultiplier == DartsConstants.MULTIPLIER_TRIPLE) { points = DartsConstants.DOUBLE_BULL_VALUE; // Triple Bull is Double Bull } - + PlayerState active = mPlayerStates.get(mActivePlayerIndex); int scoreBeforeDart = active.remainingScore; - for (int d : mCurrentTurnDarts) scoreBeforeDart -= d; - + for (int d : mCurrentTurnDarts) + scoreBeforeDart -= d; + int scoreAfterDart = scoreBeforeDart - points; - boolean isDouble = (mMultiplier == DartsConstants.MULTIPLIER_DOUBLE) || (points == DartsConstants.DOUBLE_BULL_VALUE); - + boolean isDouble = (mMultiplier == DartsConstants.MULTIPLIER_DOUBLE) + || (points == DartsConstants.DOUBLE_BULL_VALUE); + // --- DOUBLE OUT LOGIC CHECK --- if (scoreAfterDart < 0 || scoreAfterDart == DartsConstants.BUST_SCORE || (scoreAfterDart == 0 && !isDouble)) { // BUST CONDITION mCurrentTurnDarts.add(points); mCurrentTurnDartHits.add(new DartHit(baseValue, mMultiplier)); - + // Track double-out miss if trying to finish but failed if (scoreBeforeDart <= 40 && isDouble) { trackDoubleAttempt(active, true); } - + mIsTurnOver = true; mIsBustedTurn = true; - + notifyTurnIndicatorsChanged(); notifyBust(); } else if (scoreAfterDart == 0 && isDouble) { // VICTORY CONDITION mCurrentTurnDarts.add(points); mCurrentTurnDartHits.add(new DartHit(baseValue, mMultiplier)); - + // Track successful double-out trackDoubleAttempt(active, false); - + mIsTurnOver = true; - + notifyTurnIndicatorsChanged(); handleWin(active); } else { // VALID THROW mCurrentTurnDarts.add(points); mCurrentTurnDartHits.add(new DartHit(baseValue, mMultiplier)); - + notifyTurnIndicatorsChanged(); notifyGameStateChanged(); - + if (mCurrentTurnDarts.size() == DartsConstants.MAX_DARTS_PER_TURN) { mIsTurnOver = true; } } - + setMultiplier(DartsConstants.MULTIPLIER_SINGLE); } - + /** * Handles the win condition when a player finishes on zero with a double. * Updates statistics, records dart hits, increments match counters, saves the @@ -482,86 +495,93 @@ public class GameManager { private void handleWin(final PlayerState winner) { final int dartsThrown = mCurrentTurnDarts.size(); int pointsMade = 0; - for (int d : mCurrentTurnDarts) pointsMade += d; + for (int d : mCurrentTurnDarts) + pointsMade += d; final int checkoutValue = mCurrentTurnDarts.get(mCurrentTurnDarts.size() - 1); - + + // Update winner's score to 0 and increment darts thrown + winner.remainingScore = 0; + winner.dartsThrown += dartsThrown; + // Update statistics updatePlayerStats(winner, dartsThrown, pointsMade, false, checkoutValue); - + // Record dart hits recordTurnHitsToStatistics(winner, new ArrayList<>(mCurrentTurnDartHits)); - + // Increment matches played for all players incrementMatchesPlayed(); - + // Clear turn state mCurrentTurnDarts.clear(); mCurrentTurnDartHits.clear(); - + // Save completed match saveCompletedMatch(winner); - + // Mark match as completed mIsMatchCompleted = true; - + // Notify UI if (mCallback != null) { mCallback.onPlayerWin(winner, checkoutValue); } } - + /** * Submits the current turn and advances to the next player. */ public void submitTurn() { // Don't submit if no darts thrown - if (mCurrentTurnDarts.isEmpty()) return; - + if (mCurrentTurnDarts.isEmpty()) + return; + // Calculate turn total int turnTotal = 0; - for (int d : mCurrentTurnDarts) turnTotal += d; - + for (int d : mCurrentTurnDarts) + turnTotal += d; + PlayerState active = mPlayerStates.get(mActivePlayerIndex); - + // Calculate final score int finalScore = active.remainingScore - turnTotal; - + // Check for 180 if (finalScore > 0 && turnTotal == 180) { notifyOneEighty(); } - + boolean isBust = mIsBustedTurn; - + // Update score only if not bust if (!isBust) { active.remainingScore = finalScore; active.dartsThrown += mCurrentTurnDarts.size(); } - + updatePlayerStats(active, mCurrentTurnDarts.size(), turnTotal, isBust); - + // Record dart hits recordTurnHitsToStatistics(active, new ArrayList<>(mCurrentTurnDartHits)); - + // Rotate to next player mActivePlayerIndex = (mActivePlayerIndex + 1) % mPlayerStates.size(); - + // Reset turn state mCurrentTurnDarts.clear(); mCurrentTurnDartHits.clear(); mIsTurnOver = false; mIsBustedTurn = false; - + // Save progress saveMatchProgress(); - + // Notify UI notifyResetVisuals(); notifyGameStateChanged(); notifyTurnIndicatorsChanged(); } - + /** * Removes the most recently thrown dart from current turn. */ @@ -569,16 +589,16 @@ public class GameManager { if (!mCurrentTurnDarts.isEmpty()) { mCurrentTurnDarts.remove(mCurrentTurnDarts.size() - 1); mCurrentTurnDartHits.remove(mCurrentTurnDartHits.size() - 1); - + mIsTurnOver = false; mIsBustedTurn = false; - + notifyResetVisuals(); notifyTurnIndicatorsChanged(); notifyGameStateChanged(); } } - + /** * Sets the current multiplier. * @@ -590,7 +610,7 @@ public class GameManager { mCallback.onMultiplierChanged(multiplier); } } - + /** * Saves the current match progress to the database. * Creates a MatchProgress snapshot of the current game state (player scores, @@ -602,18 +622,17 @@ public class GameManager { progress.activePlayerIndex = mActivePlayerIndex; progress.startingScore = mStartingScore; progress.players = new ArrayList<>(); - + for (PlayerState state : mPlayerStates) { progress.players.add(new MatchProgress.PlayerStateSnapshot( state.playerId, state.name, state.remainingScore, - state.dartsThrown - )); + state.dartsThrown)); } - + String progressJson = MatchProgressConverter.fromProgress(progress); - + if (mMatchId > 0) { new Thread(() -> { try { @@ -632,7 +651,7 @@ public class GameManager { }).start(); } } - + /** * Saves the completed match to the database. * Marks the match as COMPLETED, saves the final game state, and updates @@ -641,32 +660,32 @@ public class GameManager { * @param winner The PlayerState of the winning player (used for logging) */ private void saveCompletedMatch(final PlayerState winner) { - if (mMatchId <= 0) return; - + if (mMatchId <= 0) + return; + new Thread(() -> { try { Match match = mDatabaseHelper.getMatchById(mMatchId); if (match != null) { match.state = Match.MatchState.COMPLETED; match.timestamp = System.currentTimeMillis(); - + final MatchProgress finalProgress = new MatchProgress(); finalProgress.activePlayerIndex = mActivePlayerIndex; finalProgress.startingScore = mStartingScore; finalProgress.players = new ArrayList<>(); - + for (PlayerState state : mPlayerStates) { finalProgress.players.add(new MatchProgress.PlayerStateSnapshot( state.playerId, state.name, state.remainingScore, - state.dartsThrown - )); + state.dartsThrown)); } - + match.participantData = MatchProgressConverter.fromProgress(finalProgress); mDatabaseHelper.updateMatch(match); - + Log.d(TAG, "saveCompletedMatch: Match " + mMatchId + " marked as completed"); } else { Log.e(TAG, "saveCompletedMatch: Match not found with ID: " + mMatchId); @@ -676,34 +695,35 @@ public class GameManager { } }).start(); } - + /** * Updates player statistics in the database after a turn. - * Convenience method that delegates to the overloaded version with checkoutValue=0. + * Convenience method that delegates to the overloaded version with + * checkoutValue=0. * - * @param active The PlayerState whose statistics should be updated + * @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 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) { + private void updatePlayerStats(final PlayerState active, final int dartsThrown, final int pointsMade, + final boolean wasBust) { updatePlayerStats(active, dartsThrown, pointsMade, wasBust, 0); } - + /** * 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 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) { + private void updatePlayerStats(final PlayerState active, final int dartsThrown, final int pointsMade, + final boolean wasBust, final int checkoutValue) { if (active.player != null && active.player.id != 0) { new Thread(() -> mDatabaseHelper.updatePlayerStatistics( active.player.id, @@ -711,23 +731,23 @@ public class GameManager { pointsMade, wasBust, checkoutValue, - active.dartsThrown - )).start(); + active.dartsThrown)).start(); } } - + /** * 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 + * @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 in the current match. * Called when a match is completed to update the match count for all @@ -740,7 +760,7 @@ public class GameManager { } new Thread(() -> mDatabaseHelper.incrementMatchesPlayed(playerIds)).start(); } - + /** * Records all dart hits from a confirmed turn to player statistics. * Updates the hit distribution map for heat map visualization. @@ -748,19 +768,21 @@ public class GameManager { * 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 + * @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; - + if (dartHits.isEmpty()) + return; + final List dbDartHits = new ArrayList<>(); for (DartHit hit : dartHits) { dbDartHits.add(new DatabaseHelper.DartHit(hit.baseValue, hit.multiplier)); } - + new Thread(() -> mDatabaseHelper.recordDartHits(playerState.playerId, dbDartHits)).start(); } - + /** * Resets the game state for a new match. * This clears all current match data but keeps the singleton instance alive. @@ -776,11 +798,11 @@ public class GameManager { mIsBustedTurn = false; mIsMatchCompleted = false; } - + // ======================================================================================== // Getters for Game State // ======================================================================================== - + /** * Gets the database ID of the current match. * @@ -789,7 +811,7 @@ public class GameManager { public int getMatchId() { return mMatchId; } - + /** * Gets the starting score for this X01 game. * @@ -798,7 +820,7 @@ public class GameManager { public int getStartingScore() { return mStartingScore; } - + /** * Gets the index of the currently active player. * @@ -807,7 +829,7 @@ public class GameManager { public int getActivePlayerIndex() { return mActivePlayerIndex; } - + /** * Gets the current dart multiplier setting. * @@ -816,7 +838,7 @@ public class GameManager { 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. @@ -826,17 +848,18 @@ public class GameManager { 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; + 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. @@ -846,7 +869,7 @@ public class GameManager { 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. @@ -856,7 +879,7 @@ public class GameManager { public List getCurrentTurnDartHits() { return new ArrayList<>(mCurrentTurnDartHits); } - + /** * Checks if the current turn is over (bust, win, or 3 darts thrown). * @@ -865,7 +888,7 @@ public class GameManager { public boolean isTurnOver() { return mIsTurnOver; } - + /** * Checks if the current turn resulted in a bust. * @@ -874,7 +897,7 @@ public class GameManager { public boolean isBustedTurn() { return mIsBustedTurn; } - + /** * Checks if the match has been completed (someone won). * @@ -883,27 +906,31 @@ public class GameManager { public boolean isMatchCompleted() { return mIsMatchCompleted; } - + /** - * Calculates the current target score (remaining score minus current turn darts). - * If the 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(); - if (active == null) return 0; - + if (active == null) + return 0; + if (mIsBustedTurn) { return active.remainingScore; } - + int turnPointsSoFar = 0; - for (int d : mCurrentTurnDarts) turnPointsSoFar += d; + for (int d : mCurrentTurnDarts) + turnPointsSoFar += d; return active.remainingScore - turnPointsSoFar; } - + /** * Gets the number of darts remaining in the current turn. * A turn consists of up to 3 darts. @@ -913,11 +940,11 @@ public class GameManager { public int getDartsRemainingInTurn() { return 3 - mCurrentTurnDarts.size(); } - + // ======================================================================================== // Callback Notification Methods // ======================================================================================== - + /** * Notifies the UI callback that the general game state has changed. * Used for updating scores, player names, averages, and checkout suggestions. @@ -928,9 +955,10 @@ public class GameManager { mCallback.onGameStateChanged(); } } - + /** - * Notifies the UI callback that the turn indicators (dart pills) should be updated. + * 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. */ @@ -939,7 +967,7 @@ public class GameManager { mCallback.onTurnIndicatorsChanged(); } } - + /** * Notifies the UI callback that a bust has occurred. * Triggers bust animations, sounds, and visual feedback. @@ -950,7 +978,7 @@ public class GameManager { mCallback.onBust(); } } - + /** * Notifies the UI callback that a perfect 180 was scored. * Triggers celebration animations, sounds, and vibrations. @@ -961,7 +989,7 @@ public class GameManager { mCallback.onOneEightyScored(); } } - + /** * Notifies the UI callback to reset visual effects (bust overlays, colors). * Called when starting a new turn or undoing darts. diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/MatchRecapView.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/MatchRecapView.java index 636e097..acd1fa4 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/MatchRecapView.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/MatchRecapView.java @@ -14,6 +14,8 @@ import com.aldo.apps.ochecompanion.R; import com.aldo.apps.ochecompanion.database.objects.Match; import com.aldo.apps.ochecompanion.ui.adapter.MainMenuGroupMatchAdapter; +import java.util.List; + /** * Displays summary of most recent match. Adapts display based on match type: * empty state (no matches), 1v1 state (2 players), or group state (3+ players). @@ -35,24 +37,27 @@ public class MatchRecapView extends FrameLayout { private View mStateGroup; // ========== 1v1 View References ========== - + /** Player 1 name in 1v1 match. */ private TextView mTvP1Name; - + /** Player 2 name in 1v1 match. */ private TextView mTvP2Name; - + /** Player 1 score in 1v1 match. */ private TextView mTvP1Score; - + /** Player 2 score in 1v1 match. */ private TextView mTvP2Score; // ========== Group View References ========== - + /** RecyclerView displaying leaderboard for group matches. */ private RecyclerView mRvLeaderboard; + /** Adapter for group match leaderboard, reused across updates. */ + private MainMenuGroupMatchAdapter mGroupMatchAdapter; + /** Constructor for programmatic instantiation. */ public MatchRecapView(@NonNull final Context context) { this(context, null); @@ -75,7 +80,7 @@ public class MatchRecapView extends FrameLayout { mTvP1Score = findViewById(R.id.tvP1Score); mTvP2Name = findViewById(R.id.tvP2Name); mTvP2Score = findViewById(R.id.tvP2Score); - + mRvLeaderboard = findViewById(R.id.rvLeaderboard); } @@ -94,13 +99,34 @@ public class MatchRecapView extends FrameLayout { } } + /** + * Binds match with pre-enriched participants (e.g., with profile pictures from + * database). + * Use this when participants have already been enriched to avoid re-parsing + * JSON. + */ + public void setMatchWithParticipants(@Nullable final Match match, + @Nullable final List enrichedParticipants) { + Log.d(TAG, "setMatchWithParticipants() called with: match = [" + match + "]"); + if (match == null) { + updateVisibility(mStateEmpty); + return; + } + + if (match.getParticipantCount() > 2) { + setupGroupStateWithParticipants(enrichedParticipants); + } else { + setup1v1State(match); + } + } + /** Configures 1v1 state with player names and scores. */ private void setup1v1State(final Match match) { updateVisibility(mState1v1); - + mTvP1Name.setText(match.getPlayerNameByPosition(0)); mTvP1Score.setText(String.valueOf(match.getPlayerScoreByPosition(0))); - + mTvP2Name.setText(match.getPlayerNameByPosition(1)); mTvP2Score.setText(String.valueOf(match.getPlayerScoreByPosition(1))); } @@ -108,12 +134,31 @@ public class MatchRecapView extends FrameLayout { /** Configures group state with leaderboard RecyclerView. */ private void setupGroupState(final Match match) { updateVisibility(mStateGroup); - - mRvLeaderboard.setLayoutManager(new LinearLayoutManager(getContext())); - - final MainMenuGroupMatchAdapter adapter = new MainMenuGroupMatchAdapter(); - mRvLeaderboard.setAdapter(adapter); - adapter.updateMatch(match); + + // Initialize adapter and layout manager only once + if (mGroupMatchAdapter == null) { + mGroupMatchAdapter = new MainMenuGroupMatchAdapter(); + mRvLeaderboard.setLayoutManager(new LinearLayoutManager(getContext())); + mRvLeaderboard.setAdapter(mGroupMatchAdapter); + } + + // Update the adapter with new match data + mGroupMatchAdapter.updateMatch(match); + } + + /** Configures group state with pre-enriched participants. */ + private void setupGroupStateWithParticipants(final List enrichedParticipants) { + updateVisibility(mStateGroup); + + // Initialize adapter and layout manager only once + if (mGroupMatchAdapter == null) { + mGroupMatchAdapter = new MainMenuGroupMatchAdapter(); + mRvLeaderboard.setLayoutManager(new LinearLayoutManager(getContext())); + mRvLeaderboard.setAdapter(mGroupMatchAdapter); + } + + // Update the adapter with enriched participant data + mGroupMatchAdapter.updateParticipants(enrichedParticipants); } /** Shows only the specified state container, hides others. */ 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 2997903..646d06d 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 @@ -2,6 +2,7 @@ package com.aldo.apps.ochecompanion.ui; import android.content.Context; import android.util.AttributeSet; +import android.util.Log; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -13,11 +14,18 @@ import com.google.android.material.imageview.ShapeableImageView; import com.aldo.apps.ochecompanion.R; /** - * Reusable MaterialCardView for displaying player info: profile picture, username, - * and career statistics. Uses Glide for image loading with fallback to default icon. + * Reusable MaterialCardView for displaying player info: profile picture, + * username, + * and career statistics. Uses Glide for image loading with fallback to default + * icon. */ public class PlayerItemView extends MaterialCardView { + /** + * Tag for debugging purposes. + */ + private static final String TAG = "PlayerItemView"; + /** Player profile picture (circular, loaded via Glide). */ private ShapeableImageView mIvAvatar; @@ -50,32 +58,35 @@ 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( - getContext().getString(R.string.txt_player_average_base), + getContext().getString(R.string.txt_player_average_base), player.careerAverage)); if (player.profilePictureUri != null) { Glide.with(getContext()) - .load(player.profilePictureUri) - .into(mIvAvatar); + .load(player.profilePictureUri) + .into(mIvAvatar); } else { mIvAvatar.setImageResource(R.drawable.ic_users); } } /** - * Binds player data along with a specific score (e.g., match score) instead of career average. + * 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) + * @param score The specific score to display (e.g., current match score) */ public void bindWithScore(@NonNull final Player player, final int score) { - + Log.d(TAG, "bindWithScore() called with: player = [" + player + "], score = [" + score + "]"); mTvUsername.setText(player.username); // Display match score instead of career average mTvStats.setText(String.valueOf(score)); @@ -89,4 +100,71 @@ public class PlayerItemView extends MaterialCardView { } } + public void bindWithGameAverage(@NonNull final Player player, final double gameAverage) { + Log.d(TAG, "bindWithGameAverage() called with: player = [" + player + + "], gameAverage = [" + gameAverage + "]"); + mTvUsername.setText(player.username); + // Display game average + mTvStats.setText(String.format( + getContext().getString(R.string.txt_player_average_base), gameAverage)); + + if (player.profilePictureUri != null) { + Glide.with(getContext()) + .load(player.profilePictureUri) + .placeholder(R.drawable.ic_users) + .into(mIvAvatar); + } else { + mIvAvatar.setImageResource(R.drawable.ic_users); + } + } + + /** + * Binds player data with game average and position indicator. + * + * @param player The Player object containing data to display + * @param gameAverage The game average to display (points per 3 darts) + * @param position The player's position (1 = 1st place, 2 = 2nd place, etc.) + * @param isWinner Whether this player won (remaining score = 0) + */ + public void bindWithGameAverageAndPosition(@NonNull final Player player, final double gameAverage, + final int position, final boolean isWinner) { + Log.d(TAG, "bindWithGameAverageAndPosition() called with: player = [" + player + + "], gameAverage = [" + gameAverage + "], position = [" + position + "], isWinner = [" + isWinner + + "]"); + + // Build username with position indicator + final String positionSuffix = getPositionSuffix(position); + final String displayName = isWinner + ? "🏆 " + player.username + " (" + positionSuffix + ")" + : player.username + " (" + positionSuffix + ")"; + + mTvUsername.setText(displayName); + + // Display game average + mTvStats.setText(String.format( + getContext().getString(R.string.txt_player_average_base), gameAverage)); + + if (player.profilePictureUri != null) { + Glide.with(getContext()) + .load(player.profilePictureUri) + .placeholder(R.drawable.ic_users) + .into(mIvAvatar); + } else { + mIvAvatar.setImageResource(R.drawable.ic_users); + } + } + + /** + * Returns the position suffix (1st, 2nd, 3rd, 4th, etc.) + */ + private String getPositionSuffix(final int position) { + if (position == 1) + return "1st"; + if (position == 2) + return "2nd"; + if (position == 3) + return "3rd"; + return position + "th"; + } + } diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/MainMenuGroupMatchAdapter.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/MainMenuGroupMatchAdapter.java index 4869e28..8605fda 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/MainMenuGroupMatchAdapter.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/MainMenuGroupMatchAdapter.java @@ -21,7 +21,8 @@ import java.util.List; /** * RecyclerView adapter for displaying group match results in Main Menu. - * Displays participants sorted by match score (descending) with their names, scores, and profile pictures. + * Displays participants sorted by match score (descending) with their names, + * scores, and profile pictures. */ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter { @@ -34,7 +35,7 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter Integer.compare(p2.score, p1.score)); - + + // Sort participants by remaining score (lowest to highest = best to worst) + // In darts, 0 remaining = winner, so lowest score = best placement + participants.sort((p1, p2) -> Integer.compare(p1.score, p2.score)); + // Add sorted participants to the display list mParticipantsList.addAll(participants); - + // Notify RecyclerView to refresh the display notifyDataSetChanged(); } + /** + * Updates the adapter with pre-enriched participant data (e.g., with profile + * pictures). + * Use this when participants have already been enriched with database data. + * + * @param participants Pre-enriched participant list to display. + */ + @SuppressLint("NotifyDataSetChanged") + public void updateParticipants(final List participants) { + // Clear any existing participant data + mParticipantsList.clear(); + + if (participants == null || participants.isEmpty()) { + Log.d(TAG, "updateParticipants: No participants provided, just clearing."); + notifyDataSetChanged(); + return; + } + + // Sort participants by remaining score (lowest to highest = best to worst) + // In darts, 0 remaining = winner, so lowest score = best placement + participants.sort((p1, p2) -> Integer.compare(p1.score, p2.score)); + + // Add sorted participants to the display list + mParticipantsList.addAll(participants); + + // Notify RecyclerView to refresh the display + notifyDataSetChanged(); + } /** * ViewHolder for displaying player items in group match view. @@ -126,20 +158,25 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter