Simplifed JavaDoc, applied coding rules
This commit is contained in:
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -28,70 +28,19 @@ import java.io.InputStream;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AddPlayerActivity manages the creation and editing of player profiles in the Oche Companion application.
|
* Manages creation and editing of player profiles with username, profile picture, and image cropping.
|
||||||
* <p>
|
* Operates in Form Mode (profile editing) or Crop Mode (interactive image cropping with pan and zoom).
|
||||||
* This activity provides a comprehensive user interface for managing player information including:
|
* Pass EXTRA_PLAYER_ID to edit existing player; otherwise creates new player.
|
||||||
* <ul>
|
|
||||||
* <li>Creating new player profiles with username and profile picture</li>
|
|
||||||
* <li>Editing existing player profiles</li>
|
|
||||||
* <li>Advanced image cropping functionality with pan and pinch-to-zoom controls</li>
|
|
||||||
* <li>Saving profile pictures to internal storage</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* The activity features two distinct UI modes:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>Form Mode:</strong> Standard profile editing with username input and profile picture selection</li>
|
|
||||||
* <li><strong>Crop Mode:</strong> Interactive image cropping interface with gesture controls for precise framing</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Image Processing Pipeline:</strong>
|
|
||||||
* <ol>
|
|
||||||
* <li>User selects an image from their gallery</li>
|
|
||||||
* <li>Activity switches to Crop Mode with the selected image</li>
|
|
||||||
* <li>User can pan (drag) and pinch-to-zoom to frame the desired area</li>
|
|
||||||
* <li>A square crop overlay shows the final cutout area</li>
|
|
||||||
* <li>Upon confirmation, the cropped image is saved to internal storage</li>
|
|
||||||
* <li>Activity returns to Form Mode with the cropped image displayed</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see AppCompatActivity
|
|
||||||
* @see Player
|
|
||||||
* @see CropOverlayView
|
|
||||||
* @see AppDatabase
|
|
||||||
* @author Oche Companion Development Team
|
|
||||||
* @version 1.0
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
*/
|
||||||
public class AddPlayerActivity extends AppCompatActivity {
|
public class AddPlayerActivity extends AppCompatActivity {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag for logging and debugging purposes.
|
* Tag for logging.
|
||||||
* Used to identify log messages originating from this activity.
|
|
||||||
*/
|
*/
|
||||||
private static final String TAG = "Oche_AddPlayer";
|
private static final String TAG = "Oche_AddPlayer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intent extra key for passing an existing player's ID for editing.
|
* Intent extra key for passing existing player's ID for editing.
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* Intent intent = new Intent(context, AddPlayerActivity.class);
|
|
||||||
* intent.putExtra(AddPlayerActivity.EXTRA_PLAYER_ID, playerId);
|
|
||||||
* startActivity(intent);
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
*/
|
*/
|
||||||
public static final String EXTRA_PLAYER_ID = "extra_player_id";
|
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.
|
* Container layout for the main player profile form.
|
||||||
* Visible during Form Mode when the user is entering player details.
|
|
||||||
*/
|
*/
|
||||||
private View mLayoutForm;
|
private View mLayoutForm;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Container layout for the image cropping interface.
|
* Container layout for the image cropping interface.
|
||||||
* Visible during Crop Mode when the user is adjusting their selected image.
|
|
||||||
*/
|
*/
|
||||||
private View mLayoutCropper;
|
private View mLayoutCropper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageView displaying the player's profile picture in the main form.
|
* ImageView displaying the player's profile picture.
|
||||||
* Clicking this view triggers the image selection process.
|
|
||||||
*/
|
*/
|
||||||
private ShapeableImageView mProfilePictureView;
|
private ShapeableImageView mProfilePictureView;
|
||||||
|
|
||||||
@@ -121,14 +67,12 @@ public class AddPlayerActivity extends AppCompatActivity {
|
|||||||
private EditText mUserNameInput;
|
private EditText mUserNameInput;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TextView displaying the activity title ("Add Player" or "Update Profile").
|
* TextView displaying the activity title.
|
||||||
* The title changes based on whether creating a new player or editing an existing one.
|
|
||||||
*/
|
*/
|
||||||
private TextView mTitleView;
|
private TextView mTitleView;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Button to save the player profile (insert new or update existing).
|
* Button to save the player profile.
|
||||||
* The button label changes based on the current mode ("Save" or "Update").
|
|
||||||
*/
|
*/
|
||||||
private MaterialButton mSaveButton;
|
private MaterialButton mSaveButton;
|
||||||
|
|
||||||
@@ -136,13 +80,11 @@ public class AddPlayerActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageView displaying the full selected image during Crop Mode.
|
* ImageView displaying the full selected image during Crop Mode.
|
||||||
* Supports pan and pinch-to-zoom gestures for precise positioning.
|
|
||||||
*/
|
*/
|
||||||
private ImageView mIvCropPreview;
|
private ImageView mIvCropPreview;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom overlay view that renders the square crop area boundary.
|
* 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;
|
private CropOverlayView mCropOverlay;
|
||||||
|
|
||||||
@@ -150,72 +92,48 @@ public class AddPlayerActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Absolute file path to the saved profile picture in internal storage.
|
* 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;
|
private String mInternalImagePath;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URI of the original, unmodified image selected from the gallery.
|
* URI of the original image selected from the gallery.
|
||||||
* Used as the source for cropping operations.
|
|
||||||
*/
|
*/
|
||||||
private Uri mRawSelectedUri;
|
private Uri mRawSelectedUri;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database ID of the player being edited.
|
* Database ID of the player being edited (-1 for new player).
|
||||||
* Defaults to -1, indicating "create new player" mode. When >= 0,
|
|
||||||
* the activity loads and updates an existing player.
|
|
||||||
*/
|
*/
|
||||||
private int mExistingPlayerId = -1;
|
private int mExistingPlayerId = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Player object loaded from the database when editing an existing player.
|
* Player object loaded from the database (null when creating new player).
|
||||||
* Null when creating a new player. Used to update existing records.
|
|
||||||
*/
|
*/
|
||||||
private Player mExistingPlayer;
|
private Player mExistingPlayer;
|
||||||
|
|
||||||
// ========== Gesture State ==========
|
// ========== Gesture State ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Last recorded X coordinate during pan gesture (drag).
|
* Last recorded X coordinate during pan gesture.
|
||||||
* Used to calculate the delta movement between touch events.
|
|
||||||
*/
|
*/
|
||||||
private float mLastTouchX;
|
private float mLastTouchX;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Last recorded Y coordinate during pan gesture (drag).
|
* Last recorded Y coordinate during pan gesture.
|
||||||
* Used to calculate the delta movement between touch events.
|
|
||||||
*/
|
*/
|
||||||
private float mLastTouchY;
|
private float mLastTouchY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detector for handling pinch-to-zoom gestures on the crop preview image.
|
* Detector for handling pinch-to-zoom gestures.
|
||||||
* Monitors multi-touch events to calculate scale changes.
|
|
||||||
*/
|
*/
|
||||||
private ScaleGestureDetector mScaleDetector;
|
private ScaleGestureDetector mScaleDetector;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current scale factor applied to the crop preview image.
|
* Current scale factor applied to the crop preview image (1.0 default, clamped 0.1 to 10.0).
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*/
|
*/
|
||||||
private float mScaleFactor = 1.0f;
|
private float mScaleFactor = 1.0f;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ActivityResultLauncher for selecting images from the device gallery.
|
* ActivityResultLauncher for selecting images from the device gallery.
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* The launcher is triggered when the user taps the profile picture placeholder.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see ActivityResultContracts.GetContent
|
|
||||||
*/
|
*/
|
||||||
private final ActivityResultLauncher<String> mGetContent = registerForActivityResult(
|
private final ActivityResultLauncher<String> mGetContent = registerForActivityResult(
|
||||||
new ActivityResultContracts.GetContent(),
|
new ActivityResultContracts.GetContent(),
|
||||||
@@ -228,31 +146,12 @@ public class AddPlayerActivity extends AppCompatActivity {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the activity is first created.
|
* Called when the activity is first created. Initializes UI and loads existing player if present.
|
||||||
* <p>
|
|
||||||
* Performs the following initialization tasks:
|
|
||||||
* <ul>
|
|
||||||
* <li>Sets the content view to the add player layout</li>
|
|
||||||
* <li>Initializes all UI component references</li>
|
|
||||||
* <li>Sets up gesture detectors for image manipulation</li>
|
|
||||||
* <li>Checks for existing player ID in intent extras and loads player data if present</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
*
|
||||||
* @param savedInstanceState If the activity is being re-initialized after previously being shut down,
|
* @param savedInstanceState Saved instance state.
|
||||||
* this Bundle contains the data it most recently supplied in
|
|
||||||
* {@link #onSaveInstanceState(Bundle)}. Otherwise, it is null.
|
|
||||||
* @see #initViews()
|
|
||||||
* @see #setupGestures()
|
|
||||||
* @see #loadExistingPlayer()
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_add_player);
|
setContentView(R.layout.activity_add_player);
|
||||||
Log.d(TAG, "AddPlayerActivity Created");
|
Log.d(TAG, "AddPlayerActivity Created");
|
||||||
@@ -272,25 +171,6 @@ public class AddPlayerActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes all UI component references and sets up click listeners.
|
* Initializes all UI component references and sets up click listeners.
|
||||||
* <p>
|
|
||||||
* This method performs the following operations:
|
|
||||||
* <ul>
|
|
||||||
* <li>Retrieves references to all UI elements using findViewById</li>
|
|
||||||
* <li>Configures click listener for profile picture to launch image picker</li>
|
|
||||||
* <li>Configures click listener for save button to persist player data</li>
|
|
||||||
* <li>Configures click listener for crop confirmation button</li>
|
|
||||||
* <li>Configures click listener for crop cancellation button</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* The profile picture view is configured to launch the gallery picker when clicked,
|
|
||||||
* filtering for image MIME types only ("image/*").
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #mGetContent
|
|
||||||
* @see #savePlayer()
|
|
||||||
* @see #performCrop()
|
|
||||||
* @see #exitCropMode()
|
|
||||||
*/
|
*/
|
||||||
private void initViews() {
|
private void initViews() {
|
||||||
// Get references to layout containers
|
// 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.
|
* Initializes gesture detectors to handle pinch-to-zoom and pan gestures.
|
||||||
* <p>
|
|
||||||
* This method configures two types of touch interactions for the crop preview image:
|
|
||||||
* <ol>
|
|
||||||
* <li><strong>Pinch-to-Zoom:</strong> Two-finger pinch gestures to scale the image</li>
|
|
||||||
* <li><strong>Pan (Drag):</strong> Single-finger drag to reposition the image</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Scale Gesture Handling:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Pan Gesture Handling:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see ScaleGestureDetector
|
|
||||||
* @see MotionEvent
|
|
||||||
*/
|
*/
|
||||||
private void setupGestures() {
|
private void setupGestures() {
|
||||||
// Initialize scale detector for pinch-to-zoom functionality
|
// Initialize scale detector for pinch-to-zoom functionality
|
||||||
mScaleDetector = new ScaleGestureDetector(this, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
mScaleDetector = new ScaleGestureDetector(this, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onScale(ScaleGestureDetector detector) {
|
public boolean onScale(final ScaleGestureDetector detector) {
|
||||||
// Apply the scale factor from the gesture
|
// Apply the scale factor from the gesture
|
||||||
mScaleFactor *= detector.getScaleFactor();
|
mScaleFactor *= detector.getScaleFactor();
|
||||||
|
|
||||||
@@ -359,7 +217,7 @@ public class AddPlayerActivity extends AppCompatActivity {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Combined touch listener for both Panning and Scaling
|
// 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
|
// Pass touch event to scale detector first to handle pinch gestures
|
||||||
mScaleDetector.onTouchEvent(event);
|
mScaleDetector.onTouchEvent(event);
|
||||||
|
|
||||||
@@ -392,26 +250,10 @@ public class AddPlayerActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Transitions the UI from Form Mode to Crop Mode.
|
* Transitions the UI from Form Mode to Crop Mode.
|
||||||
* <p>
|
|
||||||
* This method performs the following operations:
|
|
||||||
* <ul>
|
|
||||||
* <li>Hides the main form layout</li>
|
|
||||||
* <li>Shows the cropper layout</li>
|
|
||||||
* <li>Resets all image transformations (scale, translation) to default values</li>
|
|
||||||
* <li>Loads the selected image into the crop preview ImageView</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
*
|
||||||
* @param uri The URI of the raw, unmodified image selected from the gallery.
|
* @param uri The URI of the image selected from the gallery.
|
||||||
* This image will be displayed in the crop preview for manipulation.
|
|
||||||
* @see #exitCropMode()
|
|
||||||
*/
|
*/
|
||||||
private void enterCropMode(Uri uri) {
|
private void enterCropMode(final Uri uri) {
|
||||||
// Hide form layout and show cropper layout
|
// Hide form layout and show cropper layout
|
||||||
mLayoutForm.setVisibility(View.GONE);
|
mLayoutForm.setVisibility(View.GONE);
|
||||||
mLayoutCropper.setVisibility(View.VISIBLE);
|
mLayoutCropper.setVisibility(View.VISIBLE);
|
||||||
@@ -429,20 +271,6 @@ public class AddPlayerActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Transitions the UI from Crop Mode back to Form Mode.
|
* Transitions the UI from Crop Mode back to Form Mode.
|
||||||
* <p>
|
|
||||||
* This method is called when the user either:
|
|
||||||
* <ul>
|
|
||||||
* <li>Confirms their crop selection (after {@link #performCrop()} completes)</li>
|
|
||||||
* <li>Cancels the crop operation without saving</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* The method simply toggles visibility between the two layout containers,
|
|
||||||
* hiding the cropper and showing the main form.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #enterCropMode(Uri)
|
|
||||||
* @see #performCrop()
|
|
||||||
*/
|
*/
|
||||||
private void exitCropMode() {
|
private void exitCropMode() {
|
||||||
// Hide cropper layout and show form layout
|
// 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.
|
* Performs the pixel-level mathematics to extract a square crop from the selected image.
|
||||||
* <p>
|
* Accounts for ImageView fit-center scale, user translation, and user zoom.
|
||||||
* 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:
|
|
||||||
* <ol>
|
|
||||||
* <li><strong>Original Image Dimensions:</strong> The actual pixel dimensions of the source bitmap</li>
|
|
||||||
* <li><strong>ImageView Fit-Center Scale:</strong> The automatic scaling applied by Android to fit the image in the view</li>
|
|
||||||
* <li><strong>User Translation (Panning):</strong> The X/Y offset from user drag gestures</li>
|
|
||||||
* <li><strong>User Scale (Zoom):</strong> The scale factor from user pinch-to-zoom gestures</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Algorithm Overview:</strong>
|
|
||||||
* <ol>
|
|
||||||
* <li>Decode the full bitmap from the URI</li>
|
|
||||||
* <li>Calculate the fit-center scale applied by the ImageView</li>
|
|
||||||
* <li>Combine fit-center scale with user's manual zoom scale</li>
|
|
||||||
* <li>Determine the current position of the bitmap in screen space</li>
|
|
||||||
* <li>Get the crop box coordinates from the overlay</li>
|
|
||||||
* <li>Transform screen coordinates to bitmap pixel coordinates</li>
|
|
||||||
* <li>Apply bounds checking to ensure valid crop dimensions</li>
|
|
||||||
* <li>Extract the cropped region and save to internal storage</li>
|
|
||||||
* <li>Update the profile picture preview with the cropped image</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Error Handling:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #saveBitmap(Bitmap)
|
|
||||||
* @see CropOverlayView#getCropRect()
|
|
||||||
*/
|
*/
|
||||||
private void performCrop() {
|
private void performCrop() {
|
||||||
Log.d(TAG, "Finalizing crop...");
|
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.
|
* Saves a bitmap to the application's private internal storage as JPEG with 90% quality.
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>File Storage Details:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>Location:</strong> Application's private files directory ({@code getFilesDir()})</li>
|
|
||||||
* <li><strong>Format:</strong> JPEG with 90% compression quality</li>
|
|
||||||
* <li><strong>Naming:</strong> "profile_" + UUID + ".jpg"</li>
|
|
||||||
* <li><strong>Security:</strong> Files are private to this app and not accessible by other apps</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Error Handling:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
*
|
||||||
* @param bmp The bitmap image to save. Must not be null.
|
* @param bmp The bitmap image to save.
|
||||||
* @return The absolute file path to the saved image file, or null if saving failed.
|
* @return The absolute file path, or null if saving failed.
|
||||||
* @see UUID#randomUUID()
|
|
||||||
* @see Bitmap#compress(Bitmap.CompressFormat, int, java.io.OutputStream)
|
|
||||||
*/
|
*/
|
||||||
private String saveBitmap(Bitmap bmp) {
|
private String saveBitmap(final Bitmap bmp) {
|
||||||
try {
|
try {
|
||||||
// Generate a unique filename using UUID to prevent collisions
|
// Generate a unique filename using UUID to prevent collisions
|
||||||
String name = "profile_" + UUID.randomUUID().toString() + ".jpg";
|
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.
|
* Loads an existing player's data from the database and populates the UI.
|
||||||
* <p>
|
* Database operations performed on background thread.
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Operations performed:</strong>
|
|
||||||
* <ol>
|
|
||||||
* <li>Queries the database for the player by ID (on background thread)</li>
|
|
||||||
* <li>Updates the username field with the player's current username</li>
|
|
||||||
* <li>Changes the title to "Update Profile" instead of "Add Player"</li>
|
|
||||||
* <li>Changes the save button text to "Update" instead of "Save"</li>
|
|
||||||
* <li>Loads and displays the player's profile picture if one exists</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Threading:</strong>
|
|
||||||
* 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)}.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see AppDatabase#playerDao()
|
|
||||||
* @see Player
|
|
||||||
*/
|
*/
|
||||||
private void loadExistingPlayer() {
|
private void loadExistingPlayer() {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
@@ -661,32 +414,7 @@ public class AddPlayerActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates and persists the player data to the database.
|
* Validates and persists the player data to the database.
|
||||||
* <p>
|
* Inserts new player or updates existing based on mExistingPlayer.
|
||||||
* This method determines whether to insert a new player or update an existing one
|
|
||||||
* based on whether {@link #mExistingPlayer} is null.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Validation:</strong>
|
|
||||||
* The username field must not be empty (after trimming whitespace). If validation fails,
|
|
||||||
* a toast message is shown and the method returns without saving.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Database Operations:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>Update Mode:</strong> If {@link #mExistingPlayer} is not null, updates the
|
|
||||||
* existing player's username and profile picture URI</li>
|
|
||||||
* <li><strong>Insert Mode:</strong> If {@link #mExistingPlayer} is null, creates a new
|
|
||||||
* Player object and inserts it into the database</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Threading:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see Player
|
|
||||||
* @see AppDatabase#playerDao()
|
|
||||||
*/
|
*/
|
||||||
private void savePlayer() {
|
private void savePlayer() {
|
||||||
// Validate username input
|
// Validate username input
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -24,74 +24,36 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MainMenuActivity serves as the primary entry point and main screen of the Oche Companion application.
|
* Main entry point and home screen of the Oche Companion application.
|
||||||
* <p>
|
* Displays the squad of players, allows adding new players, and shows match recap with test data.
|
||||||
* This activity provides the following functionality:
|
|
||||||
* <ul>
|
|
||||||
* <li>Displays the squad of players in a RecyclerView</li>
|
|
||||||
* <li>Allows adding new players to the squad</li>
|
|
||||||
* <li>Shows a match recap view with test data for development purposes</li>
|
|
||||||
* <li>Manages the application's database connection</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see AppCompatActivity
|
|
||||||
* @see AppDatabase
|
|
||||||
* @see MatchRecapView
|
|
||||||
* @see MainMenuPlayerAdapter
|
|
||||||
* @author Oche Companion Development Team
|
|
||||||
* @version 1.0
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
*/
|
||||||
public class MainMenuActivity extends AppCompatActivity {
|
public class MainMenuActivity extends AppCompatActivity {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag for debugging purposes.
|
* Tag for debugging and logging purposes.
|
||||||
* Used for logging and identifying this activity in debug output.
|
|
||||||
*/
|
*/
|
||||||
private static final String TAG = "MainMenuActivity";
|
private static final String TAG = "MainMenuActivity";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom view component that displays a summary of a match.
|
* Custom view component that displays a match summary.
|
||||||
* <p>
|
* Can be clicked to cycle through different test data states.
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see MatchRecapView
|
|
||||||
*/
|
*/
|
||||||
private MatchRecapView mMatchRecap;
|
private MatchRecapView mMatchRecap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Counter used for cycling through different test data scenarios.
|
* Counter for cycling through different test data scenarios.
|
||||||
* <p>
|
* Increments on each click to cycle through null match, 1v1 match, and group match states.
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*/
|
*/
|
||||||
private int testCounter = 0;
|
private int mTestCounter = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the activity is first created.
|
* Initializes the activity: enables edge-to-edge display, configures window insets,
|
||||||
* <p>
|
* and sets up the match recap view with test data click listener.
|
||||||
* This method performs the following initialization tasks:
|
*
|
||||||
* <ul>
|
* @param savedInstanceState Bundle containing saved state, or null if none exists.
|
||||||
* <li>Enables edge-to-edge display for modern Android UI</li>
|
|
||||||
* <li>Sets the activity's content view to the main layout</li>
|
|
||||||
* <li>Configures window insets for proper system bar handling</li>
|
|
||||||
* <li>Initializes the database connection</li>
|
|
||||||
* <li>Sets up the match recap view with a test data click listener</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param savedInstanceState If the activity is being re-initialized after previously being shut down,
|
|
||||||
* this Bundle contains the data it most recently supplied in
|
|
||||||
* {@link #onSaveInstanceState(Bundle)}. Otherwise, it is null.
|
|
||||||
* @see AppCompatActivity#onCreate(Bundle)
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
// Enable edge-to-edge display for immersive UI experience
|
// Enable edge-to-edge display for immersive UI experience
|
||||||
@@ -111,22 +73,16 @@ public class MainMenuActivity extends AppCompatActivity {
|
|||||||
mMatchRecap = findViewById(R.id.match_recap);
|
mMatchRecap = findViewById(R.id.match_recap);
|
||||||
mMatchRecap.setOnClickListener(new View.OnClickListener() {
|
mMatchRecap.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(final View v) {
|
||||||
// Cycle through test data scenarios on each click
|
// Cycle through test data scenarios on each click
|
||||||
applyTestData(testCounter);
|
applyTestData(mTestCounter);
|
||||||
testCounter++;
|
mTestCounter++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called after {@link #onStart} when the activity is becoming visible to the user.
|
* Refreshes the squad view with latest player data from the database when activity resumes.
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see AppCompatActivity#onResume()
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
@@ -137,38 +93,8 @@ public class MainMenuActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiates a quick-start 501 game with two test players.
|
* Initiates a quick-start 501 game with two test players.
|
||||||
* <p>
|
* Creates test players "Test1" and "Test2" and launches GameActivity.
|
||||||
* This convenience method creates two test players with minimal configuration
|
* Test players are not persisted to the database.
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* The method:
|
|
||||||
* <ul>
|
|
||||||
* <li>Creates two test players named "Test1" and "Test2" with no profile pictures</li>
|
|
||||||
* <li>Adds them to a player list</li>
|
|
||||||
* <li>Launches {@link GameActivity} with a starting score of 501</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Use Cases:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Quick testing of game functionality during development</li>
|
|
||||||
* <li>Demonstrating the app without setting up a full squad</li>
|
|
||||||
* <li>Rapid game start for users who want to practice scoring</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Note:</strong> The test players created here are not persisted to the database.
|
|
||||||
* They exist only for the duration of the game session.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Triggered by the quick start button in the main menu UI.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see GameActivity#start(android.content.Context, ArrayList, int)
|
|
||||||
* @see Player
|
|
||||||
*/
|
*/
|
||||||
private void quickStart() {
|
private void quickStart() {
|
||||||
final Player playerOne = new Player("Test1", null);
|
final Player playerOne = new Player("Test1", null);
|
||||||
@@ -181,26 +107,8 @@ public class MainMenuActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes and configures the squad view component.
|
* Initializes the squad view: sets up the RecyclerView with adapter,
|
||||||
* <p>
|
* configures the add player button, and loads all players from the database on a background thread.
|
||||||
* This method performs the following operations:
|
|
||||||
* <ul>
|
|
||||||
* <li>Retrieves references to UI components (add player button and squad RecyclerView)</li>
|
|
||||||
* <li>Sets up the RecyclerView with a LinearLayoutManager</li>
|
|
||||||
* <li>Initializes and attaches the MainMenuPlayerAdapter to the RecyclerView</li>
|
|
||||||
* <li>Configures the add player button to launch the AddPlayerActivity</li>
|
|
||||||
* <li>Loads all players from the database on a background thread</li>
|
|
||||||
* <li>Updates the adapter with player data on the UI thread</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Note:</strong> Database operations are performed on a background thread to prevent
|
|
||||||
* blocking the main UI thread, which ensures the application remains responsive.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see MainMenuPlayerAdapter
|
|
||||||
* @see AddPlayerActivity
|
|
||||||
* @see LinearLayoutManager
|
|
||||||
*/
|
*/
|
||||||
private void initSquadView() {
|
private void initSquadView() {
|
||||||
// Get references to UI components
|
// 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.
|
* Applies test data to the match recap view for development and testing.
|
||||||
* <p>
|
* Cycles through null match (counter % 3 == 0), 1v1 match (counter % 3 == 1),
|
||||||
* This method creates sample player and match objects and cycles through different
|
* and group match (counter % 3 == 2) based on the counter value.
|
||||||
* display states based on the provided counter value:
|
*
|
||||||
* <ul>
|
* @param counter Counter value used to determine which test scenario to display.
|
||||||
* <li>When counter % 3 == 0: Displays null (no match)</li>
|
|
||||||
* <li>When counter % 3 == 1: Displays a 1v1 match (two players)</li>
|
|
||||||
* <li>When counter % 3 == 2: Displays a group match (four players)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Note:</strong> This method is intended for development and testing only
|
|
||||||
* and should be removed or disabled in production builds.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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)
|
|
||||||
*/
|
*/
|
||||||
private void applyTestData(final int counter) {
|
private void applyTestData(final int counter) {
|
||||||
// Create test player objects
|
// Create test player objects
|
||||||
|
|||||||
@@ -11,847 +11,62 @@ import com.aldo.apps.ochecompanion.database.objects.Match;
|
|||||||
import com.aldo.apps.ochecompanion.database.objects.Player;
|
import com.aldo.apps.ochecompanion.database.objects.Player;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main Room database class for the Oche Companion darts application.
|
* Main Room database class for the Oche Companion darts application.
|
||||||
* <p>
|
* Manages data persistence for players, matches, and statistics using the Singleton pattern.
|
||||||
* This abstract class serves as the central database access point, managing all
|
* Uses version 2 with destructive migration (data lost on schema changes).
|
||||||
* data persistence for players, matches, and related statistics. It implements
|
* Database operations must be performed on background threads.
|
||||||
* the Singleton design pattern to ensure only one database instance exists
|
*
|
||||||
* throughout the application lifecycle, preventing resource conflicts and ensuring
|
|
||||||
* data consistency.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Room Database Architecture:</strong>
|
|
||||||
* Room is Android's SQLite object-relational mapping (ORM) library that provides
|
|
||||||
* an abstraction layer over SQLite, offering:
|
|
||||||
* <ul>
|
|
||||||
* <li>Compile-time SQL query verification</li>
|
|
||||||
* <li>Convenient annotation-based entity and DAO definitions</li>
|
|
||||||
* <li>Automatic conversion between SQLite and Java/Kotlin objects</li>
|
|
||||||
* <li>LiveData and Flow support for reactive UI updates</li>
|
|
||||||
* <li>Migration support for database schema changes</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Database Entities:</strong>
|
|
||||||
* This database manages two primary tables:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>players:</strong> Squad roster with player profiles and career statistics
|
|
||||||
* (defined by {@link Player} entity)</li>
|
|
||||||
* <li><strong>matches:</strong> Completed match records with participant data and results
|
|
||||||
* (defined by {@link Match} entity)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Database Access Objects (DAOs):</strong>
|
|
||||||
* Database operations are performed through specialized DAO interfaces:
|
|
||||||
* <ul>
|
|
||||||
* <li>{@link PlayerDao}: CRUD operations for player management (insert, update, query)</li>
|
|
||||||
* <li>{@link MatchDao}: Match record operations (insert, query all, query last match)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Singleton Pattern Implementation:</strong>
|
|
||||||
* The class uses the thread-safe double-checked locking pattern to ensure a single
|
|
||||||
* database instance. This approach:
|
|
||||||
* <ul>
|
|
||||||
* <li>Prevents multiple database connections that waste resources</li>
|
|
||||||
* <li>Ensures data consistency across the application</li>
|
|
||||||
* <li>Reduces memory overhead by sharing one connection pool</li>
|
|
||||||
* <li>Thread-safe initialization prevents race conditions</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Database Versioning:</strong>
|
|
||||||
* Currently at version 2, indicating one schema change since initial creation.
|
|
||||||
* Version increments are required when:
|
|
||||||
* <ul>
|
|
||||||
* <li>Adding or removing tables</li>
|
|
||||||
* <li>Adding or removing columns</li>
|
|
||||||
* <li>Changing column types or constraints</li>
|
|
||||||
* <li>Modifying primary keys or indices</li>
|
|
||||||
* </ul>
|
|
||||||
* The {@code fallbackToDestructiveMigration()} strategy means schema changes will
|
|
||||||
* drop and recreate all tables, losing existing data. This is acceptable during
|
|
||||||
* development but should be replaced with proper migrations for production.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Migration Strategy:</strong>
|
|
||||||
* For production releases, replace destructive migration with proper migration paths:
|
|
||||||
* <pre>
|
|
||||||
* 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Database File Location:</strong>
|
|
||||||
* The SQLite database file "oche_companion_db" is stored in the app's private
|
|
||||||
* storage at:
|
|
||||||
* <pre>
|
|
||||||
* /data/data/com.aldo.apps.ochecompanion/databases/oche_companion_db
|
|
||||||
* </pre>
|
|
||||||
* This location is:
|
|
||||||
* <ul>
|
|
||||||
* <li>Private to the application (other apps cannot access)</li>
|
|
||||||
* <li>Automatically backed up with Auto Backup (if enabled)</li>
|
|
||||||
* <li>Cleared when the app is uninstalled</li>
|
|
||||||
* <li>Accessible for inspection via Android Studio Database Inspector</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Schema Export:</strong>
|
|
||||||
* The {@code exportSchema = false} setting disables automatic schema JSON export.
|
|
||||||
* For production apps, consider enabling this:
|
|
||||||
* <pre>
|
|
||||||
* @Database(entities = {...}, version = 2, exportSchema = true)
|
|
||||||
* </pre>
|
|
||||||
* And specify the export directory in build.gradle:
|
|
||||||
* <pre>
|
|
||||||
* android {
|
|
||||||
* defaultConfig {
|
|
||||||
* javaCompileOptions {
|
|
||||||
* annotationProcessorOptions {
|
|
||||||
* arguments += ["room.schemaLocation": "$projectDir/schemas"]
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* This creates version-controlled schema files for tracking changes and testing migrations.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Threading Requirements:</strong>
|
|
||||||
* Room enforces that database operations occur on background threads:
|
|
||||||
* <ul>
|
|
||||||
* <li>Attempting database operations on the main thread throws {@link IllegalStateException}</li>
|
|
||||||
* <li>Use {@link Thread}, {@link java.util.concurrent.ExecutorService}, or Kotlin coroutines</li>
|
|
||||||
* <li>UI updates after database operations must use {@code runOnUiThread()} or similar</li>
|
|
||||||
* <li>Consider using LiveData or Flow for automatic thread handling and UI updates</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Database Inspection:</strong>
|
|
||||||
* For debugging, you can inspect the database using:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>Android Studio Database Inspector:</strong> View > Tool Windows > Database Inspector</li>
|
|
||||||
* <li><strong>ADB:</strong> {@code adb shell} and SQLite command line</li>
|
|
||||||
* <li><strong>Third-party tools:</strong> DB Browser for SQLite (requires root or backup)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Performance Optimization:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Database queries are optimized with SQLite indices on primary keys</li>
|
|
||||||
* <li>Connection pooling is handled automatically by Room</li>
|
|
||||||
* <li>Consider adding custom indices for frequently queried columns:
|
|
||||||
* <pre>@Entity(indices = {@Index(value = {"username"})})</pre></li>
|
|
||||||
* <li>Use transactions for batch operations to improve performance</li>
|
|
||||||
* <li>Avoid N+1 query problems by using JOIN queries or @Relation</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Testing:</strong>
|
|
||||||
* For unit testing, create an in-memory database:
|
|
||||||
* <pre>
|
|
||||||
* @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();
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Data Backup and Restore:</strong>
|
|
||||||
* Consider implementing backup functionality:
|
|
||||||
* <ul>
|
|
||||||
* <li>Export database to external storage or cloud</li>
|
|
||||||
* <li>Use Android's Auto Backup for automatic cloud backup</li>
|
|
||||||
* <li>Provide manual export/import for user control</li>
|
|
||||||
* <li>Validate data integrity after restore operations</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Security Considerations:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Database is stored in app's private directory (secure by default)</li>
|
|
||||||
* <li>For sensitive data, consider using SQLCipher for encryption</li>
|
|
||||||
* <li>Be cautious when exporting database (may contain user data)</li>
|
|
||||||
* <li>Validate and sanitize all data before insertion to prevent SQL injection
|
|
||||||
* (Room's parameterized queries provide protection)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Future Enhancements:</strong>
|
|
||||||
* Consider these improvements for future versions:
|
|
||||||
* <ul>
|
|
||||||
* <li>Add proper migration strategies instead of destructive migration</li>
|
|
||||||
* <li>Implement database encryption with SQLCipher</li>
|
|
||||||
* <li>Add support for exporting/importing data</li>
|
|
||||||
* <li>Create additional entities for tournaments, achievements, settings</li>
|
|
||||||
* <li>Implement multi-user support with user profiles table</li>
|
|
||||||
* <li>Add full-text search capabilities with FTS tables</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see RoomDatabase
|
|
||||||
* @see Database
|
|
||||||
* @see Player
|
|
||||||
* @see Match
|
|
||||||
* @see PlayerDao
|
* @see PlayerDao
|
||||||
* @see MatchDao
|
* @see MatchDao
|
||||||
* @author Oche Companion Development Team
|
* @see Player
|
||||||
* @version 2.0
|
* @see Match
|
||||||
* @since 1.0
|
|
||||||
*/
|
*/
|
||||||
@Database(entities = {Player.class, Match.class}, version = 2, exportSchema = false)
|
@Database(entities = {Player.class, Match.class}, version = 2, exportSchema = false)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides access to Player-related database operations.
|
* Returns the PlayerDao for performing CRUD operations on player records.
|
||||||
* <p>
|
* Thread-safe and can be reused. Operations must run on background threads.
|
||||||
* This abstract method is implemented by Room at compile time, returning an
|
*
|
||||||
* instance of the {@link PlayerDao} interface. The DAO (Data Access Object)
|
* @return PlayerDao instance for player database operations
|
||||||
* provides methods for all player-related database operations including
|
|
||||||
* creating, reading, updating, and managing player records.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Available Operations:</strong>
|
|
||||||
* The returned PlayerDao provides these methods:
|
|
||||||
* <ul>
|
|
||||||
* <li>{@link PlayerDao#insert(Player)} - Add new player to squad</li>
|
|
||||||
* <li>{@link PlayerDao#update(Player)} - Update existing player information</li>
|
|
||||||
* <li>{@link PlayerDao#getPlayerById(int)} - Retrieve specific player by ID</li>
|
|
||||||
* <li>{@link PlayerDao#getAllPlayers()} - Get all players sorted alphabetically</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Thread Safety:</strong>
|
|
||||||
* 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}.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>DAO Lifecycle:</strong>
|
|
||||||
* The DAO instance is created once by Room and can be cached and reused:
|
|
||||||
* <pre>
|
|
||||||
* // 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);
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Common Usage Patterns:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Alternative Reactive Approaches:</strong>
|
|
||||||
* Consider using LiveData or Flow for automatic UI updates:
|
|
||||||
* <pre>
|
|
||||||
* // 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);
|
|
||||||
* });
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Error Handling:</strong>
|
|
||||||
* <pre>
|
|
||||||
* 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return The PlayerDao instance for accessing player-related database operations.
|
|
||||||
* Never returns null. The returned instance can be safely cached and reused.
|
|
||||||
* @see PlayerDao
|
* @see PlayerDao
|
||||||
* @see Player
|
|
||||||
*/
|
*/
|
||||||
public abstract PlayerDao playerDao();
|
public abstract PlayerDao playerDao();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides access to Match-related database operations.
|
* Returns the MatchDao for managing match records and history.
|
||||||
* <p>
|
* Thread-safe and can be reused. Operations must run on background threads.
|
||||||
* This abstract method is implemented by Room at compile time, returning an
|
*
|
||||||
* instance of the {@link MatchDao} interface. The DAO provides methods for
|
* @return MatchDao instance for match database operations
|
||||||
* storing and retrieving match records, enabling match history tracking and
|
|
||||||
* statistical analysis.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Available Operations:</strong>
|
|
||||||
* The returned MatchDao provides these methods:
|
|
||||||
* <ul>
|
|
||||||
* <li>{@link MatchDao#insert(Match)} - Save completed match to history</li>
|
|
||||||
* <li>{@link MatchDao#getAllMatches()} - Retrieve all matches ordered by most recent</li>
|
|
||||||
* <li>{@link MatchDao#getLastMatch()} - Get the most recently completed match</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Match History Display:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Match Completion Flow:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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();
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Thread Safety:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Performance Considerations:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>getAllMatches() loads all match records - consider pagination for users
|
|
||||||
* with hundreds of matches</li>
|
|
||||||
* <li>getLastMatch() uses LIMIT 1 for efficient querying</li>
|
|
||||||
* <li>Matches are ordered by timestamp DESC for chronological display</li>
|
|
||||||
* <li>Consider adding date range queries for filtering match history</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Statistical Queries:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>DAO Lifecycle:</strong>
|
|
||||||
* The DAO instance can be cached and reused throughout the app:
|
|
||||||
* <pre>
|
|
||||||
* // Cache in Application class or repository
|
|
||||||
* private MatchDao matchDao;
|
|
||||||
*
|
|
||||||
* public MatchDao getMatchDao() {
|
|
||||||
* if (matchDao == null) {
|
|
||||||
* matchDao = AppDatabase.getDatabase(context).matchDao();
|
|
||||||
* }
|
|
||||||
* return matchDao;
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Error Handling:</strong>
|
|
||||||
* <pre>
|
|
||||||
* 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return The MatchDao instance for accessing match-related database operations.
|
|
||||||
* Never returns null. The returned instance can be safely cached and reused.
|
|
||||||
* @see MatchDao
|
* @see MatchDao
|
||||||
* @see Match
|
|
||||||
*/
|
*/
|
||||||
public abstract MatchDao matchDao();
|
public abstract MatchDao matchDao();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The singleton instance of the AppDatabase.
|
* Singleton instance of the AppDatabase.
|
||||||
* <p>
|
* Volatile ensures thread-safe visibility in double-checked locking pattern.
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Volatile Keyword:</strong>
|
|
||||||
* The volatile modifier guarantees:
|
|
||||||
* <ul>
|
|
||||||
* <li>Happens-before relationship: Writes to INSTANCE are visible to all threads</li>
|
|
||||||
* <li>Prevents instruction reordering that could break double-checked locking</li>
|
|
||||||
* <li>Ensures thread sees the fully constructed object, not a partially initialized one</li>
|
|
||||||
* <li>No caching of the variable value in CPU registers</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Initialization State:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>null:</strong> Before first call to {@link #getDatabase(Context)}</li>
|
|
||||||
* <li><strong>non-null:</strong> After database is created, remains set for app lifetime</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Memory Lifecycle:</strong>
|
|
||||||
* The database instance is retained in memory for the lifetime of the application
|
|
||||||
* process. It will be garbage collected only when:
|
|
||||||
* <ul>
|
|
||||||
* <li>The app process is terminated by Android</li>
|
|
||||||
* <li>The app is killed by the user</li>
|
|
||||||
* <li>System needs to reclaim memory and kills the app's process</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Testing Considerations:</strong>
|
|
||||||
* For unit tests, you may need to reset the singleton:
|
|
||||||
* <pre>
|
|
||||||
* // 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;
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Why Singleton:</strong>
|
|
||||||
* Using a singleton for the database instance provides:
|
|
||||||
* <ul>
|
|
||||||
* <li>Single connection pool shared across the app</li>
|
|
||||||
* <li>Consistent data state across all components</li>
|
|
||||||
* <li>Reduced memory overhead (no duplicate instances)</li>
|
|
||||||
* <li>Prevention of conflicting database access</li>
|
|
||||||
* <li>Simplified database access (no need to pass instance around)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Thread Safety Analysis:</strong>
|
|
||||||
* The volatile keyword combined with synchronized block in {@link #getDatabase(Context)}
|
|
||||||
* ensures thread-safe lazy initialization:
|
|
||||||
* <pre>
|
|
||||||
* // 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
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #getDatabase(Context)
|
|
||||||
* @see RoomDatabase
|
|
||||||
*/
|
*/
|
||||||
private static volatile AppDatabase INSTANCE;
|
private static volatile AppDatabase sInstance;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the singleton instance of the AppDatabase, creating it if necessary.
|
* Gets the singleton AppDatabase instance, creating it if necessary.
|
||||||
* <p>
|
* Thread-safe using double-checked locking. Uses application context to prevent leaks.
|
||||||
* This method implements the thread-safe double-checked locking pattern to ensure
|
*
|
||||||
* only one database instance is created, even when called simultaneously from
|
* @param context Context used to create the database (converted to application context)
|
||||||
* multiple threads. The method is safe to call from any thread and can be invoked
|
* @return Singleton AppDatabase instance
|
||||||
* from any component (Activity, Fragment, Service, etc.).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Double-Checked Locking Pattern:</strong>
|
|
||||||
* The implementation uses two null checks to optimize performance:
|
|
||||||
* <ol>
|
|
||||||
* <li><strong>First check (unsynchronized):</strong> Fast path for when instance exists.
|
|
||||||
* Avoids expensive synchronization on subsequent calls.</li>
|
|
||||||
* <li><strong>Synchronized block:</strong> Ensures only one thread creates the instance.
|
|
||||||
* Prevents race conditions during initialization.</li>
|
|
||||||
* <li><strong>Second check (synchronized):</strong> Prevents multiple creation if several
|
|
||||||
* threads passed the first check simultaneously.</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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);
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Context Parameter:</strong>
|
|
||||||
* The method accepts any Context but internally uses {@code context.getApplicationContext()}
|
|
||||||
* to avoid memory leaks. This means:
|
|
||||||
* <ul>
|
|
||||||
* <li>Activity context is safe to pass (converted to app context)</li>
|
|
||||||
* <li>Fragment context is safe to pass (converted to app context)</li>
|
|
||||||
* <li>Application context is ideal but not required</li>
|
|
||||||
* <li>No risk of leaking Activity or Fragment references</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Database Builder Configuration:</strong>
|
|
||||||
* The database is created with these settings:
|
|
||||||
* <pre>
|
|
||||||
* 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Destructive Migration Strategy:</strong>
|
|
||||||
* The {@code fallbackToDestructiveMigration()} setting means:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>Advantage:</strong> No need to write migration code during development</li>
|
|
||||||
* <li><strong>Disadvantage:</strong> All data is lost when database version changes</li>
|
|
||||||
* <li><strong>Development:</strong> Acceptable for testing and iteration</li>
|
|
||||||
* <li><strong>Production:</strong> Should be replaced with proper migrations:
|
|
||||||
* <pre>.addMigrations(MIGRATION_1_2, MIGRATION_2_3)</pre>
|
|
||||||
* </li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>First Call Initialization:</strong>
|
|
||||||
* On the first call, this method will:
|
|
||||||
* <ol>
|
|
||||||
* <li>Create the SQLite database file in app's private storage</li>
|
|
||||||
* <li>Create all tables defined by entity classes (players, matches)</li>
|
|
||||||
* <li>Set up indices and constraints</li>
|
|
||||||
* <li>Initialize Room's internal structures</li>
|
|
||||||
* <li>Return the ready-to-use database instance</li>
|
|
||||||
* </ol>
|
|
||||||
* This initialization is typically fast (< 100ms) but happens on the calling thread,
|
|
||||||
* so consider calling from a background thread if concerned about main thread performance.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Subsequent Calls:</strong>
|
|
||||||
* After initialization, subsequent calls:
|
|
||||||
* <ul>
|
|
||||||
* <li>Return immediately (fast path, no synchronization)</li>
|
|
||||||
* <li>No database access or initialization overhead</li>
|
|
||||||
* <li>Thread-safe from all threads</li>
|
|
||||||
* <li>Essentially free performance-wise</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Thread Safety Guarantee:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Best Practices:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Call early in app lifecycle (Application.onCreate()) to avoid first-call delay</li>
|
|
||||||
* <li>Use application context when possible for clarity</li>
|
|
||||||
* <li>No need to cache the returned instance (method is fast)</li>
|
|
||||||
* <li>Safe to call repeatedly throughout the app</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Proactive Initialization:</strong>
|
|
||||||
* <pre>
|
|
||||||
* public class OcheCompanionApplication extends Application {
|
|
||||||
* @Override
|
|
||||||
* public void onCreate() {
|
|
||||||
* super.onCreate();
|
|
||||||
*
|
|
||||||
* // Initialize database early to avoid delay on first access
|
|
||||||
* AppDatabase.getDatabase(this);
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Error Handling:</strong>
|
|
||||||
* In rare cases, database creation might fail due to:
|
|
||||||
* <ul>
|
|
||||||
* <li>Insufficient storage space</li>
|
|
||||||
* <li>Corrupted database file</li>
|
|
||||||
* <li>File system errors</li>
|
|
||||||
* </ul>
|
|
||||||
* Room will throw RuntimeException in these cases. Consider adding error handling:
|
|
||||||
* <pre>
|
|
||||||
* 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.
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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()
|
|
||||||
*/
|
*/
|
||||||
public static AppDatabase getDatabase(final Context context) {
|
public static AppDatabase getDatabase(final Context context) {
|
||||||
// First check (unsynchronized): Fast path when instance already exists
|
// First check (unsynchronized): Fast path when instance already exists
|
||||||
// Most calls will return here after first initialization
|
// 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
|
// Synchronize on the class to ensure only one thread can create the instance
|
||||||
synchronized (AppDatabase.class) {
|
synchronized (AppDatabase.class) {
|
||||||
// Second check (synchronized): Prevent creation if another thread
|
// Second check (synchronized): Prevent creation if another thread
|
||||||
// created the instance while we were waiting for the lock
|
// created the instance while we were waiting for the lock
|
||||||
if (INSTANCE == null) {
|
if (sInstance == null) {
|
||||||
// Create the database instance
|
// Create the database instance
|
||||||
// Use application context to prevent memory leaks
|
// Use application context to prevent memory leaks
|
||||||
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
|
sInstance = Room.databaseBuilder(context.getApplicationContext(),
|
||||||
AppDatabase.class, "oche_companion_db")
|
AppDatabase.class, "oche_companion_db")
|
||||||
.fallbackToDestructiveMigration() // Drop tables on version change
|
.fallbackToDestructiveMigration() // Drop tables on version change
|
||||||
.build();
|
.build();
|
||||||
@@ -859,7 +74,7 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Return the singleton instance (thread-safe due to volatile)
|
// Return the singleton instance (thread-safe due to volatile)
|
||||||
return INSTANCE;
|
return sInstance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,282 +7,35 @@ import com.aldo.apps.ochecompanion.database.objects.Match;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data Access Object (DAO) interface for performing database operations on Match entities.
|
* Data Access Object for Match entities in the Room database.
|
||||||
* <p>
|
* Provides methods to insert matches and query match history.
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Room Database Integration:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Key Features:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Insert completed match records into the database</li>
|
|
||||||
* <li>Retrieve complete match history sorted by most recent first</li>
|
|
||||||
* <li>Query the last played match for dashboard/recap displays</li>
|
|
||||||
* <li>Thread-safe operations managed by Room</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Thread Safety:</strong>
|
|
||||||
* 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).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Database Table:</strong>
|
|
||||||
* This DAO operates on the "matches" table, which is defined by the {@link Match}
|
|
||||||
* entity class with its {@code @Entity} annotation.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see Match
|
|
||||||
* @see Dao
|
|
||||||
* @see com.aldo.apps.ochecompanion.database.AppDatabase
|
|
||||||
* @author Oche Companion Development Team
|
|
||||||
* @version 1.0
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
public interface MatchDao {
|
public interface MatchDao {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts a completed match record into the database.
|
* Inserts a completed match record into the database.
|
||||||
* <p>
|
* Must be called on a background thread.
|
||||||
* 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.).
|
* @param match The Match entity to persist
|
||||||
* Room will handle the actual SQL INSERT operation and auto-increment primary keys if configured.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Threading:</strong>
|
|
||||||
* 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).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Transaction Behavior:</strong>
|
|
||||||
* By default, Room wraps this operation in a transaction. If the insert fails, the
|
|
||||||
* transaction will be rolled back, maintaining database consistency.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Conflict Strategy:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Return Value:</strong>
|
|
||||||
* While this method returns void, the {@code @Insert} annotation can be configured
|
|
||||||
* to return:
|
|
||||||
* <ul>
|
|
||||||
* <li>{@code long} - The row ID of the newly inserted match</li>
|
|
||||||
* <li>{@code Long} - Same as above, but nullable</li>
|
|
||||||
* <li>{@code long[]} or {@code Long[]} - For bulk inserts</li>
|
|
||||||
* <li>{@code List<Long>} - Alternative for bulk inserts</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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
|
|
||||||
*/
|
*/
|
||||||
@Insert
|
@Insert
|
||||||
void insert(Match match);
|
void insert(final Match match);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all match records from the database, ordered by most recent first.
|
* Retrieves all match records ordered by most recent first.
|
||||||
* <p>
|
* Must be called on a background thread.
|
||||||
* 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
|
* @return List of all matches sorted by timestamp descending
|
||||||
* first element in the returned list.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>SQL Query:</strong>
|
|
||||||
* Executes: {@code SELECT * FROM matches ORDER BY timestamp DESC}
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Sorting:</strong>
|
|
||||||
* Matches are ordered by the "timestamp" field in descending order (DESC), meaning:
|
|
||||||
* <ul>
|
|
||||||
* <li>Index 0: Most recent match</li>
|
|
||||||
* <li>Index 1: Second most recent match</li>
|
|
||||||
* <li>Last index: Oldest match</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Threading:</strong>
|
|
||||||
* 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).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Performance Considerations:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>This query loads ALL matches into memory, which could be problematic for
|
|
||||||
* large datasets (thousands of matches)</li>
|
|
||||||
* <li>Consider using pagination (e.g., {@code LIMIT} and {@code OFFSET}) for better
|
|
||||||
* performance with large history</li>
|
|
||||||
* <li>Consider using LiveData or Flow for reactive updates</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Empty Result:</strong>
|
|
||||||
* If no matches exist in the database, this method returns an empty list (not null).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* new Thread(() -> {
|
|
||||||
* List<Match> matches = matchDao.getAllMatches();
|
|
||||||
* runOnUiThread(() -> {
|
|
||||||
* if (matches.isEmpty()) {
|
|
||||||
* showEmptyState();
|
|
||||||
* } else {
|
|
||||||
* displayMatchHistory(matches);
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
* }).start();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Suggested Improvements:</strong>
|
|
||||||
* Consider adding an index on the timestamp column for faster sorting:
|
|
||||||
* <pre>
|
|
||||||
* @Entity(tableName = "matches",
|
|
||||||
* indices = {@Index(value = {"timestamp"}, name = "index_timestamp")})
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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()
|
|
||||||
*/
|
*/
|
||||||
@Query("SELECT * FROM matches ORDER BY timestamp DESC")
|
@Query("SELECT * FROM matches ORDER BY timestamp DESC")
|
||||||
List<Match> getAllMatches();
|
List<Match> getAllMatches();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the most recently played match from the database.
|
* Retrieves the most recently played match.
|
||||||
* <p>
|
* Must be called on a background thread.
|
||||||
* 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
|
* @return The most recent match, or null if no matches exist
|
||||||
* just the single most recent match based on timestamp.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>SQL Query:</strong>
|
|
||||||
* Executes: {@code SELECT * FROM matches ORDER BY timestamp DESC LIMIT 1}
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Query Optimization:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Threading:</strong>
|
|
||||||
* 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).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Null Return Value:</strong>
|
|
||||||
* This method returns {@code null} if no matches exist in the database. Callers
|
|
||||||
* must check for null before using the returned value:
|
|
||||||
* <pre>
|
|
||||||
* Match lastMatch = matchDao.getLastMatch();
|
|
||||||
* if (lastMatch != null) {
|
|
||||||
* // Use the match
|
|
||||||
* } else {
|
|
||||||
* // Show empty state
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* new Thread(() -> {
|
|
||||||
* Match lastMatch = matchDao.getLastMatch();
|
|
||||||
* runOnUiThread(() -> {
|
|
||||||
* if (lastMatch != null) {
|
|
||||||
* matchRecapView.setMatch(lastMatch);
|
|
||||||
* } else {
|
|
||||||
* matchRecapView.setMatch(null); // Shows empty state
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
* }).start();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Use Cases:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Displaying the last match on the dashboard/main menu</li>
|
|
||||||
* <li>Showing a "play again" option with the most recent configuration</li>
|
|
||||||
* <li>Populating match recap views</li>
|
|
||||||
* <li>Checking if any matches have been played</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Performance:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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)
|
|
||||||
*/
|
*/
|
||||||
@Query("SELECT * FROM matches ORDER BY timestamp DESC LIMIT 1")
|
@Query("SELECT * FROM matches ORDER BY timestamp DESC LIMIT 1")
|
||||||
Match getLastMatch();
|
Match getLastMatch();
|
||||||
|
|||||||
@@ -10,447 +10,45 @@ import com.aldo.apps.ochecompanion.database.objects.Player;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data Access Object (DAO) interface for performing database operations on Player entities.
|
* Data Access Object for Player entities in the Room database.
|
||||||
* <p>
|
* Provides CRUD operations for managing the squad roster.
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Room Database Integration:</strong>
|
|
||||||
* The {@code @Dao} annotation marks this as a Room DAO interface. Room's annotation
|
|
||||||
* processor will automatically generate an implementation class that handles:
|
|
||||||
* <ul>
|
|
||||||
* <li>SQL query construction and execution</li>
|
|
||||||
* <li>SQLite connection management</li>
|
|
||||||
* <li>Result cursor handling and mapping to Player objects</li>
|
|
||||||
* <li>Transaction management and error handling</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Key Features:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>Insert:</strong> Add new players to the squad roster</li>
|
|
||||||
* <li><strong>Update:</strong> Modify existing player information (username, profile picture, stats)</li>
|
|
||||||
* <li><strong>Query by ID:</strong> Retrieve a specific player for editing</li>
|
|
||||||
* <li><strong>Query All:</strong> Get the complete squad list, alphabetically sorted</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Thread Safety:</strong>
|
|
||||||
* 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).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Database Table:</strong>
|
|
||||||
* This DAO operates on the "players" table, which is defined by the {@link Player}
|
|
||||||
* entity class with its {@code @Entity} annotation.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Missing Operations:</strong>
|
|
||||||
* Note that this DAO does not currently include a DELETE operation. If player deletion
|
|
||||||
* is required, consider adding:
|
|
||||||
* <pre>
|
|
||||||
* @Delete
|
|
||||||
* void delete(Player player);
|
|
||||||
*
|
|
||||||
* // or
|
|
||||||
* @Query("DELETE FROM players WHERE id = :playerId")
|
|
||||||
* void deleteById(int playerId);
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see Player
|
|
||||||
* @see Dao
|
|
||||||
* @see com.aldo.apps.ochecompanion.database.AppDatabase
|
|
||||||
* @author Oche Companion Development Team
|
|
||||||
* @version 1.0
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
public interface PlayerDao {
|
public interface PlayerDao {
|
||||||
/**
|
/**
|
||||||
* Inserts a new Player entity into the database.
|
* Inserts a new player into the database.
|
||||||
* <p>
|
* Must be called on a background thread.
|
||||||
* 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
|
* @param player The player to insert
|
||||||
* generate a primary key ID for the player if the ID field is configured with
|
|
||||||
* {@code @PrimaryKey(autoGenerate = true)}.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Threading:</strong>
|
|
||||||
* 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).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Transaction Behavior:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Conflict Strategy:</strong>
|
|
||||||
* 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:
|
|
||||||
* <pre>
|
|
||||||
* @Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
* void insert(Player player);
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Auto-Generated ID:</strong>
|
|
||||||
* 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).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Validation:</strong>
|
|
||||||
* Ensure the player object contains all required fields before insertion:
|
|
||||||
* <ul>
|
|
||||||
* <li>Username should not be null or empty</li>
|
|
||||||
* <li>Profile picture URI can be null (default avatar will be used)</li>
|
|
||||||
* <li>Career average should be initialized (default to 0.0 if not set)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param player The Player object to be inserted into the database. Must not be null.
|
|
||||||
* Should contain valid username and optionally profile picture URI.
|
|
||||||
* @throws IllegalStateException if called on the main thread (Room's default behavior)
|
|
||||||
* @throws android.database.sqlite.SQLiteConstraintException if a constraint is violated
|
|
||||||
* @throws android.database.sqlite.SQLiteException if the database operation fails
|
|
||||||
* @see Insert
|
|
||||||
* @see Player
|
|
||||||
*/
|
*/
|
||||||
@Insert
|
@Insert
|
||||||
void insert(final Player player);
|
void insert(final Player player);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates an existing player's information in the database.
|
* Updates an existing player in the database.
|
||||||
* <p>
|
* Player is identified by its primary key ID.
|
||||||
* This method modifies an existing player record in the "players" table. Room
|
* Must be called on a background thread.
|
||||||
* 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.
|
* @param player The player with updated data
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Primary Key Matching:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Threading:</strong>
|
|
||||||
* 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).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Transaction Behavior:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Conflict Strategy:</strong>
|
|
||||||
* The default conflict strategy is {@code OnConflictStrategy.ABORT}. This can be
|
|
||||||
* customized if needed:
|
|
||||||
* <pre>
|
|
||||||
* @Update(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
* void update(Player player);
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Return Value:</strong>
|
|
||||||
* 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).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Common Use Cases:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Updating player username after editing in {@link com.aldo.apps.ochecompanion.AddPlayerActivity}</li>
|
|
||||||
* <li>Changing profile picture after cropping and saving new image</li>
|
|
||||||
* <li>Updating career statistics after completing a match</li>
|
|
||||||
* <li>Modifying any player information through the edit interface</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param player The Player object containing updated values. Must not be null.
|
|
||||||
* The object's ID field must match an existing player in the database.
|
|
||||||
* @throws IllegalStateException if called on the main thread (Room's default behavior)
|
|
||||||
* @throws android.database.sqlite.SQLiteException if the database operation fails
|
|
||||||
* @see Update
|
|
||||||
* @see Player
|
|
||||||
* @see #getPlayerById(int)
|
|
||||||
*/
|
*/
|
||||||
@Update
|
@Update
|
||||||
void update(Player player);
|
void update(final Player player);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a specific player from the database by their unique identifier.
|
* Retrieves a player by their unique ID.
|
||||||
* <p>
|
* Must be called on a background thread.
|
||||||
* 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
|
* @param id The player's ID
|
||||||
* player data to populate the edit form.
|
* @return The player, or null if not found
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>SQL Query:</strong>
|
|
||||||
* Executes: {@code SELECT * FROM players WHERE id = :id LIMIT 1}
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Query Optimization:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Threading:</strong>
|
|
||||||
* 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).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Null Return Value:</strong>
|
|
||||||
* This method returns {@code null} if no player exists with the specified ID.
|
|
||||||
* Callers must always check for null before using the returned value:
|
|
||||||
* <pre>
|
|
||||||
* Player player = playerDao.getPlayerById(playerId);
|
|
||||||
* if (player != null) {
|
|
||||||
* // Use the player
|
|
||||||
* } else {
|
|
||||||
* // Handle player not found
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Common Use Cases:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Loading player data in {@link com.aldo.apps.ochecompanion.AddPlayerActivity}
|
|
||||||
* when editing an existing player</li>
|
|
||||||
* <li>Retrieving player information before updating</li>
|
|
||||||
* <li>Validating that a player exists before performing operations</li>
|
|
||||||
* <li>Displaying detailed player information in a profile view</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Performance:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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
|
|
||||||
*/
|
*/
|
||||||
@Query("SELECT * FROM players WHERE id = :id LIMIT 1")
|
@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.
|
* Retrieves all players ordered alphabetically by username.
|
||||||
* <p>
|
* Must be called on a background thread.
|
||||||
* This method queries the complete player roster from the "players" table and
|
*
|
||||||
* returns them sorted alphabetically (A-Z) by username. This provides a consistent,
|
* @return List of all players sorted A-Z
|
||||||
* user-friendly ordering for displaying the squad in lists and selection interfaces.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>SQL Query:</strong>
|
|
||||||
* Executes: {@code SELECT * FROM players ORDER BY username ASC}
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Sorting:</strong>
|
|
||||||
* Players are ordered by the "username" field in ascending alphabetical order (ASC):
|
|
||||||
* <ul>
|
|
||||||
* <li>Players with names starting with 'A' appear first</li>
|
|
||||||
* <li>Players with names starting with 'Z' appear last</li>
|
|
||||||
* <li>Case-insensitive sorting depends on database collation settings</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Threading:</strong>
|
|
||||||
* 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).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Performance Considerations:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>This query loads ALL players into memory at once</li>
|
|
||||||
* <li>For small to medium squads (10-100 players), this is efficient</li>
|
|
||||||
* <li>For very large datasets, consider pagination or filtering</li>
|
|
||||||
* <li>Consider using LiveData or Flow for automatic UI updates when data changes</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Empty Result:</strong>
|
|
||||||
* If no players exist in the database, this method returns an empty list (not null).
|
|
||||||
* This makes it safe to iterate without null checking:
|
|
||||||
* <pre>
|
|
||||||
* List<Player> players = playerDao.getAllPlayers();
|
|
||||||
* for (Player player : players) {
|
|
||||||
* // Process each player
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Common Use Cases:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Displaying the squad roster in {@link com.aldo.apps.ochecompanion.MainMenuActivity}</li>
|
|
||||||
* <li>Populating player selection lists when creating a new match</li>
|
|
||||||
* <li>Showing all players in management/roster views</li>
|
|
||||||
* <li>Calculating squad-wide statistics</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Alternative Implementations:</strong>
|
|
||||||
* Consider using LiveData for automatic UI updates:
|
|
||||||
* <pre>
|
|
||||||
* @Query("SELECT * FROM players ORDER BY username ASC")
|
|
||||||
* LiveData<List<Player>> getAllPlayersLive();
|
|
||||||
* </pre>
|
|
||||||
* Or Flow for Kotlin coroutines:
|
|
||||||
* <pre>
|
|
||||||
* @Query("SELECT * FROM players ORDER BY username ASC")
|
|
||||||
* Flow<List<Player>> getAllPlayersFlow();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Suggested Improvements:</strong>
|
|
||||||
* Consider adding an index on the username column for faster sorting:
|
|
||||||
* <pre>
|
|
||||||
* @Entity(tableName = "players",
|
|
||||||
* indices = {@Index(value = {"username"}, name = "index_username")})
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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)
|
|
||||||
*/
|
*/
|
||||||
@Query("SELECT * FROM players ORDER BY username ASC")
|
@Query("SELECT * FROM players ORDER BY username ASC")
|
||||||
List<Player> getAllPlayers();
|
List<Player> getAllPlayers();
|
||||||
|
|||||||
@@ -5,800 +5,73 @@ import androidx.room.PrimaryKey;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a completed match or game leg in the Oche Companion application.
|
* Represents a completed darts match in the Oche Companion application.
|
||||||
* <p>
|
* Room entity storing match information including game mode, timestamp, player count,
|
||||||
* This entity class stores comprehensive information about a finished darts match,
|
* and detailed performance data for all participants. Implements Serializable for
|
||||||
* including the game mode played, completion timestamp, participant count, and
|
* passing between Android components.
|
||||||
* detailed performance data for all players involved. The Match entity serves as
|
*
|
||||||
* the primary record for match history and statistics tracking.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Room Database Entity:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Serializable Interface:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Data Structure:</strong>
|
|
||||||
* Match data is stored with the following components:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>ID:</strong> Auto-generated primary key for database uniqueness</li>
|
|
||||||
* <li><strong>Timestamp:</strong> Unix epoch time marking match completion</li>
|
|
||||||
* <li><strong>Game Mode:</strong> The variant of darts played (e.g., "501", "301", "Cricket")</li>
|
|
||||||
* <li><strong>Player Count:</strong> Number of participants (supports 1v1 and group matches)</li>
|
|
||||||
* <li><strong>Participant Data:</strong> JSON-serialized performance metrics for all players</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Participant Data Format:</strong>
|
|
||||||
* The {@code participantData} field contains a JSON array with detailed player performance:
|
|
||||||
* <pre>
|
|
||||||
* [
|
|
||||||
* {
|
|
||||||
* "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
|
|
||||||
* }
|
|
||||||
* ]
|
|
||||||
* </pre>
|
|
||||||
* This structure allows for flexible storage of various statistics while maintaining
|
|
||||||
* database normalization (avoiding separate tables for each match-player relationship).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Game Mode Support:</strong>
|
|
||||||
* The application supports multiple darts game variants:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>501:</strong> Standard countdown game starting at 501 points</li>
|
|
||||||
* <li><strong>301:</strong> Faster countdown game starting at 301 points</li>
|
|
||||||
* <li><strong>Cricket:</strong> Number-based strategy game (15-20 and bulls)</li>
|
|
||||||
* <li><strong>Around the Clock:</strong> Sequential number hitting game</li>
|
|
||||||
* <li>Custom game modes can be added by extending the gameMode field</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Match Types:</strong>
|
|
||||||
* The system supports different match configurations:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>1v1 Matches:</strong> Two-player head-to-head games (playerCount = 2)</li>
|
|
||||||
* <li><strong>Group Matches:</strong> Three or more players (playerCount >= 3)</li>
|
|
||||||
* <li><strong>Solo Practice:</strong> Single player practice sessions (playerCount = 1)</li>
|
|
||||||
* </ul>
|
|
||||||
* The UI adapts based on player count, showing specialized layouts for each match type.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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);
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Database Relationships:</strong>
|
|
||||||
* While this entity doesn't use Room's {@code @Relation} annotations, it maintains
|
|
||||||
* logical relationships through the participant data:
|
|
||||||
* <ul>
|
|
||||||
* <li>Each match references multiple players via IDs in participantData JSON</li>
|
|
||||||
* <li>Player objects are stored separately in the "players" table</li>
|
|
||||||
* <li>The denormalized JSON approach optimizes read performance for match history</li>
|
|
||||||
* <li>Updates to player usernames won't automatically reflect in historical matches</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Performance Considerations:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>JSON parsing adds minimal overhead for typical match sizes (2-8 players)</li>
|
|
||||||
* <li>The timestamp field can be indexed for efficient chronological queries</li>
|
|
||||||
* <li>Consider paginating match history for users with hundreds of matches</li>
|
|
||||||
* <li>The Serializable interface has some overhead; consider Parcelable for better performance</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Data Integrity:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>The auto-generated ID ensures each match is uniquely identifiable</li>
|
|
||||||
* <li>Timestamp should always be positive and in milliseconds (Unix epoch)</li>
|
|
||||||
* <li>Player count should match the number of entries in participantData JSON</li>
|
|
||||||
* <li>Game mode string should be validated against supported game types</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Future Enhancements:</strong>
|
|
||||||
* Consider adding these fields for expanded functionality:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>duration:</strong> Match duration in seconds for time tracking</li>
|
|
||||||
* <li><strong>location:</strong> Venue or location where match was played</li>
|
|
||||||
* <li><strong>notes:</strong> User-added comments or observations</li>
|
|
||||||
* <li><strong>isRanked:</strong> Boolean flag for competitive vs casual matches</li>
|
|
||||||
* <li><strong>tournamentId:</strong> Reference to tournament entity for organized play</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see com.aldo.apps.ochecompanion.database.dao.MatchDao
|
* @see com.aldo.apps.ochecompanion.database.dao.MatchDao
|
||||||
* @see com.aldo.apps.ochecompanion.database.objects.Player
|
* @see com.aldo.apps.ochecompanion.database.objects.Player
|
||||||
* @see com.aldo.apps.ochecompanion.ui.view.MatchRecapView
|
|
||||||
* @see Entity
|
* @see Entity
|
||||||
* @see Serializable
|
* @see Serializable
|
||||||
* @author Oche Companion Development Team
|
|
||||||
* @version 1.0
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
*/
|
||||||
@Entity(tableName = "matches")
|
@Entity(tableName = "matches")
|
||||||
public class Match implements Serializable {
|
public class Match implements Serializable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The unique primary key identifier for this match in the database.
|
* Auto-generated unique primary key for this match.
|
||||||
* <p>
|
* Value is 0 before insertion, then assigned by Room using SQLite AUTOINCREMENT.
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Auto-Generation:</strong>
|
|
||||||
* The {@code @PrimaryKey(autoGenerate = true)} annotation tells Room to:
|
|
||||||
* <ul>
|
|
||||||
* <li>Automatically assign a unique ID when inserting a new match</li>
|
|
||||||
* <li>Increment the ID value for each subsequent insert</li>
|
|
||||||
* <li>Update the Match object's ID field after successful insertion</li>
|
|
||||||
* <li>Use this ID for update and delete operations</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Initial Value:</strong>
|
|
||||||
* 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).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage:</strong>
|
|
||||||
* <pre>
|
|
||||||
* 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);
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Uniqueness Guarantee:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see PrimaryKey
|
* @see PrimaryKey
|
||||||
*/
|
*/
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
public int id;
|
public int mId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unix epoch timestamp indicating when this match was completed.
|
* Unix epoch timestamp (milliseconds) when match was completed.
|
||||||
* <p>
|
* Used for chronological sorting and display. Obtained via System.currentTimeMillis().
|
||||||
* 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:
|
|
||||||
* <ul>
|
|
||||||
* <li>Sorting matches chronologically in match history views</li>
|
|
||||||
* <li>Displaying relative time ("2 hours ago", "Yesterday")</li>
|
|
||||||
* <li>Filtering matches by date range</li>
|
|
||||||
* <li>Calculating statistics over time periods</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Format:</strong>
|
|
||||||
* The timestamp is stored as a {@code long} value representing milliseconds.
|
|
||||||
* This is the standard Java/Android time format obtained via:
|
|
||||||
* <pre>
|
|
||||||
* long timestamp = System.currentTimeMillis();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Example Values:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>1737158400000L = January 17, 2025, 00:00:00 UTC</li>
|
|
||||||
* <li>1704067200000L = January 1, 2024, 00:00:00 UTC</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Conversion Examples:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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);
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Sorting:</strong>
|
|
||||||
* Matches can be ordered by timestamp to show most recent first:
|
|
||||||
* <pre>
|
|
||||||
* @Query("SELECT * FROM matches ORDER BY timestamp DESC LIMIT 10")
|
|
||||||
* List<Match> getRecentMatches();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Validation:</strong>
|
|
||||||
* The timestamp should always be:
|
|
||||||
* <ul>
|
|
||||||
* <li>Positive (non-zero)</li>
|
|
||||||
* <li>Not in the future (unless testing)</li>
|
|
||||||
* <li>Reasonable (not before app creation date)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Timezone Considerations:</strong>
|
|
||||||
* While the timestamp is stored in UTC (Unix epoch), display formatting should
|
|
||||||
* consider the user's local timezone for proper date/time presentation.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see 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.
|
* Identifier for the darts game variant played (e.g., "501", "301", "Cricket").
|
||||||
* <p>
|
* Determines scoring rules and UI display for 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Supported Game Modes:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>"501":</strong> The classic countdown game starting at 501 points.
|
|
||||||
* Players subtract their scores and must finish exactly on zero with a double.</li>
|
|
||||||
* <li><strong>"301":</strong> A faster variant starting at 301 points, following
|
|
||||||
* the same rules as 501 but requiring quicker gameplay.</li>
|
|
||||||
* <li><strong>"Cricket":</strong> Strategic game focusing on numbers 15-20 and
|
|
||||||
* the bullseye. Players must "close" numbers by hitting them three times.</li>
|
|
||||||
* <li><strong>"Around the Clock":</strong> Sequential game where players must
|
|
||||||
* hit numbers 1-20 in order, then finish with a bullseye.</li>
|
|
||||||
* <li><strong>"Killer":</strong> Multiplayer elimination game where each player
|
|
||||||
* has a designated number and tries to eliminate opponents.</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>String Format:</strong>
|
|
||||||
* Game mode strings should be:
|
|
||||||
* <ul>
|
|
||||||
* <li>Non-null and non-empty</li>
|
|
||||||
* <li>Consistent in naming (avoid "501", "Five-Oh-One", "501 Game" variations)</li>
|
|
||||||
* <li>Preferably using predefined constants to avoid typos</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Recommended Usage:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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
|
|
||||||
* );
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>UI Adaptation:</strong>
|
|
||||||
* The game mode affects how matches are displayed:
|
|
||||||
* <pre>
|
|
||||||
* 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;
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Filtering and Statistics:</strong>
|
|
||||||
* Game mode enables targeted queries and statistics:
|
|
||||||
* <pre>
|
|
||||||
* // 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Extensibility:</strong>
|
|
||||||
* New game modes can be added without schema changes:
|
|
||||||
* <ul>
|
|
||||||
* <li>Simply use a new string identifier</li>
|
|
||||||
* <li>Implement game-specific logic in the game engine</li>
|
|
||||||
* <li>Update UI to handle the new mode</li>
|
|
||||||
* <li>Ensure participant data JSON includes mode-specific metrics</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Validation:</strong>
|
|
||||||
* Consider validating game mode before inserting matches:
|
|
||||||
* <pre>
|
|
||||||
* 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);
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see com.aldo.apps.ochecompanion.ui.view.MatchRecapView#setMatch(Match)
|
|
||||||
*/
|
*/
|
||||||
public String gameMode;
|
public String mGameMode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The total number of players who participated in this match.
|
* Total number of players who participated in this match.
|
||||||
* <p>
|
* Determines match type (1=solo, 2=1v1, 3+=group) and affects UI display.
|
||||||
* This field indicates how many individuals competed in the match, which
|
* Must match the number of entries in participantData JSON.
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Valid Range:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>1:</strong> Solo practice session (single player)</li>
|
|
||||||
* <li><strong>2:</strong> Head-to-head match (1v1 duel)</li>
|
|
||||||
* <li><strong>3+:</strong> Group match or multiplayer game</li>
|
|
||||||
* </ul>
|
|
||||||
* Typically ranges from 1 to 8 players, though the system can support more.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Match Type Determination:</strong>
|
|
||||||
* The player count affects UI rendering and game logic:
|
|
||||||
* <pre>
|
|
||||||
* 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
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Data Consistency:</strong>
|
|
||||||
* The player count should always match the participant data:
|
|
||||||
* <pre>
|
|
||||||
* // 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");
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Performance Implications:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Higher player counts require more complex UI layouts</li>
|
|
||||||
* <li>Sorting and ranking algorithms scale with player count</li>
|
|
||||||
* <li>JSON parsing time increases with more participants</li>
|
|
||||||
* <li>Consider pagination for matches with many players</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage in Queries:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Statistical Analysis:</strong>
|
|
||||||
* Player count enables performance tracking by match type:
|
|
||||||
* <pre>
|
|
||||||
* // Calculate win rate in 1v1 matches
|
|
||||||
* double winRate1v1 = calculateWinRate(playerId, 2);
|
|
||||||
*
|
|
||||||
* // Calculate average placement in group matches
|
|
||||||
* double avgPlacement = calculateAveragePlacement(playerId, 3);
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Validation:</strong>
|
|
||||||
* Always validate player count before creating a match:
|
|
||||||
* <pre>
|
|
||||||
* if (playerCount < 1) {
|
|
||||||
* throw new IllegalArgumentException("playerCount must be at least 1");
|
|
||||||
* }
|
|
||||||
* if (playerCount > MAX_PLAYERS) {
|
|
||||||
* throw new IllegalArgumentException("Too many players: " + playerCount);
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see com.aldo.apps.ochecompanion.ui.view.MatchRecapView#setup1v1State()
|
|
||||||
* @see com.aldo.apps.ochecompanion.ui.view.MatchRecapView#setupGroupState()
|
|
||||||
*/
|
*/
|
||||||
public int playerCount;
|
public int mPlayerCount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialized JSON string containing detailed performance data for all match participants.
|
* JSON string containing detailed performance data for all match participants.
|
||||||
* <p>
|
* Stores player identities, rankings, scores, and statistics. Array length must
|
||||||
* This field stores comprehensive information about each player's performance in
|
* match playerCount. Uses JSON for flexible storage without additional tables.
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>JSON Structure:</strong>
|
|
||||||
* The participant data is stored as a JSON array of player objects:
|
|
||||||
* <pre>
|
|
||||||
* [
|
|
||||||
* {
|
|
||||||
* "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"
|
|
||||||
* }
|
|
||||||
* ]
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Required Fields:</strong>
|
|
||||||
* Each participant object must contain:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>id:</strong> Player database ID for linking to Player entity</li>
|
|
||||||
* <li><strong>username:</strong> Display name at time of match (snapshot)</li>
|
|
||||||
* <li><strong>rank:</strong> Final placement (1 = winner, 2 = second, etc.)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Optional Fields:</strong>
|
|
||||||
* Additional metrics that may be included:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>score:</strong> Final score or remaining points</li>
|
|
||||||
* <li><strong>average:</strong> Three-dart average throughout the match</li>
|
|
||||||
* <li><strong>highestCheckout:</strong> Largest checkout/finish</li>
|
|
||||||
* <li><strong>dartsThrown:</strong> Total number of darts thrown</li>
|
|
||||||
* <li><strong>profilePictureUri:</strong> Profile image path (snapshot)</li>
|
|
||||||
* <li><strong>180s:</strong> Number of maximum scores (180) hit</li>
|
|
||||||
* <li><strong>checkoutPercentage:</strong> Success rate on finish attempts</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Parsing Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* 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);
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Building Participant Data:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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);
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Why JSON Instead of Relations:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>Performance:</strong> Single query retrieves complete match data</li>
|
|
||||||
* <li><strong>Historical Accuracy:</strong> Captures player data as it was at match time</li>
|
|
||||||
* <li><strong>Flexibility:</strong> Can store game-specific metrics without schema changes</li>
|
|
||||||
* <li><strong>Simplicity:</strong> Avoids complex join queries and relationship management</li>
|
|
||||||
* <li><strong>Immutability:</strong> Match data remains unchanged if player profiles are updated</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Data Integrity:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Array length should match {@link #playerCount}</li>
|
|
||||||
* <li>Ranks should be sequential (1, 2, 3, ...) without gaps</li>
|
|
||||||
* <li>Player IDs should reference valid players (though not enforced by foreign key)</li>
|
|
||||||
* <li>JSON must be well-formed and parseable</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Snapshot Advantage:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Sorting Participants:</strong>
|
|
||||||
* Usually pre-sorted by rank before serialization, but can be sorted after parsing:
|
|
||||||
* <pre>
|
|
||||||
* // Sort by rank after parsing
|
|
||||||
* Collections.sort(playerList, (p1, p2) ->
|
|
||||||
* Integer.compare(p1.rank, p2.rank));
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Null Handling:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see org.json.JSONArray
|
* @see org.json.JSONArray
|
||||||
* @see org.json.JSONObject
|
* @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.
|
* Constructs a new Match entity ready for database insertion.
|
||||||
* <p>
|
* The match ID will be auto-generated by Room upon insertion.
|
||||||
* 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
|
* @param timestamp Unix epoch timestamp in milliseconds when match completed
|
||||||
* no need to set it manually. All parameters are required to create a valid match.
|
* @param gameMode Identifier for the darts game variant (e.g., "501", "Cricket")
|
||||||
* </p>
|
* @param playerCount Number of players who participated (must be at least 1)
|
||||||
* <p>
|
* @param participantData JSON string containing player performance data
|
||||||
* <strong>Constructor Parameters:</strong>
|
|
||||||
* Each parameter serves a specific purpose in defining the match:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>timestamp:</strong> Records when the match was completed for chronological ordering</li>
|
|
||||||
* <li><strong>gameMode:</strong> Identifies which darts variant was played</li>
|
|
||||||
* <li><strong>playerCount:</strong> Specifies how many players participated</li>
|
|
||||||
* <li><strong>participantData:</strong> Contains detailed performance data for all players</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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();
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Parameter Validation:</strong>
|
|
||||||
* While the constructor doesn't enforce validation, consider checking parameters
|
|
||||||
* before construction:
|
|
||||||
* <pre>
|
|
||||||
* // 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);
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Field Initialization:</strong>
|
|
||||||
* The constructor initializes all fields except {@code id}:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>id:</strong> Remains 0 (default) until Room assigns auto-generated value</li>
|
|
||||||
* <li><strong>timestamp:</strong> Set to the provided value (milliseconds since epoch)</li>
|
|
||||||
* <li><strong>gameMode:</strong> Set to the provided game identifier string</li>
|
|
||||||
* <li><strong>playerCount:</strong> Set to the provided player count</li>
|
|
||||||
* <li><strong>participantData:</strong> Set to the provided JSON string</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Database Insertion:</strong>
|
|
||||||
* After construction, insert the match using the DAO:
|
|
||||||
* <pre>
|
|
||||||
* // 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();
|
|
||||||
* });
|
|
||||||
* });
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Immutability Consideration:</strong>
|
|
||||||
* Once created and inserted, match data should generally be treated as immutable
|
|
||||||
* (historical record). If corrections are needed, consider whether to:
|
|
||||||
* <ul>
|
|
||||||
* <li>Update the existing match (rare, only for data corrections)</li>
|
|
||||||
* <li>Delete and recreate (for significant errors)</li>
|
|
||||||
* <li>Leave as-is and add a note field (preserves history)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Thread Safety:</strong>
|
|
||||||
* The constructor itself is thread-safe, but database insertion must occur on
|
|
||||||
* a background thread due to Room's main thread restrictions.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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
|
|
||||||
* @see com.aldo.apps.ochecompanion.database.dao.MatchDao#insert(Match)
|
* @see com.aldo.apps.ochecompanion.database.dao.MatchDao#insert(Match)
|
||||||
*/
|
*/
|
||||||
public Match(final long timestamp, final String gameMode, final int playerCount, final String participantData) {
|
public Match(final long timestamp, final String gameMode, final int playerCount, final String participantData) {
|
||||||
// Initialize all fields with provided values
|
this.mTimestamp = timestamp;
|
||||||
// The id field remains at default value (0) and will be auto-generated by Room upon insertion
|
this.mGameMode = gameMode;
|
||||||
this.timestamp = timestamp;
|
this.mPlayerCount = playerCount;
|
||||||
this.gameMode = gameMode;
|
this.mParticipantData = participantData;
|
||||||
this.playerCount = playerCount;
|
|
||||||
this.participantData = participantData;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,125 +10,21 @@ import com.aldo.apps.ochecompanion.database.objects.Player;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Model class representing a darts match with multiple participants.
|
* Model class representing a darts match with multiple participants.
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Key Features:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Support for matches with any number of players (1v1, group matches, etc.)</li>
|
|
||||||
* <li>Position-based player access for ordered operations</li>
|
|
||||||
* <li>Convenient retrieval of player names and career averages</li>
|
|
||||||
* <li>Flexible construction with varargs or default empty initialization</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Match Types:</strong>
|
|
||||||
* This class supports different match configurations:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>1v1 Match:</strong> Exactly 2 players for head-to-head competition</li>
|
|
||||||
* <li><strong>Group Match:</strong> 3 or more players for multi-player games</li>
|
|
||||||
* <li><strong>Solo Practice:</strong> Single player for practice sessions (if supported)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Player Ordering:</strong>
|
|
||||||
* Players are stored in the order they are added. This ordering is significant for:
|
|
||||||
* <ul>
|
|
||||||
* <li>Displaying players in consistent positions (left/right, top/bottom)</li>
|
|
||||||
* <li>Turn order during gameplay</li>
|
|
||||||
* <li>Result display and leaderboards</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Examples:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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)
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Design Notes:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>The class is mutable in that it stores player references, but doesn't provide
|
|
||||||
* methods to add/remove players after construction</li>
|
|
||||||
* <li>Player data (names, averages) is accessed directly from Player objects</li>
|
|
||||||
* <li>Currently focused on displaying match information rather than tracking live gameplay</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see Player
|
|
||||||
* @see com.aldo.apps.ochecompanion.ui.MatchRecapView
|
|
||||||
* @author Oche Companion Development Team
|
|
||||||
* @version 1.0
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
*/
|
||||||
public class Match {
|
public class Match {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag for logging and debugging purposes.
|
* Tag for logging.
|
||||||
* Used to identify log messages originating from the Match class.
|
|
||||||
*/
|
*/
|
||||||
private static final String TAG = "Match";
|
private static final String TAG = "Match";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal list of players participating in this match.
|
* List of players participating in this match.
|
||||||
* <p>
|
|
||||||
* 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)}.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* The list is initialized as an ArrayList to provide efficient random access
|
|
||||||
* by index, which is the primary access pattern for this class.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Immutability Note:</strong>
|
|
||||||
* While the list reference is final, the list contents are mutable. However,
|
|
||||||
* no public methods currently allow modification after construction.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #Match(Player...)
|
|
||||||
* @see #getAllPlayers()
|
|
||||||
* @see #getParticipantCount()
|
|
||||||
*/
|
*/
|
||||||
private final List<Player> mPlayers;
|
private final List<Player> mPlayers;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an empty Match with no participants.
|
* Constructs an empty Match with no participants.
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Scenarios:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Creating a match template before players are selected</li>
|
|
||||||
* <li>Initialization in builders or factory methods</li>
|
|
||||||
* <li>Default construction in frameworks requiring no-arg constructors</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Note:</strong>
|
|
||||||
* Currently, there are no public methods to add players after construction.
|
|
||||||
* Consider using {@link #Match(Player...)} if players are known at creation time.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* A debug log message is generated when an empty match is created.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #Match(Player...)
|
|
||||||
*/
|
*/
|
||||||
public Match() {
|
public Match() {
|
||||||
// Initialize empty player list
|
// Initialize empty player list
|
||||||
@@ -139,57 +35,8 @@ public class Match {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a Match with the specified players.
|
* Constructs a Match with the specified players.
|
||||||
* <p>
|
*
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Varargs Convenience:</strong>
|
|
||||||
* The varargs parameter allows flexible calling patterns:
|
|
||||||
* <pre>
|
|
||||||
* // 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);
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Player Ordering:</strong>
|
|
||||||
* Players are stored in the exact order they are passed. For example:
|
|
||||||
* <ul>
|
|
||||||
* <li>First player: position 0</li>
|
|
||||||
* <li>Second player: position 1</li>
|
|
||||||
* <li>And so on...</li>
|
|
||||||
* </ul>
|
|
||||||
* This ordering is important for methods like {@link #getPlayerNameByPosition(int)}.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Logging:</strong>
|
|
||||||
* Each player addition is logged at debug level for troubleshooting and verification.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Null Safety:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param players Variable number of Player objects to participate in the match.
|
* @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) {
|
public Match(final Player... players) {
|
||||||
// Initialize empty player list
|
// Initialize empty player list
|
||||||
@@ -206,37 +53,8 @@ public class Match {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the number of players participating in this match.
|
* Returns the number of players participating in this match.
|
||||||
* <p>
|
*
|
||||||
* This method provides the count of players currently registered for the match.
|
* @return The number of players in this match.
|
||||||
* The count corresponds to the number of players added during match construction.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Examples:</strong>
|
|
||||||
* <pre>
|
|
||||||
* 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
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Use Cases:</strong>
|
|
||||||
* This method is commonly used to:
|
|
||||||
* <ul>
|
|
||||||
* <li>Determine which UI layout to display (1v1 vs group match views)</li>
|
|
||||||
* <li>Validate match configuration before starting gameplay</li>
|
|
||||||
* <li>Loop through players without accessing the list directly</li>
|
|
||||||
* <li>Check if the match has any participants</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return The number of players in this match. Returns 0 if the match is empty.
|
|
||||||
* @see #getAllPlayers()
|
|
||||||
* @see com.aldo.apps.ochecompanion.ui.MatchRecapView#setMatch(Match)
|
|
||||||
*/
|
*/
|
||||||
public int getParticipantCount() {
|
public int getParticipantCount() {
|
||||||
return mPlayers.size();
|
return mPlayers.size();
|
||||||
@@ -244,50 +62,9 @@ public class Match {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the username of the player at the specified position.
|
* Retrieves the username of the player at the specified position.
|
||||||
* <p>
|
*
|
||||||
* 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).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Position Indexing:</strong>
|
|
||||||
* Positions are zero-based indices:
|
|
||||||
* <ul>
|
|
||||||
* <li>Position 0: First player added to the match</li>
|
|
||||||
* <li>Position 1: Second player added to the match</li>
|
|
||||||
* <li>Position n: (n+1)th player added to the match</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Bounds Checking:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Note on Bounds Check:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* 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"
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param position The zero-based index of the player in the match.
|
* @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.
|
||||||
* @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()
|
|
||||||
*/
|
*/
|
||||||
public String getPlayerNameByPosition(final int position) {
|
public String getPlayerNameByPosition(final int position) {
|
||||||
// Validate position is within bounds
|
// Validate position is within bounds
|
||||||
@@ -302,56 +79,9 @@ public class Match {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the career average of the player at the specified position.
|
* Retrieves the career average of the player at the specified position.
|
||||||
* <p>
|
*
|
||||||
* This method provides position-based access to player career statistics, useful for
|
|
||||||
* displaying performance metrics in match summaries and leaderboards.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Position Indexing:</strong>
|
|
||||||
* Positions are zero-based indices:
|
|
||||||
* <ul>
|
|
||||||
* <li>Position 0: First player added to the match</li>
|
|
||||||
* <li>Position 1: Second player added to the match</li>
|
|
||||||
* <li>Position n: (n+1)th player added to the match</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Bounds Checking:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Note on Bounds Check:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Career Average:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* 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
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param position The zero-based index of the player in the match.
|
* @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.
|
||||||
* @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()
|
|
||||||
*/
|
*/
|
||||||
public double getPlayerAverageByPosition(final int position) {
|
public double getPlayerAverageByPosition(final int position) {
|
||||||
// Validate position is within bounds
|
// 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.
|
* Returns the list of all players in this match.
|
||||||
* <p>
|
*
|
||||||
* This method provides access to the complete player list, useful for operations
|
* @return The list of all Player objects in this match.
|
||||||
* that need to process all players (e.g., sorting, filtering, bulk display).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>List Contents:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Mutability Warning:</strong>
|
|
||||||
* 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:
|
|
||||||
* <pre>
|
|
||||||
* List<Player> players = match.getAllPlayers();
|
|
||||||
* players.clear(); // WARNING: This clears the match's player list!
|
|
||||||
* </pre>
|
|
||||||
* If you need to modify the list without affecting the match, create a copy:
|
|
||||||
* <pre>
|
|
||||||
* List<Player> playersCopy = new ArrayList<>(match.getAllPlayers());
|
|
||||||
* playersCopy.clear(); // Safe: Only affects the copy
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Common Use Cases:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Populating adapters for RecyclerViews (e.g., {@link com.aldo.apps.ochecompanion.ui.adapter.MainMenuGroupMatchAdapter})</li>
|
|
||||||
* <li>Sorting players by score for leaderboard display</li>
|
|
||||||
* <li>Iterating through all players for statistics calculation</li>
|
|
||||||
* <li>Filtering players based on specific criteria</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Performance:</strong>
|
|
||||||
* This is an O(1) operation as it returns a reference, not a copy.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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)
|
|
||||||
*/
|
*/
|
||||||
public List<Player> getAllPlayers() {
|
public List<Player> getAllPlayers() {
|
||||||
return mPlayers;
|
return mPlayers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a string representation of this Match for debugging and logging.
|
* Returns a string representation of this Match.
|
||||||
* <p>
|
*
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Output Format:</strong>
|
|
||||||
* The generated string follows this pattern:
|
|
||||||
* <pre>
|
|
||||||
* Match {[Player1][Player2][Player3]]
|
|
||||||
* </pre>
|
|
||||||
* Each player is represented by its own {@link Player#toString()} output,
|
|
||||||
* wrapped in square brackets.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Example Output:</strong>
|
|
||||||
* <pre>
|
|
||||||
* Match {[Player{name='Alice', avg=45.5}][Player{name='Bob', avg=52.3}]]
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Note on Formatting:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Performance:</strong>
|
|
||||||
* Uses {@link StringBuilder} for efficient string concatenation, which is important
|
|
||||||
* when the match contains many players.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage:</strong>
|
|
||||||
* This method is automatically called when:
|
|
||||||
* <ul>
|
|
||||||
* <li>Logging a Match object: {@code Log.d(TAG, "Match: " + match)}</li>
|
|
||||||
* <li>Concatenating with strings: {@code "Current match: " + match}</li>
|
|
||||||
* <li>Debugging in IDE debugger (some IDEs display toString() output)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return A string representation of this match including all participating players.
|
* @return A string representation of this match including all participating players.
|
||||||
* Never null.
|
|
||||||
* @see Player#toString()
|
|
||||||
* @see StringBuilder
|
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -11,346 +11,70 @@ import android.view.View;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom view that provides a visual cropping guide overlay for image selection.
|
* Visual cropping guide overlay with semi-transparent mask and center crop window.
|
||||||
* <p>
|
* Uses path winding technique to create transparent square cutout (80% of width).
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Visual Design:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Dark semi-transparent mask (85% opacity Midnight Black) covers the entire view</li>
|
|
||||||
* <li>Transparent square cutout in the center shows the crop area</li>
|
|
||||||
* <li>Crop box is 80% of the view's width, maintaining a square aspect ratio</li>
|
|
||||||
* <li>Automatically centers the crop box within the view</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Technical Implementation:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see com.aldo.apps.ochecompanion.AddPlayerActivity
|
|
||||||
* @see Path
|
|
||||||
* @see RectF
|
|
||||||
* @author Oche Companion Development Team
|
|
||||||
* @version 1.0
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
*/
|
||||||
public class CropOverlayView extends View {
|
public class CropOverlayView extends View {
|
||||||
|
|
||||||
/**
|
/** Paint for semi-transparent dark overlay (85% opacity). */
|
||||||
* Paint object for rendering the semi-transparent dark overlay mask.
|
|
||||||
* <p>
|
|
||||||
* Configured with:
|
|
||||||
* <ul>
|
|
||||||
* <li>Anti-aliasing enabled for smooth edges</li>
|
|
||||||
* <li>Color: Midnight Black (#0A0A0A) at 85% opacity (#D90A0A0A)</li>
|
|
||||||
* <li>Style: FILL to cover the entire path area</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* The high opacity (85%) provides good contrast while still allowing the
|
|
||||||
* underlying image to be visible enough for positioning.
|
|
||||||
*/
|
|
||||||
private final Paint mMaskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
private final Paint mMaskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
|
||||||
/**
|
/** Rectangle defining transparent crop area boundaries (80% width, centered). */
|
||||||
* Rectangle defining the boundaries of the transparent crop area in screen coordinates.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* The rectangle dimensions are:
|
|
||||||
* <ul>
|
|
||||||
* <li>Width: 80% of the view's width</li>
|
|
||||||
* <li>Height: Equal to width (square aspect ratio)</li>
|
|
||||||
* <li>Position: Centered horizontally and vertically</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #getCropRect()
|
|
||||||
* @see #onLayout(boolean, int, int, int, int)
|
|
||||||
*/
|
|
||||||
private final RectF mCropRect = new RectF();
|
private final RectF mCropRect = new RectF();
|
||||||
|
|
||||||
/**
|
/** Path with CW outer rect and CCW inner rect creating transparent hole. */
|
||||||
* Path object used to create the overlay mask with a transparent center hole.
|
|
||||||
* <p>
|
|
||||||
* This path consists of two rectangles:
|
|
||||||
* <ol>
|
|
||||||
* <li><strong>Outer Rectangle (Clockwise):</strong> Covers the entire view area</li>
|
|
||||||
* <li><strong>Inner Rectangle (Counter-Clockwise):</strong> Defines the crop area</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* 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).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* The path is recalculated whenever the view's layout changes to ensure proper
|
|
||||||
* sizing and positioning.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see Path.Direction
|
|
||||||
* @see #onLayout(boolean, int, int, int, int)
|
|
||||||
*/
|
|
||||||
private final Path mPath = new Path();
|
private final Path mPath = new Path();
|
||||||
|
|
||||||
/**
|
/** Calculated side length of square crop box (80% of width). */
|
||||||
* The calculated side length of the square crop box in pixels.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* The 80% size ensures adequate padding around the crop area while maximizing
|
|
||||||
* the useful cropping space.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
private float mBoxSize;
|
private float mBoxSize;
|
||||||
|
|
||||||
/**
|
/** Constructor for programmatic instantiation. */
|
||||||
* Constructor for programmatic instantiation of the CropOverlayView.
|
public CropOverlayView(final Context context) {
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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) {
|
|
||||||
super(context);
|
super(context);
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Constructor for XML inflation. */
|
||||||
* Constructor for XML inflation of the CropOverlayView.
|
public CropOverlayView(final Context context, @Nullable final AttributeSet attrs) {
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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) {
|
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Constructor for XML inflation with style. */
|
||||||
* Constructor for XML inflation of the CropOverlayView with a specific style.
|
public CropOverlayView(final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Initializes paint with Midnight Black at 85% opacity. */
|
||||||
* Initializes the Paint object used for rendering the overlay mask.
|
|
||||||
* <p>
|
|
||||||
* This method configures the visual properties of the semi-transparent overlay
|
|
||||||
* that will darken the non-crop area of the image. The configuration includes:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>Color:</strong> Midnight Black (#0A0A0A) with 85% opacity</li>
|
|
||||||
* <li><strong>Style:</strong> FILL to cover the entire masked area</li>
|
|
||||||
* <li><strong>Anti-aliasing:</strong> Already enabled via constructor flag</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Color Choice Rationale:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* This method is called by all constructors to ensure consistent initialization
|
|
||||||
* regardless of how the view is instantiated.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see Paint.Style#FILL
|
|
||||||
*/
|
|
||||||
private void init() {
|
private void init() {
|
||||||
// Set darkened background color: Midnight Black (#0A0A0A) at 85% opacity
|
|
||||||
// Alpha value 0xD9 = 217/255 ≈ 85%
|
|
||||||
mMaskPaint.setColor(Color.parseColor("#D90A0A0A"));
|
mMaskPaint.setColor(Color.parseColor("#D90A0A0A"));
|
||||||
|
|
||||||
// Use FILL style to cover the entire path area (except the hole)
|
|
||||||
mMaskPaint.setStyle(Paint.Style.FILL);
|
mMaskPaint.setStyle(Paint.Style.FILL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Recalculates crop area and path when view size changes. */
|
||||||
* Called when the view's size or position changes to recalculate the crop area.
|
|
||||||
* <p>
|
|
||||||
* This method is responsible for:
|
|
||||||
* <ol>
|
|
||||||
* <li>Calculating the size of the square crop box (80% of view width)</li>
|
|
||||||
* <li>Centering the crop box both horizontally and vertically</li>
|
|
||||||
* <li>Updating the crop rectangle coordinates</li>
|
|
||||||
* <li>Rebuilding the path used for rendering the overlay mask</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Crop Box Sizing:</strong>
|
|
||||||
* The crop box is sized at 80% of the view's width to provide:
|
|
||||||
* <ul>
|
|
||||||
* <li>Adequate padding (10% on each side) for visual clarity</li>
|
|
||||||
* <li>Sufficient space for users to see what will be cropped out</li>
|
|
||||||
* <li>Maximum useful cropping area without overwhelming the interface</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Path Construction:</strong>
|
|
||||||
* The path is built with two rectangles using opposite winding directions:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>Outer (CW):</strong> Full view bounds - creates the mask base</li>
|
|
||||||
* <li><strong>Inner (CCW):</strong> Crop area - subtracts from the mask</li>
|
|
||||||
* </ul>
|
|
||||||
* This winding technique creates the transparent "hole" effect.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param changed True if this view's size or position has changed since the last layout.
|
|
||||||
* @param left The left position, relative to the parent.
|
|
||||||
* @param top The top position, relative to the parent.
|
|
||||||
* @param right The right position, relative to the parent.
|
|
||||||
* @param bottom The bottom position, relative to the parent.
|
|
||||||
* @see Path.Direction
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
protected void onLayout(final boolean changed, final int left, final int top, final int right, final int bottom) {
|
||||||
super.onLayout(changed, left, top, right, bottom);
|
super.onLayout(changed, left, top, right, bottom);
|
||||||
|
|
||||||
// Calculate the crop box size as 80% of the view's width
|
|
||||||
mBoxSize = getWidth() * 0.8f;
|
mBoxSize = getWidth() * 0.8f;
|
||||||
|
final float l = (getWidth() - mBoxSize) / 2;
|
||||||
// Calculate the left position to center the box horizontally
|
final float t = (getHeight() - mBoxSize) / 2;
|
||||||
float l = (getWidth() - mBoxSize) / 2;
|
|
||||||
|
|
||||||
// Calculate the top position to center the box vertically
|
|
||||||
float t = (getHeight() - mBoxSize) / 2;
|
|
||||||
|
|
||||||
// Set the crop rectangle coordinates
|
|
||||||
mCropRect.set(l, t, l + mBoxSize, t + mBoxSize);
|
mCropRect.set(l, t, l + mBoxSize, t + mBoxSize);
|
||||||
|
|
||||||
// Pre-calculate the path for the mask with a transparent center hole
|
mPath.reset();
|
||||||
mPath.reset(); // Clear any previous path data
|
|
||||||
|
|
||||||
// Add outer rectangle covering the entire view (clockwise)
|
|
||||||
mPath.addRect(0, 0, getWidth(), getHeight(), Path.Direction.CW);
|
mPath.addRect(0, 0, getWidth(), getHeight(), Path.Direction.CW);
|
||||||
|
|
||||||
// Add inner rectangle for the crop area (counter-clockwise)
|
|
||||||
// The opposite direction creates a "hole" in the mask
|
|
||||||
mPath.addRect(mCropRect, Path.Direction.CCW);
|
mPath.addRect(mCropRect, Path.Direction.CCW);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Renders the overlay mask with transparent center cutout. */
|
||||||
* Renders the semi-transparent overlay mask onto the canvas.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Rendering Process:</strong>
|
|
||||||
* The single {@link Canvas#drawPath(Path, Paint)} call efficiently renders both:
|
|
||||||
* <ul>
|
|
||||||
* <li>The dark semi-transparent overlay covering the entire view</li>
|
|
||||||
* <li>The transparent crop area in the center (created by the CCW inner rectangle)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Performance:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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)
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDraw(Canvas canvas) {
|
protected void onDraw(final Canvas canvas) {
|
||||||
super.onDraw(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);
|
canvas.drawPath(mPath, mMaskPaint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns crop box coordinates in screen space. */
|
||||||
* Provides the coordinates of the crop box for pixel-level cropping calculations.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Coordinate System:</strong>
|
|
||||||
* The returned RectF contains screen coordinates relative to this view:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>left:</strong> X-coordinate of the left edge of the crop box</li>
|
|
||||||
* <li><strong>top:</strong> Y-coordinate of the top edge of the crop box</li>
|
|
||||||
* <li><strong>right:</strong> X-coordinate of the right edge of the crop box</li>
|
|
||||||
* <li><strong>bottom:</strong> Y-coordinate of the bottom edge of the crop box</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage:</strong>
|
|
||||||
* The parent activity must transform these screen coordinates to bitmap pixel coordinates
|
|
||||||
* by accounting for:
|
|
||||||
* <ul>
|
|
||||||
* <li>ImageView fit-center scaling</li>
|
|
||||||
* <li>User's manual pan (translation) gestures</li>
|
|
||||||
* <li>User's pinch-to-zoom (scale) gestures</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return A RectF representing the transparent square crop area in screen coordinates.
|
|
||||||
* The rectangle defines a square centered in the view with dimensions equal to
|
|
||||||
* 80% of the view's width.
|
|
||||||
* @see com.aldo.apps.ochecompanion.AddPlayerActivity#performCrop()
|
|
||||||
* @see RectF
|
|
||||||
*/
|
|
||||||
public RectF getCropRect() {
|
public RectF getCropRect() {
|
||||||
return mCropRect;
|
return mCropRect;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,411 +14,105 @@ import com.aldo.apps.ochecompanion.models.Match;
|
|||||||
import com.aldo.apps.ochecompanion.ui.adapter.MainMenuGroupMatchAdapter;
|
import com.aldo.apps.ochecompanion.ui.adapter.MainMenuGroupMatchAdapter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom composite view that displays a summary of the most recently played match.
|
* Displays summary of most recent match. Adapts display based on match type:
|
||||||
* <p>
|
* empty state (no matches), 1v1 state (2 players), or group state (3+ players).
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Supported States:</strong>
|
|
||||||
* <ol>
|
|
||||||
* <li><strong>Empty State:</strong> Displayed when no match history exists yet.
|
|
||||||
* Shows a placeholder or welcome message to encourage users to start playing.</li>
|
|
||||||
* <li><strong>1v1 State:</strong> Displayed for head-to-head matches (exactly 2 players).
|
|
||||||
* Shows a side-by-side comparison of player names and scores.</li>
|
|
||||||
* <li><strong>Group State:</strong> Displayed for group matches (3 or more players).
|
|
||||||
* Shows a mini-leaderboard with all participants sorted by their performance.</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Key Features:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Automatic state switching based on match data</li>
|
|
||||||
* <li>Clean state management ensuring only one view state is visible at a time</li>
|
|
||||||
* <li>Efficient RecyclerView implementation for group matches</li>
|
|
||||||
* <li>Graceful handling of null/missing match data</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see Match
|
|
||||||
* @see MainMenuGroupMatchAdapter
|
|
||||||
* @see FrameLayout
|
|
||||||
* @author Oche Companion Development Team
|
|
||||||
* @version 1.0
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
*/
|
||||||
public class MatchRecapView extends FrameLayout {
|
public class MatchRecapView extends FrameLayout {
|
||||||
|
|
||||||
/**
|
/** View container for empty state (no match history). */
|
||||||
* View container for the empty state display.
|
private View mStateEmpty;
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Visibility is managed by {@link #updateVisibility(View)} to ensure
|
|
||||||
* only one state is visible at a time.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #setMatch(Match)
|
|
||||||
* @see #updateVisibility(View)
|
|
||||||
*/
|
|
||||||
private View stateEmpty;
|
|
||||||
|
|
||||||
/**
|
/** View container for 1v1 match state (exactly 2 players). */
|
||||||
* View container for the 1v1 match state display.
|
private View mState1v1;
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Visibility is managed by {@link #updateVisibility(View)} to ensure
|
|
||||||
* only one state is visible at a time.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #setup1v1State(Match)
|
|
||||||
* @see #updateVisibility(View)
|
|
||||||
*/
|
|
||||||
private View state1v1;
|
|
||||||
|
|
||||||
/**
|
/** View container for group match state (3+ players). */
|
||||||
* View container for the group match state display.
|
private View mStateGroup;
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Visibility is managed by {@link #updateVisibility(View)} to ensure
|
|
||||||
* only one state is visible at a time.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #setupGroupState(Match)
|
|
||||||
* @see #updateVisibility(View)
|
|
||||||
*/
|
|
||||||
private View stateGroup;
|
|
||||||
|
|
||||||
// ========== 1v1 View References ==========
|
// ========== 1v1 View References ==========
|
||||||
|
|
||||||
/**
|
/** Player 1 name in 1v1 match. */
|
||||||
* TextView displaying the name of the first player in a 1v1 match.
|
private TextView mTvP1Name;
|
||||||
* Used only in the 1v1 state.
|
|
||||||
*/
|
|
||||||
private TextView tvP1Name;
|
|
||||||
|
|
||||||
/**
|
/** Player 2 name in 1v1 match. */
|
||||||
* TextView displaying the name of the second player in a 1v1 match.
|
private TextView mTvP2Name;
|
||||||
* Used only in the 1v1 state.
|
|
||||||
*/
|
|
||||||
private TextView tvP2Name;
|
|
||||||
|
|
||||||
/**
|
/** Player 1 score in 1v1 match. */
|
||||||
* TextView displaying the score/average of the first player in a 1v1 match.
|
private TextView mTvP1Score;
|
||||||
* Used only in the 1v1 state.
|
|
||||||
*/
|
|
||||||
private TextView tvP1Score;
|
|
||||||
|
|
||||||
/**
|
/** Player 2 score in 1v1 match. */
|
||||||
* TextView displaying the score/average of the second player in a 1v1 match.
|
private TextView mTvP2Score;
|
||||||
* Used only in the 1v1 state.
|
|
||||||
*/
|
|
||||||
private TextView tvP2Score;
|
|
||||||
|
|
||||||
// ========== Group View References ==========
|
// ========== Group View References ==========
|
||||||
|
|
||||||
/**
|
/** RecyclerView displaying leaderboard for group matches. */
|
||||||
* RecyclerView displaying the leaderboard for group matches.
|
private RecyclerView mRvLeaderboard;
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Used only in the group state.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see MainMenuGroupMatchAdapter
|
|
||||||
* @see #setupGroupState(Match)
|
|
||||||
*/
|
|
||||||
private RecyclerView rvLeaderboard;
|
|
||||||
|
|
||||||
/**
|
/** Constructor for programmatic instantiation. */
|
||||||
* Constructor for programmatic instantiation of the MatchRecapView.
|
|
||||||
* <p>
|
|
||||||
* This constructor delegates to the two-parameter constructor with a null
|
|
||||||
* AttributeSet, which in turn inflates the layout and initializes all child views.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param context The Context in which the view is running, through which it can
|
|
||||||
* access the current theme, resources, etc.
|
|
||||||
* @see #MatchRecapView(Context, AttributeSet)
|
|
||||||
*/
|
|
||||||
public MatchRecapView(@NonNull final Context context) {
|
public MatchRecapView(@NonNull final Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Constructor for XML inflation. */
|
||||||
* Constructor for XML inflation of the MatchRecapView.
|
|
||||||
* <p>
|
|
||||||
* This constructor is called when the view is inflated from an XML layout file.
|
|
||||||
* It performs the following initialization:
|
|
||||||
* <ol>
|
|
||||||
* <li>Calls the parent FrameLayout constructor</li>
|
|
||||||
* <li>Inflates the view_match_recap layout into this container</li>
|
|
||||||
* <li>Initializes all child view references via {@link #initViews()}</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* After construction, the view defaults to showing no content until
|
|
||||||
* {@link #setMatch(Match)} is called with valid match data.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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()
|
|
||||||
*/
|
|
||||||
public MatchRecapView(@NonNull final Context context, @Nullable final AttributeSet attrs) {
|
public MatchRecapView(@NonNull final Context context, @Nullable final AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
// Inflate the layout for this composite view
|
|
||||||
inflate(context, R.layout.view_match_recap, this);
|
inflate(context, R.layout.view_match_recap, this);
|
||||||
// Initialize all child view references
|
|
||||||
initViews();
|
initViews();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Initializes references to all child views. */
|
||||||
* Initializes references to all child views within the inflated layout.
|
|
||||||
* <p>
|
|
||||||
* This method retrieves and stores references to all UI components needed for
|
|
||||||
* the three different states:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>State containers:</strong> Empty, 1v1, and Group view containers</li>
|
|
||||||
* <li><strong>1v1 components:</strong> Player name and score TextViews</li>
|
|
||||||
* <li><strong>Group components:</strong> Leaderboard RecyclerView</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* All views must exist in the R.layout.view_match_recap layout file,
|
|
||||||
* otherwise this method will throw a NullPointerException.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* This method is called once during construction and does not need to be
|
|
||||||
* called again during the view's lifecycle.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #MatchRecapView(Context, AttributeSet)
|
|
||||||
*/
|
|
||||||
private void initViews() {
|
private void initViews() {
|
||||||
// Initialize state container references
|
mStateEmpty = findViewById(R.id.stateEmpty);
|
||||||
stateEmpty = findViewById(R.id.stateEmpty);
|
mState1v1 = findViewById(R.id.state1v1);
|
||||||
state1v1 = findViewById(R.id.state1v1);
|
mStateGroup = findViewById(R.id.stateGroup);
|
||||||
stateGroup = findViewById(R.id.stateGroup);
|
|
||||||
|
|
||||||
// Initialize 1v1 match view references
|
mTvP1Name = findViewById(R.id.tvP1Name);
|
||||||
tvP1Name = findViewById(R.id.tvP1Name);
|
mTvP1Score = findViewById(R.id.tvP1Score);
|
||||||
tvP1Score = findViewById(R.id.tvP1Score);
|
mTvP2Name = findViewById(R.id.tvP2Name);
|
||||||
tvP2Name = findViewById(R.id.tvP2Name);
|
mTvP2Score = findViewById(R.id.tvP2Score);
|
||||||
tvP2Score = findViewById(R.id.tvP2Score);
|
|
||||||
|
|
||||||
// Initialize group match view references
|
mRvLeaderboard = findViewById(R.id.rvLeaderboard);
|
||||||
rvLeaderboard = findViewById(R.id.rvLeaderboard);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Binds match and updates display (empty, 1v1, or group state). */
|
||||||
* Binds a Match object to the view and updates the display accordingly.
|
|
||||||
* <p>
|
|
||||||
* 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:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>Null match:</strong> Shows the empty state</li>
|
|
||||||
* <li><strong>2 players:</strong> Shows the 1v1 state with head-to-head comparison</li>
|
|
||||||
* <li><strong>3+ players:</strong> Shows the group state with leaderboard</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>State Selection Logic:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage:</strong>
|
|
||||||
* This method should be called whenever the match data changes, such as:
|
|
||||||
* <ul>
|
|
||||||
* <li>When the activity first loads</li>
|
|
||||||
* <li>After a new match is completed</li>
|
|
||||||
* <li>When resuming the activity to refresh with latest data</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param match The Match object from the database, or null if no match history exists.
|
|
||||||
* The match should contain all necessary player and score information.
|
|
||||||
* @see Match#getParticipantCount()
|
|
||||||
* @see #setup1v1State(Match)
|
|
||||||
* @see #setupGroupState(Match)
|
|
||||||
* @see #updateVisibility(View)
|
|
||||||
*/
|
|
||||||
public void setMatch(@Nullable final Match match) {
|
public void setMatch(@Nullable final Match match) {
|
||||||
// Handle null case - no match history exists
|
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
updateVisibility(stateEmpty);
|
updateVisibility(mStateEmpty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine which state to show based on participant count
|
|
||||||
if (match.getParticipantCount() > 2) {
|
if (match.getParticipantCount() > 2) {
|
||||||
// 3+ players: Show group leaderboard
|
|
||||||
setupGroupState(match);
|
setupGroupState(match);
|
||||||
} else {
|
} else {
|
||||||
// Exactly 2 players: Show 1v1 comparison
|
|
||||||
setup1v1State(match);
|
setup1v1State(match);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Configures 1v1 state with player names and scores. */
|
||||||
* Configures and displays the 1v1 match state.
|
|
||||||
* <p>
|
|
||||||
* This method sets up the view for displaying a head-to-head match between two players.
|
|
||||||
* It performs the following operations:
|
|
||||||
* <ol>
|
|
||||||
* <li>Switches visibility to show only the 1v1 state container</li>
|
|
||||||
* <li>Retrieves player data by position (0 for player 1, 1 for player 2)</li>
|
|
||||||
* <li>Populates the player name TextViews</li>
|
|
||||||
* <li>Populates the player score/average TextViews</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Data Retrieval:</strong>
|
|
||||||
* 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).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Assumptions:</strong>
|
|
||||||
* This method assumes the match contains exactly 2 players. The caller
|
|
||||||
* ({@link #setMatch(Match)}) should verify the participant count before calling this method.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param match The Match object containing exactly 2 players. Must not be null.
|
|
||||||
* @see Match#getPlayerNameByPosition(int)
|
|
||||||
* @see Match#getPlayerAverageByPosition(int)
|
|
||||||
* @see #updateVisibility(View)
|
|
||||||
*/
|
|
||||||
private void setup1v1State(final Match match) {
|
private void setup1v1State(final Match match) {
|
||||||
// Switch to 1v1 state visibility
|
updateVisibility(mState1v1);
|
||||||
updateVisibility(state1v1);
|
|
||||||
|
|
||||||
// Populate player 1 information (left side)
|
mTvP1Name.setText(match.getPlayerNameByPosition(0));
|
||||||
tvP1Name.setText(match.getPlayerNameByPosition(0));
|
mTvP1Score.setText(String.valueOf(match.getPlayerAverageByPosition(0)));
|
||||||
tvP1Score.setText(String.valueOf(match.getPlayerAverageByPosition(0)));
|
|
||||||
|
|
||||||
// Populate player 2 information (right side)
|
mTvP2Name.setText(match.getPlayerNameByPosition(1));
|
||||||
tvP2Name.setText(match.getPlayerNameByPosition(1));
|
mTvP2Score.setText(String.valueOf(match.getPlayerAverageByPosition(1)));
|
||||||
tvP2Score.setText(String.valueOf(match.getPlayerAverageByPosition(1)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Configures group state with leaderboard RecyclerView. */
|
||||||
* Configures and displays the group match state with a leaderboard.
|
|
||||||
* <p>
|
|
||||||
* This method sets up the view for displaying a match with 3 or more players.
|
|
||||||
* It performs the following operations:
|
|
||||||
* <ol>
|
|
||||||
* <li>Switches visibility to show only the group state container</li>
|
|
||||||
* <li>Configures the RecyclerView with a LinearLayoutManager</li>
|
|
||||||
* <li>Creates and attaches a MainMenuGroupMatchAdapter</li>
|
|
||||||
* <li>Populates the adapter with match data (players are automatically sorted by score)</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>RecyclerView Configuration:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Adapter Behavior:</strong>
|
|
||||||
* 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).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Performance Note:</strong>
|
|
||||||
* 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)}.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param match The Match object containing 3 or more players. Must not be null.
|
|
||||||
* @see MainMenuGroupMatchAdapter
|
|
||||||
* @see LinearLayoutManager
|
|
||||||
* @see #updateVisibility(View)
|
|
||||||
*/
|
|
||||||
private void setupGroupState(final Match match) {
|
private void setupGroupState(final Match match) {
|
||||||
// Switch to group state visibility
|
updateVisibility(mStateGroup);
|
||||||
updateVisibility(stateGroup);
|
|
||||||
|
|
||||||
// Configure the RecyclerView with a vertical LinearLayoutManager
|
mRvLeaderboard.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
rvLeaderboard.setLayoutManager(new LinearLayoutManager(getContext()));
|
|
||||||
|
|
||||||
// Create and configure the adapter for displaying the player leaderboard
|
|
||||||
final MainMenuGroupMatchAdapter adapter = new MainMenuGroupMatchAdapter();
|
final MainMenuGroupMatchAdapter adapter = new MainMenuGroupMatchAdapter();
|
||||||
rvLeaderboard.setAdapter(adapter);
|
mRvLeaderboard.setAdapter(adapter);
|
||||||
|
|
||||||
// Populate the adapter with match data (players will be sorted automatically)
|
|
||||||
adapter.updateMatch(match);
|
adapter.updateMatch(match);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Shows only the specified state container, hides others. */
|
||||||
* Updates the visibility of all state containers, showing only the specified active view.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Visibility Logic:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>The view matching activeView is set to {@link View#VISIBLE}</li>
|
|
||||||
* <li>All other views are set to {@link View#GONE} (completely removed from layout)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Why GONE instead of INVISIBLE:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* This centralized visibility management prevents inconsistent states where multiple
|
|
||||||
* containers might be visible simultaneously.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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
|
|
||||||
*/
|
|
||||||
private void updateVisibility(final View activeView) {
|
private void updateVisibility(final View activeView) {
|
||||||
// Set empty state visibility
|
mStateEmpty.setVisibility(activeView == mStateEmpty ? VISIBLE : GONE);
|
||||||
stateEmpty.setVisibility(activeView == stateEmpty ? VISIBLE : GONE);
|
mState1v1.setVisibility(activeView == mState1v1 ? VISIBLE : GONE);
|
||||||
|
mStateGroup.setVisibility(activeView == mStateGroup ? VISIBLE : GONE);
|
||||||
// Set 1v1 state visibility
|
|
||||||
state1v1.setVisibility(activeView == state1v1 ? VISIBLE : GONE);
|
|
||||||
|
|
||||||
// Set group state visibility
|
|
||||||
stateGroup.setVisibility(activeView == stateGroup ? VISIBLE : GONE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,272 +13,56 @@ import com.google.android.material.imageview.ShapeableImageView;
|
|||||||
import com.aldo.apps.ochecompanion.R;
|
import com.aldo.apps.ochecompanion.R;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable custom view component for displaying individual player information in a card format.
|
* Reusable MaterialCardView for displaying player info: profile picture, username,
|
||||||
* <p>
|
* and career statistics. Uses Glide for image loading with fallback to default icon.
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Key Features:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Styled MaterialCardView with custom colors, elevation, and corner radius</li>
|
|
||||||
* <li>Profile picture display with Glide integration for efficient image loading</li>
|
|
||||||
* <li>Automatic fallback to default avatar icon when no profile picture exists</li>
|
|
||||||
* <li>Career average statistics display with formatted text</li>
|
|
||||||
* <li>Reusable across different contexts (squad lists, match recaps, leaderboards)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Contexts:</strong>
|
|
||||||
* This view is used in multiple places throughout the app:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>Squad List:</strong> In {@link com.aldo.apps.ochecompanion.ui.adapter.MainMenuPlayerAdapter}
|
|
||||||
* for displaying the user's roster of players</li>
|
|
||||||
* <li><strong>Group Matches:</strong> In {@link com.aldo.apps.ochecompanion.ui.adapter.MainMenuGroupMatchAdapter}
|
|
||||||
* for displaying match participants in leaderboard format</li>
|
|
||||||
* <li><strong>Match Recaps:</strong> Anywhere player information needs to be displayed consistently</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Design Pattern:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Styling:</strong>
|
|
||||||
* The card appearance is configured in {@link #initViews()} with:
|
|
||||||
* <ul>
|
|
||||||
* <li>Background color from {@code R.color.surface_primary}</li>
|
|
||||||
* <li>Corner radius from {@code R.dimen.radius_m}</li>
|
|
||||||
* <li>Elevation from {@code R.dimen.card_elevation}</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see MaterialCardView
|
|
||||||
* @see Player
|
|
||||||
* @see com.aldo.apps.ochecompanion.ui.adapter.MainMenuPlayerAdapter
|
|
||||||
* @see com.aldo.apps.ochecompanion.ui.adapter.MainMenuGroupMatchAdapter
|
|
||||||
* @author Oche Companion Development Team
|
|
||||||
* @version 1.0
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
*/
|
||||||
public class PlayerItemView extends MaterialCardView {
|
public class PlayerItemView extends MaterialCardView {
|
||||||
|
|
||||||
/**
|
/** Player profile picture (circular, loaded via Glide). */
|
||||||
* ShapeableImageView displaying the player's profile picture or avatar.
|
private ShapeableImageView mIvAvatar;
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* The ShapeableImageView type allows for easy customization of the image shape
|
|
||||||
* through XML attributes, supporting circular, rounded rectangle, or custom shapes.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #bind(Player)
|
|
||||||
* @see ShapeableImageView
|
|
||||||
*/
|
|
||||||
private ShapeableImageView ivAvatar;
|
|
||||||
|
|
||||||
/**
|
/** Player username. */
|
||||||
* TextView displaying the player's username.
|
private TextView mTvUsername;
|
||||||
* <p>
|
|
||||||
* Shows the {@link Player#username} field. This is the primary identifier
|
|
||||||
* for the player in the UI.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
private TextView tvUsername;
|
|
||||||
|
|
||||||
/**
|
/** Player career average statistics. */
|
||||||
* TextView displaying the player's career statistics.
|
private TextView mTvStats;
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
private TextView tvStats;
|
|
||||||
|
|
||||||
/**
|
/** Constructor for programmatic instantiation. */
|
||||||
* Constructor for programmatic instantiation of the PlayerItemView.
|
public PlayerItemView(@NonNull final Context context) {
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Use this constructor when creating PlayerItemView instances directly in code
|
|
||||||
* rather than inflating from XML.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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) {
|
|
||||||
this(context, null);
|
this(context, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Constructor for XML inflation. */
|
||||||
* Constructor for XML inflation of the PlayerItemView.
|
public PlayerItemView(@NonNull final Context context, @Nullable final AttributeSet attrs) {
|
||||||
* <p>
|
|
||||||
* 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:
|
|
||||||
* <ol>
|
|
||||||
* <li>Calls the parent MaterialCardView constructor</li>
|
|
||||||
* <li>Inflates the item_player_small layout into this container</li>
|
|
||||||
* <li>Initializes all child views and applies card styling via {@link #initViews()}</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* After construction, the view is ready to receive player data through the
|
|
||||||
* {@link #bind(Player)} method.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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) {
|
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
// Inflate the player item layout into this card view
|
|
||||||
inflate(context, R.layout.item_player_small, this);
|
inflate(context, R.layout.item_player_small, this);
|
||||||
// Initialize child views and apply styling
|
|
||||||
initViews();
|
initViews();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Configures card styling and initializes child view references. */
|
||||||
* Initializes child view references and applies MaterialCardView styling.
|
|
||||||
* <p>
|
|
||||||
* This method performs two main tasks:
|
|
||||||
* <ol>
|
|
||||||
* <li><strong>Card Styling:</strong> Configures the MaterialCardView's visual properties
|
|
||||||
* including background color, corner radius, and elevation for a consistent
|
|
||||||
* Material Design appearance</li>
|
|
||||||
* <li><strong>View References:</strong> Retrieves and stores references to all child
|
|
||||||
* views (avatar, username, stats) for efficient data binding</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Styling Details:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>Background Color:</strong> Uses {@code R.color.surface_primary} for
|
|
||||||
* consistent theming across the app</li>
|
|
||||||
* <li><strong>Corner Radius:</strong> Uses {@code R.dimen.radius_m} for medium-sized
|
|
||||||
* rounded corners following Material Design guidelines</li>
|
|
||||||
* <li><strong>Elevation:</strong> Uses {@code R.dimen.card_elevation} to create subtle
|
|
||||||
* depth and visual hierarchy</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* This method is called once during construction and does not need to be called
|
|
||||||
* again during the view's lifecycle.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #PlayerItemView(Context, AttributeSet)
|
|
||||||
* @see MaterialCardView#setCardBackgroundColor(int)
|
|
||||||
* @see MaterialCardView#setRadius(float)
|
|
||||||
* @see MaterialCardView#setCardElevation(float)
|
|
||||||
*/
|
|
||||||
private void initViews() {
|
private void initViews() {
|
||||||
// ========== Card Styling Configuration ==========
|
|
||||||
|
|
||||||
// Set card background color from theme
|
|
||||||
setCardBackgroundColor(getContext().getColor(R.color.surface_primary));
|
setCardBackgroundColor(getContext().getColor(R.color.surface_primary));
|
||||||
|
|
||||||
// Set corner radius for rounded edges
|
|
||||||
setRadius(getResources().getDimension(R.dimen.radius_m));
|
setRadius(getResources().getDimension(R.dimen.radius_m));
|
||||||
|
|
||||||
// Set elevation for Material Design shadow effect
|
|
||||||
setCardElevation(getResources().getDimension(R.dimen.card_elevation));
|
setCardElevation(getResources().getDimension(R.dimen.card_elevation));
|
||||||
|
|
||||||
// ========== Child View References ==========
|
mIvAvatar = findViewById(R.id.ivPlayerProfile);
|
||||||
|
mTvUsername = findViewById(R.id.tvPlayerName);
|
||||||
// Get reference to the avatar/profile picture ImageView
|
mTvStats = findViewById(R.id.tvPlayerAvg);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Binds player data to view components (username, stats, avatar). */
|
||||||
* Binds a Player object to this view, populating all UI components with player data.
|
|
||||||
* <p>
|
|
||||||
* This method updates the view's content to display information for the specified player:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>Username:</strong> Sets the player's name in the username TextView</li>
|
|
||||||
* <li><strong>Career Average:</strong> Formats and displays the player's career statistics</li>
|
|
||||||
* <li><strong>Profile Picture:</strong> Loads the player's avatar image or shows a default icon</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Image Loading Strategy:</strong>
|
|
||||||
* The method uses Glide library for efficient image loading:
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>With Profile Picture:</strong> If {@link Player#profilePictureUri} is not null,
|
|
||||||
* Glide loads the image from the file URI with automatic caching, memory management,
|
|
||||||
* and placeholder handling</li>
|
|
||||||
* <li><strong>Without Profile Picture:</strong> If no URI is available, displays a default
|
|
||||||
* user icon ({@code R.drawable.ic_users}) as a fallback</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Text Formatting:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Performance:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage:</strong>
|
|
||||||
* This method should be called whenever:
|
|
||||||
* <ul>
|
|
||||||
* <li>A new player is to be displayed in this view</li>
|
|
||||||
* <li>Player data has been updated and the view needs to refresh</li>
|
|
||||||
* <li>The view is being recycled in a RecyclerView adapter</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param player The Player entity from the database containing all necessary display information.
|
|
||||||
* Must not be null.
|
|
||||||
* @throws NullPointerException if player is null.
|
|
||||||
* @see Player#username
|
|
||||||
* @see Player#careerAverage
|
|
||||||
* @see Player#profilePictureUri
|
|
||||||
* @see Glide
|
|
||||||
*/
|
|
||||||
public void bind(@NonNull final Player player) {
|
public void bind(@NonNull final Player player) {
|
||||||
// Set the player's username
|
mTvUsername.setText(player.username);
|
||||||
tvUsername.setText(player.username);
|
mTvStats.setText(String.format(
|
||||||
|
|
||||||
// Format and set the career average statistics
|
|
||||||
tvStats.setText(String.format(
|
|
||||||
getContext().getString(R.string.txt_player_average_base),
|
getContext().getString(R.string.txt_player_average_base),
|
||||||
player.careerAverage));
|
player.careerAverage));
|
||||||
|
|
||||||
// Load and display the profile picture
|
|
||||||
if (player.profilePictureUri != null) {
|
if (player.profilePictureUri != null) {
|
||||||
// Profile picture exists - load it using Glide for efficient caching
|
|
||||||
Glide.with(getContext())
|
Glide.with(getContext())
|
||||||
.load(player.profilePictureUri)
|
.load(player.profilePictureUri)
|
||||||
.into(ivAvatar);
|
.into(mIvAvatar);
|
||||||
} else {
|
} else {
|
||||||
// No profile picture - show default user icon
|
mIvAvatar.setImageResource(R.drawable.ic_users);
|
||||||
ivAvatar.setImageResource(R.drawable.ic_users);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,350 +11,63 @@ import androidx.annotation.Nullable;
|
|||||||
import com.aldo.apps.ochecompanion.R;
|
import com.aldo.apps.ochecompanion.R;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom hero-style button component for initiating quick match sessions.
|
* Hero-style button for quick match initiation. Features large primary label
|
||||||
* <p>
|
* and secondary descriptive subtext. Automatically uppercases all text.
|
||||||
* 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:
|
|
||||||
* <ul>
|
|
||||||
* <li>A large, bold primary label (e.g., "QUICK START")</li>
|
|
||||||
* <li>A secondary descriptive subtext showing game mode and rules</li>
|
|
||||||
* <li>Hero-style prominent appearance matching dashboard mockups</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Design Philosophy:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Key Features:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Dual-text hierarchy with main title and descriptive subtitle</li>
|
|
||||||
* <li>Automatic text uppercasing for visual consistency</li>
|
|
||||||
* <li>Convenience method for updating game context (mode and rules)</li>
|
|
||||||
* <li>Built-in clickable and focusable configuration for accessibility</li>
|
|
||||||
* <li>Efficient merge-tag layout inflation pattern</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage Example:</strong>
|
|
||||||
* <pre>
|
|
||||||
* QuickStartButton button = findViewById(R.id.quickStartButton);
|
|
||||||
* button.setMainText("Quick Start");
|
|
||||||
* button.updateContext("501", "Double Out");
|
|
||||||
* button.setOnClickListener(v -> startMatch());
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Text Formatting:</strong>
|
|
||||||
* Both main and sub text are automatically converted to uppercase to maintain
|
|
||||||
* consistent visual styling and match the high-impact design aesthetic.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Accessibility:</strong>
|
|
||||||
* The component is configured as clickable and focusable, ensuring it works
|
|
||||||
* properly with touch, keyboard navigation, and accessibility services.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see FrameLayout
|
|
||||||
* @see TextView
|
|
||||||
* @author Oche Companion Development Team
|
|
||||||
* @version 1.0
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
*/
|
||||||
public class QuickStartButton extends FrameLayout {
|
public class QuickStartButton extends FrameLayout {
|
||||||
|
|
||||||
/**
|
/** Primary bold label (automatically uppercased). */
|
||||||
* TextView displaying the primary, bold label for the button.
|
private TextView mTvMainLabel;
|
||||||
* <p>
|
|
||||||
* 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)}.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* This is the most visually prominent element of the component and should
|
|
||||||
* clearly communicate the primary action to the user.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #setMainText(String)
|
|
||||||
*/
|
|
||||||
private TextView tvMainLabel;
|
|
||||||
|
|
||||||
/**
|
/** Secondary descriptive text (mode/rules, automatically uppercased). */
|
||||||
* TextView displaying the secondary descriptive text below the main label.
|
private TextView mTvSubLabel;
|
||||||
* <p>
|
|
||||||
* 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)}.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* This provides users with quick information about what configuration will
|
|
||||||
* be used for the match without requiring them to navigate to settings.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #setSubText(String)
|
|
||||||
* @see #updateContext(String, String)
|
|
||||||
*/
|
|
||||||
private TextView tvSubLabel;
|
|
||||||
|
|
||||||
/**
|
/** Constructor for programmatic instantiation. */
|
||||||
* Constructor for programmatic instantiation of the QuickStartButton.
|
public QuickStartButton(@NonNull final Context context) {
|
||||||
* <p>
|
|
||||||
* This constructor delegates to the two-parameter constructor with a null
|
|
||||||
* AttributeSet, which handles the actual initialization including layout
|
|
||||||
* inflation and view setup.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Use this constructor when creating QuickStartButton instances directly in code
|
|
||||||
* rather than inflating from XML.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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) {
|
|
||||||
this(context, null);
|
this(context, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Constructor for XML inflation. */
|
||||||
* Constructor for XML inflation of the QuickStartButton.
|
public QuickStartButton(@NonNull final Context context, @Nullable final AttributeSet attrs) {
|
||||||
* <p>
|
|
||||||
* 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:
|
|
||||||
* <ol>
|
|
||||||
* <li>Calls the parent FrameLayout constructor</li>
|
|
||||||
* <li>Configures the view as clickable and focusable for proper interaction handling</li>
|
|
||||||
* <li>Inflates the internal layout using the merge-tag pattern for efficiency</li>
|
|
||||||
* <li>Initializes child view references via {@link #initViews()}</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Merge Tag Pattern:</strong>
|
|
||||||
* The layout inflation uses {@code attachToRoot = true}, which is appropriate when
|
|
||||||
* using the {@code <merge>} tag in the layout XML. This pattern eliminates an
|
|
||||||
* unnecessary wrapper ViewGroup, making the view hierarchy more efficient.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Interaction Configuration:</strong>
|
|
||||||
* The view is explicitly set as clickable and focusable to ensure:
|
|
||||||
* <ul>
|
|
||||||
* <li>Proper touch event handling</li>
|
|
||||||
* <li>Visual feedback on interaction (ripple effects, state changes)</li>
|
|
||||||
* <li>Keyboard navigation support for accessibility</li>
|
|
||||||
* <li>Screen reader compatibility</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param context The Context in which the view is running, through which it can
|
|
||||||
* access the current theme, resources, etc.
|
|
||||||
* @param attrs The attributes of the XML tag that is inflating the view. May be null
|
|
||||||
* if instantiated programmatically.
|
|
||||||
* @see #initViews()
|
|
||||||
*/
|
|
||||||
public QuickStartButton(@NonNull Context context, @Nullable AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
|
|
||||||
// Ensure the component is clickable and focusable for proper interaction
|
|
||||||
// This enables touch feedback, keyboard navigation, and accessibility
|
|
||||||
setClickable(true);
|
setClickable(true);
|
||||||
setFocusable(true);
|
setFocusable(true);
|
||||||
|
|
||||||
// Inflate the internal layout using the merge tag pattern
|
|
||||||
// attachToRoot = true is used because view_quick_start.xml uses <merge>
|
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_quick_start, this, true);
|
LayoutInflater.from(context).inflate(R.layout.view_quick_start, this, true);
|
||||||
|
|
||||||
// Initialize references to child views
|
|
||||||
initViews();
|
initViews();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Initializes references to child TextViews. */
|
||||||
* Initializes references to child TextView components within the inflated layout.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* This method is called once during construction and does not need to be called
|
|
||||||
* again during the view's lifecycle.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Required Layout Elements:</strong>
|
|
||||||
* The R.layout.view_quick_start layout must contain:
|
|
||||||
* <ul>
|
|
||||||
* <li>{@code R.id.tvQuickStartMain} - The main title TextView</li>
|
|
||||||
* <li>{@code R.id.tvQuickStartSub} - The subtitle TextView</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #QuickStartButton(Context, AttributeSet)
|
|
||||||
*/
|
|
||||||
private void initViews() {
|
private void initViews() {
|
||||||
// Get reference to the main label TextView
|
mTvMainLabel = findViewById(R.id.tvQuickStartMain);
|
||||||
tvMainLabel = findViewById(R.id.tvQuickStartMain);
|
mTvSubLabel = findViewById(R.id.tvQuickStartSub);
|
||||||
|
|
||||||
// Get reference to the sub-label TextView
|
|
||||||
tvSubLabel = findViewById(R.id.tvQuickStartSub);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Sets main text (automatically uppercased). */
|
||||||
* Sets the primary bold text displayed on the button.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Typical Usage:</strong>
|
|
||||||
* Use this method to set action-oriented text that clearly communicates the
|
|
||||||
* button's purpose, such as:
|
|
||||||
* <ul>
|
|
||||||
* <li>"Quick Start"</li>
|
|
||||||
* <li>"Start Match"</li>
|
|
||||||
* <li>"Play Now"</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Text Transformation:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Null Safety:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param text The main title string to display. Should be concise and action-oriented.
|
|
||||||
* Will be converted to uppercase automatically.
|
|
||||||
* @see #setSubText(String)
|
|
||||||
*/
|
|
||||||
public void setMainText(final String text) {
|
public void setMainText(final String text) {
|
||||||
// Check if TextView is initialized before setting text
|
if (mTvMainLabel != null) {
|
||||||
if (tvMainLabel != null) {
|
mTvMainLabel.setText(text.toUpperCase());
|
||||||
// Convert text to uppercase for consistent styling
|
|
||||||
tvMainLabel.setText(text.toUpperCase());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Sets subtitle text (automatically uppercased). */
|
||||||
* Sets the secondary descriptive text displayed below the main label.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Typical Usage:</strong>
|
|
||||||
* Use this method to set descriptive text that provides details about the
|
|
||||||
* match configuration, such as:
|
|
||||||
* <ul>
|
|
||||||
* <li>"Standard 501 • Double Out"</li>
|
|
||||||
* <li>"301 - Single In/Double Out"</li>
|
|
||||||
* <li>"Cricket • Standard Rules"</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Text Transformation:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Null Safety:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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)
|
|
||||||
*/
|
|
||||||
public void setSubText(final String text) {
|
public void setSubText(final String text) {
|
||||||
// Check if TextView is initialized before setting text
|
if (mTvSubLabel != null) {
|
||||||
if (tvSubLabel != null) {
|
mTvSubLabel.setText(text.toUpperCase());
|
||||||
// Convert text to uppercase for consistent styling
|
|
||||||
tvSubLabel.setText(text.toUpperCase());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Updates subtitle with formatted mode and rules ("MODE - RULES"). */
|
||||||
* Convenience method to update the subtitle based on game mode and rules.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Formatting Behavior:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>If rules are provided: Displays "[MODE] - [RULES]" (e.g., "501 - DOUBLE OUT")</li>
|
|
||||||
* <li>If rules are empty/null: Displays only "[MODE]" (e.g., "501")</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Example Usage:</strong>
|
|
||||||
* <pre>
|
|
||||||
* // 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"
|
|
||||||
* </pre>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Advantages over setSubText:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Provides consistent formatting across the app</li>
|
|
||||||
* <li>Handles null/empty rules gracefully</li>
|
|
||||||
* <li>Reduces code duplication in calling code</li>
|
|
||||||
* <li>Makes intent clearer (semantic method name)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Implementation Details:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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
|
|
||||||
*/
|
|
||||||
public void updateContext(final String mode, final String rules) {
|
public void updateContext(final String mode, final String rules) {
|
||||||
// Use StringBuilder for efficient string concatenation
|
|
||||||
final StringBuilder stringBuilder = new StringBuilder(mode);
|
final StringBuilder stringBuilder = new StringBuilder(mode);
|
||||||
|
|
||||||
// Only append rules if they are provided (not null and not empty)
|
|
||||||
if (!TextUtils.isEmpty(rules)) {
|
if (!TextUtils.isEmpty(rules)) {
|
||||||
// Add separator between mode and rules
|
|
||||||
stringBuilder.append(" - ");
|
stringBuilder.append(" - ");
|
||||||
// Append the rules text
|
|
||||||
stringBuilder.append(rules);
|
stringBuilder.append(rules);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the combined text as the subtitle
|
|
||||||
setSubText(stringBuilder.toString());
|
setSubText(stringBuilder.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,80 +21,23 @@ import java.util.Comparator;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RecyclerView adapter for displaying group match player information in the Main Menu.
|
* RecyclerView adapter for displaying group match results in Main Menu.
|
||||||
* <p>
|
* Displays players sorted by career average with their names, scores, and profile pictures.
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Key Features:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Displays player name, career average, and profile picture</li>
|
|
||||||
* <li>Automatically sorts players by career average</li>
|
|
||||||
* <li>Uses custom {@link PlayerItemView} for consistent player display</li>
|
|
||||||
* <li>Integrates with Glide for efficient image loading</li>
|
|
||||||
* <li>Optimized for infrequent updates (typically once per activity lifecycle)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see RecyclerView.Adapter
|
|
||||||
* @see GroupMatchHolder
|
|
||||||
* @see PlayerScoreComparator
|
|
||||||
* @see Match
|
|
||||||
* @see Player
|
|
||||||
* @author Oche Companion Development Team
|
|
||||||
* @version 1.0
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
*/
|
||||||
public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGroupMatchAdapter.GroupMatchHolder> {
|
public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGroupMatchAdapter.GroupMatchHolder> {
|
||||||
|
|
||||||
/**
|
/** Tag for logging. */
|
||||||
* Tag for logging and debugging purposes.
|
|
||||||
* Used to identify log messages originating from this adapter.
|
|
||||||
*/
|
|
||||||
private static final String TAG = "MainMenuGroupMatchAdapt";
|
private static final String TAG = "MainMenuGroupMatchAdapt";
|
||||||
|
|
||||||
/**
|
/** List of players sorted by career average. */
|
||||||
* Internal list holding the players to be displayed in the RecyclerView.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #updateMatch(Match)
|
|
||||||
* @see Player
|
|
||||||
*/
|
|
||||||
private final List<Player> mPlayersList = new ArrayList<>();
|
private final List<Player> mPlayersList = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new {@link GroupMatchHolder} to display player information.
|
* Creates a new ViewHolder with a PlayerItemView.
|
||||||
* <p>
|
*
|
||||||
* This method is called by the RecyclerView when it needs a new ViewHolder to represent
|
* @param parent The parent ViewGroup.
|
||||||
* a player item. The method creates a custom {@link PlayerItemView} and configures its
|
* @param viewType The view type (unused).
|
||||||
* layout parameters to match the parent's width and wrap its content height.
|
* @return A new GroupMatchHolder.
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Layout Configuration:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Width: MATCH_PARENT (fills available width)</li>
|
|
||||||
* <li>Height: WRAP_CONTENT (adjusts to content size)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param parent The ViewGroup into which the new View will be added after it is bound
|
|
||||||
* to an adapter position.
|
|
||||||
* @param viewType The view type of the new View. Not used in this implementation as
|
|
||||||
* all items use the same view type.
|
|
||||||
* @return A new GroupMatchHolder that holds a PlayerItemView.
|
|
||||||
* @see GroupMatchHolder
|
|
||||||
* @see PlayerItemView
|
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
@@ -112,17 +55,10 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGrou
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds player data to a ViewHolder at the specified position.
|
* Binds player data to the ViewHolder at the specified position.
|
||||||
* <p>
|
*
|
||||||
* This method is called by the RecyclerView to display player data at the specified position.
|
* @param holder The ViewHolder to update.
|
||||||
* It retrieves the player from the internal list and delegates the display logic to the
|
* @param position The position in the data set.
|
||||||
* {@link GroupMatchHolder#setPlayer(Player)} method.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param holder The ViewHolder which should be updated to represent the player
|
|
||||||
* at the given position in the data set.
|
|
||||||
* @param position The position of the player within the adapter's data set.
|
|
||||||
* @see GroupMatchHolder#setPlayer(Player)
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull final GroupMatchHolder holder, final int position) {
|
public void onBindViewHolder(@NonNull final GroupMatchHolder holder, final int position) {
|
||||||
@@ -131,13 +67,9 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGrou
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the total number of players in the data set held by the adapter.
|
* Returns the number of players.
|
||||||
* <p>
|
*
|
||||||
* This method is called by the RecyclerView to determine how many items to display.
|
* @return Player count.
|
||||||
* The count is based on the number of players currently stored in {@link #mPlayersList}.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return The total number of players in the list.
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount() {
|
public int getItemCount() {
|
||||||
@@ -145,30 +77,9 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGrou
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the adapter with new match data and refreshes the display.
|
* Updates the adapter with match data, sorting players by career average.
|
||||||
* <p>
|
*
|
||||||
* This method performs the following operations:
|
* @param match The match containing players to display.
|
||||||
* <ol>
|
|
||||||
* <li>Clears the existing player list</li>
|
|
||||||
* <li>Retrieves all players from the provided match</li>
|
|
||||||
* <li>Sorts players by their career average (ascending order)</li>
|
|
||||||
* <li>Adds the sorted players to the internal list</li>
|
|
||||||
* <li>Notifies the RecyclerView to refresh the display</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Performance Consideration:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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
|
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
public void updateMatch(final Match match) {
|
public void updateMatch(final Match match) {
|
||||||
@@ -200,73 +111,24 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGrou
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewHolder class for displaying individual player items in group match view.
|
* ViewHolder for displaying player items in group match view.
|
||||||
* <p>
|
* Hides chevron since items are not clickable.
|
||||||
* This ViewHolder manages the UI components for a single player entry, including:
|
|
||||||
* <ul>
|
|
||||||
* <li>Player name</li>
|
|
||||||
* <li>Player career average score</li>
|
|
||||||
* <li>Player profile picture/avatar</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* The ViewHolder uses a {@link PlayerItemView} as its root view and automatically
|
|
||||||
* hides the chevron icon since group match items are not clickable/expandable.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Image Loading:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see RecyclerView.ViewHolder
|
|
||||||
* @see PlayerItemView
|
|
||||||
* @see Player
|
|
||||||
*/
|
*/
|
||||||
public static class GroupMatchHolder extends RecyclerView.ViewHolder {
|
public static class GroupMatchHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
/**
|
/** TextView displaying the player's name. */
|
||||||
* TextView displaying the player's username.
|
|
||||||
* Shows the {@link Player#username} field.
|
|
||||||
*/
|
|
||||||
private final TextView mPlayerNameView;
|
private final TextView mPlayerNameView;
|
||||||
|
|
||||||
/**
|
/** TextView displaying the player's career average. */
|
||||||
* TextView displaying the player's career average score.
|
|
||||||
* <p>
|
|
||||||
* Shows the {@link Player#careerAverage} field formatted according to
|
|
||||||
* the string resource {@code R.string.txt_player_average_base}.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
private final TextView mPlayerScoreView;
|
private final TextView mPlayerScoreView;
|
||||||
|
|
||||||
/**
|
/** ShapeableImageView displaying the player's profile picture. */
|
||||||
* ShapeableImageView displaying the player's profile picture.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
private final ShapeableImageView mPlayerImageView;
|
private final ShapeableImageView mPlayerImageView;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new GroupMatchHolder and initializes its child views.
|
* Constructs a new GroupMatchHolder and initializes child views.
|
||||||
* <p>
|
*
|
||||||
* This constructor performs the following setup:
|
* @param itemView The root view (PlayerItemView).
|
||||||
* <ol>
|
|
||||||
* <li>Calls the parent ViewHolder constructor</li>
|
|
||||||
* <li>Retrieves references to the player name, score, and image views</li>
|
|
||||||
* <li>Hides the chevron icon since group match items are non-interactive</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* The chevron icon is hidden because players in group match view are displayed
|
|
||||||
* for informational purposes only and do not support click/expand actions.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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.
|
|
||||||
*/
|
*/
|
||||||
public GroupMatchHolder(@NonNull final View itemView) {
|
public GroupMatchHolder(@NonNull final View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
@@ -281,27 +143,9 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGrou
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds a Player object to this ViewHolder, updating all displayed information.
|
* Binds player data to this ViewHolder.
|
||||||
* <p>
|
*
|
||||||
* This method updates the UI components with the player's data:
|
* @param player The player to display.
|
||||||
* <ul>
|
|
||||||
* <li><strong>Name:</strong> Sets the player's username in the name TextView</li>
|
|
||||||
* <li><strong>Score:</strong> Formats and displays the career average using a string resource</li>
|
|
||||||
* <li><strong>Avatar:</strong> Loads the profile picture using Glide, or shows a default icon</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Image Loading Strategy:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param player The Player object whose information should be displayed.
|
|
||||||
* Must not be null.
|
|
||||||
* @see Player#username
|
|
||||||
* @see Player#careerAverage
|
|
||||||
* @see Player#profilePictureUri
|
|
||||||
*/
|
*/
|
||||||
public void setPlayer(final Player player) {
|
public void setPlayer(final Player player) {
|
||||||
// Set player name
|
// Set player name
|
||||||
@@ -326,42 +170,16 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGrou
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comparator for sorting Player objects based on their career average scores.
|
* Comparator for sorting players by career average in ascending order.
|
||||||
* <p>
|
|
||||||
* This comparator implements ascending order sorting, meaning players with lower
|
|
||||||
* career averages will appear before players with higher averages in the sorted list.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Sorting Behavior:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Returns negative if p1's average < p2's average (p1 comes first)</li>
|
|
||||||
* <li>Returns zero if p1's average == p2's average (equal priority)</li>
|
|
||||||
* <li>Returns positive if p1's average > p2's average (p2 comes first)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage:</strong>
|
|
||||||
* This comparator is used in {@link #updateMatch(Match)} to sort the player list
|
|
||||||
* before displaying it in the RecyclerView.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see Comparator
|
|
||||||
* @see Player#careerAverage
|
|
||||||
* @see #updateMatch(Match)
|
|
||||||
*/
|
*/
|
||||||
public static class PlayerScoreComparator implements Comparator<Player> {
|
public static class PlayerScoreComparator implements Comparator<Player> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compares two Player objects based on their career average scores.
|
* Compares two players by career average.
|
||||||
* <p>
|
*
|
||||||
* Uses {@link Double#compare(double, double)} to perform a numerical comparison
|
* @param p1 First player.
|
||||||
* of the career average values, which properly handles special cases like NaN and infinity.
|
* @param p2 Second player.
|
||||||
* </p>
|
* @return Comparison result.
|
||||||
*
|
|
||||||
* @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.
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int compare(final Player p1, final Player p2) {
|
public int compare(final Player p1, final Player p2) {
|
||||||
|
|||||||
@@ -24,80 +24,24 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RecyclerView adapter for displaying the squad of players in the Main Menu.
|
* RecyclerView adapter for displaying the player squad in Main Menu.
|
||||||
* <p>
|
* Shows player names, career averages, and profile pictures with click-to-edit functionality.
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Key Features:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Displays player name, career average, and profile picture</li>
|
|
||||||
* <li>Supports click-to-edit functionality for each player</li>
|
|
||||||
* <li>Uses custom {@link PlayerItemView} for consistent player display</li>
|
|
||||||
* <li>Integrates with Glide for efficient image loading and caching</li>
|
|
||||||
* <li>Optimized for infrequent updates (typically once per activity lifecycle)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Usage:</strong>
|
|
||||||
* This adapter is used in the Main Menu activity to display the complete squad roster,
|
|
||||||
* allowing users to view and manage their players.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see RecyclerView.Adapter
|
|
||||||
* @see PlayerCardHolder
|
|
||||||
* @see Player
|
|
||||||
* @see AddPlayerActivity
|
|
||||||
* @author Oche Companion Development Team
|
|
||||||
* @version 1.0
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
*/
|
||||||
public class MainMenuPlayerAdapter extends RecyclerView.Adapter<MainMenuPlayerAdapter.PlayerCardHolder> {
|
public class MainMenuPlayerAdapter extends RecyclerView.Adapter<MainMenuPlayerAdapter.PlayerCardHolder> {
|
||||||
|
|
||||||
/**
|
/** Tag for logging. */
|
||||||
* Tag for logging and debugging purposes.
|
|
||||||
* Used to identify log messages originating from this adapter.
|
|
||||||
*/
|
|
||||||
private static final String TAG = "MainMenuPlayerAdapter";
|
private static final String TAG = "MainMenuPlayerAdapter";
|
||||||
|
|
||||||
/**
|
/** List of all players to display. */
|
||||||
* Internal list holding all players to be displayed in the RecyclerView.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see #updatePlayers(List)
|
|
||||||
* @see Player
|
|
||||||
*/
|
|
||||||
private final List<Player> mPlayersList = new ArrayList<>();
|
private final List<Player> mPlayersList = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new {@link PlayerCardHolder} to display player information.
|
* Creates a new ViewHolder with a PlayerItemView.
|
||||||
* <p>
|
*
|
||||||
* This method is called by the RecyclerView when it needs a new ViewHolder to represent
|
* @param parent The parent ViewGroup.
|
||||||
* a player item. The method creates a custom {@link PlayerItemView} and configures its
|
* @param viewType The view type (unused).
|
||||||
* layout parameters to match the parent's width and wrap its content height.
|
* @return A new PlayerCardHolder.
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Layout Configuration:</strong>
|
|
||||||
* <ul>
|
|
||||||
* <li>Width: MATCH_PARENT (fills available width)</li>
|
|
||||||
* <li>Height: WRAP_CONTENT (adjusts to content size)</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param parent The ViewGroup into which the new View will be added after it is bound
|
|
||||||
* to an adapter position.
|
|
||||||
* @param viewType The view type of the new View. Not used in this implementation as
|
|
||||||
* all items use the same view type.
|
|
||||||
* @return A new PlayerCardHolder that holds a PlayerItemView.
|
|
||||||
* @see PlayerCardHolder
|
|
||||||
* @see PlayerItemView
|
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
@@ -115,17 +59,10 @@ public class MainMenuPlayerAdapter extends RecyclerView.Adapter<MainMenuPlayerAd
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds player data to a ViewHolder at the specified position.
|
* Binds player data to the ViewHolder at the specified position.
|
||||||
* <p>
|
*
|
||||||
* This method is called by the RecyclerView to display player data at the specified position.
|
* @param holder The ViewHolder to update.
|
||||||
* It retrieves the player from the internal list and delegates the display logic to the
|
* @param position The position in the data set.
|
||||||
* {@link PlayerCardHolder#setPlayer(Player)} method.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param holder The ViewHolder which should be updated to represent the player
|
|
||||||
* at the given position in the data set.
|
|
||||||
* @param position The position of the player within the adapter's data set.
|
|
||||||
* @see PlayerCardHolder#setPlayer(Player)
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull final PlayerCardHolder holder, final int position) {
|
public void onBindViewHolder(@NonNull final PlayerCardHolder holder, final int position) {
|
||||||
@@ -134,13 +71,9 @@ public class MainMenuPlayerAdapter extends RecyclerView.Adapter<MainMenuPlayerAd
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the total number of players in the data set held by the adapter.
|
* Returns the number of players.
|
||||||
* <p>
|
*
|
||||||
* This method is called by the RecyclerView to determine how many items to display.
|
* @return Player count.
|
||||||
* The count is based on the number of players currently stored in {@link #mPlayersList}.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return The total number of players in the list.
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount() {
|
public int getItemCount() {
|
||||||
@@ -148,28 +81,9 @@ public class MainMenuPlayerAdapter extends RecyclerView.Adapter<MainMenuPlayerAd
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the adapter with a new list of players and refreshes the display.
|
* Updates the adapter with a new list of players.
|
||||||
* <p>
|
*
|
||||||
* This method performs the following operations:
|
* @param players The list of players to display.
|
||||||
* <ol>
|
|
||||||
* <li>Clears the existing player list</li>
|
|
||||||
* <li>Adds all players from the provided list</li>
|
|
||||||
* <li>Notifies the RecyclerView to refresh the display</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Performance Consideration:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param players The list of Player objects to display. Must not be null.
|
|
||||||
* Can be empty to clear the display.
|
|
||||||
* @see Player
|
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
public void updatePlayers(final List<Player> players) {
|
public void updatePlayers(final List<Player> players) {
|
||||||
@@ -193,75 +107,23 @@ public class MainMenuPlayerAdapter extends RecyclerView.Adapter<MainMenuPlayerAd
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewHolder class for displaying individual player items in the squad list.
|
* ViewHolder for displaying player items with click-to-edit functionality.
|
||||||
* <p>
|
|
||||||
* This ViewHolder manages the UI components for a single player entry, including:
|
|
||||||
* <ul>
|
|
||||||
* <li>Player name</li>
|
|
||||||
* <li>Player career average score</li>
|
|
||||||
* <li>Player profile picture/avatar</li>
|
|
||||||
* <li>Click interaction to edit player details</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* The ViewHolder uses a {@link PlayerItemView} as its root view and supports click
|
|
||||||
* events that navigate to the {@link AddPlayerActivity} for editing player information.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Image Loading:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see RecyclerView.ViewHolder
|
|
||||||
* @see PlayerItemView
|
|
||||||
* @see Player
|
|
||||||
* @see AddPlayerActivity
|
|
||||||
*/
|
*/
|
||||||
public static class PlayerCardHolder extends RecyclerView.ViewHolder {
|
public static class PlayerCardHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
/**
|
/** TextView displaying the player's name. */
|
||||||
* TextView displaying the player's username.
|
|
||||||
* Shows the {@link Player#username} field.
|
|
||||||
*/
|
|
||||||
private final TextView mPlayerNameView;
|
private final TextView mPlayerNameView;
|
||||||
|
|
||||||
/**
|
/** TextView displaying the player's career average. */
|
||||||
* TextView displaying the player's career average score.
|
|
||||||
* <p>
|
|
||||||
* Shows the {@link Player#careerAverage} field formatted according to
|
|
||||||
* the string resource {@code R.string.txt_player_average_base}.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
private final TextView mPlayerScoreView;
|
private final TextView mPlayerScoreView;
|
||||||
|
|
||||||
/**
|
/** ShapeableImageView displaying the player's profile picture. */
|
||||||
* ShapeableImageView displaying the player's profile picture.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
private final ShapeableImageView mPlayerImageView;
|
private final ShapeableImageView mPlayerImageView;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new PlayerCardHolder and initializes its child views.
|
* Constructs a new PlayerCardHolder and initializes child views.
|
||||||
* <p>
|
*
|
||||||
* This constructor performs the following setup:
|
* @param itemView The root view (PlayerItemView).
|
||||||
* <ol>
|
|
||||||
* <li>Calls the parent ViewHolder constructor</li>
|
|
||||||
* <li>Retrieves references to the player name, score, and image views</li>
|
|
||||||
* </ol>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Unlike group match items, the chevron icon is kept visible since player items
|
|
||||||
* are interactive and clicking them navigates to the edit screen.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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.
|
|
||||||
*/
|
*/
|
||||||
public PlayerCardHolder(@NonNull final View itemView) {
|
public PlayerCardHolder(@NonNull final View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
@@ -273,35 +135,9 @@ public class MainMenuPlayerAdapter extends RecyclerView.Adapter<MainMenuPlayerAd
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds a Player object to this ViewHolder, updating all displayed information.
|
* Binds player data to this ViewHolder and sets up click listener.
|
||||||
* <p>
|
*
|
||||||
* This method updates the UI components with the player's data:
|
* @param player The player to display.
|
||||||
* <ul>
|
|
||||||
* <li><strong>Click Listener:</strong> Sets up navigation to edit player details</li>
|
|
||||||
* <li><strong>Name:</strong> Sets the player's username in the name TextView</li>
|
|
||||||
* <li><strong>Score:</strong> Formats and displays the career average using a string resource</li>
|
|
||||||
* <li><strong>Avatar:</strong> Loads the profile picture using Glide, or shows a default icon</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Interaction:</strong>
|
|
||||||
* When the item is clicked, it launches {@link AddPlayerActivity} with the player's ID,
|
|
||||||
* allowing the user to edit the player's information.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Image Loading Strategy:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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)
|
|
||||||
*/
|
*/
|
||||||
public void setPlayer(final Player player) {
|
public void setPlayer(final Player player) {
|
||||||
Log.d(TAG, "setPlayer() called with: player = [" + player + "]");
|
Log.d(TAG, "setPlayer() called with: player = [" + player + "]");
|
||||||
@@ -330,25 +166,10 @@ public class MainMenuPlayerAdapter extends RecyclerView.Adapter<MainMenuPlayerAd
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launches the AddPlayerActivity to edit the specified player's information.
|
* Launches AddPlayerActivity to edit the player's information.
|
||||||
* <p>
|
*
|
||||||
* This helper method creates an intent to start {@link AddPlayerActivity} in edit mode,
|
* @param context The context to launch activity from.
|
||||||
* passing the player's ID as an extra. The activity will load the player's existing data
|
* @param player The player to edit.
|
||||||
* and allow the user to modify their username and profile picture.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* <strong>Intent Configuration:</strong>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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
|
|
||||||
*/
|
*/
|
||||||
private void startEditPlayerActivity(final Context context, final Player player) {
|
private void startEditPlayerActivity(final Context context, final Player player) {
|
||||||
// Create intent for AddPlayerActivity
|
// Create intent for AddPlayerActivity
|
||||||
|
|||||||
Reference in New Issue
Block a user