Refactored GameActivity

Refactored GameActivity to make use of the newly introduced GameManager to centralize the Game logic into one class and shrink the GameActivity to the relevant parts.
This commit is contained in:
Alexander Doerflinger
2026-02-06 08:09:53 +01:00
parent 039350e988
commit 4b8766b304
22 changed files with 1236 additions and 976 deletions

View File

@@ -27,10 +27,6 @@
<activity android:name=".BaseActivity"
android:exported="false"
android:configChanges="uiMode"/>
<activity
android:name=".TestActivity"
android:exported="false"
android:configChanges="uiMode" />
<activity
android:name=".SettingsActivity"
android:exported="false"

View File

@@ -18,11 +18,12 @@ import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
@@ -211,6 +212,13 @@ public class AddPlayerActivity extends BaseActivity {
// Set up touch gesture handlers for image cropping
setupGestures();
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
handleBackPressed();
}
});
// Check if editing an existing player
if (getIntent().hasExtra(EXTRA_PLAYER_ID)) {
mExistingPlayerId = getIntent().getLongExtra(EXTRA_PLAYER_ID, -1);
@@ -218,15 +226,13 @@ public class AddPlayerActivity extends BaseActivity {
}
}
@Override
public void onBackPressed() {
Log.d(TAG, "onBackPressed() called with StatsView shown = [" + mIsStatsViewShown + "]");
private void handleBackPressed() {
if (mIsStatsViewShown) {
mPlayerStatsView.setVisibility(View.GONE);
mIsStatsViewShown = false;
return;
}
super.onBackPressed();
finish();
}
/**

View File

@@ -5,6 +5,7 @@ import android.content.res.Configuration;
import android.os.Bundle;
import com.aldo.apps.ochecompanion.utils.Log;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.preference.PreferenceManager;
@@ -64,7 +65,7 @@ public abstract class BaseActivity extends AppCompatActivity {
}
@Override
public void onConfigurationChanged(final Configuration newConfig) {
public void onConfigurationChanged(final @NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.d(TAG, "========================================");

View File

@@ -4,7 +4,6 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import com.aldo.apps.ochecompanion.utils.Log;
import android.view.View;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
@@ -15,7 +14,6 @@ import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.aldo.apps.ochecompanion.database.AppDatabase;
import com.aldo.apps.ochecompanion.database.DatabaseHelper;
import com.aldo.apps.ochecompanion.database.objects.Player;
import com.aldo.apps.ochecompanion.database.objects.Match;
@@ -25,7 +23,6 @@ import com.aldo.apps.ochecompanion.ui.adapter.MainMenuPlayerAdapter;
import com.aldo.apps.ochecompanion.utils.DartsConstants;
import com.aldo.apps.ochecompanion.utils.UIConstants;
import java.util.ArrayList;
import java.util.List;
/**
@@ -93,7 +90,7 @@ public class MainMenuActivity extends BaseActivity {
quickStartBtn.setOnClickListener(v -> quickStart());
findViewById(R.id.btnSettings).setOnClickListener(v -> launchSettings());
final List<Match> ongoingMatches = (List<Match>) mDatabaseHelper.getOngoingMatches();
final List<Match> ongoingMatches = mDatabaseHelper.getOngoingMatches();
if (ongoingMatches != null && !ongoingMatches.isEmpty()) {
mOngoingMatch = ongoingMatches.get(0);
}
@@ -110,8 +107,6 @@ public class MainMenuActivity extends BaseActivity {
mTestCounter++;
new Thread(() -> mDatabaseHelper.printAllMatches()).start();
});
findViewById(R.id.title_view).setOnClickListener(v -> startActivity(new Intent(MainMenuActivity.this, TestActivity.class)));
}
/**
@@ -178,7 +173,7 @@ public class MainMenuActivity extends BaseActivity {
startActivity(intent);
});
new Thread(() -> {
final List<Player> allPlayers = (List<Player>) mDatabaseHelper.getAllPlayers();
final List<Player> allPlayers = mDatabaseHelper.getAllPlayers();
runOnUiThread(() -> adapter.updatePlayers(allPlayers));
}).start();

View File

@@ -3,7 +3,6 @@ package com.aldo.apps.ochecompanion;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

View File

@@ -1,57 +0,0 @@
package com.aldo.apps.ochecompanion;
import android.os.Bundle;
import com.aldo.apps.ochecompanion.utils.Log;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.aldo.apps.ochecompanion.database.AppDatabase;
import com.aldo.apps.ochecompanion.database.objects.Player;
import com.aldo.apps.ochecompanion.database.objects.Statistics;
import com.aldo.apps.ochecompanion.ui.HeatmapView;
import java.util.List;
public class TestActivity extends BaseActivity {
private static final String TAG = "TestActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_test);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
final HeatmapView heatmap = findViewById(R.id.heatmap);
new Thread(() -> {
// Access the singleton database and query all players
final List<Player> allPlayers = AppDatabase.getDatabase(getApplicationContext())
.playerDao()
.getAllPlayers();
if (allPlayers == null || allPlayers.isEmpty()) {
Log.d(TAG, "onCreate: Cannot continue");
return;
}
final Player firstPlayer = allPlayers.get(0);
final Statistics stats = AppDatabase.getDatabase(this)
.statisticsDao()
.getStatisticsForPlayer(firstPlayer.id);
runOnUiThread(() -> {
Log.d(TAG, "onCreate: Applying stats [" + stats + "]");
heatmap.setStats(stats);
});
}).start();
}
}

View File

@@ -25,7 +25,7 @@ import com.aldo.apps.ochecompanion.utils.converters.HitDistributionConverter;
* @see Player
* @see Match
*/
@Database(entities = {Player.class, Match.class, Statistics.class}, version = 12, exportSchema = false)
@Database(entities = {Player.class, Match.class, Statistics.class}, version = 14, exportSchema = false)
@TypeConverters({HitDistributionConverter.class})
public abstract class AppDatabase extends RoomDatabase {

View File

@@ -6,6 +6,8 @@ import com.aldo.apps.ochecompanion.utils.Log;
import com.aldo.apps.ochecompanion.database.objects.Match;
import com.aldo.apps.ochecompanion.database.objects.Player;
import com.aldo.apps.ochecompanion.database.objects.Statistics;
import com.aldo.apps.ochecompanion.utils.MatchProgress;
import com.aldo.apps.ochecompanion.utils.converters.MatchProgressConverter;
import java.util.ArrayList;
import java.util.HashMap;
@@ -292,16 +294,9 @@ public class DatabaseHelper {
*
* @return List of all players, or empty list if none exist
*/
public List<?> getAllPlayers() {
public List<Player> getAllPlayers() {
try {
return mExecutor.submit(() -> {
try {
return mDatabase.playerDao().getAllPlayers();
} catch (Exception e) {
Log.e(TAG, "getAllPlayers: Failed to retrieve players", e);
return new java.util.ArrayList<>();
}
}).get();
return mExecutor.submit(() -> mDatabase.playerDao().getAllPlayers()).get();
} catch (Exception e) {
Log.e(TAG, "getAllPlayers: Failed to submit task", e);
return new java.util.ArrayList<>();
@@ -309,7 +304,45 @@ public class DatabaseHelper {
}
public long createNewMatch(final String gameMode, final List<Player> players) {
final Match match = new Match(System.currentTimeMillis(), gameMode, players.size(), null, Match.MatchState.ONGOING);
// Parse starting score from gameMode string
int startingScore = 501; // Default
try {
startingScore = Integer.parseInt(gameMode);
} catch (NumberFormatException e) {
Log.w(TAG, "createNewMatch: Could not parse gameMode as integer, using default 501");
}
// Create initial MatchProgress with player data
final MatchProgress initialProgress = new MatchProgress();
initialProgress.activePlayerIndex = 0; // First player starts
initialProgress.startingScore = startingScore;
initialProgress.players = new ArrayList<>();
// Create player state snapshots with initial values
if (players != null && !players.isEmpty()) {
for (Player player : players) {
initialProgress.players.add(new MatchProgress.PlayerStateSnapshot(
player.id,
player.username,
startingScore, // Initial score equals starting score
0 // No darts thrown yet
));
}
} else {
// Create guest player if no players provided
initialProgress.players.add(new MatchProgress.PlayerStateSnapshot(
0L, // Guest has ID 0
"GUEST",
startingScore,
0
));
}
// Convert to JSON
final String participantData = MatchProgressConverter.fromProgress(initialProgress);
final Match match = new Match(System.currentTimeMillis(), gameMode,
initialProgress.players.size(), participantData, Match.MatchState.ONGOING);
try {
return mExecutor.submit(() -> {
try {
@@ -340,16 +373,9 @@ public class DatabaseHelper {
});
}
public List<?> getOngoingMatches() {
public List<Match> getOngoingMatches() {
try {
return mExecutor.submit(() -> {
try {
return mDatabase.matchDao().getOngoingMatches();
} catch (Exception e) {
Log.d(TAG, "getOngoingMatch() failed");
return new ArrayList<>();
}
}).get();
return mExecutor.submit(() -> mDatabase.matchDao().getOngoingMatches()).get();
} catch (Exception e) {
Log.e(TAG, "getOngoingMatch: Failed fetching ongoing matches");
return new ArrayList<>();

View File

@@ -5,7 +5,6 @@ import static androidx.room.OnConflictStrategy.REPLACE;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Update;

View File

@@ -1,12 +1,10 @@
package com.aldo.apps.ochecompanion.database.objects;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import com.aldo.apps.ochecompanion.utils.DartsConstants;
import com.aldo.apps.ochecompanion.utils.MatchProgress;
import com.aldo.apps.ochecompanion.utils.converters.MatchProgressConverter;
@@ -267,7 +265,7 @@ public class Match implements Serializable {
participant.put("username", player.username);
participant.put("photoUri", player.profilePictureUri);
participant.put("careerAverage", player.careerAverage);
final int score = (scores != null && scores.containsKey(player.id)) ? scores.get(player.id) : 0;
final int score = (scores != null && scores.containsKey((int) player.id)) ? scores.get((int) player.id) : 0;
participant.put("score", score);
participants.put(participant);
}
@@ -361,6 +359,7 @@ public class Match implements Serializable {
}
@Override
@NonNull
public String toString() {
return "Match{" +
"id=" + id +

View File

@@ -5,7 +5,6 @@ import com.aldo.apps.ochecompanion.utils.Log;
import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
import androidx.room.TypeConverters;
import com.aldo.apps.ochecompanion.utils.converters.HitDistributionConverter;

View File

@@ -0,0 +1,699 @@
package com.aldo.apps.ochecompanion.game;
import android.content.Context;
import com.aldo.apps.ochecompanion.database.DatabaseHelper;
import com.aldo.apps.ochecompanion.database.objects.Match;
import com.aldo.apps.ochecompanion.database.objects.Player;
import com.aldo.apps.ochecompanion.utils.DartsConstants;
import com.aldo.apps.ochecompanion.utils.Log;
import com.aldo.apps.ochecompanion.utils.MatchProgress;
import com.aldo.apps.ochecompanion.utils.converters.MatchProgressConverter;
import java.util.ArrayList;
import java.util.List;
/**
* GameManager: Singleton manager for handling all X01 game business logic.
* <p>
* This class serves as the central data pool and business logic handler for an active darts match.
* It manages:
* - Match state (scores, active player, dart throws)
* - Database operations (loading/saving match progress)
* - Game rules (bust detection, double-out, win conditions)
* - Statistics tracking
* <p>
* The GameManager decouples business logic from UI, making GameActivity a simple view controller
* that only handles UI updates via the GameStateCallback interface.
*/
public class GameManager {
private static final String TAG = "GameManager";
// Singleton instance
private static GameManager sInstance;
// Dependencies
private final DatabaseHelper mDatabaseHelper;
private GameStateCallback mCallback;
// Game State
private int mMatchId = -1;
private int mStartingScore = DartsConstants.DEFAULT_GAME_SCORE;
private int mActivePlayerIndex = 0;
private int mMultiplier = 1;
private final List<PlayerState> mPlayerStates = new ArrayList<>();
private final List<Integer> mCurrentTurnDarts = new ArrayList<>();
private final List<DartHit> mCurrentTurnDartHits = new ArrayList<>();
private boolean mIsTurnOver = false;
private boolean mIsBustedTurn = false;
private boolean mIsMatchCompleted = false;
/**
* Callback interface for communicating game state changes to the UI layer.
*/
public interface GameStateCallback {
/**
* Called when the game state has changed and UI should be refreshed.
*/
void onGameStateChanged();
/**
* Called when the turn indicators (dart pills) should be updated.
*/
void onTurnIndicatorsChanged();
/**
* Called when the multiplier has changed.
* @param multiplier The new multiplier value (1=Single, 2=Double, 3=Triple)
*/
void onMultiplierChanged(int multiplier);
/**
* Called when a bust occurs.
*/
void onBust();
/**
* Called when a player wins the match.
* @param winner The winning player's state
* @param checkoutValue The final dart value that won the game
*/
void onPlayerWin(PlayerState winner, int checkoutValue);
/**
* Called when a perfect 180 is scored.
*/
void onOneEightyScored();
/**
* Called to reset visual effects after a bust.
*/
void onResetVisuals();
}
/**
* Represents a single dart hit with its base value and multiplier.
*/
public static class DartHit {
public final int baseValue;
public final int multiplier;
public DartHit(final int baseValue, final int multiplier) {
this.baseValue = baseValue;
this.multiplier = multiplier;
}
}
/**
* State holder for a single player's X01 game progress.
*/
public static class PlayerState {
public final Player player;
public final long playerId;
public final String name;
public int remainingScore;
public int dartsThrown = 0;
public PlayerState(final Player player, final int startScore) {
this.player = player;
this.playerId = player.id;
this.name = player.username;
this.remainingScore = startScore;
}
}
/**
* Private constructor to enforce singleton pattern.
*/
private GameManager(final Context context) {
mDatabaseHelper = DatabaseHelper.getInstance(context);
}
/**
* Gets the singleton instance of GameManager.
*
* @param context Application or Activity context
* @return The singleton GameManager instance
*/
public static synchronized GameManager getInstance(final Context context) {
if (sInstance == null) {
sInstance = new GameManager(context.getApplicationContext());
}
return sInstance;
}
/**
* Registers a callback to receive game state updates.
*
* @param callback The callback to register
*/
public void setCallback(final GameStateCallback callback) {
mCallback = callback;
}
/**
* Initializes a new game or loads an existing match from the database.
* This should be called when starting/resuming a match.
*
* @param matchId The match ID to load, or -1 to create a new match
* @param startingScore The starting score (501, 301, etc.)
* @param onComplete Callback when initialization is complete
*/
public void initializeMatch(final int matchId, final int startingScore, final Runnable onComplete) {
mStartingScore = startingScore;
mMatchId = matchId;
new Thread(() -> {
final List<Player> allPlayers = mDatabaseHelper.getAllPlayers();
Log.d(TAG, "initializeMatch: Loading players, count = " + (allPlayers != null ? allPlayers.size() : 0));
Match match = null;
if (matchId > 0) {
// Try to load existing match
match = mDatabaseHelper.getMatchById(matchId);
Log.d(TAG, "initializeMatch: Loaded match from DB: " + match);
if (match != null && match.participantData != null && !match.participantData.isEmpty()) {
// Load match progress from database
try {
final MatchProgress progress = MatchProgressConverter.fromString(match.participantData);
if (progress != null) {
Log.d(TAG, "initializeMatch: Found saved progress with " + progress.players.size() + " players");
// Initialize player states
initializePlayerStates(allPlayers);
loadMatchProgress(progress);
if (onComplete != null) {
onComplete.run();
}
notifyGameStateChanged();
return;
} else {
Log.w(TAG, "initializeMatch: Progress is null, treating as new match");
match = null;
}
} catch (Exception e) {
Log.e(TAG, "initializeMatch: Failed to load match progress", e);
match = null;
}
}
}
// Create new match if not found or invalid
if (match == null) {
final long newMatchId = mDatabaseHelper.createNewMatch(String.valueOf(startingScore), allPlayers);
if (newMatchId > 0) {
mMatchId = (int) newMatchId;
Log.d(TAG, "initializeMatch: Created new match with ID: " + mMatchId);
} else {
Log.e(TAG, "initializeMatch: Failed to create new match");
}
// Setup new game
initializePlayerStates(allPlayers);
}
if (onComplete != null) {
onComplete.run();
}
notifyGameStateChanged();
}).start();
}
/**
* Initializes player states from the provided player list.
*/
private void initializePlayerStates(final List<Player> players) {
mPlayerStates.clear();
if (players != null && !players.isEmpty()) {
for (Player p : players) {
mPlayerStates.add(new PlayerState(p, mStartingScore));
}
} else {
// Create guest player if no players available
final Player guest = new Player("GUEST", null);
mPlayerStates.add(new PlayerState(guest, mStartingScore));
}
}
/**
* Loads match progress from a saved state.
*/
private void loadMatchProgress(final MatchProgress progress) {
if (progress == null || mPlayerStates.isEmpty()) return;
Log.d(TAG, "loadMatchProgress: Loading saved match progress");
// Restore active player index
mActivePlayerIndex = progress.activePlayerIndex;
// Restore player scores and darts thrown
for (int i = 0; i < progress.players.size() && i < mPlayerStates.size(); i++) {
MatchProgress.PlayerStateSnapshot snapshot = progress.players.get(i);
PlayerState state = mPlayerStates.get(i);
state.remainingScore = snapshot.remainingScore;
state.dartsThrown = snapshot.dartsThrown;
}
Log.d(TAG, "loadMatchProgress: Match progress loaded successfully");
}
/**
* Processes a dart throw when a keyboard number is tapped.
*
* @param baseValue Face value of the number hit (1-20 or 25 for Bull)
*/
public void onNumberTap(final int baseValue) {
if (mCurrentTurnDarts.size() >= 3 || mIsTurnOver) return;
int points = baseValue * mMultiplier;
if (baseValue == DartsConstants.BULL_VALUE && mMultiplier == DartsConstants.MULTIPLIER_TRIPLE) {
points = DartsConstants.DOUBLE_BULL_VALUE; // Triple Bull is Double Bull
}
PlayerState active = mPlayerStates.get(mActivePlayerIndex);
int scoreBeforeDart = active.remainingScore;
for (int d : mCurrentTurnDarts) scoreBeforeDart -= d;
int scoreAfterDart = scoreBeforeDart - points;
boolean isDouble = (mMultiplier == DartsConstants.MULTIPLIER_DOUBLE) || (points == DartsConstants.DOUBLE_BULL_VALUE);
// --- DOUBLE OUT LOGIC CHECK ---
if (scoreAfterDart < 0 || scoreAfterDart == DartsConstants.BUST_SCORE || (scoreAfterDart == 0 && !isDouble)) {
// BUST CONDITION
mCurrentTurnDarts.add(points);
mCurrentTurnDartHits.add(new DartHit(baseValue, mMultiplier));
// Track double-out miss if trying to finish but failed
if (scoreBeforeDart <= 40 && isDouble) {
trackDoubleAttempt(active, true);
}
mIsTurnOver = true;
mIsBustedTurn = true;
notifyTurnIndicatorsChanged();
notifyBust();
} else if (scoreAfterDart == 0 && isDouble) {
// VICTORY CONDITION
mCurrentTurnDarts.add(points);
mCurrentTurnDartHits.add(new DartHit(baseValue, mMultiplier));
// Track successful double-out
trackDoubleAttempt(active, false);
mIsTurnOver = true;
notifyTurnIndicatorsChanged();
handleWin(active);
} else {
// VALID THROW
mCurrentTurnDarts.add(points);
mCurrentTurnDartHits.add(new DartHit(baseValue, mMultiplier));
notifyTurnIndicatorsChanged();
notifyGameStateChanged();
if (mCurrentTurnDarts.size() == DartsConstants.MAX_DARTS_PER_TURN) {
mIsTurnOver = true;
}
}
setMultiplier(DartsConstants.MULTIPLIER_SINGLE);
}
/**
* Handles the win condition when a player finishes on zero with a double.
*/
private void handleWin(final PlayerState winner) {
final int dartsThrown = mCurrentTurnDarts.size();
int pointsMade = 0;
for (int d : mCurrentTurnDarts) pointsMade += d;
final int checkoutValue = mCurrentTurnDarts.get(mCurrentTurnDarts.size() - 1);
// Update statistics
updatePlayerStats(winner, dartsThrown, pointsMade, false, checkoutValue);
// Record dart hits
recordTurnHitsToStatistics(winner, new ArrayList<>(mCurrentTurnDartHits));
// Increment matches played for all players
incrementMatchesPlayed();
// Clear turn state
mCurrentTurnDarts.clear();
mCurrentTurnDartHits.clear();
// Save completed match
saveCompletedMatch(winner);
// Mark match as completed
mIsMatchCompleted = true;
// Notify UI
if (mCallback != null) {
mCallback.onPlayerWin(winner, checkoutValue);
}
}
/**
* Submits the current turn and advances to the next player.
*/
public void submitTurn() {
// Don't submit if no darts thrown
if (mCurrentTurnDarts.isEmpty()) return;
// Calculate turn total
int turnTotal = 0;
for (int d : mCurrentTurnDarts) turnTotal += d;
PlayerState active = mPlayerStates.get(mActivePlayerIndex);
// Calculate final score
int finalScore = active.remainingScore - turnTotal;
// Check for 180
if (finalScore > 0 && turnTotal == 180) {
notifyOneEighty();
}
boolean isBust = mIsBustedTurn;
// Update score only if not bust
if (!isBust) {
active.remainingScore = finalScore;
active.dartsThrown += mCurrentTurnDarts.size();
}
updatePlayerStats(active, mCurrentTurnDarts.size(), turnTotal, isBust);
// Record dart hits
recordTurnHitsToStatistics(active, new ArrayList<>(mCurrentTurnDartHits));
// Rotate to next player
mActivePlayerIndex = (mActivePlayerIndex + 1) % mPlayerStates.size();
// Reset turn state
mCurrentTurnDarts.clear();
mCurrentTurnDartHits.clear();
mIsTurnOver = false;
mIsBustedTurn = false;
// Save progress
saveMatchProgress();
// Notify UI
notifyResetVisuals();
notifyGameStateChanged();
notifyTurnIndicatorsChanged();
}
/**
* Removes the most recently thrown dart from current turn.
*/
public void undoLastDart() {
if (!mCurrentTurnDarts.isEmpty()) {
mCurrentTurnDarts.remove(mCurrentTurnDarts.size() - 1);
mCurrentTurnDartHits.remove(mCurrentTurnDartHits.size() - 1);
mIsTurnOver = false;
mIsBustedTurn = false;
notifyResetVisuals();
notifyTurnIndicatorsChanged();
notifyGameStateChanged();
}
}
/**
* Sets the current multiplier.
*
* @param multiplier The multiplier value (1=Single, 2=Double, 3=Triple)
*/
public void setMultiplier(final int multiplier) {
mMultiplier = multiplier;
if (mCallback != null) {
mCallback.onMultiplierChanged(multiplier);
}
}
/**
* Saves the current match progress to the database.
*/
private void saveMatchProgress() {
final MatchProgress progress = new MatchProgress();
progress.activePlayerIndex = mActivePlayerIndex;
progress.startingScore = mStartingScore;
progress.players = new ArrayList<>();
for (PlayerState state : mPlayerStates) {
progress.players.add(new MatchProgress.PlayerStateSnapshot(
state.playerId,
state.name,
state.remainingScore,
state.dartsThrown
));
}
String progressJson = MatchProgressConverter.fromProgress(progress);
if (mMatchId > 0) {
new Thread(() -> {
try {
Match match = mDatabaseHelper.getMatchById(mMatchId);
if (match != null) {
match.participantData = progressJson;
match.timestamp = System.currentTimeMillis();
mDatabaseHelper.updateMatch(match);
Log.d(TAG, "saveMatchProgress: Saved match progress for match ID: " + mMatchId);
} else {
Log.e(TAG, "saveMatchProgress: Match not found with ID: " + mMatchId);
}
} catch (Exception e) {
Log.e(TAG, "saveMatchProgress: Failed to save progress", e);
}
}).start();
}
}
/**
* Saves the completed match to the database.
*/
private void saveCompletedMatch(final PlayerState winner) {
if (mMatchId <= 0) return;
new Thread(() -> {
try {
Match match = mDatabaseHelper.getMatchById(mMatchId);
if (match != null) {
match.state = Match.MatchState.COMPLETED;
match.timestamp = System.currentTimeMillis();
final MatchProgress finalProgress = new MatchProgress();
finalProgress.activePlayerIndex = mActivePlayerIndex;
finalProgress.startingScore = mStartingScore;
finalProgress.players = new ArrayList<>();
for (PlayerState state : mPlayerStates) {
finalProgress.players.add(new MatchProgress.PlayerStateSnapshot(
state.playerId,
state.name,
state.remainingScore,
state.dartsThrown
));
}
match.participantData = MatchProgressConverter.fromProgress(finalProgress);
mDatabaseHelper.updateMatch(match);
Log.d(TAG, "saveCompletedMatch: Match " + mMatchId + " marked as completed");
} else {
Log.e(TAG, "saveCompletedMatch: Match not found with ID: " + mMatchId);
}
} catch (Exception e) {
Log.e(TAG, "saveCompletedMatch: Failed to save completed match", e);
}
}).start();
}
/**
* Updates player statistics in the database.
*/
private void updatePlayerStats(final PlayerState active, final int dartsThrown, final int pointsMade,
final boolean wasBust) {
updatePlayerStats(active, dartsThrown, pointsMade, wasBust, 0);
}
/**
* Updates player statistics in the database with optional checkout value.
*/
private void updatePlayerStats(final PlayerState active, final int dartsThrown, final int pointsMade,
final boolean wasBust, final int checkoutValue) {
if (active.player != null && active.player.id != 0) {
new Thread(() -> mDatabaseHelper.updatePlayerStatistics(
active.player.id,
dartsThrown,
pointsMade,
wasBust,
checkoutValue,
active.dartsThrown
)).start();
}
}
/**
* Tracks a double-out attempt in player statistics.
*/
private void trackDoubleAttempt(final PlayerState playerState, final boolean isMissed) {
new Thread(() -> mDatabaseHelper.trackDoubleAttempt(playerState.playerId, isMissed)).start();
}
/**
* Increments matches played counter for all players.
*/
private void incrementMatchesPlayed() {
final List<Long> playerIds = new ArrayList<>();
for (PlayerState playerState : mPlayerStates) {
playerIds.add(playerState.playerId);
}
new Thread(() -> mDatabaseHelper.incrementMatchesPlayed(playerIds)).start();
}
/**
* Records dart hits to player statistics.
*/
private void recordTurnHitsToStatistics(final PlayerState playerState, final List<DartHit> dartHits) {
if (dartHits.isEmpty()) return;
final List<DatabaseHelper.DartHit> dbDartHits = new ArrayList<>();
for (DartHit hit : dartHits) {
dbDartHits.add(new DatabaseHelper.DartHit(hit.baseValue, hit.multiplier));
}
new Thread(() -> mDatabaseHelper.recordDartHits(playerState.playerId, dbDartHits)).start();
}
/**
* Resets the game state for a new match.
* This clears all current match data but keeps the singleton instance alive.
*/
public void resetGame() {
mMatchId = -1;
mActivePlayerIndex = 0;
mMultiplier = 1;
mPlayerStates.clear();
mCurrentTurnDarts.clear();
mCurrentTurnDartHits.clear();
mIsTurnOver = false;
mIsBustedTurn = false;
mIsMatchCompleted = false;
}
// ========================================================================================
// Getters for Game State
// ========================================================================================
public int getMatchId() {
return mMatchId;
}
public int getStartingScore() {
return mStartingScore;
}
public int getActivePlayerIndex() {
return mActivePlayerIndex;
}
public int getMultiplier() {
return mMultiplier;
}
public List<PlayerState> getPlayerStates() {
return new ArrayList<>(mPlayerStates);
}
public PlayerState getActivePlayer() {
if (mPlayerStates.isEmpty()) return null;
return mPlayerStates.get(mActivePlayerIndex);
}
public List<Integer> getCurrentTurnDarts() {
return new ArrayList<>(mCurrentTurnDarts);
}
public List<DartHit> getCurrentTurnDartHits() {
return new ArrayList<>(mCurrentTurnDartHits);
}
public boolean isTurnOver() {
return mIsTurnOver;
}
public boolean isBustedTurn() {
return mIsBustedTurn;
}
public boolean isMatchCompleted() {
return mIsMatchCompleted;
}
/**
* Calculates the current target score (remaining - current turn darts).
* If turn is busted, returns the remaining score without subtracting bust darts.
*/
public int getCurrentTarget() {
PlayerState active = getActivePlayer();
if (active == null) return 0;
if (mIsBustedTurn) {
return active.remainingScore;
}
int turnPointsSoFar = 0;
for (int d : mCurrentTurnDarts) turnPointsSoFar += d;
return active.remainingScore - turnPointsSoFar;
}
/**
* Gets the number of darts remaining in the current turn.
*/
public int getDartsRemainingInTurn() {
return 3 - mCurrentTurnDarts.size();
}
// ========================================================================================
// Callback Notification Methods
// ========================================================================================
private void notifyGameStateChanged() {
if (mCallback != null) {
mCallback.onGameStateChanged();
}
}
private void notifyTurnIndicatorsChanged() {
if (mCallback != null) {
mCallback.onTurnIndicatorsChanged();
}
}
private void notifyBust() {
if (mCallback != null) {
mCallback.onBust();
}
}
private void notifyOneEighty() {
if (mCallback != null) {
mCallback.onOneEightyScored();
}
}
private void notifyResetVisuals() {
if (mCallback != null) {
mCallback.onResetVisuals();
}
}
}

View File

@@ -65,4 +65,20 @@ public class PlayerItemView extends MaterialCardView {
mIvAvatar.setImageResource(R.drawable.ic_users);
}
}
public void bindWithScore(@NonNull final Player player, final int score) {
mTvUsername.setText(player.username);
// Display match score instead of career average
mTvStats.setText(String.valueOf(score));
if (player.profilePictureUri != null) {
Glide.with(getContext())
.load(player.profilePictureUri)
.into(mIvAvatar);
} else {
mIvAvatar.setImageResource(R.drawable.ic_users);
}
}
}

View File

@@ -2,6 +2,7 @@ package com.aldo.apps.ochecompanion.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.widget.ScrollView;
import android.widget.TextView;
@@ -19,6 +20,8 @@ import com.google.android.material.imageview.ShapeableImageView;
*/
public class PlayerStatsView extends ScrollView {
private static final String TAG = "PlayerStatsView";
// UI References
private HeatmapView mHeatmap;
private ShapeableImageView mIvAvatar;
@@ -55,6 +58,10 @@ public class PlayerStatsView extends ScrollView {
* Binds both the player identity and their accumulated stats to the UI.
*/
public void bind(@NonNull final Player player, final @NonNull Statistics stats) {
if (player == null || stats == null) {
Log.e(TAG, "bind: Cannot bind, return");
return;
}
// 1. Identity
mTvUsername.setText(player.username.toUpperCase());
if (player.profilePictureUri != null) {

View File

@@ -113,27 +113,19 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGrou
*/
public static class GroupMatchHolder extends RecyclerView.ViewHolder {
/** TextView displaying the player's name. */
private final TextView mPlayerNameView;
/** TextView displaying the player's career average. */
private final TextView mPlayerScoreView;
/** ShapeableImageView displaying the player's profile picture. */
private final ShapeableImageView mPlayerImageView;
/**
* The underlying {@link PlayerItemView} to be populated.
*/
private final PlayerItemView mItemView;
/**
* Constructs a new GroupMatchHolder and initializes child views.
*
* @param itemView The root view (PlayerItemView).
*/
public GroupMatchHolder(@NonNull final View itemView) {
public GroupMatchHolder(@NonNull final PlayerItemView itemView) {
super(itemView);
// Initialize references to child views
mPlayerNameView = itemView.findViewById(R.id.tvPlayerName);
mPlayerScoreView = itemView.findViewById(R.id.tvPlayerAvg);
mPlayerImageView = itemView.findViewById(R.id.ivPlayerProfile);
mItemView = itemView;
// Hide the chevron icon as group match items are not interactive
itemView.findViewById(R.id.ivChevron).setVisibility(View.GONE);
@@ -147,23 +139,7 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGrou
public void setParticipant(final Match.ParticipantData participantData) {
final Player player = participantData.player;
final int score = participantData.score;
// Set player name
mPlayerNameView.setText(player.username);
// Display match score instead of career average
mPlayerScoreView.setText(String.valueOf(score));
// Load profile picture or show default icon
if (player.profilePictureUri != null) {
// Use Glide to load image from URI with caching and memory management
Glide.with(itemView.getContext())
.load(player.profilePictureUri)
.into(mPlayerImageView);
} else {
// No profile picture available - show default user icon
mPlayerImageView.setImageResource(R.drawable.ic_users);
}
mItemView.bindWithScore(player, score);
}
}
}

View File

@@ -111,27 +111,19 @@ public class MainMenuPlayerAdapter extends RecyclerView.Adapter<MainMenuPlayerAd
*/
public static class PlayerCardHolder extends RecyclerView.ViewHolder {
/** TextView displaying the player's name. */
private final TextView mPlayerNameView;
/** TextView displaying the player's career average. */
private final TextView mPlayerScoreView;
/** ShapeableImageView displaying the player's profile picture. */
private final ShapeableImageView mPlayerImageView;
/**
* The underlying {@link PlayerItemView} to be populated.
*/
private final PlayerItemView mItemView;
/**
* Constructs a new PlayerCardHolder and initializes child views.
*
* @param itemView The root view (PlayerItemView).
*/
public PlayerCardHolder(@NonNull final View itemView) {
public PlayerCardHolder(@NonNull final PlayerItemView itemView) {
super(itemView);
// Initialize references to child views
mPlayerNameView = itemView.findViewById(R.id.tvPlayerName);
mPlayerScoreView = itemView.findViewById(R.id.tvPlayerAvg);
mPlayerImageView = itemView.findViewById(R.id.ivPlayerProfile);
mItemView = itemView;
}
/**
@@ -144,25 +136,7 @@ public class MainMenuPlayerAdapter extends RecyclerView.Adapter<MainMenuPlayerAd
// Set up click listener to navigate to edit player screen
itemView.setOnClickListener(v -> startEditPlayerActivity(itemView.getContext(), player));
// Set player name
mPlayerNameView.setText(player.username);
// Format and set career average score
mPlayerScoreView.setText(String.format(
itemView.getContext().getString(R.string.txt_player_average_base),
player.careerAverage));
// Load profile picture or show default icon
if (player.profilePictureUri != null) {
// Use Glide to load image from URI with caching and memory management
Glide.with(itemView.getContext())
.load(player.profilePictureUri)
.into(mPlayerImageView);
} else {
// No profile picture available - show default user icon
mPlayerImageView.setImageResource(R.drawable.ic_users);
}
mItemView.bind(player);
}
/**

View File

@@ -3,7 +3,7 @@ package com.aldo.apps.ochecompanion.utils;
/**
* Class {@link Log} is a wrapper class around android.util.Log.
*
* <p>
* The sole purpose of this class is to have a single TAG by which all log output from the
* CoreSyncService can later on be found in the log. The classes using this logging class may
* still define their custom tag. This will ease identifying OcheCompanion logs.

View File

@@ -7,7 +7,6 @@ import android.content.Context;
import android.media.AudioAttributes;
import android.media.SoundPool;
import android.os.Build;
import com.aldo.apps.ochecompanion.utils.Log;
import com.aldo.apps.ochecompanion.R;
@@ -23,12 +22,6 @@ public final class SoundEngine {
*/
private static final String TAG = "SoundEngine";
/**
* Application context used for audio operations.
* On Android R+, uses attribution context for proper audio tracking.
*/
private final Context mContext;
/**
* Singleton instance of the SoundEngine.
*/
@@ -71,12 +64,18 @@ public final class SoundEngine {
* @param context Application context for loading sound resources
*/
private SoundEngine(final Context context) {
Context contextToUse;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
mContext = context.createAttributionContext("oche_gameplay");
contextToUse = context.createAttributionContext("oche_gameplay");
} else {
mContext = context;
contextToUse = context;
}
mSoundPool = new SoundPool.Builder()
final SoundPool.Builder soundPoolBuilder = new SoundPool.Builder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
soundPoolBuilder.setContext(contextToUse);
}
mSoundPool = soundPoolBuilder
.setMaxStreams(5)
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(USAGE_GAME)

View File

@@ -58,7 +58,7 @@ public class HitDistributionConverter {
return multiplier == 2 ? "db" : "sb";
}
String prefix = "";
String prefix;
if (multiplier == 3) prefix = "t";
else if (multiplier == 2) prefix = "d";
else prefix = "s";

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".TestActivity">
<com.aldo.apps.ochecompanion.ui.HeatmapView
android:id="@+id/heatmap"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_gravity="center" />
</androidx.constraintlayout.widget.ConstraintLayout>