Fixed the Day/Night Mode handling in the app
This commit is contained in:
@@ -24,22 +24,30 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.OcheCompanion">
|
android:theme="@style/Theme.OcheCompanion">
|
||||||
|
<activity android:name=".BaseActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:configChanges="uiMode"/>
|
||||||
<activity
|
<activity
|
||||||
android:name=".TestActivity"
|
android:name=".TestActivity"
|
||||||
android:exported="false" />
|
android:exported="false"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".SettingsActivity"
|
android:name=".SettingsActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/title_activity_settings" />
|
android:label="@string/title_activity_settings"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".GameActivity"
|
android:name=".GameActivity"
|
||||||
android:exported="false" />
|
android:exported="false"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".AddPlayerActivity"
|
android:name=".AddPlayerActivity"
|
||||||
android:exported="false" />
|
android:exported="false"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainMenuActivity"
|
android:name=".MainMenuActivity"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
|
android:configChanges="uiMode">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import java.util.UUID;
|
|||||||
* Operates in Form Mode (profile editing) or Crop Mode (interactive image cropping with pan and zoom).
|
* Operates in Form Mode (profile editing) or Crop Mode (interactive image cropping with pan and zoom).
|
||||||
* Pass EXTRA_PLAYER_ID to edit existing player; otherwise creates new player.
|
* Pass EXTRA_PLAYER_ID to edit existing player; otherwise creates new player.
|
||||||
*/
|
*/
|
||||||
public class AddPlayerActivity extends AppCompatActivity {
|
public class AddPlayerActivity extends BaseActivity {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag for logging.
|
* Tag for logging.
|
||||||
|
|||||||
117
app/src/main/java/com/aldo/apps/ochecompanion/BaseActivity.java
Normal file
117
app/src/main/java/com/aldo/apps/ochecompanion/BaseActivity.java
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package com.aldo.apps.ochecompanion;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base activity that handles theme management for all activities in the app.
|
||||||
|
* All activities should extend this class to ensure consistent theme behavior.
|
||||||
|
*/
|
||||||
|
public abstract class BaseActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private static final String TAG = "BaseActivity";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SharedPreferences instance for accessing app settings.
|
||||||
|
*/
|
||||||
|
protected SharedPreferences mSettingsPref;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
// Apply theme before calling super to ensure correct theme is set
|
||||||
|
mSettingsPref = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
|
||||||
|
// Log current system UI mode
|
||||||
|
final int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||||
|
Log.d(TAG, getClass().getSimpleName() + " - onCreate: Current system night mode = " +
|
||||||
|
(currentNightMode == Configuration.UI_MODE_NIGHT_YES ? "NIGHT" : "DAY"));
|
||||||
|
Log.d(TAG, getClass().getSimpleName() + " - onCreate: Current AppCompatDelegate mode = " +
|
||||||
|
AppCompatDelegate.getDefaultNightMode());
|
||||||
|
|
||||||
|
applyThemeMode();
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
final int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||||
|
Log.d(TAG, getClass().getSimpleName() + " - onStart: System night mode = " +
|
||||||
|
(currentNightMode == Configuration.UI_MODE_NIGHT_YES ? "NIGHT" : "DAY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
final int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||||
|
Log.d(TAG, getClass().getSimpleName() + " - onResume: System night mode = " +
|
||||||
|
(currentNightMode == Configuration.UI_MODE_NIGHT_YES ? "NIGHT" : "DAY"));
|
||||||
|
// Reapply theme in case preferences changed
|
||||||
|
applyThemeMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
Log.d(TAG, getClass().getSimpleName() + " - onStop");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfigurationChanged(final Configuration newConfig) {
|
||||||
|
super.onConfigurationChanged(newConfig);
|
||||||
|
|
||||||
|
Log.d(TAG, "========================================");
|
||||||
|
Log.d(TAG, getClass().getSimpleName() + " - onConfigurationChanged CALLED!");
|
||||||
|
|
||||||
|
// Check if night mode configuration changed
|
||||||
|
final int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||||
|
final boolean isNightMode = currentNightMode == Configuration.UI_MODE_NIGHT_YES;
|
||||||
|
|
||||||
|
Log.d(TAG, getClass().getSimpleName() + " - onConfigurationChanged: System night mode = " + isNightMode);
|
||||||
|
Log.d(TAG, getClass().getSimpleName() + " - onConfigurationChanged: uiMode value = " + currentNightMode);
|
||||||
|
|
||||||
|
// Check if we're in auto mode
|
||||||
|
final boolean isAutoMode = mSettingsPref.getBoolean(getString(R.string.pref_key_day_night_mode_auto), true);
|
||||||
|
Log.d(TAG, getClass().getSimpleName() + " - onConfigurationChanged: Auto mode enabled = " + isAutoMode);
|
||||||
|
|
||||||
|
if (isAutoMode) {
|
||||||
|
// In auto mode - recreate activity to apply system theme
|
||||||
|
Log.d(TAG, getClass().getSimpleName() + " - onConfigurationChanged: Recreating activity to apply system theme");
|
||||||
|
recreate();
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, getClass().getSimpleName() + " - onConfigurationChanged: Not in auto mode, ignoring system change");
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "========================================");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the user's theme preference (auto, light, or dark mode).
|
||||||
|
* Called automatically by BaseActivity, no need to call manually.
|
||||||
|
*/
|
||||||
|
private void applyThemeMode() {
|
||||||
|
final boolean isAutoMode = mSettingsPref.getBoolean(getString(R.string.pref_key_day_night_mode_auto), true);
|
||||||
|
|
||||||
|
Log.d(TAG, getClass().getSimpleName() + " - applyThemeMode: Auto mode = " + isAutoMode);
|
||||||
|
|
||||||
|
if (isAutoMode) {
|
||||||
|
// Follow system theme
|
||||||
|
Log.d(TAG, getClass().getSimpleName() + " - applyThemeMode: Setting MODE_NIGHT_FOLLOW_SYSTEM");
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
|
||||||
|
} else {
|
||||||
|
// Use manual preference
|
||||||
|
final boolean isDarkMode = mSettingsPref.getBoolean(getString(R.string.pref_key_day_night_mode), false);
|
||||||
|
Log.d(TAG, getClass().getSimpleName() + " - applyThemeMode: Manual mode - dark mode = " + isDarkMode);
|
||||||
|
AppCompatDelegate.setDefaultNightMode(
|
||||||
|
isDarkMode ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ import androidx.core.view.ViewCompat;
|
|||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.aldo.apps.ochecompanion.database.AppDatabase;
|
import com.aldo.apps.ochecompanion.database.DatabaseHelper;
|
||||||
import com.aldo.apps.ochecompanion.database.objects.Player;
|
import com.aldo.apps.ochecompanion.database.objects.Player;
|
||||||
import com.aldo.apps.ochecompanion.database.objects.Statistics;
|
import com.aldo.apps.ochecompanion.database.objects.Statistics;
|
||||||
import com.aldo.apps.ochecompanion.ui.PlayerStatsView;
|
import com.aldo.apps.ochecompanion.ui.PlayerStatsView;
|
||||||
@@ -52,7 +52,7 @@ import nl.dionsegijn.konfetti.xml.KonfettiView;
|
|||||||
* Provides numeric keyboard, real-time checkout suggestions, Double Out enforcement,
|
* Provides numeric keyboard, real-time checkout suggestions, Double Out enforcement,
|
||||||
* and bust detection. Enforces standard darts rules including finishing on doubles.
|
* and bust detection. Enforces standard darts rules including finishing on doubles.
|
||||||
*/
|
*/
|
||||||
public class GameActivity extends AppCompatActivity {
|
public class GameActivity extends BaseActivity {
|
||||||
|
|
||||||
private static final String TAG = "GameActivity";
|
private static final String TAG = "GameActivity";
|
||||||
|
|
||||||
@@ -237,15 +237,9 @@ public class GameActivity extends AppCompatActivity {
|
|||||||
private boolean mIsAudioEnabled;
|
private boolean mIsAudioEnabled;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to the Room database instance for statistics and player data access.
|
* Centralized database helper that manages all database operations with proper synchronization.
|
||||||
*/
|
*/
|
||||||
private AppDatabase mDatabase;
|
private DatabaseHelper mDatabaseHelper;
|
||||||
|
|
||||||
/**
|
|
||||||
* Locks for synchronizing statistics updates per player to prevent race conditions.
|
|
||||||
* Each player gets their own lock object to allow concurrent updates for different players.
|
|
||||||
*/
|
|
||||||
private final java.util.Map<Long, Object> mPlayerStatsLocks = new java.util.HashMap<>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts GameActivity with specified players and starting score.
|
* Starts GameActivity with specified players and starting score.
|
||||||
@@ -279,19 +273,41 @@ public class GameActivity extends AppCompatActivity {
|
|||||||
return insets;
|
return insets;
|
||||||
});
|
});
|
||||||
mSoundEngine = SoundEngine.getInstance(this);
|
mSoundEngine = SoundEngine.getInstance(this);
|
||||||
mDatabase = AppDatabase.getDatabase(this);
|
mDatabaseHelper = DatabaseHelper.getInstance(this);
|
||||||
|
|
||||||
// Extract game parameters from intent
|
// Extract game parameters from intent
|
||||||
mStartingScore = getIntent().getIntExtra(EXTRA_START_SCORE, DartsConstants.DEFAULT_GAME_SCORE);
|
mStartingScore = getIntent().getIntExtra(EXTRA_START_SCORE, DartsConstants.DEFAULT_GAME_SCORE);
|
||||||
mMatchUuid = getIntent().getStringExtra(EXTRA_MATCH_UUID);
|
mMatchUuid = getIntent().getStringExtra(EXTRA_MATCH_UUID);
|
||||||
|
|
||||||
// Initialize activity components in order
|
// Initialize activity components in order
|
||||||
initViews();
|
initViews();
|
||||||
setupKeyboard();
|
setupKeyboard();
|
||||||
new Thread(() -> {
|
|
||||||
final List<Player> allAvailablePlayers = AppDatabase.getDatabase(GameActivity.this).playerDao().getAllPlayers();
|
// Only setup a new game if we're not restoring from saved state
|
||||||
Log.d(TAG, "onCreate: allAvailablePlayers = [" + allAvailablePlayers + "]");
|
if (savedInstanceState == null) {
|
||||||
runOnUiThread(() -> setupGame(allAvailablePlayers));
|
new Thread(() -> {
|
||||||
}).start();
|
final List<Player> allAvailablePlayers = (List<Player>) mDatabaseHelper.getAllPlayers();
|
||||||
|
Log.d(TAG, "onCreate: allAvailablePlayers = [" + allAvailablePlayers + "]");
|
||||||
|
runOnUiThread(() -> setupGame(allAvailablePlayers));
|
||||||
|
}).start();
|
||||||
|
} else {
|
||||||
|
// We're restoring - load players synchronously since onRestoreInstanceState is called immediately
|
||||||
|
Log.d(TAG, "onCreate: Loading players for state restoration");
|
||||||
|
final List<Player> allAvailablePlayers = (List<Player>) mDatabaseHelper.getAllPlayers();
|
||||||
|
Log.d(TAG, "onCreate: restoring with players = [" + allAvailablePlayers + "]");
|
||||||
|
|
||||||
|
// Initialize player states structure
|
||||||
|
mPlayerStates = new ArrayList<>();
|
||||||
|
if (allAvailablePlayers != null && !allAvailablePlayers.isEmpty()) {
|
||||||
|
for (Player p : allAvailablePlayers) {
|
||||||
|
mPlayerStates.add(new X01State(p, mStartingScore));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final Player guest = new Player("GUEST", null);
|
||||||
|
mPlayerStates.add(new X01State(guest, mStartingScore));
|
||||||
|
}
|
||||||
|
// The actual state restoration happens in onRestoreInstanceState
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -302,6 +318,113 @@ public class GameActivity extends AppCompatActivity {
|
|||||||
mIsVibrationEnabled = settingPrefs.getBoolean(getString(R.string.pref_key_vibration_feedback), true);
|
mIsVibrationEnabled = settingPrefs.getBoolean(getString(R.string.pref_key_vibration_feedback), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSaveInstanceState(Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
Log.d(TAG, "onSaveInstanceState: Saving game state");
|
||||||
|
|
||||||
|
// Save basic game state
|
||||||
|
outState.putInt("activePlayerIndex", mActivePlayerIndex);
|
||||||
|
outState.putInt("multiplier", mMultiplier);
|
||||||
|
outState.putInt("startingScore", mStartingScore);
|
||||||
|
outState.putString("matchUuid", mMatchUuid);
|
||||||
|
outState.putBoolean("isTurnOver", mIsTurnOver);
|
||||||
|
|
||||||
|
// Save current turn darts
|
||||||
|
int[] dartsArray = new int[mCurrentTurnDarts.size()];
|
||||||
|
for (int i = 0; i < mCurrentTurnDarts.size(); i++) {
|
||||||
|
dartsArray[i] = mCurrentTurnDarts.get(i);
|
||||||
|
}
|
||||||
|
outState.putIntArray("currentTurnDarts", dartsArray);
|
||||||
|
|
||||||
|
// Save current turn dart hits (base values and multipliers)
|
||||||
|
if (!mCurrentTurnDartHits.isEmpty()) {
|
||||||
|
int[] baseValues = new int[mCurrentTurnDartHits.size()];
|
||||||
|
int[] multipliers = new int[mCurrentTurnDartHits.size()];
|
||||||
|
for (int i = 0; i < mCurrentTurnDartHits.size(); i++) {
|
||||||
|
baseValues[i] = mCurrentTurnDartHits.get(i).baseValue;
|
||||||
|
multipliers[i] = mCurrentTurnDartHits.get(i).multiplier;
|
||||||
|
}
|
||||||
|
outState.putIntArray("dartHitBaseValues", baseValues);
|
||||||
|
outState.putIntArray("dartHitMultipliers", multipliers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save player states
|
||||||
|
if (mPlayerStates != null && !mPlayerStates.isEmpty()) {
|
||||||
|
int playerCount = mPlayerStates.size();
|
||||||
|
outState.putInt("playerCount", playerCount);
|
||||||
|
|
||||||
|
long[] playerIds = new long[playerCount];
|
||||||
|
int[] remainingScores = new int[playerCount];
|
||||||
|
int[] dartsThrown = new int[playerCount];
|
||||||
|
|
||||||
|
for (int i = 0; i < playerCount; i++) {
|
||||||
|
X01State state = mPlayerStates.get(i);
|
||||||
|
playerIds[i] = state.playerId;
|
||||||
|
remainingScores[i] = state.remainingScore;
|
||||||
|
dartsThrown[i] = state.dartsThrown;
|
||||||
|
}
|
||||||
|
|
||||||
|
outState.putLongArray("playerIds", playerIds);
|
||||||
|
outState.putIntArray("remainingScores", remainingScores);
|
||||||
|
outState.putIntArray("dartsThrown", dartsThrown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||||
|
super.onRestoreInstanceState(savedInstanceState);
|
||||||
|
Log.d(TAG, "onRestoreInstanceState: Restoring game state");
|
||||||
|
|
||||||
|
// Restore basic game state
|
||||||
|
mActivePlayerIndex = savedInstanceState.getInt("activePlayerIndex", 0);
|
||||||
|
mMultiplier = savedInstanceState.getInt("multiplier", 1);
|
||||||
|
mStartingScore = savedInstanceState.getInt("startingScore", DartsConstants.DEFAULT_GAME_SCORE);
|
||||||
|
mMatchUuid = savedInstanceState.getString("matchUuid");
|
||||||
|
mIsTurnOver = savedInstanceState.getBoolean("isTurnOver", false);
|
||||||
|
|
||||||
|
// Restore current turn darts
|
||||||
|
mCurrentTurnDarts.clear();
|
||||||
|
int[] dartsArray = savedInstanceState.getIntArray("currentTurnDarts");
|
||||||
|
if (dartsArray != null) {
|
||||||
|
for (int dart : dartsArray) {
|
||||||
|
mCurrentTurnDarts.add(dart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore current turn dart hits
|
||||||
|
mCurrentTurnDartHits.clear();
|
||||||
|
int[] baseValues = savedInstanceState.getIntArray("dartHitBaseValues");
|
||||||
|
int[] multipliers = savedInstanceState.getIntArray("dartHitMultipliers");
|
||||||
|
if (baseValues != null && multipliers != null) {
|
||||||
|
for (int i = 0; i < baseValues.length; i++) {
|
||||||
|
mCurrentTurnDartHits.add(new DartHit(baseValues[i], multipliers[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore player states
|
||||||
|
int playerCount = savedInstanceState.getInt("playerCount", 0);
|
||||||
|
if (playerCount > 0 && mPlayerStates != null) {
|
||||||
|
long[] playerIds = savedInstanceState.getLongArray("playerIds");
|
||||||
|
int[] remainingScores = savedInstanceState.getIntArray("remainingScores");
|
||||||
|
int[] dartsThrown = savedInstanceState.getIntArray("dartsThrown");
|
||||||
|
|
||||||
|
if (playerIds != null && remainingScores != null && dartsThrown != null) {
|
||||||
|
for (int i = 0; i < playerCount && i < mPlayerStates.size(); i++) {
|
||||||
|
mPlayerStates.get(i).remainingScore = remainingScores[i];
|
||||||
|
mPlayerStates.get(i).dartsThrown = dartsThrown[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update UI to reflect restored state (only if mPlayerStates is initialized)
|
||||||
|
if (mPlayerStates != null && !mPlayerStates.isEmpty()) {
|
||||||
|
updateUI();
|
||||||
|
updateTurnIndicators();
|
||||||
|
setMultiplier(mMultiplier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes UI component references and sets up click listeners.
|
* Initializes UI component references and sets up click listeners.
|
||||||
*/
|
*/
|
||||||
@@ -497,7 +620,7 @@ public class GameActivity extends AppCompatActivity {
|
|||||||
/**
|
/**
|
||||||
* Updates player statistics in the database after a turn.
|
* Updates player statistics in the database after a turn.
|
||||||
* Tracks darts thrown, points made, missed darts, and updates career average.
|
* Tracks darts thrown, points made, missed darts, and updates career average.
|
||||||
* Runs on background thread to avoid blocking UI.
|
* Delegates to DatabaseHelper for thread-safe execution.
|
||||||
*
|
*
|
||||||
* @param active Current player's game state
|
* @param active Current player's game state
|
||||||
* @param dartsThrown Number of darts thrown this turn
|
* @param dartsThrown Number of darts thrown this turn
|
||||||
@@ -519,67 +642,16 @@ public class GameActivity extends AppCompatActivity {
|
|||||||
* @param checkoutValue The checkout score if this was a winning turn (0 if not a checkout)
|
* @param checkoutValue The checkout score if this was a winning turn (0 if not a checkout)
|
||||||
*/
|
*/
|
||||||
private void updatePlayerStats(final GameActivity.X01State active, final int dartsThrown, final int pointsMade, final boolean wasBust, final int checkoutValue) {
|
private void updatePlayerStats(final GameActivity.X01State active, final int dartsThrown, final int pointsMade, final boolean wasBust, final int checkoutValue) {
|
||||||
new Thread(() -> {
|
if (active.player != null && active.player.id != 0) {
|
||||||
final Player player = active.player;
|
mDatabaseHelper.updatePlayerStatistics(
|
||||||
if (player != null && player.id != 0) {
|
active.player.id,
|
||||||
final Statistics playerStats = AppDatabase.getDatabase(GameActivity.this).statisticsDao().getStatisticsForPlayer(player.id);
|
dartsThrown,
|
||||||
|
pointsMade,
|
||||||
// Track darts thrown or missed
|
wasBust,
|
||||||
if (wasBust) {
|
checkoutValue,
|
||||||
// On bust, all darts in the turn are wasted
|
active.dartsThrown
|
||||||
playerStats.addMissedDarts(dartsThrown);
|
);
|
||||||
Log.d(TAG, "updatePlayerStats: Bust! Recorded " + dartsThrown + " missed darts");
|
}
|
||||||
} else {
|
|
||||||
// Normal turn - record darts and points
|
|
||||||
playerStats.saveDartsThrown(dartsThrown, pointsMade);
|
|
||||||
Log.d(TAG, "updatePlayerStats: dartsThrown = [" + dartsThrown + "], pointsMade = [" + pointsMade + "]");
|
|
||||||
|
|
||||||
// Track missed darts if turn ended early (less than 3 darts)
|
|
||||||
if (dartsThrown < 3) {
|
|
||||||
playerStats.addMissedDarts(3 - dartsThrown);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track scoring milestones (60+, 100+, 140+, 180)
|
|
||||||
if (pointsMade >= 180) {
|
|
||||||
playerStats.setCount180(playerStats.getCount180() + 1);
|
|
||||||
Log.d(TAG, "updatePlayerStats: Perfect 180! Total: " + playerStats.getCount180());
|
|
||||||
} else if (pointsMade >= 140) {
|
|
||||||
playerStats.setCount140Plus(playerStats.getCount140Plus() + 1);
|
|
||||||
} else if (pointsMade >= 100) {
|
|
||||||
playerStats.setCount100Plus(playerStats.getCount100Plus() + 1);
|
|
||||||
} else if (pointsMade >= 60) {
|
|
||||||
playerStats.setCount60Plus(playerStats.getCount60Plus() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track first 9 darts statistics (first 3 turns of the match)
|
|
||||||
if (active.dartsThrown < 9) {
|
|
||||||
final long dartsToAdd = Math.min(dartsThrown, 9 - active.dartsThrown);
|
|
||||||
playerStats.setTotalFirst9Darts(playerStats.getTotalFirst9Darts() + dartsToAdd);
|
|
||||||
playerStats.setTotalFirst9Points(playerStats.getTotalFirst9Points() + pointsMade);
|
|
||||||
Log.d(TAG, "updatePlayerStats: First 9 tracking - darts: " + dartsToAdd + ", points: " + pointsMade);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track successful checkout
|
|
||||||
if (checkoutValue > 0) {
|
|
||||||
playerStats.setSuccessfulCheckouts(playerStats.getSuccessfulCheckouts() + 1);
|
|
||||||
playerStats.setHighestCheckout(checkoutValue);
|
|
||||||
Log.d(TAG, "updatePlayerStats: Checkout tracked - value: " + checkoutValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "updatePlayerStats: statistics = [" + playerStats + "]");
|
|
||||||
AppDatabase.getDatabase(GameActivity.this).statisticsDao().updateStatistics(playerStats);
|
|
||||||
|
|
||||||
// Calculate career average: total points / total darts thrown
|
|
||||||
final long totalDarts = playerStats.getTotalDartsThrown();
|
|
||||||
if (totalDarts > 0) {
|
|
||||||
player.careerAverage = (double) playerStats.getTotalPoints() / totalDarts * 3;
|
|
||||||
} else {
|
|
||||||
player.careerAverage = 0.0;
|
|
||||||
}
|
|
||||||
AppDatabase.getDatabase(GameActivity.this).playerDao().update(player);
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -781,50 +853,33 @@ public class GameActivity extends AppCompatActivity {
|
|||||||
/**
|
/**
|
||||||
* Tracks a double-out attempt in player statistics.
|
* Tracks a double-out attempt in player statistics.
|
||||||
* Records whether the attempt was successful or missed.
|
* Records whether the attempt was successful or missed.
|
||||||
|
* Delegates to DatabaseHelper for thread-safe execution.
|
||||||
*
|
*
|
||||||
* @param playerState X01State of the player
|
* @param playerState X01State of the player
|
||||||
* @param isMissed true if the double-out was missed, false if successful
|
* @param isMissed true if the double-out was missed, false if successful
|
||||||
*/
|
*/
|
||||||
private void trackDoubleAttempt(final X01State playerState, final boolean isMissed) {
|
private void trackDoubleAttempt(final X01State playerState, final boolean isMissed) {
|
||||||
new Thread(() -> {
|
mDatabaseHelper.trackDoubleAttempt(playerState.playerId, isMissed);
|
||||||
try {
|
|
||||||
Statistics playerStats = mDatabase.statisticsDao().getStatisticsForPlayer(playerState.playerId);
|
|
||||||
if (playerStats != null) {
|
|
||||||
playerStats.addDoubleOutTarget(isMissed);
|
|
||||||
mDatabase.statisticsDao().updateStatistics(playerStats);
|
|
||||||
Log.d(TAG, "trackDoubleAttempt: Recorded double attempt (missed=" + isMissed + ") for player " + playerState.name);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "trackDoubleAttempt: Failed to track double attempt", e);
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increments matchesPlayed counter for all players in the current match.
|
* Increments matchesPlayed counter for all players in the current match.
|
||||||
* Called when a match is completed (someone wins).
|
* Called when a match is completed (someone wins).
|
||||||
|
* Delegates to DatabaseHelper for thread-safe execution.
|
||||||
*/
|
*/
|
||||||
private void incrementMatchesPlayed() {
|
private void incrementMatchesPlayed() {
|
||||||
new Thread(() -> {
|
final List<Long> playerIds = new ArrayList<>();
|
||||||
try {
|
for (X01State playerState : mPlayerStates) {
|
||||||
for (X01State playerState : mPlayerStates) {
|
playerIds.add(playerState.playerId);
|
||||||
Statistics playerStats = mDatabase.statisticsDao().getStatisticsForPlayer(playerState.playerId);
|
}
|
||||||
if (playerStats != null) {
|
mDatabaseHelper.incrementMatchesPlayed(playerIds);
|
||||||
playerStats.addCompletedMatch();
|
|
||||||
mDatabase.statisticsDao().updateStatistics(playerStats);
|
|
||||||
Log.d(TAG, "incrementMatchesPlayed: Incremented for player " + playerState.name + ", total: " + playerStats.getMatchesPlayed());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "incrementMatchesPlayed: Failed to increment matches", e);
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records all dart hits from a confirmed turn to the player's statistics.
|
* Records all dart hits from a confirmed turn to the player's statistics.
|
||||||
* Updates the hit distribution map for heat map visualization.
|
* Updates the hit distribution map for heat map visualization.
|
||||||
* Only called after turn is submitted to avoid recording unconfirmed throws.
|
* Only called after turn is submitted to avoid recording unconfirmed throws.
|
||||||
|
* Delegates to DatabaseHelper for thread-safe execution.
|
||||||
*
|
*
|
||||||
* @param playerState X01State of the player
|
* @param playerState X01State of the player
|
||||||
* @param dartHits List of dart hit details from the turn
|
* @param dartHits List of dart hit details from the turn
|
||||||
@@ -832,33 +887,13 @@ public class GameActivity extends AppCompatActivity {
|
|||||||
private void recordTurnHitsToStatistics(final X01State playerState, final List<DartHit> dartHits) {
|
private void recordTurnHitsToStatistics(final X01State playerState, final List<DartHit> dartHits) {
|
||||||
if (dartHits.isEmpty()) return;
|
if (dartHits.isEmpty()) return;
|
||||||
|
|
||||||
new Thread(() -> {
|
// Convert local DartHit to DatabaseHelper.DartHit
|
||||||
// Get or create lock object for this player to prevent race conditions
|
final List<DatabaseHelper.DartHit> dbDartHits = new ArrayList<>();
|
||||||
final Object lock;
|
for (DartHit hit : dartHits) {
|
||||||
synchronized (mPlayerStatsLocks) {
|
dbDartHits.add(new DatabaseHelper.DartHit(hit.baseValue, hit.multiplier));
|
||||||
lock = mPlayerStatsLocks.computeIfAbsent(playerState.playerId, k -> new Object());
|
}
|
||||||
}
|
|
||||||
|
mDatabaseHelper.recordDartHits(playerState.playerId, dbDartHits);
|
||||||
// Synchronize the read-modify-write operation for this player
|
|
||||||
synchronized (lock) {
|
|
||||||
try {
|
|
||||||
Statistics playerStats = mDatabase.statisticsDao().getStatisticsForPlayer(playerState.playerId);
|
|
||||||
if (playerStats != null) {
|
|
||||||
Log.d(TAG, "recordTurnHitsToStatistics: Before recording - hitDistribution size: " + playerStats.getHitDistribution().size());
|
|
||||||
// Record all darts from this turn
|
|
||||||
for (DartHit hit : dartHits) {
|
|
||||||
playerStats.recordDartHit(hit.baseValue, hit.multiplier);
|
|
||||||
}
|
|
||||||
Log.d(TAG, "recordTurnHitsToStatistics: After recording - hitDistribution size: " + playerStats.getHitDistribution().size());
|
|
||||||
Log.d(TAG, "recordTurnHitsToStatistics: hitDistribution contents: " + playerStats.getHitDistribution());
|
|
||||||
mDatabase.statisticsDao().updateStatistics(playerStats);
|
|
||||||
Log.d(TAG, "recordTurnHitsToStatistics: Recorded " + dartHits.size() + " darts for player " + playerState.name);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "recordTurnHitsToStatistics: Failed to record hits", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -906,12 +941,12 @@ public class GameActivity extends AppCompatActivity {
|
|||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
final Player player = mPlayerStates.get(mActivePlayerIndex).player;
|
final Player player = mPlayerStates.get(mActivePlayerIndex).player;
|
||||||
final Statistics statistics = AppDatabase.getDatabase(GameActivity.this).statisticsDao().getStatisticsForPlayer(player.id);
|
final Statistics statistics = mDatabaseHelper.getStatisticsForPlayer(player.id);
|
||||||
runOnUiThread(() -> {
|
runOnUiThread(() -> {
|
||||||
mStatsView.bind(player, statistics);
|
mStatsView.bind(player, statistics);
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "attachPlayerStats: Failed to increment matches", e);
|
Log.e(TAG, "attachPlayerStats: Failed to retrieve player statistics", e);
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import android.view.View;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.activity.EdgeToEdge;
|
import androidx.activity.EdgeToEdge;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.core.graphics.Insets;
|
import androidx.core.graphics.Insets;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
@@ -32,7 +31,7 @@ import java.util.List;
|
|||||||
* Main entry point and home screen of the Oche Companion application.
|
* Main entry point and home screen of the Oche Companion application.
|
||||||
* Displays the squad of players, allows adding new players, and shows match recap with test data.
|
* Displays the squad of players, allows adding new players, and shows match recap with test data.
|
||||||
*/
|
*/
|
||||||
public class MainMenuActivity extends AppCompatActivity {
|
public class MainMenuActivity extends BaseActivity {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag for debugging and logging purposes.
|
* Tag for debugging and logging purposes.
|
||||||
@@ -70,7 +69,7 @@ public class MainMenuActivity extends AppCompatActivity {
|
|||||||
EdgeToEdge.enable(this);
|
EdgeToEdge.enable(this);
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
mSettingsPref = PreferenceManager.getDefaultSharedPreferences(this);
|
mSettingsPref = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
|
||||||
// Configure window insets to properly handle system bars (status bar, navigation bar)
|
// Configure window insets to properly handle system bars (status bar, navigation bar)
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
|
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
|
||||||
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import com.aldo.apps.ochecompanion.ui.MainMenuPreferencesFragment;
|
|||||||
* Hosts the MainMenuPreferencesFragment which displays available settings
|
* Hosts the MainMenuPreferencesFragment which displays available settings
|
||||||
* including day/night mode and standard game mode selection.
|
* including day/night mode and standard game mode selection.
|
||||||
*/
|
*/
|
||||||
public class SettingsActivity extends AppCompatActivity {
|
public class SettingsActivity extends BaseActivity {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the settings activity and loads the preferences fragment.
|
* Initializes the settings activity and loads the preferences fragment.
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import com.aldo.apps.ochecompanion.ui.HeatmapView;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class TestActivity extends AppCompatActivity {
|
public class TestActivity extends BaseActivity {
|
||||||
|
|
||||||
private static final String TAG = "TestActivity";
|
private static final String TAG = "TestActivity";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,342 @@
|
|||||||
|
package com.aldo.apps.ochecompanion.database;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.aldo.apps.ochecompanion.database.objects.Player;
|
||||||
|
import com.aldo.apps.ochecompanion.database.objects.Statistics;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized database helper that manages all database operations with proper synchronization.
|
||||||
|
* Handles threading internally to prevent race conditions and simplify database access.
|
||||||
|
* All database operations are executed on a background thread pool with per-player locking
|
||||||
|
* to ensure data integrity while allowing concurrent updates for different players.
|
||||||
|
*/
|
||||||
|
public class DatabaseHelper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag for debugging purposes.
|
||||||
|
*/
|
||||||
|
private static final String TAG = "DatabaseHelper";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton instance of DatabaseHelper.
|
||||||
|
*/
|
||||||
|
private static volatile DatabaseHelper sInstance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to the Room database.
|
||||||
|
*/
|
||||||
|
private final AppDatabase mDatabase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single-threaded executor for database operations to ensure sequential execution.
|
||||||
|
* Prevents race conditions by serializing all database writes.
|
||||||
|
*/
|
||||||
|
private final ExecutorService mExecutor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-player locks for fine-grained synchronization.
|
||||||
|
* Allows concurrent updates for different players while preventing conflicts for the same player.
|
||||||
|
*/
|
||||||
|
private final Map<Long, Object> mPlayerLocks = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor for singleton pattern.
|
||||||
|
*
|
||||||
|
* @param context Application context
|
||||||
|
*/
|
||||||
|
private DatabaseHelper(final Context context) {
|
||||||
|
mDatabase = AppDatabase.getDatabase(context.getApplicationContext());
|
||||||
|
mExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the singleton DatabaseHelper instance.
|
||||||
|
*
|
||||||
|
* @param context Context used to initialize the database
|
||||||
|
* @return Singleton DatabaseHelper instance
|
||||||
|
*/
|
||||||
|
public static DatabaseHelper getInstance(final Context context) {
|
||||||
|
if (sInstance == null) {
|
||||||
|
synchronized (DatabaseHelper.class) {
|
||||||
|
if (sInstance == null) {
|
||||||
|
sInstance = new DatabaseHelper(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets or creates a lock object for a specific player.
|
||||||
|
* Thread-safe method to retrieve player-specific locks.
|
||||||
|
*
|
||||||
|
* @param playerId The player's ID
|
||||||
|
* @return Lock object for the specified player
|
||||||
|
*/
|
||||||
|
private Object getPlayerLock(final long playerId) {
|
||||||
|
synchronized (mPlayerLocks) {
|
||||||
|
return mPlayerLocks.computeIfAbsent(playerId, k -> new Object());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates player statistics after a turn.
|
||||||
|
* Handles darts thrown, points made, bust tracking, and milestone counting.
|
||||||
|
*
|
||||||
|
* @param playerId Player's database ID
|
||||||
|
* @param dartsThrown Number of darts thrown this turn
|
||||||
|
* @param pointsMade Total points scored this turn
|
||||||
|
* @param wasBust Whether the turn resulted in a bust
|
||||||
|
* @param checkoutValue The checkout score if this was a winning turn (0 if not a checkout)
|
||||||
|
* @param totalDartsThrownInMatch Total darts thrown by player in current match (for first 9 tracking)
|
||||||
|
*/
|
||||||
|
public void updatePlayerStatistics(final long playerId, final int dartsThrown, final int pointsMade,
|
||||||
|
final boolean wasBust, final int checkoutValue, final int totalDartsThrownInMatch) {
|
||||||
|
mExecutor.execute(() -> {
|
||||||
|
final Object lock = getPlayerLock(playerId);
|
||||||
|
synchronized (lock) {
|
||||||
|
try {
|
||||||
|
final Statistics playerStats = mDatabase.statisticsDao().getStatisticsForPlayer(playerId);
|
||||||
|
if (playerStats == null) {
|
||||||
|
Log.w(TAG, "updatePlayerStatistics: No statistics found for player " + playerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track darts thrown or missed
|
||||||
|
if (wasBust) {
|
||||||
|
// On bust, all darts in the turn are wasted
|
||||||
|
playerStats.addMissedDarts(dartsThrown);
|
||||||
|
Log.d(TAG, "updatePlayerStatistics: Bust! Recorded " + dartsThrown + " missed darts");
|
||||||
|
} else {
|
||||||
|
// Normal turn - record darts and points
|
||||||
|
playerStats.saveDartsThrown(dartsThrown, pointsMade);
|
||||||
|
Log.d(TAG, "updatePlayerStatistics: dartsThrown = [" + dartsThrown + "], pointsMade = [" + pointsMade + "]");
|
||||||
|
|
||||||
|
// Track missed darts if turn ended early (less than 3 darts)
|
||||||
|
if (dartsThrown < 3) {
|
||||||
|
playerStats.addMissedDarts(3 - dartsThrown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track scoring milestones (60+, 100+, 140+, 180)
|
||||||
|
if (pointsMade >= 180) {
|
||||||
|
playerStats.setCount180(playerStats.getCount180() + 1);
|
||||||
|
Log.d(TAG, "updatePlayerStatistics: Perfect 180! Total: " + playerStats.getCount180());
|
||||||
|
} else if (pointsMade >= 140) {
|
||||||
|
playerStats.setCount140Plus(playerStats.getCount140Plus() + 1);
|
||||||
|
} else if (pointsMade >= 100) {
|
||||||
|
playerStats.setCount100Plus(playerStats.getCount100Plus() + 1);
|
||||||
|
} else if (pointsMade >= 60) {
|
||||||
|
playerStats.setCount60Plus(playerStats.getCount60Plus() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track first 9 darts statistics (first 3 turns of the match)
|
||||||
|
if (totalDartsThrownInMatch < 9) {
|
||||||
|
final long dartsToAdd = Math.min(dartsThrown, 9 - totalDartsThrownInMatch);
|
||||||
|
playerStats.setTotalFirst9Darts(playerStats.getTotalFirst9Darts() + dartsToAdd);
|
||||||
|
playerStats.setTotalFirst9Points(playerStats.getTotalFirst9Points() + pointsMade);
|
||||||
|
Log.d(TAG, "updatePlayerStatistics: First 9 tracking - darts: " + dartsToAdd + ", points: " + pointsMade);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track successful checkout
|
||||||
|
if (checkoutValue > 0) {
|
||||||
|
playerStats.setSuccessfulCheckouts(playerStats.getSuccessfulCheckouts() + 1);
|
||||||
|
playerStats.setHighestCheckout(checkoutValue);
|
||||||
|
Log.d(TAG, "updatePlayerStatistics: Checkout tracked - value: " + checkoutValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "updatePlayerStatistics: statistics = [" + playerStats + "]");
|
||||||
|
mDatabase.statisticsDao().updateStatistics(playerStats);
|
||||||
|
|
||||||
|
// Update player's career average
|
||||||
|
final Player player = mDatabase.playerDao().getPlayerById(playerId);
|
||||||
|
if (player != null) {
|
||||||
|
final long totalDarts = playerStats.getTotalDartsThrown();
|
||||||
|
if (totalDarts > 0) {
|
||||||
|
player.careerAverage = (double) playerStats.getTotalPoints() / totalDarts * 3;
|
||||||
|
} else {
|
||||||
|
player.careerAverage = 0.0;
|
||||||
|
}
|
||||||
|
mDatabase.playerDao().update(player);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "updatePlayerStatistics: Failed to update statistics", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks a double-out attempt in player statistics.
|
||||||
|
* Records whether the attempt was successful or missed.
|
||||||
|
*
|
||||||
|
* @param playerId Player's database ID
|
||||||
|
* @param isMissed true if the double-out was missed, false if successful
|
||||||
|
*/
|
||||||
|
public void trackDoubleAttempt(final long playerId, final boolean isMissed) {
|
||||||
|
mExecutor.execute(() -> {
|
||||||
|
final Object lock = getPlayerLock(playerId);
|
||||||
|
synchronized (lock) {
|
||||||
|
try {
|
||||||
|
final Statistics playerStats = mDatabase.statisticsDao().getStatisticsForPlayer(playerId);
|
||||||
|
if (playerStats != null) {
|
||||||
|
playerStats.addDoubleOutTarget(isMissed);
|
||||||
|
mDatabase.statisticsDao().updateStatistics(playerStats);
|
||||||
|
Log.d(TAG, "trackDoubleAttempt: Recorded double attempt (missed=" + isMissed + ") for player " + playerId);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "trackDoubleAttempt: Failed to track double attempt", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments matchesPlayed counter for all specified players.
|
||||||
|
* Called when a match is completed (someone wins).
|
||||||
|
*
|
||||||
|
* @param playerIds List of player IDs to update
|
||||||
|
*/
|
||||||
|
public void incrementMatchesPlayed(final List<Long> playerIds) {
|
||||||
|
mExecutor.execute(() -> {
|
||||||
|
for (final long playerId : playerIds) {
|
||||||
|
final Object lock = getPlayerLock(playerId);
|
||||||
|
synchronized (lock) {
|
||||||
|
try {
|
||||||
|
final Statistics playerStats = mDatabase.statisticsDao().getStatisticsForPlayer(playerId);
|
||||||
|
if (playerStats != null) {
|
||||||
|
playerStats.addCompletedMatch();
|
||||||
|
mDatabase.statisticsDao().updateStatistics(playerStats);
|
||||||
|
Log.d(TAG, "incrementMatchesPlayed: Incremented for player " + playerId + ", total: " + playerStats.getMatchesPlayed());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "incrementMatchesPlayed: Failed to increment matches for player " + playerId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single dart hit with base value and multiplier.
|
||||||
|
*/
|
||||||
|
public static class DartHit {
|
||||||
|
/**
|
||||||
|
* The dartboard number hit (1-20 or 25 for bull).
|
||||||
|
*/
|
||||||
|
public final int baseValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The multiplier applied to the base value (1=single, 2=double, 3=triple).
|
||||||
|
*/
|
||||||
|
public final int multiplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a DartHit with the specified base value and multiplier.
|
||||||
|
*
|
||||||
|
* @param baseValue The dartboard number (1-20 or 25 for bull)
|
||||||
|
* @param multiplier The multiplier (1=single, 2=double, 3=triple)
|
||||||
|
*/
|
||||||
|
public DartHit(final int baseValue, final int multiplier) {
|
||||||
|
this.baseValue = baseValue;
|
||||||
|
this.multiplier = multiplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records all dart hits from a confirmed turn to the player's statistics.
|
||||||
|
* Updates the hit distribution map for heat map visualization.
|
||||||
|
*
|
||||||
|
* @param playerId Player's database ID
|
||||||
|
* @param dartHits List of dart hit details from the turn
|
||||||
|
*/
|
||||||
|
public void recordDartHits(final long playerId, final List<DartHit> dartHits) {
|
||||||
|
if (dartHits == null || dartHits.isEmpty()) return;
|
||||||
|
|
||||||
|
mExecutor.execute(() -> {
|
||||||
|
final Object lock = getPlayerLock(playerId);
|
||||||
|
synchronized (lock) {
|
||||||
|
try {
|
||||||
|
final Statistics playerStats = mDatabase.statisticsDao().getStatisticsForPlayer(playerId);
|
||||||
|
if (playerStats != null) {
|
||||||
|
Log.d(TAG, "recordDartHits: Before recording - hitDistribution size: " + playerStats.getHitDistribution().size());
|
||||||
|
// Record all darts from this turn
|
||||||
|
for (final DartHit hit : dartHits) {
|
||||||
|
playerStats.recordDartHit(hit.baseValue, hit.multiplier);
|
||||||
|
}
|
||||||
|
Log.d(TAG, "recordDartHits: After recording - hitDistribution size: " + playerStats.getHitDistribution().size());
|
||||||
|
Log.d(TAG, "recordDartHits: hitDistribution contents: " + playerStats.getHitDistribution());
|
||||||
|
mDatabase.statisticsDao().updateStatistics(playerStats);
|
||||||
|
Log.d(TAG, "recordDartHits: Recorded " + dartHits.size() + " darts for player " + playerId);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "recordDartHits: Failed to record hits", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all players from the database synchronously.
|
||||||
|
* Blocks until the operation completes to ensure consistency with any pending writes.
|
||||||
|
*
|
||||||
|
* @return List of all players, or empty list if none exist
|
||||||
|
*/
|
||||||
|
public List<?> 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();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "getAllPlayers: Failed to submit task", e);
|
||||||
|
return new java.util.ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves statistics for a specific player synchronously.
|
||||||
|
* Blocks until the operation completes to ensure consistency with any pending writes.
|
||||||
|
*
|
||||||
|
* @param playerId The player's database ID
|
||||||
|
* @return Statistics object for the player, or null if not found
|
||||||
|
*/
|
||||||
|
public Statistics getStatisticsForPlayer(final long playerId) {
|
||||||
|
final Object lock = getPlayerLock(playerId);
|
||||||
|
try {
|
||||||
|
return mExecutor.submit(() -> {
|
||||||
|
synchronized (lock) {
|
||||||
|
try {
|
||||||
|
return mDatabase.statisticsDao().getStatisticsForPlayer(playerId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "getStatisticsForPlayer: Failed to retrieve statistics for player " + playerId, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).get();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "getStatisticsForPlayer: Failed to submit task", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuts down the executor service.
|
||||||
|
* Should be called when the helper is no longer needed.
|
||||||
|
*/
|
||||||
|
public void shutdown() {
|
||||||
|
mExecutor.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,10 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* HeatmapView: A custom high-performance rendering component that draws a
|
* HeatmapView: A custom high-performance rendering component that draws a
|
||||||
* dartboard and overlays player performance data as a color-coded heatmap.
|
* dartboard and overlays player performance data as a color-coded heatmap.
|
||||||
|
* Optimized Palette:
|
||||||
|
* - Zero hits: Subtle semi-transparent "ghost" segments.
|
||||||
|
* - Low frequency: Volt Green (Starting "cool" color).
|
||||||
|
* - High frequency: Double Red (Intense "hot" color).
|
||||||
*/
|
*/
|
||||||
public class HeatmapView extends View {
|
public class HeatmapView extends View {
|
||||||
|
|
||||||
@@ -114,27 +118,35 @@ public class HeatmapView extends View {
|
|||||||
super.onDraw(canvas);
|
super.onDraw(canvas);
|
||||||
if (mStats == null) return;
|
if (mStats == null) return;
|
||||||
|
|
||||||
final int coldColor = Color.parseColor("#1AFFFFFF"); // Faded ghost board
|
// Resolve branding colors from resources
|
||||||
final int hotColor = ContextCompat.getColor(getContext(), R.color.volt_green);
|
int coldColor = ContextCompat.getColor(getContext(), R.color.volt_green);
|
||||||
|
int hotColor = ContextCompat.getColor(getContext(), R.color.double_red);
|
||||||
|
int emptyColor = Color.parseColor("#1AFFFFFF"); // Subtle ghost segments for zero data
|
||||||
|
|
||||||
// Draw every calculated path with heat-weighted colors
|
|
||||||
for (final Map.Entry<String, Path> entry : mSegmentPaths.entrySet()) {
|
for (final Map.Entry<String, Path> entry : mSegmentPaths.entrySet()) {
|
||||||
final String key = entry.getKey();
|
final String key = entry.getKey();
|
||||||
|
|
||||||
// Handle the "Split Single" segments by mapping them to the same "sX" key
|
|
||||||
final String statsKey = key.contains("_") ? key.substring(0, key.indexOf("_")) : key;
|
final String statsKey = key.contains("_") ? key.substring(0, key.indexOf("_")) : key;
|
||||||
|
|
||||||
final float weight = mStats.getNormalizedWeight(statsKey);
|
// Check if there are any hits recorded for this segment
|
||||||
final int color = (int) mColorEvaluator.evaluate(weight, coldColor, hotColor);
|
final Integer hitCount = mStats.getHitDistribution().get(statsKey);
|
||||||
|
int color;
|
||||||
|
|
||||||
|
if (hitCount == null || hitCount == 0) {
|
||||||
|
color = emptyColor;
|
||||||
|
} else {
|
||||||
|
// Fetch the normalized heat (0.0 to 1.0) and evaluate against Green -> Red
|
||||||
|
final float weight = mStats.getNormalizedWeight(statsKey);
|
||||||
|
color = (int) mColorEvaluator.evaluate(weight, coldColor, hotColor);
|
||||||
|
}
|
||||||
|
|
||||||
mBasePaint.setColor(color);
|
mBasePaint.setColor(color);
|
||||||
canvas.drawPath(entry.getValue(), mBasePaint);
|
canvas.drawPath(entry.getValue(), mBasePaint);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final aesthetic: draw a thin wireframe over the segments
|
// Final wireframe overlay for professional aesthetics
|
||||||
mBasePaint.setStyle(Paint.Style.STROKE);
|
mBasePaint.setStyle(Paint.Style.STROKE);
|
||||||
mBasePaint.setStrokeWidth(1f);
|
mBasePaint.setStrokeWidth(1.2f);
|
||||||
mBasePaint.setColor(Color.parseColor("#33FFFFFF"));
|
mBasePaint.setColor(Color.parseColor("#26FFFFFF"));
|
||||||
for (final Path p : mSegmentPaths.values()) {
|
for (final Path p : mSegmentPaths.values()) {
|
||||||
canvas.drawPath(p, mBasePaint);
|
canvas.drawPath(p, mBasePaint);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.aldo.apps.ochecompanion.ui;
|
package com.aldo.apps.ochecompanion.ui;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceFragmentCompat;
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
import androidx.preference.SwitchPreference;
|
import androidx.preference.SwitchPreference;
|
||||||
@@ -15,6 +17,8 @@ import com.aldo.apps.ochecompanion.R;
|
|||||||
*/
|
*/
|
||||||
public class MainMenuPreferencesFragment extends PreferenceFragmentCompat {
|
public class MainMenuPreferencesFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
private static final String TAG = "PreferencesFragment";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the preference screen from the main_menu_preferences XML resource.
|
* Initializes the preference screen from the main_menu_preferences XML resource.
|
||||||
* Called automatically by the fragment lifecycle.
|
* Called automatically by the fragment lifecycle.
|
||||||
@@ -36,7 +40,38 @@ public class MainMenuPreferencesFragment extends PreferenceFragmentCompat {
|
|||||||
|
|
||||||
autoPref.setOnPreferenceChangeListener((preference, newValue) -> {
|
autoPref.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
boolean isAutoEnabled = (Boolean) newValue;
|
boolean isAutoEnabled = (Boolean) newValue;
|
||||||
|
Log.d(TAG, "Auto mode changed to: " + isAutoEnabled);
|
||||||
manualPref.setEnabled(!isAutoEnabled);
|
manualPref.setEnabled(!isAutoEnabled);
|
||||||
|
|
||||||
|
// Apply theme immediately and recreate activity to ensure system theme is detected
|
||||||
|
if (isAutoEnabled) {
|
||||||
|
Log.d(TAG, "Switching to MODE_NIGHT_FOLLOW_SYSTEM and recreating");
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
|
||||||
|
// Recreate activity to properly apply system theme
|
||||||
|
requireActivity().recreate();
|
||||||
|
} else {
|
||||||
|
// Use current manual preference
|
||||||
|
boolean isDarkMode = manualPref.isChecked();
|
||||||
|
Log.d(TAG, "Switching to manual mode, dark mode = " + isDarkMode);
|
||||||
|
AppCompatDelegate.setDefaultNightMode(
|
||||||
|
isDarkMode ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
manualPref.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
boolean isDarkMode = (Boolean) newValue;
|
||||||
|
Log.d(TAG, "Manual dark mode changed to: " + isDarkMode);
|
||||||
|
// Only apply if auto mode is disabled
|
||||||
|
if (!autoPref.isChecked()) {
|
||||||
|
Log.d(TAG, "Applying manual theme change");
|
||||||
|
AppCompatDelegate.setDefaultNightMode(
|
||||||
|
isDarkMode ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Ignoring manual change - auto mode is enabled");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,28 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Base.Theme.OcheCompanion" parent="Theme.Material3.DayNight.NoActionBar">
|
<style name="Base.Theme.OcheCompanion" parent="Theme.Material3.Dark.NoActionBar">
|
||||||
<!-- Customize your dark theme here. -->
|
<!-- Primary brand color -->
|
||||||
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
|
<item name="colorPrimary">@color/volt_green</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/volt_green</item>
|
||||||
|
<item name="colorOnPrimary">@color/midnight_black</item>
|
||||||
|
|
||||||
|
<!-- Secondary brand color -->
|
||||||
|
<item name="colorSecondary">@color/volt_green</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/volt_green</item>
|
||||||
|
<item name="colorOnSecondary">@color/midnight_black</item>
|
||||||
|
|
||||||
|
<!-- Background colors -->
|
||||||
|
<item name="android:colorBackground">@color/background_primary</item>
|
||||||
|
<item name="colorSurface">@color/surface_primary</item>
|
||||||
|
<item name="colorOnBackground">@color/text_primary</item>
|
||||||
|
<item name="colorOnSurface">@color/text_primary</item>
|
||||||
|
|
||||||
|
<!-- Status bar -->
|
||||||
|
<item name="android:statusBarColor">@color/background_primary</item>
|
||||||
|
<item name="android:windowLightStatusBar">false</item>
|
||||||
|
|
||||||
|
<!-- Navigation bar -->
|
||||||
|
<item name="android:navigationBarColor">@color/surface_primary</item>
|
||||||
|
<item name="android:windowLightNavigationBar">false</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,8 +1,29 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Base.Theme.OcheCompanion" parent="Theme.Material3.DayNight.NoActionBar">
|
<style name="Base.Theme.OcheCompanion" parent="Theme.Material3.Light.NoActionBar">
|
||||||
<!-- Customize your light theme here. -->
|
<!-- Primary brand color -->
|
||||||
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
<item name="colorPrimary">@color/volt_green</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/volt_green</item>
|
||||||
|
<item name="colorOnPrimary">@color/text_on_volt</item>
|
||||||
|
|
||||||
|
<!-- Secondary brand color -->
|
||||||
|
<item name="colorSecondary">@color/volt_green</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/volt_green</item>
|
||||||
|
<item name="colorOnSecondary">@color/text_on_volt</item>
|
||||||
|
|
||||||
|
<!-- Background colors -->
|
||||||
|
<item name="android:colorBackground">@color/background_primary</item>
|
||||||
|
<item name="colorSurface">@color/surface_primary</item>
|
||||||
|
<item name="colorOnBackground">@color/text_primary</item>
|
||||||
|
<item name="colorOnSurface">@color/text_primary</item>
|
||||||
|
|
||||||
|
<!-- Status bar -->
|
||||||
|
<item name="android:statusBarColor">@color/background_primary</item>
|
||||||
|
<item name="android:windowLightStatusBar">true</item>
|
||||||
|
|
||||||
|
<!-- Navigation bar -->
|
||||||
|
<item name="android:navigationBarColor">@color/surface_primary</item>
|
||||||
|
<item name="android:windowLightNavigationBar">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.OcheCompanion" parent="Base.Theme.OcheCompanion" />
|
<style name="Theme.OcheCompanion" parent="Base.Theme.OcheCompanion" />
|
||||||
|
|||||||
Reference in New Issue
Block a user