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 @@
+
+
- * This activity provides a comprehensive user interface for managing player information including: - *
- * The activity features two distinct UI modes: - *
- * Image Processing Pipeline: - *
- * 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- * 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: - *
- * 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: - *
- * Algorithm Overview: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * Multiplier System: - *
- * Checkout Engine: - * Provides optimal finishing routes when player is within checkout range (≤170): - *
- * Performance Optimizations: - *
- * Future Enhancements: - * Consider adding: - *
- * Value type: {@code ArrayList
- * 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: - *
- * Valid values: - *
- * 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- * 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: - *
- * Used for: - *
- * When {@code true}: - *
- * Set to {@code true} when: - *
- * 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- * 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: - *
- * 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: - *
- * 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: - *
- * Common Start Scores: - *
- * Initializes the game by: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * Player List Handling: - *
- * 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- * This is the core game logic method that handles: - *
- * Scoring Calculation: - *
- * points = baseValue × multiplier - * Special case: Bull (25) with Triple (3×) = 50 (Double Bull) - *- * - *
- * Bust Conditions (turn ends, score reverts): - *
- * Win Condition (game ends): - * Player reaches exactly zero with a double or bullseye (50): - *
- * Valid Throw (continues turn): - *
- * Double Detection: - * A dart is considered a double if: - *
- * 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: - *
- * 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: - *
- * Visual Feedback by Multiplier: - *
- * MaterialButton Styling: - * The method uses MaterialButton-specific APIs: - *
- * 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: - *- * 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: - *
- * Turn Processing: - *
- * Player Rotation: - *
- * mActivePlayerIndex = (mActivePlayerIndex + 1) % playerCount - *- * This cycles through players: 0 → 1 → 2 → 0 → 1 → ... - * - *
- * State Reset: - * After submission: - *
- * 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: - *
- * This method provides undo functionality, allowing players to correct mistakes - * during their turn. It: - *
- * Use Cases: - *
- * Limitations: - *
- * 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: - *
- * 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: - *
- * Performance: - * This method is called frequently during gameplay, so it's optimized to: - *
- * 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: - *
- * Visual Effects: - * When a checkout route is available: - *
- * 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: - *
- * 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: - *
- * Visual States: - *
- * Dart not thrown: [ ] (empty background) - * Dart thrown: [60] (green text, active background) - *- * - *
- * Display Labels: - * Dart values are formatted for clarity: - *
- * Example Progression: - *
- * No darts: [ ] [ ] [ ] - * 1 dart: [60] [ ] [ ] - * 2 darts: [60] [60] [ ] - * 3 darts: [60] [60] [20] - * After submit:[ ] [ ] [ ] (cleared for next turn) - *- * - *
- * When Called: - *
- * The visual feedback helps players: - *
- * 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: - *
- * 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: - *
- * Current Implementation: - * The current implementation is minimal and immediately ends the game. Future - * enhancements could include: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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- * This method implements sophisticated checkout logic that considers: - *
- * Return Format: - * Returns strings formatted as: - *
- * Algorithm Steps: - *
- * 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: - *
- * This activity provides the following functionality: - *
- * 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: - *
- * 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: - *
- * Use Cases: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * Database Entities: - * This database manages two primary tables: - *
- * Database Access Objects (DAOs): - * Database operations are performed through specialized DAO interfaces: - *
- * Singleton Pattern Implementation: - * The class uses the thread-safe double-checked locking pattern to ensure a single - * database instance. This approach: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * Database Inspection: - * For debugging, you can inspect the database using: - *
- * Performance Optimization: - *
@Entity(indices = {@Index(value = {"username"})})- * 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: - *
- * Security Considerations: - *
- * Future Enhancements: - * Consider these improvements for future versions: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * Initialization State: - *
- * Memory Lifecycle: - * The database instance is retained in memory for the lifetime of the application - * process. It will be garbage collected only when: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)- *
- * First Call Initialization: - * On the first call, this method will: - *
- * Subsequent Calls: - * After initialization, subsequent calls: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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- * 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: - *
- * 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: - *
- * Key Features: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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): - *
- * 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: - *
- * 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: - *
- * 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- * 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: - *
- * 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: - *
- * Match Types: - * The system supports different match configurations: - *
- * 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: - *
- * Performance Considerations: - *
- * Data Integrity: - *
- * Future Enhancements: - * Consider adding these fields for expanded functionality: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * String Format: - * Game mode strings should be: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * Optional Fields: - * Additional metrics that may be included: - *
- * 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: - *
- * Data Integrity: - *
- * 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: - *
- * 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}: - *
- * 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: - *
- * 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: - *
- * Player Lifecycle: - *
- * 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: - *
- * Career Statistics: - * The {@code careerAverage} field tracks the player's overall performance: - *
- * Match Count Tracking: - * The {@code matchesPlayed} counter: - *
- * Database Relationships: - * While this entity doesn't use explicit Room relations, players are referenced in: - *
- * 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: - *
- * Future Enhancements: - * Consider adding these fields for expanded functionality: - *
- * UI Integration: - * Players are displayed in various UI components: - *
- * Performance Notes: - *
- * 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: - *
- * 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: - *
- * Cross-References: - * The player ID is stored in: - *
- * 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: - *
- * Character Guidelines: - * While not enforced by the database schema, usernames should ideally: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * File Management: - * Consider these file management practices: - *
- * 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: - *
- * Migration Consideration: - * If implementing cloud sync in the future, consider: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * Alternative Metrics: - * Future enhancements might track additional averages: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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- * 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: - *
- * 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: - *
- * 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: - *
- * Match Types: - * This class supports different match configurations: - *
- * Player Ordering: - * Players are stored in the order they are added. This ordering is significant for: - *
- * 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: - *
- * 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- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * This path consists of two rectangles: - *
- * 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 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: - *
- * Crop Box Sizing: - * The crop box is sized at 80% of the view's width to provide: - *
- * Path Construction: - * The path is built with two rectangles using opposite winding directions: - *
- * 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: - *
- * 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: - *
- * Usage: - * The parent activity must transform these screen coordinates to bitmap pixel coordinates - * by accounting for: - *
- * 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: - *
- * Key Features: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * This method sets up the view for displaying a head-to-head match between two players. - * It performs the following operations: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * Usage Contexts: - * This view is used in multiple places throughout the app: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * Styling Details: - *
- * 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: - *
- * Image Loading Strategy: - * The method uses Glide library for efficient image loading: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * Merge Tag Pattern:
- * The layout inflation uses {@code attachToRoot = true}, which is appropriate when
- * using the {@code
- * Interaction Configuration: - * The view is explicitly set as clickable and focusable to ensure: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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: - *
- * 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- * 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- * 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: - *
- * 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- * 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: - *
- * 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- * 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- * Sorting Behavior: - *
- * 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- * 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: - *
- * 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- * 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- * 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: - *
- * 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- * 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: - *
- * 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- * 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- * 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