diff --git a/TestCheckoutGeneration.java b/TestCheckoutGeneration.java
new file mode 100644
index 0000000..fe5750e
--- /dev/null
+++ b/TestCheckoutGeneration.java
@@ -0,0 +1,38 @@
+import com.aldo.apps.ochecompanion.utils.CheckoutConstants;
+import java.util.List;
+
+/**
+ * Quick test to verify checkout generation statistics.
+ */
+public class TestCheckoutGeneration {
+ public static void main(String[] args) {
+ System.out.println("=== Checkout Generation Statistics ===");
+ System.out.println("Total unique scores with checkouts: " + CheckoutConstants.getAvailableCheckouts().size());
+ System.out.println("Total checkout combinations: " + CheckoutConstants.getTotalRoutesCount());
+
+ System.out.println("\n=== Sample Checkouts ===");
+
+ // Test some common scores
+ int[] testScores = {2, 40, 50, 100, 120, 141, 170};
+ for (int score : testScores) {
+ String[] optimal = CheckoutConstants.getCheckoutRoute(score);
+ List all = CheckoutConstants.getAllCheckoutRoutes(score);
+
+ if (optimal != null) {
+ System.out.println("\nScore " + score + ":");
+ System.out.println(" Optimal: " + String.join(", ", optimal));
+ System.out.println(" Total routes: " + (all != null ? all.size() : 0));
+
+ // Show first 3 alternatives if available
+ if (all != null && all.size() > 1) {
+ System.out.println(" Alternatives:");
+ for (int i = 0; i < Math.min(3, all.size()); i++) {
+ if (!java.util.Arrays.equals(all.get(i), optimal)) {
+ System.out.println(" - " + String.join(", ", all.get(i)));
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 03e7243..ec791f8 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -38,10 +38,12 @@ dependencies {
implementation(libs.material)
implementation(libs.activity)
implementation(libs.constraintlayout)
+ implementation(libs.preference)
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
implementation(libs.glide)
implementation(libs.room.runtime)
annotationProcessor(libs.room.compiler)
+ implementation(libs.preferences)
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 14ef756..192a615 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -16,6 +16,10 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.OcheCompanion">
+
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 1b4cd3f..cb62c97 100644
--- a/app/src/main/java/com/aldo/apps/ochecompanion/AddPlayerActivity.java
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/AddPlayerActivity.java
@@ -17,8 +17,13 @@ import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
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.CropOverlayView;
import com.aldo.apps.ochecompanion.utils.UIConstants;
import com.google.android.material.button.MaterialButton;
@@ -110,7 +115,7 @@ public class AddPlayerActivity extends AppCompatActivity {
/**
* Database ID of the player being edited (-1 for new player).
*/
- private int mExistingPlayerId = -1;
+ private long mExistingPlayerId = -1;
/**
* Player object loaded from the database (null when creating new player).
@@ -162,6 +167,12 @@ public class AddPlayerActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_player);
Log.d(TAG, "AddPlayerActivity Created");
+ // Configure window insets to properly handle system bars (status bar, navigation bar)
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
+ Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
+ return insets;
+ });
// Initialize all UI components and their click listeners
initViews();
@@ -171,7 +182,7 @@ public class AddPlayerActivity extends AppCompatActivity {
// Check if editing an existing player
if (getIntent().hasExtra(EXTRA_PLAYER_ID)) {
- mExistingPlayerId = getIntent().getIntExtra(EXTRA_PLAYER_ID, -1);
+ mExistingPlayerId = getIntent().getLongExtra(EXTRA_PLAYER_ID, -1);
loadExistingPlayer();
}
}
@@ -459,8 +470,14 @@ public class AddPlayerActivity extends AppCompatActivity {
AppDatabase.getDatabase(this).playerDao().update(mExistingPlayer);
} else {
// Create and insert new player
- Player p = new Player(name, mInternalImagePath);
- AppDatabase.getDatabase(this).playerDao().insert(p);
+ final Player p = new Player(name, mInternalImagePath);
+ final long newUserId = AppDatabase.getDatabase(this).playerDao().insert(p);
+ final Player dbPlayer = AppDatabase.getDatabase(this).playerDao().getPlayerById(newUserId);
+ if (dbPlayer != null) {
+ Log.d(TAG, "savePlayer: Player has been created, create stats as well.");
+ final Statistics playerStats = new Statistics(dbPlayer.id);
+ AppDatabase.getDatabase(this).statisticsDao().insertStatistics(playerStats);
+ }
}
// Close activity on main thread after save completes
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 3edef79..61c6c1c 100644
--- a/app/src/main/java/com/aldo/apps/ochecompanion/GameActivity.java
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/GameActivity.java
@@ -5,6 +5,7 @@ import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.os.Bundle;
+import android.util.Log;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
@@ -14,8 +15,16 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
+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.Match;
import com.aldo.apps.ochecompanion.database.objects.Player;
+import com.aldo.apps.ochecompanion.database.objects.Statistics;
import com.aldo.apps.ochecompanion.utils.CheckoutConstants;
+import com.aldo.apps.ochecompanion.utils.CheckoutEngine;
import com.aldo.apps.ochecompanion.utils.DartsConstants;
import com.aldo.apps.ochecompanion.utils.UIConstants;
import com.google.android.material.button.MaterialButton;
@@ -30,6 +39,8 @@ import java.util.UUID;
*/
public class GameActivity extends AppCompatActivity {
+ private static final String TAG = "GameActivity";
+
/**
* Intent extra key for player list. Type: ArrayList
*/
@@ -175,21 +186,33 @@ public class GameActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_game);
+ // Configure window insets to properly handle system bars (status bar, navigation bar)
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
+ Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
+ return insets;
+ });
+
// Extract game parameters from intent
mStartingScore = getIntent().getIntExtra(EXTRA_START_SCORE, DartsConstants.DEFAULT_GAME_SCORE);
mMatchUuid = getIntent().getStringExtra(EXTRA_MATCH_UUID);
- ArrayList participants = getIntent().getParcelableArrayListExtra(EXTRA_PLAYERS);
-
// Initialize activity components in order
initViews();
setupKeyboard();
- setupGame(participants);
+ new Thread(() -> {
+ final List allAvailablePlayers = AppDatabase.getDatabase(GameActivity.this).playerDao().getAllPlayers();
+ Log.d(TAG, "onCreate: allAvailablePlayers = [" + allAvailablePlayers + "]");
+ runOnUiThread(() -> {
+ setupGame(allAvailablePlayers);
+ });
+ }).start();
}
/**
* Initializes UI component references and sets up click listeners.
*/
private void initViews() {
+ Log.d(TAG, "initViews() called");
tvScorePrimary = findViewById(R.id.tvScorePrimary);
tvPlayerName = findViewById(R.id.tvPlayerName);
tvLegAvg = findViewById(R.id.tvLegAvg);
@@ -218,6 +241,7 @@ public class GameActivity extends AppCompatActivity {
* Dynamically creates and configures numeric keyboard buttons (1-20).
*/
private void setupKeyboard() {
+ Log.d(TAG, "setupKeyboard() called");
glKeyboard.removeAllViews();
mKeyboardButtons.clear();
@@ -243,13 +267,15 @@ public class GameActivity extends AppCompatActivity {
* @param players List of Player objects (can be null/empty)
*/
private void setupGame(final List players) {
+ Log.d(TAG, "setupGame() called with: players = [" + players + "]");
mPlayerStates = new ArrayList<>();
if (players != null && !players.isEmpty()) {
for (Player p : players) {
- mPlayerStates.add(new X01State(p.username, mStartingScore));
+ mPlayerStates.add(new X01State(p, mStartingScore));
}
} else {
- mPlayerStates.add(new X01State("GUEST 1", mStartingScore));
+ final Player guest = new Player("GUEST", null);
+ mPlayerStates.add(new X01State(guest, mStartingScore));
}
updateUI();
setMultiplier(DartsConstants.MULTIPLIER_SINGLE);
@@ -289,7 +315,7 @@ public class GameActivity extends AppCompatActivity {
mCurrentTurnDarts.add(points);
updateTurnIndicators();
mIsTurnOver = true;
- handleWin(active);
+ handleWin(active, mCurrentTurnDarts.size(), scoreBeforeDart);
} else {
// VALID THROW
mCurrentTurnDarts.add(points);
@@ -350,6 +376,31 @@ public class GameActivity extends AppCompatActivity {
}
}
+ private void updatePlayerStats(GameActivity.X01State active, int dartsThrown, int pointsMade, boolean wasBust) {
+ 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);
+ }
+ Log.d(TAG, "submitTurn: statistics = [" + playerStats + "]");
+ AppDatabase.getDatabase(GameActivity.this).statisticsDao().updateStatistics(playerStats);
+
+ // Calculate career average: total points / total darts thrown
+ final long totalDarts = playerStats.getDartsThrown();
+ if (totalDarts > 0) {
+ player.careerAverage = (double) playerStats.getOverallPointsMade() / totalDarts;
+ } else {
+ player.careerAverage = 0.0;
+ }
+ AppDatabase.getDatabase(GameActivity.this).playerDao().update(player);
+ }
+ }).start();
+ }
+
/**
* Finalizes current turn and advances to next player.
* Updates player score (unless bust), rotates to next player, resets turn state.
@@ -377,7 +428,7 @@ public class GameActivity extends AppCompatActivity {
active.remainingScore = finalScore;
active.dartsThrown += mCurrentTurnDarts.size();
}
- // If bust, score remains unchanged
+ updatePlayerStats(active, mCurrentTurnDarts.size(), turnTotal, isBust);
// Rotate to next player
mActivePlayerIndex = (mActivePlayerIndex + 1) % mPlayerStates.size();
@@ -468,7 +519,7 @@ public class GameActivity extends AppCompatActivity {
*/
private void updateCheckoutSuggestion(final int score, final int dartsLeft) {
if (score <= DartsConstants.MAX_CHECKOUT_SCORE && score > DartsConstants.BUST_SCORE && dartsLeft > 0) {
- String route = CheckoutEngine.getRoute(score, dartsLeft);
+ String route = CheckoutEngine.calculateCheckout(score, dartsLeft);
if (route != null) {
layoutCheckoutSuggestion.setVisibility(View.VISIBLE);
@@ -524,7 +575,8 @@ public class GameActivity extends AppCompatActivity {
*
* @param winner X01State of the winning player
*/
- private void handleWin(final X01State winner) {
+ private void handleWin(final X01State winner, final int dartsThrown, final int pointsMade) {
+ updatePlayerStats(winner, dartsThrown, pointsMade, false);
// Show win notification
Toast.makeText(this, winner.name + " WINS!", Toast.LENGTH_LONG).show();
@@ -543,10 +595,16 @@ public class GameActivity extends AppCompatActivity {
* Tracks name, remaining score, and darts thrown.
*/
private static class X01State {
+
/**
- * Player's display name.
+ * The {@link Player} object to update stats as well.
*/
- String name;
+ final Player player;
+
+ /**
+ * Player's display name for convenience purpose, as extracted from the player object.
+ */
+ final String name;
/**
* Player's current remaining score.
@@ -561,103 +619,13 @@ public class GameActivity extends AppCompatActivity {
/**
* Constructs X01State for a player.
*
- * @param name Player's display name
+ * @param player The actual {@link Player} instance.
* @param startScore Starting score for the game
*/
- X01State(final String name, final int startScore) {
- this.name = name;
+ X01State(final Player player, final int startScore) {
+ this.player = player;
+ this.name = player.username;
this.remainingScore = startScore;
}
}
-
- /**
- * Static helper class that provides optimal checkout route suggestions for X01 games.
- *
- * The CheckoutEngine calculates the best way to finish a game given a target score
- * and number of darts remaining. It combines pre-calculated routes for classic
- * finishes with intelligent logic for direct doubles and setup darts.
- *
- *
- * Checkout Logic Priority:
- *
- * - Direct Doubles: For scores ≤40 and even, suggest immediate double
- * - Bullseye: For score of 50, suggest BULL (double bull)
- * - Setup Darts: For odd scores with multiple darts, suggest setup + double
- * - Pre-calculated Routes: For common finishes like 170, 141
- * - Fallback: Generic high-score route (T20 Route)
- *
- *
- *
- * Key Rule: Never suggests a route that would leave a score of 1, as
- * this is impossible to finish (no double equals 1).
- *
- *
- * Example Suggestions:
- *
- * getRoute(32, 1) → "D16" (Direct double)
- * getRoute(50, 1) → "BULL" (Bullseye)
- * getRoute(40, 2) → "D20" (Direct double)
- * getRoute(41, 2) → "1 • D20" (Setup dart to leave 40)
- * getRoute(170, 3) → "T20 • T20 • BULL" (Pre-calculated route)
- * getRoute(100, 2) → "T20 Route" (General high-score advice)
- *
- *
- *
- * Design Pattern:
- * This is a pure static utility class with no instance state. All methods are
- * static and thread-safe. The checkout map is initialized once in a static block.
- *
- *
- * @see #getRoute(int, int)
- */
- private static class CheckoutEngine {
-
- /**
- * Returns optimal checkout route for given score and darts remaining.
- * Considers direct doubles, setup darts, and pre-calculated routes.
- *
- * @param score Target score to finish
- * @param dartsLeft Number of darts remaining (1-3)
- * @return Checkout route string or null if unavailable
- */
- public static String getRoute(final int score, final int dartsLeft) {
- // 1. Direct Out check (highest priority)
- if (score <= DartsConstants.MAX_DIRECT_DOUBLE && score % 2 == 0) {
- return DartsConstants.PREFIX_DOUBLE + (score / 2);
- }
- if (score == DartsConstants.DOUBLE_BULL_VALUE) return DartsConstants.LABEL_BULLSEYE;
-
- // 2. Logic for Setup Darts (preventing score of 1)
- if (dartsLeft >= 2) {
- // Example: Score is 7. Suggesting D3 leaves 1 (Bust).
- // Suggesting 1 leaves 6 (D3). Correct.
- if (score <= 41 && score % 2 != 0) {
- // Try to leave a common double (32, 40, 16)
- if (score - DartsConstants.SETUP_DOUBLE_32 > 0 && score - DartsConstants.SETUP_DOUBLE_32 <= DartsConstants.MAX_DARTBOARD_NUMBER) {
- return (score - DartsConstants.SETUP_DOUBLE_32) + DartsConstants.CHECKOUT_SEPARATOR + DartsConstants.PREFIX_DOUBLE + "16";
- }
- if (score - DartsConstants.SETUP_DOUBLE_40 > 0 && score - DartsConstants.SETUP_DOUBLE_40 <= DartsConstants.MAX_DARTBOARD_NUMBER) {
- return (score - DartsConstants.SETUP_DOUBLE_40) + DartsConstants.CHECKOUT_SEPARATOR + DartsConstants.PREFIX_DOUBLE + "20";
- }
- return "1" + DartsConstants.CHECKOUT_SEPARATOR + DartsConstants.PREFIX_DOUBLE + ((score - 1) / 2); // Default setup
- }
- }
-
- // 3. Fallback to Map or High Scoring Route
- if (CheckoutConstants.hasCheckoutRoute(score)) {
- String[] parts = CheckoutConstants.getCheckoutRoute(score);
- if (parts != null && parts.length <= dartsLeft) {
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < parts.length; i++) {
- sb.append(parts[i]);
- if (i < parts.length - 1) sb.append(DartsConstants.CHECKOUT_SEPARATOR);
- }
- return sb.toString();
- }
- }
-
- if (score > DartsConstants.HIGH_SCORE_THRESHOLD) return DartsConstants.LABEL_T20_ROUTE;
- return null;
- }
- }
}
\ No newline at end of file
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 c13a14e..f9421b6 100644
--- a/app/src/main/java/com/aldo/apps/ochecompanion/MainMenuActivity.java
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/MainMenuActivity.java
@@ -1,7 +1,9 @@
package com.aldo.apps.ochecompanion;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.os.Bundle;
+import android.util.Log;
import android.view.View;
import android.widget.TextView;
@@ -10,6 +12,7 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
+import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -47,6 +50,11 @@ public class MainMenuActivity extends AppCompatActivity {
*/
private int mTestCounter = 0;
+ /**
+ * The {@link SharedPreferences} containing the currently selected settings.
+ */
+ private SharedPreferences mSettingsPref;
+
/**
* Initializes the activity: enables edge-to-edge display, configures window insets,
* and sets up the match recap view with test data click listener.
@@ -60,6 +68,7 @@ public class MainMenuActivity extends AppCompatActivity {
// Enable edge-to-edge display for immersive UI experience
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
+ mSettingsPref = PreferenceManager.getDefaultSharedPreferences(this);
// Configure window insets to properly handle system bars (status bar, navigation bar)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
@@ -69,16 +78,14 @@ public class MainMenuActivity extends AppCompatActivity {
});
findViewById(R.id.quick_start_btn).setOnClickListener(v -> quickStart());
+ findViewById(R.id.btnSettings).setOnClickListener(v -> launchSettings());
// Set up match recap view with test data functionality
mMatchRecap = findViewById(R.id.match_recap);
- mMatchRecap.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(final View v) {
- // Cycle through test data scenarios on each click
- applyTestData(mTestCounter);
- mTestCounter++;
- }
+ mMatchRecap.setOnClickListener(v -> {
+ // Cycle through test data scenarios on each click
+ applyTestData(mTestCounter);
+ mTestCounter++;
});
}
@@ -90,6 +97,16 @@ public class MainMenuActivity extends AppCompatActivity {
super.onResume();
// Refresh the squad view with latest player data
initSquadView();
+ // Apply the last finished match if available.
+ applyLastMatch();
+ }
+
+ /**
+ * Launches the settings activity when the settings button is clicked.
+ */
+ private void launchSettings() {
+ final Intent intent = new Intent(MainMenuActivity.this, SettingsActivity.class);
+ startActivity(intent);
}
/**
@@ -144,6 +161,20 @@ public class MainMenuActivity extends AppCompatActivity {
}).start();
}
+ /**
+ * Applies the last completed match from the database to the match recap view.
+ */
+ private void applyLastMatch() {
+ // Database operations must be run on a background thread to keep the UI responsive.
+ new Thread(() -> {
+ final Match lastMatch = AppDatabase.getDatabase(getApplicationContext())
+ .matchDao()
+ .getLastCompletedMatch();
+
+ // Post-database query UI updates must happen back on the main (UI) thread
+ runOnUiThread(() -> mMatchRecap.setMatch(lastMatch));
+ }).start();
+ }
/**
* Applies test data to the match recap view for development and testing.
@@ -153,6 +184,7 @@ public class MainMenuActivity extends AppCompatActivity {
* @param counter Counter value used to determine which test scenario to display.
*/
private void applyTestData(final int counter) {
+ Log.d(TAG, "applyTestData: Applying Test Data [" + counter + "]");
// Create test player objects
final Player playerOne = new Player(DartsConstants.TEST_PLAYER_1, null);
playerOne.id = 1;
@@ -176,16 +208,22 @@ public class MainMenuActivity extends AppCompatActivity {
// Create test match objects with different player configurations and scores
final Match match1on1 = new Match("501", java.util.Arrays.asList(playerOne, playerTwo), scores1v1);
+ match1on1.markCompleted(); // Mark as completed for test data
+
final Match matchGroup = new Match("501", java.util.Arrays.asList(playerOne, playerTwo, playerThree, playerFour), scoresGroup);
+ matchGroup.markCompleted(); // Mark as completed for test data
// Cycle through different test scenarios based on counter value
if (counter % UIConstants.TEST_CYCLE_MODULO == 0) {
+ Log.d(TAG, "applyTestData: No recent match selected.");
// Scenario 1: No match (null state)
mMatchRecap.setMatch(null);
} else if (counter % UIConstants.TEST_CYCLE_MODULO == 1) {
+ Log.d(TAG, "applyTestData: 1 on 1 Match");
// Scenario 2: 1v1 match (two players)
mMatchRecap.setMatch(match1on1);
} else {
+ Log.d(TAG, "applyTestData: Group Match.");
// Scenario 3: Group match (four players)
mMatchRecap.setMatch(matchGroup);
}
diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/SettingsActivity.java b/app/src/main/java/com/aldo/apps/ochecompanion/SettingsActivity.java
new file mode 100644
index 0000000..a9eb815
--- /dev/null
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/SettingsActivity.java
@@ -0,0 +1,47 @@
+package com.aldo.apps.ochecompanion;
+
+import android.os.Bundle;
+
+import androidx.appcompat.app.ActionBar;
+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.ui.MainMenuPreferencesFragment;
+
+/**
+ * Settings activity for configuring app-wide preferences.
+ * Hosts the MainMenuPreferencesFragment which displays available settings
+ * including day/night mode and standard game mode selection.
+ */
+public class SettingsActivity extends AppCompatActivity {
+
+ /**
+ * Initializes the settings activity and loads the preferences fragment.
+ * Configures window insets and enables the back button in the action bar.
+ *
+ * @param savedInstanceState Bundle containing saved state, or null if none exists
+ */
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.settings_activity);
+ // Configure window insets to properly handle system bars (status bar, navigation bar)
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
+ Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
+ return insets;
+ });
+ if (savedInstanceState == null) {
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.settings, new MainMenuPreferencesFragment())
+ .commit();
+ }
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+}
\ 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 748c968..158c128 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
@@ -7,8 +7,10 @@ import androidx.room.RoomDatabase;
import com.aldo.apps.ochecompanion.database.dao.MatchDao;
import com.aldo.apps.ochecompanion.database.dao.PlayerDao;
+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;
/**
* Main Room database class for the Oche Companion darts application.
@@ -21,7 +23,7 @@ import com.aldo.apps.ochecompanion.database.objects.Player;
* @see Player
* @see Match
*/
-@Database(entities = {Player.class, Match.class}, version = 3, exportSchema = false)
+@Database(entities = {Player.class, Match.class, Statistics.class}, version = 10, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
/**
@@ -42,6 +44,15 @@ public abstract class AppDatabase extends RoomDatabase {
*/
public abstract MatchDao matchDao();
+ /**
+ * Returns the StatisticsDao for managing statistics records for players.
+ * Thread-safe and can be reused. Operations must run on background threads.
+ *
+ * @return StatisticsDao instance for player stats database operations
+ * @see StatisticsDao
+ */
+ public abstract StatisticsDao statisticsDao();
+
/**
* Singleton instance of the AppDatabase.
* Volatile ensures thread-safe visibility in double-checked locking pattern.
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 64c7de4..fcce018 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
@@ -39,4 +39,41 @@ public interface MatchDao {
*/
@Query("SELECT * FROM matches ORDER BY timestamp DESC LIMIT 1")
Match getLastMatch();
+
+ /**
+ * Retrieves the most recently completed match.
+ * Must be called on a background thread.
+ *
+ * @return The most recent completed match, or null if no completed matches exist
+ */
+ @Query("SELECT * FROM matches WHERE state = 'COMPLETED' ORDER BY timestamp DESC LIMIT 1")
+ Match getLastCompletedMatch();
+
+ /**
+ * Retrieves all completed matches ordered by most recent first.
+ * Must be called on a background thread.
+ *
+ * @return List of completed matches sorted by timestamp descending
+ */
+ @Query("SELECT * FROM matches WHERE state = 'COMPLETED' ORDER BY timestamp DESC")
+ List getCompletedMatches();
+
+ /**
+ * Retrieves all ongoing matches.
+ * Must be called on a background thread.
+ *
+ * @return List of ongoing matches
+ */
+ @Query("SELECT * FROM matches WHERE state = 'ONGOING' ORDER BY timestamp DESC")
+ List getOngoingMatches();
+
+ /**
+ * Retrieves matches by state.
+ * Must be called on a background thread.
+ *
+ * @param state The match state to filter by ("ONGOING", "COMPLETED", or "CANCELED")
+ * @return List of matches with the specified state
+ */
+ @Query("SELECT * FROM matches WHERE state = :state ORDER BY timestamp DESC")
+ List getMatchesByState(final String state);
}
diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/PlayerDao.java b/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/PlayerDao.java
index 1e89e29..e0827ca 100644
--- a/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/PlayerDao.java
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/PlayerDao.java
@@ -1,8 +1,11 @@
package com.aldo.apps.ochecompanion.database.dao;
+import static androidx.room.OnConflictStrategy.REPLACE;
+
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Update;
@@ -22,8 +25,8 @@ public interface PlayerDao {
*
* @param player The player to insert
*/
- @Insert
- void insert(final Player player);
+ @Insert(onConflict = REPLACE)
+ long insert(final Player player);
/**
* Updates an existing player in the database.
@@ -53,7 +56,7 @@ public interface PlayerDao {
* @return The player, or null if not found
*/
@Query("SELECT * FROM players WHERE id = :id LIMIT 1")
- Player getPlayerById(final int id);
+ Player getPlayerById(final long id);
/**
* Retrieves all players ordered alphabetically by username.
diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/StatisticsDao.java b/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/StatisticsDao.java
new file mode 100644
index 0000000..7bb7154
--- /dev/null
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/StatisticsDao.java
@@ -0,0 +1,56 @@
+package com.aldo.apps.ochecompanion.database.dao;
+
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.Query;
+import androidx.room.Update;
+
+import com.aldo.apps.ochecompanion.database.objects.Statistics;
+
+/**
+ * Data Access Object for Statistics entities in the Room database.
+ * Provides CRUD operations for managing player performance statistics.
+ * All operations must be executed on background threads.
+ */
+@Dao
+public interface StatisticsDao {
+
+ /**
+ * Inserts a new statistics record into the database.
+ * Must be called on a background thread.
+ *
+ * @param statistics The statistics entity to persist
+ */
+ @Insert
+ void insertStatistics(final Statistics statistics);
+
+ /**
+ * Retrieves statistics for a specific player by their ID.
+ * Must be called on a background thread.
+ *
+ * @param playerId The unique player ID
+ * @return Statistics record for the player, or null if not found
+ */
+ @Query("SELECT * FROM statistics WHERE playerId = :playerId")
+ Statistics getStatisticsForPlayer(final long playerId);
+
+ /**
+ * Updates an existing statistics record in the database.
+ * Statistics is identified by its primary key (playerId).
+ * Must be called on a background thread.
+ *
+ * @param statistics The statistics entity with updated data
+ */
+ @Update
+ void updateStatistics(final Statistics statistics);
+
+ /**
+ * Deletes a statistics record from the database.
+ * Must be called on a background thread.
+ *
+ * @param statistics The statistics entity to delete
+ */
+ @Delete
+ void deleteStatistics(final Statistics statistics);
+}
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 5030ea4..6b5217f 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
@@ -13,11 +13,11 @@ import java.util.List;
import java.util.Map;
/**
- * Represents a completed darts match in the Oche Companion application.
+ * Represents a darts match in the Oche Companion application.
* Room entity storing match information including game mode, timestamp, player count,
- * 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.
+ * 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
* @see com.aldo.apps.ochecompanion.database.objects.Player
@@ -27,6 +27,20 @@ import java.util.Map;
@Entity(tableName = "matches")
public class Match implements Serializable {
+ /**
+ * Represents the current state of a match.
+ */
+ 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.
@@ -67,27 +81,35 @@ public class Match implements Serializable {
*/
public String participantData;
+ /**
+ * Current state of the match (ONGOING, COMPLETED, or CANCELED).
+ * New matches start as ONGOING and transition to COMPLETED or CANCELED.
+ */
+ public MatchState state;
+
/**
* 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 completed
+ * @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)
* @see com.aldo.apps.ochecompanion.database.dao.MatchDao#insert(Match)
*/
- public Match(final long timestamp, final String gameMode, final int playerCount, final String participantData) {
+ 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;
this.participantData = participantData;
+ this.state = state;
}
/**
* Convenience constructor for creating a Match from a list of Player objects.
* Automatically generates JSON participant data and sets timestamp to current time.
- * All players will have a score of 0.
+ * 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
@@ -98,11 +120,13 @@ public class Match implements Serializable {
this.gameMode = gameMode;
this.playerCount = players.size();
this.participantData = generateParticipantJson(players, null);
+ this.state = MatchState.ONGOING;
}
/**
* Convenience constructor for creating a Match from players with their scores.
* 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
@@ -114,6 +138,7 @@ public class Match implements Serializable {
this.gameMode = gameMode;
this.playerCount = players.size();
this.participantData = generateParticipantJson(players, scores);
+ this.state = MatchState.ONGOING;
}
/**
@@ -293,4 +318,46 @@ public class Match implements Serializable {
}
return participants;
}
+
+ /**
+ * Marks this match as completed and updates the timestamp.
+ */
+ public void markCompleted() {
+ this.state = MatchState.COMPLETED;
+ this.timestamp = System.currentTimeMillis();
+ }
+
+ /**
+ * Marks this match as canceled.
+ */
+ public void markCanceled() {
+ this.state = MatchState.CANCELED;
+ }
+
+ /**
+ * Checks if the match is currently ongoing.
+ *
+ * @return true if the match is in ONGOING state
+ */
+ public boolean isOngoing() {
+ return this.state == MatchState.ONGOING;
+ }
+
+ /**
+ * Checks if the match is completed.
+ *
+ * @return true if the match is in COMPLETED state
+ */
+ public boolean isCompleted() {
+ return this.state == MatchState.COMPLETED;
+ }
+
+ /**
+ * Checks if the match was canceled.
+ *
+ * @return true if the match is in CANCELED state
+ */
+ public boolean isCanceled() {
+ return this.state == MatchState.CANCELED;
+ }
}
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 7fbcbdd..75adbe4 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
@@ -19,7 +19,7 @@ public class Player implements Parcelable {
* Room auto-populates on insert.
*/
@PrimaryKey(autoGenerate = true)
- public int id;
+ public long id;
/**
* Player's display name shown throughout the app. Should be non-null and
@@ -66,7 +66,7 @@ public class Player implements Parcelable {
* @param in Parcel containing serialized Player data
*/
protected Player(final Parcel in) {
- id = in.readInt();
+ id = in.readLong();
username = in.readString();
profilePictureUri = in.readString();
careerAverage = in.readDouble();
@@ -101,7 +101,7 @@ public class Player implements Parcelable {
*/
@Override
public void writeToParcel(final Parcel dest, final int flags) {
- dest.writeInt(id);
+ dest.writeLong(id);
dest.writeString(username);
dest.writeString(profilePictureUri);
dest.writeDouble(careerAverage);
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
new file mode 100644
index 0000000..f270549
--- /dev/null
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/database/objects/Statistics.java
@@ -0,0 +1,186 @@
+package com.aldo.apps.ochecompanion.database.objects;
+
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.PrimaryKey;
+
+/**
+ * Room database entity representing player performance statistics.
+ * Tracks comprehensive career metrics including darts thrown, accuracy,
+ * matches played, and double-out success rates. Each player has exactly
+ * one Statistics record, identified by the player's ID.
+ *
+ * @see com.aldo.apps.ochecompanion.database.dao.StatisticsDao
+ * @see Player
+ */
+@Entity(tableName = "statistics")
+public class Statistics {
+
+ /**
+ * Player ID this statistics record belongs to (primary key).
+ * References the Player entity's ID field.
+ */
+ @PrimaryKey
+ private long playerId;
+
+ /**
+ * Total number of darts thrown across all matches.
+ * Used to calculate career average and accuracy metrics.
+ */
+ private long dartsThrown;
+
+ /**
+ * Total number of darts that missed the intended target.
+ * Contributes to accuracy calculations and player skill assessment.
+ */
+ private long dartsMissed;
+
+ /**
+ * Cumulative points scored across all darts thrown.
+ * Used with dartsThrown to calculate career average.
+ */
+ private long overallPointsMade;
+
+ /**
+ * Total number of completed matches for this player.
+ * Provides context for statistical significance.
+ */
+ private int matchesPlayed;
+
+ /**
+ * Total number of double-out attempts (both successful and missed).
+ * Tracks how often player attempts to finish on doubles.
+ */
+ private int doubleOutsTargeted;
+
+ /**
+ * Number of failed double-out attempts.
+ * Used to calculate double-out success rate.
+ */
+ private int doubleOutsMissed;
+
+ /**
+ * Constructs a new Statistics record for a player.
+ * Initializes all counters to zero.
+ *
+ * @param playerId The unique player ID this statistics belongs to
+ */
+ public Statistics(final long playerId) {
+ this.playerId = playerId;
+ dartsThrown = 0;
+ overallPointsMade = 0;
+ matchesPlayed = 0;
+ doubleOutsTargeted = 0;
+ doubleOutsMissed = 0;
+ }
+
+ /**
+ * Records darts thrown in a turn with their point value.
+ *
+ * @param dartsThrown Number of darts thrown in the turn
+ * @param pointsMade Total points scored with those darts
+ */
+ public void saveDartsThrown(final long dartsThrown, final long pointsMade) {
+ this.dartsThrown += dartsThrown;
+ this.overallPointsMade += pointsMade;
+ }
+
+ /**
+ * Adds missed darts to the statistics.
+ * Used when a player doesn't throw all 3 darts in a turn (e.g., checkout or bust).
+ *
+ * @param dartsMissed Number of darts not thrown
+ */
+ public void addMissedDarts(final long dartsMissed) {
+ this.dartsMissed += dartsMissed;
+ }
+
+ /**
+ * Increments the completed matches counter.
+ * Should be called when a match finishes.
+ */
+ public void addCompletedMatch() {
+ matchesPlayed++;
+ }
+
+ /**
+ * Records a double-out attempt.
+ *
+ * @param isMissed true if the double-out was missed, false if successful
+ */
+ public void addDoubleOutTarget(final boolean isMissed) {
+ doubleOutsTargeted++;
+ if (isMissed) {
+ doubleOutsMissed++;
+ }
+ }
+
+ public long getPlayerId() {
+ return playerId;
+ }
+
+ public void setPlayerId(long playerId) {
+ this.playerId = playerId;
+ }
+
+ public long getDartsThrown() {
+ return dartsThrown;
+ }
+
+ public void setDartsThrown(long dartsThrown) {
+ this.dartsThrown = dartsThrown;
+ }
+
+ public long getDartsMissed() {
+ return dartsMissed;
+ }
+
+ public void setDartsMissed(long dartsMissed) {
+ this.dartsMissed = dartsMissed;
+ }
+
+ public long getOverallPointsMade() {
+ return overallPointsMade;
+ }
+
+ public void setOverallPointsMade(long overallPointsMade) {
+ this.overallPointsMade = overallPointsMade;
+ }
+
+ public int getMatchesPlayed() {
+ return matchesPlayed;
+ }
+
+ public void setMatchesPlayed(int matchesPlayed) {
+ this.matchesPlayed = matchesPlayed;
+ }
+
+ public int getDoubleOutsTargeted() {
+ return doubleOutsTargeted;
+ }
+
+ public void setDoubleOutsTargeted(int doubleOutsTargeted) {
+ this.doubleOutsTargeted = doubleOutsTargeted;
+ }
+
+ public int getDoubleOutsMissed() {
+ return doubleOutsMissed;
+ }
+
+ public void setDoubleOutsMissed(int doubleOutsMissed) {
+ this.doubleOutsMissed = doubleOutsMissed;
+ }
+
+ @Override
+ public String toString() {
+ return "Statistics{" +
+ "playerId=" + playerId +
+ ", dartsThrown=" + dartsThrown +
+ ", dartsMissed=" + dartsMissed +
+ ", overallPointsMade=" + overallPointsMade +
+ ", matchesPlayed=" + matchesPlayed +
+ ", doubleOutsTargeted=" + doubleOutsTargeted +
+ ", doubleOutsMissed=" + doubleOutsMissed +
+ '}';
+ }
+}
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
new file mode 100644
index 0000000..ef00d29
--- /dev/null
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/MainMenuPreferencesFragment.java
@@ -0,0 +1,27 @@
+package com.aldo.apps.ochecompanion.ui;
+
+import android.os.Bundle;
+
+import androidx.preference.PreferenceFragmentCompat;
+
+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.
+ * Preferences are automatically persisted to SharedPreferences by the AndroidX Preference library.
+ */
+public class MainMenuPreferencesFragment extends PreferenceFragmentCompat {
+
+ /**
+ * Initializes the preference screen from the main_menu_preferences XML resource.
+ * Called automatically by the fragment lifecycle.
+ *
+ * @param savedInstanceState Bundle containing saved state, or null if none exists
+ * @param rootKey Optional preference hierarchy root key
+ */
+ @Override
+ public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
+ setPreferencesFromResource(R.xml.main_menu_preferences, rootKey);
+ }
+}
diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/utils/CheckoutConstants.java b/app/src/main/java/com/aldo/apps/ochecompanion/utils/CheckoutConstants.java
index 46e2754..7248691 100644
--- a/app/src/main/java/com/aldo/apps/ochecompanion/utils/CheckoutConstants.java
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/utils/CheckoutConstants.java
@@ -1,80 +1,191 @@
package com.aldo.apps.ochecompanion.utils;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
/**
- * Pre-calculated checkout routes for classic darts finishes.
- * Contains standard 3-dart checkout combinations for common scores.
+ * Pre-calculated checkout routes for all possible double-out combinations.
+ * Generates all valid 1-dart, 2-dart, and 3-dart checkouts during class initialization.
+ * Stores multiple routes per score for flexibility, with optimal route selection.
*/
public final class CheckoutConstants {
+ private static final int SINGLE = 1;
+ private static final int DOUBLE = 2;
+ private static final int TRIPLE = 3;
+
// Prevent instantiation
private CheckoutConstants() {
throw new UnsupportedOperationException("Constants class cannot be instantiated");
}
/**
- * Map of pre-calculated checkout routes.
- * Key: Target score, Value: Array of dart notations to achieve the checkout
+ * Map of all possible checkout routes.
+ * Key: Target score, Value: List of all possible routes (each route is an array of dart notations)
*/
- private static final Map CHECKOUT_MAP = new HashMap<>();
+ private static final Map> ALL_CHECKOUTS = new HashMap<>();
+
+ /**
+ * Map of optimal checkout routes (best route per score).
+ * Key: Target score, Value: Optimal dart notation array
+ */
+ private static final Map OPTIMAL_CHECKOUTS = new HashMap<>();
static {
- // Maximum 3-dart checkouts
- CHECKOUT_MAP.put(170, new String[]{"T20", "T20", "BULL"});
- CHECKOUT_MAP.put(167, new String[]{"T20", "T19", "BULL"});
- CHECKOUT_MAP.put(164, new String[]{"T20", "T18", "BULL"});
- CHECKOUT_MAP.put(161, new String[]{"T20", "T17", "BULL"});
- CHECKOUT_MAP.put(160, new String[]{"T20", "T20", "D20"});
-
- // Common high finishes
- CHECKOUT_MAP.put(141, new String[]{"T20", "T19", "D12"});
- CHECKOUT_MAP.put(140, new String[]{"T20", "T20", "D10"});
- CHECKOUT_MAP.put(139, new String[]{"T20", "T19", "D11"});
- CHECKOUT_MAP.put(138, new String[]{"T20", "T18", "D12"});
- CHECKOUT_MAP.put(137, new String[]{"T20", "T19", "D10"});
- CHECKOUT_MAP.put(136, new String[]{"T20", "T20", "D8"});
- CHECKOUT_MAP.put(135, new String[]{"T20", "T17", "D12"});
- CHECKOUT_MAP.put(134, new String[]{"T20", "T14", "D16"});
- CHECKOUT_MAP.put(133, new String[]{"T20", "T19", "D8"});
- CHECKOUT_MAP.put(132, new String[]{"T20", "T16", "D12"});
- CHECKOUT_MAP.put(131, new String[]{"T20", "T13", "D16"});
- CHECKOUT_MAP.put(130, new String[]{"T20", "T20", "D5"});
-
- // Mid-range finishes
- CHECKOUT_MAP.put(121, new String[]{"T17", "T18", "D10"});
- CHECKOUT_MAP.put(120, new String[]{"T20", "20", "D20"});
- CHECKOUT_MAP.put(119, new String[]{"T19", "T12", "D13"});
- CHECKOUT_MAP.put(118, new String[]{"T18", "T14", "D14"});
- CHECKOUT_MAP.put(117, new String[]{"T20", "17", "D20"});
- CHECKOUT_MAP.put(116, new String[]{"T19", "19", "D20"});
- CHECKOUT_MAP.put(115, new String[]{"T19", "18", "D20"});
- CHECKOUT_MAP.put(114, new String[]{"T18", "18", "D20"});
- CHECKOUT_MAP.put(113, new String[]{"T19", "16", "D20"});
- CHECKOUT_MAP.put(112, new String[]{"T20", "12", "D20"});
- CHECKOUT_MAP.put(111, new String[]{"T20", "11", "D20"});
- CHECKOUT_MAP.put(110, new String[]{"T20", "10", "D20"});
-
- // Lower finishes
- CHECKOUT_MAP.put(107, new String[]{"T19", "10", "D20"});
- CHECKOUT_MAP.put(106, new String[]{"T20", "10", "D18"});
- CHECKOUT_MAP.put(105, new String[]{"T20", "13", "D16"});
- CHECKOUT_MAP.put(104, new String[]{"T18", "18", "D16"});
- CHECKOUT_MAP.put(103, new String[]{"T17", "12", "D20"});
- CHECKOUT_MAP.put(102, new String[]{"T20", "10", "D16"});
- CHECKOUT_MAP.put(101, new String[]{"T17", "10", "D20"});
- CHECKOUT_MAP.put(100, new String[]{"T20", "D20"});
+ generateAllCheckouts();
+ selectOptimalCheckouts();
}
/**
- * Gets the pre-calculated checkout route for a given score.
+ * Generates all possible 1-dart, 2-dart, and 3-dart double-out combinations.
+ */
+ private static void generateAllCheckouts() {
+ // Generate 1-dart checkouts (D1-D20, plus Bull)
+ for (int first = 1; first <= 20; first++) {
+ final int score = first * DOUBLE;
+ addCheckout(score, new String[]{DartsConstants.PREFIX_DOUBLE + first});
+ }
+ addCheckout(50, new String[]{DartsConstants.LABEL_BULLSEYE});
+
+ // Generate 2-dart checkouts
+ for (int doubleOut = 1; doubleOut <= 20; doubleOut++) {
+ final int outValue = doubleOut * DOUBLE;
+
+ for (int setup = 1; setup <= 20; setup++) {
+ // Single + Double
+ final int singleScore = (setup * SINGLE) + outValue;
+ addCheckout(singleScore, new String[]{String.valueOf(setup), DartsConstants.PREFIX_DOUBLE + doubleOut});
+
+ // Double + Double
+ final int doubleScore = (setup * DOUBLE) + outValue;
+ addCheckout(doubleScore, new String[]{DartsConstants.PREFIX_DOUBLE + setup, DartsConstants.PREFIX_DOUBLE + doubleOut});
+
+ // Triple + Double
+ final int tripleScore = (setup * TRIPLE) + outValue;
+ addCheckout(tripleScore, new String[]{DartsConstants.PREFIX_TRIPLE + setup, DartsConstants.PREFIX_DOUBLE + doubleOut});
+ }
+ }
+
+ // Generate 3-dart checkouts
+ for (int doubleOut = 1; doubleOut <= 20; doubleOut++) {
+ final int outValue = doubleOut * DOUBLE;
+
+ for (int second = 1; second <= 20; second++) {
+ final int singleSecond = second * SINGLE;
+ final int doubleSecond = second * DOUBLE;
+ final int tripleSecond = second * TRIPLE;
+
+ for (int first = 1; first <= 20; first++) {
+ // All 9 combinations of (single/double/triple) + (single/double/triple) + double
+ addCheckout((first * SINGLE) + singleSecond + outValue,
+ new String[]{String.valueOf(first), String.valueOf(second), DartsConstants.PREFIX_DOUBLE + doubleOut});
+ addCheckout((first * SINGLE) + doubleSecond + outValue,
+ new String[]{String.valueOf(first), DartsConstants.PREFIX_DOUBLE + second, DartsConstants.PREFIX_DOUBLE + doubleOut});
+ addCheckout((first * SINGLE) + tripleSecond + outValue,
+ new String[]{String.valueOf(first), DartsConstants.PREFIX_TRIPLE + second, DartsConstants.PREFIX_DOUBLE + doubleOut});
+
+ addCheckout((first * DOUBLE) + singleSecond + outValue,
+ new String[]{DartsConstants.PREFIX_DOUBLE + first, String.valueOf(second), DartsConstants.PREFIX_DOUBLE + doubleOut});
+ addCheckout((first * DOUBLE) + doubleSecond + outValue,
+ new String[]{DartsConstants.PREFIX_DOUBLE + first, DartsConstants.PREFIX_DOUBLE + second, DartsConstants.PREFIX_DOUBLE + doubleOut});
+ addCheckout((first * DOUBLE) + tripleSecond + outValue,
+ new String[]{DartsConstants.PREFIX_DOUBLE + first, DartsConstants.PREFIX_TRIPLE + second, DartsConstants.PREFIX_DOUBLE + doubleOut});
+
+ addCheckout((first * TRIPLE) + singleSecond + outValue,
+ new String[]{DartsConstants.PREFIX_TRIPLE + first, String.valueOf(second), DartsConstants.PREFIX_DOUBLE + doubleOut});
+ addCheckout((first * TRIPLE) + doubleSecond + outValue,
+ new String[]{DartsConstants.PREFIX_TRIPLE + first, DartsConstants.PREFIX_DOUBLE + second, DartsConstants.PREFIX_DOUBLE + doubleOut});
+ addCheckout((first * TRIPLE) + tripleSecond + outValue,
+ new String[]{DartsConstants.PREFIX_TRIPLE + first, DartsConstants.PREFIX_TRIPLE + second, DartsConstants.PREFIX_DOUBLE + doubleOut});
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds a checkout route to the map.
+ */
+ private static void addCheckout(final int score, final String[] route) {
+ ALL_CHECKOUTS.computeIfAbsent(score, k -> new ArrayList<>()).add(route);
+ }
+
+ /**
+ * Selects the optimal route for each score based on preferred strategies.
+ * Preferences: Fewer darts > T20/T19 usage > Higher finishing double
+ */
+ private static void selectOptimalCheckouts() {
+ for (Map.Entry> entry : ALL_CHECKOUTS.entrySet()) {
+ final int score = entry.getKey();
+ final List routes = entry.getValue();
+
+ String[] bestRoute = routes.get(0);
+ int bestScore = scoreRoute(bestRoute);
+
+ for (String[] route : routes) {
+ final int routeScore = scoreRoute(route);
+ if (routeScore > bestScore) {
+ bestScore = routeScore;
+ bestRoute = route;
+ }
+ }
+
+ OPTIMAL_CHECKOUTS.put(score, bestRoute);
+ }
+ }
+
+ /**
+ * Scores a route based on desirability.
+ * Higher score = better route
+ */
+ private static int scoreRoute(final String[] route) {
+ int score = 0;
+
+ // Prefer fewer darts (most important)
+ score += (4 - route.length) * 10000;
+
+ // Prefer T20 (second most important)
+ for (String dart : route) {
+ if ("T20".equals(dart)) score += 100;
+ else if ("T19".equals(dart)) score += 90;
+ else if (dart.startsWith("T")) score += 50;
+ }
+
+ // Prefer higher finishing doubles
+ final String lastDart = route[route.length - 1];
+ if (lastDart.startsWith("D")) {
+ try {
+ final int doubleValue = Integer.parseInt(lastDart.substring(1));
+ score += doubleValue;
+ } catch (NumberFormatException e) {
+ // Ignore parsing errors
+ }
+ } else if (DartsConstants.LABEL_BULLSEYE.equals(lastDart)) {
+ score += 25; // Bull gets high score
+ }
+
+ return score;
+ }
+
+ /**
+ * Gets the optimal pre-calculated checkout route for a given score.
*
* @param score The target score to checkout
* @return Array of dart notations, or null if no route exists
*/
public static String[] getCheckoutRoute(final int score) {
- return CHECKOUT_MAP.get(score);
+ return OPTIMAL_CHECKOUTS.get(score);
+ }
+
+ /**
+ * Gets all possible checkout routes for a given score.
+ *
+ * @param score The target score to checkout
+ * @return List of all possible routes, or null if no routes exist
+ */
+ public static List getAllCheckoutRoutes(final int score) {
+ return ALL_CHECKOUTS.get(score);
}
/**
@@ -84,7 +195,7 @@ public final class CheckoutConstants {
* @return true if a route exists, false otherwise
*/
public static boolean hasCheckoutRoute(final int score) {
- return CHECKOUT_MAP.containsKey(score);
+ return OPTIMAL_CHECKOUTS.containsKey(score);
}
/**
@@ -93,6 +204,19 @@ public final class CheckoutConstants {
* @return Set of scores with pre-calculated routes
*/
public static java.util.Set getAvailableCheckouts() {
- return CHECKOUT_MAP.keySet();
+ return OPTIMAL_CHECKOUTS.keySet();
+ }
+
+ /**
+ * Gets the total number of unique checkout combinations generated.
+ *
+ * @return Total number of routes across all scores
+ */
+ public static int getTotalRoutesCount() {
+ int total = 0;
+ for (List routes : ALL_CHECKOUTS.values()) {
+ total += routes.size();
+ }
+ return total;
}
}
diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/utils/CheckoutEngine.java b/app/src/main/java/com/aldo/apps/ochecompanion/utils/CheckoutEngine.java
new file mode 100644
index 0000000..9109508
--- /dev/null
+++ b/app/src/main/java/com/aldo/apps/ochecompanion/utils/CheckoutEngine.java
@@ -0,0 +1,301 @@
+package com.aldo.apps.ochecompanion.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Checkout calculation engine for X01 darts games.
+ * Provides optimal double-out routes for any score with 1-3 darts remaining.
+ * Tracks if players are following suggested paths and identifies double-out attempts.
+ */
+public final class CheckoutEngine {
+
+ // Prevent instantiation
+ private CheckoutEngine() {
+ throw new UnsupportedOperationException("Utility class cannot be instantiated");
+ }
+
+ /** All possible double-out finishes: D1 through D20, plus Bull (50) */
+ private static final int[] DOUBLE_OUT_VALUES = {
+ 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, // D1-D10
+ 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, // D11-D20
+ 50 // Double Bull
+ };
+
+ /** Map of double values to their display labels (e.g., 40 -> "D20") */
+ private static final Map DOUBLE_LABELS = new HashMap<>();
+
+ static {
+ // Initialize double labels
+ for (int i = 1; i <= 20; i++) {
+ DOUBLE_LABELS.put(i * 2, DartsConstants.PREFIX_DOUBLE + i);
+ }
+ DOUBLE_LABELS.put(50, DartsConstants.LABEL_BULLSEYE);
+ }
+
+ /**
+ * Calculates optimal checkout route for given score and darts remaining.
+ *
+ * @param score Target score to finish
+ * @param dartsLeft Number of darts remaining (1-3)
+ * @return Checkout route string or null if no route available
+ */
+ public static String calculateCheckout(final int score, final int dartsLeft) {
+ if (dartsLeft <= 0 || score <= 0 || score == 1) return null;
+
+ switch (dartsLeft) {
+ case 1:
+ return calculateOneDartDoubleOut(score);
+ case 2:
+ return calculateTwoDartDoubleOut(score);
+ case 3:
+ return calculateThreeDartDoubleOut(score);
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Calculates checkout for exactly one dart remaining.
+ * Only suggests if score is a valid double (2-40 even, or 50).
+ *
+ * @param score Target score
+ * @return Double-out dart label or null
+ */
+ public static String calculateOneDartDoubleOut(final int score) {
+ if (isValidDoubleOut(score)) {
+ return DOUBLE_LABELS.get(score);
+ }
+ return null;
+ }
+
+ /**
+ * Calculates checkout for exactly two darts remaining.
+ * Tries: direct double, then setup dart + double.
+ *
+ * @param score Target score
+ * @return Checkout route or null
+ */
+ public static String calculateTwoDartDoubleOut(final int score) {
+ // Try direct double out
+ if (isValidDoubleOut(score)) {
+ return DOUBLE_LABELS.get(score);
+ }
+
+ // Check pre-calculated routes from CheckoutConstants
+ if (CheckoutConstants.hasCheckoutRoute(score)) {
+ final String[] parts = CheckoutConstants.getCheckoutRoute(score);
+ if (parts != null && parts.length <= 2) {
+ return String.join(DartsConstants.CHECKOUT_SEPARATOR, parts);
+ }
+ }
+
+ // Try setup dart logic for odd scores
+ if (score % 2 != 0 && score <= 41) {
+ return calculateSetupDart(score);
+ }
+
+ // Try to leave a valid double
+ for (int doubleValue : DOUBLE_OUT_VALUES) {
+ final int setup = score - doubleValue;
+ if (setup > 0 && setup <= 60) { // Single segment max is 20, triple is 60
+ return setup + DartsConstants.CHECKOUT_SEPARATOR + DOUBLE_LABELS.get(doubleValue);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Calculates checkout for exactly three darts remaining.
+ * Tries: direct double, two-dart checkout, then three-dart combinations.
+ *
+ * @param score Target score
+ * @return Checkout route or null
+ */
+ public static String calculateThreeDartDoubleOut(final int score) {
+ // Try simpler options first
+ final String oneDart = calculateOneDartDoubleOut(score);
+ if (oneDart != null) return oneDart;
+
+ final String twoDart = calculateTwoDartDoubleOut(score);
+ if (twoDart != null) return twoDart;
+
+ // Check pre-calculated three-dart routes
+ if (CheckoutConstants.hasCheckoutRoute(score)) {
+ final String[] parts = CheckoutConstants.getCheckoutRoute(score);
+ if (parts != null && parts.length <= 3) {
+ return String.join(DartsConstants.CHECKOUT_SEPARATOR, parts);
+ }
+ }
+
+ // Try to build a three-dart route
+ // Strategy: High score + setup + double
+ if (score > 50 && score <= 170) {
+ // Try T20 (60) + remainder
+ int remainder = score - 60;
+ if (remainder > 0) {
+ final String twoRemaining = calculateTwoDartDoubleOut(remainder);
+ if (twoRemaining != null) {
+ return "T20" + DartsConstants.CHECKOUT_SEPARATOR + twoRemaining;
+ }
+ }
+
+ // Try T19 (57) + remainder
+ remainder = score - 57;
+ if (remainder > 0) {
+ final String twoRemaining = calculateTwoDartDoubleOut(remainder);
+ if (twoRemaining != null) {
+ return "T19" + DartsConstants.CHECKOUT_SEPARATOR + twoRemaining;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Calculates optimal setup dart to leave a common double.
+ * Prefers leaving D20 (40) or D16 (32).
+ *
+ * @param score Odd score requiring setup
+ * @return Setup route (e.g., "1 • D20")
+ */
+ private static String calculateSetupDart(final int score) {
+ // Try to leave D20 (40)
+ if (score > 40 && score - 40 <= 20) {
+ return (score - 40) + DartsConstants.CHECKOUT_SEPARATOR + DartsConstants.PREFIX_DOUBLE + "20";
+ }
+
+ // Try to leave D16 (32)
+ if (score > 32 && score - 32 <= 20) {
+ return (score - 32) + DartsConstants.CHECKOUT_SEPARATOR + DartsConstants.PREFIX_DOUBLE + "16";
+ }
+
+ // General solution: throw 1 to leave an even score
+ if (score > 1) {
+ final int remainder = score - 1;
+ if (isValidDoubleOut(remainder)) {
+ return "1" + DartsConstants.CHECKOUT_SEPARATOR + DOUBLE_LABELS.get(remainder);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Checks if a score is a valid double-out finish.
+ *
+ * @param score Score to check
+ * @return true if score can be finished with a double
+ */
+ public static boolean isValidDoubleOut(final int score) {
+ return DOUBLE_LABELS.containsKey(score);
+ }
+
+ /**
+ * Checks if a dart value represents a double-out attempt.
+ * Used for tracking double attempts in statistics.
+ *
+ * @param dartValue Point value of the dart thrown
+ * @return true if dart was a double (2-40 even, or 50)
+ */
+ public static boolean isDoubleOutDart(final int dartValue) {
+ return isValidDoubleOut(dartValue);
+ }
+
+ /**
+ * Extracts the first dart from a checkout route string.
+ *
+ * @param checkoutRoute Full route (e.g., "T20 • T20 • BULL")
+ * @return First dart string (e.g., "T20") or null
+ */
+ public static String getFirstDartFromRoute(final String checkoutRoute) {
+ if (checkoutRoute == null || checkoutRoute.isEmpty()) return null;
+ final String[] parts = checkoutRoute.split(DartsConstants.CHECKOUT_SEPARATOR);
+ return parts.length > 0 ? parts[0].trim() : null;
+ }
+
+ /**
+ * Checks if a thrown dart matches the suggested first dart in a route.
+ * Used to determine if player is following the recommended path.
+ *
+ * @param checkoutRoute Suggested route (e.g., "T20 • D20")
+ * @param dartThrown Point value thrown (e.g., 60)
+ * @return true if dart follows the route
+ */
+ public static boolean isFollowingSuggestedRoute(final String checkoutRoute, final int dartThrown) {
+ final String firstDart = getFirstDartFromRoute(checkoutRoute);
+ if (firstDart == null) return false;
+
+ // Parse the expected dart value
+ final int expectedValue = parseDartValue(firstDart);
+ return expectedValue == dartThrown;
+ }
+
+ /**
+ * Parses a dart label into its point value.
+ *
+ * @param dartLabel Label like "T20", "D16", "BULL", or "25"
+ * @return Point value (e.g., 60, 32, 50, 25)
+ */
+ private static int parseDartValue(final String dartLabel) {
+ if (dartLabel == null || dartLabel.isEmpty()) return 0;
+
+ final String label = dartLabel.trim();
+
+ // Handle bullseye
+ if (label.equals(DartsConstants.LABEL_BULLSEYE) || label.equals(DartsConstants.LABEL_DOUBLE_BULL)) {
+ return DartsConstants.DOUBLE_BULL_VALUE;
+ }
+ if (label.equals(DartsConstants.LABEL_BULL)) {
+ return DartsConstants.BULL_VALUE;
+ }
+
+ // Handle prefixed darts (D16, T20, etc.)
+ if (label.startsWith(DartsConstants.PREFIX_TRIPLE)) {
+ try {
+ final int base = Integer.parseInt(label.substring(1));
+ return base * DartsConstants.MULTIPLIER_TRIPLE;
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ if (label.startsWith(DartsConstants.PREFIX_DOUBLE)) {
+ try {
+ final int base = Integer.parseInt(label.substring(1));
+ return base * DartsConstants.MULTIPLIER_DOUBLE;
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ // Plain number
+ try {
+ return Integer.parseInt(label);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Gets all possible double-out values.
+ *
+ * @return Array of valid double finish scores
+ */
+ public static int[] getAllDoubleOutValues() {
+ return DOUBLE_OUT_VALUES.clone();
+ }
+
+ /**
+ * Checks if a remaining score can be finished with the given number of darts.
+ *
+ * @param score Remaining score
+ * @param dartsLeft Darts available
+ * @return true if checkout is possible
+ */
+ public static boolean hasCheckoutRoute(final int score, final int dartsLeft) {
+ return calculateCheckout(score, dartsLeft) != null;
+ }
+}
diff --git a/app/src/main/res/drawable/ic_day_night_mode.xml b/app/src/main/res/drawable/ic_day_night_mode.xml
new file mode 100644
index 0000000..d142a93
--- /dev/null
+++ b/app/src/main/res/drawable/ic_day_night_mode.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_standard_game_mode.xml b/app/src/main/res/drawable/ic_standard_game_mode.xml
new file mode 100644
index 0000000..38ea93d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_standard_game_mode.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_add_player.xml b/app/src/main/res/layout/activity_add_player.xml
index 645c2ed..1288d7b 100644
--- a/app/src/main/res/layout/activity_add_player.xml
+++ b/app/src/main/res/layout/activity_add_player.xml
@@ -3,7 +3,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="@color/midnight_black">
+ android:background="@color/surface_primary"
+ android:id="@+id/main">
+ tools:context=".GameActivity"
+ android:background="@color/background_primary"
+ android:id="@+id/main">
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
new file mode 100644
index 0000000..b4e7989
--- /dev/null
+++ b/app/src/main/res/values/arrays.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ - 701
+ - 501
+ - 301
+ - Cricket
+
+
+
+
+ - @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
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index db79c0b..520c6d8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -29,8 +29,36 @@
Bull
Submit Turn
+
+ day_night_mode
+ standard_game_mode
+ 701
+ 501
+ 301
+ Cricket
+ Day/Night Mode
+ Standard Game Mode
+ The Standard Game Mode to be selected for the Quick Start\nCurrently selected: %s
+
+
Application Logo
Settings
Match History
+ SettingsActivity
+
+
+ Messages
+ Sync
+
+
+ Your signature
+ Default reply action
+
+
+ Sync email periodically
+ Download incoming attachments
+ Automatically download attachments for incoming emails
+
+ Only download attachments when manually requested
\ No newline at end of file
diff --git a/app/src/main/res/xml/main_menu_preferences.xml b/app/src/main/res/xml/main_menu_preferences.xml
new file mode 100644
index 0000000..07d769d
--- /dev/null
+++ b/app/src/main/res/xml/main_menu_preferences.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4e76b86..e9f7907 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -9,6 +9,8 @@ activity = "1.12.2"
constraintlayout = "2.2.1"
glide = "5.0.5"
room = "2.8.4"
+preferences = "1.2.0"
+preference = "1.2.1"
[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
@@ -21,6 +23,8 @@ constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayo
glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide"}
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room"}
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room"}
+preferences = { group = "androidx.preference", name="preference-ktx", version.ref="preferences" }
+preference = { group = "androidx.preference", name = "preference", version.ref = "preference" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }