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:
+ *
+ * - Automatic pre-selection of players from the most recent match for speed
+ * - Dynamic button state that displays the current selection count
+ * - Integration with {@link PlayerSelectionAdapter} for multi-select functionality
+ * - Validation to ensure at least one player is selected before starting
+ *
+ *
+ * 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:
+ *
+ * - A {@link RecyclerView} for displaying the player list
+ * - A {@link MaterialButton} for confirming the selection
+ *
+ *
+ * @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:
+ *
+ * - Binds UI components ({@link RecyclerView}, {@link MaterialButton})
+ * - Configures the {@link RecyclerView} with a {@link LinearLayoutManager}
+ * - Initializes the {@link PlayerSelectionAdapter} with selection callback
+ * - Sets up the confirmation button click listener
+ * - Triggers asynchronous data loading via {@link #loadData()}
+ *
+ *
+ * @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:
+ *
+ * - Fetching all players from the database
+ * - Retrieving the last match's participant data for pre-selection
+ * - Parsing the last match data using {@link MatchProgressConverter}
+ * - Extracting player IDs from the previous match
+ * - Updating the UI on the main thread with loaded data
+ * - Pre-selecting players who participated in the last match
+ *
+ *
+ * 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:
+ *
+ * - Enabled State: The button is enabled only when at least one player
+ * is selected (count > 0), preventing match initiation with empty selections
+ * - Text Display: Shows "START MATCH (count)" when players are selected,
+ * or "SELECT PLAYERS" as a prompt when no selections are made
+ *
+ *
+ * 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:
+ *
+ * - Constructs a list of selected {@link Player} objects by filtering
+ * {@link #mAllPlayers} based on {@link #mSelectedIds}
+ * - Validates that at least one player is selected (early return if empty)
+ * - Launches {@link GameActivity} with the selected players and default score
+ * ({@link DartsConstants#DEFAULT_GAME_SCORE})
+ * - Dismisses this dialog fragment after successfully starting the match
+ *
+ *
+ * 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:
+ *
+ * - Player identity information (username and profile picture)
+ * - High-level performance metrics (career average, first-9 average, checkout percentage, best finish)
+ * - Threshold score counters (60+, 100+, 140+, and 180 counts)
+ * - Interactive dartboard heatmap visualization via {@link HeatmapView}
+ *
+ *
+ * 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:
+ *
+ * - Calls the superclass constructor with context and attributes
+ * - Inflates the {@code R.layout.player_stats_layout} resource into this view
+ * - Initializes all child views via {@link #initViews()}
+ *
+ *
+ * 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:
+ *
+ * - Identity: Profile image and username text view
+ * - Performance Metrics: Career average, first-9 average, checkout percentage,
+ * and best finish text views
+ * - Threshold Counters: Four text views for 60+, 100+, 140+, and 180 counts
+ * - Visualization: The {@link HeatmapView} for dartboard hit distribution
+ *
+ *
+ * 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:
+ *
+ * - 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)
+ * - High-Level Metrics: Formats and displays career average, first-9 average,
+ * checkout percentage (with % symbol), and highest checkout value
+ * - Threshold Totals: Displays counts for 60+, 100+, 140+, and 180 point turns
+ * - Heatmap Rendering: Passes statistics to the {@link HeatmapView} for
+ * visual dartboard representation
+ *
+ *
+ * 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