diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 74ec68d..a688c31 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -47,4 +47,5 @@ dependencies {
annotationProcessor(libs.room.compiler)
implementation(libs.preferences)
implementation(libs.konfetti)
+ implementation(libs.gson)
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0dff777..13eba5d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,11 +6,14 @@
that other apps created.
-->
-
+
-
-
+
+
mCurrentTurnDarts = new ArrayList<>();
+ /**
+ * Dart hit details (base value and multiplier) for current turn.
+ * Parallel to mCurrentTurnDarts, used for hit distribution tracking.
+ * Cleared when turn is submitted.
+ */
+ private final List mCurrentTurnDartHits = new ArrayList<>();
+
/**
* Flag indicating turn has ended (bust, win, or 3 darts thrown).
* Prevents additional dart entry until turn is submitted.
*/
private boolean mIsTurnOver = false;
+ /**
+ * Helper class to track dart hit details for statistics.
+ */
+ private static class DartHit {
+ /**
+ * The dartboard number hit (1-20 or 25 for bull).
+ */
+ final int baseValue;
+
+ /**
+ * The multiplier applied to the base value (1=single, 2=double, 3=triple).
+ */
+ 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)
+ */
+ DartHit(final int baseValue, final int multiplier) {
+ this.baseValue = baseValue;
+ this.multiplier = multiplier;
+ }
+ }
+
/**
* Cached references to keyboard buttons (1-20) for efficient styling updates.
*/
@@ -173,8 +208,34 @@ public class GameActivity extends AppCompatActivity {
*/
private GridLayout glKeyboard;
+ /**
+ * Singleton instance of SoundEngine for playing game sound effects.
+ */
private SoundEngine mSoundEngine;
+ /**
+ * Flag indicating whether haptic feedback is enabled.
+ * Controlled by user preferences.
+ */
+ private boolean mIsVibrationEnabled;
+
+ /**
+ * Flag indicating whether audio feedback is enabled.
+ * Controlled by user preferences.
+ */
+ private boolean mIsAudioEnabled;
+
+ /**
+ * Reference to the Room database instance for statistics and player data access.
+ */
+ private AppDatabase mDatabase;
+
+ /**
+ * Locks for synchronizing statistics updates per player to prevent race conditions.
+ * Each player gets their own lock object to allow concurrent updates for different players.
+ */
+ private final java.util.Map mPlayerStatsLocks = new java.util.HashMap<>();
+
/**
* Starts GameActivity with specified players and starting score.
*
@@ -207,6 +268,7 @@ public class GameActivity extends AppCompatActivity {
return insets;
});
mSoundEngine = SoundEngine.getInstance(this);
+ mDatabase = AppDatabase.getDatabase(this);
// Extract game parameters from intent
mStartingScore = getIntent().getIntExtra(EXTRA_START_SCORE, DartsConstants.DEFAULT_GAME_SCORE);
@@ -221,6 +283,14 @@ public class GameActivity extends AppCompatActivity {
}).start();
}
+ @Override
+ protected void onResume() {
+ super.onResume();
+ final SharedPreferences settingPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+ mIsAudioEnabled = settingPrefs.getBoolean(getString(R.string.pref_key_audio_feedback), true);
+ mIsVibrationEnabled = settingPrefs.getBoolean(getString(R.string.pref_key_vibration_feedback), true);
+ }
+
/**
* Initializes UI component references and sets up click listeners.
*/
@@ -319,20 +389,35 @@ public class GameActivity extends AppCompatActivity {
if (scoreAfterDart < 0 || scoreAfterDart == DartsConstants.BUST_SCORE || (scoreAfterDart == 0 && !isDouble)) {
// BUST CONDITION: Score < 0, Score == 1, or Score == 0 on a non-double
mCurrentTurnDarts.add(points);
+ mCurrentTurnDartHits.add(new DartHit(baseValue, mMultiplier));
+
+ // Track double-out miss if trying to finish but failed
+ if (scoreBeforeDart <= 40 && isDouble) {
+ trackDoubleAttempt(active, true); // Missed double-out attempt
+ }
+
updateTurnIndicators();
mIsTurnOver = true;
- mSoundEngine.playBustedSound();
+ if (mIsAudioEnabled) {
+ mSoundEngine.playBustedSound();
+ }
Toast.makeText(this, "BUST!", Toast.LENGTH_SHORT).show();
// In a pro interface, we usually wait for "Submit" or auto-submit after a short delay
} else if (scoreAfterDart == 0 && isDouble) {
// VICTORY CONDITION
mCurrentTurnDarts.add(points);
+ mCurrentTurnDartHits.add(new DartHit(baseValue, mMultiplier));
+
+ // Track successful double-out
+ trackDoubleAttempt(active, false);
+
updateTurnIndicators();
mIsTurnOver = true;
handleWin(active, mCurrentTurnDarts.size(), scoreBeforeDart);
} else {
// VALID THROW
mCurrentTurnDarts.add(points);
+ mCurrentTurnDartHits.add(new DartHit(baseValue, mMultiplier));
updateTurnIndicators();
updateUI();
@@ -400,23 +485,76 @@ public class GameActivity extends AppCompatActivity {
* @param pointsMade Total points scored this turn
* @param wasBust Whether the turn resulted in a bust
*/
- private void updatePlayerStats(GameActivity.X01State active, int dartsThrown, int pointsMade, boolean wasBust) {
+ private void updatePlayerStats(final GameActivity.X01State active, final int dartsThrown, final int pointsMade, final boolean wasBust) {
+ updatePlayerStats(active, dartsThrown, pointsMade, wasBust, 0);
+ }
+
+ /**
+ * Updates player statistics in the database after a turn.
+ * Overload that accepts checkout value for tracking successful finishes.
+ *
+ * @param active Current player's game state
+ * @param dartsThrown Number of darts thrown this turn
+ * @param pointsMade Total points scored this turn
+ * @param wasBust Whether the turn resulted in a bust
+ * @param checkoutValue The checkout score if this was a winning turn (0 if not a checkout)
+ */
+ private void updatePlayerStats(final GameActivity.X01State active, final int dartsThrown, final int pointsMade, final boolean wasBust, final int checkoutValue) {
new Thread(() -> {
final Player player = active.player;
if (player != null && player.id != 0) {
final Statistics playerStats = AppDatabase.getDatabase(GameActivity.this).statisticsDao().getStatisticsForPlayer(player.id);
- playerStats.saveDartsThrown(dartsThrown, pointsMade);
- Log.d(TAG, "submitTurn: dartsThrown = [" + dartsThrown + "], pointsMade = [" + pointsMade + "]");
- if (!wasBust && dartsThrown < 3) {
- playerStats.addMissedDarts(3 - dartsThrown);
+
+ // Track darts thrown or missed
+ if (wasBust) {
+ // On bust, all darts in the turn are wasted
+ playerStats.addMissedDarts(dartsThrown);
+ Log.d(TAG, "updatePlayerStats: Bust! Recorded " + dartsThrown + " missed darts");
+ } else {
+ // Normal turn - record darts and points
+ playerStats.saveDartsThrown(dartsThrown, pointsMade);
+ Log.d(TAG, "updatePlayerStats: dartsThrown = [" + dartsThrown + "], pointsMade = [" + pointsMade + "]");
+
+ // Track missed darts if turn ended early (less than 3 darts)
+ if (dartsThrown < 3) {
+ playerStats.addMissedDarts(3 - dartsThrown);
+ }
}
- Log.d(TAG, "submitTurn: statistics = [" + playerStats + "]");
+
+ // Track scoring milestones (60+, 100+, 140+, 180)
+ if (pointsMade >= 180) {
+ playerStats.setCount180(playerStats.getCount180() + 1);
+ Log.d(TAG, "updatePlayerStats: Perfect 180! Total: " + playerStats.getCount180());
+ } else if (pointsMade >= 140) {
+ playerStats.setCount140Plus(playerStats.getCount140Plus() + 1);
+ } else if (pointsMade >= 100) {
+ playerStats.setCount100Plus(playerStats.getCount100Plus() + 1);
+ } else if (pointsMade >= 60) {
+ playerStats.setCount60Plus(playerStats.getCount60Plus() + 1);
+ }
+
+ // Track first 9 darts statistics (first 3 turns of the match)
+ if (active.dartsThrown < 9) {
+ final long dartsToAdd = Math.min(dartsThrown, 9 - active.dartsThrown);
+ playerStats.setTotalFirst9Darts(playerStats.getTotalFirst9Darts() + dartsToAdd);
+ playerStats.setTotalFirst9Points(playerStats.getTotalFirst9Points() + pointsMade);
+ Log.d(TAG, "updatePlayerStats: First 9 tracking - darts: " + dartsToAdd + ", points: " + pointsMade);
+ }
+
+ // Track successful checkout
+ if (checkoutValue > 0) {
+ playerStats.setSuccessfulCheckouts(playerStats.getSuccessfulCheckouts() + 1);
+ playerStats.setHighestCheckout(checkoutValue);
+ Log.d(TAG, "updatePlayerStats: Checkout tracked - value: " + checkoutValue);
+ }
+
+ Log.d(TAG, "updatePlayerStats: statistics = [" + playerStats + "]");
AppDatabase.getDatabase(GameActivity.this).statisticsDao().updateStatistics(playerStats);
// Calculate career average: total points / total darts thrown
- final long totalDarts = playerStats.getDartsThrown();
+ final long totalDarts = playerStats.getTotalDartsThrown();
if (totalDarts > 0) {
- player.careerAverage = (double) playerStats.getOverallPointsMade() / totalDarts * 3;
+ player.careerAverage = (double) playerStats.getTotalPoints() / totalDarts * 3;
} else {
player.careerAverage = 0.0;
}
@@ -445,20 +583,24 @@ public class GameActivity extends AppCompatActivity {
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, "submitTurn: Pattern vibration");
- // Pattern that should match the 180 shout.
- 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, "submitTurn: Vibrating legacy mode");
- vibrator.vibrate(500);
- } else {
- Log.e(TAG, "submitTurn: Vibrator not available");
+ final Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
+ if (vibrator != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Log.d(TAG, "submitTurn: Pattern vibration");
+ // Pattern that should match the 180 shout.
+ 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, "submitTurn: Vibrating legacy mode");
+ vibrator.vibrate(500);
+ } else {
+ Log.e(TAG, "submitTurn: Vibrator not available");
+ }
+ }
+ if (mIsAudioEnabled) {
+ mSoundEngine.playOneHundredAndEightySound();
}
- mSoundEngine.playOneHundredAndEightySound();
}
// Re-check logic for non-double finish or score of 1
@@ -473,11 +615,16 @@ public class GameActivity extends AppCompatActivity {
}
updatePlayerStats(active, mCurrentTurnDarts.size(), turnTotal, isBust);
+ // Record all dart hits to statistics now that turn is confirmed
+ // IMPORTANT: Pass a copy to avoid race condition with clearing the list
+ recordTurnHitsToStatistics(active, new ArrayList<>(mCurrentTurnDartHits));
+
// Rotate to next player
mActivePlayerIndex = (mActivePlayerIndex + 1) % mPlayerStates.size();
// Reset turn state
mCurrentTurnDarts.clear();
+ mCurrentTurnDartHits.clear();
mIsTurnOver = false;
// Update UI for next player
@@ -612,21 +759,122 @@ public class GameActivity extends AppCompatActivity {
return String.valueOf(score);
}
+ /**
+ * Tracks a double-out attempt in player statistics.
+ * Records whether the attempt was successful or missed.
+ *
+ * @param playerState X01State of the player
+ * @param isMissed true if the double-out was missed, false if successful
+ */
+ private void trackDoubleAttempt(final X01State playerState, final boolean isMissed) {
+ new Thread(() -> {
+ try {
+ Statistics playerStats = mDatabase.statisticsDao().getStatisticsForPlayer(playerState.playerId);
+ if (playerStats != null) {
+ playerStats.addDoubleOutTarget(isMissed);
+ mDatabase.statisticsDao().updateStatistics(playerStats);
+ Log.d(TAG, "trackDoubleAttempt: Recorded double attempt (missed=" + isMissed + ") for player " + playerState.name);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "trackDoubleAttempt: Failed to track double attempt", e);
+ }
+ }).start();
+ }
+
+ /**
+ * Increments matchesPlayed counter for all players in the current match.
+ * Called when a match is completed (someone wins).
+ */
+ private void incrementMatchesPlayed() {
+ new Thread(() -> {
+ try {
+ for (X01State playerState : mPlayerStates) {
+ Statistics playerStats = mDatabase.statisticsDao().getStatisticsForPlayer(playerState.playerId);
+ if (playerStats != null) {
+ playerStats.addCompletedMatch();
+ mDatabase.statisticsDao().updateStatistics(playerStats);
+ Log.d(TAG, "incrementMatchesPlayed: Incremented for player " + playerState.name + ", total: " + playerStats.getMatchesPlayed());
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "incrementMatchesPlayed: Failed to increment matches", e);
+ }
+ }).start();
+ }
+
+ /**
+ * Records all dart hits from a confirmed turn to the player's statistics.
+ * Updates the hit distribution map for heat map visualization.
+ * Only called after turn is submitted to avoid recording unconfirmed throws.
+ *
+ * @param playerState X01State of the player
+ * @param dartHits List of dart hit details from the turn
+ */
+ private void recordTurnHitsToStatistics(final X01State playerState, final List dartHits) {
+ if (dartHits.isEmpty()) return;
+
+ new Thread(() -> {
+ // Get or create lock object for this player to prevent race conditions
+ final Object lock;
+ synchronized (mPlayerStatsLocks) {
+ lock = mPlayerStatsLocks.computeIfAbsent(playerState.playerId, k -> new Object());
+ }
+
+ // Synchronize the read-modify-write operation for this player
+ synchronized (lock) {
+ try {
+ Statistics playerStats = mDatabase.statisticsDao().getStatisticsForPlayer(playerState.playerId);
+ if (playerStats != null) {
+ Log.d(TAG, "recordTurnHitsToStatistics: Before recording - hitDistribution size: " + playerStats.getHitDistribution().size());
+ // Record all darts from this turn
+ for (DartHit hit : dartHits) {
+ playerStats.recordDartHit(hit.baseValue, hit.multiplier);
+ }
+ Log.d(TAG, "recordTurnHitsToStatistics: After recording - hitDistribution size: " + playerStats.getHitDistribution().size());
+ Log.d(TAG, "recordTurnHitsToStatistics: hitDistribution contents: " + playerStats.getHitDistribution());
+ mDatabase.statisticsDao().updateStatistics(playerStats);
+ Log.d(TAG, "recordTurnHitsToStatistics: Recorded " + dartHits.size() + " darts for player " + playerState.name);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "recordTurnHitsToStatistics: Failed to record hits", e);
+ }
+ }
+ }).start();
+ }
+
/**
* Handles win condition when a player finishes on zero with a double.
* Updates statistics, displays win toast, and plays celebration animation.
*
* @param winner X01State of the winning player
* @param dartsThrown Number of darts thrown in the winning turn
- * @param pointsMade Points scored in the winning turn
+ * @param pointsMade Points scored in the winning turn (total turn score)
*/
private void handleWin(final X01State winner, final int dartsThrown, final int pointsMade) {
- updatePlayerStats(winner, dartsThrown, pointsMade, false);
+ // Calculate checkout value (the score of the FINAL dart, not the whole turn)
+ final int checkoutValue = mCurrentTurnDarts.get(mCurrentTurnDarts.size() - 1);
+
+ // Update statistics with correct checkout value
+ updatePlayerStats(winner, dartsThrown, pointsMade, false, checkoutValue);
+
+ // Record all dart hits from winning turn to hit distribution
+ // IMPORTANT: Pass a copy to avoid race condition with clearing the list
+ recordTurnHitsToStatistics(winner, new ArrayList<>(mCurrentTurnDartHits));
+
+ // Increment matchesPlayed for all players in the match
+ incrementMatchesPlayed();
+
+ // Clear turn state after recording
+ mCurrentTurnDarts.clear();
+ mCurrentTurnDartHits.clear();
+
// Show win notification
- Toast.makeText(this, winner.name + " WINS!", Toast.LENGTH_LONG).show();
+ Toast.makeText(this, winner.name + " WINS! Checkout: " + checkoutValue, Toast.LENGTH_LONG).show();
playWinnerAnimation(winner.name);
- mSoundEngine.playWinnerSound();
+ if (mIsAudioEnabled) {
+ mSoundEngine.playWinnerSound();
+ }
// TODO: Consider adding:
// - Statistics display
@@ -682,6 +930,11 @@ public class GameActivity extends AppCompatActivity {
*/
final Player player;
+ /**
+ * Player's ID from the database for statistics lookup.
+ */
+ final long playerId;
+
/**
* Player's display name for convenience purpose, as extracted from the player object.
*/
@@ -705,6 +958,7 @@ public class GameActivity extends AppCompatActivity {
*/
X01State(final Player player, final int startScore) {
this.player = player;
+ this.playerId = player.id;
this.name = player.username;
this.remainingScore = startScore;
}
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 fd46fbf..f5d70c7 100644
--- a/app/src/main/java/com/aldo/apps/ochecompanion/MainMenuActivity.java
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/MainMenuActivity.java
@@ -4,6 +4,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
+import android.view.View;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
@@ -91,6 +92,8 @@ public class MainMenuActivity extends AppCompatActivity {
applyTestData(mTestCounter);
mTestCounter++;
});
+
+ findViewById(R.id.title_view).setOnClickListener(v -> startActivity(new Intent(MainMenuActivity.this, TestActivity.class)));
}
/**
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 a9eb815..72e0358 100644
--- a/app/src/main/java/com/aldo/apps/ochecompanion/SettingsActivity.java
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/SettingsActivity.java
@@ -44,4 +44,6 @@ public class SettingsActivity extends AppCompatActivity {
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
+
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/TestActivity.java b/app/src/main/java/com/aldo/apps/ochecompanion/TestActivity.java
new file mode 100644
index 0000000..12203d4
--- /dev/null
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/TestActivity.java
@@ -0,0 +1,57 @@
+package com.aldo.apps.ochecompanion;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.activity.EdgeToEdge;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+
+import com.aldo.apps.ochecompanion.database.AppDatabase;
+import com.aldo.apps.ochecompanion.database.objects.Player;
+import com.aldo.apps.ochecompanion.database.objects.Statistics;
+import com.aldo.apps.ochecompanion.ui.HeatmapView;
+
+import java.util.List;
+
+public class TestActivity extends AppCompatActivity {
+
+ private static final String TAG = "TestActivity";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ EdgeToEdge.enable(this);
+ setContentView(R.layout.activity_test);
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
+ Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
+ return insets;
+ });
+
+ final HeatmapView heatmap = findViewById(R.id.heatmap);
+ new Thread(() -> {
+ // Access the singleton database and query all players
+ final List allPlayers = AppDatabase.getDatabase(getApplicationContext())
+ .playerDao()
+ .getAllPlayers();
+
+ if (allPlayers == null || allPlayers.isEmpty()) {
+ Log.d(TAG, "onCreate: Cannot continue");
+ return;
+ }
+
+ final Player firstPlayer = allPlayers.get(0);
+ final Statistics stats = AppDatabase.getDatabase(this)
+ .statisticsDao()
+ .getStatisticsForPlayer(firstPlayer.id);
+
+ runOnUiThread(() -> {
+ Log.d(TAG, "onCreate: Applying stats [" + stats + "]");
+ heatmap.setStats(stats);
+ });
+ }).start();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/database/AppDatabase.java b/app/src/main/java/com/aldo/apps/ochecompanion/database/AppDatabase.java
index 158c128..14aae1e 100644
--- a/app/src/main/java/com/aldo/apps/ochecompanion/database/AppDatabase.java
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/database/AppDatabase.java
@@ -4,6 +4,7 @@ import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
+import androidx.room.TypeConverters;
import com.aldo.apps.ochecompanion.database.dao.MatchDao;
import com.aldo.apps.ochecompanion.database.dao.PlayerDao;
@@ -11,6 +12,7 @@ import com.aldo.apps.ochecompanion.database.dao.StatisticsDao;
import com.aldo.apps.ochecompanion.database.objects.Match;
import com.aldo.apps.ochecompanion.database.objects.Player;
import com.aldo.apps.ochecompanion.database.objects.Statistics;
+import com.aldo.apps.ochecompanion.utils.converters.HitDistributionConverter;
/**
* Main Room database class for the Oche Companion darts application.
@@ -23,7 +25,8 @@ import com.aldo.apps.ochecompanion.database.objects.Statistics;
* @see Player
* @see Match
*/
-@Database(entities = {Player.class, Match.class, Statistics.class}, version = 10, exportSchema = false)
+@Database(entities = {Player.class, Match.class, Statistics.class}, version = 12, exportSchema = false)
+@TypeConverters({HitDistributionConverter.class})
public abstract class AppDatabase extends RoomDatabase {
/**
diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/database/objects/Player.java b/app/src/main/java/com/aldo/apps/ochecompanion/database/objects/Player.java
index 75adbe4..9f89e42 100644
--- a/app/src/main/java/com/aldo/apps/ochecompanion/database/objects/Player.java
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/database/objects/Player.java
@@ -58,8 +58,6 @@ public class Player implements Parcelable {
this.profilePictureUri = profilePictureUri;
}
-
-
/**
* Parcelable constructor to reconstruct Player from Parcel.
*
diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/database/objects/Statistics.java b/app/src/main/java/com/aldo/apps/ochecompanion/database/objects/Statistics.java
index f270549..26faea3 100644
--- a/app/src/main/java/com/aldo/apps/ochecompanion/database/objects/Statistics.java
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/database/objects/Statistics.java
@@ -1,8 +1,16 @@
package com.aldo.apps.ochecompanion.database.objects;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
import androidx.room.Entity;
-import androidx.room.ForeignKey;
import androidx.room.PrimaryKey;
+import androidx.room.TypeConverters;
+
+import com.aldo.apps.ochecompanion.utils.converters.HitDistributionConverter;
+
+import java.util.HashMap;
+import java.util.Map;
/**
* Room database entity representing player performance statistics.
@@ -16,6 +24,11 @@ import androidx.room.PrimaryKey;
@Entity(tableName = "statistics")
public class Statistics {
+ /**
+ * Tag for debugging purposes.
+ */
+ private static final String TAG = "Statistics";
+
/**
* Player ID this statistics record belongs to (primary key).
* References the Player entity's ID field.
@@ -23,11 +36,17 @@ public class Statistics {
@PrimaryKey
private long playerId;
+ /**
+ * Cumulative points scored across all darts thrown.
+ * Used with dartsThrown to calculate career average.
+ */
+ private long totalPoints;
+
/**
* Total number of darts thrown across all matches.
* Used to calculate career average and accuracy metrics.
*/
- private long dartsThrown;
+ private long totalDartsThrown;
/**
* Total number of darts that missed the intended target.
@@ -36,22 +55,64 @@ public class Statistics {
private long dartsMissed;
/**
- * Cumulative points scored across all darts thrown.
- * Used with dartsThrown to calculate career average.
+ * Total points scored in the first 9 darts of matches.
+ * Used to calculate the "first 9" average, measuring starting consistency.
*/
- private long overallPointsMade;
+ private long totalFirst9Points;
+
+ /**
+ * Total number of first 9 darts thrown in matches.
+ * Used with totalFirst9Points to calculate first 9 average.
+ */
+ private long totalFirst9Darts;
+
+ /**
+ * Count of turns scoring 60 or more points.
+ * Tracks high-scoring consistency (e.g., T20).
+ */
+ private int count60Plus;
+
+ /**
+ * Count of turns scoring 100 or more points.
+ * Tracks century+ scoring frequency.
+ */
+ private int count100Plus;
+
+ /**
+ * Count of turns scoring 140 or more points.
+ * Tracks elite-level scoring (T20-T20-T20 = 140+).
+ */
+ private int count140Plus;
+
+ /**
+ * Count of perfect 180 scores (T20-T20-T20).
+ * The highest possible three-dart score.
+ */
+ private int count180;
/**
* Total number of completed matches for this player.
* Provides context for statistical significance.
*/
private int matchesPlayed;
+
+ /**
+ * Number of successful checkout finishes on doubles.
+ * Used with totalDartsAtDouble to calculate checkout percentage.
+ */
+ private int successfulCheckouts;
+
+ /**
+ * Highest checkout score achieved by the player.
+ * Tracks the player's best finishing performance.
+ */
+ private int highestCheckout;
/**
* Total number of double-out attempts (both successful and missed).
* Tracks how often player attempts to finish on doubles.
*/
- private int doubleOutsTargeted;
+ private int totalDartsAtDouble;
/**
* Number of failed double-out attempts.
@@ -59,6 +120,13 @@ public class Statistics {
*/
private int doubleOutsMissed;
+ /**
+ * Hit distribution map tracking frequency of each board segment hit.
+ * Keys are segment identifiers (e.g., "t20", "d16", "s5", "sb", "db").
+ * Values are hit counts. Used for heat map visualization and accuracy analysis.
+ */
+ private Map hitDistribution;
+
/**
* Constructs a new Statistics record for a player.
* Initializes all counters to zero.
@@ -67,11 +135,12 @@ public class Statistics {
*/
public Statistics(final long playerId) {
this.playerId = playerId;
- dartsThrown = 0;
- overallPointsMade = 0;
+ totalDartsThrown = 0;
+ totalPoints = 0;
matchesPlayed = 0;
- doubleOutsTargeted = 0;
+ totalDartsAtDouble = 0;
doubleOutsMissed = 0;
+ hitDistribution = new HashMap<>();
}
/**
@@ -81,8 +150,8 @@ public class Statistics {
* @param pointsMade Total points scored with those darts
*/
public void saveDartsThrown(final long dartsThrown, final long pointsMade) {
- this.dartsThrown += dartsThrown;
- this.overallPointsMade += pointsMade;
+ this.totalDartsThrown += dartsThrown;
+ this.totalPoints += pointsMade;
}
/**
@@ -109,78 +178,423 @@ public class Statistics {
* @param isMissed true if the double-out was missed, false if successful
*/
public void addDoubleOutTarget(final boolean isMissed) {
- doubleOutsTargeted++;
+ totalDartsAtDouble++;
if (isMissed) {
doubleOutsMissed++;
}
}
+ /**
+ * Records a dart hit on a specific board segment.
+ * Increments the hit count for the given segment in the distribution map.
+ *
+ * @param baseValue The dartboard number (1-20 or 25 for bull)
+ * @param multiplier The multiplier (1=single, 2=double, 3=triple)
+ */
+ public void recordDartHit(final int baseValue, final int multiplier) {
+ if (hitDistribution == null) {
+ hitDistribution = new HashMap<>();
+ }
+ final String segmentKey = HitDistributionConverter.getSegmentKey(baseValue, multiplier);
+ hitDistribution = HitDistributionConverter.recordHit(hitDistribution, segmentKey);
+ Log.d(TAG, "recordDartHit: Recorded hit on " + segmentKey + ", total hits: " + hitDistribution.get(segmentKey));
+ }
+
+ /**
+ * Gets the hit distribution map.
+ *
+ * @return Map of segment keys to hit counts
+ */
+ public Map getHitDistribution() {
+ if (hitDistribution == null) {
+ hitDistribution = new HashMap<>();
+ }
+ return hitDistribution;
+ }
+
+ /**
+ * Sets the hit distribution map.
+ * Used by Room when loading from database.
+ *
+ * @param hitDistribution Map of segment keys to hit counts
+ */
+ public void setHitDistribution(final Map hitDistribution) {
+ this.hitDistribution = hitDistribution != null ? hitDistribution : new HashMap<>();
+ }
+
+ /**
+ * Gets the player ID this statistics record belongs to.
+ *
+ * @return Player ID (primary key)
+ */
public long getPlayerId() {
return playerId;
}
- public void setPlayerId(long playerId) {
+ /**
+ * Sets the player ID for this statistics record.
+ * Used by Room when loading from database.
+ *
+ * @param playerId Player ID to set
+ */
+ public void setPlayerId(final long playerId) {
this.playerId = playerId;
}
- public long getDartsThrown() {
- return dartsThrown;
+ /**
+ * Gets the total number of darts thrown across all matches.
+ *
+ * @return Total darts thrown
+ */
+ public long getTotalDartsThrown() {
+ return totalDartsThrown;
}
- public void setDartsThrown(long dartsThrown) {
- this.dartsThrown = dartsThrown;
+ /**
+ * Sets the total number of darts thrown.
+ * Used by Room when loading from database.
+ *
+ * @param totalDartsThrown Total darts thrown to set
+ */
+ public void setTotalDartsThrown(final long totalDartsThrown) {
+ this.totalDartsThrown = totalDartsThrown;
}
+ /**
+ * Gets the total number of darts that missed the target.
+ *
+ * @return Total darts missed
+ */
public long getDartsMissed() {
return dartsMissed;
}
- public void setDartsMissed(long dartsMissed) {
+ /**
+ * Sets the total number of darts missed.
+ * Used by Room when loading from database.
+ *
+ * @param dartsMissed Total darts missed to set
+ */
+ public void setDartsMissed(final long dartsMissed) {
this.dartsMissed = dartsMissed;
}
- public long getOverallPointsMade() {
- return overallPointsMade;
+ /**
+ * Gets the cumulative points scored across all darts thrown.
+ *
+ * @return Total points made
+ */
+ public long getTotalPoints() {
+ return totalPoints;
}
- public void setOverallPointsMade(long overallPointsMade) {
- this.overallPointsMade = overallPointsMade;
+ /**
+ * Sets the cumulative points scored.
+ * Used by Room when loading from database.
+ *
+ * @param totalPoints Total points made to set
+ */
+ public void setTotalPoints(final long totalPoints) {
+ this.totalPoints = totalPoints;
}
+ /**
+ * Gets the total number of completed matches for this player.
+ *
+ * @return Total matches played
+ */
public int getMatchesPlayed() {
return matchesPlayed;
}
- public void setMatchesPlayed(int matchesPlayed) {
+ /**
+ * Sets the total number of completed matches.
+ * Used by Room when loading from database.
+ *
+ * @param matchesPlayed Total matches played to set
+ */
+ public void setMatchesPlayed(final int matchesPlayed) {
this.matchesPlayed = matchesPlayed;
}
- public int getDoubleOutsTargeted() {
- return doubleOutsTargeted;
+ /**
+ * Gets the total number of double-out attempts (successful and missed).
+ *
+ * @return Total double-out attempts
+ */
+ public int getTotalDartsAtDouble() {
+ return totalDartsAtDouble;
}
- public void setDoubleOutsTargeted(int doubleOutsTargeted) {
- this.doubleOutsTargeted = doubleOutsTargeted;
+ /**
+ * Sets the total number of double-out attempts.
+ * Used by Room when loading from database.
+ *
+ * @param totalDartsAtDouble Total double-out attempts to set
+ */
+ public void setTotalDartsAtDouble(final int totalDartsAtDouble) {
+ this.totalDartsAtDouble = totalDartsAtDouble;
}
+ /**
+ * Gets the number of failed double-out attempts.
+ *
+ * @return Total double-outs missed
+ */
public int getDoubleOutsMissed() {
return doubleOutsMissed;
}
- public void setDoubleOutsMissed(int doubleOutsMissed) {
+ /**
+ * Gets the total points scored in first 9 darts.
+ *
+ * @return Total first 9 points
+ */
+ public long getTotalFirst9Points() {
+ return totalFirst9Points;
+ }
+
+ /**
+ * Sets the total points scored in first 9 darts.
+ * Used by Room when loading from database.
+ *
+ * @param totalFirst9Points Total first 9 points to set
+ */
+ public void setTotalFirst9Points(final long totalFirst9Points) {
+ this.totalFirst9Points = totalFirst9Points;
+ }
+
+ /**
+ * Gets the total number of first 9 darts thrown.
+ *
+ * @return Total first 9 darts
+ */
+ public long getTotalFirst9Darts() {
+ return totalFirst9Darts;
+ }
+
+ /**
+ * Sets the total number of first 9 darts thrown.
+ * Used by Room when loading from database.
+ *
+ * @param totalFirst9Darts Total first 9 darts to set
+ */
+ public void setTotalFirst9Darts(final long totalFirst9Darts) {
+ this.totalFirst9Darts = totalFirst9Darts;
+ }
+
+ /**
+ * Gets the count of 60+ point turns.
+ *
+ * @return Count of 60+ scores
+ */
+ public int getCount60Plus() {
+ return count60Plus;
+ }
+
+ /**
+ * Sets the count of 60+ point turns.
+ * Used by Room when loading from database.
+ *
+ * @param count60Plus Count of 60+ scores to set
+ */
+ public void setCount60Plus(final int count60Plus) {
+ this.count60Plus = count60Plus;
+ }
+
+ /**
+ * Gets the count of 100+ point turns.
+ *
+ * @return Count of 100+ scores
+ */
+ public int getCount100Plus() {
+ return count100Plus;
+ }
+
+ /**
+ * Sets the count of 100+ point turns.
+ * Used by Room when loading from database.
+ *
+ * @param count100Plus Count of 100+ scores to set
+ */
+ public void setCount100Plus(final int count100Plus) {
+ this.count100Plus = count100Plus;
+ }
+
+ /**
+ * Gets the count of 140+ point turns.
+ *
+ * @return Count of 140+ scores
+ */
+ public int getCount140Plus() {
+ return count140Plus;
+ }
+
+ /**
+ * Sets the count of 140+ point turns.
+ * Used by Room when loading from database.
+ *
+ * @param count140Plus Count of 140+ scores to set
+ */
+ public void setCount140Plus(final int count140Plus) {
+ this.count140Plus = count140Plus;
+ }
+
+ /**
+ * Gets the count of perfect 180 scores.
+ *
+ * @return Count of 180 scores
+ */
+ public int getCount180() {
+ return count180;
+ }
+
+ /**
+ * Sets the count of perfect 180 scores.
+ * Used by Room when loading from database.
+ *
+ * @param count180 Count of 180 scores to set
+ */
+ public void setCount180(final int count180) {
+ this.count180 = count180;
+ }
+
+ /**
+ * Gets the number of successful checkout finishes.
+ *
+ * @return Count of successful checkouts
+ */
+ public int getSuccessfulCheckouts() {
+ return successfulCheckouts;
+ }
+
+ /**
+ * Sets the number of successful checkout finishes.
+ * Used by Room when loading from database.
+ *
+ * @param successfulCheckouts Count of successful checkouts to set
+ */
+ public void setSuccessfulCheckouts(final int successfulCheckouts) {
+ this.successfulCheckouts = successfulCheckouts;
+ }
+
+ /**
+ * Gets the highest checkout score achieved.
+ *
+ * @return Highest checkout value
+ */
+ public int getHighestCheckout() {
+ return highestCheckout;
+ }
+
+ /**
+ * Sets the highest checkout score achieved.
+ * Used by Room when loading from database.
+ *
+ * @param highestCheckout Highest checkout value to set
+ */
+ public void setHighestCheckout(final int highestCheckout) {
+ if (highestCheckout > this.highestCheckout) {
+ this.highestCheckout = highestCheckout;
+ } else {
+ Log.i(TAG, "setHighestCheckout: New checkout ["
+ + highestCheckout + "] not higher than existing [" + this.highestCheckout + "], do not set");
+ }
+ }
+
+ /**
+ * Calculates the career three-dart average.
+ *
+ * @return Three-dart average (0.0 if no darts thrown)
+ */
+ public double getAverage() {
+ if (totalDartsThrown == 0) return 0.0;
+ return ((double) totalPoints / totalDartsThrown) * 3;
+ }
+
+ /**
+ * Calculates the "First 9" scoring average.
+ * Measures consistency in the opening phase of matches.
+ *
+ * @return Three-dart first 9 average (0.0 if no first 9 darts thrown)
+ */
+ public double getFirst9Average() {
+ if (totalFirst9Darts == 0) return 0.0;
+ return ((double) totalFirst9Points / totalFirst9Darts) * 3;
+ }
+
+ /**
+ * Calculates the checkout success percentage.
+ * Measures efficiency at finishing on doubles.
+ *
+ * @return Checkout percentage (0.0-100.0, or 0.0 if no attempts)
+ */
+ public double getCheckoutPercentage() {
+ if (totalDartsAtDouble == 0) return 0.0;
+ return ((double) successfulCheckouts / totalDartsAtDouble) * 100;
+ }
+
+ /**
+ * Sets the number of failed double-out attempts.
+ * Used by Room when loading from database.
+ *
+ * @param doubleOutsMissed Total double-outs missed to set
+ */
+ public void setDoubleOutsMissed(final int doubleOutsMissed) {
this.doubleOutsMissed = doubleOutsMissed;
}
+ /**
+ * Finds the highest hit count across all segments.
+ * Useful for normalizing heatmap colors based on the player's most frequent target.
+ */
+ public int getMaxHits() {
+ if (hitDistribution == null || hitDistribution.isEmpty()) return 0;
+ int max = 0;
+ for (int val : hitDistribution.values()) {
+ if (val > max) max = val;
+ }
+ return max;
+ }
+
+ /**
+ * Returns a value between 0.0 and 1.0 representing how frequently
+ * this segment was hit compared to the most-hit segment.
+ * * @param segmentKey The segment to check (e.g. "t20")
+ * @return A float weight for color interpolation.
+ */
+ public float getNormalizedWeight(final String segmentKey) {
+ final int max = getMaxHits();
+ if (max == 0) return 0.0f;
+ final Integer hits = hitDistribution.get(segmentKey);
+ return hits == null ? 0.0f : (float) hits / max;
+ }
+
+ /**
+ * Returns a string representation of the statistics for debugging.
+ *
+ * @return String containing all statistical fields and their values
+ */
+ @NonNull
@Override
public String toString() {
return "Statistics{" +
"playerId=" + playerId +
- ", dartsThrown=" + dartsThrown +
+ ", totalPoints=" + totalPoints +
+ ", totalDartsThrown=" + totalDartsThrown +
", dartsMissed=" + dartsMissed +
- ", overallPointsMade=" + overallPointsMade +
+ ", totalFirst9Points=" + totalFirst9Points +
+ ", totalFirst9Darts=" + totalFirst9Darts +
+ ", count60Plus=" + count60Plus +
+ ", count100Plus=" + count100Plus +
+ ", count140Plus=" + count140Plus +
+ ", count180=" + count180 +
", matchesPlayed=" + matchesPlayed +
- ", doubleOutsTargeted=" + doubleOutsTargeted +
+ ", successfulCheckouts=" + successfulCheckouts +
+ ", highestCheckout=" + highestCheckout +
+ ", totalDartsAtDouble=" + totalDartsAtDouble +
", doubleOutsMissed=" + doubleOutsMissed +
+ ", hitDistribution=" + (hitDistribution != null ? hitDistribution.size() + " segments" : "null") +
'}';
}
}
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
new file mode 100644
index 0000000..6bd5504
--- /dev/null
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/HeatmapView.java
@@ -0,0 +1,143 @@
+package com.aldo.apps.ochecompanion.ui;
+
+import android.animation.ArgbEvaluator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import com.aldo.apps.ochecompanion.R;
+import com.aldo.apps.ochecompanion.database.objects.Statistics;
+import java.util.HashMap;
+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.
+ */
+public class HeatmapView extends View {
+
+ private final Paint mBasePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final ArgbEvaluator mColorEvaluator = new ArgbEvaluator();
+
+ // Geometry configuration
+ private float mCenterX, mCenterY, mRadius;
+ private final Map mSegmentPaths = new HashMap<>();
+ private Statistics mStats;
+
+ // Standard Dartboard Segment Order (clockwise starting from 20 at the top)
+ 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(); }
+
+ private void init() {
+ mBasePaint.setStyle(Paint.Style.FILL);
+ }
+
+ /**
+ * Binds the player's statistics to the view and triggers a redraw.
+ */
+ public void setStats(final Statistics stats) {
+ this.mStats = stats;
+ invalidate();
+ }
+
+ @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
+
+ calculatePaths();
+ }
+
+ /**
+ * Calculates the Path for every segment on the board based on the view size.
+ */
+ private void calculatePaths() {
+ mSegmentPaths.clear();
+ final float angleStep = 360f / 20f;
+ final float startOffset = -90f - (angleStep / 2f); // Center 20 at the top
+
+ 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
+ }
+
+ // Bulls are simple circles
+ final Path sbPath = new Path();
+ sbPath.addCircle(mCenterX, mCenterY, mRadius * 0.15f, Path.Direction.CW);
+ mSegmentPaths.put("sb", sbPath);
+
+ final Path dbPath = new Path();
+ dbPath.addCircle(mCenterX, mCenterY, mRadius * 0.07f, Path.Direction.CW);
+ mSegmentPaths.put("db", dbPath);
+ }
+
+ 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(
+ mCenterX - mRadius * outerFactor, mCenterY - mRadius * outerFactor,
+ mCenterX + mRadius * outerFactor, mCenterY + mRadius * outerFactor
+ );
+ final RectF innerRect = new RectF(
+ mCenterX - mRadius * innerFactor, mCenterY - mRadius * innerFactor,
+ mCenterX + mRadius * innerFactor, mCenterY + mRadius * innerFactor
+ );
+
+ path.arcTo(outerRect, startAngle, sweep);
+ path.arcTo(innerRect, startAngle + sweep, -sweep);
+ path.close();
+ return path;
+ }
+
+ @Override
+ protected void onDraw(@NonNull final Canvas canvas) {
+ super.onDraw(canvas);
+ if (mStats == null) return;
+
+ final int coldColor = Color.parseColor("#1AFFFFFF"); // Faded ghost board
+ final int hotColor = ContextCompat.getColor(getContext(), R.color.volt_green);
+
+ // Draw every calculated path with heat-weighted colors
+ for (final Map.Entry entry : mSegmentPaths.entrySet()) {
+ final String key = entry.getKey();
+
+ // Handle the "Split Single" segments by mapping them to the same "sX" key
+ final String statsKey = key.contains("_") ? key.substring(0, key.indexOf("_")) : key;
+
+ final float weight = mStats.getNormalizedWeight(statsKey);
+ final int color = (int) mColorEvaluator.evaluate(weight, coldColor, hotColor);
+
+ mBasePaint.setColor(color);
+ canvas.drawPath(entry.getValue(), mBasePaint);
+ }
+
+ // Final aesthetic: draw a thin wireframe over the segments
+ mBasePaint.setStyle(Paint.Style.STROKE);
+ mBasePaint.setStrokeWidth(1f);
+ mBasePaint.setColor(Color.parseColor("#33FFFFFF"));
+ for (final Path p : mSegmentPaths.values()) {
+ canvas.drawPath(p, mBasePaint);
+ }
+ mBasePaint.setStyle(Paint.Style.FILL);
+ }
+}
\ 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 ef00d29..3f62e48 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
@@ -2,7 +2,9 @@ package com.aldo.apps.ochecompanion.ui;
import android.os.Bundle;
+import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.SwitchPreference;
import com.aldo.apps.ochecompanion.R;
@@ -23,5 +25,45 @@ public class MainMenuPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
setPreferencesFromResource(R.xml.main_menu_preferences, rootKey);
+ // --- 1. Day/Night Auto-Disable Logic ---
+ 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
+ manualPref.setEnabled(!autoPref.isChecked());
+
+ autoPref.setOnPreferenceChangeListener((preference, newValue) -> {
+ boolean isAutoEnabled = (Boolean) newValue;
+ manualPref.setEnabled(!isAutoEnabled);
+ return true;
+ });
+ }
+
+ // --- 2. Button Toggles for Audio and Vibration ---
+ 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) {
+ final Preference pref = findPreference(key);
+ if (pref != null) {
+ // Initialize icon based on current saved value
+ final boolean isEnabled = getPreferenceManager().getSharedPreferences().getBoolean(key, true);
+ pref.setIcon(isEnabled ? iconOn : iconOff);
+
+ pref.setOnPreferenceClickListener(p -> {
+ boolean currentState = getPreferenceManager().getSharedPreferences().getBoolean(key, true);
+ boolean newState = !currentState;
+
+ // Save the new state
+ getPreferenceManager().getSharedPreferences().edit().putBoolean(key, newState).apply();
+
+ // Update the icon visually
+ p.setIcon(newState ? iconOn : iconOff);
+ return true;
+ });
+ }
}
}
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
new file mode 100644
index 0000000..ffb248c
--- /dev/null
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/utils/converters/HitDistributionConverter.java
@@ -0,0 +1,68 @@
+package com.aldo.apps.ochecompanion.utils.converters;
+
+import androidx.room.TypeConverter;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
+
+public class HitDistributionConverter {
+
+ private static final Gson gson = new Gson();
+
+ /**
+ * Converts a JSON string from the database back into a Map for logic processing.
+ */
+ @TypeConverter
+ public static Map fromString(final String value) {
+ if (value == null || value.isEmpty()) {
+ return new HashMap<>();
+ }
+ final Type mapType = new TypeToken