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:supportsRtl="true"
|
||||
android:theme="@style/Theme.OcheCompanion">
|
||||
<activity android:name=".BaseActivity"
|
||||
android:exported="false"
|
||||
android:configChanges="uiMode"/>
|
||||
<activity
|
||||
android:name=".TestActivity"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:configChanges="uiMode" />
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_settings" />
|
||||
android:label="@string/title_activity_settings"
|
||||
android:configChanges="uiMode" />
|
||||
<activity
|
||||
android:name=".GameActivity"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:configChanges="uiMode" />
|
||||
<activity
|
||||
android:name=".AddPlayerActivity"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:configChanges="uiMode" />
|
||||
<activity
|
||||
android:name=".MainMenuActivity"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:configChanges="uiMode">
|
||||
<intent-filter>
|
||||
<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).
|
||||
* 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.
|
||||
|
||||
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.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.Statistics;
|
||||
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,
|
||||
* 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";
|
||||
|
||||
@@ -237,15 +237,9 @@ public class GameActivity extends AppCompatActivity {
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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<>();
|
||||
private DatabaseHelper mDatabaseHelper;
|
||||
|
||||
/**
|
||||
* Starts GameActivity with specified players and starting score.
|
||||
@@ -279,19 +273,41 @@ public class GameActivity extends AppCompatActivity {
|
||||
return insets;
|
||||
});
|
||||
mSoundEngine = SoundEngine.getInstance(this);
|
||||
mDatabase = AppDatabase.getDatabase(this);
|
||||
mDatabaseHelper = DatabaseHelper.getInstance(this);
|
||||
|
||||
// Extract game parameters from intent
|
||||
mStartingScore = getIntent().getIntExtra(EXTRA_START_SCORE, DartsConstants.DEFAULT_GAME_SCORE);
|
||||
mMatchUuid = getIntent().getStringExtra(EXTRA_MATCH_UUID);
|
||||
|
||||
// Initialize activity components in order
|
||||
initViews();
|
||||
setupKeyboard();
|
||||
|
||||
// Only setup a new game if we're not restoring from saved state
|
||||
if (savedInstanceState == null) {
|
||||
new Thread(() -> {
|
||||
final List<Player> allAvailablePlayers = AppDatabase.getDatabase(GameActivity.this).playerDao().getAllPlayers();
|
||||
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
|
||||
@@ -302,6 +318,113 @@ public class GameActivity extends AppCompatActivity {
|
||||
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.
|
||||
*/
|
||||
@@ -497,7 +620,7 @@ public class GameActivity extends AppCompatActivity {
|
||||
/**
|
||||
* Updates player statistics in the database after a turn.
|
||||
* 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 dartsThrown Number of darts thrown this turn
|
||||
@@ -519,69 +642,18 @@ public class GameActivity extends AppCompatActivity {
|
||||
* @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) {
|
||||
new Thread(() -> {
|
||||
final Player player = active.player;
|
||||
if (player != null && player.id != 0) {
|
||||
final Statistics playerStats = AppDatabase.getDatabase(GameActivity.this).statisticsDao().getStatisticsForPlayer(player.id);
|
||||
|
||||
// Track darts thrown or missed
|
||||
if (wasBust) {
|
||||
// On bust, all darts in the turn are wasted
|
||||
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);
|
||||
if (active.player != null && active.player.id != 0) {
|
||||
mDatabaseHelper.updatePlayerStatistics(
|
||||
active.player.id,
|
||||
dartsThrown,
|
||||
pointsMade,
|
||||
wasBust,
|
||||
checkoutValue,
|
||||
active.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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes current turn and advances to next player.
|
||||
* Updates player score (unless bust), rotates to next player, resets turn state.
|
||||
@@ -781,50 +853,33 @@ public class GameActivity extends AppCompatActivity {
|
||||
/**
|
||||
* Tracks a double-out attempt in player statistics.
|
||||
* Records whether the attempt was successful or missed.
|
||||
* Delegates to DatabaseHelper for thread-safe execution.
|
||||
*
|
||||
* @param playerState X01State of the player
|
||||
* @param isMissed true if the double-out was missed, false if successful
|
||||
*/
|
||||
private void trackDoubleAttempt(final X01State playerState, final boolean isMissed) {
|
||||
new Thread(() -> {
|
||||
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();
|
||||
mDatabaseHelper.trackDoubleAttempt(playerState.playerId, isMissed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments matchesPlayed counter for all players in the current match.
|
||||
* Called when a match is completed (someone wins).
|
||||
* Delegates to DatabaseHelper for thread-safe execution.
|
||||
*/
|
||||
private void incrementMatchesPlayed() {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
final List<Long> playerIds = new ArrayList<>();
|
||||
for (X01State playerState : mPlayerStates) {
|
||||
Statistics playerStats = mDatabase.statisticsDao().getStatisticsForPlayer(playerState.playerId);
|
||||
if (playerStats != null) {
|
||||
playerStats.addCompletedMatch();
|
||||
mDatabase.statisticsDao().updateStatistics(playerStats);
|
||||
Log.d(TAG, "incrementMatchesPlayed: Incremented for player " + playerState.name + ", total: " + playerStats.getMatchesPlayed());
|
||||
playerIds.add(playerState.playerId);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "incrementMatchesPlayed: Failed to increment matches", e);
|
||||
}
|
||||
}).start();
|
||||
mDatabaseHelper.incrementMatchesPlayed(playerIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records all dart hits from a confirmed turn to the player's statistics.
|
||||
* Updates the hit distribution map for heat map visualization.
|
||||
* 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 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) {
|
||||
if (dartHits.isEmpty()) return;
|
||||
|
||||
new Thread(() -> {
|
||||
// Get or create lock object for this player to prevent race conditions
|
||||
final Object lock;
|
||||
synchronized (mPlayerStatsLocks) {
|
||||
lock = mPlayerStatsLocks.computeIfAbsent(playerState.playerId, k -> new Object());
|
||||
// Convert local DartHit to DatabaseHelper.DartHit
|
||||
final List<DatabaseHelper.DartHit> dbDartHits = new ArrayList<>();
|
||||
for (DartHit hit : dartHits) {
|
||||
dbDartHits.add(new DatabaseHelper.DartHit(hit.baseValue, hit.multiplier));
|
||||
}
|
||||
|
||||
// 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();
|
||||
mDatabaseHelper.recordDartHits(playerState.playerId, dbDartHits);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -906,12 +941,12 @@ public class GameActivity extends AppCompatActivity {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
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(() -> {
|
||||
mStatsView.bind(player, statistics);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "attachPlayerStats: Failed to increment matches", e);
|
||||
Log.e(TAG, "attachPlayerStats: Failed to retrieve player statistics", e);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.EdgeToEdge;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
@@ -32,7 +31,7 @@ import java.util.List;
|
||||
* Main entry point and home screen of the Oche Companion application.
|
||||
* Displays the squad of players, allows adding new players, and shows match recap with test data.
|
||||
*/
|
||||
public class MainMenuActivity extends AppCompatActivity {
|
||||
public class MainMenuActivity extends BaseActivity {
|
||||
|
||||
/**
|
||||
* Tag for debugging and logging purposes.
|
||||
|
||||
@@ -15,7 +15,7 @@ import com.aldo.apps.ochecompanion.ui.MainMenuPreferencesFragment;
|
||||
* Hosts the MainMenuPreferencesFragment which displays available settings
|
||||
* 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.
|
||||
|
||||
@@ -16,7 +16,7 @@ import com.aldo.apps.ochecompanion.ui.HeatmapView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TestActivity extends AppCompatActivity {
|
||||
public class TestActivity extends BaseActivity {
|
||||
|
||||
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
|
||||
* 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 {
|
||||
|
||||
@@ -114,27 +118,35 @@ public class HeatmapView extends View {
|
||||
super.onDraw(canvas);
|
||||
if (mStats == null) return;
|
||||
|
||||
final int coldColor = Color.parseColor("#1AFFFFFF"); // Faded ghost board
|
||||
final int hotColor = ContextCompat.getColor(getContext(), R.color.volt_green);
|
||||
// Resolve branding colors from resources
|
||||
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()) {
|
||||
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;
|
||||
|
||||
// Check if there are any hits recorded for this segment
|
||||
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);
|
||||
final int color = (int) mColorEvaluator.evaluate(weight, coldColor, hotColor);
|
||||
color = (int) mColorEvaluator.evaluate(weight, coldColor, hotColor);
|
||||
}
|
||||
|
||||
mBasePaint.setColor(color);
|
||||
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.setStrokeWidth(1f);
|
||||
mBasePaint.setColor(Color.parseColor("#33FFFFFF"));
|
||||
mBasePaint.setStrokeWidth(1.2f);
|
||||
mBasePaint.setColor(Color.parseColor("#26FFFFFF"));
|
||||
for (final Path p : mSegmentPaths.values()) {
|
||||
canvas.drawPath(p, mBasePaint);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.aldo.apps.ochecompanion.ui;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.SwitchPreference;
|
||||
@@ -15,6 +17,8 @@ import com.aldo.apps.ochecompanion.R;
|
||||
*/
|
||||
public class MainMenuPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
private static final String TAG = "PreferencesFragment";
|
||||
|
||||
/**
|
||||
* Initializes the preference screen from the main_menu_preferences XML resource.
|
||||
* Called automatically by the fragment lifecycle.
|
||||
@@ -36,7 +40,38 @@ public class MainMenuPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
autoPref.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
boolean isAutoEnabled = (Boolean) newValue;
|
||||
Log.d(TAG, "Auto mode changed to: " + 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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Base.Theme.OcheCompanion" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your dark theme here. -->
|
||||
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
|
||||
<style name="Base.Theme.OcheCompanion" parent="Theme.Material3.Dark.NoActionBar">
|
||||
<!-- Primary brand color -->
|
||||
<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>
|
||||
</resources>
|
||||
@@ -1,8 +1,29 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Base.Theme.OcheCompanion" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your light theme here. -->
|
||||
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
||||
<style name="Base.Theme.OcheCompanion" parent="Theme.Material3.Light.NoActionBar">
|
||||
<!-- Primary brand color -->
|
||||
<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 name="Theme.OcheCompanion" parent="Base.Theme.OcheCompanion" />
|
||||
|
||||
Reference in New Issue
Block a user