From 52a18b40d0e19a07f294e51f288e04b846747abd Mon Sep 17 00:00:00 2001 From: Alexander Doerflinger Date: Wed, 28 Jan 2026 14:00:46 +0100 Subject: [PATCH] Simplifed JavaDoc, applied coding rules --- .idea/vcs.xml | 6 + .../apps/ochecompanion/AddPlayerActivity.java | 332 +---- .../aldo/apps/ochecompanion/GameActivity.java | 1271 ++--------------- .../apps/ochecompanion/MainMenuActivity.java | 159 +-- .../ochecompanion/database/AppDatabase.java | 839 +---------- .../ochecompanion/database/dao/MatchDao.java | 275 +--- .../ochecompanion/database/dao/PlayerDao.java | 446 +----- .../ochecompanion/database/objects/Match.java | 799 +---------- .../database/objects/Player.java | 1102 +------------- .../aldo/apps/ochecompanion/models/Match.java | 382 +---- .../ochecompanion/ui/CropOverlayView.java | 318 +---- .../apps/ochecompanion/ui/MatchRecapView.java | 396 +---- .../apps/ochecompanion/ui/PlayerItemView.java | 258 +--- .../ochecompanion/ui/QuickStartButton.java | 327 +---- .../ui/adapter/MainMenuGroupMatchAdapter.java | 254 +--- .../ui/adapter/MainMenuPlayerAdapter.java | 245 +--- 16 files changed, 513 insertions(+), 6896 deletions(-) create mode 100644 .idea/vcs.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file 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 9e7a318..f6e6434 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/AddPlayerActivity.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/AddPlayerActivity.java @@ -28,70 +28,19 @@ import java.io.InputStream; import java.util.UUID; /** - * AddPlayerActivity manages the creation and editing of player profiles in the Oche Companion application. - *

- * This activity provides a comprehensive user interface for managing player information including: - *

- *

- *

- * The activity features two distinct UI modes: - *

- *

- *

- * Image Processing Pipeline: - *

    - *
  1. User selects an image from their gallery
  2. - *
  3. Activity switches to Crop Mode with the selected image
  4. - *
  5. User can pan (drag) and pinch-to-zoom to frame the desired area
  6. - *
  7. A square crop overlay shows the final cutout area
  8. - *
  9. Upon confirmation, the cropped image is saved to internal storage
  10. - *
  11. Activity returns to Form Mode with the cropped image displayed
  12. - *
- *

- *

- * Usage: - * To edit an existing player, pass the player ID via intent extra using {@link #EXTRA_PLAYER_ID}. - * If no extra is provided, the activity operates in "create new player" mode. - *

- * - * @see AppCompatActivity - * @see Player - * @see CropOverlayView - * @see AppDatabase - * @author Oche Companion Development Team - * @version 1.0 - * @since 1.0 + * Manages creation and editing of player profiles with username, profile 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. */ public class AddPlayerActivity extends AppCompatActivity { /** - * Tag for logging and debugging purposes. - * Used to identify log messages originating from this activity. + * Tag for logging. */ private static final String TAG = "Oche_AddPlayer"; /** - * Intent extra key for passing an existing player's ID for editing. - *

- * When this extra is present, the activity loads the player's existing data - * and operates in "edit mode". Without this extra, the activity creates a new player. - *

- *

- * Usage example: - *

-     * Intent intent = new Intent(context, AddPlayerActivity.class);
-     * intent.putExtra(AddPlayerActivity.EXTRA_PLAYER_ID, playerId);
-     * startActivity(intent);
-     * 
- *

+ * Intent extra key for passing existing player's ID for editing. */ public static final String EXTRA_PLAYER_ID = "extra_player_id"; @@ -99,19 +48,16 @@ public class AddPlayerActivity extends AppCompatActivity { /** * Container layout for the main player profile form. - * Visible during Form Mode when the user is entering player details. */ private View mLayoutForm; /** * Container layout for the image cropping interface. - * Visible during Crop Mode when the user is adjusting their selected image. */ private View mLayoutCropper; /** - * ImageView displaying the player's profile picture in the main form. - * Clicking this view triggers the image selection process. + * ImageView displaying the player's profile picture. */ private ShapeableImageView mProfilePictureView; @@ -121,14 +67,12 @@ public class AddPlayerActivity extends AppCompatActivity { private EditText mUserNameInput; /** - * TextView displaying the activity title ("Add Player" or "Update Profile"). - * The title changes based on whether creating a new player or editing an existing one. + * TextView displaying the activity title. */ private TextView mTitleView; /** - * Button to save the player profile (insert new or update existing). - * The button label changes based on the current mode ("Save" or "Update"). + * Button to save the player profile. */ private MaterialButton mSaveButton; @@ -136,13 +80,11 @@ public class AddPlayerActivity extends AppCompatActivity { /** * ImageView displaying the full selected image during Crop Mode. - * Supports pan and pinch-to-zoom gestures for precise positioning. */ private ImageView mIvCropPreview; /** * Custom overlay view that renders the square crop area boundary. - * Shows the user exactly what portion of the image will be extracted. */ private CropOverlayView mCropOverlay; @@ -150,72 +92,48 @@ public class AddPlayerActivity extends AppCompatActivity { /** * Absolute file path to the saved profile picture in internal storage. - * Set after the user confirms their cropped image. This path is persisted - * in the database with the player record. */ private String mInternalImagePath; /** - * URI of the original, unmodified image selected from the gallery. - * Used as the source for cropping operations. + * URI of the original image selected from the gallery. */ private Uri mRawSelectedUri; /** - * Database ID of the player being edited. - * Defaults to -1, indicating "create new player" mode. When >= 0, - * the activity loads and updates an existing player. + * Database ID of the player being edited (-1 for new player). */ private int mExistingPlayerId = -1; /** - * Player object loaded from the database when editing an existing player. - * Null when creating a new player. Used to update existing records. + * Player object loaded from the database (null when creating new player). */ private Player mExistingPlayer; // ========== Gesture State ========== /** - * Last recorded X coordinate during pan gesture (drag). - * Used to calculate the delta movement between touch events. + * Last recorded X coordinate during pan gesture. */ private float mLastTouchX; /** - * Last recorded Y coordinate during pan gesture (drag). - * Used to calculate the delta movement between touch events. + * Last recorded Y coordinate during pan gesture. */ private float mLastTouchY; /** - * Detector for handling pinch-to-zoom gestures on the crop preview image. - * Monitors multi-touch events to calculate scale changes. + * Detector for handling pinch-to-zoom gestures. */ private ScaleGestureDetector mScaleDetector; /** - * Current scale factor applied to the crop preview image. - *

- * Starts at 1.0 (no zoom) and is modified by pinch gestures. - * Clamped between 0.1 (minimum zoom) and 10.0 (maximum zoom) to prevent - * the image from becoming unusably small or excessively large. - *

+ * Current scale factor applied to the crop preview image (1.0 default, clamped 0.1 to 10.0). */ private float mScaleFactor = 1.0f; /** * ActivityResultLauncher for selecting images from the device gallery. - *

- * Registered using the {@link ActivityResultContracts.GetContent} contract, - * which provides a standard way to pick content of a specific MIME type. - * Upon successful selection, automatically transitions to Crop Mode. - *

- *

- * The launcher is triggered when the user taps the profile picture placeholder. - *

- * - * @see ActivityResultContracts.GetContent */ private final ActivityResultLauncher mGetContent = registerForActivityResult( new ActivityResultContracts.GetContent(), @@ -228,31 +146,12 @@ public class AddPlayerActivity extends AppCompatActivity { }); /** - * Called when the activity is first created. - *

- * Performs the following initialization tasks: - *

- *

- *

- * If {@link #EXTRA_PLAYER_ID} is present in the intent, the activity operates in - * "edit mode" and loads the existing player's data. Otherwise, it operates in - * "create new player" mode. - *

+ * Called when the activity is first created. Initializes UI and loads existing player if present. * - * @param savedInstanceState If the activity is being re-initialized after previously being shut down, - * this Bundle contains the data it most recently supplied in - * {@link #onSaveInstanceState(Bundle)}. Otherwise, it is null. - * @see #initViews() - * @see #setupGestures() - * @see #loadExistingPlayer() + * @param savedInstanceState Saved instance state. */ @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_add_player); Log.d(TAG, "AddPlayerActivity Created"); @@ -272,25 +171,6 @@ public class AddPlayerActivity extends AppCompatActivity { /** * Initializes all UI component references and sets up click listeners. - *

- * This method performs the following operations: - *

- *

- *

- * The profile picture view is configured to launch the gallery picker when clicked, - * filtering for image MIME types only ("image/*"). - *

- * - * @see #mGetContent - * @see #savePlayer() - * @see #performCrop() - * @see #exitCropMode() */ private void initViews() { // Get references to layout containers @@ -315,35 +195,13 @@ public class AddPlayerActivity extends AppCompatActivity { } /** - * Initializes gesture detectors to handle pinch-to-zoom and pan (drag) gestures. - *

- * This method configures two types of touch interactions for the crop preview image: - *

    - *
  1. Pinch-to-Zoom: Two-finger pinch gestures to scale the image
  2. - *
  3. Pan (Drag): Single-finger drag to reposition the image
  4. - *
- *

- *

- * Scale Gesture Handling: - * The scale detector monitors multi-touch events and calculates scale changes. - * The scale factor is clamped between 0.1× (minimum) and 10.0× (maximum) to prevent - * the image from becoming unusably small or excessively large. - *

- *

- * Pan Gesture Handling: - * Pan gestures are only processed when a scale gesture is not in progress, preventing - * conflicts between the two gesture types. The translation is calculated based on the - * delta between consecutive touch positions. - *

- * - * @see ScaleGestureDetector - * @see MotionEvent + * Initializes gesture detectors to handle pinch-to-zoom and pan gestures. */ private void setupGestures() { // Initialize scale detector for pinch-to-zoom functionality mScaleDetector = new ScaleGestureDetector(this, new ScaleGestureDetector.SimpleOnScaleGestureListener() { @Override - public boolean onScale(ScaleGestureDetector detector) { + public boolean onScale(final ScaleGestureDetector detector) { // Apply the scale factor from the gesture mScaleFactor *= detector.getScaleFactor(); @@ -359,7 +217,7 @@ public class AddPlayerActivity extends AppCompatActivity { }); // Combined touch listener for both Panning and Scaling - mIvCropPreview.setOnTouchListener((v, event) -> { + mIvCropPreview.setOnTouchListener((final View v, final MotionEvent event) -> { // Pass touch event to scale detector first to handle pinch gestures mScaleDetector.onTouchEvent(event); @@ -392,26 +250,10 @@ public class AddPlayerActivity extends AppCompatActivity { /** * Transitions the UI from Form Mode to Crop Mode. - *

- * This method performs the following operations: - *

- *

- *

- * Resetting transformations ensures that each crop session starts with the image - * in a predictable state (1:1 scale, centered position), providing a consistent - * user experience. - *

* - * @param uri The URI of the raw, unmodified image selected from the gallery. - * This image will be displayed in the crop preview for manipulation. - * @see #exitCropMode() + * @param uri The URI of the image selected from the gallery. */ - private void enterCropMode(Uri uri) { + private void enterCropMode(final Uri uri) { // Hide form layout and show cropper layout mLayoutForm.setVisibility(View.GONE); mLayoutCropper.setVisibility(View.VISIBLE); @@ -429,20 +271,6 @@ public class AddPlayerActivity extends AppCompatActivity { /** * Transitions the UI from Crop Mode back to Form Mode. - *

- * This method is called when the user either: - *

- *

- *

- * The method simply toggles visibility between the two layout containers, - * hiding the cropper and showing the main form. - *

- * - * @see #enterCropMode(Uri) - * @see #performCrop() */ private void exitCropMode() { // Hide cropper layout and show form layout @@ -452,39 +280,7 @@ public class AddPlayerActivity extends AppCompatActivity { /** * Performs the pixel-level mathematics to extract a square crop from the selected image. - *

- * This is the core image processing method that handles the complex coordinate transformations - * required to crop the image accurately. The calculation accounts for multiple transformation layers: - *

    - *
  1. Original Image Dimensions: The actual pixel dimensions of the source bitmap
  2. - *
  3. ImageView Fit-Center Scale: The automatic scaling applied by Android to fit the image in the view
  4. - *
  5. User Translation (Panning): The X/Y offset from user drag gestures
  6. - *
  7. User Scale (Zoom): The scale factor from user pinch-to-zoom gestures
  8. - *
- *

- *

- * Algorithm Overview: - *

    - *
  1. Decode the full bitmap from the URI
  2. - *
  3. Calculate the fit-center scale applied by the ImageView
  4. - *
  5. Combine fit-center scale with user's manual zoom scale
  6. - *
  7. Determine the current position of the bitmap in screen space
  8. - *
  9. Get the crop box coordinates from the overlay
  10. - *
  11. Transform screen coordinates to bitmap pixel coordinates
  12. - *
  13. Apply bounds checking to ensure valid crop dimensions
  14. - *
  15. Extract the cropped region and save to internal storage
  16. - *
  17. Update the profile picture preview with the cropped image
  18. - *
- *

- *

- * Error Handling: - * If any step fails (bitmap decoding, file I/O, etc.), an error is logged and a - * toast message is displayed to the user. The method gracefully handles errors - * without crashing the application. - *

- * - * @see #saveBitmap(Bitmap) - * @see CropOverlayView#getCropRect() + * Accounts for ImageView fit-center scale, user translation, and user zoom. */ private void performCrop() { Log.d(TAG, "Finalizing crop..."); @@ -560,33 +356,12 @@ public class AddPlayerActivity extends AppCompatActivity { } /** - * Saves a bitmap to the application's private internal storage directory. - *

- * This method generates a unique filename using a UUID and saves the bitmap - * as a JPEG file with 90% quality. The file is stored in the app's private - * files directory, which is only accessible to this application. - *

- *

- * File Storage Details: - *

- *

- *

- * Error Handling: - * If any I/O error occurs during the save operation, the exception is logged - * and null is returned, allowing the caller to handle the failure gracefully. - *

+ * Saves a bitmap to the application's private internal storage as JPEG with 90% quality. * - * @param bmp The bitmap image to save. Must not be null. - * @return The absolute file path to the saved image file, or null if saving failed. - * @see UUID#randomUUID() - * @see Bitmap#compress(Bitmap.CompressFormat, int, java.io.OutputStream) + * @param bmp The bitmap image to save. + * @return The absolute file path, or null if saving failed. */ - private String saveBitmap(Bitmap bmp) { + private String saveBitmap(final Bitmap bmp) { try { // Generate a unique filename using UUID to prevent collisions String name = "profile_" + UUID.randomUUID().toString() + ".jpg"; @@ -609,29 +384,7 @@ public class AddPlayerActivity extends AppCompatActivity { /** * Loads an existing player's data from the database and populates the UI. - *

- * This method is called during {@link #onCreate(Bundle)} when {@link #EXTRA_PLAYER_ID} - * is present in the intent, indicating that the activity should edit an existing player - * rather than create a new one. - *

- *

- * Operations performed: - *

    - *
  1. Queries the database for the player by ID (on background thread)
  2. - *
  3. Updates the username field with the player's current username
  4. - *
  5. Changes the title to "Update Profile" instead of "Add Player"
  6. - *
  7. Changes the save button text to "Update" instead of "Save"
  8. - *
  9. Loads and displays the player's profile picture if one exists
  10. - *
- *

- *

- * Threading: - * Database operations are performed on a background thread to avoid blocking the UI. - * UI updates are posted back to the main thread using {@link #runOnUiThread(Runnable)}. - *

- * - * @see AppDatabase#playerDao() - * @see Player + * Database operations performed on background thread. */ private void loadExistingPlayer() { new Thread(() -> { @@ -661,32 +414,7 @@ public class AddPlayerActivity extends AppCompatActivity { /** * Validates and persists the player data to the database. - *

- * This method determines whether to insert a new player or update an existing one - * based on whether {@link #mExistingPlayer} is null. - *

- *

- * Validation: - * The username field must not be empty (after trimming whitespace). If validation fails, - * a toast message is shown and the method returns without saving. - *

- *

- * Database Operations: - *

- *

- *

- * Threading: - * Database operations are performed on a background thread to prevent blocking the UI. - * After the save operation completes, the activity finishes on the main thread. - *

- * - * @see Player - * @see AppDatabase#playerDao() + * Inserts new player or updates existing based on mExistingPlayer. */ private void savePlayer() { // Validate username input 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 0b3da4a..28e26b4 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/GameActivity.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/GameActivity.java @@ -22,161 +22,19 @@ import java.util.List; import java.util.Map; /** - * The main game activity for playing X01 darts games (501, 301, etc.) in the Oche Companion app. - *

- * This activity provides a high-performance, professional-grade scoring interface optimized for - * rapid dart entry during live gameplay. It features a specialized numeric keyboard with dynamic - * visual feedback, real-time checkout route suggestions, and strict enforcement of standard darts - * rules including Double Out finish requirements and bust conditions. - *

- *

- * Game Features: - *

- *

- *

- * X01 Game Rules Enforced: - *

- *

- *

- * UI Layout Structure: - *

- * ┌─────────────────────────────────────┐
- * │  Player Name          AVG: 85.3     │  ← Header
- * │                                     │
- * │         REMAINING: 301              │  ← Score Display
- * │                                     │
- * │  [Checkout: T20 • D20]             │  ← Smart Route (conditional)
- * │                                     │
- * │  [Dart1] [Dart2] [Dart3]           │  ← Turn Indicators
- * │                                     │
- * │  [ 1×] [ 2×] [ 3×]                 │  ← Multiplier Buttons
- * │                                     │
- * │  ┌───────────────────────────┐     │
- * │  │ 1   2   3   4   5        │     │  ← Numeric Keyboard
- * │  │ 6   7   8   9   10       │     │
- * │  │ 11  12  13  14  15       │     │
- * │  │ 16  17  18  19  20       │     │
- * │  └───────────────────────────┘     │
- * │  [Undo] [Submit Turn]              │  ← Action Buttons
- * └─────────────────────────────────────┘
- * 
- *

- *

- * Game Flow: - *

    - *
  1. Activity is started with player list and starting score (e.g., 501)
  2. - *
  3. First player begins their turn
  4. - *
  5. Player selects multiplier (1×, 2×, or 3×)
  6. - *
  7. Player taps number on keyboard to register dart
  8. - *
  9. Score updates immediately, checkout routes appear when applicable
  10. - *
  11. After 3 darts or winning/busting, turn ends (auto or via Submit)
  12. - *
  13. Next player's turn begins automatically
  14. - *
  15. Game continues until a player finishes exactly on 0 with a double
  16. - *
- *

- *

- * Usage Example: - *

- * // Start a 501 game with two players
- * ArrayList<Player> players = new ArrayList<>();
- * players.add(player1);
- * players.add(player2);
- * GameActivity.start(context, players, 501);
- * 
- * // Or start a 301 game
- * GameActivity.start(context, players, 301);
- * 
- *

- *

- * Bust Conditions: - * A turn results in a bust (score reverts, turn ends) when: - *

- * Example: Player on 32 hits single 16, leaving 16. Then hits single 16 = BUST (not a double). - *

- *

- * Multiplier System: - *

- * Note: Multiplier resets to 1× after each dart for safety. - *

- *

- * Checkout Engine: - * Provides optimal finishing routes when player is within checkout range (≤170): - *

- *

- *

- * Performance Optimizations: - *

- *

- *

- * Future Enhancements: - * Consider adding: - *

- *

- * - * @see Player - * @see CheckoutEngine - * @see X01State - * @author Oche Companion Development Team - * @version 1.0 - * @since 1.0 + * Main game activity for playing X01 darts games (501, 301, etc.). + * Provides numeric keyboard, real-time checkout suggestions, Double Out enforcement, + * and bust detection. Enforces standard darts rules including finishing on doubles. */ public class GameActivity extends AppCompatActivity { /** - * Intent extra key for passing the list of participating players. - *

- * Value type: {@code ArrayList} (Parcelable) - *

+ * Intent extra key for player list. Type: ArrayList */ private static final String EXTRA_PLAYERS = "extra_players"; /** - * Intent extra key for passing the starting score for the X01 game. - *

- * Value type: {@code int} (typically 501, 301, or 701) - *

+ * Intent extra key for starting score. Type: int (typically 501, 301, or 701) */ private static final String EXTRA_START_SCORE = "extra_start_score"; @@ -185,128 +43,42 @@ public class GameActivity extends AppCompatActivity { // ======================================================================================== /** - * Index of the player whose turn is currently active. - *

- * Cycles through player indices (0 to playerCount-1) as turns are completed. - * After each turn submission, this increments modulo player count to rotate turns. - *

- *

- * Example for 2 players: - *

    - *
  • Player 1 turn: mActivePlayerIndex = 0
  • - *
  • Player 2 turn: mActivePlayerIndex = 1
  • - *
  • Player 1 turn: mActivePlayerIndex = 0 (wraps around)
  • - *
- *

+ * Index of the current active player (0 to playerCount-1). + * Cycles through players as turns complete. */ private int mActivePlayerIndex = 0; /** - * Current multiplier selected for the next dart throw. - *

- * Valid values: - *

    - *
  • 1: Single (normal value)
  • - *
  • 2: Double (2× value, red visual)
  • - *
  • 3: Triple (3× value, blue visual)
  • - *
- *

- *

- * Automatically resets to 1 after each dart is thrown for safety, preventing - * accidental double/triple throws. Players must explicitly select multiplier - * before each dart. - *

+ * Current multiplier for the next dart (1=Single, 2=Double, 3=Triple). + * Resets to 1 after each dart for safety. */ private int mMultiplier = 1; /** - * The starting score for this X01 game (typically 501, 301, or 701). - *

- * This value is set once at game initialization from the intent extra and - * determines each player's initial {@link X01State#remainingScore}. Used for - * calculating three-dart averages throughout the game. - *

+ * Starting score for this X01 game (typically 501, 301, or 701). */ private int mStartingScore = 501; /** - * List of player game states, one for each participant. - *

- * Each {@link X01State} tracks an individual player's current score, darts thrown, - * and name. The list order determines turn order, and {@link #mActivePlayerIndex} - * references the current player's state. - *

- *

- * Initialized in {@link #setupGame(List)} with all players starting at - * {@link #mStartingScore}. - *

+ * List of player game states, one per participant. + * Order determines turn order. */ private List mPlayerStates; /** - * Stores the point values of darts thrown in the current turn (up to 3 darts). - *

- * Each dart's final point value (base × multiplier) is added to this list as - * it's thrown. The list is cleared when the turn is submitted or when switching - * to the next player. - *

- *

- * Example: - *

    - *
  • Player hits T20: [60]
  • - *
  • Player hits T20: [60, 60]
  • - *
  • Player hits D10: [60, 60, 20]
  • - *
  • Turn submitted: [] (cleared)
  • - *
- *

- *

- * Used for: - *

    - *
  • Displaying dart indicators in UI
  • - *
  • Calculating turn total
  • - *
  • Determining when 3 darts are complete
  • - *
  • Undo functionality
  • - *
- *

+ * Point values of darts thrown in current turn (up to 3). + * Cleared when turn is submitted. */ - private List mCurrentTurnDarts = new ArrayList<>(); + private final List mCurrentTurnDarts = new ArrayList<>(); /** - * Flag indicating whether the current turn has ended due to bust or win condition. - *

- * When {@code true}: - *

    - *
  • No more darts can be thrown in this turn
  • - *
  • Player must submit turn (or it auto-submits)
  • - *
  • Prevents accidental additional dart entry
  • - *
- *

- *

- * Set to {@code true} when: - *

    - *
  • Player busts (score < 0, score = 1, or finishes on non-double)
  • - *
  • Player wins (score = 0 on a double)
  • - *
  • Three darts have been thrown
  • - *
- *

- *

- * Reset to {@code false} when turn is submitted and next player's turn begins. - *

+ * Flag indicating turn has ended (bust, win, or 3 darts thrown). + * Prevents additional dart entry until turn is submitted. */ private boolean mIsTurnOver = false; /** - * Cached references to keyboard buttons as MaterialButtons for safe dynamic styling. - *

- * All 20 keyboard buttons (1-20) are stored in this list for efficient access - * during multiplier changes. This allows rapid visual updates (color, stroke, background) - * without repeated findViewById calls. - *

- *

- * Populated once during {@link #setupKeyboard()} and reused throughout the activity - * lifecycle. Each button's onClick listener is set to call {@link #onNumberTap(int)} - * with the corresponding number value. - *

+ * Cached references to keyboard buttons (1-20) for efficient styling updates. */ private final List mKeyboardButtons = new ArrayList<>(); @@ -315,159 +87,64 @@ public class GameActivity extends AppCompatActivity { // ======================================================================================== /** - * TextView displaying the active player's current remaining score. - *

- * Shows the number the player needs to reach zero (e.g., "301", "147", "32"). - * Updates after each dart and when switching players. Large, prominently displayed - * as it's the most critical piece of information during play. - *

+ * TextView displaying the active player's remaining score. */ private TextView tvScorePrimary; /** - * TextView displaying the active player's name. - *

- * Shows in uppercase (e.g., "JOHN DOE") to clearly identify whose turn it is. - * Updates when switching to the next player's turn. - *

+ * TextView displaying the active player's name (uppercase). */ private TextView tvPlayerName; /** - * TextView displaying the active player's current leg average. - *

- * Shows three-dart average for the current leg (e.g., "AVG: 85.3"). - * Calculated as: ((startingScore - remainingScore) / dartsThrown) × 3 - * Updates after each dart to provide real-time performance feedback. - *

+ * TextView displaying the active player's three-dart average. */ private TextView tvLegAvg; /** * TextView displaying the suggested checkout route. - *

- * Shows optimal finishing path when player is within checkout range (≤170). - * Examples: "D16", "T20 • D20", "T20 • T20 • BULL" - * Text is set by {@link CheckoutEngine#getRoute(int, int)}. - *

*/ private TextView tvCheckout; /** - * Container layout for the checkout suggestion display. - *

- * Visibility controlled based on whether a checkout route is available: - *

    - *
  • VISIBLE when score ≤ 170 and valid route exists
  • - *
  • GONE when score > 170 or no route available
  • - *
- * Animated with pulsing alpha effect when visible to draw attention. - *

+ * Container layout for checkout suggestion display. + * Visible when score ≤170 and route is available. */ private LinearLayout layoutCheckoutSuggestion; /** - * Button view for selecting single (1×) multiplier. - *

- * Visual state changes when selected (full opacity, active background). - * Default multiplier - automatically selected after each dart throw. - *

+ * Button for selecting single (1×) multiplier. */ private View btnSingle; /** - * Button view for selecting double (2×) multiplier. - *

- * Visual state changes when selected (full opacity, red background). - * Used for doubles ring on dartboard and finishing darts (Double Out). - *

+ * Button for selecting double (2×) multiplier. */ private View btnDouble; /** - * Button view for selecting triple (3×) multiplier. - *

- * Visual state changes when selected (full opacity, blue background). - * Used for triples ring on dartboard - highest scoring option. - *

+ * Button for selecting triple (3×) multiplier. */ private View btnTriple; /** - * Array of three TextViews showing the darts thrown in the current turn. - *

- * Each TextView displays a dart's score (e.g., "60", "DB", "25") and changes - * appearance based on whether it's been thrown: - *

    - *
  • Thrown: Active background, green text, score displayed
  • - *
  • Not thrown: Empty background, empty text
  • - *
- * Provides visual feedback of turn progress ("I've thrown 2 of 3 darts"). - *

+ * Array of three TextViews showing darts thrown in current turn. */ private TextView[] tvDartPills = new TextView[3]; /** - * GridLayout container holding all numeric keyboard buttons. - *

- * Holds 20 MaterialButton instances (1-20) arranged in a grid pattern for - * easy dart entry. Buttons are dynamically created in {@link #setupKeyboard()} - * and added to this layout. Separate from Bull button which is in layout XML. - *

+ * GridLayout container holding numeric keyboard buttons (1-20). */ private GridLayout glKeyboard; /** - * Static helper method to start GameActivity with the required game parameters. - *

- * This convenience method creates and configures the intent with all necessary extras, - * then starts the activity. Preferred over manually creating intents to ensure correct - * parameter passing. - *

- *

- * Usage Example: - *

-     * // Start a 501 game with two players
-     * ArrayList<Player> players = new ArrayList<>();
-     * players.add(player1);
-     * players.add(player2);
-     * GameActivity.start(this, players, 501);
-     * 
-     * // Start a 301 game
-     * GameActivity.start(this, selectedPlayers, 301);
-     * 
-     * // Start a 701 game
-     * GameActivity.start(this, tournamentPlayers, 701);
-     * 
- *

- *

- * Player Requirements: - * The players list should: - *

    - *
  • Contain at least 1 player (if empty, defaults to "GUEST 1")
  • - *
  • Have all players properly initialized with usernames
  • - *
  • Be an ArrayList to support Parcelable serialization
  • - *
- *

- *

- * Common Start Scores: - *

    - *
  • 501: Standard professional game length
  • - *
  • 301: Shorter casual game
  • - *
  • 701: Longer championship format
  • - *
- *

- * - * @param context The context from which to start the activity (Activity, Fragment context, etc.). - * Must not be null. - * @param players The list of players participating in the game. Can be null or empty (defaults - * to single guest player). Players should have valid usernames set. - * @param startScore The starting score for the X01 game. Typically 501, 301, or 701. - * Must be positive. - * @see #EXTRA_PLAYERS - * @see #EXTRA_START_SCORE + * Starts GameActivity with specified players and starting score. + * + * @param context The context from which to start the activity + * @param players The list of players (can be null/empty) + * @param startScore The starting score (typically 501, 301, or 701) */ - public static void start(Context context, ArrayList players, int startScore) { + public static void start(final Context context, final ArrayList players, final int startScore) { Intent intent = new Intent(context, GameActivity.class); intent.putParcelableArrayListExtra(EXTRA_PLAYERS, players); intent.putExtra(EXTRA_START_SCORE, startScore); @@ -475,26 +152,12 @@ public class GameActivity extends AppCompatActivity { } /** - * Called when the activity is first created. - *

- * Initializes the game by: - *

    - *
  1. Setting the content view from layout resource
  2. - *
  3. Extracting start score and player list from intent extras
  4. - *
  5. Initializing UI component references
  6. - *
  7. Setting up the numeric keyboard with buttons
  8. - *
  9. Creating player game states and displaying initial UI
  10. - *
- *

- *

- * If no starting score is provided in the intent, defaults to 501. - * If no players are provided, creates a single guest player. - *

- * - * @param savedInstanceState Bundle containing saved state (not currently used for restoration) + * Initializes the activity, extracts intent extras, sets up UI and game state. + * + * @param savedInstanceState Bundle containing saved state */ @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_game); @@ -509,20 +172,7 @@ public class GameActivity extends AppCompatActivity { } /** - * Initializes all UI component references and sets up click listeners. - *

- * This method: - *

    - *
  • Finds and caches references to all UI elements via findViewById
  • - *
  • Populates the dart pill TextView array
  • - *
  • Sets up click listeners for multiplier buttons (1×, 2×, 3×)
  • - *
  • Sets up click listeners for action buttons (Submit, Undo)
  • - *
- *

- *

- * Called once during {@link #onCreate(Bundle)} before any game logic runs. - * All UI components must exist in the layout or this will throw NullPointerException. - *

+ * Initializes UI component references and sets up click listeners. */ private void initViews() { tvScorePrimary = findViewById(R.id.tvScorePrimary); @@ -550,28 +200,7 @@ public class GameActivity extends AppCompatActivity { } /** - * Dynamically creates and configures the numeric keyboard buttons (1-20). - *

- * This method: - *

    - *
  • Clears any existing buttons from the keyboard grid
  • - *
  • Creates 20 MaterialButton instances via layout inflation
  • - *
  • Sets each button's text to its corresponding number
  • - *
  • Assigns onClick listeners to call {@link #onNumberTap(int)}
  • - *
  • Adds buttons to the keyboard GridLayout
  • - *
  • Caches button references in {@link #mKeyboardButtons} for styling
  • - *
- *

- *

- * Buttons are created dynamically rather than in XML to allow flexible styling - * and consistent appearance across all keyboard numbers. Uses view_keyboard_button - * layout resource as a template for each button. - *

- *

- * Called once during {@link #onCreate(Bundle)} to build the keyboard interface. - * The cached button list is used later by {@link #setMultiplier(int)} to update - * button colors based on selected multiplier. - *

+ * Dynamically creates and configures numeric keyboard buttons (1-20). */ private void setupKeyboard() { glKeyboard.removeAllViews(); @@ -594,35 +223,11 @@ public class GameActivity extends AppCompatActivity { /** * Initializes game state with player data and displays initial UI. - *

- * This method: - *

    - *
  • Creates {@link X01State} objects for each player with starting score
  • - *
  • Handles null or empty player lists by creating a default guest player
  • - *
  • Initializes the first player's turn
  • - *
  • Updates UI to show first player's information
  • - *
  • Sets multiplier to default (Single/1×)
  • - *
- *

- *

- * Player List Handling: - *

    - *
  • If players is null or empty: Creates single player "GUEST 1"
  • - *
  • If players provided: Creates state for each player in order
  • - *
- * Turn order matches the order of players in the list. - *

- *

- * Called once during {@link #onCreate(Bundle)} after UI initialization. - *

- * - * @param players List of Player objects participating in the game. Can be null or empty. - * Each player should have a valid username set. - * @see X01State - * @see #updateUI() - * @see #setMultiplier(int) + * Creates X01State for each player or a default guest player if list is empty. + * + * @param players List of Player objects (can be null/empty) */ - private void setupGame(List players) { + private void setupGame(final List players) { mPlayerStates = new ArrayList<>(); if (players != null && !players.isEmpty()) { for (Player p : players) { @@ -636,111 +241,12 @@ public class GameActivity extends AppCompatActivity { } /** - * Processes a single dart throw when a keyboard number button is tapped. - *

- * This is the core game logic method that handles: - *

    - *
  • Calculating final dart value (base × multiplier)
  • - *
  • Detecting and enforcing bust conditions
  • - *
  • Detecting and handling winning dart (Double Out)
  • - *
  • Updating turn state and UI for valid throws
  • - *
  • Preventing input when turn is complete
  • - *
- *

- *

- * Scoring Calculation: - *

-     * points = baseValue × multiplier
-     * Special case: Bull (25) with Triple (3×) = 50 (Double Bull)
-     * 
- *

- *

- * Bust Conditions (turn ends, score reverts): - *

    - *
  1. Negative Score: scoreAfterDart < 0
  2. - *
  3. Score of 1: scoreAfterDart == 1 (impossible to finish)
  4. - *
  5. Zero on Non-Double: scoreAfterDart == 0 but dart wasn't a double
  6. - *
- * When bust occurs: - *
    - *
  • Dart is added to turn darts (for display)
  • - *
  • Turn is marked as over (mIsTurnOver = true)
  • - *
  • Score does NOT change (reverts in submitTurn)
  • - *
  • Toast notification shown
  • - *
- *

- *

- * Win Condition (game ends): - * Player reaches exactly zero with a double or bullseye (50): - *

    - *
  • Dart is added to turn darts
  • - *
  • Turn is marked as over
  • - *
  • {@link #handleWin(X01State)} is called
  • - *
  • Activity finishes after displaying winner
  • - *
- *

- *

- * Valid Throw (continues turn): - *

    - *
  • Dart is added to turn darts
  • - *
  • Turn indicators updated to show dart
  • - *
  • UI updated with new score and checkout suggestions
  • - *
  • If 3 darts thrown, turn is marked as over
  • - *
- *

- *

- * Double Detection: - * A dart is considered a double if: - *

    - *
  • Multiplier is 2 (Double ring), OR
  • - *
  • Points = 50 (Double Bull)
  • - *
- *

- *

- * Multiplier Reset: - * After processing the dart, multiplier automatically resets to 1 (Single) - * for safety, preventing accidental double/triple throws. - *

- *

- * Example Scenarios: - *

-     * // Scenario 1: Valid throw
-     * Player on 301, multiplier=3, taps 20
-     * → points = 60, scoreAfter = 241, valid
-     * → Dart added, UI updated, continue turn
-     * 
-     * // Scenario 2: Bust (negative)
-     * Player on 32, multiplier=3, taps 20
-     * → points = 60, scoreAfter = -28, BUST
-     * → Turn over, score stays 32
-     * 
-     * // Scenario 3: Bust (score of 1)
-     * Player on 33, multiplier=1, taps 32
-     * → points = 32, scoreAfter = 1, BUST
-     * → Turn over, score stays 33
-     * 
-     * // Scenario 4: Bust (zero on single)
-     * Player on 20, multiplier=1, taps 20
-     * → points = 20, scoreAfter = 0, isDouble=false, BUST
-     * → Turn over, score stays 20
-     * 
-     * // Scenario 5: Win!
-     * Player on 20, multiplier=2, taps 10
-     * → points = 20, scoreAfter = 0, isDouble=true, WIN!
-     * → Game ends, winner announced
-     * 
- *

- * - * @param baseValue The face value of the number hit (1-20, or 25 for Bull). - * Must be positive. Multiplier is applied to calculate final points. - * @see #mMultiplier - * @see #mCurrentTurnDarts - * @see #mIsTurnOver - * @see #handleWin(X01State) - * @see #updateTurnIndicators() - * @see #updateUI() + * Processes a dart throw when a keyboard number is tapped. + * Handles scoring, bust detection, win detection, and UI updates. + * + * @param baseValue Face value of the number hit (1-20 or 25 for Bull) */ - public void onNumberTap(int baseValue) { + public void onNumberTap(final int baseValue) { if (mCurrentTurnDarts.size() >= 3 || mIsTurnOver) return; int points = baseValue * mMultiplier; @@ -782,102 +288,20 @@ public class GameActivity extends AppCompatActivity { } /** - * Handler for Bull button tap in the UI. - *

- * This is a convenience method that delegates to {@link #onNumberTap(int)} with - * the Bull's base value of 25. The actual scoring is calculated based on the - * current multiplier: - *

    - *
  • Single (1×): 25 points (outer bull)
  • - *
  • Double (2×): 50 points (double bull / bullseye)
  • - *
  • Triple (3×): 50 points (treated as double bull)
  • - *
- *

- *

- * Note: Triple Bull is not a standard darts scoring area, so it's treated as - * Double Bull (50 points) in the scoring logic. - *

- *

- * Can be called from XML layout via android:onClick="onBullTap" attribute. - *

- * - * @param v The View that was clicked (Bull button), not used in logic - * @see #onNumberTap(int) + * Handler for Bull button tap. Delegates to onNumberTap with base value 25. + * + * @param v The clicked View (Bull button) */ - public void onBullTap(View v) { + public void onBullTap(final View v) { onNumberTap(25); } /** - * Sets the current multiplier and updates all UI elements to reflect the selection. - *

- * This method handles both the logical state change and all visual feedback: - *

    - *
  • Updates {@link #mMultiplier} field
  • - *
  • Changes multiplier button appearances (opacity, backgrounds)
  • - *
  • Updates all keyboard button styles (colors, backgrounds, strokes)
  • - *
- *

- *

- * Visual Feedback by Multiplier: - *

    - *
  • Single (1×): - *
      - *
    • Button: Full opacity, default background
    • - *
    • Keyboard: Default colors, transparent backgrounds, subtle borders
    • - *
    - *
  • - *
  • Double (2×): - *
      - *
    • Button: Full opacity, red background
    • - *
    • Keyboard: Red text, light red backgrounds, red borders
    • - *
    • Color theme: #FF3B30 (double ring red)
    • - *
    - *
  • - *
  • Triple (3×): - *
      - *
    • Button: Full opacity, blue background
    • - *
    • Keyboard: Blue text, light blue backgrounds, blue borders
    • - *
    • Color theme: #007AFF (triple ring blue)
    • - *
    - *
  • - *
- * Inactive multiplier buttons are shown at 40% opacity. - *

- *

- * MaterialButton Styling: - * The method uses MaterialButton-specific APIs: - *

    - *
  • {@code setTextColor}: Changes button text color
  • - *
  • {@code setBackgroundColor}: Sets button fill color
  • - *
  • {@code setStrokeColor}: Sets button border color
  • - *
- * These provide consistent Material Design appearance across all keyboard buttons. - *

- *

- * Performance: - * Efficiently updates all 20 keyboard buttons using cached {@link #mKeyboardButtons} - * list, avoiding repeated findViewById calls. Style updates are instant with no - * noticeable lag. - *

- *

- * Automatic Reset: - * This method is automatically called with multiplier 1 after each dart throw - * (in {@link #onNumberTap(int)}), ensuring players must explicitly select Double - * or Triple for each dart. - *

- * - * @param m The multiplier value to set. Valid values: - *
    - *
  • 1 = Single
  • - *
  • 2 = Double
  • - *
  • 3 = Triple
  • - *
- * Other values will cause unexpected visual behavior. - * @see #mMultiplier - * @see #mKeyboardButtons + * Sets the multiplier and updates UI elements (button appearances and keyboard colors). + * + * @param m The multiplier value (1=Single, 2=Double, 3=Triple) */ - private void setMultiplier(int m) { + private void setMultiplier(final int m) { mMultiplier = m; btnSingle.setAlpha(m == 1 ? 1.0f : 0.4f); btnDouble.setAlpha(m == 2 ? 1.0f : 0.4f); @@ -910,62 +334,8 @@ public class GameActivity extends AppCompatActivity { } /** - * Finalizes the current turn and advances to the next player. - *

- * This method is called when the user explicitly taps "Submit Turn" button or - * when a turn ends automatically (3 darts thrown, bust, or win). It handles: - *

    - *
  • Calculating total points scored in the turn
  • - *
  • Verifying bust conditions (redundant safety check)
  • - *
  • Updating player's score and darts thrown count
  • - *
  • Rotating to next player's turn
  • - *
  • Resetting turn state for new player
  • - *
  • Updating UI to show next player's information
  • - *
- *

- *

- * Turn Processing: - *

    - *
  1. Calculate turn total by summing all dart values
  2. - *
  3. Calculate what final score would be after turn
  4. - *
  5. Perform bust check (redundant as {@link #onNumberTap(int)} already validates)
  6. - *
  7. If not bust: Update player's remaining score and darts thrown count
  8. - *
  9. If bust: Score remains unchanged (darts thrown not counted)
  10. - *
- *

- *

- * Player Rotation: - *

-     * mActivePlayerIndex = (mActivePlayerIndex + 1) % playerCount
-     * 
- * This cycles through players: 0 → 1 → 2 → 0 → 1 → ... - *

- *

- * State Reset: - * After submission: - *

    - *
  • {@link #mCurrentTurnDarts} is cleared
  • - *
  • {@link #mIsTurnOver} is reset to false
  • - *
  • UI is updated to show next player
  • - *
  • Turn indicators are cleared
  • - *
- *

- *

- * Safety Check: - * Returns immediately if no darts have been thrown (empty turn), preventing - * submission of zero-dart turns. - *

- *

- * Note: The {@code isFinishDart} check is largely redundant as win conditions - * are already handled in {@link #onNumberTap(int)} and would have ended the game - * before reaching this method. - *

- * - * @see #mActivePlayerIndex - * @see #mCurrentTurnDarts - * @see #mIsTurnOver - * @see #updateUI() - * @see #updateTurnIndicators() + * Finalizes current turn and advances to next player. + * Updates player score (unless bust), rotates to next player, resets turn state. */ private void submitTurn() { // Don't submit if no darts thrown @@ -1005,88 +375,21 @@ public class GameActivity extends AppCompatActivity { } /** - * Determines if a specific dart in the current turn sequence was a finishing dart. - *

- * This is a placeholder method for future enhancement. Currently always returns - * {@code true} because win logic is handled immediately in {@link #onNumberTap(int)} - * when the dart is thrown. - *

- *

- * Current Limitation: - * The implementation doesn't track which multiplier was used for each dart in the - * turn, making it impossible to retrospectively determine if a dart was a double. - * The immediate win detection in {@code onNumberTap} makes this method largely - * unnecessary in current flow. - *

- *

- * Future Enhancement: - * To properly implement this method, would need to: - *

    - *
  • Store multiplier with each dart value in a data structure like - * {@code List>} (value, multiplier)
  • - *
  • Check if the dart at dartIndex had multiplier of 2 or was value 50
  • - *
  • Use this for post-turn validation or turn history display
  • - *
- *

- * - * @param dartIndex The index of the dart in {@link #mCurrentTurnDarts} to check (0-2) - * @return Currently always returns {@code true}. Should return {@code true} if the - * dart was a double or bullseye, {@code false} otherwise. - * @see #onNumberTap(int) - * @see #submitTurn() + * Placeholder method to determine if a dart was a finishing dart. + * Currently always returns true. + * + * @param dartIndex Index of dart in current turn (0-2) + * @return Always true in current implementation */ - private boolean isFinishDart(int dartIndex) { + private boolean isFinishDart(final int dartIndex) { // In this UI implementation, we'd need to track multipliers per dart if we wanted // to check history post-hoc. For now, onNumberTap handles immediate win logic. return true; } /** - * Removes the most recently thrown dart from the current turn. - *

- * This method provides undo functionality, allowing players to correct mistakes - * during their turn. It: - *

    - *
  • Removes the last dart from {@link #mCurrentTurnDarts}
  • - *
  • Resets {@link #mIsTurnOver} flag (allows more darts to be thrown)
  • - *
  • Updates turn indicators to remove the dart pill display
  • - *
  • Updates UI to recalculate score and checkout suggestions
  • - *
- *

- *

- * Use Cases: - *

    - *
  • Player accidentally tapped wrong number
  • - *
  • Player selected wrong multiplier before tapping
  • - *
  • Scorer recorded incorrect dart
  • - *
  • Player wants to review before submitting turn
  • - *
- *

- *

- * Limitations: - *

    - *
  • Only affects current turn (can't undo previous turns)
  • - *
  • Only removes one dart at a time (must call multiple times for multiple darts)
  • - *
  • Does nothing if turn is empty (safe to call repeatedly)
  • - *
- *

- *

- * Example Usage: - *

-     * Turn state: [60, 60, 20] (T20, T20, D10)
-     * User taps Undo
-     * New state: [60, 60] (last dart removed)
-     * Turn can continue with new dart entry
-     * 
- *

- *

- * Triggered by the "Undo" button in the UI. - *

- * - * @see #mCurrentTurnDarts - * @see #mIsTurnOver - * @see #updateTurnIndicators() - * @see #updateUI() + * Removes the most recently thrown dart from current turn. + * Allows turn to continue and updates UI. */ private void undoLastDart() { if (!mCurrentTurnDarts.isEmpty()) { @@ -1103,63 +406,8 @@ public class GameActivity extends AppCompatActivity { } /** - * Updates all UI elements to reflect the current game state. - *

- * This method refreshes all dynamic UI components based on the active player's - * current state and the darts thrown so far in the turn. It: - *

    - *
  • Updates primary score display with remaining score
  • - *
  • Updates player name display
  • - *
  • Calculates and displays current three-dart average
  • - *
  • Calculates current target score (accounting for turn darts)
  • - *
  • Updates checkout route suggestions
  • - *
- *

- *

- * Three-Dart Average Calculation: - *

-     * Average = ((startingScore - remainingScore) / dartsThrown) × 3
-     * 
-     * Example:
-     * Starting score: 501
-     * Remaining score: 201
-     * Darts thrown: 30
-     * Points scored: 501 - 201 = 300
-     * Average: (300 / 30) × 3 = 30.0
-     * 
- * Displayed as "AVG: 30.0" in the UI. - *

- *

- * Current Target Calculation: - * The target score considers darts thrown but not yet submitted: - *

-     * currentTarget = remainingScore - sumOfCurrentTurnDarts
-     * 
- * This allows checkout suggestions to update in real-time as darts are thrown. - *

- *

- * When Called: - *

    - *
  • After each valid dart throw
  • - *
  • When switching to next player's turn
  • - *
  • After undoing a dart
  • - *
  • During initial game setup
  • - *
- *

- *

- * Performance: - * This method is called frequently during gameplay, so it's optimized to: - *

    - *
  • Only update TextViews (fast operations)
  • - *
  • Perform minimal calculations
  • - *
  • Not trigger layout recalculations
  • - *
- *

- * - * @see X01State - * @see #mActivePlayerIndex - * @see #mCurrentTurnDarts - * @see #updateCheckoutSuggestion(int, int) + * Updates UI elements to reflect current game state. + * Refreshes score, player name, average, and checkout suggestions. */ private void updateUI() { X01State active = mPlayerStates.get(mActivePlayerIndex); @@ -1186,70 +434,13 @@ public class GameActivity extends AppCompatActivity { } /** - * Updates the checkout route suggestion display based on the current score and darts left. - *

- * This method determines whether to show a checkout suggestion and what route to display. - * The checkout engine provides optimal finishing paths when a score is within achievable - * range (≤170 points with available darts). - *

- *

- * Visibility Conditions: - * Checkout suggestion is shown when ALL conditions are met: - *

    - *
  • Score is ≤ 170 (maximum 3-dart checkout)
  • - *
  • Score is > 1 (score of 1 is impossible to finish)
  • - *
  • At least 1 dart remaining in turn
  • - *
  • {@link CheckoutEngine} returns a valid route (not null)
  • - *
- * Otherwise, the suggestion is hidden. - *

- *

- * Visual Effects: - * When a checkout route is available: - *

    - *
  • Suggestion layout becomes VISIBLE
  • - *
  • Route text is displayed (e.g., "T20 • D20", "D16")
  • - *
  • Pulsing animation starts (alpha fades 0.5 → 1.0 → 0.5 continuously)
  • - *
  • Animation draws attention to the suggested route
  • - *
- * When no route is available: - *
    - *
  • Animation is stopped
  • - *
  • Suggestion layout becomes GONE
  • - *
- *

- *

- * Example Scenarios: - *

-     * Score = 32, Darts = 1 → Shows "D16"
-     * Score = 40, Darts = 2 → Shows "D20" or setup route
-     * Score = 170, Darts = 3 → Shows "T20 • T20 • BULL"
-     * Score = 1, Darts = 3 → Hidden (impossible)
-     * Score = 180, Darts = 3 → Hidden (too high)
-     * 
- *

- *

- * Animation Details: - * The pulsing animation: - *

    - *
  • Duration: 1000ms per cycle
  • - *
  • Range: 50% to 100% opacity
  • - *
  • Mode: REVERSE (fades in and out smoothly)
  • - *
  • Repeat: INFINITE (continues until hidden)
  • - *
- *

- *

- * Called automatically by {@link #updateUI()} whenever game state changes. - *

- * - * @param score The target score to finish, accounting for darts already thrown in - * current turn. Must be non-negative. - * @param dartsLeft The number of darts remaining in the current turn (0-3). - * @see CheckoutEngine#getRoute(int, int) - * @see #layoutCheckoutSuggestion - * @see #tvCheckout + * Updates checkout route suggestion display based on score and darts remaining. + * Shows pulsing animation when route is available. + * + * @param score Target score to finish + * @param dartsLeft Number of darts remaining (0-3) */ - private void updateCheckoutSuggestion(int score, int dartsLeft) { + private void updateCheckoutSuggestion(final int score, final int dartsLeft) { if (score <= 170 && score > 1 && dartsLeft > 0) { String route = CheckoutEngine.getRoute(score, dartsLeft); @@ -1270,62 +461,8 @@ public class GameActivity extends AppCompatActivity { } /** - * Updates the three dart indicator pills to show the current turn's dart values. - *

- * This method provides visual feedback for the darts thrown in the current turn by - * updating the three "pill" TextViews. Each pill can be in one of two states: - *

    - *
  • Active (dart thrown): Shows dart value, green text, active background
  • - *
  • Empty (dart not thrown): Blank text, empty/inactive background
  • - *
- *

- *

- * Visual States: - *

-     * Dart not thrown: [   ] (empty background)
-     * Dart thrown:     [60] (green text, active background)
-     * 
- *

- *

- * Display Labels: - * Dart values are formatted for clarity: - *

    - *
  • Regular scores: Numeric value (e.g., "60", "20", "15")
  • - *
  • Single Bull: "B" (25 points)
  • - *
  • Double Bull: "DB" (50 points)
  • - *
- *

- *

- * Example Progression: - *

-     * No darts:    [   ] [   ] [   ]
-     * 1 dart:      [60] [   ] [   ]
-     * 2 darts:     [60] [60] [   ]
-     * 3 darts:     [60] [60] [20]
-     * After submit:[   ] [   ] [   ]  (cleared for next turn)
-     * 
- *

- *

- * When Called: - *

    - *
  • After each dart is thrown ({@link #onNumberTap(int)})
  • - *
  • After undoing a dart ({@link #undoLastDart()})
  • - *
  • After submitting a turn ({@link #submitTurn()})
  • - *
- *

- *

- * The visual feedback helps players: - *

    - *
  • See what they've scored so far in the turn
  • - *
  • Know how many darts they have left (empty pills)
  • - *
  • Verify dart entry before submitting
  • - *
  • Calculate turn totals mentally
  • - *
- *

- * - * @see #mCurrentTurnDarts - * @see #tvDartPills - * @see #getDartLabel(int) + * Updates three dart indicator pills to show current turn's dart values. + * Shows active state with green text for thrown darts, empty for not thrown. */ private void updateTurnIndicators() { for (int i = 0; i < 3; i++) { @@ -1343,36 +480,12 @@ public class GameActivity extends AppCompatActivity { } /** - * Converts a dart point value into a display-friendly label. - *

- * This method formats dart scores for clarity in the turn indicator pills, - * using special abbreviations for Bull scores to save space and improve - * readability. - *

- *

- * Label Formats: - *

    - *
  • 50: "DB" (Double Bull / Bullseye)
  • - *
  • 25: "B" (Bull / Outer Bull)
  • - *
  • Other scores: Numeric string (e.g., "60", "20", "15")
  • - *
- *

- *

- * Example Conversions: - *

-     * getDartLabel(50) → "DB"
-     * getDartLabel(25) → "B"
-     * getDartLabel(60) → "60"
-     * getDartLabel(20) → "20"
-     * getDartLabel(1) → "1"
-     * 
- *

- * - * @param score The dart's point value to format. Typically 0-60, 25, or 50. - * @return A string label suitable for display in UI. Never returns null. - * @see #updateTurnIndicators() + * Converts dart point value to display label (50="DB", 25="B", else numeric). + * + * @param score Dart's point value + * @return Display label for UI */ - private String getDartLabel(int score) { + private String getDartLabel(final int score) { if (score == 50) return "DB"; // Double Bull / Bullseye if (score == 25) return "B"; // Single Bull // Return numeric value for all other scores @@ -1380,40 +493,12 @@ public class GameActivity extends AppCompatActivity { } /** - * Handles the win condition when a player finishes the game. - *

- * This method is called from {@link #onNumberTap(int)} when a player successfully - * finishes on exactly zero with a double. It: - *

    - *
  • Displays a toast notification announcing the winner
  • - *
  • Finishes the activity, returning to previous screen
  • - *
- *

- *

- * Current Implementation: - * The current implementation is minimal and immediately ends the game. Future - * enhancements could include: - *

    - *
  • Displaying a more elaborate win screen
  • - *
  • Showing final game statistics
  • - *
  • Playing win sound effects or animations
  • - *
  • Saving match results to database
  • - *
  • Asking if players want a rematch
  • - *
  • Updating player career statistics
  • - *
- *

- *

- * Toast Duration: - * Uses LENGTH_LONG (approximately 3.5 seconds) to ensure players see the win - * message before the activity closes. - *

- * - * @param winner The {@link X01State} of the player who won the game. Used to - * display the winner's name in the toast notification. - * @see X01State - * @see #onNumberTap(int) + * Handles win condition when a player finishes on zero with a double. + * Displays win toast and finishes activity. + * + * @param winner X01State of the winning player */ - private void handleWin(X01State winner) { + private void handleWin(final X01State winner) { // Show win notification Toast.makeText(this, winner.name + " WINS!", Toast.LENGTH_LONG).show(); @@ -1428,94 +513,32 @@ public class GameActivity extends AppCompatActivity { } /** - * Internal state holder for a single player's X01 game progress. - *

- * This lightweight class tracks all necessary information for one player during - * an X01 game. Each player in the game has their own X01State instance stored - * in the {@link #mPlayerStates} list. - *

- *

- * Fields: - *

    - *
  • name: Player's display name (from Player.username)
  • - *
  • remainingScore: Current score (starts at game's starting score)
  • - *
  • dartsThrown: Total number of darts thrown so far (for average calc)
  • - *
- *

- *

- * Usage Example: - *

-     * X01State player1 = new X01State("John Doe", 501);
-     * // Player throws T20, T20, T20 (180 points)
-     * player1.remainingScore -= 180;  // Now 321
-     * player1.dartsThrown += 3;
-     * 
-     * // Calculate three-dart average
-     * double avg = ((501 - player1.remainingScore) / player1.dartsThrown) * 3;
-     * 
- *

- *

- * State Lifecycle: - *

    - *
  1. Created in {@link #setupGame(List)} with starting score
  2. - *
  3. Updated throughout game as darts are thrown
  4. - *
  5. Used to calculate averages and display current score
  6. - *
  7. Discarded when activity ends
  8. - *
- *

- *

- * Note: This is an inner class (not static) but could be made static as it - * doesn't access outer class members. - *

- * - * @see #mPlayerStates - * @see #setupGame(List) - * @see #updateUI() + * State holder for a single player's X01 game progress. + * Tracks name, remaining score, and darts thrown. */ private static class X01State { /** - * The player's display name. - *

- * Copied from {@link Player#username} during game initialization. - * Used for UI display and win announcements. - *

+ * Player's display name. */ String name; /** - * The player's current remaining score. - *

- * Starts at the game's starting score (e.g., 501) and decreases as darts - * are scored. Game is won when this reaches exactly 0 with a double. - *

- *

- * Updated in {@link GameActivity#submitTurn()} after each turn (unless bust). - *

+ * Player's current remaining score. */ int remainingScore; /** - * Total number of darts thrown by this player so far in the game. - *

- * Used to calculate the three-dart average: - *

-         * average = ((startScore - remainingScore) / dartsThrown) × 3
-         * 
- *

- *

- * Incremented in {@link GameActivity#submitTurn()} after each valid turn. - * Bust turns don't increment this counter. - *

+ * Total darts thrown by this player (for average calculation). */ int dartsThrown = 0; /** - * Constructs a new X01State for a player. - * - * @param name The player's display name - * @param startScore The game's starting score (e.g., 501, 301, 701) + * Constructs X01State for a player. + * + * @param name Player's display name + * @param startScore Starting score for the game */ - X01State(String name, int startScore) { + X01State(final String name, final int startScore) { this.name = name; this.remainingScore = startScore; } @@ -1563,21 +586,7 @@ public class GameActivity extends AppCompatActivity { */ private static class CheckoutEngine { /** - * Map of pre-calculated checkout routes for classic finishes. - *

- * Key: Target score (e.g., 170, 141) - * Value: Array of dart descriptions (e.g., ["T20", "T20", "BULL"]) - *

- *

- * Current Routes: - *

    - *
  • 170: T20 • T20 • BULL (maximum 3-dart checkout)
  • - *
  • 141: T20 • T19 • D12 (common high finish)
  • - *
- *

- *

- * Additional routes can be added for other common finishes (e.g., 167, 164, 160). - *

+ * Pre-calculated checkout routes for classic finishes. */ private static final Map checkoutMap = new HashMap<>(); @@ -1593,84 +602,14 @@ public class GameActivity extends AppCompatActivity { } /** - * Returns an optimal checkout route for the given score and darts remaining. - *

- * This method implements sophisticated checkout logic that considers: - *

    - *
  • Whether the score can be finished directly with a double
  • - *
  • If setup darts are needed to reach a double
  • - *
  • Avoiding leaving a score of 1 (impossible to finish)
  • - *
  • Pre-calculated routes for classic high finishes
  • - *
- *

- *

- * Return Format: - * Returns strings formatted as: - *

    - *
  • Single dart: "D16", "BULL"
  • - *
  • Multiple darts: "T20 • D20", "1 • D20", "T20 • T20 • BULL"
  • - *
  • Generic advice: "T20 Route" (for high scores)
  • - *
- * The bullet character (•) separates individual darts in multi-dart routes. - *

- *

- * Algorithm Steps: - *

    - *
  1. Check for direct double finish (score ≤40, even) → "D[N]"
  2. - *
  3. Check for bullseye finish (score = 50) → "BULL"
  4. - *
  5. If multiple darts available and odd score: - *
      - *
    • Try to leave common double (32→D16, 40→D20, 16→D8)
    • - *
    • Default: suggest 1 to leave even score for double
    • - *
    - *
  6. - *
  7. Check pre-calculated routes map
  8. - *
  9. Fallback to generic "T20 Route" for high scores (>60)
  10. - *
  11. Return null if no route available
  12. - *
- *

- *

- * Setup Dart Logic: - * When score is odd with 2+ darts left, suggests setup darts to leave doubles: - *

-         * Score 41: Suggest "1 • D20" (leaves 40)
-         * Score 39: Suggest "7 • D16" (leaves 32)
-         * Score 47: Suggest "7 • D20" (leaves 40)
-         * 
- * Prioritizes leaving 32 (D16) or 40 (D20) as these are common target doubles. - *

- *

- * Example Calls: - *

-         * CheckoutEngine.getRoute(32, 1);   // Returns "D16"
-         * CheckoutEngine.getRoute(50, 1);   // Returns "BULL"
-         * CheckoutEngine.getRoute(40, 2);   // Returns "D20"
-         * CheckoutEngine.getRoute(41, 2);   // Returns "1 • D20"
-         * CheckoutEngine.getRoute(170, 3);  // Returns "T20 • T20 • BULL"
-         * CheckoutEngine.getRoute(180, 3);  // Returns null (impossible)
-         * CheckoutEngine.getRoute(100, 2);  // Returns "T20 Route"
-         * CheckoutEngine.getRoute(1, 1);    // Returns null (impossible)
-         * 
- *

- *

- * Limitations: - *

    - *
  • Doesn't verify if player has the skill to hit suggested route
  • - *
  • Setup dart logic is simplified (doesn't consider all optimal routes)
  • - *
  • Pre-calculated map is incomplete (only has a few common finishes)
  • - *
  • "T20 Route" is vague advice for complex high-score situations
  • - *
- *

- * - * @param score The target score to finish. Must be positive. Scores > 170 typically - * return null as they're not achievable in 3 darts. - * @param dartsLeft Number of darts remaining in the turn (1-3). With 0 darts, - * returns null. - * @return A string describing the optimal checkout route, or null if no route - * is available or the score is impossible to finish with the given darts. - * @see #checkoutMap + * 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(int score, int dartsLeft) { + public static String getRoute(final int score, final int dartsLeft) { // 1. Direct Out check (highest priority) if (score <= 40 && score % 2 == 0) return "D" + (score / 2); if (score == 50) return "BULL"; 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 08f9100..1baf4ba 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/MainMenuActivity.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/MainMenuActivity.java @@ -24,74 +24,36 @@ import java.util.ArrayList; import java.util.List; /** - * MainMenuActivity serves as the primary entry point and main screen of the Oche Companion application. - *

- * This activity provides the following functionality: - *

    - *
  • Displays the squad of players in a RecyclerView
  • - *
  • Allows adding new players to the squad
  • - *
  • Shows a match recap view with test data for development purposes
  • - *
  • Manages the application's database connection
  • - *
- *

- * - * @see AppCompatActivity - * @see AppDatabase - * @see MatchRecapView - * @see MainMenuPlayerAdapter - * @author Oche Companion Development Team - * @version 1.0 - * @since 1.0 + * Main entry point and home screen of the Oche Companion application. + * Displays the squad of players, allows adding new players, and shows match recap with test data. */ public class MainMenuActivity extends AppCompatActivity { /** - * Tag for debugging purposes. - * Used for logging and identifying this activity in debug output. + * Tag for debugging and logging purposes. */ private static final String TAG = "MainMenuActivity"; /** - * Custom view component that displays a summary of a match. - *

- * This view shows match information including players, scores, and match status. - * It can be clicked to cycle through different test data states for development purposes. - *

- * - * @see MatchRecapView + * Custom view component that displays a match summary. + * Can be clicked to cycle through different test data states. */ private MatchRecapView mMatchRecap; /** - * Counter used for cycling through different test data scenarios. - *

- * This counter increments each time the match recap view is clicked, allowing - * developers to cycle through different test states (null match, 1v1 match, group match). - * The counter value modulo 3 determines which test scenario is displayed. - *

+ * Counter for cycling through different test data scenarios. + * Increments on each click to cycle through null match, 1v1 match, and group match states. */ - private int testCounter = 0; + private int mTestCounter = 0; /** - * Called when the activity is first created. - *

- * This method performs the following initialization tasks: - *

    - *
  • Enables edge-to-edge display for modern Android UI
  • - *
  • Sets the activity's content view to the main layout
  • - *
  • Configures window insets for proper system bar handling
  • - *
  • Initializes the database connection
  • - *
  • Sets up the match recap view with a test data click listener
  • - *
- *

- * - * @param savedInstanceState If the activity is being re-initialized after previously being shut down, - * this Bundle contains the data it most recently supplied in - * {@link #onSaveInstanceState(Bundle)}. Otherwise, it is null. - * @see AppCompatActivity#onCreate(Bundle) + * Initializes the activity: enables edge-to-edge display, configures window insets, + * and sets up the match recap view with test data click listener. + * + * @param savedInstanceState Bundle containing saved state, or null if none exists. */ @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Enable edge-to-edge display for immersive UI experience @@ -111,22 +73,16 @@ public class MainMenuActivity extends AppCompatActivity { mMatchRecap = findViewById(R.id.match_recap); mMatchRecap.setOnClickListener(new View.OnClickListener() { @Override - public void onClick(View v) { + public void onClick(final View v) { // Cycle through test data scenarios on each click - applyTestData(testCounter); - testCounter++; + applyTestData(mTestCounter); + mTestCounter++; } }); } /** - * Called after {@link #onStart} when the activity is becoming visible to the user. - *

- * This method is called every time the activity comes to the foreground, making it - * the ideal place to refresh the squad view with the latest player data from the database. - *

- * - * @see AppCompatActivity#onResume() + * Refreshes the squad view with latest player data from the database when activity resumes. */ @Override protected void onResume() { @@ -137,38 +93,8 @@ public class MainMenuActivity extends AppCompatActivity { /** * Initiates a quick-start 501 game with two test players. - *

- * This convenience method creates two test players with minimal configuration - * and immediately launches a 501 game. It's designed for rapid testing and - * development, allowing developers or users to quickly jump into a game without - * setting up real player profiles. - *

- *

- * The method: - *

    - *
  • Creates two test players named "Test1" and "Test2" with no profile pictures
  • - *
  • Adds them to a player list
  • - *
  • Launches {@link GameActivity} with a starting score of 501
  • - *
- *

- *

- * Use Cases: - *

    - *
  • Quick testing of game functionality during development
  • - *
  • Demonstrating the app without setting up a full squad
  • - *
  • Rapid game start for users who want to practice scoring
  • - *
- *

- *

- * Note: The test players created here are not persisted to the database. - * They exist only for the duration of the game session. - *

- *

- * Triggered by the quick start button in the main menu UI. - *

- * - * @see GameActivity#start(android.content.Context, ArrayList, int) - * @see Player + * Creates test players "Test1" and "Test2" and launches GameActivity. + * Test players are not persisted to the database. */ private void quickStart() { final Player playerOne = new Player("Test1", null); @@ -181,26 +107,8 @@ public class MainMenuActivity extends AppCompatActivity { } /** - * Initializes and configures the squad view component. - *

- * This method performs the following operations: - *

    - *
  • Retrieves references to UI components (add player button and squad RecyclerView)
  • - *
  • Sets up the RecyclerView with a LinearLayoutManager
  • - *
  • Initializes and attaches the MainMenuPlayerAdapter to the RecyclerView
  • - *
  • Configures the add player button to launch the AddPlayerActivity
  • - *
  • Loads all players from the database on a background thread
  • - *
  • Updates the adapter with player data on the UI thread
  • - *
- *

- *

- * Note: Database operations are performed on a background thread to prevent - * blocking the main UI thread, which ensures the application remains responsive. - *

- * - * @see MainMenuPlayerAdapter - * @see AddPlayerActivity - * @see LinearLayoutManager + * Initializes the squad view: sets up the RecyclerView with adapter, + * configures the add player button, and loads all players from the database on a background thread. */ private void initSquadView() { // Get references to UI components @@ -237,26 +145,11 @@ public class MainMenuActivity extends AppCompatActivity { /** - * Applies test data to the match recap view for development and testing purposes. - *

- * This method creates sample player and match objects and cycles through different - * display states based on the provided counter value: - *

    - *
  • When counter % 3 == 0: Displays null (no match)
  • - *
  • When counter % 3 == 1: Displays a 1v1 match (two players)
  • - *
  • When counter % 3 == 2: Displays a group match (four players)
  • - *
- *

- *

- * Note: This method is intended for development and testing only - * and should be removed or disabled in production builds. - *

- * - * @param counter The counter value used to determine which test scenario to display. - * The value is evaluated using modulo 3 to cycle through test states. - * @see Match - * @see Player - * @see MatchRecapView#setMatch(Match) + * Applies test data to the match recap view for development and testing. + * Cycles through null match (counter % 3 == 0), 1v1 match (counter % 3 == 1), + * and group match (counter % 3 == 2) based on the counter value. + * + * @param counter Counter value used to determine which test scenario to display. */ private void applyTestData(final int counter) { // Create test player objects 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 f2a78db..ed0d386 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 @@ -11,847 +11,62 @@ import com.aldo.apps.ochecompanion.database.objects.Match; import com.aldo.apps.ochecompanion.database.objects.Player; /** - * The main Room database class for the Oche Companion darts application. - *

- * This abstract class serves as the central database access point, managing all - * data persistence for players, matches, and related statistics. It implements - * the Singleton design pattern to ensure only one database instance exists - * throughout the application lifecycle, preventing resource conflicts and ensuring - * data consistency. - *

- *

- * Room Database Architecture: - * Room is Android's SQLite object-relational mapping (ORM) library that provides - * an abstraction layer over SQLite, offering: - *

    - *
  • Compile-time SQL query verification
  • - *
  • Convenient annotation-based entity and DAO definitions
  • - *
  • Automatic conversion between SQLite and Java/Kotlin objects
  • - *
  • LiveData and Flow support for reactive UI updates
  • - *
  • Migration support for database schema changes
  • - *
- *

- *

- * Database Entities: - * This database manages two primary tables: - *

    - *
  • players: Squad roster with player profiles and career statistics - * (defined by {@link Player} entity)
  • - *
  • matches: Completed match records with participant data and results - * (defined by {@link Match} entity)
  • - *
- *

- *

- * Database Access Objects (DAOs): - * Database operations are performed through specialized DAO interfaces: - *

    - *
  • {@link PlayerDao}: CRUD operations for player management (insert, update, query)
  • - *
  • {@link MatchDao}: Match record operations (insert, query all, query last match)
  • - *
- *

- *

- * Singleton Pattern Implementation: - * The class uses the thread-safe double-checked locking pattern to ensure a single - * database instance. This approach: - *

    - *
  • Prevents multiple database connections that waste resources
  • - *
  • Ensures data consistency across the application
  • - *
  • Reduces memory overhead by sharing one connection pool
  • - *
  • Thread-safe initialization prevents race conditions
  • - *
- *

- *

- * Usage Example: - *

- * // Get database instance (can be called from any activity/fragment)
- * AppDatabase db = AppDatabase.getDatabase(context);
- * 
- * // Access DAOs for database operations
- * PlayerDao playerDao = db.playerDao();
- * MatchDao matchDao = db.matchDao();
- * 
- * // Perform database operations (must be on background thread)
- * new Thread(() -> {
- *     // Insert a new player
- *     Player player = new Player("John Doe", "/path/to/pic.jpg");
- *     playerDao.insert(player);
- *     
- *     // Query all players
- *     List<Player> allPlayers = playerDao.getAllPlayers();
- *     
- *     // Insert a match
- *     Match match = new Match(System.currentTimeMillis(), "501", 2, participantJson);
- *     matchDao.insert(match);
- *     
- *     // Get recent matches
- *     List<Match> recentMatches = matchDao.getAllMatches();
- * }).start();
- * 
- *

- *

- * Database Versioning: - * Currently at version 2, indicating one schema change since initial creation. - * Version increments are required when: - *

    - *
  • Adding or removing tables
  • - *
  • Adding or removing columns
  • - *
  • Changing column types or constraints
  • - *
  • Modifying primary keys or indices
  • - *
- * The {@code fallbackToDestructiveMigration()} strategy means schema changes will - * drop and recreate all tables, losing existing data. This is acceptable during - * development but should be replaced with proper migrations for production. - *

- *

- * Migration Strategy: - * For production releases, replace destructive migration with proper migration paths: - *

- * static final Migration MIGRATION_1_2 = new Migration(1, 2) {
- *     @Override
- *     public void migrate(@NonNull SupportSQLiteDatabase database) {
- *         // Example: Add new column to players table
- *         database.execSQL("ALTER TABLE players ADD COLUMN wins INTEGER NOT NULL DEFAULT 0");
- *     }
- * };
- * 
- * INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
- *                 AppDatabase.class, "oche_companion_db")
- *         .addMigrations(MIGRATION_1_2)  // Add migration instead of destructive
- *         .build();
- * 
- *

- *

- * Database File Location: - * The SQLite database file "oche_companion_db" is stored in the app's private - * storage at: - *

- * /data/data/com.aldo.apps.ochecompanion/databases/oche_companion_db
- * 
- * This location is: - *
    - *
  • Private to the application (other apps cannot access)
  • - *
  • Automatically backed up with Auto Backup (if enabled)
  • - *
  • Cleared when the app is uninstalled
  • - *
  • Accessible for inspection via Android Studio Database Inspector
  • - *
- *

- *

- * Schema Export: - * The {@code exportSchema = false} setting disables automatic schema JSON export. - * For production apps, consider enabling this: - *

- * @Database(entities = {...}, version = 2, exportSchema = true)
- * 
- * And specify the export directory in build.gradle: - *
- * android {
- *     defaultConfig {
- *         javaCompileOptions {
- *             annotationProcessorOptions {
- *                 arguments += ["room.schemaLocation": "$projectDir/schemas"]
- *             }
- *         }
- *     }
- * }
- * 
- * This creates version-controlled schema files for tracking changes and testing migrations. - *

- *

- * Threading Requirements: - * Room enforces that database operations occur on background threads: - *

    - *
  • Attempting database operations on the main thread throws {@link IllegalStateException}
  • - *
  • Use {@link Thread}, {@link java.util.concurrent.ExecutorService}, or Kotlin coroutines
  • - *
  • UI updates after database operations must use {@code runOnUiThread()} or similar
  • - *
  • Consider using LiveData or Flow for automatic thread handling and UI updates
  • - *
- *

- *

- * Database Inspection: - * For debugging, you can inspect the database using: - *

    - *
  • Android Studio Database Inspector: View > Tool Windows > Database Inspector
  • - *
  • ADB: {@code adb shell} and SQLite command line
  • - *
  • Third-party tools: DB Browser for SQLite (requires root or backup)
  • - *
- *

- *

- * Performance Optimization: - *

    - *
  • Database queries are optimized with SQLite indices on primary keys
  • - *
  • Connection pooling is handled automatically by Room
  • - *
  • Consider adding custom indices for frequently queried columns: - *
    @Entity(indices = {@Index(value = {"username"})})
  • - *
  • Use transactions for batch operations to improve performance
  • - *
  • Avoid N+1 query problems by using JOIN queries or @Relation
  • - *
- *

- *

- * Testing: - * For unit testing, create an in-memory database: - *

- * @Before
- * public void createDb() {
- *     Context context = ApplicationProvider.getApplicationContext();
- *     db = Room.inMemoryDatabaseBuilder(context, AppDatabase.class)
- *             .allowMainThreadQueries()  // OK for tests
- *             .build();
- *     playerDao = db.playerDao();
- * }
- * 
- * @After
- * public void closeDb() {
- *     db.close();
- * }
- * 
- *

- *

- * Data Backup and Restore: - * Consider implementing backup functionality: - *

    - *
  • Export database to external storage or cloud
  • - *
  • Use Android's Auto Backup for automatic cloud backup
  • - *
  • Provide manual export/import for user control
  • - *
  • Validate data integrity after restore operations
  • - *
- *

- *

- * Security Considerations: - *

    - *
  • Database is stored in app's private directory (secure by default)
  • - *
  • For sensitive data, consider using SQLCipher for encryption
  • - *
  • Be cautious when exporting database (may contain user data)
  • - *
  • Validate and sanitize all data before insertion to prevent SQL injection - * (Room's parameterized queries provide protection)
  • - *
- *

- *

- * Future Enhancements: - * Consider these improvements for future versions: - *

    - *
  • Add proper migration strategies instead of destructive migration
  • - *
  • Implement database encryption with SQLCipher
  • - *
  • Add support for exporting/importing data
  • - *
  • Create additional entities for tournaments, achievements, settings
  • - *
  • Implement multi-user support with user profiles table
  • - *
  • Add full-text search capabilities with FTS tables
  • - *
- *

- * - * @see RoomDatabase - * @see Database - * @see Player - * @see Match + * Main Room database class for the Oche Companion darts application. + * Manages data persistence for players, matches, and statistics using the Singleton pattern. + * Uses version 2 with destructive migration (data lost on schema changes). + * Database operations must be performed on background threads. + * * @see PlayerDao * @see MatchDao - * @author Oche Companion Development Team - * @version 2.0 - * @since 1.0 + * @see Player + * @see Match */ @Database(entities = {Player.class, Match.class}, version = 2, exportSchema = false) public abstract class AppDatabase extends RoomDatabase { /** - * Provides access to Player-related database operations. - *

- * This abstract method is implemented by Room at compile time, returning an - * instance of the {@link PlayerDao} interface. The DAO (Data Access Object) - * provides methods for all player-related database operations including - * creating, reading, updating, and managing player records. - *

- *

- * Available Operations: - * The returned PlayerDao provides these methods: - *

    - *
  • {@link PlayerDao#insert(Player)} - Add new player to squad
  • - *
  • {@link PlayerDao#update(Player)} - Update existing player information
  • - *
  • {@link PlayerDao#getPlayerById(int)} - Retrieve specific player by ID
  • - *
  • {@link PlayerDao#getAllPlayers()} - Get all players sorted alphabetically
  • - *
- *

- *

- * Usage Example: - *

-     * // Get database instance
-     * AppDatabase db = AppDatabase.getDatabase(context);
-     * 
-     * // Get PlayerDao
-     * PlayerDao playerDao = db.playerDao();
-     * 
-     * // Perform operations on background thread
-     * new Thread(() -> {
-     *     // Create and insert new player
-     *     Player newPlayer = new Player("Alice", "/path/to/pic.jpg");
-     *     playerDao.insert(newPlayer);
-     *     
-     *     // Query all players
-     *     List<Player> squad = playerDao.getAllPlayers();
-     *     
-     *     // Update player stats
-     *     Player player = playerDao.getPlayerById(5);
-     *     if (player != null) {
-     *         player.careerAverage = 85.5;
-     *         player.matchesPlayed++;
-     *         playerDao.update(player);
-     *     }
-     *     
-     *     // Update UI on main thread
-     *     runOnUiThread(() -> updateSquadDisplay(squad));
-     * }).start();
-     * 
- *

- *

- * Thread Safety: - * While the DAO instance itself is thread-safe and can be reused across multiple - * threads, all database operations must be performed on background threads to - * comply with Room's threading policy. Attempting to execute queries on the - * main thread will result in an {@link IllegalStateException}. - *

- *

- * DAO Lifecycle: - * The DAO instance is created once by Room and can be cached and reused: - *

-     * // Good: Reuse DAO instance
-     * PlayerDao playerDao = db.playerDao();
-     * // Use playerDao for multiple operations
-     * 
-     * // Also fine: Get DAO each time (Room caches internally)
-     * db.playerDao().insert(player1);
-     * db.playerDao().insert(player2);
-     * 
- *

- *

- * Common Usage Patterns: - *

-     * // Pattern 1: Simple query and UI update
-     * ExecutorService executor = Executors.newSingleThreadExecutor();
-     * executor.execute(() -> {
-     *     List<Player> players = db.playerDao().getAllPlayers();
-     *     runOnUiThread(() -> adapter.setPlayers(players));
-     * });
-     * 
-     * // Pattern 2: Insert and navigate
-     * new Thread(() -> {
-     *     Player player = new Player("John", picUri);
-     *     db.playerDao().insert(player);
-     *     runOnUiThread(() -> {
-     *         Toast.makeText(context, "Player added", Toast.LENGTH_SHORT).show();
-     *         finish();
-     *     });
-     * }).start();
-     * 
-     * // Pattern 3: Edit flow (query, modify, update)
-     * new Thread(() -> {
-     *     Player player = db.playerDao().getPlayerById(playerId);
-     *     if (player != null) {
-     *         player.username = newName;
-     *         player.profilePictureUri = newPicUri;
-     *         db.playerDao().update(player);
-     *         runOnUiThread(() -> Toast.makeText(context, "Updated", Toast.LENGTH_SHORT).show());
-     *     }
-     * }).start();
-     * 
- *

- *

- * Alternative Reactive Approaches: - * Consider using LiveData or Flow for automatic UI updates: - *

-     * // If PlayerDao had LiveData support:
-     * // @Query("SELECT * FROM players ORDER BY username ASC")
-     * // LiveData<List<Player>> getAllPlayersLive();
-     * 
-     * // Usage in Activity/Fragment:
-     * db.playerDao().getAllPlayersLive().observe(this, players -> {
-     *     // UI automatically updates when data changes
-     *     adapter.setPlayers(players);
-     * });
-     * 
- *

- *

- * Error Handling: - *

-     * new Thread(() -> {
-     *     try {
-     *         playerDao.insert(player);
-     *         runOnUiThread(() -> showSuccess());
-     *     } catch (SQLiteConstraintException e) {
-     *         Log.e(TAG, "Constraint violation", e);
-     *         runOnUiThread(() -> showError("Failed to save player"));
-     *     } catch (Exception e) {
-     *         Log.e(TAG, "Database error", e);
-     *         runOnUiThread(() -> showError("Unexpected error"));
-     *     }
-     * }).start();
-     * 
- *

- * - * @return The PlayerDao instance for accessing player-related database operations. - * Never returns null. The returned instance can be safely cached and reused. + * Returns the PlayerDao for performing CRUD operations on player records. + * Thread-safe and can be reused. Operations must run on background threads. + * + * @return PlayerDao instance for player database operations * @see PlayerDao - * @see Player */ public abstract PlayerDao playerDao(); /** - * Provides access to Match-related database operations. - *

- * This abstract method is implemented by Room at compile time, returning an - * instance of the {@link MatchDao} interface. The DAO provides methods for - * storing and retrieving match records, enabling match history tracking and - * statistical analysis. - *

- *

- * Available Operations: - * The returned MatchDao provides these methods: - *

    - *
  • {@link MatchDao#insert(Match)} - Save completed match to history
  • - *
  • {@link MatchDao#getAllMatches()} - Retrieve all matches ordered by most recent
  • - *
  • {@link MatchDao#getLastMatch()} - Get the most recently completed match
  • - *
- *

- *

- * Usage Example: - *

-     * // Get database instance
-     * AppDatabase db = AppDatabase.getDatabase(context);
-     * 
-     * // Get MatchDao
-     * MatchDao matchDao = db.matchDao();
-     * 
-     * // Save completed match (background thread)
-     * new Thread(() -> {
-     *     // Create match record with participant data
-     *     String participantJson = buildParticipantData(players, scores);
-     *     Match match = new Match(
-     *         System.currentTimeMillis(),
-     *         "501",
-     *         2,
-     *         participantJson
-     *     );
-     *     
-     *     // Insert into database
-     *     matchDao.insert(match);
-     *     
-     *     // Get match history for display
-     *     List<Match> allMatches = matchDao.getAllMatches();
-     *     
-     *     // Update UI with latest match
-     *     Match lastMatch = matchDao.getLastMatch();
-     *     runOnUiThread(() -> displayMatchRecap(lastMatch));
-     * }).start();
-     * 
- *

- *

- * Match History Display: - *

-     * // Load match history for main menu
-     * new Thread(() -> {
-     *     List<Match> recentMatches = db.matchDao().getAllMatches();
-     *     
-     *     runOnUiThread(() -> {
-     *         if (recentMatches.isEmpty()) {
-     *             // Show empty state - no matches played yet
-     *             showEmptyMatchHistory();
-     *         } else {
-     *             // Display most recent match in recap view
-     *             Match lastMatch = recentMatches.get(0);
-     *             matchRecapView.setMatch(lastMatch);
-     *         }
-     *     });
-     * }).start();
-     * 
- *

- *

- * Match Completion Flow: - *

-     * // After match ends, save to database
-     * private void saveMatchResult(List<Player> players, Map<Player, Score> scores) {
-     *     new Thread(() -> {
-     *         // Build participant data JSON
-     *         JSONArray participants = new JSONArray();
-     *         for (Player player : players) {
-     *             JSONObject data = new JSONObject();
-     *             data.put("id", player.id);
-     *             data.put("username", player.username);
-     *             data.put("rank", scores.get(player).rank);
-     *             data.put("average", scores.get(player).average);
-     *             participants.put(data);
-     *         }
-     *         
-     *         // Create and save match
-     *         Match match = new Match(
-     *             System.currentTimeMillis(),
-     *             currentGameMode,
-     *             players.size(),
-     *             participants.toString()
-     *         );
-     *         
-     *         db.matchDao().insert(match);
-     *         
-     *         // Navigate to results screen
-     *         runOnUiThread(() -> {
-     *             Intent intent = new Intent(this, MatchResultsActivity.class);
-     *             intent.putExtra("match", match);
-     *             startActivity(intent);
-     *         });
-     *     }).start();
-     * }
-     * 
- *

- *

- * Thread Safety: - * Like PlayerDao, the MatchDao instance is thread-safe and can be reused across - * threads. However, all database operations must execute on background threads. - * Room will throw an {@link IllegalStateException} if database operations are - * attempted on the main thread. - *

- *

- * Performance Considerations: - *

    - *
  • getAllMatches() loads all match records - consider pagination for users - * with hundreds of matches
  • - *
  • getLastMatch() uses LIMIT 1 for efficient querying
  • - *
  • Matches are ordered by timestamp DESC for chronological display
  • - *
  • Consider adding date range queries for filtering match history
  • - *
- *

- *

- * Statistical Queries: - *

-     * // Get match history for analysis
-     * new Thread(() -> {
-     *     List<Match> allMatches = db.matchDao().getAllMatches();
-     *     
-     *     // Calculate statistics
-     *     int totalMatches = allMatches.size();
-     *     Map<String, Integer> gameModeCount = new HashMap<>();
-     *     
-     *     for (Match match : allMatches) {
-     *         gameModeCount.merge(match.gameMode, 1, Integer::sum);
-     *     }
-     *     
-     *     // Display stats
-     *     runOnUiThread(() -> showStatistics(totalMatches, gameModeCount));
-     * }).start();
-     * 
- *

- *

- * DAO Lifecycle: - * The DAO instance can be cached and reused throughout the app: - *

-     * // Cache in Application class or repository
-     * private MatchDao matchDao;
-     * 
-     * public MatchDao getMatchDao() {
-     *     if (matchDao == null) {
-     *         matchDao = AppDatabase.getDatabase(context).matchDao();
-     *     }
-     *     return matchDao;
-     * }
-     * 
- *

- *

- * Error Handling: - *

-     * new Thread(() -> {
-     *     try {
-     *         matchDao.insert(match);
-     *         runOnUiThread(() -> {
-     *             Toast.makeText(context, "Match saved", Toast.LENGTH_SHORT).show();
-     *         });
-     *     } catch (Exception e) {
-     *         Log.e(TAG, "Failed to save match", e);
-     *         runOnUiThread(() -> {
-     *             Toast.makeText(context, "Error saving match", Toast.LENGTH_SHORT).show();
-     *         });
-     *     }
-     * }).start();
-     * 
- *

- * - * @return The MatchDao instance for accessing match-related database operations. - * Never returns null. The returned instance can be safely cached and reused. + * Returns the MatchDao for managing match records and history. + * Thread-safe and can be reused. Operations must run on background threads. + * + * @return MatchDao instance for match database operations * @see MatchDao - * @see Match */ public abstract MatchDao matchDao(); /** - * The singleton instance of the AppDatabase. - *

- * This static field holds the single database instance for the entire application. - * Using the volatile keyword ensures proper visibility across threads in the - * double-checked locking pattern, preventing potential issues where one thread's - * changes might not be visible to other threads. - *

- *

- * Volatile Keyword: - * The volatile modifier guarantees: - *

    - *
  • Happens-before relationship: Writes to INSTANCE are visible to all threads
  • - *
  • Prevents instruction reordering that could break double-checked locking
  • - *
  • Ensures thread sees the fully constructed object, not a partially initialized one
  • - *
  • No caching of the variable value in CPU registers
  • - *
- *

- *

- * Initialization State: - *

    - *
  • null: Before first call to {@link #getDatabase(Context)}
  • - *
  • non-null: After database is created, remains set for app lifetime
  • - *
- *

- *

- * Memory Lifecycle: - * The database instance is retained in memory for the lifetime of the application - * process. It will be garbage collected only when: - *

    - *
  • The app process is terminated by Android
  • - *
  • The app is killed by the user
  • - *
  • System needs to reclaim memory and kills the app's process
  • - *
- *

- *

- * Testing Considerations: - * For unit tests, you may need to reset the singleton: - *

-     * // In test teardown (requires reflection or test-only setter)
-     * @After
-     * public void tearDown() {
-     *     // Close database
-     *     if (AppDatabase.INSTANCE != null) {
-     *         AppDatabase.INSTANCE.close();
-     *         // Reset singleton (would need package-private setter)
-     *         AppDatabase.INSTANCE = null;
-     *     }
-     * }
-     * 
- *

- *

- * Why Singleton: - * Using a singleton for the database instance provides: - *

    - *
  • Single connection pool shared across the app
  • - *
  • Consistent data state across all components
  • - *
  • Reduced memory overhead (no duplicate instances)
  • - *
  • Prevention of conflicting database access
  • - *
  • Simplified database access (no need to pass instance around)
  • - *
- *

- *

- * Thread Safety Analysis: - * The volatile keyword combined with synchronized block in {@link #getDatabase(Context)} - * ensures thread-safe lazy initialization: - *

-     * // Thread 1 checks: INSTANCE == null (true)
-     * // Thread 1 enters synchronized block
-     * // Thread 1 creates database instance
-     * // Thread 1 assigns to INSTANCE (volatile write)
-     * 
-     * // Thread 2 checks: INSTANCE == null (false, sees Thread 1's write)
-     * // Thread 2 returns existing instance without entering synchronized block
-     * 
- *

- * - * @see #getDatabase(Context) - * @see RoomDatabase + * Singleton instance of the AppDatabase. + * Volatile ensures thread-safe visibility in double-checked locking pattern. */ - private static volatile AppDatabase INSTANCE; + private static volatile AppDatabase sInstance; /** - * Gets the singleton instance of the AppDatabase, creating it if necessary. - *

- * This method implements the thread-safe double-checked locking pattern to ensure - * only one database instance is created, even when called simultaneously from - * multiple threads. The method is safe to call from any thread and can be invoked - * from any component (Activity, Fragment, Service, etc.). - *

- *

- * Double-Checked Locking Pattern: - * The implementation uses two null checks to optimize performance: - *

    - *
  1. First check (unsynchronized): Fast path for when instance exists. - * Avoids expensive synchronization on subsequent calls.
  2. - *
  3. Synchronized block: Ensures only one thread creates the instance. - * Prevents race conditions during initialization.
  4. - *
  5. Second check (synchronized): Prevents multiple creation if several - * threads passed the first check simultaneously.
  6. - *
- *

- *

- * Usage Example: - *

-     * // From Activity
-     * public class MainActivity extends AppCompatActivity {
-     *     @Override
-     *     protected void onCreate(Bundle savedInstanceState) {
-     *         super.onCreate(savedInstanceState);
-     *         
-     *         // Get database instance
-     *         AppDatabase db = AppDatabase.getDatabase(this);
-     *         
-     *         // Use DAOs for database operations
-     *         new Thread(() -> {
-     *             List<Player> players = db.playerDao().getAllPlayers();
-     *             runOnUiThread(() -> displayPlayers(players));
-     *         }).start();
-     *     }
-     * }
-     * 
-     * // From Fragment
-     * AppDatabase db = AppDatabase.getDatabase(requireContext());
-     * 
-     * // From Service
-     * AppDatabase db = AppDatabase.getDatabase(getApplicationContext());
-     * 
-     * // From anywhere with Context
-     * AppDatabase db = AppDatabase.getDatabase(context);
-     * 
- *

- *

- * Context Parameter: - * The method accepts any Context but internally uses {@code context.getApplicationContext()} - * to avoid memory leaks. This means: - *

    - *
  • Activity context is safe to pass (converted to app context)
  • - *
  • Fragment context is safe to pass (converted to app context)
  • - *
  • Application context is ideal but not required
  • - *
  • No risk of leaking Activity or Fragment references
  • - *
- *

- *

- * Database Builder Configuration: - * The database is created with these settings: - *

-     * Room.databaseBuilder(
-     *     context.getApplicationContext(),          // App context to prevent leaks
-     *     AppDatabase.class,                        // Database class
-     *     "oche_companion_db"                       // Database file name
-     * )
-     * .fallbackToDestructiveMigration()            // Drop tables on version change
-     * .build();
-     * 
- *

- *

- * Destructive Migration Strategy: - * The {@code fallbackToDestructiveMigration()} setting means: - *

    - *
  • Advantage: No need to write migration code during development
  • - *
  • Disadvantage: All data is lost when database version changes
  • - *
  • Development: Acceptable for testing and iteration
  • - *
  • Production: Should be replaced with proper migrations: - *
    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
    - *
  • - *
- *

- *

- * First Call Initialization: - * On the first call, this method will: - *

    - *
  1. Create the SQLite database file in app's private storage
  2. - *
  3. Create all tables defined by entity classes (players, matches)
  4. - *
  5. Set up indices and constraints
  6. - *
  7. Initialize Room's internal structures
  8. - *
  9. Return the ready-to-use database instance
  10. - *
- * This initialization is typically fast (< 100ms) but happens on the calling thread, - * so consider calling from a background thread if concerned about main thread performance. - *

- *

- * Subsequent Calls: - * After initialization, subsequent calls: - *

    - *
  • Return immediately (fast path, no synchronization)
  • - *
  • No database access or initialization overhead
  • - *
  • Thread-safe from all threads
  • - *
  • Essentially free performance-wise
  • - *
- *

- *

- * Thread Safety Guarantee: - *

-     * // Safe to call from multiple threads simultaneously
-     * Thread thread1 = new Thread(() -> {
-     *     AppDatabase db = AppDatabase.getDatabase(context);
-     *     db.playerDao().insert(player1);
-     * });
-     * 
-     * Thread thread2 = new Thread(() -> {
-     *     AppDatabase db = AppDatabase.getDatabase(context);
-     *     db.playerDao().insert(player2);
-     * });
-     * 
-     * thread1.start();
-     * thread2.start();
-     * // Both threads will use the same database instance
-     * 
- *

- *

- * Best Practices: - *

    - *
  • Call early in app lifecycle (Application.onCreate()) to avoid first-call delay
  • - *
  • Use application context when possible for clarity
  • - *
  • No need to cache the returned instance (method is fast)
  • - *
  • Safe to call repeatedly throughout the app
  • - *
- *

- *

- * Proactive Initialization: - *

-     * public class OcheCompanionApplication extends Application {
-     *     @Override
-     *     public void onCreate() {
-     *         super.onCreate();
-     *         
-     *         // Initialize database early to avoid delay on first access
-     *         AppDatabase.getDatabase(this);
-     *     }
-     * }
-     * 
- *

- *

- * Error Handling: - * In rare cases, database creation might fail due to: - *

    - *
  • Insufficient storage space
  • - *
  • Corrupted database file
  • - *
  • File system errors
  • - *
- * Room will throw RuntimeException in these cases. Consider adding error handling: - *
-     * try {
-     *     AppDatabase db = AppDatabase.getDatabase(context);
-     *     // Use database
-     * } catch (Exception e) {
-     *     Log.e(TAG, "Failed to open database", e);
-     *     // Show error to user, attempt recovery, etc.
-     * }
-     * 
- *

- * - * @param context The application context used to create the database. While any - * Context type can be passed (Activity, Fragment, Service, etc.), - * the method internally uses {@code context.getApplicationContext()} - * to prevent memory leaks. Must not be null. - * @return The singleton AppDatabase instance, fully initialized and ready for use. - * Never returns null. The same instance is returned on all subsequent calls. - * @throws IllegalArgumentException if context is null (thrown by Room.databaseBuilder) - * @throws RuntimeException if database creation fails due to filesystem or other errors - * @see Room#databaseBuilder(Context, Class, String) - * @see RoomDatabase.Builder#fallbackToDestructiveMigration() + * Gets the singleton AppDatabase instance, creating it if necessary. + * Thread-safe using double-checked locking. Uses application context to prevent leaks. + * + * @param context Context used to create the database (converted to application context) + * @return Singleton AppDatabase instance */ public static AppDatabase getDatabase(final Context context) { // First check (unsynchronized): Fast path when instance already exists // Most calls will return here after first initialization - if (INSTANCE == null) { + if (sInstance == null) { // Synchronize on the class to ensure only one thread can create the instance synchronized (AppDatabase.class) { // Second check (synchronized): Prevent creation if another thread // created the instance while we were waiting for the lock - if (INSTANCE == null) { + if (sInstance == null) { // Create the database instance // Use application context to prevent memory leaks - INSTANCE = Room.databaseBuilder(context.getApplicationContext(), + sInstance = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "oche_companion_db") .fallbackToDestructiveMigration() // Drop tables on version change .build(); @@ -859,7 +74,7 @@ public abstract class AppDatabase extends RoomDatabase { } } // Return the singleton instance (thread-safe due to volatile) - return INSTANCE; + return sInstance; } } 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 d5c28f5..64c7de4 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 @@ -7,282 +7,35 @@ import com.aldo.apps.ochecompanion.database.objects.Match; import java.util.List; /** - * Data Access Object (DAO) interface for performing database operations on Match entities. - *

- * This interface defines the contract for accessing and manipulating match data in the - * Room database. It provides methods for inserting new match records and querying match - * history. Room generates the implementation of this interface at compile time. - *

- *

- * Room Database Integration: - * The {@code @Dao} annotation indicates that this is a Room DAO interface. Room will - * automatically generate an implementation class that handles all the database operations, - * SQLite connections, cursor management, and data mapping. - *

- *

- * Key Features: - *

    - *
  • Insert completed match records into the database
  • - *
  • Retrieve complete match history sorted by most recent first
  • - *
  • Query the last played match for dashboard/recap displays
  • - *
  • Thread-safe operations managed by Room
  • - *
- *

- *

- * Thread Safety: - * All database operations should be performed on a background thread to avoid blocking - * the main UI thread. Room enforces this for most operations on main thread and will - * throw an exception if database operations are attempted on the main thread (unless - * explicitly configured otherwise). - *

- *

- * Usage Example: - *

- * // Get DAO instance from database
- * MatchDao matchDao = AppDatabase.getDatabase(context).matchDao();
- * 
- * // Insert a new match (on background thread)
- * new Thread(() -> {
- *     matchDao.insert(newMatch);
- * }).start();
- * 
- * // Query last match (on background thread)
- * new Thread(() -> {
- *     Match lastMatch = matchDao.getLastMatch();
- *     // Use lastMatch on UI thread
- *     runOnUiThread(() -> displayMatch(lastMatch));
- * }).start();
- * 
- *

- *

- * Database Table: - * This DAO operates on the "matches" table, which is defined by the {@link Match} - * entity class with its {@code @Entity} annotation. - *

- * - * @see Match - * @see Dao - * @see com.aldo.apps.ochecompanion.database.AppDatabase - * @author Oche Companion Development Team - * @version 1.0 - * @since 1.0 + * Data Access Object for Match entities in the Room database. + * Provides methods to insert matches and query match history. */ @Dao public interface MatchDao { /** * Inserts a completed match record into the database. - *

- * This method persists a new match entry to the "matches" table. The match should - * represent a completed game with all necessary information (players, scores, timestamp, etc.). - * Room will handle the actual SQL INSERT operation and auto-increment primary keys if configured. - *

- *

- * Threading: - * This operation must be performed on a background thread. Attempting to call this - * on the main thread will result in an exception (unless Room is configured to allow - * main thread queries, which is not recommended). - *

- *

- * Transaction Behavior: - * By default, Room wraps this operation in a transaction. If the insert fails, the - * transaction will be rolled back, maintaining database consistency. - *

- *

- * Conflict Strategy: - * The default conflict strategy is {@code OnConflictStrategy.ABORT}, which means if - * a conflict occurs (e.g., primary key violation), the insert will fail with an exception. - * This can be customized by adding a parameter to the {@code @Insert} annotation. - *

- *

- * Return Value: - * While this method returns void, the {@code @Insert} annotation can be configured - * to return: - *

    - *
  • {@code long} - The row ID of the newly inserted match
  • - *
  • {@code Long} - Same as above, but nullable
  • - *
  • {@code long[]} or {@code Long[]} - For bulk inserts
  • - *
  • {@code List} - Alternative for bulk inserts
  • - *
- *

- *

- * Usage Example: - *

-     * Match match = new Match(...);
-     * match.setTimestamp(System.currentTimeMillis());
-     * 
-     * new Thread(() -> {
-     *     try {
-     *         matchDao.insert(match);
-     *         // Match successfully saved
-     *     } catch (Exception e) {
-     *         // Handle insert failure
-     *         Log.e(TAG, "Failed to insert match", e);
-     *     }
- * }).start();
-     * 
- *

- * - * @param match The Match entity to persist. Must not be null. Should contain all - * required fields including timestamp, player information, and scores. - * @throws IllegalStateException if called on the main thread (Room's default behavior) - * @throws SQLiteException if the database operation fails - * @see Insert - * @see Match + * Must be called on a background thread. + * + * @param match The Match entity to persist */ @Insert - void insert(Match match); + void insert(final Match match); /** - * Retrieves all match records from the database, ordered by most recent first. - *

- * This method queries the complete match history from the "matches" table and returns - * them in descending order by timestamp. The most recently played match will be the - * first element in the returned list. - *

- *

- * SQL Query: - * Executes: {@code SELECT * FROM matches ORDER BY timestamp DESC} - *

- *

- * Sorting: - * Matches are ordered by the "timestamp" field in descending order (DESC), meaning: - *

    - *
  • Index 0: Most recent match
  • - *
  • Index 1: Second most recent match
  • - *
  • Last index: Oldest match
  • - *
- *

- *

- * Threading: - * This operation must be performed on a background thread. Attempting to call this - * on the main thread will result in an exception (unless Room is configured to allow - * main thread queries, which is not recommended). - *

- *

- * Performance Considerations: - *

    - *
  • This query loads ALL matches into memory, which could be problematic for - * large datasets (thousands of matches)
  • - *
  • Consider using pagination (e.g., {@code LIMIT} and {@code OFFSET}) for better - * performance with large history
  • - *
  • Consider using LiveData or Flow for reactive updates
  • - *
- *

- *

- * Empty Result: - * If no matches exist in the database, this method returns an empty list (not null). - *

- *

- * Usage Example: - *

-     * new Thread(() -> {
-     *     List<Match> matches = matchDao.getAllMatches();
-     *     runOnUiThread(() -> {
-     *         if (matches.isEmpty()) {
-     *             showEmptyState();
-     *         } else {
-     *             displayMatchHistory(matches);
-     *         }
-     *     });
-     * }).start();
-     * 
- *

- *

- * Suggested Improvements: - * Consider adding an index on the timestamp column for faster sorting: - *

-     * @Entity(tableName = "matches",
-     *        indices = {@Index(value = {"timestamp"}, name = "index_timestamp")})
-     * 
- *

- * - * @return A list of all Match entities sorted by timestamp in descending order - * (most recent first). Returns an empty list if no matches exist. - * Never returns null. - * @throws IllegalStateException if called on the main thread (Room's default behavior) - * @throws SQLiteException if the database query fails - * @see Query - * @see Match - * @see #getLastMatch() + * Retrieves all match records ordered by most recent first. + * Must be called on a background thread. + * + * @return List of all matches sorted by timestamp descending */ @Query("SELECT * FROM matches ORDER BY timestamp DESC") List getAllMatches(); /** - * Retrieves the most recently played match from the database. - *

- * This method is specifically designed for dashboard and recap displays where only - * the last match needs to be shown. It queries the "matches" table and returns - * just the single most recent match based on timestamp. - *

- *

- * SQL Query: - * Executes: {@code SELECT * FROM matches ORDER BY timestamp DESC LIMIT 1} - *

- *

- * Query Optimization: - * The {@code LIMIT 1} clause ensures that only one record is retrieved, making this - * significantly more efficient than {@link #getAllMatches()} when you only need the - * last match. The database can stop searching after finding the first result. - *

- *

- * Threading: - * This operation must be performed on a background thread. Attempting to call this - * on the main thread will result in an exception (unless Room is configured to allow - * main thread queries, which is not recommended). - *

- *

- * Null Return Value: - * This method returns {@code null} if no matches exist in the database. Callers - * must check for null before using the returned value: - *

-     * Match lastMatch = matchDao.getLastMatch();
-     * if (lastMatch != null) {
-     *     // Use the match
-     * } else {
-     *     // Show empty state
-     * }
-     * 
- *

- *

- * Usage Example: - *

-     * new Thread(() -> {
-     *     Match lastMatch = matchDao.getLastMatch();
-     *     runOnUiThread(() -> {
-     *         if (lastMatch != null) {
-     *             matchRecapView.setMatch(lastMatch);
-     *         } else {
-     *             matchRecapView.setMatch(null); // Shows empty state
-     *         }
-     *     });
-     * }).start();
-     * 
- *

- *

- * Use Cases: - *

    - *
  • Displaying the last match on the dashboard/main menu
  • - *
  • Showing a "play again" option with the most recent configuration
  • - *
  • Populating match recap views
  • - *
  • Checking if any matches have been played
  • - *
- *

- *

- * Performance: - * This is an efficient query due to the {@code LIMIT 1} clause. If the timestamp - * column is indexed, performance will be excellent even with thousands of matches. - *

- * - * @return The most recent Match entity based on timestamp, or {@code null} if - * no matches exist in the database. - * @throws IllegalStateException if called on the main thread (Room's default behavior) - * @throws SQLiteException if the database query fails - * @see Query - * @see Match - * @see #getAllMatches() - * @see com.aldo.apps.ochecompanion.ui.MatchRecapView#setMatch(com.aldo.apps.ochecompanion.models.Match) + * Retrieves the most recently played match. + * Must be called on a background thread. + * + * @return The most recent match, or null if no matches exist */ @Query("SELECT * FROM matches ORDER BY timestamp DESC LIMIT 1") Match getLastMatch(); 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 e6b0553..758b26c 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 @@ -10,447 +10,45 @@ import com.aldo.apps.ochecompanion.database.objects.Player; import java.util.List; /** - * Data Access Object (DAO) interface for performing database operations on Player entities. - *

- * This interface defines the contract for accessing and manipulating player data in the - * Room database. It provides comprehensive CRUD (Create, Read, Update) operations for - * managing the squad roster. Room generates the implementation of this interface at - * compile time, handling all SQL generation, cursor management, and data mapping. - *

- *

- * Room Database Integration: - * The {@code @Dao} annotation marks this as a Room DAO interface. Room's annotation - * processor will automatically generate an implementation class that handles: - *

    - *
  • SQL query construction and execution
  • - *
  • SQLite connection management
  • - *
  • Result cursor handling and mapping to Player objects
  • - *
  • Transaction management and error handling
  • - *
- *

- *

- * Key Features: - *

    - *
  • Insert: Add new players to the squad roster
  • - *
  • Update: Modify existing player information (username, profile picture, stats)
  • - *
  • Query by ID: Retrieve a specific player for editing
  • - *
  • Query All: Get the complete squad list, alphabetically sorted
  • - *
- *

- *

- * Thread Safety: - * All database operations must be performed on a background thread to avoid blocking - * the main UI thread. Room enforces this requirement by default and will throw an - * {@link IllegalStateException} if database operations are attempted on the main thread - * (unless explicitly configured to allow it, which is not recommended). - *

- *

- * Usage Example: - *

- * // Get DAO instance from database
- * PlayerDao playerDao = AppDatabase.getDatabase(context).playerDao();
- * 
- * // Insert a new player (background thread)
- * new Thread(() -> {
- *     Player newPlayer = new Player("John Doe", "/path/to/pic.jpg");
- *     playerDao.insert(newPlayer);
- * }).start();
- * 
- * // Query all players (background thread)
- * new Thread(() -> {
- *     List<Player> players = playerDao.getAllPlayers();
- *     runOnUiThread(() -> updateUI(players));
- * }).start();
- * 
- * // Update existing player (background thread)
- * new Thread(() -> {
- *     Player player = playerDao.getPlayerById(playerId);
- *     player.username = "New Name";
- *     playerDao.update(player);
- * }).start();
- * 
- *

- *

- * Database Table: - * This DAO operates on the "players" table, which is defined by the {@link Player} - * entity class with its {@code @Entity} annotation. - *

- *

- * Missing Operations: - * Note that this DAO does not currently include a DELETE operation. If player deletion - * is required, consider adding: - *

- * @Delete
- * void delete(Player player);
- * 
- * // or
- * @Query("DELETE FROM players WHERE id = :playerId")
- * void deleteById(int playerId);
- * 
- *

- * - * @see Player - * @see Dao - * @see com.aldo.apps.ochecompanion.database.AppDatabase - * @author Oche Companion Development Team - * @version 1.0 - * @since 1.0 + * Data Access Object for Player entities in the Room database. + * Provides CRUD operations for managing the squad roster. */ @Dao public interface PlayerDao { /** - * Inserts a new Player entity into the database. - *

- * This method persists a new player to the "players" table, adding them to the - * squad roster. Room will handle the actual SQL INSERT operation and automatically - * generate a primary key ID for the player if the ID field is configured with - * {@code @PrimaryKey(autoGenerate = true)}. - *

- *

- * Threading: - * This operation must be performed on a background thread. Attempting to call this - * on the main thread will result in an {@link IllegalStateException} (unless Room - * is explicitly configured to allow main thread queries, which is not recommended). - *

- *

- * Transaction Behavior: - * By default, Room wraps this operation in a database transaction. If the insert - * fails for any reason, the transaction will be rolled back, ensuring database - * consistency and atomicity. - *

- *

- * Conflict Strategy: - * The default conflict strategy is {@code OnConflictStrategy.ABORT}, which means - * if a conflict occurs (e.g., duplicate primary key), the insert will fail with - * an exception. This can be customized by adding a parameter to the {@code @Insert} - * annotation: - *

-     * @Insert(onConflict = OnConflictStrategy.REPLACE)
-     * void insert(Player player);
-     * 
- *

- *

- * Auto-Generated ID: - * After insertion, if the Player entity's ID field is auto-generated, the passed - * player object's ID field will be updated with the generated value (assuming the - * ID field is not final). - *

- *

- * Usage Example: - *

-     * Player newPlayer = new Player("Alice", "/path/to/profile.jpg");
-     * newPlayer.careerAverage = 45.5;
-     * 
-     * new Thread(() -> {
-     *     try {
-     *         playerDao.insert(newPlayer);
-     *         // After insert, newPlayer.id will contain the auto-generated ID
-     *         Log.d(TAG, "Inserted player with ID: " + newPlayer.id);
-     *     } catch (Exception e) {
-     *         Log.e(TAG, "Failed to insert player", e);
-     *     }
-     * }).start();
-     * 
- *

- *

- * Validation: - * Ensure the player object contains all required fields before insertion: - *

    - *
  • Username should not be null or empty
  • - *
  • Profile picture URI can be null (default avatar will be used)
  • - *
  • Career average should be initialized (default to 0.0 if not set)
  • - *
- *

- * - * @param player The Player object to be inserted into the database. Must not be null. - * Should contain valid username and optionally profile picture URI. - * @throws IllegalStateException if called on the main thread (Room's default behavior) - * @throws android.database.sqlite.SQLiteConstraintException if a constraint is violated - * @throws android.database.sqlite.SQLiteException if the database operation fails - * @see Insert - * @see Player + * Inserts a new player into the database. + * Must be called on a background thread. + * + * @param player The player to insert */ @Insert void insert(final Player player); /** - * Updates an existing player's information in the database. - *

- * This method modifies an existing player record in the "players" table. Room - * identifies the player to update using the primary key ID field in the provided - * Player object. All fields of the player will be updated to match the provided values. - *

- *

- * Primary Key Matching: - * Room uses the {@code @PrimaryKey} field (typically "id") to identify which - * database row to update. The player object must have a valid ID that exists in - * the database, or the update will have no effect. - *

- *

- * Threading: - * This operation must be performed on a background thread. Attempting to call this - * on the main thread will result in an {@link IllegalStateException} (unless Room - * is explicitly configured to allow main thread queries, which is not recommended). - *

- *

- * Transaction Behavior: - * By default, Room wraps this operation in a database transaction. If the update - * fails, the transaction will be rolled back, preventing partial updates and - * maintaining database consistency. - *

- *

- * Conflict Strategy: - * The default conflict strategy is {@code OnConflictStrategy.ABORT}. This can be - * customized if needed: - *

-     * @Update(onConflict = OnConflictStrategy.REPLACE)
-     * void update(Player player);
-     * 
- *

- *

- * Return Value: - * While this method returns void, the {@code @Update} annotation can be configured - * to return {@code int} indicating the number of rows updated (typically 1 for - * successful single-player updates, 0 if no matching player was found). - *

- *

- * Usage Example: - *

-     * // Typical update flow: query, modify, update
-     * new Thread(() -> {
-     *     // First, retrieve the player
-     *     Player player = playerDao.getPlayerById(playerId);
-     *     
-     *     if (player != null) {
-     *         // Modify the player's data
-     *         player.username = "Updated Name";
-     *         player.profilePictureUri = "/path/to/new/pic.jpg";
-     *         player.careerAverage = 52.3;
-     *         
-     *         // Update in database
-     *         playerDao.update(player);
-     *         
-     *         runOnUiThread(() -> {
-     *             Toast.makeText(context, "Player updated", Toast.LENGTH_SHORT).show();
-     *         });
-     *     }
-     * }).start();
-     * 
- *

- *

- * Common Use Cases: - *

    - *
  • Updating player username after editing in {@link com.aldo.apps.ochecompanion.AddPlayerActivity}
  • - *
  • Changing profile picture after cropping and saving new image
  • - *
  • Updating career statistics after completing a match
  • - *
  • Modifying any player information through the edit interface
  • - *
- *

- * - * @param player The Player object containing updated values. Must not be null. - * The object's ID field must match an existing player in the database. - * @throws IllegalStateException if called on the main thread (Room's default behavior) - * @throws android.database.sqlite.SQLiteException if the database operation fails - * @see Update - * @see Player - * @see #getPlayerById(int) + * Updates an existing player in the database. + * Player is identified by its primary key ID. + * Must be called on a background thread. + * + * @param player The player with updated data */ @Update - void update(Player player); + void update(final Player player); /** - * Retrieves a specific player from the database by their unique identifier. - *

- * This method queries the "players" table for a player with the specified ID. - * It's primarily used when editing player information, as it provides the current - * player data to populate the edit form. - *

- *

- * SQL Query: - * Executes: {@code SELECT * FROM players WHERE id = :id LIMIT 1} - *

- *

- * Query Optimization: - * The {@code LIMIT 1} clause ensures that only one record is retrieved, even though - * the ID is a primary key and should be unique. This is a safeguard and optimization - * that tells the database to stop searching after finding the first match. - *

- *

- * Threading: - * This operation must be performed on a background thread. Attempting to call this - * on the main thread will result in an {@link IllegalStateException} (unless Room - * is explicitly configured to allow main thread queries, which is not recommended). - *

- *

- * Null Return Value: - * This method returns {@code null} if no player exists with the specified ID. - * Callers must always check for null before using the returned value: - *

-     * Player player = playerDao.getPlayerById(playerId);
-     * if (player != null) {
-     *     // Use the player
-     * } else {
-     *     // Handle player not found
-     * }
-     * 
- *

- *

- * Usage Example: - *

-     * // Load player for editing
-     * int playerIdToEdit = getIntent().getIntExtra(EXTRA_PLAYER_ID, -1);
-     * 
-     * new Thread(() -> {
-     *     Player existingPlayer = playerDao.getPlayerById(playerIdToEdit);
-     *     
-     *     runOnUiThread(() -> {
-     *         if (existingPlayer != null) {
-     *             // Populate edit form with existing data
-     *             usernameEditText.setText(existingPlayer.username);
-     *             loadProfilePicture(existingPlayer.profilePictureUri);
-     *         } else {
-     *             // Player not found - show error or close activity
-     *             Toast.makeText(this, "Player not found", Toast.LENGTH_SHORT).show();
-     *             finish();
-     *         }
-     *     });
-     * }).start();
-     * 
- *

- *

- * Common Use Cases: - *

    - *
  • Loading player data in {@link com.aldo.apps.ochecompanion.AddPlayerActivity} - * when editing an existing player
  • - *
  • Retrieving player information before updating
  • - *
  • Validating that a player exists before performing operations
  • - *
  • Displaying detailed player information in a profile view
  • - *
- *

- *

- * Performance: - * This is an efficient query as it uses the primary key index. Lookup by ID is - * O(log n) or better, making it suitable for frequent calls. - *

- * - * @param id The unique primary key ID of the player to retrieve. Should be a - * positive integer representing an existing player's ID. - * @return The Player object if found, or {@code null} if no player exists with - * the specified ID. - * @throws IllegalStateException if called on the main thread (Room's default behavior) - * @throws android.database.sqlite.SQLiteException if the database query fails - * @see Query - * @see Player - * @see #update(Player) - * @see com.aldo.apps.ochecompanion.AddPlayerActivity + * Retrieves a player by their unique ID. + * Must be called on a background thread. + * + * @param id The player's ID + * @return The player, or null if not found */ @Query("SELECT * FROM players WHERE id = :id LIMIT 1") - Player getPlayerById(int id); + Player getPlayerById(final int id); /** - * Retrieves all players from the database, ordered alphabetically by username. - *

- * This method queries the complete player roster from the "players" table and - * returns them sorted alphabetically (A-Z) by username. This provides a consistent, - * user-friendly ordering for displaying the squad in lists and selection interfaces. - *

- *

- * SQL Query: - * Executes: {@code SELECT * FROM players ORDER BY username ASC} - *

- *

- * Sorting: - * Players are ordered by the "username" field in ascending alphabetical order (ASC): - *

    - *
  • Players with names starting with 'A' appear first
  • - *
  • Players with names starting with 'Z' appear last
  • - *
  • Case-insensitive sorting depends on database collation settings
  • - *
- *

- *

- * Threading: - * This operation must be performed on a background thread. Attempting to call this - * on the main thread will result in an {@link IllegalStateException} (unless Room - * is explicitly configured to allow main thread queries, which is not recommended). - *

- *

- * Performance Considerations: - *

    - *
  • This query loads ALL players into memory at once
  • - *
  • For small to medium squads (10-100 players), this is efficient
  • - *
  • For very large datasets, consider pagination or filtering
  • - *
  • Consider using LiveData or Flow for automatic UI updates when data changes
  • - *
- *

- *

- * Empty Result: - * If no players exist in the database, this method returns an empty list (not null). - * This makes it safe to iterate without null checking: - *

-     * List<Player> players = playerDao.getAllPlayers();
-     * for (Player player : players) {
-     *     // Process each player
-     * }
-     * 
- *

- *

- * Usage Example: - *

-     * // Load squad for display in RecyclerView
-     * new Thread(() -> {
-     *     List<Player> allPlayers = playerDao.getAllPlayers();
-     *     
-     *     runOnUiThread(() -> {
-     *         if (allPlayers.isEmpty()) {
-     *             // Show empty state - encourage user to add players
-     *             showEmptySquadMessage();
-     *         } else {
-     *             // Update RecyclerView adapter with player list
-     *             playerAdapter.updatePlayers(allPlayers);
-     *         }
-     *     });
-     * }).start();
-     * 
- *

- *

- * Common Use Cases: - *

    - *
  • Displaying the squad roster in {@link com.aldo.apps.ochecompanion.MainMenuActivity}
  • - *
  • Populating player selection lists when creating a new match
  • - *
  • Showing all players in management/roster views
  • - *
  • Calculating squad-wide statistics
  • - *
- *

- *

- * Alternative Implementations: - * Consider using LiveData for automatic UI updates: - *

-     * @Query("SELECT * FROM players ORDER BY username ASC")
-     * LiveData<List<Player>> getAllPlayersLive();
-     * 
- * Or Flow for Kotlin coroutines: - *
-     * @Query("SELECT * FROM players ORDER BY username ASC")
-     * Flow<List<Player>> getAllPlayersFlow();
-     * 
- *

- *

- * Suggested Improvements: - * Consider adding an index on the username column for faster sorting: - *

-     * @Entity(tableName = "players",
-     *        indices = {@Index(value = {"username"}, name = "index_username")})
-     * 
- *

- * - * @return A list of all Player entities sorted alphabetically by username in - * ascending order (A-Z). Returns an empty list if no players exist. - * Never returns null. - * @throws IllegalStateException if called on the main thread (Room's default behavior) - * @throws android.database.sqlite.SQLiteException if the database query fails - * @see Query - * @see Player - * @see com.aldo.apps.ochecompanion.ui.adapter.MainMenuPlayerAdapter#updatePlayers(List) + * Retrieves all players ordered alphabetically by username. + * Must be called on a background thread. + * + * @return List of all players sorted A-Z */ @Query("SELECT * FROM players ORDER BY username ASC") List getAllPlayers(); 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 a57c03c..8095226 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 @@ -5,800 +5,73 @@ import androidx.room.PrimaryKey; import java.io.Serializable; /** - * Represents a completed match or game leg in the Oche Companion application. - *

- * This entity class stores comprehensive information about a finished darts match, - * including the game mode played, completion timestamp, participant count, and - * detailed performance data for all players involved. The Match entity serves as - * the primary record for match history and statistics tracking. - *

- *

- * Room Database Entity: - * The {@code @Entity} annotation designates this class as a Room database table. - * Match records are stored in the "matches" table and can be queried using - * {@link com.aldo.apps.ochecompanion.database.dao.MatchDao} methods. - *

- *

- * Serializable Interface: - * This class implements {@link Serializable} to allow Match objects to be passed - * between Android components using Intent extras or Bundle arguments. This is - * useful when navigating to match detail screens or sharing match data between - * activities. - *

- *

- * Data Structure: - * Match data is stored with the following components: - *

    - *
  • ID: Auto-generated primary key for database uniqueness
  • - *
  • Timestamp: Unix epoch time marking match completion
  • - *
  • Game Mode: The variant of darts played (e.g., "501", "301", "Cricket")
  • - *
  • Player Count: Number of participants (supports 1v1 and group matches)
  • - *
  • Participant Data: JSON-serialized performance metrics for all players
  • - *
- *

- *

- * Participant Data Format: - * The {@code participantData} field contains a JSON array with detailed player performance: - *

- * [
- *   {
- *     "id": 1,
- *     "username": "John Doe",
- *     "rank": 1,
- *     "score": 501,
- *     "average": 92.4,
- *     "highestCheckout": 170
- *   },
- *   {
- *     "id": 2,
- *     "username": "Jane Smith",
- *     "rank": 2,
- *     "score": 420,
- *     "average": 81.0,
- *     "highestCheckout": 120
- *   }
- * ]
- * 
- * This structure allows for flexible storage of various statistics while maintaining - * database normalization (avoiding separate tables for each match-player relationship). - *

- *

- * Game Mode Support: - * The application supports multiple darts game variants: - *

    - *
  • 501: Standard countdown game starting at 501 points
  • - *
  • 301: Faster countdown game starting at 301 points
  • - *
  • Cricket: Number-based strategy game (15-20 and bulls)
  • - *
  • Around the Clock: Sequential number hitting game
  • - *
  • Custom game modes can be added by extending the gameMode field
  • - *
- *

- *

- * Match Types: - * The system supports different match configurations: - *

    - *
  • 1v1 Matches: Two-player head-to-head games (playerCount = 2)
  • - *
  • Group Matches: Three or more players (playerCount >= 3)
  • - *
  • Solo Practice: Single player practice sessions (playerCount = 1)
  • - *
- * The UI adapts based on player count, showing specialized layouts for each match type. - *

- *

- * Usage Example: - *

- * // Create a new match record after game completion
- * String participantJson = buildParticipantJson(players, finalScores, rankings);
- * Match newMatch = new Match(
- *     System.currentTimeMillis(),  // Current time
- *     "501",                        // Game mode
- *     2,                            // Two players
- *     participantJson               // Serialized player data
- * );
- * 
- * // Insert into database (on background thread)
- * new Thread(() -> {
- *     matchDao.insert(newMatch);
- *     // After insert, newMatch.id will contain the auto-generated ID
- * }).start();
- * 
- * // Query and display matches
- * new Thread(() -> {
- *     List<Match> recentMatches = matchDao.getAllMatches();
- *     runOnUiThread(() -> updateMatchRecapUI(recentMatches));
- * }).start();
- * 
- * // Pass match to detail activity
- * Intent intent = new Intent(this, MatchDetailActivity.class);
- * intent.putExtra("match_object", matchToView); // Uses Serializable
- * startActivity(intent);
- * 
- *

- *

- * Database Relationships: - * While this entity doesn't use Room's {@code @Relation} annotations, it maintains - * logical relationships through the participant data: - *

    - *
  • Each match references multiple players via IDs in participantData JSON
  • - *
  • Player objects are stored separately in the "players" table
  • - *
  • The denormalized JSON approach optimizes read performance for match history
  • - *
  • Updates to player usernames won't automatically reflect in historical matches
  • - *
- *

- *

- * Performance Considerations: - *

    - *
  • JSON parsing adds minimal overhead for typical match sizes (2-8 players)
  • - *
  • The timestamp field can be indexed for efficient chronological queries
  • - *
  • Consider paginating match history for users with hundreds of matches
  • - *
  • The Serializable interface has some overhead; consider Parcelable for better performance
  • - *
- *

- *

- * Data Integrity: - *

    - *
  • The auto-generated ID ensures each match is uniquely identifiable
  • - *
  • Timestamp should always be positive and in milliseconds (Unix epoch)
  • - *
  • Player count should match the number of entries in participantData JSON
  • - *
  • Game mode string should be validated against supported game types
  • - *
- *

- *

- * Future Enhancements: - * Consider adding these fields for expanded functionality: - *

    - *
  • duration: Match duration in seconds for time tracking
  • - *
  • location: Venue or location where match was played
  • - *
  • notes: User-added comments or observations
  • - *
  • isRanked: Boolean flag for competitive vs casual matches
  • - *
  • tournamentId: Reference to tournament entity for organized play
  • - *
- *

- * + * Represents a completed 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. + * * @see com.aldo.apps.ochecompanion.database.dao.MatchDao * @see com.aldo.apps.ochecompanion.database.objects.Player - * @see com.aldo.apps.ochecompanion.ui.view.MatchRecapView * @see Entity * @see Serializable - * @author Oche Companion Development Team - * @version 1.0 - * @since 1.0 */ @Entity(tableName = "matches") public class Match implements Serializable { /** - * The unique primary key identifier for this match in the database. - *

- * This field is auto-generated by Room when a new match is inserted into the - * database. The ID is automatically assigned by SQLite's AUTOINCREMENT mechanism, - * ensuring that each match has a unique, sequential identifier. - *

- *

- * Auto-Generation: - * The {@code @PrimaryKey(autoGenerate = true)} annotation tells Room to: - *

    - *
  • Automatically assign a unique ID when inserting a new match
  • - *
  • Increment the ID value for each subsequent insert
  • - *
  • Update the Match object's ID field after successful insertion
  • - *
  • Use this ID for update and delete operations
  • - *
- *

- *

- * Initial Value: - * Before insertion, this field typically has a value of 0. After the match is - * inserted into the database, Room updates this field with the generated ID - * (usually starting from 1 and incrementing). - *

- *

- * Usage: - *

-     * Match match = new Match(timestamp, gameMode, playerCount, participantData);
-     * // match.id is 0 at this point
-     * 
-     * matchDao.insert(match);
-     * // match.id now contains the auto-generated value (e.g., 42)
-     * 
-     * // Use the ID for subsequent operations
-     * Match retrieved = matchDao.getLastMatch();
-     * Log.d(TAG, "Match ID: " + retrieved.id);
-     * 
- *

- *

- * Uniqueness Guarantee: - * SQLite's AUTOINCREMENT ensures that IDs are never reused, even if matches - * are deleted. This prevents conflicts and maintains referential integrity - * if match IDs are stored externally. - *

- * + * Auto-generated unique primary key for this match. + * Value is 0 before insertion, then assigned by Room using SQLite AUTOINCREMENT. + * * @see PrimaryKey */ @PrimaryKey(autoGenerate = true) - public int id; + public int mId; /** - * Unix epoch timestamp indicating when this match was completed. - *

- * This field stores the precise moment the match ended, measured in milliseconds - * since January 1, 1970, 00:00:00 UTC (Unix epoch). The timestamp is used for: - *

    - *
  • Sorting matches chronologically in match history views
  • - *
  • Displaying relative time ("2 hours ago", "Yesterday")
  • - *
  • Filtering matches by date range
  • - *
  • Calculating statistics over time periods
  • - *
- *

- *

- * Format: - * The timestamp is stored as a {@code long} value representing milliseconds. - * This is the standard Java/Android time format obtained via: - *

-     * long timestamp = System.currentTimeMillis();
-     * 
- *

- *

- * Example Values: - *

    - *
  • 1737158400000L = January 17, 2025, 00:00:00 UTC
  • - *
  • 1704067200000L = January 1, 2024, 00:00:00 UTC
  • - *
- *

- *

- * Conversion Examples: - *

-     * // Convert to Date object
-     * Date matchDate = new Date(match.timestamp);
-     * 
-     * // Format for display
-     * SimpleDateFormat sdf = new SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault());
-     * String displayDate = sdf.format(matchDate);
-     * 
-     * // Calculate time ago
-     * long hoursAgo = (System.currentTimeMillis() - match.timestamp) / (1000 * 60 * 60);
-     * 
-     * // Use with Calendar
-     * Calendar calendar = Calendar.getInstance();
-     * calendar.setTimeInMillis(match.timestamp);
-     * int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
-     * 
- *

- *

- * Sorting: - * Matches can be ordered by timestamp to show most recent first: - *

-     * @Query("SELECT * FROM matches ORDER BY timestamp DESC LIMIT 10")
-     * List<Match> getRecentMatches();
-     * 
- *

- *

- * Validation: - * The timestamp should always be: - *

    - *
  • Positive (non-zero)
  • - *
  • Not in the future (unless testing)
  • - *
  • Reasonable (not before app creation date)
  • - *
- *

- *

- * Timezone Considerations: - * While the timestamp is stored in UTC (Unix epoch), display formatting should - * consider the user's local timezone for proper date/time presentation. - *

- * + * Unix epoch timestamp (milliseconds) when match was completed. + * Used for chronological sorting and display. Obtained via System.currentTimeMillis(). + * * @see System#currentTimeMillis() - * @see java.util.Date - * @see java.text.SimpleDateFormat */ - public long timestamp; + public long mTimestamp; /** - * The name or identifier of the game variant that was played in this match. - *

- * This field specifies which darts game mode was used, allowing the application - * to display appropriate statistics, rules, and UI elements for that particular - * game type. The game mode determines scoring rules, win conditions, and the - * overall structure of the match. - *

- *

- * Supported Game Modes: - *

    - *
  • "501": The classic countdown game starting at 501 points. - * Players subtract their scores and must finish exactly on zero with a double.
  • - *
  • "301": A faster variant starting at 301 points, following - * the same rules as 501 but requiring quicker gameplay.
  • - *
  • "Cricket": Strategic game focusing on numbers 15-20 and - * the bullseye. Players must "close" numbers by hitting them three times.
  • - *
  • "Around the Clock": Sequential game where players must - * hit numbers 1-20 in order, then finish with a bullseye.
  • - *
  • "Killer": Multiplayer elimination game where each player - * has a designated number and tries to eliminate opponents.
  • - *
- *

- *

- * String Format: - * Game mode strings should be: - *

    - *
  • Non-null and non-empty
  • - *
  • Consistent in naming (avoid "501", "Five-Oh-One", "501 Game" variations)
  • - *
  • Preferably using predefined constants to avoid typos
  • - *
- *

- *

- * Recommended Usage: - *

-     * // Define constants for game modes
-     * public static final String GAME_MODE_501 = "501";
-     * public static final String GAME_MODE_301 = "301";
-     * public static final String GAME_MODE_CRICKET = "Cricket";
-     * 
-     * // Use constants when creating matches
-     * Match match = new Match(
-     *     System.currentTimeMillis(),
-     *     GAME_MODE_501,  // Instead of hardcoded "501"
-     *     2,
-     *     participantJson
-     * );
-     * 
- *

- *

- * UI Adaptation: - * The game mode affects how matches are displayed: - *

-     * switch (match.gameMode) {
-     *     case "501":
-     *     case "301":
-     *         // Show countdown score display
-     *         // Highlight checkout attempts
-     *         break;
-     *     case "Cricket":
-     *         // Show cricket scoreboard with marks
-     *         // Display closed numbers
-     *         break;
-     *     case "Around the Clock":
-     *         // Show sequential progress indicator
-     *         break;
-     * }
-     * 
- *

- *

- * Filtering and Statistics: - * Game mode enables targeted queries and statistics: - *

-     * // Get all 501 matches
-     * @Query("SELECT * FROM matches WHERE gameMode = '501'")
-     * List<Match> get501Matches();
-     * 
-     * // Calculate average score by game mode
-     * Map<String, Double> averagesByMode = calculateAveragesByGameMode();
-     * 
- *

- *

- * Extensibility: - * New game modes can be added without schema changes: - *

    - *
  • Simply use a new string identifier
  • - *
  • Implement game-specific logic in the game engine
  • - *
  • Update UI to handle the new mode
  • - *
  • Ensure participant data JSON includes mode-specific metrics
  • - *
- *

- *

- * Validation: - * Consider validating game mode before inserting matches: - *

-     * private static final Set<String> VALID_GAME_MODES = new HashSet<>(
-     *     Arrays.asList("501", "301", "Cricket", "Around the Clock", "Killer")
-     * );
-     * 
-     * if (!VALID_GAME_MODES.contains(gameMode)) {
-     *     throw new IllegalArgumentException("Invalid game mode: " + gameMode);
-     * }
-     * 
- *

- * - * @see com.aldo.apps.ochecompanion.ui.view.MatchRecapView#setMatch(Match) + * Identifier for the darts game variant played (e.g., "501", "301", "Cricket"). + * Determines scoring rules and UI display for this match. */ - public String gameMode; + public String mGameMode; /** - * The total number of players who participated in this match. - *

- * This field indicates how many individuals competed in the match, which - * determines the match type and affects how the UI displays the results. - * The player count must match the number of player entries in the - * {@link #participantData} JSON array. - *

- *

- * Valid Range: - *

    - *
  • 1: Solo practice session (single player)
  • - *
  • 2: Head-to-head match (1v1 duel)
  • - *
  • 3+: Group match or multiplayer game
  • - *
- * Typically ranges from 1 to 8 players, though the system can support more. - *

- *

- * Match Type Determination: - * The player count affects UI rendering and game logic: - *

-     * if (match.playerCount == 1) {
-     *     // Show solo practice UI
-     *     // Display personal stats only
-     *     // No ranking or comparison
-     * } else if (match.playerCount == 2) {
-     *     // Show 1v1 layout (MatchRecapView.setup1v1State)
-     *     // Display winner/loser clearly
-     *     // Show head-to-head comparison
-     * } else {
-     *     // Show group match layout (MatchRecapView.setupGroupState)
-     *     // Display ranked list of all players
-     *     // Show leaderboard-style results
-     * }
-     * 
- *

- *

- * Data Consistency: - * The player count should always match the participant data: - *

-     * // Validate consistency
-     * JSONArray participants = new JSONArray(match.participantData);
-     * if (participants.length() != match.playerCount) {
-     *     Log.w(TAG, "Mismatch: playerCount=" + match.playerCount + 
-     *           " but participantData has " + participants.length() + " entries");
-     * }
-     * 
- *

- *

- * Performance Implications: - *

    - *
  • Higher player counts require more complex UI layouts
  • - *
  • Sorting and ranking algorithms scale with player count
  • - *
  • JSON parsing time increases with more participants
  • - *
  • Consider pagination for matches with many players
  • - *
- *

- *

- * Usage in Queries: - *

-     * // Get all 1v1 matches
-     * @Query("SELECT * FROM matches WHERE playerCount = 2")
-     * List<Match> getDuelMatches();
-     * 
-     * // Get group matches only
-     * @Query("SELECT * FROM matches WHERE playerCount >= 3")
-     * List<Match> getGroupMatches();
-     * 
- *

- *

- * Statistical Analysis: - * Player count enables performance tracking by match type: - *

-     * // Calculate win rate in 1v1 matches
-     * double winRate1v1 = calculateWinRate(playerId, 2);
-     * 
-     * // Calculate average placement in group matches
-     * double avgPlacement = calculateAveragePlacement(playerId, 3);
-     * 
- *

- *

- * Validation: - * Always validate player count before creating a match: - *

-     * if (playerCount < 1) {
-     *     throw new IllegalArgumentException("playerCount must be at least 1");
-     * }
-     * if (playerCount > MAX_PLAYERS) {
-     *     throw new IllegalArgumentException("Too many players: " + playerCount);
-     * }
-     * 
- *

- * - * @see com.aldo.apps.ochecompanion.ui.view.MatchRecapView#setup1v1State() - * @see com.aldo.apps.ochecompanion.ui.view.MatchRecapView#setupGroupState() + * Total number of players who participated in this match. + * Determines match type (1=solo, 2=1v1, 3+=group) and affects UI display. + * Must match the number of entries in participantData JSON. */ - public int playerCount; + public int mPlayerCount; /** - * Serialized JSON string containing detailed performance data for all match participants. - *

- * This field stores comprehensive information about each player's performance in - * the match, including their identity, final ranking, scores, and various statistics. - * Using JSON serialization allows flexible storage of different metrics without - * requiring separate database tables for each match-player relationship. - *

- *

- * JSON Structure: - * The participant data is stored as a JSON array of player objects: - *

-     * [
-     *   {
-     *     "id": 1,                    // Player database ID
-     *     "username": "John Doe",     // Player display name
-     *     "rank": 1,                  // Final placement (1 = winner)
-     *     "score": 501,               // Final score or points
-     *     "average": 92.4,            // Three-dart average
-     *     "highestCheckout": 170,     // Largest finish
-     *     "dartsThrown": 45,          // Total darts thrown
-     *     "profilePictureUri": "/path/to/pic.jpg"  // Profile image
-     *   },
-     *   {
-     *     "id": 2,
-     *     "username": "Jane Smith",
-     *     "rank": 2,
-     *     "score": 420,
-     *     "average": 81.0,
-     *     "highestCheckout": 120,
-     *     "dartsThrown": 51,
-     *     "profilePictureUri": "/path/to/pic2.jpg"
-     *   }
-     * ]
-     * 
- *

- *

- * Required Fields: - * Each participant object must contain: - *

    - *
  • id: Player database ID for linking to Player entity
  • - *
  • username: Display name at time of match (snapshot)
  • - *
  • rank: Final placement (1 = winner, 2 = second, etc.)
  • - *
- *

- *

- * Optional Fields: - * Additional metrics that may be included: - *

    - *
  • score: Final score or remaining points
  • - *
  • average: Three-dart average throughout the match
  • - *
  • highestCheckout: Largest checkout/finish
  • - *
  • dartsThrown: Total number of darts thrown
  • - *
  • profilePictureUri: Profile image path (snapshot)
  • - *
  • 180s: Number of maximum scores (180) hit
  • - *
  • checkoutPercentage: Success rate on finish attempts
  • - *
- *

- *

- * Parsing Example: - *

-     * try {
-     *     JSONArray participants = new JSONArray(match.participantData);
-     *     
-     *     for (int i = 0; i < participants.length(); i++) {
-     *         JSONObject player = participants.getJSONObject(i);
-     *         
-     *         int playerId = player.getInt("id");
-     *         String username = player.getString("username");
-     *         int rank = player.getInt("rank");
-     *         double average = player.optDouble("average", 0.0);
-     *         
-     *         // Use the data to populate UI
-     *         displayPlayerResult(username, rank, average);
-     *     }
-     * } catch (JSONException e) {
-     *     Log.e(TAG, "Failed to parse participant data", e);
-     * }
-     * 
- *

- *

- * Building Participant Data: - *

-     * // Create JSON array when match ends
-     * JSONArray participants = new JSONArray();
-     * 
-     * for (Player player : matchPlayers) {
-     *     JSONObject playerData = new JSONObject();
-     *     playerData.put("id", player.id);
-     *     playerData.put("username", player.username);
-     *     playerData.put("rank", player.finalRank);
-     *     playerData.put("score", player.finalScore);
-     *     playerData.put("average", player.calculateAverage());
-     *     playerData.put("highestCheckout", player.highestCheckout);
-     *     playerData.put("profilePictureUri", player.profilePictureUri);
-     *     
-     *     participants.put(playerData);
-     * }
-     * 
-     * String participantDataString = participants.toString();
-     * Match match = new Match(timestamp, gameMode, playerCount, participantDataString);
-     * 
- *

- *

- * Why JSON Instead of Relations: - *

    - *
  • Performance: Single query retrieves complete match data
  • - *
  • Historical Accuracy: Captures player data as it was at match time
  • - *
  • Flexibility: Can store game-specific metrics without schema changes
  • - *
  • Simplicity: Avoids complex join queries and relationship management
  • - *
  • Immutability: Match data remains unchanged if player profiles are updated
  • - *
- *

- *

- * Data Integrity: - *

    - *
  • Array length should match {@link #playerCount}
  • - *
  • Ranks should be sequential (1, 2, 3, ...) without gaps
  • - *
  • Player IDs should reference valid players (though not enforced by foreign key)
  • - *
  • JSON must be well-formed and parseable
  • - *
- *

- *

- * Snapshot Advantage: - * Storing username and profile picture in the match data (rather than just ID) - * preserves the historical record. If a player later changes their username from - * "John Doe" to "JD_Pro", the old match will still show "John Doe" as they were - * known at that time. - *

- *

- * Sorting Participants: - * Usually pre-sorted by rank before serialization, but can be sorted after parsing: - *

-     * // Sort by rank after parsing
-     * Collections.sort(playerList, (p1, p2) -> 
-     *     Integer.compare(p1.rank, p2.rank));
-     * 
- *

- *

- * Null Handling: - * This field should never be null. If a match has no valid participant data, - * consider using an empty array "[]" or not creating the match at all. - *

- * + * JSON string containing detailed performance data for all match participants. + * Stores player identities, rankings, scores, and statistics. Array length must + * match playerCount. Uses JSON for flexible storage without additional tables. + * * @see org.json.JSONArray * @see org.json.JSONObject - * @see com.aldo.apps.ochecompanion.database.objects.Player */ - public String participantData; + public String mParticipantData; /** - * Constructs a new Match entity with the specified parameters. - *

- * This constructor creates a complete match record ready for insertion into the - * database. The match ID will be auto-generated by Room upon insertion; there is - * no need to set it manually. All parameters are required to create a valid match. - *

- *

- * Constructor Parameters: - * Each parameter serves a specific purpose in defining the match: - *

    - *
  • timestamp: Records when the match was completed for chronological ordering
  • - *
  • gameMode: Identifies which darts variant was played
  • - *
  • playerCount: Specifies how many players participated
  • - *
  • participantData: Contains detailed performance data for all players
  • - *
- *

- *

- * Usage Example: - *

-     * // Build participant data JSON
-     * JSONArray participants = new JSONArray();
-     * for (Player player : finalStandings) {
-     *     JSONObject playerData = new JSONObject();
-     *     playerData.put("id", player.id);
-     *     playerData.put("username", player.username);
-     *     playerData.put("rank", player.rank);
-     *     playerData.put("average", player.calculateAverage());
-     *     participants.put(playerData);
-     * }
-     * 
-     * // Create match object
-     * Match completedMatch = new Match(
-     *     System.currentTimeMillis(),        // Current time in milliseconds
-     *     "501",                             // Game mode identifier
-     *     2,                                 // Number of players
-     *     participants.toString()            // Serialized participant data
-     * );
-     * 
-     * // Insert into database (background thread required)
-     * new Thread(() -> {
-     *     matchDao.insert(completedMatch);
-     *     // completedMatch.id will now contain the auto-generated ID
-     *     Log.d(TAG, "Match saved with ID: " + completedMatch.id);
-     * }).start();
-     * 
- *

- *

- * Parameter Validation: - * While the constructor doesn't enforce validation, consider checking parameters - * before construction: - *

-     * // Validate before creating match
-     * if (timestamp <= 0) {
-     *     throw new IllegalArgumentException("Invalid timestamp");
-     * }
-     * if (gameMode == null || gameMode.isEmpty()) {
-     *     throw new IllegalArgumentException("Game mode is required");
-     * }
-     * if (playerCount < 1) {
-     *     throw new IllegalArgumentException("At least one player required");
-     * }
-     * if (participantData == null || participantData.isEmpty()) {
-     *     throw new IllegalArgumentException("Participant data is required");
-     * }
-     * 
-     * // Validate JSON format
-     * try {
-     *     JSONArray test = new JSONArray(participantData);
-     *     if (test.length() != playerCount) {
-     *         throw new IllegalArgumentException("Player count mismatch");
-     *     }
-     * } catch (JSONException e) {
-     *     throw new IllegalArgumentException("Invalid JSON format", e);
-     * }
-     * 
-     * // Create match after validation
-     * Match match = new Match(timestamp, gameMode, playerCount, participantData);
-     * 
- *

- *

- * Field Initialization: - * The constructor initializes all fields except {@code id}: - *

    - *
  • id: Remains 0 (default) until Room assigns auto-generated value
  • - *
  • timestamp: Set to the provided value (milliseconds since epoch)
  • - *
  • gameMode: Set to the provided game identifier string
  • - *
  • playerCount: Set to the provided player count
  • - *
  • participantData: Set to the provided JSON string
  • - *
- *

- *

- * Database Insertion: - * After construction, insert the match using the DAO: - *

-     * // Get DAO instance
-     * MatchDao matchDao = AppDatabase.getDatabase(context).matchDao();
-     * 
-     * // Insert on background thread (required by Room)
-     * ExecutorService executor = Executors.newSingleThreadExecutor();
-     * executor.execute(() -> {
-     *     matchDao.insert(completedMatch);
-     *     
-     *     // Update UI on main thread
-     *     runOnUiThread(() -> {
-     *         Toast.makeText(context, "Match saved!", Toast.LENGTH_SHORT).show();
-     *         refreshMatchHistory();
-     *     });
-     * });
-     * 
- *

- *

- * Immutability Consideration: - * Once created and inserted, match data should generally be treated as immutable - * (historical record). If corrections are needed, consider whether to: - *

    - *
  • Update the existing match (rare, only for data corrections)
  • - *
  • Delete and recreate (for significant errors)
  • - *
  • Leave as-is and add a note field (preserves history)
  • - *
- *

- *

- * Thread Safety: - * The constructor itself is thread-safe, but database insertion must occur on - * a background thread due to Room's main thread restrictions. - *

- * - * @param timestamp The Unix epoch timestamp in milliseconds indicating when the match - * was completed. Should be positive and not in the future. Typically - * obtained via {@link System#currentTimeMillis()}. - * @param gameMode The identifier for the darts game variant played (e.g., "501", - * "301", "Cricket"). Should match one of the supported game modes. - * Must not be null or empty. - * @param playerCount The total number of players who participated in the match. - * Must be at least 1. Should match the number of entries in - * the participantData JSON array. - * @param participantData A JSON-formatted string containing an array of player - * performance objects. Each object should include player ID, - * username, rank, and relevant statistics. Must be valid JSON - * and must not be null or empty. - * @see #timestamp - * @see #gameMode - * @see #playerCount - * @see #participantData + * 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 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 * @see com.aldo.apps.ochecompanion.database.dao.MatchDao#insert(Match) */ public Match(final long timestamp, final String gameMode, final int playerCount, final String participantData) { - // Initialize all fields with provided values - // The id field remains at default value (0) and will be auto-generated by Room upon insertion - this.timestamp = timestamp; - this.gameMode = gameMode; - this.playerCount = playerCount; - this.participantData = participantData; + this.mTimestamp = timestamp; + this.mGameMode = gameMode; + this.mPlayerCount = playerCount; + this.mParticipantData = participantData; } } 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 eff9be9..d83046f 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 @@ -7,970 +7,82 @@ import androidx.room.Entity; import androidx.room.PrimaryKey; /** - * Represents a player entity in the Oche Companion darts application. - *

- * This entity class stores comprehensive information about each player in the squad, - * including their identity, profile picture, and career statistics. Player objects - * serve as the foundation for roster management, match participation tracking, and - * statistical analysis throughout the application. - *

- *

- * Room Database Entity: - * The {@code @Entity} annotation designates this class as a Room database table. - * Player records are stored in the "players" table and can be queried, inserted, - * updated, and managed using {@link com.aldo.apps.ochecompanion.database.dao.PlayerDao} - * methods. - *

- *

- * Data Structure: - * Each player record contains: - *

    - *
  • ID: Auto-generated unique identifier for database operations
  • - *
  • Username: Display name shown throughout the application
  • - *
  • Profile Picture URI: File path to the player's avatar image
  • - *
  • Career Average: Overall three-dart average across all matches
  • - *
  • Matches Played: Total count of completed matches
  • - *
- *

- *

- * Player Lifecycle: - *

    - *
  1. Creation: Player is created via {@link com.aldo.apps.ochecompanion.AddPlayerActivity} - * with username and optional profile picture
  2. - *
  3. Roster Display: Players appear in the squad list on the main menu
  4. - *
  5. Match Participation: Players are selected for matches, their performance - * is recorded
  6. - *
  7. Statistics Update: Career stats are updated after each match completion
  8. - *
  9. Profile Editing: Username and picture can be modified through edit interface
  10. - *
- *

- *

- * Usage Example: - *

- * // Create a new player
- * Player newPlayer = new Player("John Doe", "/path/to/profile.jpg");
- * newPlayer.careerAverage = 85.5;  // Optional initial stats
- * newPlayer.matchesPlayed = 0;     // Starts at 0 by default
- * 
- * // Insert into database (background thread required)
- * new Thread(() -> {
- *     playerDao.insert(newPlayer);
- *     // After insert, newPlayer.id contains the auto-generated ID
- *     Log.d(TAG, "Player created with ID: " + newPlayer.id);
- * }).start();
- * 
- * // Query and display players
- * new Thread(() -> {
- *     List<Player> allPlayers = playerDao.getAllPlayers();
- *     runOnUiThread(() -> updateSquadUI(allPlayers));
- * }).start();
- * 
- * // Update player statistics after match
- * new Thread(() -> {
- *     Player player = playerDao.getPlayerById(playerId);
- *     player.matchesPlayed++;
- *     player.careerAverage = calculateNewAverage(player, matchAverage);
- *     playerDao.update(player);
- * }).start();
- * 
- *

- *

- * Profile Picture Management: - * Profile pictures are stored as URI strings pointing to local files in the app's - * private storage. This approach: - *

    - *
  • Avoids bloating the database with binary data
  • - *
  • Allows efficient image loading with Glide or Picasso
  • - *
  • Enables easy image replacement without database updates
  • - *
  • Supports null values for players without custom avatars (default avatar shown)
  • - *
- *

- *

- * Career Statistics: - * The {@code careerAverage} field tracks the player's overall performance: - *

    - *
  • Calculated as three-dart average across all completed matches
  • - *
  • Updated after each match using weighted or running average algorithm
  • - *
  • Displayed in player cards and comparison views
  • - *
  • Used for skill-based matchmaking or handicap calculations
  • - *
- *

- *

- * Match Count Tracking: - * The {@code matchesPlayed} counter: - *

    - *
  • Increments by 1 after each completed match
  • - *
  • Provides context for statistical significance (100 matches vs 5 matches)
  • - *
  • Enables filtering of experienced vs. new players
  • - *
  • Used in achievement and milestone tracking
  • - *
- *

- *

- * Database Relationships: - * While this entity doesn't use explicit Room relations, players are referenced in: - *

    - *
  • {@link Match} entities via player IDs in participantData JSON
  • - *
  • RecyclerView adapters for squad and player selection displays
  • - *
  • Match creation flows where players are chosen from the roster
  • - *
- *

- *

- * Thread Safety: - * All database operations on Player objects must be performed on background threads - * to comply with Room's threading requirements. Direct field access is not - * thread-safe; use proper synchronization if accessing from multiple threads. - *

- *

- * Validation Considerations: - * When creating or updating players, consider validating: - *

    - *
  • Username is not null or empty
  • - *
  • Username length is within reasonable bounds (e.g., 1-30 characters)
  • - *
  • Profile picture URI points to a valid, accessible file (if not null)
  • - *
  • Career average is non-negative and realistic (typically 0-120 for darts)
  • - *
  • Matches played count is non-negative
  • - *
- *

- *

- * Future Enhancements: - * Consider adding these fields for expanded functionality: - *

    - *
  • wins/losses: Win-loss record tracking
  • - *
  • highestCheckout: Best finish/checkout across career
  • - *
  • favourite Game Mode: Most-played game variant
  • - *
  • createdDate: Timestamp when player was added to squad
  • - *
  • nickname: Alternative display name or alias
  • - *
  • skillRating: ELO or skill-based rating system
  • - *
- *

- *

- * UI Integration: - * Players are displayed in various UI components: - *

    - *
  • {@link com.aldo.apps.ochecompanion.ui.adapter.MainMenuPlayerAdapter} - Squad roster grid
  • - *
  • {@link com.aldo.apps.ochecompanion.ui.view.PlayerItemView} - Individual player cards
  • - *
  • {@link com.aldo.apps.ochecompanion.AddPlayerActivity} - Player creation/editing form
  • - *
  • Match setup screens - Player selection for new matches
  • - *
- *

- *

- * Performance Notes: - *

    - *
  • Player queries are fast due to indexed primary key (ID)
  • - *
  • Consider adding index on username for search functionality: - * {@code @Entity(indices = {@Index(value = {"username"})})}
  • - *
  • Profile picture URIs keep queries lightweight compared to storing images directly
  • - *
  • The simple schema makes inserts, updates, and queries efficient
  • - *
- *

- * - * @see com.aldo.apps.ochecompanion.database.dao.PlayerDao - * @see com.aldo.apps.ochecompanion.database.objects.Match - * @see com.aldo.apps.ochecompanion.AddPlayerActivity - * @see com.aldo.apps.ochecompanion.ui.adapter.MainMenuPlayerAdapter - * @see com.aldo.apps.ochecompanion.ui.view.PlayerItemView - * @see Entity - * @author Oche Companion Development Team - * @version 1.0 - * @since 1.0 + * Room database entity representing a player in the darts application. + * Stores player identity (ID, username, profile picture), and career statistics + * (average, matches played). Players are stored in the "players" table. */ @Entity(tableName = "players") public class Player implements Parcelable { /** - * The unique primary key identifier for this player in the database. - *

- * This field serves as the player's permanent identifier throughout the application - * and is used for all database operations, match participation tracking, and - * cross-referencing with match records. - *

- *

- * Auto-Generation: - * The {@code @PrimaryKey(autoGenerate = true)} annotation instructs Room to: - *

    - *
  • Automatically assign a unique ID when a new player is inserted
  • - *
  • Increment the ID value sequentially (1, 2, 3, ...)
  • - *
  • Update the Player object's ID field after successful insertion
  • - *
  • Use this ID as the primary key for update and query operations
  • - *
- *

- *

- * Initial State: - * Before database insertion, this field has a default value of 0. After the - * player is inserted via {@link com.aldo.apps.ochecompanion.database.dao.PlayerDao#insert(Player)}, - * Room automatically populates this field with the generated ID value. - *

- *

- * Usage Examples: - *

-     * // Before insertion
-     * Player player = new Player("Alice", "/path/to/pic.jpg");
-     * Log.d(TAG, "ID before insert: " + player.id);  // Output: 0
-     * 
-     * // Insert into database
-     * playerDao.insert(player);
-     * Log.d(TAG, "ID after insert: " + player.id);   // Output: 15 (or next available ID)
-     * 
-     * // Use ID to query specific player
-     * Player retrieved = playerDao.getPlayerById(player.id);
-     * 
-     * // Use ID in match participant data
-     * JSONObject participantData = new JSONObject();
-     * participantData.put("id", player.id);  // Reference this player in match
-     * 
- *

- *

- * Uniqueness and Permanence: - * SQLite's AUTOINCREMENT mechanism ensures: - *

    - *
  • No two players will ever have the same ID
  • - *
  • IDs are never reused, even after player deletion
  • - *
  • ID sequence continues across app restarts
  • - *
  • Historical match references remain valid
  • - *
- *

- *

- * Cross-References: - * The player ID is stored in: - *

    - *
  • {@link Match#participantData} JSON arrays to link match participants
  • - *
  • Intent extras when navigating to {@link com.aldo.apps.ochecompanion.AddPlayerActivity} - * for editing
  • - *
  • Adapter item positions for click handling and UI updates
  • - *
- *

- *

- * Integer Range: - * Using {@code int} supports up to 2,147,483,647 players, which is far beyond - * any realistic squad size. If migrating to multi-user cloud sync in the future, - * consider using {@code long} or UUID strings for globally unique identifiers. - *

- * - * @see PrimaryKey - * @see com.aldo.apps.ochecompanion.database.dao.PlayerDao#getPlayerById(int) - * @see com.aldo.apps.ochecompanion.database.dao.PlayerDao#insert(Player) + * Unique auto-generated database identifier. Set to 0 before insertion, + * Room auto-populates on insert. */ @PrimaryKey(autoGenerate = true) - public int id; + public int mId; /** - * The display name of the player shown throughout the application. - *

- * This field stores the player's chosen name or alias, which appears in squad - * lists, player selection screens, match results, leaderboards, and all other - * UI components where the player is identified. The username serves as the - * primary human-readable identifier for each player. - *

- *

- * Display Locations: - * The username is prominently shown in: - *

    - *
  • Squad roster grid on the main menu
  • - *
  • Player cards in {@link com.aldo.apps.ochecompanion.ui.view.PlayerItemView}
  • - *
  • Match recap views showing participant names
  • - *
  • Player selection lists when creating new matches
  • - *
  • Edit player form in {@link com.aldo.apps.ochecompanion.AddPlayerActivity}
  • - *
  • Match history and statistical comparisons
  • - *
- *

- *

- * Character Guidelines: - * While not enforced by the database schema, usernames should ideally: - *

    - *
  • Be non-null and non-empty (required for meaningful identification)
  • - *
  • Contain 1-30 characters (reasonable display length)
  • - *
  • Support Unicode characters (international names, emojis)
  • - *
  • Avoid leading/trailing whitespace (trim before saving)
  • - *
  • Be unique within the squad (not enforced, but recommended for UX)
  • - *
- *

- *

- * Validation Example: - *

-     * // Recommended validation before creating player
-     * private boolean isValidUsername(String username) {
-     *     if (username == null || username.trim().isEmpty()) {
-     *         showError("Username cannot be empty");
-     *         return false;
-     *     }
-     *     
-     *     String trimmed = username.trim();
-     *     if (trimmed.length() > 30) {
-     *         showError("Username too long (max 30 characters)");
-     *         return false;
-     *     }
-     *     
-     *     // Optional: Check for duplicate names
-     *     if (isUsernameTaken(trimmed)) {
-     *         showError("Username already exists in squad");
-     *         return false;
-     *     }
-     *     
-     *     return true;
-     * }
-     * 
-     * // Use validated username
-     * String validUsername = username.trim();
-     * Player player = new Player(validUsername, profilePicUri);
-     * 
- *

- *

- * Editing Usernames: - * Players can change their username via the edit interface: - *

-     * // Load player for editing
-     * Player player = playerDao.getPlayerById(playerId);
-     * player.username = "New Name";
-     * playerDao.update(player);
-     * 
-     * // Note: Historical match records store username snapshots,
-     * // so past matches will still show the old name
-     * 
- *

- *

- * Database Storage: - * Stored as TEXT in SQLite, supporting any valid UTF-8 string. Room handles - * all string encoding/decoding automatically. Consider adding an index if - * implementing username search: - *

-     * @Entity(tableName = "players",
-     *        indices = {@Index(value = {"username"}, name = "index_username")})
-     * 
- *

- *

- * Null Handling: - * This field should never be null in practice. If a player somehow has a null - * username, the UI should display a placeholder like "Unnamed Player" or - * "Player #[ID]" to prevent blank spaces or crashes. - *

- *

- * Case Sensitivity: - * SQLite comparisons are case-insensitive by default for ASCII characters. - * "John" and "john" are considered different usernames, but queries like - * {@code WHERE username = 'john'} will match "JOHN" or "John" unless - * {@code COLLATE BINARY} is specified. - *

- *

- * International Support: - * The field fully supports Unicode characters, allowing names in any language: - *

    - *
  • Chinese: 王明
  • - *
  • Arabic: محمد
  • - *
  • Cyrillic: Иван
  • - *
  • Emoji: John 🎯
  • - *
- * Ensure UI fonts support these characters for proper display. - *

- * - * @see com.aldo.apps.ochecompanion.AddPlayerActivity - * @see com.aldo.apps.ochecompanion.ui.view.PlayerItemView#bind(Player) + * Player's display name shown throughout the app. Should be non-null and + * ideally 1-30 characters. Supports Unicode. */ - public String username; + public String mUsername; /** - * The file system path (URI) to the player's profile picture image. - *

- * This field stores a string representation of the file path where the player's - * avatar or profile picture is located in the app's private storage. Rather than - * storing image data directly in the database (which would bloat it), we store - * only the path reference and load the image on-demand using image loading - * libraries like Glide. - *

- *

- * Storage Strategy: - * Profile pictures are stored as local files because: - *

    - *
  • Database Efficiency: Avoiding BLOB storage keeps database - * size small and queries fast
  • - *
  • Image Loading: Glide and Picasso efficiently cache and - * load images from file paths
  • - *
  • Easy Replacement: Updating the image file doesn't require - * database updates
  • - *
  • Memory Management: Images aren't loaded into memory until - * needed for display
  • - *
- *

- *

- * File Path Format: - * The URI typically follows this pattern: - *

-     * /data/data/com.aldo.apps.ochecompanion/files/profile_pictures/player_123_20250128.jpg
-     * 
- * Or uses content URI format: - *
-     * content://media/external/images/media/456
-     * 
- *

- *

- * Image Creation Flow: - *

-     * // In AddPlayerActivity, after user crops image:
-     * File outputFile = new File(getFilesDir(), "profile_pictures/player_" + 
-     *                           System.currentTimeMillis() + ".jpg");
-     * 
-     * // Save cropped bitmap to file
-     * try (FileOutputStream out = new FileOutputStream(outputFile)) {
-     *     croppedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, out);
-     * }
-     * 
-     * // Store file path in player object
-     * String profilePicUri = outputFile.getAbsolutePath();
-     * Player player = new Player(username, profilePicUri);
-     * playerDao.insert(player);
-     * 
- *

- *

- * Loading Images with Glide: - *

-     * // In PlayerItemView or adapter
-     * if (player.profilePictureUri != null && !player.profilePictureUri.isEmpty()) {
-     *     // Load custom profile picture
-     *     Glide.with(context)
-     *          .load(new File(player.profilePictureUri))
-     *          .placeholder(R.drawable.default_avatar)
-     *          .error(R.drawable.default_avatar)
-     *          .circleCrop()
-     *          .into(profileImageView);
-     * } else {
-     *     // Show default avatar
-     *     profileImageView.setImageResource(R.drawable.default_avatar);
-     * }
-     * 
- *

- *

- * Null and Empty Values: - * This field can be null or empty, indicating the player has no custom profile - * picture. In such cases, the UI should display a default avatar image: - *

    - *
  • Generic silhouette or icon
  • - *
  • Player's initials in a colored circle
  • - *
  • Default darts-themed avatar
  • - *
- *

- *

- * File Management: - * Consider these file management practices: - *

    - *
  • Deletion: When a player is deleted or changes their picture, - * clean up old image files to prevent storage bloat
  • - *
  • Validation: Verify file exists before attempting to load
  • - *
  • Error Handling: Gracefully fall back to default avatar if - * file is missing or corrupted
  • - *
  • Format: Support common formats (JPEG, PNG, WebP)
  • - *
- *

- *

- * File Cleanup Example: - *

-     * // When updating player's profile picture
-     * Player player = playerDao.getPlayerById(playerId);
-     * String oldPicturePath = player.profilePictureUri;
-     * 
-     * // Set new picture
-     * player.profilePictureUri = newPicturePath;
-     * playerDao.update(player);
-     * 
-     * // Delete old picture file
-     * if (oldPicturePath != null && !oldPicturePath.isEmpty()) {
-     *     File oldFile = new File(oldPicturePath);
-     *     if (oldFile.exists()) {
-     *         oldFile.delete();
-     *     }
-     * }
-     * 
- *

- *

- * Image Optimization: - * Profile pictures should be: - *

    - *
  • Resized to reasonable dimensions (e.g., 500x500 pixels max)
  • - *
  • Compressed to balance quality and file size (JPEG quality 85-90)
  • - *
  • Cropped to square aspect ratio for consistent display
  • - *
  • Stored in app's private storage for security and isolation
  • - *
- *

- *

- * Migration Consideration: - * If implementing cloud sync in the future, consider: - *

    - *
  • Uploading images to cloud storage (Firebase Storage, S3)
  • - *
  • Storing cloud URLs instead of local file paths
  • - *
  • Implementing offline caching with local fallbacks
  • - *
  • Supporting both local and remote image sources
  • - *
- *

- * - * @see com.aldo.apps.ochecompanion.AddPlayerActivity - * @see com.aldo.apps.ochecompanion.ui.view.PlayerItemView#bind(Player) - * @see com.bumptech.glide.Glide + * File path to player's profile picture. Can be null (shows default avatar). + * Stored as path reference to avoid database bloat. */ - public String profilePictureUri; + public String mProfilePictureUri; /** - * The player's career-long three-dart average across all completed matches. - *

- * This field represents the player's overall skill level and consistency, - * calculated as the average score per three darts thrown throughout their - * entire match history. In darts, the three-dart average is the standard - * metric for measuring player performance and ability. - *

- *

- * Calculation Method: - * The career average is typically calculated using one of these approaches: - *

    - *
  • Running Average: (Total points scored) / (Total darts thrown / 3)
  • - *
  • Weighted Average: Recent matches weighted more heavily than older ones
  • - *
  • Match Average of Averages: Average of individual match averages
  • - *
- *

- *

- * Update Example: - *

-     * // After a match completes, update player's career average
-     * Player player = playerDao.getPlayerById(playerId);
-     * 
-     * // Method 1: Simple running average
-     * double totalPoints = player.careerAverage * player.matchesPlayed;
-     * totalPoints += matchAverage;  // Add this match's average
-     * player.matchesPlayed++;
-     * player.careerAverage = totalPoints / player.matchesPlayed;
-     * 
-     * // Method 2: Weighted average (70% old, 30% new)
-     * player.careerAverage = (player.careerAverage * 0.7) + (matchAverage * 0.3);
-     * player.matchesPlayed++;
-     * 
-     * playerDao.update(player);
-     * 
- *

- *

- * Typical Value Ranges: - * Three-dart averages in darts typically fall within these ranges: - *

    - *
  • 0-40: Beginner player, still learning fundamentals
  • - *
  • 40-60: Casual player, recreational skill level
  • - *
  • 60-80: Experienced player, competitive at local level
  • - *
  • 80-100: Advanced player, regional competitive level
  • - *
  • 100+: Professional or semi-professional level
  • - *
- * World-class professionals maintain averages of 100-105+. - *

- *

- * Display Format: - *

-     * // Format for display in UI
-     * String displayAverage = String.format(Locale.getDefault(), "%.1f", 
-     *                                      player.careerAverage);
-     * // Example output: "85.3"
-     * 
-     * // Or with context
-     * String statText = "Average: " + displayAverage + " ppd";
-     * // Example output: "Average: 85.3 ppd" (points per dart)
-     * 
- *

- *

- * Default Value: - * Initialized to 0.0 by default, indicating no match history yet. When displaying - * a player with 0.0 average and 0 matches played, consider showing placeholder - * text like "No matches yet" or "N/A" instead of "0.0". - *

- *

- * Statistical Significance: - * The reliability of the career average increases with match count: - *

-     * if (player.matchesPlayed < 5) {
-     *     // Low sample size - show with disclaimer
-     *     displayText = player.careerAverage + " (limited data)";
-     * } else if (player.matchesPlayed < 20) {
-     *     // Moderate sample size
-     *     displayText = player.careerAverage + " (" + player.matchesPlayed + " matches)";
-     * } else {
-     *     // Reliable sample size
-     *     displayText = String.valueOf(player.careerAverage);
-     * }
-     * 
- *

- *

- * Comparison and Ranking: - * Used to rank players in leaderboards and skill-based groupings: - *

-     * // Sort players by skill level
-     * List<Player> rankedPlayers = new ArrayList<>(allPlayers);
-     * Collections.sort(rankedPlayers, (p1, p2) -> 
-     *     Double.compare(p2.careerAverage, p1.careerAverage));  // Descending
-     * 
-     * // Find players in similar skill range for balanced matches
-     * double targetAverage = 75.0;
-     * double tolerance = 10.0;
-     * List<Player> similarSkill = players.stream()
-     *     .filter(p -> Math.abs(p.careerAverage - targetAverage) <= tolerance)
-     *     .collect(Collectors.toList());
-     * 
- *

- *

- * Data Integrity: - *

    - *
  • Should never be negative (minimum 0.0)
  • - *
  • Realistically shouldn't exceed 120 (theoretical maximum ~180)
  • - *
  • Should be 0.0 if matchesPlayed is 0
  • - *
  • Precision to 1-2 decimal places is sufficient
  • - *
- *

- *

- * Alternative Metrics: - * Future enhancements might track additional averages: - *

    - *
  • First 9 darts average (excludes checkout phase)
  • - *
  • Per-game-mode averages (501 average, Cricket average)
  • - *
  • Recent form average (last 10 matches only)
  • - *
  • Peak average (highest match average achieved)
  • - *
- *

- * - * @see #matchesPlayed - * @see com.aldo.apps.ochecompanion.ui.view.PlayerItemView#bind(Player) + * Player's career three-dart average across all completed matches. + * Represents overall skill level. Typical ranges: 0-40 (beginner), + * 40-60 (casual), 60-80 (experienced), 80-100 (advanced), 100+ (pro). */ - public double careerAverage = 0.0; + public double mCareerAverage = 0.0; /** - * The total number of matches this player has completed. - *

- * This counter tracks how many times the player has participated in and - * finished a match. It provides context for statistical significance, - * enables experience-based features, and supports achievement tracking. - * The count increases by 1 after each match completion, regardless of - * the outcome (win, loss, or placement in group matches). - *

- *

- * Update Pattern: - *

-     * // After completing a match
-     * Player player = playerDao.getPlayerById(playerId);
-     * 
-     * // Increment match count
-     * player.matchesPlayed++;
-     * 
-     * // Also update career average at the same time
-     * player.careerAverage = calculateNewAverage(player, matchAverage);
-     * 
-     * playerDao.update(player);
-     * 
- *

- *

- * Default Value: - * Initialized to 0 by default, indicating a newly created player who hasn't - * yet participated in any matches. This is appropriate for new squad members - * added through {@link com.aldo.apps.ochecompanion.AddPlayerActivity}. - *

- *

- * Display Usage: - *

-     * // Show experience level in player profile
-     * String experienceText;
-     * if (player.matchesPlayed == 0) {
-     *     experienceText = "New Player";
-     * } else if (player.matchesPlayed == 1) {
-     *     experienceText = "1 match played";
-     * } else {
-     *     experienceText = player.matchesPlayed + " matches played";
-     * }
-     * 
-     * // Use for statistical context
-     * if (player.matchesPlayed < 10) {
-     *     showDisclaimer("Statistics based on limited match history");
-     * }
-     * 
- *

- *

- * Experience-Based Features: - * Match count enables various gameplay features: - *

    - *
  • Achievements: Unlock badges at 10, 50, 100, 500 matches
  • - *
  • Ranking Eligibility: Require minimum matches for leaderboards
  • - *
  • Skill Confidence: More matches = more reliable average
  • - *
  • Veteran Status: Display special icons for experienced players
  • - *
- *

- *

- * Statistical Significance Example: - *

-     * // Determine if statistics are reliable
-     * public String getSkillReliability(Player player) {
-     *     if (player.matchesPlayed == 0) {
-     *         return "No data";
-     *     } else if (player.matchesPlayed < 5) {
-     *         return "Very limited data";
-     *     } else if (player.matchesPlayed < 10) {
-     *         return "Limited data";
-     *     } else if (player.matchesPlayed < 30) {
-     *         return "Moderate data";
-     *     } else {
-     *         return "Reliable data";
-     *     }
-     * }
-     * 
- *

- *

- * Filtering and Queries: - *

-     * // Get experienced players only
-     * @Query("SELECT * FROM players WHERE matchesPlayed >= 20 ORDER BY careerAverage DESC")
-     * List<Player> getExperiencedPlayers();
-     * 
-     * // Get players who need more matches for stats
-     * @Query("SELECT * FROM players WHERE matchesPlayed < 10")
-     * List<Player> getNewPlayers();
-     * 
- *

- *

- * Data Consistency: - * The match count should align with the player's career average: - *

    - *
  • If matchesPlayed = 0, careerAverage should be 0.0
  • - *
  • If careerAverage > 0, matchesPlayed should be > 0
  • - *
  • Both values should increase together after each match
  • - *
- *

- *

- * Validation: - *

-     * // Validate data consistency
-     * if (player.matchesPlayed < 0) {
-     *     Log.e(TAG, "Invalid match count: " + player.matchesPlayed);
-     *     player.matchesPlayed = 0;
-     * }
-     * 
-     * if (player.matchesPlayed == 0 && player.careerAverage != 0.0) {
-     *     Log.w(TAG, "Inconsistent data: 0 matches but non-zero average");
-     *     player.careerAverage = 0.0;
-     * }
-     * 
- *

- *

- * Match Type Consideration: - * This counter increments regardless of match type: - *

    - *
  • 1v1 head-to-head matches
  • - *
  • Group matches (3+ players)
  • - *
  • Solo practice sessions (if tracked)
  • - *
  • Tournament games
  • - *
- * Consider adding separate counters for different match types if detailed - * tracking is needed. - *

- *

- * UI Presentation: - * Match count is typically displayed alongside the career average: - *

-     * // In player card or profile
-     * averageText.setText(String.format("%.1f avg", player.careerAverage));
-     * matchesText.setText(player.matchesPlayed + " matches");
-     * 
-     * // Or combined
-     * statsText.setText(String.format("%.1f avg • %d matches", 
-     *                   player.careerAverage, player.matchesPlayed));
-     * 
- *

- *

- * Maximum Value: - * Using {@code int} supports up to 2,147,483,647 matches, which is essentially - * unlimited for any realistic usage. Even playing 10 matches per day, it would - * take 588,000+ years to overflow. - *

- * - * @see #careerAverage - * @see com.aldo.apps.ochecompanion.database.objects.Match + * Total number of completed matches for this player. Provides context for + * statistical significance and enables experience-based features. */ - public int matchesPlayed = 0; + public int mMatchesPlayed = 0; /** - * Constructs a new Player with the specified username and profile picture. - *

- * This constructor creates a player object ready for insertion into the database. - * The player's ID will be auto-generated by Room upon insertion, and statistical - * fields (careerAverage, matchesPlayed) are initialized to their default values - * of 0.0 and 0 respectively. - *

- *

- * Required Parameters: - *

    - *
  • username: The player's display name (should be non-null)
  • - *
  • profilePictureUri: Path to profile image (can be null for - * default avatar)
  • - *
- *

- *

- * Usage Examples: - *

-     * // Create player with custom profile picture
-     * String picturePath = "/data/data/app/files/profile_pics/player_123.jpg";
-     * Player player1 = new Player("John Doe", picturePath);
-     * 
-     * // Create player without profile picture (will use default avatar)
-     * Player player2 = new Player("Jane Smith", null);
-     * 
-     * // Create player with empty string (equivalent to null for display purposes)
-     * Player player3 = new Player("Bob Wilson", "");
-     * 
-     * // Insert into database (background thread required)
-     * new Thread(() -> {
-     *     playerDao.insert(player1);
-     *     // After insertion, player1.id contains auto-generated ID
-     *     // careerAverage is 0.0, matchesPlayed is 0
-     * }).start();
-     * 
- *

- *

- * Parameter Validation: - * While the constructor doesn't enforce validation, it's recommended to validate - * parameters before construction: - *

-     * // Validate before creating player
-     * String trimmedName = username.trim();
-     * if (trimmedName.isEmpty()) {
-     *     throw new IllegalArgumentException("Username cannot be empty");
-     * }
-     * if (trimmedName.length() > 30) {
-     *     throw new IllegalArgumentException("Username too long (max 30 chars)");
-     * }
-     * 
-     * // Validate profile picture path if provided
-     * if (profilePictureUri != null && !profilePictureUri.isEmpty()) {
-     *     File pictureFile = new File(profilePictureUri);
-     *     if (!pictureFile.exists()) {
-     *         Log.w(TAG, "Profile picture file does not exist: " + profilePictureUri);
-     *         // Decide whether to use null or keep invalid path
-     *     }
-     * }
-     * 
-     * // Create player with validated data
-     * Player player = new Player(trimmedName, profilePictureUri);
-     * 
- *

- *

- * Field Initialization: - * After construction, the player object has these values: - *

    - *
  • id: 0 (will be auto-generated on insert)
  • - *
  • username: Set to provided value
  • - *
  • profilePictureUri: Set to provided value (can be null)
  • - *
  • careerAverage: 0.0 (default initialization)
  • - *
  • matchesPlayed: 0 (default initialization)
  • - *
- *

- *

- * Typical Creation Flow: - *

-     * // In AddPlayerActivity after user completes form
-     * 
-     * // 1. Get username from input
-     * String username = usernameEditText.getText().toString().trim();
-     * 
-     * // 2. Get profile picture path (or null if no picture selected)
-     * String profilePicUri = (croppedImageFile != null) ? 
-     *                        croppedImageFile.getAbsolutePath() : null;
-     * 
-     * // 3. Create player object
-     * final Player newPlayer = new Player(username, profilePicUri);
-     * 
-     * // 4. Insert into database on background thread
-     * new Thread(() -> {
-     *     playerDao.insert(newPlayer);
-     *     
-     *     runOnUiThread(() -> {
-     *         Toast.makeText(this, "Player added to squad!", Toast.LENGTH_SHORT).show();
-     *         finish();  // Return to main menu
-     *     });
-     * }).start();
-     * 
- *

- *

- * Alternative Construction Approach: - * For more complex initialization, create and then modify: - *

-     * // Create base player
-     * Player player = new Player("Alice Johnson", profilePath);
-     * 
-     * // Set initial statistics if importing from another system
-     * player.careerAverage = 78.5;  // Imported average
-     * player.matchesPlayed = 42;     // Imported match count
-     * 
-     * // Insert with pre-populated stats
-     * playerDao.insert(player);
-     * 
- *

- *

- * Null Handling: - * The constructor accepts null for profilePictureUri, which is valid and indicates - * no custom profile picture. The UI should display a default avatar in this case. - * However, passing null for username will create a player with a null name, which - * should be avoided as it causes UI issues. - *

- *

- * Database Insertion: - * Remember that the Player object is not persisted until explicitly inserted: - *

-     * Player player = new Player("Tom", null);
-     * // player exists only in memory at this point
-     * 
-     * playerDao.insert(player);
-     * // now player is persisted in database and player.id is set
-     * 
- *

- *

- * Room Constructor Requirements: - * Room requires this constructor to instantiate Player objects when reading from - * the database. The constructor parameters must match the entity fields (excluding - * the auto-generated ID and default-initialized fields). - *

- * - * @param username The display name for the player. Should be non-null, non-empty, - * and ideally 1-30 characters. Supports Unicode for international - * names. This will be shown throughout the app in player lists, - * match results, and statistics. - * @param profilePictureUri The file system path or content URI to the player's - * profile picture. Can be null or empty string, in which - * case the UI will display a default avatar. Should point - * to a valid image file if provided. - * @see #id - * @see #username - * @see #profilePictureUri - * @see #careerAverage - * @see #matchesPlayed - * @see com.aldo.apps.ochecompanion.AddPlayerActivity - * @see com.aldo.apps.ochecompanion.database.dao.PlayerDao#insert(Player) + * Constructs a new Player ready for database insertion. ID is auto-generated + * by Room, statistical fields default to 0. + * + * @param username Player's display name (should be non-null, 1-30 chars) + * @param profilePictureUri Path to profile image (null for default avatar) */ public Player(final String username, final String profilePictureUri) { - // Initialize the player's identity fields with provided values - // Statistical fields (careerAverage, matchesPlayed) use their default values (0.0 and 0) - // The id field remains 0 and will be auto-generated by Room upon database insertion - this.username = username; - this.profilePictureUri = profilePictureUri; + this.mUsername = username; + this.mProfilePictureUri = profilePictureUri; } /** - * Parcelable constructor used by the CREATOR to reconstruct the object. - * @param in The Parcel containing the serialized data. + * Parcelable constructor to reconstruct Player from Parcel. + * + * @param in Parcel containing serialized Player data */ protected Player(final Parcel in) { - id = in.readInt(); - username = in.readString(); - profilePictureUri = in.readString(); - careerAverage = in.readDouble(); - matchesPlayed = in.readInt(); + mId = in.readInt(); + mUsername = in.readString(); + mProfilePictureUri = in.readString(); + mCareerAverage = in.readDouble(); + mMatchesPlayed = in.readInt(); } /** - * Required CREATOR field for Parcelable implementation. + * Required Parcelable CREATOR field for Player reconstruction. */ - public static final Creator CREATOR = new Creator() { + public static final Creator sCREATOR = new Creator() { @Override - public Player createFromParcel(Parcel in) { + public Player createFromParcel(final Parcel in) { return new Player(in); } @Override - public Player[] newArray(int size) { + public Player[] newArray(final int size) { return new Player[size]; } }; @@ -981,134 +93,34 @@ public class Player implements Parcelable { } /** - * Flattens the object into a Parcel. + * Writes Player fields to Parcel for serialization. + * + * @param dest Destination Parcel + * @param flags Additional flags */ @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(id); - dest.writeString(username); - dest.writeString(profilePictureUri); - dest.writeDouble(careerAverage); - dest.writeInt(matchesPlayed); + public void writeToParcel(final Parcel dest, final int flags) { + dest.writeInt(mId); + dest.writeString(mUsername); + dest.writeString(mProfilePictureUri); + dest.writeDouble(mCareerAverage); + dest.writeInt(mMatchesPlayed); } /** - * Returns a string representation of this Player object for debugging and logging. - *

- * This method provides a human-readable representation of the player's state, - * including all field values. It's primarily used for debugging, logging, and - * development purposes to quickly inspect player data without a debugger. - *

- *

- * Output Format: - * The returned string follows this pattern: - *

-     * Player{id=15, username='John Doe', profilePictureUri='/path/to/pic.jpg', careerAverage=85.3, matchesPlayed=42}
-     * 
- *

- *

- * Usage Examples: - *

-     * // Logging player creation
-     * Player player = new Player("Alice", "/path/to/pic.jpg");
-     * Log.d(TAG, "Created: " + player.toString());
-     * // Output: Player{id=0, username='Alice', profilePictureUri='/path/to/pic.jpg', careerAverage=0.0, matchesPlayed=0}
-     * 
-     * // Debugging database queries
-     * List<Player> players = playerDao.getAllPlayers();
-     * for (Player p : players) {
-     *     Log.d(TAG, p.toString());
-     * }
-     * 
-     * // Quick inspection during development
-     * System.out.println(player);  // Implicitly calls toString()
-     * 
-     * // Logging state changes
-     * Log.d(TAG, "Before update: " + player);
-     * player.careerAverage = 90.0;
-     * Log.d(TAG, "After update: " + player);
-     * 
- *

- *

- * Field Inclusion: - * All fields are included in the string representation: - *

    - *
  • id: Shows the database ID (0 before insertion)
  • - *
  • username: Enclosed in single quotes to show exact value
  • - *
  • profilePictureUri: Full path string in single quotes
  • - *
  • careerAverage: Shown with decimal precision
  • - *
  • matchesPlayed: Integer count
  • - *
- *

- *

- * Null Handling: - * If profilePictureUri is null, it will be displayed as the literal string "null": - *

-     * Player{id=5, username='Bob', profilePictureUri='null', careerAverage=0.0, matchesPlayed=0}
-     * 
- *

- *

- * Not for UI Display: - * This method is NOT intended for user-facing text. For UI display, format - * individual fields appropriately: - *

-     * // DON'T use toString() in UI
-     * textView.setText(player.toString());  // Shows debug format
-     * 
-     * // DO format for UI display
-     * textView.setText(player.username);
-     * statsText.setText(String.format("%.1f avg • %d matches", 
-     *                   player.careerAverage, player.matchesPlayed));
-     * 
- *

- *

- * Logging Best Practices: - *

-     * // Verbose logging for detailed debugging
-     * Log.v(TAG, "Player loaded: " + player);
-     * 
-     * // Debug logging for development
-     * Log.d(TAG, "Updated player stats: " + player);
-     * 
-     * // Error logging with context
-     * Log.e(TAG, "Failed to save player: " + player, exception);
-     * 
-     * // Conditional logging
-     * if (BuildConfig.DEBUG) {
-     *     Log.d(TAG, "All players: " + players.toString());
-     * }
-     * 
- *

- *

- * Performance Note: - * String concatenation in toString() creates temporary objects. Avoid calling - * toString() in performance-critical loops unless necessary. For production builds, - * consider using Timber or similar libraries that can be disabled in release builds. - *

- *

- * Privacy Consideration: - * Be cautious when logging player data in production: - *

    - *
  • Username is personally identifiable information
  • - *
  • Profile picture paths might reveal device file structure
  • - *
  • Consider redacting sensitive info in production logs
  • - *
  • Ensure logs don't leak to crash reporting services unintentionally
  • - *
- *

- * - * @return A string representation of this Player object in the format: - * "Player{id=[id], username='[name]', profilePictureUri='[uri]', - * careerAverage=[avg], matchesPlayed=[count]}" - * @see Object#toString() + * Returns string representation of Player for debugging and logging. + * Not intended for UI display. + * + * @return String in format "Player{mId=X, mUsername='...', ...}" */ @Override public String toString() { return "Player{" + - "id=" + id + - ", username='" + username + '\'' + - ", profilePictureUri='" + profilePictureUri + '\'' + - ", careerAverage=" + careerAverage + - ", matchesPlayed=" + matchesPlayed + + "mId=" + mId + + ", mUsername='" + mUsername + '\'' + + ", mProfilePictureUri='" + mProfilePictureUri + '\'' + + ", mCareerAverage=" + mCareerAverage + + ", mMatchesPlayed=" + mMatchesPlayed + '}'; } } diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/models/Match.java b/app/src/main/java/com/aldo/apps/ochecompanion/models/Match.java index 2ef177c..288a0ca 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/models/Match.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/models/Match.java @@ -10,125 +10,21 @@ import com.aldo.apps.ochecompanion.database.objects.Player; /** * Model class representing a darts match with multiple participants. - *

- * This class serves as a data container for match information in the Oche Companion app, - * maintaining a collection of players participating in a single darts game session. - * It provides convenient methods for accessing player information by position and - * retrieving match statistics. - *

- *

- * Key Features: - *

    - *
  • Support for matches with any number of players (1v1, group matches, etc.)
  • - *
  • Position-based player access for ordered operations
  • - *
  • Convenient retrieval of player names and career averages
  • - *
  • Flexible construction with varargs or default empty initialization
  • - *
- *

- *

- * Match Types: - * This class supports different match configurations: - *

    - *
  • 1v1 Match: Exactly 2 players for head-to-head competition
  • - *
  • Group Match: 3 or more players for multi-player games
  • - *
  • Solo Practice: Single player for practice sessions (if supported)
  • - *
- *

- *

- * Player Ordering: - * Players are stored in the order they are added. This ordering is significant for: - *

    - *
  • Displaying players in consistent positions (left/right, top/bottom)
  • - *
  • Turn order during gameplay
  • - *
  • Result display and leaderboards
  • - *
- *

- *

- * Usage Examples: - *

- * // Create a 1v1 match
- * Match match = new Match(player1, player2);
- * 
- * // Create a group match
- * Match groupMatch = new Match(player1, player2, player3, player4);
- * 
- * // Create an empty match and add players later
- * Match emptyMatch = new Match();
- * // (Note: No public add method currently, consider adding if needed)
- * 
- *

- *

- * Design Notes: - *

    - *
  • The class is mutable in that it stores player references, but doesn't provide - * methods to add/remove players after construction
  • - *
  • Player data (names, averages) is accessed directly from Player objects
  • - *
  • Currently focused on displaying match information rather than tracking live gameplay
  • - *
- *

- * - * @see Player - * @see com.aldo.apps.ochecompanion.ui.MatchRecapView - * @author Oche Companion Development Team - * @version 1.0 - * @since 1.0 */ public class Match { /** - * Tag for logging and debugging purposes. - * Used to identify log messages originating from the Match class. + * Tag for logging. */ private static final String TAG = "Match"; /** - * Internal list of players participating in this match. - *

- * Players are stored in the order they were added during match creation. - * This ordering is preserved and can be used for position-based queries - * via {@link #getPlayerNameByPosition(int)} and {@link #getPlayerAverageByPosition(int)}. - *

- *

- * The list is initialized as an ArrayList to provide efficient random access - * by index, which is the primary access pattern for this class. - *

- *

- * Immutability Note: - * While the list reference is final, the list contents are mutable. However, - * no public methods currently allow modification after construction. - *

- * - * @see #Match(Player...) - * @see #getAllPlayers() - * @see #getParticipantCount() + * List of players participating in this match. */ private final List mPlayers; /** * Constructs an empty Match with no participants. - *

- * This constructor creates a new match instance with an empty player list. - * It's useful for scenarios where players will be added dynamically later, - * or for placeholder/initialization purposes. - *

- *

- * Usage Scenarios: - *

    - *
  • Creating a match template before players are selected
  • - *
  • Initialization in builders or factory methods
  • - *
  • Default construction in frameworks requiring no-arg constructors
  • - *
- *

- *

- * Note: - * Currently, there are no public methods to add players after construction. - * Consider using {@link #Match(Player...)} if players are known at creation time. - *

- *

- * A debug log message is generated when an empty match is created. - *

- * - * @see #Match(Player...) */ public Match() { // Initialize empty player list @@ -139,57 +35,8 @@ public class Match { /** * Constructs a Match with the specified players. - *

- * This constructor creates a new match instance and populates it with the provided - * players. The players are added in the order they appear in the parameter list, - * which establishes their position indices for future queries. - *

- *

- * Varargs Convenience: - * The varargs parameter allows flexible calling patterns: - *

-     * // 1v1 match
-     * Match match1 = new Match(player1, player2);
-     * 
-     * // Group match
-     * Match match2 = new Match(player1, player2, player3, player4);
-     * 
-     * // Single player (if supported)
-     * Match match3 = new Match(player1);
-     * 
-     * // From array
-     * Player[] playerArray = {player1, player2, player3};
-     * Match match4 = new Match(playerArray);
-     * 
- *

- *

- * Player Ordering: - * Players are stored in the exact order they are passed. For example: - *

    - *
  • First player: position 0
  • - *
  • Second player: position 1
  • - *
  • And so on...
  • - *
- * This ordering is important for methods like {@link #getPlayerNameByPosition(int)}. - *

- *

- * Logging: - * Each player addition is logged at debug level for troubleshooting and verification. - *

- *

- * Null Safety: - * This constructor does not explicitly check for null players. Callers should ensure - * all provided Player objects are non-null to avoid NullPointerExceptions in - * subsequent operations. - *

- * + * * @param players Variable number of Player objects to participate in the match. - * Can be empty (equivalent to calling {@link #Match()}), - * but typically contains 2 or more players for competitive games. - * Players should not be null. - * @see Player - * @see #Match() - * @see #getParticipantCount() */ public Match(final Player... players) { // Initialize empty player list @@ -206,37 +53,8 @@ public class Match { /** * Returns the number of players participating in this match. - *

- * This method provides the count of players currently registered for the match. - * The count corresponds to the number of players added during match construction. - *

- *

- * Usage Examples: - *

-     * Match match1v1 = new Match(player1, player2);
-     * int count1 = match1v1.getParticipantCount(); // Returns 2
-     * 
-     * Match groupMatch = new Match(p1, p2, p3, p4);
-     * int count2 = groupMatch.getParticipantCount(); // Returns 4
-     * 
-     * Match emptyMatch = new Match();
-     * int count3 = emptyMatch.getParticipantCount(); // Returns 0
-     * 
- *

- *

- * Use Cases: - * This method is commonly used to: - *

    - *
  • Determine which UI layout to display (1v1 vs group match views)
  • - *
  • Validate match configuration before starting gameplay
  • - *
  • Loop through players without accessing the list directly
  • - *
  • Check if the match has any participants
  • - *
- *

- * - * @return The number of players in this match. Returns 0 if the match is empty. - * @see #getAllPlayers() - * @see com.aldo.apps.ochecompanion.ui.MatchRecapView#setMatch(Match) + * + * @return The number of players in this match. */ public int getParticipantCount() { return mPlayers.size(); @@ -244,50 +62,9 @@ public class Match { /** * Retrieves the username of the player at the specified position. - *

- * This method provides position-based access to player names, useful for displaying - * players in specific UI locations (e.g., player 1 on the left, player 2 on the right - * in a 1v1 display). - *

- *

- * Position Indexing: - * Positions are zero-based indices: - *

    - *
  • Position 0: First player added to the match
  • - *
  • Position 1: Second player added to the match
  • - *
  • Position n: (n+1)th player added to the match
  • - *
- *

- *

- * Bounds Checking: - * The method includes basic bounds validation. If the position is out of range - * (negative or greater than the number of players), it returns "INVALID" rather - * than throwing an exception. - *

- *

- * Note on Bounds Check: - * The current implementation has a potential bug: the condition {@code position <= mPlayers.size()} - * should likely be {@code position < mPlayers.size()} since valid indices are 0 to (size-1). - * The current code may throw an IndexOutOfBoundsException when position equals size. - *

- *

- * Usage Example: - *

-     * Match match = new Match(player1, player2, player3);
-     * String name0 = match.getPlayerNameByPosition(0); // Returns player1.username
-     * String name1 = match.getPlayerNameByPosition(1); // Returns player2.username
-     * String name2 = match.getPlayerNameByPosition(2); // Returns player3.username
-     * String invalid = match.getPlayerNameByPosition(3); // Returns "INVALID"
-     * 
- *

- * + * * @param position The zero-based index of the player in the match. - * Should be in the range [0, participantCount). - * @return The username of the player at the specified position, or "INVALID" - * if the position is out of bounds. - * @see Player#username - * @see #getPlayerAverageByPosition(int) - * @see #getParticipantCount() + * @return The username of the player at the specified position, or "INVALID" if the position is out of bounds. */ public String getPlayerNameByPosition(final int position) { // Validate position is within bounds @@ -302,56 +79,9 @@ public class Match { /** * Retrieves the career average of the player at the specified position. - *

- * This method provides position-based access to player career statistics, useful for - * displaying performance metrics in match summaries and leaderboards. - *

- *

- * Position Indexing: - * Positions are zero-based indices: - *

    - *
  • Position 0: First player added to the match
  • - *
  • Position 1: Second player added to the match
  • - *
  • Position n: (n+1)th player added to the match
  • - *
- *

- *

- * Bounds Checking: - * The method includes basic bounds validation. If the position is out of range - * (negative or greater than the number of players), it returns -1 as a sentinel - * value rather than throwing an exception. - *

- *

- * Note on Bounds Check: - * The current implementation has a potential bug: the condition {@code position <= mPlayers.size()} - * should likely be {@code position < mPlayers.size()} since valid indices are 0 to (size-1). - * The current code may throw an IndexOutOfBoundsException when position equals size. - *

- *

- * Career Average: - * The returned value represents the player's career average score across all matches - * they've played, as stored in {@link Player#careerAverage}. This is typically - * calculated and updated by other parts of the application. - *

- *

- * Usage Example: - *

-     * Match match = new Match(player1, player2, player3);
-     * double avg0 = match.getPlayerAverageByPosition(0); // Returns player1.careerAverage
-     * double avg1 = match.getPlayerAverageByPosition(1); // Returns player2.careerAverage
-     * double avg2 = match.getPlayerAverageByPosition(2); // Returns player3.careerAverage
-     * double invalid = match.getPlayerAverageByPosition(3); // Returns -1
-     * 
- *

- * + * * @param position The zero-based index of the player in the match. - * Should be in the range [0, participantCount). - * @return The career average of the player at the specified position, or -1 - * if the position is out of bounds. The average is a double value - * representing the player's historical performance. - * @see Player#careerAverage - * @see #getPlayerNameByPosition(int) - * @see #getParticipantCount() + * @return The career average of the player at the specified position, or -1 if the position is out of bounds. */ public double getPlayerAverageByPosition(final int position) { // Validate position is within bounds @@ -365,102 +95,18 @@ public class Match { } /** - * Returns a direct reference to the internal list of all players in this match. - *

- * This method provides access to the complete player list, useful for operations - * that need to process all players (e.g., sorting, filtering, bulk display). - *

- *

- * List Contents: - * The returned list contains players in the order they were added during match - * construction. The list is the same instance used internally by this Match object. - *

- *

- * Mutability Warning: - * This method returns a direct reference to the internal list, not a copy. - * Modifications to the returned list will affect the match's internal state: - *

-     * List<Player> players = match.getAllPlayers();
-     * players.clear(); // WARNING: This clears the match's player list!
-     * 
- * If you need to modify the list without affecting the match, create a copy: - *
-     * List<Player> playersCopy = new ArrayList<>(match.getAllPlayers());
-     * playersCopy.clear(); // Safe: Only affects the copy
-     * 
- *

- *

- * Common Use Cases: - *

    - *
  • Populating adapters for RecyclerViews (e.g., {@link com.aldo.apps.ochecompanion.ui.adapter.MainMenuGroupMatchAdapter})
  • - *
  • Sorting players by score for leaderboard display
  • - *
  • Iterating through all players for statistics calculation
  • - *
  • Filtering players based on specific criteria
  • - *
- *

- *

- * Performance: - * This is an O(1) operation as it returns a reference, not a copy. - *

- * - * @return A direct reference to the list of all Player objects in this match. - * The list maintains the order in which players were added. - * Never null, but may be empty if no players were added. - * @see Player - * @see #getParticipantCount() - * @see com.aldo.apps.ochecompanion.ui.adapter.MainMenuGroupMatchAdapter#updateMatch(Match) + * Returns the list of all players in this match. + * + * @return The list of all Player objects in this match. */ public List getAllPlayers() { return mPlayers; } /** - * Returns a string representation of this Match for debugging and logging. - *

- * This method generates a human-readable string that includes information about - * all players participating in the match. The format is designed to be concise - * yet informative for debugging purposes. - *

- *

- * Output Format: - * The generated string follows this pattern: - *

-     * Match {[Player1][Player2][Player3]]
-     * 
- * Each player is represented by its own {@link Player#toString()} output, - * wrapped in square brackets. - *

- *

- * Example Output: - *

-     * Match {[Player{name='Alice', avg=45.5}][Player{name='Bob', avg=52.3}]]
-     * 
- *

- *

- * Note on Formatting: - * The method includes an extra closing bracket at the end, which appears to be - * unintentional. The string ends with "]]" instead of "}". Consider changing - * the final append from "].append("]")} to just "}") for proper bracket matching. - *

- *

- * Performance: - * Uses {@link StringBuilder} for efficient string concatenation, which is important - * when the match contains many players. - *

- *

- * Usage: - * This method is automatically called when: - *

    - *
  • Logging a Match object: {@code Log.d(TAG, "Match: " + match)}
  • - *
  • Concatenating with strings: {@code "Current match: " + match}
  • - *
  • Debugging in IDE debugger (some IDEs display toString() output)
  • - *
- *

- * + * Returns a string representation of this Match. + * * @return A string representation of this match including all participating players. - * Never null. - * @see Player#toString() - * @see StringBuilder */ @NonNull @Override diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/CropOverlayView.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/CropOverlayView.java index 6fd5b52..5085f61 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/CropOverlayView.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/CropOverlayView.java @@ -11,346 +11,70 @@ import android.view.View; import androidx.annotation.Nullable; /** - * Custom view that provides a visual cropping guide overlay for image selection. - *

- * This view is designed for use in the Oche Companion app's image cropping interface, - * specifically within the {@link com.aldo.apps.ochecompanion.AddPlayerActivity}. - * It creates a semi-transparent dark overlay across the entire view with a transparent - * square "window" in the center, allowing users to see exactly which portion of their - * image will be captured as their profile picture. - *

- *

- * Visual Design: - *

    - *
  • Dark semi-transparent mask (85% opacity Midnight Black) covers the entire view
  • - *
  • Transparent square cutout in the center shows the crop area
  • - *
  • Crop box is 80% of the view's width, maintaining a square aspect ratio
  • - *
  • Automatically centers the crop box within the view
  • - *
- *

- *

- * Technical Implementation: - * The overlay uses a {@link Path} with clockwise and counter-clockwise rectangles to create - * a "hole punch" effect. The outer rectangle (entire view) is drawn clockwise, while the - * inner rectangle (crop area) is drawn counter-clockwise. This winding direction technique - * causes the inner rectangle to subtract from the outer one, creating a transparent window. - *

- *

- * Usage: - * This view is typically overlaid on top of an {@link android.widget.ImageView} in crop mode. - * The parent activity can retrieve the crop rectangle coordinates via {@link #getCropRect()} - * to perform the actual pixel-level cropping calculations. - *

- * - * @see com.aldo.apps.ochecompanion.AddPlayerActivity - * @see Path - * @see RectF - * @author Oche Companion Development Team - * @version 1.0 - * @since 1.0 + * Visual cropping guide overlay with semi-transparent mask and center crop window. + * Uses path winding technique to create transparent square cutout (80% of width). */ public class CropOverlayView extends View { - /** - * Paint object for rendering the semi-transparent dark overlay mask. - *

- * Configured with: - *

    - *
  • Anti-aliasing enabled for smooth edges
  • - *
  • Color: Midnight Black (#0A0A0A) at 85% opacity (#D90A0A0A)
  • - *
  • Style: FILL to cover the entire path area
  • - *
- *

- * The high opacity (85%) provides good contrast while still allowing the - * underlying image to be visible enough for positioning. - */ + /** Paint for semi-transparent dark overlay (85% opacity). */ private final Paint mMaskPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - /** - * Rectangle defining the boundaries of the transparent crop area in screen coordinates. - *

- * This rectangle represents the "window" through which the user sees the unobscured - * portion of their image. The coordinates are calculated in {@link #onLayout(boolean, int, int, int, int)} - * and are used both for drawing the overlay and for providing crop coordinates to the - * parent activity. - *

- *

- * The rectangle dimensions are: - *

    - *
  • Width: 80% of the view's width
  • - *
  • Height: Equal to width (square aspect ratio)
  • - *
  • Position: Centered horizontally and vertically
  • - *
- *

- * - * @see #getCropRect() - * @see #onLayout(boolean, int, int, int, int) - */ + /** Rectangle defining transparent crop area boundaries (80% width, centered). */ private final RectF mCropRect = new RectF(); - /** - * Path object used to create the overlay mask with a transparent center hole. - *

- * This path consists of two rectangles: - *

    - *
  1. Outer Rectangle (Clockwise): Covers the entire view area
  2. - *
  3. Inner Rectangle (Counter-Clockwise): Defines the crop area
  4. - *
- *

- *

- * The opposing winding directions create a "hole punch" effect where the inner - * rectangle subtracts from the outer one, resulting in a transparent window. - * This technique leverages Android's path fill-type rules (even-odd or winding). - *

- *

- * The path is recalculated whenever the view's layout changes to ensure proper - * sizing and positioning. - *

- * - * @see Path.Direction - * @see #onLayout(boolean, int, int, int, int) - */ + /** Path with CW outer rect and CCW inner rect creating transparent hole. */ private final Path mPath = new Path(); - /** - * The calculated side length of the square crop box in pixels. - *

- * This value is computed in {@link #onLayout(boolean, int, int, int, int)} as - * 80% of the view's width. It's stored for potential reuse and to maintain - * consistency between layout calculations. - *

- *

- * The 80% size ensures adequate padding around the crop area while maximizing - * the useful cropping space. - *

- */ + /** Calculated side length of square crop box (80% of width). */ private float mBoxSize; - /** - * Constructor for programmatic instantiation of the CropOverlayView. - *

- * This constructor is used when creating the view directly in Java/Kotlin code - * rather than inflating from XML. It initializes the view and configures all - * necessary paint and drawing resources. - *

- * - * @param context The Context in which the view is running, through which it can - * access the current theme, resources, etc. - * @see #init() - */ - public CropOverlayView(Context context) { + /** Constructor for programmatic instantiation. */ + public CropOverlayView(final Context context) { super(context); init(); } - /** - * Constructor for XML inflation of the CropOverlayView. - *

- * This constructor is called when the view is inflated from an XML layout file. - * It allows the view to be defined declaratively in layout resources. The AttributeSet - * parameter provides access to any XML attributes defined for this view. - *

- * - * @param context The Context in which the view is running, through which it can - * access the current theme, resources, etc. - * @param attrs The attributes of the XML tag that is inflating the view. May be null - * if no attributes are specified. - * @see #init() - */ - public CropOverlayView(Context context, @Nullable AttributeSet attrs) { + /** Constructor for XML inflation. */ + public CropOverlayView(final Context context, @Nullable final AttributeSet attrs) { super(context, attrs); init(); } - /** - * Constructor for XML inflation of the CropOverlayView with a specific style. - *

- * This constructor is called when the view is inflated from an XML layout file - * with a style attribute. It allows for theme-based customization of the view's - * appearance, though this view currently uses hardcoded visual properties. - *

- * - * @param context The Context in which the view is running, through which it can - * access the current theme, resources, etc. - * @param attrs The attributes of the XML tag that is inflating the view. May be null - * if no attributes are specified. - * @param defStyleAttr An attribute in the current theme that contains a reference to - * a style resource that supplies default values for the view. - * Can be 0 to not look for defaults. - * @see #init() - */ - public CropOverlayView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + /** Constructor for XML inflation with style. */ + public CropOverlayView(final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } - /** - * Initializes the Paint object used for rendering the overlay mask. - *

- * This method configures the visual properties of the semi-transparent overlay - * that will darken the non-crop area of the image. The configuration includes: - *

    - *
  • Color: Midnight Black (#0A0A0A) with 85% opacity
  • - *
  • Style: FILL to cover the entire masked area
  • - *
  • Anti-aliasing: Already enabled via constructor flag
  • - *
- *

- *

- * Color Choice Rationale: - * The 85% opacity (0xD9) provides strong contrast to highlight the crop area - * while maintaining enough transparency for users to see the portions of the - * image that will be cropped out. This helps with precise positioning. - *

- *

- * This method is called by all constructors to ensure consistent initialization - * regardless of how the view is instantiated. - *

- * - * @see Paint.Style#FILL - */ + /** Initializes paint with Midnight Black at 85% opacity. */ private void init() { - // Set darkened background color: Midnight Black (#0A0A0A) at 85% opacity - // Alpha value 0xD9 = 217/255 ≈ 85% mMaskPaint.setColor(Color.parseColor("#D90A0A0A")); - - // Use FILL style to cover the entire path area (except the hole) mMaskPaint.setStyle(Paint.Style.FILL); } - /** - * Called when the view's size or position changes to recalculate the crop area. - *

- * This method is responsible for: - *

    - *
  1. Calculating the size of the square crop box (80% of view width)
  2. - *
  3. Centering the crop box both horizontally and vertically
  4. - *
  5. Updating the crop rectangle coordinates
  6. - *
  7. Rebuilding the path used for rendering the overlay mask
  8. - *
- *

- *

- * Crop Box Sizing: - * The crop box is sized at 80% of the view's width to provide: - *

    - *
  • Adequate padding (10% on each side) for visual clarity
  • - *
  • Sufficient space for users to see what will be cropped out
  • - *
  • Maximum useful cropping area without overwhelming the interface
  • - *
- *

- *

- * Path Construction: - * The path is built with two rectangles using opposite winding directions: - *

    - *
  • Outer (CW): Full view bounds - creates the mask base
  • - *
  • Inner (CCW): Crop area - subtracts from the mask
  • - *
- * This winding technique creates the transparent "hole" effect. - *

- * - * @param changed True if this view's size or position has changed since the last layout. - * @param left The left position, relative to the parent. - * @param top The top position, relative to the parent. - * @param right The right position, relative to the parent. - * @param bottom The bottom position, relative to the parent. - * @see Path.Direction - */ + /** Recalculates crop area and path when view size changes. */ @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + protected void onLayout(final boolean changed, final int left, final int top, final int right, final int bottom) { super.onLayout(changed, left, top, right, bottom); - // Calculate the crop box size as 80% of the view's width mBoxSize = getWidth() * 0.8f; - - // Calculate the left position to center the box horizontally - float l = (getWidth() - mBoxSize) / 2; - - // Calculate the top position to center the box vertically - float t = (getHeight() - mBoxSize) / 2; - - // Set the crop rectangle coordinates + final float l = (getWidth() - mBoxSize) / 2; + final float t = (getHeight() - mBoxSize) / 2; mCropRect.set(l, t, l + mBoxSize, t + mBoxSize); - // Pre-calculate the path for the mask with a transparent center hole - mPath.reset(); // Clear any previous path data - - // Add outer rectangle covering the entire view (clockwise) + mPath.reset(); mPath.addRect(0, 0, getWidth(), getHeight(), Path.Direction.CW); - - // Add inner rectangle for the crop area (counter-clockwise) - // The opposite direction creates a "hole" in the mask mPath.addRect(mCropRect, Path.Direction.CCW); } - /** - * Renders the semi-transparent overlay mask onto the canvas. - *

- * This method is called by the Android framework whenever the view needs to be drawn. - * It draws the pre-calculated path that contains the full-screen mask with a - * transparent square cutout in the center. - *

- *

- * Rendering Process: - * The single {@link Canvas#drawPath(Path, Paint)} call efficiently renders both: - *

    - *
  • The dark semi-transparent overlay covering the entire view
  • - *
  • The transparent crop area in the center (created by the CCW inner rectangle)
  • - *
- *

- *

- * Performance: - * The path is pre-calculated in {@link #onLayout(boolean, int, int, int, int)} rather - * than being recalculated on every draw call, ensuring smooth rendering performance. - *

- * - * @param canvas The Canvas on which the view will be drawn. This canvas is provided - * by the Android framework and is used to draw the overlay mask. - * @see #onLayout(boolean, int, int, int, int) - * @see Canvas#drawPath(Path, Paint) - */ + /** Renders the overlay mask with transparent center cutout. */ @Override - protected void onDraw(Canvas canvas) { + protected void onDraw(final Canvas canvas) { super.onDraw(canvas); - - // Draw the path which contains the full screen minus the square cutout - // The path was pre-calculated in onLayout() for performance canvas.drawPath(mPath, mMaskPaint); } - /** - * Provides the coordinates of the crop box for pixel-level cropping calculations. - *

- * This method returns the rectangle that defines the transparent crop area in screen - * coordinates. The parent activity (typically {@link com.aldo.apps.ochecompanion.AddPlayerActivity}) - * uses these coordinates to calculate which pixels from the source image should be - * extracted for the cropped result. - *

- *

- * Coordinate System: - * The returned RectF contains screen coordinates relative to this view: - *

    - *
  • left: X-coordinate of the left edge of the crop box
  • - *
  • top: Y-coordinate of the top edge of the crop box
  • - *
  • right: X-coordinate of the right edge of the crop box
  • - *
  • bottom: Y-coordinate of the bottom edge of the crop box
  • - *
- *

- *

- * Usage: - * The parent activity must transform these screen coordinates to bitmap pixel coordinates - * by accounting for: - *

    - *
  • ImageView fit-center scaling
  • - *
  • User's manual pan (translation) gestures
  • - *
  • User's pinch-to-zoom (scale) gestures
  • - *
- *

- * - * @return A RectF representing the transparent square crop area in screen coordinates. - * The rectangle defines a square centered in the view with dimensions equal to - * 80% of the view's width. - * @see com.aldo.apps.ochecompanion.AddPlayerActivity#performCrop() - * @see RectF - */ + /** Returns crop box coordinates in screen space. */ public RectF getCropRect() { return mCropRect; } diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/MatchRecapView.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/MatchRecapView.java index faa03dd..975e191 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/MatchRecapView.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/MatchRecapView.java @@ -14,411 +14,105 @@ import com.aldo.apps.ochecompanion.models.Match; import com.aldo.apps.ochecompanion.ui.adapter.MainMenuGroupMatchAdapter; /** - * Custom composite view that displays a summary of the most recently played match. - *

- * This view is designed to provide users with a quick overview of their last game session - * in the Oche Companion app. It intelligently adapts its display based on the match type - * and data availability, supporting three distinct visual states. - *

- *

- * Supported States: - *

    - *
  1. Empty State: Displayed when no match history exists yet. - * Shows a placeholder or welcome message to encourage users to start playing.
  2. - *
  3. 1v1 State: Displayed for head-to-head matches (exactly 2 players). - * Shows a side-by-side comparison of player names and scores.
  4. - *
  5. Group State: Displayed for group matches (3 or more players). - * Shows a mini-leaderboard with all participants sorted by their performance.
  6. - *
- *

- *

- * Key Features: - *

    - *
  • Automatic state switching based on match data
  • - *
  • Clean state management ensuring only one view state is visible at a time
  • - *
  • Efficient RecyclerView implementation for group matches
  • - *
  • Graceful handling of null/missing match data
  • - *
- *

- *

- * Usage: - * This view is typically used in the Main Menu activity to display the most recent match. - * The parent activity calls {@link #setMatch(Match)} to update the display whenever the - * match data changes or when the activity resumes. - *

- * - * @see Match - * @see MainMenuGroupMatchAdapter - * @see FrameLayout - * @author Oche Companion Development Team - * @version 1.0 - * @since 1.0 + * Displays summary of most recent match. Adapts display based on match type: + * empty state (no matches), 1v1 state (2 players), or group state (3+ players). */ public class MatchRecapView extends FrameLayout { - /** - * View container for the empty state display. - *

- * This view is shown when no match history exists in the database. - * It typically contains placeholder content, empty state illustrations, - * or encouraging messages to prompt users to start their first game. - *

- *

- * Visibility is managed by {@link #updateVisibility(View)} to ensure - * only one state is visible at a time. - *

- * - * @see #setMatch(Match) - * @see #updateVisibility(View) - */ - private View stateEmpty; + /** View container for empty state (no match history). */ + private View mStateEmpty; - /** - * View container for the 1v1 match state display. - *

- * This view is shown when the last match was a head-to-head game between - * exactly two players. It presents a side-by-side comparison showing both - * players' names and their respective scores. - *

- *

- * Visibility is managed by {@link #updateVisibility(View)} to ensure - * only one state is visible at a time. - *

- * - * @see #setup1v1State(Match) - * @see #updateVisibility(View) - */ - private View state1v1; + /** View container for 1v1 match state (exactly 2 players). */ + private View mState1v1; - /** - * View container for the group match state display. - *

- * This view is shown when the last match involved 3 or more players. - * It contains a RecyclerView that displays a mini-leaderboard with all - * participants sorted by their performance. - *

- *

- * Visibility is managed by {@link #updateVisibility(View)} to ensure - * only one state is visible at a time. - *

- * - * @see #setupGroupState(Match) - * @see #updateVisibility(View) - */ - private View stateGroup; + /** View container for group match state (3+ players). */ + private View mStateGroup; // ========== 1v1 View References ========== - /** - * TextView displaying the name of the first player in a 1v1 match. - * Used only in the 1v1 state. - */ - private TextView tvP1Name; + /** Player 1 name in 1v1 match. */ + private TextView mTvP1Name; - /** - * TextView displaying the name of the second player in a 1v1 match. - * Used only in the 1v1 state. - */ - private TextView tvP2Name; + /** Player 2 name in 1v1 match. */ + private TextView mTvP2Name; - /** - * TextView displaying the score/average of the first player in a 1v1 match. - * Used only in the 1v1 state. - */ - private TextView tvP1Score; + /** Player 1 score in 1v1 match. */ + private TextView mTvP1Score; - /** - * TextView displaying the score/average of the second player in a 1v1 match. - * Used only in the 1v1 state. - */ - private TextView tvP2Score; + /** Player 2 score in 1v1 match. */ + private TextView mTvP2Score; // ========== Group View References ========== - /** - * RecyclerView displaying the leaderboard for group matches. - *

- * This RecyclerView is configured with a {@link LinearLayoutManager} and uses - * a {@link MainMenuGroupMatchAdapter} to display all participants in the match, - * sorted by their performance scores. - *

- *

- * Used only in the group state. - *

- * - * @see MainMenuGroupMatchAdapter - * @see #setupGroupState(Match) - */ - private RecyclerView rvLeaderboard; + /** RecyclerView displaying leaderboard for group matches. */ + private RecyclerView mRvLeaderboard; - /** - * Constructor for programmatic instantiation of the MatchRecapView. - *

- * This constructor delegates to the two-parameter constructor with a null - * AttributeSet, which in turn inflates the layout and initializes all child views. - *

- * - * @param context The Context in which the view is running, through which it can - * access the current theme, resources, etc. - * @see #MatchRecapView(Context, AttributeSet) - */ + /** Constructor for programmatic instantiation. */ public MatchRecapView(@NonNull final Context context) { this(context, null); } - /** - * Constructor for XML inflation of the MatchRecapView. - *

- * This constructor is called when the view is inflated from an XML layout file. - * It performs the following initialization: - *

    - *
  1. Calls the parent FrameLayout constructor
  2. - *
  3. Inflates the view_match_recap layout into this container
  4. - *
  5. Initializes all child view references via {@link #initViews()}
  6. - *
- *

- *

- * After construction, the view defaults to showing no content until - * {@link #setMatch(Match)} is called with valid match data. - *

- * - * @param context The Context in which the view is running, through which it can - * access the current theme, resources, etc. - * @param attrs The attributes of the XML tag that is inflating the view. May be null - * if no attributes are specified. - * @see #initViews() - */ + /** Constructor for XML inflation. */ public MatchRecapView(@NonNull final Context context, @Nullable final AttributeSet attrs) { super(context, attrs); - // Inflate the layout for this composite view inflate(context, R.layout.view_match_recap, this); - // Initialize all child view references initViews(); } - /** - * Initializes references to all child views within the inflated layout. - *

- * This method retrieves and stores references to all UI components needed for - * the three different states: - *

    - *
  • State containers: Empty, 1v1, and Group view containers
  • - *
  • 1v1 components: Player name and score TextViews
  • - *
  • Group components: Leaderboard RecyclerView
  • - *
- *

- *

- * All views must exist in the R.layout.view_match_recap layout file, - * otherwise this method will throw a NullPointerException. - *

- *

- * This method is called once during construction and does not need to be - * called again during the view's lifecycle. - *

- * - * @see #MatchRecapView(Context, AttributeSet) - */ + /** Initializes references to all child views. */ private void initViews() { - // Initialize state container references - stateEmpty = findViewById(R.id.stateEmpty); - state1v1 = findViewById(R.id.state1v1); - stateGroup = findViewById(R.id.stateGroup); + mStateEmpty = findViewById(R.id.stateEmpty); + mState1v1 = findViewById(R.id.state1v1); + mStateGroup = findViewById(R.id.stateGroup); - // Initialize 1v1 match view references - tvP1Name = findViewById(R.id.tvP1Name); - tvP1Score = findViewById(R.id.tvP1Score); - tvP2Name = findViewById(R.id.tvP2Name); - tvP2Score = findViewById(R.id.tvP2Score); + mTvP1Name = findViewById(R.id.tvP1Name); + mTvP1Score = findViewById(R.id.tvP1Score); + mTvP2Name = findViewById(R.id.tvP2Name); + mTvP2Score = findViewById(R.id.tvP2Score); - // Initialize group match view references - rvLeaderboard = findViewById(R.id.rvLeaderboard); + mRvLeaderboard = findViewById(R.id.rvLeaderboard); } - /** - * Binds a Match object to the view and updates the display accordingly. - *

- * This is the main entry point for updating the view's content. It analyzes the - * provided match data and automatically selects the appropriate state to display: - *

    - *
  • Null match: Shows the empty state
  • - *
  • 2 players: Shows the 1v1 state with head-to-head comparison
  • - *
  • 3+ players: Shows the group state with leaderboard
  • - *
- *

- *

- * State Selection Logic: - * The method first checks for null, then examines the participant count to determine - * which state is appropriate. Each state has its own setup method that handles the - * specific data binding and view configuration. - *

- *

- * Usage: - * This method should be called whenever the match data changes, such as: - *

    - *
  • When the activity first loads
  • - *
  • After a new match is completed
  • - *
  • When resuming the activity to refresh with latest data
  • - *
- *

- * - * @param match The Match object from the database, or null if no match history exists. - * The match should contain all necessary player and score information. - * @see Match#getParticipantCount() - * @see #setup1v1State(Match) - * @see #setupGroupState(Match) - * @see #updateVisibility(View) - */ + /** Binds match and updates display (empty, 1v1, or group state). */ public void setMatch(@Nullable final Match match) { - // Handle null case - no match history exists if (match == null) { - updateVisibility(stateEmpty); + updateVisibility(mStateEmpty); return; } - // Determine which state to show based on participant count if (match.getParticipantCount() > 2) { - // 3+ players: Show group leaderboard setupGroupState(match); } else { - // Exactly 2 players: Show 1v1 comparison setup1v1State(match); } } - /** - * Configures and displays the 1v1 match state. - *

- * This method sets up the view for displaying a head-to-head match between two players. - * It performs the following operations: - *

    - *
  1. Switches visibility to show only the 1v1 state container
  2. - *
  3. Retrieves player data by position (0 for player 1, 1 for player 2)
  4. - *
  5. Populates the player name TextViews
  6. - *
  7. Populates the player score/average TextViews
  8. - *
- *

- *

- * Data Retrieval: - * Player information is retrieved by position index rather than by ID, assuming - * the match stores players in a predictable order. Position 0 corresponds to the - * first player (typically displayed on the left), and position 1 corresponds to - * the second player (typically displayed on the right). - *

- *

- * Assumptions: - * This method assumes the match contains exactly 2 players. The caller - * ({@link #setMatch(Match)}) should verify the participant count before calling this method. - *

- * - * @param match The Match object containing exactly 2 players. Must not be null. - * @see Match#getPlayerNameByPosition(int) - * @see Match#getPlayerAverageByPosition(int) - * @see #updateVisibility(View) - */ + /** Configures 1v1 state with player names and scores. */ private void setup1v1State(final Match match) { - // Switch to 1v1 state visibility - updateVisibility(state1v1); + updateVisibility(mState1v1); - // Populate player 1 information (left side) - tvP1Name.setText(match.getPlayerNameByPosition(0)); - tvP1Score.setText(String.valueOf(match.getPlayerAverageByPosition(0))); + mTvP1Name.setText(match.getPlayerNameByPosition(0)); + mTvP1Score.setText(String.valueOf(match.getPlayerAverageByPosition(0))); - // Populate player 2 information (right side) - tvP2Name.setText(match.getPlayerNameByPosition(1)); - tvP2Score.setText(String.valueOf(match.getPlayerAverageByPosition(1))); + mTvP2Name.setText(match.getPlayerNameByPosition(1)); + mTvP2Score.setText(String.valueOf(match.getPlayerAverageByPosition(1))); } - /** - * Configures and displays the group match state with a leaderboard. - *

- * This method sets up the view for displaying a match with 3 or more players. - * It performs the following operations: - *

    - *
  1. Switches visibility to show only the group state container
  2. - *
  3. Configures the RecyclerView with a LinearLayoutManager
  4. - *
  5. Creates and attaches a MainMenuGroupMatchAdapter
  6. - *
  7. Populates the adapter with match data (players are automatically sorted by score)
  8. - *
- *

- *

- * RecyclerView Configuration: - * The RecyclerView is configured with a {@link LinearLayoutManager} in vertical - * orientation, displaying players in a scrollable list. Each player entry shows - * their name, score/average, and profile picture. - *

- *

- * Adapter Behavior: - * The {@link MainMenuGroupMatchAdapter} automatically sorts players by their - * career average when the match data is provided, displaying them in ascending - * order (lowest to highest scores). - *

- *

- * Performance Note: - * A new adapter instance is created each time this method is called. For better - * performance in scenarios with frequent updates, consider reusing the adapter - * and calling only {@link MainMenuGroupMatchAdapter#updateMatch(Match)}. - *

- * - * @param match The Match object containing 3 or more players. Must not be null. - * @see MainMenuGroupMatchAdapter - * @see LinearLayoutManager - * @see #updateVisibility(View) - */ + /** Configures group state with leaderboard RecyclerView. */ private void setupGroupState(final Match match) { - // Switch to group state visibility - updateVisibility(stateGroup); + updateVisibility(mStateGroup); - // Configure the RecyclerView with a vertical LinearLayoutManager - rvLeaderboard.setLayoutManager(new LinearLayoutManager(getContext())); + mRvLeaderboard.setLayoutManager(new LinearLayoutManager(getContext())); - // Create and configure the adapter for displaying the player leaderboard final MainMenuGroupMatchAdapter adapter = new MainMenuGroupMatchAdapter(); - rvLeaderboard.setAdapter(adapter); - - // Populate the adapter with match data (players will be sorted automatically) + mRvLeaderboard.setAdapter(adapter); adapter.updateMatch(match); } - /** - * Updates the visibility of all state containers, showing only the specified active view. - *

- * This method implements a mutually exclusive visibility pattern, ensuring that only - * one state container is visible at any given time. It iterates through all three state - * containers (empty, 1v1, group) and sets each one to either VISIBLE or GONE based on - * whether it matches the activeView parameter. - *

- *

- * Visibility Logic: - *

    - *
  • The view matching activeView is set to {@link View#VISIBLE}
  • - *
  • All other views are set to {@link View#GONE} (completely removed from layout)
  • - *
- *

- *

- * Why GONE instead of INVISIBLE: - * Using {@link View#GONE} rather than {@link View#INVISIBLE} ensures that hidden - * states don't occupy any layout space, resulting in cleaner rendering and better - * performance. - *

- *

- * This centralized visibility management prevents inconsistent states where multiple - * containers might be visible simultaneously. - *

- * - * @param activeView The view container that should be made visible. Must be one of: - * {@link #stateEmpty}, {@link #state1v1}, or {@link #stateGroup}. - * All other state containers will be hidden. - * @see View#VISIBLE - * @see View#GONE - */ + /** Shows only the specified state container, hides others. */ private void updateVisibility(final View activeView) { - // Set empty state visibility - stateEmpty.setVisibility(activeView == stateEmpty ? VISIBLE : GONE); - - // Set 1v1 state visibility - state1v1.setVisibility(activeView == state1v1 ? VISIBLE : GONE); - - // Set group state visibility - stateGroup.setVisibility(activeView == stateGroup ? VISIBLE : GONE); + mStateEmpty.setVisibility(activeView == mStateEmpty ? VISIBLE : GONE); + mState1v1.setVisibility(activeView == mState1v1 ? VISIBLE : GONE); + mStateGroup.setVisibility(activeView == mStateGroup ? VISIBLE : GONE); } } diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerItemView.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerItemView.java index 554cda6..b0a6410 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerItemView.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerItemView.java @@ -13,272 +13,56 @@ import com.google.android.material.imageview.ShapeableImageView; import com.aldo.apps.ochecompanion.R; /** - * Reusable custom view component for displaying individual player information in a card format. - *

- * This view extends {@link MaterialCardView} to provide a consistent, styled card layout for - * displaying player information throughout the Oche Companion app. It encapsulates the UI - * components and binding logic needed to present a player's profile picture, username, and - * career statistics in a visually appealing and consistent manner. - *

- *

- * Key Features: - *

    - *
  • Styled MaterialCardView with custom colors, elevation, and corner radius
  • - *
  • Profile picture display with Glide integration for efficient image loading
  • - *
  • Automatic fallback to default avatar icon when no profile picture exists
  • - *
  • Career average statistics display with formatted text
  • - *
  • Reusable across different contexts (squad lists, match recaps, leaderboards)
  • - *
- *

- *

- * Usage Contexts: - * This view is used in multiple places throughout the app: - *

    - *
  • Squad List: In {@link com.aldo.apps.ochecompanion.ui.adapter.MainMenuPlayerAdapter} - * for displaying the user's roster of players
  • - *
  • Group Matches: In {@link com.aldo.apps.ochecompanion.ui.adapter.MainMenuGroupMatchAdapter} - * for displaying match participants in leaderboard format
  • - *
  • Match Recaps: Anywhere player information needs to be displayed consistently
  • - *
- *

- *

- * Design Pattern: - * This view follows the ViewHolder pattern by encapsulating both the layout and binding logic, - * making it easy to reuse across different RecyclerView adapters without code duplication. - *

- *

- * Styling: - * The card appearance is configured in {@link #initViews()} with: - *

    - *
  • Background color from {@code R.color.surface_primary}
  • - *
  • Corner radius from {@code R.dimen.radius_m}
  • - *
  • Elevation from {@code R.dimen.card_elevation}
  • - *
- *

- * - * @see MaterialCardView - * @see Player - * @see com.aldo.apps.ochecompanion.ui.adapter.MainMenuPlayerAdapter - * @see com.aldo.apps.ochecompanion.ui.adapter.MainMenuGroupMatchAdapter - * @author Oche Companion Development Team - * @version 1.0 - * @since 1.0 + * Reusable MaterialCardView for displaying player info: profile picture, username, + * and career statistics. Uses Glide for image loading with fallback to default icon. */ public class PlayerItemView extends MaterialCardView { - /** - * ShapeableImageView displaying the player's profile picture or avatar. - *

- * This ImageView is configured to display circular profile pictures. It uses - * the Glide library to load images from file URIs when available. If the player - * has no profile picture, a default user icon ({@code R.drawable.ic_users}) is displayed. - *

- *

- * The ShapeableImageView type allows for easy customization of the image shape - * through XML attributes, supporting circular, rounded rectangle, or custom shapes. - *

- * - * @see #bind(Player) - * @see ShapeableImageView - */ - private ShapeableImageView ivAvatar; + /** Player profile picture (circular, loaded via Glide). */ + private ShapeableImageView mIvAvatar; - /** - * TextView displaying the player's username. - *

- * Shows the {@link Player#username} field. This is the primary identifier - * for the player in the UI. - *

- */ - private TextView tvUsername; + /** Player username. */ + private TextView mTvUsername; - /** - * TextView displaying the player's career statistics. - *

- * Shows the {@link Player#careerAverage} formatted using the string resource - * {@code R.string.txt_player_average_base}. This provides users with a quick - * overview of the player's performance history. - *

- */ - private TextView tvStats; + /** Player career average statistics. */ + private TextView mTvStats; - /** - * Constructor for programmatic instantiation of the PlayerItemView. - *

- * This constructor delegates to the two-parameter constructor with a null - * AttributeSet, which in turn inflates the layout and initializes all child views - * and styling. - *

- *

- * Use this constructor when creating PlayerItemView instances directly in code - * rather than inflating from XML. - *

- * - * @param context The Context in which the view is running, through which it can - * access the current theme, resources, etc. - * @see #PlayerItemView(Context, AttributeSet) - */ - public PlayerItemView(@NonNull Context context) { + /** Constructor for programmatic instantiation. */ + public PlayerItemView(@NonNull final Context context) { this(context, null); } - /** - * Constructor for XML inflation of the PlayerItemView. - *

- * This constructor is called when the view is inflated from an XML layout file, - * or when programmatically created via the single-parameter constructor. - * It performs the following initialization: - *

    - *
  1. Calls the parent MaterialCardView constructor
  2. - *
  3. Inflates the item_player_small layout into this container
  4. - *
  5. Initializes all child views and applies card styling via {@link #initViews()}
  6. - *
- *

- *

- * After construction, the view is ready to receive player data through the - * {@link #bind(Player)} method. - *

- * - * @param context The Context in which the view is running, through which it can - * access the current theme, resources, etc. - * @param attrs The attributes of the XML tag that is inflating the view. May be null - * if instantiated programmatically. - * @see #initViews() - */ - public PlayerItemView(@NonNull Context context, @Nullable AttributeSet attrs) { + /** Constructor for XML inflation. */ + public PlayerItemView(@NonNull final Context context, @Nullable final AttributeSet attrs) { super(context, attrs); - // Inflate the player item layout into this card view inflate(context, R.layout.item_player_small, this); - // Initialize child views and apply styling initViews(); } - /** - * Initializes child view references and applies MaterialCardView styling. - *

- * This method performs two main tasks: - *

    - *
  1. Card Styling: Configures the MaterialCardView's visual properties - * including background color, corner radius, and elevation for a consistent - * Material Design appearance
  2. - *
  3. View References: Retrieves and stores references to all child - * views (avatar, username, stats) for efficient data binding
  4. - *
- *

- *

- * Styling Details: - *

    - *
  • Background Color: Uses {@code R.color.surface_primary} for - * consistent theming across the app
  • - *
  • Corner Radius: Uses {@code R.dimen.radius_m} for medium-sized - * rounded corners following Material Design guidelines
  • - *
  • Elevation: Uses {@code R.dimen.card_elevation} to create subtle - * depth and visual hierarchy
  • - *
- *

- *

- * This method is called once during construction and does not need to be called - * again during the view's lifecycle. - *

- * - * @see #PlayerItemView(Context, AttributeSet) - * @see MaterialCardView#setCardBackgroundColor(int) - * @see MaterialCardView#setRadius(float) - * @see MaterialCardView#setCardElevation(float) - */ + /** Configures card styling and initializes child view references. */ private void initViews() { - // ========== Card Styling Configuration ========== - - // Set card background color from theme setCardBackgroundColor(getContext().getColor(R.color.surface_primary)); - - // Set corner radius for rounded edges setRadius(getResources().getDimension(R.dimen.radius_m)); - - // Set elevation for Material Design shadow effect setCardElevation(getResources().getDimension(R.dimen.card_elevation)); - // ========== Child View References ========== - - // Get reference to the avatar/profile picture ImageView - ivAvatar = findViewById(R.id.ivPlayerProfile); - - // Get reference to the username TextView - tvUsername = findViewById(R.id.tvPlayerName); - - // Get reference to the career stats TextView - tvStats = findViewById(R.id.tvPlayerAvg); + mIvAvatar = findViewById(R.id.ivPlayerProfile); + mTvUsername = findViewById(R.id.tvPlayerName); + mTvStats = findViewById(R.id.tvPlayerAvg); } - /** - * Binds a Player object to this view, populating all UI components with player data. - *

- * This method updates the view's content to display information for the specified player: - *

    - *
  • Username: Sets the player's name in the username TextView
  • - *
  • Career Average: Formats and displays the player's career statistics
  • - *
  • Profile Picture: Loads the player's avatar image or shows a default icon
  • - *
- *

- *

- * Image Loading Strategy: - * The method uses Glide library for efficient image loading: - *

    - *
  • With Profile Picture: If {@link Player#profilePictureUri} is not null, - * Glide loads the image from the file URI with automatic caching, memory management, - * and placeholder handling
  • - *
  • Without Profile Picture: If no URI is available, displays a default - * user icon ({@code R.drawable.ic_users}) as a fallback
  • - *
- *

- *

- * Text Formatting: - * The career average is formatted using {@code R.string.txt_player_average_base} which - * typically includes a format specifier (e.g., "Avg: %.2f") to ensure consistent - * numerical presentation across the app. - *

- *

- * Performance: - * This method is designed to be called frequently (e.g., during RecyclerView scrolling) - * and uses efficient operations. Glide handles image caching automatically to minimize - * disk I/O and network requests. - *

- *

- * Usage: - * This method should be called whenever: - *

    - *
  • A new player is to be displayed in this view
  • - *
  • Player data has been updated and the view needs to refresh
  • - *
  • The view is being recycled in a RecyclerView adapter
  • - *
- *

- * - * @param player The Player entity from the database containing all necessary display information. - * Must not be null. - * @throws NullPointerException if player is null. - * @see Player#username - * @see Player#careerAverage - * @see Player#profilePictureUri - * @see Glide - */ + /** Binds player data to view components (username, stats, avatar). */ public void bind(@NonNull final Player player) { - // Set the player's username - tvUsername.setText(player.username); - - // Format and set the career average statistics - tvStats.setText(String.format( + mTvUsername.setText(player.username); + mTvStats.setText(String.format( getContext().getString(R.string.txt_player_average_base), player.careerAverage)); - // Load and display the profile picture if (player.profilePictureUri != null) { - // Profile picture exists - load it using Glide for efficient caching Glide.with(getContext()) .load(player.profilePictureUri) - .into(ivAvatar); + .into(mIvAvatar); } else { - // No profile picture - show default user icon - ivAvatar.setImageResource(R.drawable.ic_users); + mIvAvatar.setImageResource(R.drawable.ic_users); } } } diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/QuickStartButton.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/QuickStartButton.java index b8ac67a..568e3be 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/QuickStartButton.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/QuickStartButton.java @@ -11,350 +11,63 @@ import androidx.annotation.Nullable; import com.aldo.apps.ochecompanion.R; /** - * Custom hero-style button component for initiating quick match sessions. - *

- * This prominent interactive view is designed to be the primary call-to-action on the - * dashboard, providing users with a fast and intuitive way to start a new match with - * pre-configured settings. The component features a distinctive visual design with: - *

    - *
  • A large, bold primary label (e.g., "QUICK START")
  • - *
  • A secondary descriptive subtext showing game mode and rules
  • - *
  • Hero-style prominent appearance matching dashboard mockups
  • - *
- *

- *

- * Design Philosophy: - * This button follows the "hero component" design pattern, where a single large, - * visually distinctive element serves as the primary action on a screen. This makes - * it immediately obvious to users how to start playing without navigating through - * multiple menus or configuration screens. - *

- *

- * Key Features: - *

    - *
  • Dual-text hierarchy with main title and descriptive subtitle
  • - *
  • Automatic text uppercasing for visual consistency
  • - *
  • Convenience method for updating game context (mode and rules)
  • - *
  • Built-in clickable and focusable configuration for accessibility
  • - *
  • Efficient merge-tag layout inflation pattern
  • - *
- *

- *

- * Usage Example: - *

- * QuickStartButton button = findViewById(R.id.quickStartButton);
- * button.setMainText("Quick Start");
- * button.updateContext("501", "Double Out");
- * button.setOnClickListener(v -> startMatch());
- * 
- *

- *

- * Text Formatting: - * Both main and sub text are automatically converted to uppercase to maintain - * consistent visual styling and match the high-impact design aesthetic. - *

- *

- * Accessibility: - * The component is configured as clickable and focusable, ensuring it works - * properly with touch, keyboard navigation, and accessibility services. - *

- * - * @see FrameLayout - * @see TextView - * @author Oche Companion Development Team - * @version 1.0 - * @since 1.0 + * Hero-style button for quick match initiation. Features large primary label + * and secondary descriptive subtext. Automatically uppercases all text. */ public class QuickStartButton extends FrameLayout { - /** - * TextView displaying the primary, bold label for the button. - *

- * This TextView typically shows the main action text such as "QUICK START". - * The text is rendered in a large, prominent font and is automatically - * converted to uppercase when set via {@link #setMainText(String)}. - *

- *

- * This is the most visually prominent element of the component and should - * clearly communicate the primary action to the user. - *

- * - * @see #setMainText(String) - */ - private TextView tvMainLabel; + /** Primary bold label (automatically uppercased). */ + private TextView mTvMainLabel; - /** - * TextView displaying the secondary descriptive text below the main label. - *

- * This TextView shows additional context about the quick start action, - * such as the game mode and rules (e.g., "501 - DOUBLE OUT"). The text - * is automatically converted to uppercase when set via {@link #setSubText(String)} - * or {@link #updateContext(String, String)}. - *

- *

- * This provides users with quick information about what configuration will - * be used for the match without requiring them to navigate to settings. - *

- * - * @see #setSubText(String) - * @see #updateContext(String, String) - */ - private TextView tvSubLabel; + /** Secondary descriptive text (mode/rules, automatically uppercased). */ + private TextView mTvSubLabel; - /** - * Constructor for programmatic instantiation of the QuickStartButton. - *

- * This constructor delegates to the two-parameter constructor with a null - * AttributeSet, which handles the actual initialization including layout - * inflation and view setup. - *

- *

- * Use this constructor when creating QuickStartButton instances directly in code - * rather than inflating from XML. - *

- * - * @param context The Context in which the view is running, through which it can - * access the current theme, resources, etc. - * @see #QuickStartButton(Context, AttributeSet) - */ - public QuickStartButton(@NonNull Context context) { + /** Constructor for programmatic instantiation. */ + public QuickStartButton(@NonNull final Context context) { this(context, null); } - /** - * Constructor for XML inflation of the QuickStartButton. - *

- * This constructor is called when the view is inflated from an XML layout file, - * or when programmatically created via the single-parameter constructor. - * It performs the following initialization: - *

    - *
  1. Calls the parent FrameLayout constructor
  2. - *
  3. Configures the view as clickable and focusable for proper interaction handling
  4. - *
  5. Inflates the internal layout using the merge-tag pattern for efficiency
  6. - *
  7. Initializes child view references via {@link #initViews()}
  8. - *
- *

- *

- * Merge Tag Pattern: - * The layout inflation uses {@code attachToRoot = true}, which is appropriate when - * using the {@code } tag in the layout XML. This pattern eliminates an - * unnecessary wrapper ViewGroup, making the view hierarchy more efficient. - *

- *

- * Interaction Configuration: - * The view is explicitly set as clickable and focusable to ensure: - *

    - *
  • Proper touch event handling
  • - *
  • Visual feedback on interaction (ripple effects, state changes)
  • - *
  • Keyboard navigation support for accessibility
  • - *
  • Screen reader compatibility
  • - *
- *

- * - * @param context The Context in which the view is running, through which it can - * access the current theme, resources, etc. - * @param attrs The attributes of the XML tag that is inflating the view. May be null - * if instantiated programmatically. - * @see #initViews() - */ - public QuickStartButton(@NonNull Context context, @Nullable AttributeSet attrs) { + /** Constructor for XML inflation. */ + public QuickStartButton(@NonNull final Context context, @Nullable final AttributeSet attrs) { super(context, attrs); - // Ensure the component is clickable and focusable for proper interaction - // This enables touch feedback, keyboard navigation, and accessibility setClickable(true); setFocusable(true); - // Inflate the internal layout using the merge tag pattern - // attachToRoot = true is used because view_quick_start.xml uses LayoutInflater.from(context).inflate(R.layout.view_quick_start, this, true); - // Initialize references to child views initViews(); } - /** - * Initializes references to child TextView components within the inflated layout. - *

- * This method retrieves and stores references to the main label and sub-label - * TextViews that comprise the button's visual content. These references are - * used by the public setter methods to update the button's displayed text. - *

- *

- * This method is called once during construction and does not need to be called - * again during the view's lifecycle. - *

- *

- * Required Layout Elements: - * The R.layout.view_quick_start layout must contain: - *

    - *
  • {@code R.id.tvQuickStartMain} - The main title TextView
  • - *
  • {@code R.id.tvQuickStartSub} - The subtitle TextView
  • - *
- *

- * - * @see #QuickStartButton(Context, AttributeSet) - */ + /** Initializes references to child TextViews. */ private void initViews() { - // Get reference to the main label TextView - tvMainLabel = findViewById(R.id.tvQuickStartMain); - - // Get reference to the sub-label TextView - tvSubLabel = findViewById(R.id.tvQuickStartSub); + mTvMainLabel = findViewById(R.id.tvQuickStartMain); + mTvSubLabel = findViewById(R.id.tvQuickStartSub); } - /** - * Sets the primary bold text displayed on the button. - *

- * This method updates the main label TextView with the provided text, which is - * automatically converted to uppercase to maintain visual consistency with the - * component's bold, high-impact design aesthetic. - *

- *

- * Typical Usage: - * Use this method to set action-oriented text that clearly communicates the - * button's purpose, such as: - *

    - *
  • "Quick Start"
  • - *
  • "Start Match"
  • - *
  • "Play Now"
  • - *
- *

- *

- * Text Transformation: - * The input text is converted to uppercase using {@link String#toUpperCase()} - * before being set on the TextView, ensuring consistent visual presentation - * regardless of how the input string is formatted. - *

- *

- * Null Safety: - * The method includes a null check for the TextView reference to prevent - * NullPointerExceptions if called before the view is fully initialized, - * though this should not occur under normal circumstances. - *

- * - * @param text The main title string to display. Should be concise and action-oriented. - * Will be converted to uppercase automatically. - * @see #setSubText(String) - */ + /** Sets main text (automatically uppercased). */ public void setMainText(final String text) { - // Check if TextView is initialized before setting text - if (tvMainLabel != null) { - // Convert text to uppercase for consistent styling - tvMainLabel.setText(text.toUpperCase()); + if (mTvMainLabel != null) { + mTvMainLabel.setText(text.toUpperCase()); } } - /** - * Sets the secondary descriptive text displayed below the main label. - *

- * This method updates the subtitle TextView with the provided text, which is - * automatically converted to uppercase to maintain visual consistency. The - * subtitle typically provides additional context about the quick start action, - * such as game mode and rules information. - *

- *

- * Typical Usage: - * Use this method to set descriptive text that provides details about the - * match configuration, such as: - *

    - *
  • "Standard 501 • Double Out"
  • - *
  • "301 - Single In/Double Out"
  • - *
  • "Cricket • Standard Rules"
  • - *
- *

- *

- * Text Transformation: - * The input text is converted to uppercase using {@link String#toUpperCase()} - * before being set on the TextView, ensuring consistent visual presentation - * with the main label. - *

- *

- * Null Safety: - * The method includes a null check for the TextView reference to prevent - * NullPointerExceptions if called before the view is fully initialized, - * though this should not occur under normal circumstances. - *

- * - * @param text The subtitle string to display. Should provide context about the - * match configuration. Will be converted to uppercase automatically. - * @see #setMainText(String) - * @see #updateContext(String, String) - */ + /** Sets subtitle text (automatically uppercased). */ public void setSubText(final String text) { - // Check if TextView is initialized before setting text - if (tvSubLabel != null) { - // Convert text to uppercase for consistent styling - tvSubLabel.setText(text.toUpperCase()); + if (mTvSubLabel != null) { + mTvSubLabel.setText(text.toUpperCase()); } } - /** - * Convenience method to update the subtitle based on game mode and rules. - *

- * This method provides a structured way to update the button's subtitle by combining - * game mode and rules information into a formatted string. It handles the concatenation - * logic and properly formats the output with a separator between mode and rules. - *

- *

- * Formatting Behavior: - *

    - *
  • If rules are provided: Displays "[MODE] - [RULES]" (e.g., "501 - DOUBLE OUT")
  • - *
  • If rules are empty/null: Displays only "[MODE]" (e.g., "501")
  • - *
- *

- *

- * Example Usage: - *

-     * // With rules
-     * button.updateContext("501", "Double Out");
-     * // Results in: "501 - DOUBLE OUT"
-     * 
-     * // Without rules
-     * button.updateContext("301", null);
-     * // Results in: "301"
-     * 
-     * button.updateContext("Cricket", "");
-     * // Results in: "CRICKET"
-     * 
- *

- *

- * Advantages over setSubText: - *

    - *
  • Provides consistent formatting across the app
  • - *
  • Handles null/empty rules gracefully
  • - *
  • Reduces code duplication in calling code
  • - *
  • Makes intent clearer (semantic method name)
  • - *
- *

- *

- * Implementation Details: - * The method uses {@link StringBuilder} for efficient string concatenation, - * especially useful if called frequently. It checks if rules are empty using - * {@link TextUtils#isEmpty(CharSequence)} which handles both null and empty strings. - *

- * - * @param mode The game mode identifier (e.g., "501", "301", "Cricket"). - * Should not be null or empty as this is the primary information. - * @param rules Optional summary of game rules (e.g., "Double Out", "Single In/Double Out"). - * Can be null or empty, in which case only the mode is displayed. - * @see #setSubText(String) - * @see TextUtils#isEmpty(CharSequence) - * @see StringBuilder - */ + /** Updates subtitle with formatted mode and rules ("MODE - RULES"). */ public void updateContext(final String mode, final String rules) { - // Use StringBuilder for efficient string concatenation final StringBuilder stringBuilder = new StringBuilder(mode); - // Only append rules if they are provided (not null and not empty) if (!TextUtils.isEmpty(rules)) { - // Add separator between mode and rules stringBuilder.append(" - "); - // Append the rules text stringBuilder.append(rules); } - // Set the combined text as the subtitle setSubText(stringBuilder.toString()); } } diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/MainMenuGroupMatchAdapter.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/MainMenuGroupMatchAdapter.java index f9cd7bb..b54bd34 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/MainMenuGroupMatchAdapter.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/MainMenuGroupMatchAdapter.java @@ -21,80 +21,23 @@ import java.util.Comparator; import java.util.List; /** - * RecyclerView adapter for displaying group match player information in the Main Menu. - *

- * This adapter is specifically designed for matches with 3 or more players (group matches). - * It displays a sorted list of players with their names, career averages, and profile pictures. - * Players are automatically sorted by their career average scores when the match data is updated. - *

- *

- * Key Features: - *

    - *
  • Displays player name, career average, and profile picture
  • - *
  • Automatically sorts players by career average
  • - *
  • Uses custom {@link PlayerItemView} for consistent player display
  • - *
  • Integrates with Glide for efficient image loading
  • - *
  • Optimized for infrequent updates (typically once per activity lifecycle)
  • - *
- *

- *

- * Usage: - * This adapter is used in the Main Menu to display the results of the last played group match, - * providing users with a quick overview of player standings. - *

- * - * @see RecyclerView.Adapter - * @see GroupMatchHolder - * @see PlayerScoreComparator - * @see Match - * @see Player - * @author Oche Companion Development Team - * @version 1.0 - * @since 1.0 + * RecyclerView adapter for displaying group match results in Main Menu. + * Displays players sorted by career average with their names, scores, and profile pictures. */ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter { - /** - * Tag for logging and debugging purposes. - * Used to identify log messages originating from this adapter. - */ + /** Tag for logging. */ private static final String TAG = "MainMenuGroupMatchAdapt"; - /** - * Internal list holding the players to be displayed in the RecyclerView. - *

- * This list is populated and sorted when {@link #updateMatch(Match)} is called. - * Players are sorted by their career average using {@link PlayerScoreComparator}. - * The list is cleared and repopulated on each match update. - *

- * - * @see #updateMatch(Match) - * @see Player - */ + /** List of players sorted by career average. */ private final List mPlayersList = new ArrayList<>(); /** - * Creates a new {@link GroupMatchHolder} to display player information. - *

- * This method is called by the RecyclerView when it needs a new ViewHolder to represent - * a player item. The method creates a custom {@link PlayerItemView} and configures its - * layout parameters to match the parent's width and wrap its content height. - *

- *

- * Layout Configuration: - *

    - *
  • Width: MATCH_PARENT (fills available width)
  • - *
  • Height: WRAP_CONTENT (adjusts to content size)
  • - *
- *

- * - * @param parent The ViewGroup into which the new View will be added after it is bound - * to an adapter position. - * @param viewType The view type of the new View. Not used in this implementation as - * all items use the same view type. - * @return A new GroupMatchHolder that holds a PlayerItemView. - * @see GroupMatchHolder - * @see PlayerItemView + * Creates a new ViewHolder with a PlayerItemView. + * + * @param parent The parent ViewGroup. + * @param viewType The view type (unused). + * @return A new GroupMatchHolder. */ @NonNull @Override @@ -112,17 +55,10 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter - * This method is called by the RecyclerView to display player data at the specified position. - * It retrieves the player from the internal list and delegates the display logic to the - * {@link GroupMatchHolder#setPlayer(Player)} method. - *

- * - * @param holder The ViewHolder which should be updated to represent the player - * at the given position in the data set. - * @param position The position of the player within the adapter's data set. - * @see GroupMatchHolder#setPlayer(Player) + * Binds player data to the ViewHolder at the specified position. + * + * @param holder The ViewHolder to update. + * @param position The position in the data set. */ @Override public void onBindViewHolder(@NonNull final GroupMatchHolder holder, final int position) { @@ -131,13 +67,9 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter - * This method is called by the RecyclerView to determine how many items to display. - * The count is based on the number of players currently stored in {@link #mPlayersList}. - *

- * - * @return The total number of players in the list. + * Returns the number of players. + * + * @return Player count. */ @Override public int getItemCount() { @@ -145,30 +77,9 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter - * This method performs the following operations: - *
    - *
  1. Clears the existing player list
  2. - *
  3. Retrieves all players from the provided match
  4. - *
  5. Sorts players by their career average (ascending order)
  6. - *
  7. Adds the sorted players to the internal list
  8. - *
  9. Notifies the RecyclerView to refresh the display
  10. - *
- *

- *

- * Performance Consideration: - * This method uses {@link #notifyDataSetChanged()}, which triggers a full refresh - * of the RecyclerView. The {@code @SuppressLint("NotifyDataSetChanged")} annotation - * is used because this method is typically called only once per activity lifecycle, - * making the performance impact negligible. For frequent updates, more granular - * notification methods (like notifyItemInserted, notifyItemChanged) would be preferable. - *

- * - * @param match The Match object containing the players to display. Must not be null - * and should contain at least one player. - * @see Match#getAllPlayers() - * @see PlayerScoreComparator + * Updates the adapter with match data, sorting players by career average. + * + * @param match The match containing players to display. */ @SuppressLint("NotifyDataSetChanged") public void updateMatch(final Match match) { @@ -200,73 +111,24 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter - * This ViewHolder manages the UI components for a single player entry, including: - *
    - *
  • Player name
  • - *
  • Player career average score
  • - *
  • Player profile picture/avatar
  • - *
- *

- *

- * The ViewHolder uses a {@link PlayerItemView} as its root view and automatically - * hides the chevron icon since group match items are not clickable/expandable. - *

- *

- * Image Loading: - * Profile pictures are loaded using the Glide library for efficient memory management - * and caching. If a player has no profile picture, a default user icon is displayed. - *

- * - * @see RecyclerView.ViewHolder - * @see PlayerItemView - * @see Player + * ViewHolder for displaying player items in group match view. + * Hides chevron since items are not clickable. */ public static class GroupMatchHolder extends RecyclerView.ViewHolder { - /** - * TextView displaying the player's username. - * Shows the {@link Player#username} field. - */ + /** TextView displaying the player's name. */ private final TextView mPlayerNameView; - /** - * TextView displaying the player's career average score. - *

- * Shows the {@link Player#careerAverage} field formatted according to - * the string resource {@code R.string.txt_player_average_base}. - *

- */ + /** TextView displaying the player's career average. */ private final TextView mPlayerScoreView; - /** - * ShapeableImageView displaying the player's profile picture. - *

- * Displays the image from {@link Player#profilePictureUri} if available, - * or a default user icon ({@code R.drawable.ic_users}) if no picture is set. - * Images are loaded using the Glide library for optimal performance. - *

- */ + /** ShapeableImageView displaying the player's profile picture. */ private final ShapeableImageView mPlayerImageView; /** - * Constructs a new GroupMatchHolder and initializes its child views. - *

- * This constructor performs the following setup: - *

    - *
  1. Calls the parent ViewHolder constructor
  2. - *
  3. Retrieves references to the player name, score, and image views
  4. - *
  5. Hides the chevron icon since group match items are non-interactive
  6. - *
- *

- *

- * The chevron icon is hidden because players in group match view are displayed - * for informational purposes only and do not support click/expand actions. - *

- * - * @param itemView The root view of the ViewHolder, expected to be a {@link PlayerItemView}. - * Must not be null and must contain the required child views. + * Constructs a new GroupMatchHolder and initializes child views. + * + * @param itemView The root view (PlayerItemView). */ public GroupMatchHolder(@NonNull final View itemView) { super(itemView); @@ -281,27 +143,9 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter - * This method updates the UI components with the player's data: - *
    - *
  • Name: Sets the player's username in the name TextView
  • - *
  • Score: Formats and displays the career average using a string resource
  • - *
  • Avatar: Loads the profile picture using Glide, or shows a default icon
  • - *
- *

- *

- * Image Loading Strategy: - * If the player has a profile picture URI, Glide is used to load the image asynchronously - * with automatic caching and memory management. If no URI is available, a default user - * icon ({@code R.drawable.ic_users}) is displayed instead. - *

- * - * @param player The Player object whose information should be displayed. - * Must not be null. - * @see Player#username - * @see Player#careerAverage - * @see Player#profilePictureUri + * Binds player data to this ViewHolder. + * + * @param player The player to display. */ public void setPlayer(final Player player) { // Set player name @@ -326,42 +170,16 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter - * This comparator implements ascending order sorting, meaning players with lower - * career averages will appear before players with higher averages in the sorted list. - *

- *

- * Sorting Behavior: - *

    - *
  • Returns negative if p1's average < p2's average (p1 comes first)
  • - *
  • Returns zero if p1's average == p2's average (equal priority)
  • - *
  • Returns positive if p1's average > p2's average (p2 comes first)
  • - *
- *

- *

- * Usage: - * This comparator is used in {@link #updateMatch(Match)} to sort the player list - * before displaying it in the RecyclerView. - *

- * - * @see Comparator - * @see Player#careerAverage - * @see #updateMatch(Match) + * Comparator for sorting players by career average in ascending order. */ public static class PlayerScoreComparator implements Comparator { /** - * Compares two Player objects based on their career average scores. - *

- * Uses {@link Double#compare(double, double)} to perform a numerical comparison - * of the career average values, which properly handles special cases like NaN and infinity. - *

- * - * @param p1 The first Player to compare. - * @param p2 The second Player to compare. - * @return A negative integer if p1's average is less than p2's average, - * zero if they are equal, or a positive integer if p1's average is greater. + * Compares two players by career average. + * + * @param p1 First player. + * @param p2 Second player. + * @return Comparison result. */ @Override public int compare(final Player p1, final Player p2) { diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/MainMenuPlayerAdapter.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/MainMenuPlayerAdapter.java index 7de578b..20b1cb0 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/MainMenuPlayerAdapter.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/MainMenuPlayerAdapter.java @@ -24,80 +24,24 @@ import java.util.ArrayList; import java.util.List; /** - * RecyclerView adapter for displaying the squad of players in the Main Menu. - *

- * This adapter manages the display of all players in the user's squad, showing their names, - * career statistics, and profile pictures. Each player item is clickable, allowing users to - * navigate to the player edit screen to update their information. - *

- *

- * Key Features: - *

    - *
  • Displays player name, career average, and profile picture
  • - *
  • Supports click-to-edit functionality for each player
  • - *
  • Uses custom {@link PlayerItemView} for consistent player display
  • - *
  • Integrates with Glide for efficient image loading and caching
  • - *
  • Optimized for infrequent updates (typically once per activity lifecycle)
  • - *
- *

- *

- * Usage: - * This adapter is used in the Main Menu activity to display the complete squad roster, - * allowing users to view and manage their players. - *

- * - * @see RecyclerView.Adapter - * @see PlayerCardHolder - * @see Player - * @see AddPlayerActivity - * @author Oche Companion Development Team - * @version 1.0 - * @since 1.0 + * RecyclerView adapter for displaying the player squad in Main Menu. + * Shows player names, career averages, and profile pictures with click-to-edit functionality. */ public class MainMenuPlayerAdapter extends RecyclerView.Adapter { - /** - * Tag for logging and debugging purposes. - * Used to identify log messages originating from this adapter. - */ + /** Tag for logging. */ private static final String TAG = "MainMenuPlayerAdapter"; - /** - * Internal list holding all players to be displayed in the RecyclerView. - *

- * This list is populated when {@link #updatePlayers(List)} is called. - * The list maintains the order in which players are added from the database. - * All modifications to this list trigger a full RecyclerView refresh. - *

- * - * @see #updatePlayers(List) - * @see Player - */ + /** List of all players to display. */ private final List mPlayersList = new ArrayList<>(); /** - * Creates a new {@link PlayerCardHolder} to display player information. - *

- * This method is called by the RecyclerView when it needs a new ViewHolder to represent - * a player item. The method creates a custom {@link PlayerItemView} and configures its - * layout parameters to match the parent's width and wrap its content height. - *

- *

- * Layout Configuration: - *

    - *
  • Width: MATCH_PARENT (fills available width)
  • - *
  • Height: WRAP_CONTENT (adjusts to content size)
  • - *
- *

- * - * @param parent The ViewGroup into which the new View will be added after it is bound - * to an adapter position. - * @param viewType The view type of the new View. Not used in this implementation as - * all items use the same view type. - * @return A new PlayerCardHolder that holds a PlayerItemView. - * @see PlayerCardHolder - * @see PlayerItemView + * Creates a new ViewHolder with a PlayerItemView. + * + * @param parent The parent ViewGroup. + * @param viewType The view type (unused). + * @return A new PlayerCardHolder. */ @NonNull @Override @@ -115,17 +59,10 @@ public class MainMenuPlayerAdapter extends RecyclerView.Adapter - * This method is called by the RecyclerView to display player data at the specified position. - * It retrieves the player from the internal list and delegates the display logic to the - * {@link PlayerCardHolder#setPlayer(Player)} method. - *

- * - * @param holder The ViewHolder which should be updated to represent the player - * at the given position in the data set. - * @param position The position of the player within the adapter's data set. - * @see PlayerCardHolder#setPlayer(Player) + * Binds player data to the ViewHolder at the specified position. + * + * @param holder The ViewHolder to update. + * @param position The position in the data set. */ @Override public void onBindViewHolder(@NonNull final PlayerCardHolder holder, final int position) { @@ -134,13 +71,9 @@ public class MainMenuPlayerAdapter extends RecyclerView.Adapter - * This method is called by the RecyclerView to determine how many items to display. - * The count is based on the number of players currently stored in {@link #mPlayersList}. - *

- * - * @return The total number of players in the list. + * Returns the number of players. + * + * @return Player count. */ @Override public int getItemCount() { @@ -148,28 +81,9 @@ public class MainMenuPlayerAdapter extends RecyclerView.Adapter - * This method performs the following operations: - *
    - *
  1. Clears the existing player list
  2. - *
  3. Adds all players from the provided list
  4. - *
  5. Notifies the RecyclerView to refresh the display
  6. - *
- *

- *

- * Performance Consideration: - * This method uses {@link #notifyDataSetChanged()}, which triggers a full refresh - * of the RecyclerView. The {@code @SuppressLint("NotifyDataSetChanged")} annotation - * is used because this method is typically called only once per activity lifecycle - * (when the activity resumes), making the performance impact negligible. For frequent - * updates, more granular notification methods (like notifyItemInserted, notifyItemChanged) - * would be preferable. - *

- * - * @param players The list of Player objects to display. Must not be null. - * Can be empty to clear the display. - * @see Player + * Updates the adapter with a new list of players. + * + * @param players The list of players to display. */ @SuppressLint("NotifyDataSetChanged") public void updatePlayers(final List players) { @@ -193,75 +107,23 @@ public class MainMenuPlayerAdapter extends RecyclerView.Adapter - * This ViewHolder manages the UI components for a single player entry, including: - *
    - *
  • Player name
  • - *
  • Player career average score
  • - *
  • Player profile picture/avatar
  • - *
  • Click interaction to edit player details
  • - *
- *

- *

- * The ViewHolder uses a {@link PlayerItemView} as its root view and supports click - * events that navigate to the {@link AddPlayerActivity} for editing player information. - *

- *

- * Image Loading: - * Profile pictures are loaded using the Glide library for efficient memory management, - * caching, and smooth scrolling performance. If a player has no profile picture, - * a default user icon is displayed. - *

- * - * @see RecyclerView.ViewHolder - * @see PlayerItemView - * @see Player - * @see AddPlayerActivity + * ViewHolder for displaying player items with click-to-edit functionality. */ public static class PlayerCardHolder extends RecyclerView.ViewHolder { - /** - * TextView displaying the player's username. - * Shows the {@link Player#username} field. - */ + /** TextView displaying the player's name. */ private final TextView mPlayerNameView; - /** - * TextView displaying the player's career average score. - *

- * Shows the {@link Player#careerAverage} field formatted according to - * the string resource {@code R.string.txt_player_average_base}. - *

- */ + /** TextView displaying the player's career average. */ private final TextView mPlayerScoreView; - /** - * ShapeableImageView displaying the player's profile picture. - *

- * Displays the image from {@link Player#profilePictureUri} if available, - * or a default user icon ({@code R.drawable.ic_users}) if no picture is set. - * Images are loaded using the Glide library for optimal performance. - *

- */ + /** ShapeableImageView displaying the player's profile picture. */ private final ShapeableImageView mPlayerImageView; /** - * Constructs a new PlayerCardHolder and initializes its child views. - *

- * This constructor performs the following setup: - *

    - *
  1. Calls the parent ViewHolder constructor
  2. - *
  3. Retrieves references to the player name, score, and image views
  4. - *
- *

- *

- * Unlike group match items, the chevron icon is kept visible since player items - * are interactive and clicking them navigates to the edit screen. - *

- * - * @param itemView The root view of the ViewHolder, expected to be a {@link PlayerItemView}. - * Must not be null and must contain the required child views. + * Constructs a new PlayerCardHolder and initializes child views. + * + * @param itemView The root view (PlayerItemView). */ public PlayerCardHolder(@NonNull final View itemView) { super(itemView); @@ -273,35 +135,9 @@ public class MainMenuPlayerAdapter extends RecyclerView.Adapter - * This method updates the UI components with the player's data: - *
    - *
  • Click Listener: Sets up navigation to edit player details
  • - *
  • Name: Sets the player's username in the name TextView
  • - *
  • Score: Formats and displays the career average using a string resource
  • - *
  • Avatar: Loads the profile picture using Glide, or shows a default icon
  • - *
- *

- *

- * Interaction: - * When the item is clicked, it launches {@link AddPlayerActivity} with the player's ID, - * allowing the user to edit the player's information. - *

- *

- * Image Loading Strategy: - * If the player has a profile picture URI, Glide is used to load the image asynchronously - * with automatic caching and memory management. If no URI is available, a default user - * icon ({@code R.drawable.ic_users}) is displayed instead. - *

- * - * @param player The Player object whose information should be displayed. - * Must not be null. - * @see Player#username - * @see Player#careerAverage - * @see Player#profilePictureUri - * @see Player#id - * @see #startEditPlayerActivity(Context, Player) + * Binds player data to this ViewHolder and sets up click listener. + * + * @param player The player to display. */ public void setPlayer(final Player player) { Log.d(TAG, "setPlayer() called with: player = [" + player + "]"); @@ -330,25 +166,10 @@ public class MainMenuPlayerAdapter extends RecyclerView.Adapter - * This helper method creates an intent to start {@link AddPlayerActivity} in edit mode, - * passing the player's ID as an extra. The activity will load the player's existing data - * and allow the user to modify their username and profile picture. - *

- *

- * Intent Configuration: - * The player's ID is passed via the {@link AddPlayerActivity#EXTRA_PLAYER_ID} extra, - * which signals to the activity that it should load and edit an existing player - * rather than creating a new one. - *

- * - * @param context The Context from which the activity should be launched. - * Typically obtained from the item view. - * @param player The Player object whose information should be edited. - * The player's ID is passed to the edit activity. - * @see AddPlayerActivity - * @see AddPlayerActivity#EXTRA_PLAYER_ID + * Launches AddPlayerActivity to edit the player's information. + * + * @param context The context to launch activity from. + * @param player The player to edit. */ private void startEditPlayerActivity(final Context context, final Player player) { // Create intent for AddPlayerActivity