Made use of the DatabaseHelper in all cases now.

Fixed continue logic by moving it to onResume
This commit is contained in:
Alexander Doerflinger
2026-02-06 15:45:19 +01:00
parent e2ec4f478e
commit 796e0e4389
7 changed files with 467 additions and 209 deletions

174
README.md
View File

@@ -1,70 +1,118 @@
# OcheCompanion # Oche Companion
An Android darts companion app for scoring X01 games (301, 501) with automatic Double Out enforcement, bust detection, and intelligent checkout suggestions. Features player squad management with career statistics, match history tracking, and quick-start practice mode. An Android darts companion app for scoring X01 games (301, 501, 701) with automatic Double Out enforcement, bust detection, and intelligent checkout suggestions. Features comprehensive player statistics with heatmap visualization, career tracking, match history, sound effects, and configurable settings.
## Features ## Features
### Game Scoring ### 🎯 Game Scoring
- **X01 Game Support** - Play standard darts games (301, 501, etc.) - **X01 Game Support** - Play standard darts games (301, 501, 701, etc.)
- **Double Out Enforcement** - Automatically enforces finishing with doubles - **Double Out Enforcement** - Automatically enforces finishing with doubles
- **Bust Detection** - Prevents invalid scores and reverts turns - **Bust Detection** - Prevents invalid scores and reverts turns with visual/haptic feedback
- **Checkout Suggestions** - Intelligent route recommendations for finishing combinations - **Intelligent Checkout Engine** - Real-time route recommendations for finishing combinations (≤170)
- **Custom Keyboard** - Intuitive numeric input with multiplier selection (Single, Double, Triple) - **Custom Keyboard** - Intuitive numeric input with multiplier selection (Single, Double, Triple)
- **Turn Indicators** - Visual dart pills showing current turn progress (3 darts max)
- **Sound Effects** - Audio feedback for wins, busts, and perfect 180 scores
- **Undo Support** - Remove last dart or revert entire turn
- **GameManager Architecture** - Clean separation of business logic from UI via singleton pattern
### Player Management ### 👥 Player Management
- **Squad Roster** - Maintain your team with profile pictures - **Squad Roster** - Maintain your team with custom profile pictures
- **Career Statistics** - Track average scores and matches played per player - **Advanced Statistics** - Comprehensive career tracking including:
- **Custom Profiles** - Add players with personalized avatars - Total matches played and win rate
- Average score per dart and per turn
- Highest checkout and best game average
- Segment-by-segment hit frequency (singles, doubles, triples, bull)
- Total darts thrown and 180s scored
- **Heatmap Visualization** - Interactive dartboard showing hit frequency with color-coded intensity
- **Player Stats Dashboard** - Dedicated view with scrollable statistics and performance metrics
- **Image Cropping** - Custom crop overlay for profile picture editing with zoom controls (0.1x - 10x)
### Match History ### 📊 Match History
- **Match Tracking** - Complete history of all played games - **Match Tracking** - Complete history of all played games with timestamps
- **Detailed Recaps** - View winners, scores, and participant information - **Detailed Recaps** - View winners, final scores, and participant information
- **Persistent Storage** - All data saved locally using Room database - **Persistent Storage** - All data saved locally using Room database with proper relationships
- **Match Resume** - Load and continue unfinished matches
### Quick Features ### ⚙️ Settings & Customization
- **Settings Activity** - Dedicated preferences screen
- **Day/Night Mode** - Theme selection for comfortable viewing
- **Standard Game Mode** - Configure default starting score (301, 501, 701)
- **Quick Start Mode** - Launch practice games instantly without database persistence - **Quick Start Mode** - Launch practice games instantly without database persistence
- **Material Design UI** - Modern, intuitive interface with custom views
### 🎨 UI/UX
- **Material Design 3** - Modern, polished interface with custom views
- **Confetti Celebrations** - Visual effects for game wins
- **Responsive Layouts** - Optimized for various screen sizes
- **Custom Views** - Purpose-built components (HeatmapView, PlayerStatsView, MatchRecapView, etc.)
- **Vibration Feedback** - Haptic responses for busts and key actions
- **BaseActivity** - Centralized theme management across all activities
## Tech Stack ## Tech Stack
- **Language**: Java - **Language**: Java 11
- **Architecture**: MVVM pattern - **Architecture**: MVVM pattern with singleton GameManager
- **Database**: Room Persistence Library - **Database**: Room Persistence Library with DAOs
- **UI Components**: Material Design, Custom Views - **UI Components**: Material Design 3, Custom Views
- **Image Loading**: Glide - **Image Loading**: Glide
- **Min SDK**: 24 (Android 7.0) - **Animations**: Konfetti (confetti effects)
- **Audio**: SoundPool API for low-latency game sounds
- **Data Serialization**: Gson (for complex data structures)
- **Min SDK**: 24 (Android 7.0 Nougat)
- **Target SDK**: 36
## Project Structure ## Project Structure
``` ```
app/src/main/java/com/aldo/apps/ochecompanion/ app/src/main/java/com/aldo/apps/ochecompanion/
├── MainMenuActivity.java # Main entry point ├── MainMenuActivity.java # Main entry point with player list & match history
├── GameActivity.java # Live game scoring ├── GameActivity.java # Live game scoring UI controller
├── AddPlayerActivity.java # Player creation ├── AddPlayerActivity.java # Player creation with image cropping
├── SettingsActivity.java # App preferences and configuration
├── BaseActivity.java # Base activity for theme management
├── game/
│ └── GameManager.java # Singleton business logic manager
├── database/ ├── database/
│ ├── AppDatabase.java # Room database singleton │ ├── AppDatabase.java # Room database singleton
│ ├── DatabaseHelper.java # Database operations helper
│ ├── dao/ │ ├── dao/
│ │ ├── PlayerDao.java # Player CRUD operations │ │ ├── PlayerDao.java # Player CRUD operations
│ │ ── MatchDao.java # Match CRUD operations │ │ ── MatchDao.java # Match CRUD operations
│ │ └── StatisticsDao.java # Statistics CRUD operations
│ └── objects/ │ └── objects/
│ ├── Player.java # Player entity │ ├── Player.java # Player entity
── Match.java # Match entity ── Match.java # Match entity
├── models/ │ └── Statistics.java # Statistics entity
│ └── Match.java # Match model ├── ui/
└── ui/ │ ├── HeatmapView.java # Dartboard heatmap visualization
├── MatchRecapView.java # Custom match recap view ├── PlayerStatsView.java # Comprehensive stats dashboard
├── PlayerItemView.java # Custom player card view ├── MatchRecapView.java # Custom match recap view
├── QuickStartButton.java # Quick start button ├── PlayerItemView.java # Custom player card view
├── CropOverlayView.java # Image cropping overlay ├── QuickStartButton.java # Quick start button
── adapter/ ── CropOverlayView.java # Image cropping overlay
├── MainMenuPlayerAdapter.java ├── PlayerSelectionDialogFragment.java # Player picker dialog
── MainMenuGroupMatchAdapter.java ── MainMenuPreferencesFragment.java # Settings fragment
│ └── adapter/
│ ├── MainMenuPlayerAdapter.java
│ ├── MainMenuGroupMatchAdapter.java
│ └── PlayerSelectionAdapter.java
└── utils/
├── CheckoutEngine.java # Checkout route calculation
├── CheckoutConstants.java # Pre-calculated checkout routes (2-170)
├── DartsConstants.java # Game constants (scores, multipliers, labels)
├── UIConstants.java # UI constants (colors, animations, scales)
├── SoundEngine.java # Singleton sound effect manager
├── ResourceHelper.java # Resource utilities
├── Log.java # Custom logging wrapper
├── MatchProgress.java # Match state serialization
└── converters/
├── IntListConverter.java # Room type converter for lists
└── MatchProgressConverter.java # Room type converter for match state
``` ```
## Getting Started ## Getting Started
### Prerequisites ### Prerequisites
- Android Studio Arctic Fox or later - Android Studio Hedgehog or later
- JDK 11 or later - JDK 11 or later
- Android SDK with API Level 24+ - Android SDK with API Level 24+
@@ -79,7 +127,7 @@ git clone https://github.com/yourusername/OcheCompanion.git
3. Sync Gradle files 3. Sync Gradle files
4. Run the app on an emulator or physical device 4. Run the app on an emulator or physical device (API 24+)
### Building ### Building
@@ -95,18 +143,43 @@ git clone https://github.com/yourusername/OcheCompanion.git
## Usage ## Usage
1. **Add Players** - Tap the add button to create player profiles ### Getting Started
2. **Start a Game** - Select players and choose game type (301, 501, etc.) 1. **Add Players** - Tap the add button (+) to create player profiles with custom avatars
3. **Score Throws** - Use the custom keyboard to input dart scores 2. **Configure Settings** - Access settings to choose day/night mode and default game type
4. **Finish Games** - Follow checkout suggestions to finish with doubles 3. **Start a Match** - Select players from your roster and choose starting score (301, 501, 701)
5. **View History** - Review past matches and statistics 4. **Quick Practice** - Use Quick Start button for instant practice games without saving
### During a Game
1. **Select Multiplier** - Choose Single, Double, or Triple before each dart
2. **Tap Numbers** - Input dart scores using the numeric keyboard (1-20 or Bull)
3. **Follow Suggestions** - Checkout routes appear automatically when score ≤ 170
4. **Undo Mistakes** - Use undo button to remove last dart
5. **Submit Turn** - Advance to next player after 3 darts or when ready
6. **View Stats** - Tap player card to see detailed statistics and heatmap
### After a Game
1. **Review Match** - View match recap with winner and final scores
2. **Check Statistics** - Access player stats to see updated career metrics
3. **View History** - Browse past matches in the main menu
## Game Rules ## Game Rules
- Games use standard X01 format (301, 501, etc.) - Games use standard X01 format (301, 501, 701, etc.)
- Players must finish by hitting a double (Double Out) - Players must finish by hitting a double (Double Out rule)
- Busting (going below zero or landing on 1) reverts the turn - Busting (going below zero or landing on exactly 1) reverts the entire turn
- Checkout suggestions appear when score is 170 or below - Checkout suggestions appear when remaining score is 170 or below
- Maximum of 3 darts per turn
- Perfect score of 180 (Triple 20 × 3) triggers special celebration
## Code Quality
This project follows Android best practices:
-**No Magic Numbers** - All constants extracted to centralized constant classes
-**Comprehensive Documentation** - JavaDoc comments on all public methods
-**MVVM Architecture** - Clear separation of concerns
-**Singleton Pattern** - GameManager and SoundEngine for efficient resource management
-**Type Safety** - Room database with compile-time verification
-**Proper Naming** - Android conventions (m-prefix for members, s-prefix for statics)
## License ## License
@@ -117,6 +190,7 @@ This project is licensed under the MIT License - see the LICENSE file for detail
- Darts rules and checkout combinations from standard competitive play - Darts rules and checkout combinations from standard competitive play
- Material Design guidelines from Google - Material Design guidelines from Google
- Android Architecture Components - Android Architecture Components
- Sound effects for game events
--- ---

View File

@@ -9,6 +9,7 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import com.aldo.apps.ochecompanion.database.DatabaseHelper;
import com.aldo.apps.ochecompanion.ui.PlayerStatsView; import com.aldo.apps.ochecompanion.ui.PlayerStatsView;
import com.aldo.apps.ochecompanion.utils.Log; import com.aldo.apps.ochecompanion.utils.Log;
import android.view.MotionEvent; import android.view.MotionEvent;
@@ -43,8 +44,10 @@ import java.io.InputStream;
import java.util.UUID; import java.util.UUID;
/** /**
* Manages creation and editing of player profiles with username, profile picture, and image cropping. * Manages creation and editing of player profiles with username, profile
* Operates in Form Mode (profile editing) or Crop Mode (interactive image cropping with pan and zoom). * picture, and image cropping.
* Operates in Form Mode (profile editing) or Crop Mode (interactive image
* cropping with pan and zoom).
* Pass EXTRA_PLAYER_ID to edit existing player; otherwise creates new player. * Pass EXTRA_PLAYER_ID to edit existing player; otherwise creates new player.
*/ */
public class AddPlayerActivity extends BaseActivity { public class AddPlayerActivity extends BaseActivity {
@@ -163,10 +166,16 @@ public class AddPlayerActivity extends BaseActivity {
private ScaleGestureDetector mScaleDetector; private ScaleGestureDetector mScaleDetector;
/** /**
* Current scale factor applied to the crop preview image (1.0 default, clamped 0.1 to 10.0). * Current scale factor applied to the crop preview image (1.0 default, clamped
* 0.1 to 10.0).
*/ */
private float mScaleFactor = UIConstants.SCALE_NORMAL; private float mScaleFactor = UIConstants.SCALE_NORMAL;
/**
* Database helper for synchronous database operations.
*/
private DatabaseHelper mDatabaseHelper;
/** /**
* ActivityResultLauncher for selecting images from the device gallery. * ActivityResultLauncher for selecting images from the device gallery.
*/ */
@@ -190,7 +199,8 @@ public class AddPlayerActivity extends BaseActivity {
}); });
/** /**
* Called when the activity is first created. Initializes UI and loads existing player if present. * Called when the activity is first created. Initializes UI and loads existing
* player if present.
* *
* @param savedInstanceState Saved instance state. * @param savedInstanceState Saved instance state.
*/ */
@@ -199,13 +209,16 @@ public class AddPlayerActivity extends BaseActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_player); setContentView(R.layout.activity_add_player);
Log.d(TAG, "AddPlayerActivity Created"); Log.d(TAG, "AddPlayerActivity Created");
// Configure window insets to properly handle system bars (status bar, navigation bar) // Configure window insets to properly handle system bars (status bar,
// navigation bar)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets; return insets;
}); });
mDatabaseHelper = DatabaseHelper.getInstance(this);
// Initialize all UI components and their click listeners // Initialize all UI components and their click listeners
initViews(); initViews();
@@ -227,7 +240,8 @@ public class AddPlayerActivity extends BaseActivity {
} }
/** /**
* Handles the back button press. If the stats view is currently shown, it hides it instead of exiting. * Handles the back button press. If the stats view is currently shown, it hides
* it instead of exiting.
* Otherwise, it finishes the activity as normal. * Otherwise, it finishes the activity as normal.
*/ */
private void handleBackPressed() { private void handleBackPressed() {
@@ -290,7 +304,8 @@ public class AddPlayerActivity extends BaseActivity {
} }
/** /**
* Displays a rationale dialog explaining why the app needs the specified permission. * Displays a rationale dialog explaining why the app needs the specified
* permission.
* *
* @param permission The permission for which to show the rationale * @param permission The permission for which to show the rationale
*/ */
@@ -298,7 +313,8 @@ public class AddPlayerActivity extends BaseActivity {
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setTitle(R.string.txt_permission_hint_title) .setTitle(R.string.txt_permission_hint_title)
.setMessage(R.string.txt_permission_hint_description) .setMessage(R.string.txt_permission_hint_description)
.setPositiveButton(R.string.txt_permission_hint_button_ok, (d, w) -> requestPermissionLauncher.launch(permission)) .setPositiveButton(R.string.txt_permission_hint_button_ok,
(d, w) -> requestPermissionLauncher.launch(permission))
.setNegativeButton(R.string.txt_permission_hint_button_cancel, null) .setNegativeButton(R.string.txt_permission_hint_button_cancel, null)
.show(); .show();
} }
@@ -368,11 +384,11 @@ public class AddPlayerActivity extends BaseActivity {
mLayoutCropper.setVisibility(View.VISIBLE); mLayoutCropper.setVisibility(View.VISIBLE);
// Reset transformation state for a fresh start // Reset transformation state for a fresh start
mScaleFactor = UIConstants.SCALE_NORMAL; // Reset zoom to 100% mScaleFactor = UIConstants.SCALE_NORMAL; // Reset zoom to 100%
mIvCropPreview.setScaleX(UIConstants.SCALE_NORMAL); mIvCropPreview.setScaleX(UIConstants.SCALE_NORMAL);
mIvCropPreview.setScaleY(UIConstants.SCALE_NORMAL); mIvCropPreview.setScaleY(UIConstants.SCALE_NORMAL);
mIvCropPreview.setTranslationX(0); // Reset horizontal position mIvCropPreview.setTranslationX(0); // Reset horizontal position
mIvCropPreview.setTranslationY(0); // Reset vertical position mIvCropPreview.setTranslationY(0); // Reset vertical position
// Load the selected image into the preview // Load the selected image into the preview
mIvCropPreview.setImageURI(uri); mIvCropPreview.setImageURI(uri);
@@ -388,7 +404,8 @@ public class AddPlayerActivity extends BaseActivity {
} }
/** /**
* Performs the pixel-level mathematics to extract a square crop from the selected image. * Performs the pixel-level mathematics to extract a square crop from the
* selected image.
* Accounts for ImageView fit-center scale, user translation, and user zoom. * Accounts for ImageView fit-center scale, user translation, and user zoom.
*/ */
private void performCrop() { private void performCrop() {
@@ -429,13 +446,16 @@ public class AddPlayerActivity extends BaseActivity {
Log.d(TAG, String.format("Crop Pixels: X=%d, Y=%d, Size=%d | UserZoom=%.2f", cX, cY, cSize, mScaleFactor)); Log.d(TAG, String.format("Crop Pixels: X=%d, Y=%d, Size=%d | UserZoom=%.2f", cX, cY, cSize, mScaleFactor));
// Bounds checks to prevent Bitmap.createBitmap from crashing with invalid coordinates // Bounds checks to prevent Bitmap.createBitmap from crashing with invalid
cX = Math.max(0, cX); // Ensure X is not negative // coordinates
cY = Math.max(0, cY); // Ensure Y is not negative cX = Math.max(0, cX); // Ensure X is not negative
cY = Math.max(0, cY); // Ensure Y is not negative
// Clamp crop size to not exceed bitmap boundaries // Clamp crop size to not exceed bitmap boundaries
if (cX + cSize > bmpW) cSize = (int) bmpW - cX; if (cX + cSize > bmpW)
if (cY + cSize > bmpH) cSize = (int) bmpH - cY; cSize = (int) bmpW - cX;
if (cY + cSize > bmpH)
cSize = (int) bmpH - cY;
// Ensure size is at least 1px to avoid crashes // Ensure size is at least 1px to avoid crashes
cSize = Math.max(1, cSize); cSize = Math.max(1, cSize);
@@ -448,7 +468,7 @@ public class AddPlayerActivity extends BaseActivity {
// Update the profile picture preview if save was successful // Update the profile picture preview if save was successful
if (mInternalImagePath != null) { if (mInternalImagePath != null) {
mProfilePictureView.setImageTintList(null); // Remove any tint mProfilePictureView.setImageTintList(null); // Remove any tint
mProfilePictureView.setImageBitmap(cropped); mProfilePictureView.setImageBitmap(cropped);
} }
@@ -465,7 +485,8 @@ public class AddPlayerActivity extends BaseActivity {
} }
/** /**
* Saves a bitmap to the application's private internal storage as JPEG with 90% quality. * Saves a bitmap to the application's private internal storage as JPEG with 90%
* quality.
* *
* @param bmp The bitmap image to save. * @param bmp The bitmap image to save.
* @return The absolute file path, or null if saving failed. * @return The absolute file path, or null if saving failed.
@@ -498,8 +519,8 @@ public class AddPlayerActivity extends BaseActivity {
private void loadExistingPlayer() { private void loadExistingPlayer() {
new Thread(() -> { new Thread(() -> {
// Query the database for the player (background thread) // Query the database for the player (background thread)
mExistingPlayer = AppDatabase.getDatabase(this).playerDao().getPlayerById(mExistingPlayerId); mExistingPlayer = mDatabaseHelper.getPlayerById(mExistingPlayerId);
final Statistics statistics = AppDatabase.getDatabase(this).statisticsDao().getStatisticsForPlayer(mExistingPlayerId); final Statistics statistics = mDatabaseHelper.getStatisticsForPlayer(mExistingPlayerId);
// Update UI on the main thread // Update UI on the main thread
runOnUiThread(() -> { runOnUiThread(() -> {
@@ -510,8 +531,10 @@ public class AddPlayerActivity extends BaseActivity {
// Update UI labels for "edit mode" // Update UI labels for "edit mode"
mTitleView.setText(R.string.txt_update_profile_header); mTitleView.setText(R.string.txt_update_profile_header);
mSaveButton.setText(R.string.txt_update_profile_username_save); mSaveButton.setText(R.string.txt_update_profile_username_save);
if (mBtnDelete != null) mBtnDelete.setVisibility(View.VISIBLE); if (mBtnDelete != null)
if (mShowStatsButton != null) mShowStatsButton.setVisibility(View.VISIBLE); mBtnDelete.setVisibility(View.VISIBLE);
if (mShowStatsButton != null)
mShowStatsButton.setVisibility(View.VISIBLE);
mShowStatsButton.setOnClickListener(v -> { mShowStatsButton.setOnClickListener(v -> {
mPlayerStatsView.setVisibility(View.VISIBLE); mPlayerStatsView.setVisibility(View.VISIBLE);
mIsStatsViewShown = true; mIsStatsViewShown = true;
@@ -523,7 +546,7 @@ public class AddPlayerActivity extends BaseActivity {
// Load existing profile picture if available // Load existing profile picture if available
if (mExistingPlayer.profilePictureUri != null) { if (mExistingPlayer.profilePictureUri != null) {
mInternalImagePath = mExistingPlayer.profilePictureUri; mInternalImagePath = mExistingPlayer.profilePictureUri;
mProfilePictureView.setImageTintList(null); // Remove placeholder tint mProfilePictureView.setImageTintList(null); // Remove placeholder tint
mProfilePictureView.setImageURI(Uri.fromFile(new File(mInternalImagePath))); mProfilePictureView.setImageURI(Uri.fromFile(new File(mInternalImagePath)));
} }
} }
@@ -534,12 +557,14 @@ public class AddPlayerActivity extends BaseActivity {
/** /**
* Deletes the currently loaded player from the database. * Deletes the currently loaded player from the database.
* Shows confirmation toast and closes the activity upon successful deletion. * Shows confirmation toast and closes the activity upon successful deletion.
* Runs database operation on background thread. Does nothing if no player is loaded. * Runs database operation on background thread. Does nothing if no player is
* loaded.
*/ */
private void deletePlayer() { private void deletePlayer() {
if (mExistingPlayer == null) return; if (mExistingPlayer == null)
return;
new Thread(() -> { new Thread(() -> {
AppDatabase.getDatabase(this).playerDao().delete(mExistingPlayer); mDatabaseHelper.deletePlayer(mExistingPlayer);
runOnUiThread(() -> { runOnUiThread(() -> {
Toast.makeText(this, "Player removed from squad", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Player removed from squad", Toast.LENGTH_SHORT).show();
finish(); finish();
@@ -565,16 +590,16 @@ public class AddPlayerActivity extends BaseActivity {
// Update existing player // Update existing player
mExistingPlayer.username = name; mExistingPlayer.username = name;
mExistingPlayer.profilePictureUri = mInternalImagePath; mExistingPlayer.profilePictureUri = mInternalImagePath;
AppDatabase.getDatabase(this).playerDao().update(mExistingPlayer); mDatabaseHelper.updatePlayer(mExistingPlayer);
} else { } else {
// Create and insert new player // Create and insert new player
final Player p = new Player(name, mInternalImagePath); final Player p = new Player(name, mInternalImagePath);
final long newUserId = AppDatabase.getDatabase(this).playerDao().insert(p); final long newUserId = mDatabaseHelper.insertPlayer(p);
final Player dbPlayer = AppDatabase.getDatabase(this).playerDao().getPlayerById(newUserId); final Player dbPlayer = mDatabaseHelper.getPlayerById(newUserId);
if (dbPlayer != null) { if (dbPlayer != null) {
Log.d(TAG, "savePlayer: Player has been created, create stats as well."); Log.d(TAG, "savePlayer: Player has been created, create stats as well.");
final Statistics playerStats = new Statistics(dbPlayer.id); final Statistics playerStats = new Statistics(dbPlayer.id);
AppDatabase.getDatabase(this).statisticsDao().insertStatistics(playerStats); mDatabaseHelper.insertStats(playerStats);
} }
} }

View File

@@ -62,6 +62,9 @@ import nl.dionsegijn.konfetti.xml.KonfettiView;
*/ */
public class GameActivity extends BaseActivity implements GameManager.GameStateCallback { public class GameActivity extends BaseActivity implements GameManager.GameStateCallback {
/**
* Tag for logging purposes.
*/
private static final String TAG = "GameActivity"; private static final String TAG = "GameActivity";
/** /**

View File

@@ -86,21 +86,8 @@ public class MainMenuActivity extends BaseActivity {
return insets; return insets;
}); });
final QuickStartButton quickStartBtn = findViewById(R.id.quick_start_btn);
final String defaultGameMode = mSettingsPref.getString(getString(R.string.pref_desc_standard_game_mode),
getString(R.string.pref_game_mode_501_value));
quickStartBtn.setSubText(defaultGameMode);
quickStartBtn.setOnClickListener(v -> quickStart());
findViewById(R.id.btnSettings).setOnClickListener(v -> launchSettings());
final List<Match> ongoingMatches = mDatabaseHelper.getOngoingMatches(); findViewById(R.id.btnSettings).setOnClickListener(v -> launchSettings());
if (ongoingMatches != null && !ongoingMatches.isEmpty()) {
mOngoingMatch = ongoingMatches.get(0);
}
if (mOngoingMatch != null) {
Log.d(TAG, "onCreate: Found ongoing match [" + mOngoingMatch + "]");
quickStartBtn.setSubText("Continue match with " + mOngoingMatch.gameMode + " score");
}
// Set up match recap view with test data functionality // Set up match recap view with test data functionality
mMatchRecap = findViewById(R.id.match_recap); mMatchRecap = findViewById(R.id.match_recap);
@@ -122,6 +109,19 @@ public class MainMenuActivity extends BaseActivity {
initSquadView(); initSquadView();
// Apply the last finished match if available. // Apply the last finished match if available.
applyLastMatch(); applyLastMatch();
final QuickStartButton quickStartBtn = findViewById(R.id.quick_start_btn);
final String defaultGameMode = mSettingsPref.getString(getString(R.string.pref_desc_standard_game_mode),
getString(R.string.pref_game_mode_501_value));
quickStartBtn.setSubText(defaultGameMode);
quickStartBtn.setOnClickListener(v -> quickStart());
final List<Match> ongoingMatches = mDatabaseHelper.getOngoingMatches();
if (ongoingMatches != null && !ongoingMatches.isEmpty()) {
mOngoingMatch = ongoingMatches.get(0);
}
if (mOngoingMatch != null) {
Log.d(TAG, "onCreate: Found ongoing match [" + mOngoingMatch + "]");
quickStartBtn.setSubText("Continue match with " + mOngoingMatch.gameMode + " score");
}
} }
/** /**

View File

@@ -17,10 +17,14 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
/** /**
* Centralized database helper that manages all database operations with proper synchronization. * Centralized database helper that manages all database operations with proper
* Handles threading internally to prevent race conditions and simplify database access. * synchronization.
* All database operations are executed on a background thread pool with per-player locking * Handles threading internally to prevent race conditions and simplify database
* to ensure data integrity while allowing concurrent updates for different players. * access.
* All database operations are executed on a background thread pool with
* per-player locking
* to ensure data integrity while allowing concurrent updates for different
* players.
*/ */
public class DatabaseHelper { public class DatabaseHelper {
@@ -40,14 +44,16 @@ public class DatabaseHelper {
private final AppDatabase mDatabase; private final AppDatabase mDatabase;
/** /**
* Single-threaded executor for database operations to ensure sequential execution. * Single-threaded executor for database operations to ensure sequential
* execution.
* Prevents race conditions by serializing all database writes. * Prevents race conditions by serializing all database writes.
*/ */
private final ExecutorService mExecutor; private final ExecutorService mExecutor;
/** /**
* Per-player locks for fine-grained synchronization. * Per-player locks for fine-grained synchronization.
* Allows concurrent updates for different players while preventing conflicts for the same player. * Allows concurrent updates for different players while preventing conflicts
* for the same player.
*/ */
private final Map<Long, Object> mPlayerLocks = new HashMap<>(); private final Map<Long, Object> mPlayerLocks = new HashMap<>();
@@ -95,15 +101,17 @@ public class DatabaseHelper {
* Updates player statistics after a turn. * Updates player statistics after a turn.
* Handles darts thrown, points made, bust tracking, and milestone counting. * Handles darts thrown, points made, bust tracking, and milestone counting.
* *
* @param playerId Player's database ID * @param playerId Player's database ID
* @param dartsThrown Number of darts thrown this turn * @param dartsThrown Number of darts thrown this turn
* @param pointsMade Total points scored this turn * @param pointsMade Total points scored this turn
* @param wasBust Whether the turn resulted in a bust * @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) * @param checkoutValue The checkout score if this was a winning turn
* @param totalDartsThrownInMatch Total darts thrown by player in current match (for first 9 tracking) * (0 if not a checkout)
* @param totalDartsThrownInMatch Total darts thrown by player in current match
* (for first 9 tracking)
*/ */
public void updatePlayerStatistics(final long playerId, final int dartsThrown, final int pointsMade, public void updatePlayerStatistics(final long playerId, final int dartsThrown, final int pointsMade,
final boolean wasBust, final int checkoutValue, final int totalDartsThrownInMatch) { final boolean wasBust, final int checkoutValue, final int totalDartsThrownInMatch) {
mExecutor.execute(() -> { mExecutor.execute(() -> {
final Object lock = getPlayerLock(playerId); final Object lock = getPlayerLock(playerId);
synchronized (lock) { synchronized (lock) {
@@ -122,7 +130,8 @@ public class DatabaseHelper {
} else { } else {
// Normal turn - record darts and points // Normal turn - record darts and points
playerStats.saveDartsThrown(dartsThrown, pointsMade); playerStats.saveDartsThrown(dartsThrown, pointsMade);
Log.d(TAG, "updatePlayerStatistics: dartsThrown = [" + dartsThrown + "], pointsMade = [" + pointsMade + "]"); Log.d(TAG, "updatePlayerStatistics: dartsThrown = [" + dartsThrown + "], pointsMade = ["
+ pointsMade + "]");
// Track missed darts if turn ended early (less than 3 darts) // Track missed darts if turn ended early (less than 3 darts)
if (dartsThrown < 3) { if (dartsThrown < 3) {
@@ -147,7 +156,8 @@ public class DatabaseHelper {
final long dartsToAdd = Math.min(dartsThrown, 9 - totalDartsThrownInMatch); final long dartsToAdd = Math.min(dartsThrown, 9 - totalDartsThrownInMatch);
playerStats.setTotalFirst9Darts(playerStats.getTotalFirst9Darts() + dartsToAdd); playerStats.setTotalFirst9Darts(playerStats.getTotalFirst9Darts() + dartsToAdd);
playerStats.setTotalFirst9Points(playerStats.getTotalFirst9Points() + pointsMade); playerStats.setTotalFirst9Points(playerStats.getTotalFirst9Points() + pointsMade);
Log.d(TAG, "updatePlayerStatistics: First 9 tracking - darts: " + dartsToAdd + ", points: " + pointsMade); Log.d(TAG, "updatePlayerStatistics: First 9 tracking - darts: " + dartsToAdd + ", points: "
+ pointsMade);
} }
// Track successful checkout // Track successful checkout
@@ -194,7 +204,8 @@ public class DatabaseHelper {
if (playerStats != null) { if (playerStats != null) {
playerStats.addDoubleOutTarget(isMissed); playerStats.addDoubleOutTarget(isMissed);
mDatabase.statisticsDao().updateStatistics(playerStats); mDatabase.statisticsDao().updateStatistics(playerStats);
Log.d(TAG, "trackDoubleAttempt: Recorded double attempt (missed=" + isMissed + ") for player " + playerId); Log.d(TAG, "trackDoubleAttempt: Recorded double attempt (missed=" + isMissed + ") for player "
+ playerId);
} }
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "trackDoubleAttempt: Failed to track double attempt", e); Log.e(TAG, "trackDoubleAttempt: Failed to track double attempt", e);
@@ -219,7 +230,8 @@ public class DatabaseHelper {
if (playerStats != null) { if (playerStats != null) {
playerStats.addCompletedMatch(); playerStats.addCompletedMatch();
mDatabase.statisticsDao().updateStatistics(playerStats); mDatabase.statisticsDao().updateStatistics(playerStats);
Log.d(TAG, "incrementMatchesPlayed: Incremented for player " + playerId + ", total: " + playerStats.getMatchesPlayed()); Log.d(TAG, "incrementMatchesPlayed: Incremented for player " + playerId + ", total: "
+ playerStats.getMatchesPlayed());
} }
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "incrementMatchesPlayed: Failed to increment matches for player " + playerId, e); Log.e(TAG, "incrementMatchesPlayed: Failed to increment matches for player " + playerId, e);
@@ -246,7 +258,7 @@ public class DatabaseHelper {
/** /**
* Constructs a DartHit with the specified base value and multiplier. * Constructs a DartHit with the specified base value and multiplier.
* *
* @param baseValue The dartboard number (1-20 or 25 for bull) * @param baseValue The dartboard number (1-20 or 25 for bull)
* @param multiplier The multiplier (1=single, 2=double, 3=triple) * @param multiplier The multiplier (1=single, 2=double, 3=triple)
*/ */
public DartHit(final int baseValue, final int multiplier) { public DartHit(final int baseValue, final int multiplier) {
@@ -263,7 +275,8 @@ public class DatabaseHelper {
* @param dartHits List of dart hit details from the turn * @param dartHits List of dart hit details from the turn
*/ */
public void recordDartHits(final long playerId, final List<DartHit> dartHits) { public void recordDartHits(final long playerId, final List<DartHit> dartHits) {
if (dartHits == null || dartHits.isEmpty()) return; if (dartHits == null || dartHits.isEmpty())
return;
mExecutor.execute(() -> { mExecutor.execute(() -> {
final Object lock = getPlayerLock(playerId); final Object lock = getPlayerLock(playerId);
@@ -271,12 +284,14 @@ public class DatabaseHelper {
try { try {
final Statistics playerStats = mDatabase.statisticsDao().getStatisticsForPlayer(playerId); final Statistics playerStats = mDatabase.statisticsDao().getStatisticsForPlayer(playerId);
if (playerStats != null) { if (playerStats != null) {
Log.d(TAG, "recordDartHits: Before recording - hitDistribution size: " + playerStats.getHitDistribution().size()); Log.d(TAG, "recordDartHits: Before recording - hitDistribution size: "
+ playerStats.getHitDistribution().size());
// Record all darts from this turn // Record all darts from this turn
for (final DartHit hit : dartHits) { for (final DartHit hit : dartHits) {
playerStats.recordDartHit(hit.baseValue, hit.multiplier); playerStats.recordDartHit(hit.baseValue, hit.multiplier);
} }
Log.d(TAG, "recordDartHits: After recording - hitDistribution size: " + playerStats.getHitDistribution().size()); Log.d(TAG, "recordDartHits: After recording - hitDistribution size: "
+ playerStats.getHitDistribution().size());
Log.d(TAG, "recordDartHits: hitDistribution contents: " + playerStats.getHitDistribution()); Log.d(TAG, "recordDartHits: hitDistribution contents: " + playerStats.getHitDistribution());
mDatabase.statisticsDao().updateStatistics(playerStats); mDatabase.statisticsDao().updateStatistics(playerStats);
Log.d(TAG, "recordDartHits: Recorded " + dartHits.size() + " darts for player " + playerId); Log.d(TAG, "recordDartHits: Recorded " + dartHits.size() + " darts for player " + playerId);
@@ -290,7 +305,8 @@ public class DatabaseHelper {
/** /**
* Retrieves all players from the database synchronously. * Retrieves all players from the database synchronously.
* Blocks until the operation completes to ensure consistency with any pending writes. * Blocks until the operation completes to ensure consistency with any pending
* writes.
* *
* @return List of all players, or empty list if none exist * @return List of all players, or empty list if none exist
*/ */
@@ -304,12 +320,13 @@ public class DatabaseHelper {
} }
/** /**
* Creates a new match record in the database with the specified game mode and players. * Creates a new match record in the database with the specified game mode and
* players.
* Initializes the match progress with starting scores and player data. * Initializes the match progress with starting scores and player data.
* Blocks until the operation completes to return the new match ID. * Blocks until the operation completes to return the new match ID.
* *
* @param gameMode The game mode string (e.g., "501") * @param gameMode The game mode string (e.g., "501")
* @param players List of Player objects participating in the match * @param players List of Player objects participating in the match
* @return The ID of the newly created match, or -1 if creation failed * @return The ID of the newly created match, or -1 if creation failed
*/ */
public long createNewMatch(final String gameMode, final List<Player> players) { public long createNewMatch(final String gameMode, final List<Player> players) {
@@ -343,8 +360,7 @@ public class DatabaseHelper {
0L, // Guest has ID 0 0L, // Guest has ID 0
"GUEST", "GUEST",
startingScore, startingScore,
0 0));
));
} }
// Convert to JSON // Convert to JSON
@@ -384,7 +400,9 @@ public class DatabaseHelper {
/** /**
* Retrieves all ongoing matches from the database synchronously. * Retrieves all ongoing matches from the database synchronously.
* Blocks until the operation completes to ensure consistency with any pending writes. * Blocks until the operation completes to ensure consistency with any pending
* writes.
*
* @return List of ongoing matches, or empty list if none exist * @return List of ongoing matches, or empty list if none exist
*/ */
public List<Match> getOngoingMatches() { public List<Match> getOngoingMatches() {
@@ -419,8 +437,11 @@ public class DatabaseHelper {
/** /**
* Retrieves the most recently completed match from the database synchronously. * Retrieves the most recently completed match from the database synchronously.
* Blocks until the operation completes to ensure consistency with any pending writes. * Blocks until the operation completes to ensure consistency with any pending
* @return The most recent completed match, or null if no completed matches exist * writes.
*
* @return The most recent completed match, or null if no completed matches
* exist
*/ */
public Match getLastCompletedMatch() { public Match getLastCompletedMatch() {
try { try {
@@ -438,9 +459,34 @@ public class DatabaseHelper {
} }
} }
/**
* Retrieves the participant data from the most recent match in the database.
* This method is synchronous and blocks until the database operation completes.
*
* @return The participant data (JSON string) from the most recent match,
* or null if no matches are found.
*/
public String getLastMatchParticipantData() {
try {
return mExecutor.submit(() -> {
try {
return mDatabase.matchDao().getLastMatchParticipantData();
} catch (Exception e) {
Log.e(TAG, "getLastMatchParticipantData: Failed to retrieve last match participant data", e);
return null;
}
}).get();
} catch (Exception e) {
Log.e(TAG, "getLastMatchParticipantData: Failed to submit task", e);
return null;
}
}
/** /**
* Retrieves a match by its unique ID from the database synchronously. * Retrieves a match by its unique ID from the database synchronously.
* Blocks until the operation completes to ensure consistency with any pending writes. * Blocks until the operation completes to ensure consistency with any pending
* writes.
*
* @param matchId The unique identifier of the match * @param matchId The unique identifier of the match
* @return The match with the specified ID, or null if no such match exists * @return The match with the specified ID, or null if no such match exists
*/ */
@@ -462,7 +508,8 @@ public class DatabaseHelper {
/** /**
* Retrieves statistics for a specific player synchronously. * Retrieves statistics for a specific player synchronously.
* Blocks until the operation completes to ensure consistency with any pending writes. * Blocks until the operation completes to ensure consistency with any pending
* writes.
* *
* @param playerId The player's database ID * @param playerId The player's database ID
* @return Statistics object for the player, or null if not found * @return Statistics object for the player, or null if not found
@@ -486,6 +533,89 @@ public class DatabaseHelper {
} }
} }
/**
* Inserts a new player into the database.
*
* @param player The Player entity to insert
*/
public long insertPlayer(final Player player) {
try {
return mExecutor.submit(() -> mDatabase.playerDao().insert(player)).get();
} catch (Exception e) {
Log.e(TAG, "insertPlayer: Failed to submit task", e);
return -1;
}
}
/**
* Updates an existing player in the database.
*
* @param player The Player entity to update
*/
public void updatePlayer(final Player player) {
mExecutor.execute(() -> {
try {
mDatabase.playerDao().update(player);
} catch (Exception e) {
Log.e(TAG, "updatePlayer: Failed to update player", e);
}
});
}
/**
* Deletes a player from the database.
*
* @param player The Player entity to delete
*/
public void deletePlayer(final Player player) {
mExecutor.execute(() -> {
try {
mDatabase.playerDao().delete(player);
} catch (Exception e) {
Log.e(TAG, "deletePlayer: Failed to delete player", e);
}
});
}
/**
* Retrieves a player by their database ID synchronously.
* Blocks until the operation completes to ensure consistency with any pending
* writes.
*
* @param playerId The player's database ID
* @return Player object for the specified player, or null if not found
*/
public Player getPlayerById(final long playerId) {
try {
return mExecutor.submit(() -> {
try {
return mDatabase.playerDao().getPlayerById(playerId);
} catch (Exception e) {
Log.e(TAG, "getPlayerById: Failed to retrieve player", e);
return null;
}
}).get();
} catch (final Exception e) {
Log.e(TAG, "getPlayerById: Failed to submit task", e);
return null;
}
}
/**
* Inserts a PlayerStats entity into the database.
*
* @param playerStats The PlayerStats entity to insert
*/
public void insertStats(final Statistics playerStats) {
mExecutor.execute(() -> {
try {
mDatabase.statisticsDao().insertStatistics(playerStats);
} catch (Exception e) {
Log.e(TAG, "insertStats: Failed to insert stats", e);
}
});
}
/** /**
* Shuts down the executor service. * Shuts down the executor service.
* Should be called when the helper is no longer needed. * Should be called when the helper is no longer needed.

View File

@@ -5,6 +5,7 @@ import androidx.room.Entity;
import androidx.room.Ignore; import androidx.room.Ignore;
import androidx.room.PrimaryKey; import androidx.room.PrimaryKey;
import com.aldo.apps.ochecompanion.utils.Log;
import com.aldo.apps.ochecompanion.utils.MatchProgress; import com.aldo.apps.ochecompanion.utils.MatchProgress;
import com.aldo.apps.ochecompanion.utils.converters.MatchProgressConverter; import com.aldo.apps.ochecompanion.utils.converters.MatchProgressConverter;

View File

@@ -13,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.aldo.apps.ochecompanion.GameActivity; import com.aldo.apps.ochecompanion.GameActivity;
import com.aldo.apps.ochecompanion.R; import com.aldo.apps.ochecompanion.R;
import com.aldo.apps.ochecompanion.database.AppDatabase; import com.aldo.apps.ochecompanion.database.AppDatabase;
import com.aldo.apps.ochecompanion.database.DatabaseHelper;
import com.aldo.apps.ochecompanion.database.objects.Player; import com.aldo.apps.ochecompanion.database.objects.Player;
import com.aldo.apps.ochecompanion.ui.adapter.PlayerSelectionAdapter; import com.aldo.apps.ochecompanion.ui.adapter.PlayerSelectionAdapter;
import com.aldo.apps.ochecompanion.utils.DartsConstants; import com.aldo.apps.ochecompanion.utils.DartsConstants;
@@ -27,19 +28,25 @@ import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
* PlayerSelectionDialogFragment: A modern bottom-sheet selector for match participants. * PlayerSelectionDialogFragment: A modern bottom-sheet selector for match
* participants.
* <p> * <p>
* This {@link BottomSheetDialogFragment} provides a user interface for selecting players * This {@link BottomSheetDialogFragment} provides a user interface for
* selecting players
* from the database before starting a new match. It features: * from the database before starting a new match. It features:
* <ul> * <ul>
* <li>Automatic pre-selection of players from the most recent match for speed</li> * <li>Automatic pre-selection of players from the most recent match for
* <li>Dynamic button state that displays the current selection count</li> * speed</li>
* <li>Integration with {@link PlayerSelectionAdapter} for multi-select functionality</li> * <li>Dynamic button state that displays the current selection count</li>
* <li>Validation to ensure at least one player is selected before starting</li> * <li>Integration with {@link PlayerSelectionAdapter} for multi-select
* functionality</li>
* <li>Validation to ensure at least one player is selected before starting</li>
* </ul> * </ul>
* <p> * <p>
* The dialog automatically loads all players from the database on creation and queries * The dialog automatically loads all players from the database on creation and
* the last match's participant data to pre-populate selections, improving user experience * queries
* the last match's participant data to pre-populate selections, improving user
* experience
* for consecutive matches with the same players. * for consecutive matches with the same players.
* *
* @see PlayerSelectionAdapter * @see PlayerSelectionAdapter
@@ -86,50 +93,65 @@ public class PlayerSelectionDialogFragment extends BottomSheetDialogFragment {
private MaterialButton mBtnStart; private MaterialButton mBtnStart;
/** /**
* The adapter that manages the player selection list in the {@link RecyclerView}. * The adapter that manages the player selection list in the
* {@link RecyclerView}.
* <p> * <p>
* Handles player selection logic, visual feedback, and communicates * Handles player selection logic, visual feedback, and communicates
* selection changes via {@link #onSelectionChanged()}. * selection changes via {@link #onSelectionChanged()}.
*/ */
private PlayerSelectionAdapter mAdapter; private PlayerSelectionAdapter mAdapter;
/**
* The database helper instance used for database operations.
*/
private DatabaseHelper mDatabaseHelper;
/** /**
* Creates and returns the view hierarchy associated with this fragment. * Creates and returns the view hierarchy associated with this fragment.
* <p> * <p>
* Inflates the player selection bottom sheet layout, which includes: * Inflates the player selection bottom sheet layout, which includes:
* <ul> * <ul>
* <li>A {@link RecyclerView} for displaying the player list</li> * <li>A {@link RecyclerView} for displaying the player list</li>
* <li>A {@link MaterialButton} for confirming the selection</li> * <li>A {@link MaterialButton} for confirming the selection</li>
* </ul> * </ul>
* *
* @param inflater The {@link LayoutInflater} used to inflate views in the fragment * @param inflater The {@link LayoutInflater} used to inflate views in
* @param container The parent view that the fragment's UI should be attached to, * the fragment
* @param container The parent view that the fragment's UI should be
* attached to,
* or {@code null} if not attached * or {@code null} if not attached
* @param savedInstanceState If non-null, this fragment is being re-constructed from a * @param savedInstanceState If non-null, this fragment is being re-constructed
* from a
* previous saved state * previous saved state
* @return The root {@link View} of the inflated layout * @return The root {@link View} of the inflated layout
*/ */
@Nullable @Nullable
@Override @Override
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
mDatabaseHelper = DatabaseHelper.getInstance(getContext());
return inflater.inflate(R.layout.layout_player_selection_sheet, container, false); return inflater.inflate(R.layout.layout_player_selection_sheet, container, false);
} }
/** /**
* Called immediately after {@link #onCreateView} has returned, but before any saved * Called immediately after {@link #onCreateView} has returned, but before any
* saved
* state has been restored into the view. * state has been restored into the view.
* <p> * <p>
* This method performs the following initialization steps: * This method performs the following initialization steps:
* <ol> * <ol>
* <li>Binds UI components ({@link RecyclerView}, {@link MaterialButton})</li> * <li>Binds UI components ({@link RecyclerView}, {@link MaterialButton})</li>
* <li>Configures the {@link RecyclerView} with a {@link LinearLayoutManager}</li> * <li>Configures the {@link RecyclerView} with a
* <li>Initializes the {@link PlayerSelectionAdapter} with selection callback</li> * {@link LinearLayoutManager}</li>
* <li>Sets up the confirmation button click listener</li> * <li>Initializes the {@link PlayerSelectionAdapter} with selection
* <li>Triggers asynchronous data loading via {@link #loadData()}</li> * callback</li>
* <li>Sets up the confirmation button click listener</li>
* <li>Triggers asynchronous data loading via {@link #loadData()}</li>
* </ol> * </ol>
* *
* @param view The {@link View} returned by {@link #onCreateView} * @param view The {@link View} returned by {@link #onCreateView}
* @param savedInstanceState If non-null, this fragment is being re-constructed from a * @param savedInstanceState If non-null, this fragment is being re-constructed
* from a
* previous saved state * previous saved state
*/ */
@Override @Override
@@ -154,12 +176,12 @@ public class PlayerSelectionDialogFragment extends BottomSheetDialogFragment {
* This method performs database operations on a background thread to avoid * This method performs database operations on a background thread to avoid
* blocking the UI. The loading process includes: * blocking the UI. The loading process includes:
* <ol> * <ol>
* <li>Fetching all players from the database</li> * <li>Fetching all players from the database</li>
* <li>Retrieving the last match's participant data for pre-selection</li> * <li>Retrieving the last match's participant data for pre-selection</li>
* <li>Parsing the last match data using {@link MatchProgressConverter}</li> * <li>Parsing the last match data using {@link MatchProgressConverter}</li>
* <li>Extracting player IDs from the previous match</li> * <li>Extracting player IDs from the previous match</li>
* <li>Updating the UI on the main thread with loaded data</li> * <li>Updating the UI on the main thread with loaded data</li>
* <li>Pre-selecting players who participated in the last match</li> * <li>Pre-selecting players who participated in the last match</li>
* </ol> * </ol>
* <p> * <p>
* This pre-selection behavior significantly improves user experience when * This pre-selection behavior significantly improves user experience when
@@ -167,13 +189,11 @@ public class PlayerSelectionDialogFragment extends BottomSheetDialogFragment {
*/ */
private void loadData() { private void loadData() {
new Thread(() -> { new Thread(() -> {
final AppDatabase db = AppDatabase.getDatabase(requireContext());
// 1. Get All Players // 1. Get All Players
final List<Player> players = db.playerDao().getAllPlayers(); final List<Player> players = mDatabaseHelper.getAllPlayers();
// 2. Get Last Participants for Pre-selection // 2. Get Last Participants for Pre-selection
final String lastJson = db.matchDao().getLastMatchParticipantData(); final String lastJson = mDatabaseHelper.getLastMatchParticipantData();
final MatchProgress lastProgress = MatchProgressConverter.fromString(lastJson); final MatchProgress lastProgress = MatchProgressConverter.fromString(lastJson);
final Set<Integer> lastPlayerIds = new HashSet<>(); final Set<Integer> lastPlayerIds = new HashSet<>();
@@ -204,7 +224,8 @@ public class PlayerSelectionDialogFragment extends BottomSheetDialogFragment {
} }
/** /**
* Callback method invoked by {@link PlayerSelectionAdapter} when the selection state changes. * Callback method invoked by {@link PlayerSelectionAdapter} when the selection
* state changes.
* <p> * <p>
* This method is triggered whenever a player is selected or deselected in the * This method is triggered whenever a player is selected or deselected in the
* {@link RecyclerView}. It delegates to {@link #updateButtonState()} to reflect * {@link RecyclerView}. It delegates to {@link #updateButtonState()} to reflect
@@ -222,10 +243,12 @@ public class PlayerSelectionDialogFragment extends BottomSheetDialogFragment {
* <p> * <p>
* This method performs two UI updates: * This method performs two UI updates:
* <ul> * <ul>
* <li><b>Enabled State:</b> The button is enabled only when at least one player * <li><b>Enabled State:</b> The button is enabled only when at least one player
* is selected (count > 0), preventing match initiation with empty selections</li> * is selected (count > 0), preventing match initiation with empty
* <li><b>Text Display:</b> Shows "START MATCH (count)" when players are selected, * selections</li>
* or "SELECT PLAYERS" as a prompt when no selections are made</li> * <li><b>Text Display:</b> Shows "START MATCH (count)" when players are
* selected,
* or "SELECT PLAYERS" as a prompt when no selections are made</li>
* </ul> * </ul>
* <p> * <p>
* This provides clear visual feedback about the current selection state and * This provides clear visual feedback about the current selection state and
@@ -242,12 +265,13 @@ public class PlayerSelectionDialogFragment extends BottomSheetDialogFragment {
* <p> * <p>
* This method performs the following operations: * This method performs the following operations:
* <ol> * <ol>
* <li>Constructs a list of selected {@link Player} objects by filtering * <li>Constructs a list of selected {@link Player} objects by filtering
* {@link #mAllPlayers} based on {@link #mSelectedIds}</li> * {@link #mAllPlayers} based on {@link #mSelectedIds}</li>
* <li>Validates that at least one player is selected (early return if empty)</li> * <li>Validates that at least one player is selected (early return if
* <li>Launches {@link GameActivity} with the selected players and default score * empty)</li>
* ({@link DartsConstants#DEFAULT_GAME_SCORE})</li> * <li>Launches {@link GameActivity} with the selected players and default score
* <li>Dismisses this dialog fragment after successfully starting the match</li> * ({@link DartsConstants#DEFAULT_GAME_SCORE})</li>
* <li>Dismisses this dialog fragment after successfully starting the match</li>
* </ol> * </ol>
* <p> * <p>
* This method is triggered by the confirmation button click listener set up * This method is triggered by the confirmation button click listener set up
@@ -261,7 +285,8 @@ public class PlayerSelectionDialogFragment extends BottomSheetDialogFragment {
} }
} }
if (selectedList.isEmpty()) return; if (selectedList.isEmpty())
return;
// Start the game! // Start the game!
GameActivity.start(requireContext(), selectedList, DartsConstants.DEFAULT_GAME_SCORE); GameActivity.start(requireContext(), selectedList, DartsConstants.DEFAULT_GAME_SCORE);