diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/GameActivity.java b/app/src/main/java/com/aldo/apps/ochecompanion/GameActivity.java index eeb89ae..40adae6 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/GameActivity.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/GameActivity.java @@ -70,6 +70,11 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC */ private static final String EXTRA_MATCH_ID = "extra_match_uuid"; + /** + * Intent extra for a player list. Making it possible to start a match with pre-defined players. + */ + private static final String EXTRA_PLAYERS = "extra_players"; + // ======================================================================================== // Game Manager (Singleton Business Logic Handler) @@ -196,9 +201,18 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC * @param matchId The ID of the match to be started/loaded. */ public static void start(final Context context, final int startScore, final int matchId) { + final GameManager gameManager = GameManager.getInstance(context); + gameManager.initializeMatch(matchId, startScore,null); + + Intent intent = new Intent(context, GameActivity.class); + context.startActivity(intent); + } + + public static void start(final Context context, final List players, final int startScore) { + final GameManager gameManager = GameManager.getInstance(context); + gameManager.initializeMatch(-1, startScore, players,null); + Intent intent = new Intent(context, GameActivity.class); - intent.putExtra(EXTRA_START_SCORE, startScore); - intent.putExtra(EXTRA_MATCH_ID, matchId); context.startActivity(intent); } @@ -223,16 +237,12 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC // Initialize GameManager singleton mGameManager = GameManager.getInstance(this); - mGameManager.setCallback(this); - // Extract game parameters from intent - final int startingScore = getIntent().getIntExtra(EXTRA_START_SCORE, DartsConstants.DEFAULT_GAME_SCORE); - final int matchId = getIntent().getIntExtra(EXTRA_MATCH_ID, -1); - // Initialize activity components in order initViews(); setupKeyboard(); + mGameManager.setCallback(this); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override @@ -240,13 +250,9 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC handleBackPressed(); } }); - - // Initialize match through GameManager (handles loading existing or creating new) - mGameManager.initializeMatch(matchId, startingScore, () -> runOnUiThread(() -> { - updateUI(); - updateTurnIndicators(); - setMultiplier(DartsConstants.MULTIPLIER_SINGLE); - })); + updateUI(); + updateTurnIndicators(); + setMultiplier(DartsConstants.MULTIPLIER_SINGLE); } @Override diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/MainMenuActivity.java b/app/src/main/java/com/aldo/apps/ochecompanion/MainMenuActivity.java index ff9f05a..68e6b82 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/MainMenuActivity.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/MainMenuActivity.java @@ -3,6 +3,8 @@ package com.aldo.apps.ochecompanion; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; + +import com.aldo.apps.ochecompanion.ui.PlayerSelectionDialogFragment; import com.aldo.apps.ochecompanion.utils.Log; import android.widget.TextView; @@ -145,10 +147,13 @@ public class MainMenuActivity extends BaseActivity { } catch (final NumberFormatException exception) { Log.e(TAG, "quickStart: Could not extract score from [" + mOngoingMatch + "], use default"); } + Log.d(TAG, "quickStart: Starting match with StartingScore = [" + startingScore + + "] and matchId = [" + matchId + "]"); + GameActivity.start(MainMenuActivity.this, startingScore, matchId); + } else { + Log.d(TAG, "quickStart: No ongoing matches found"); + new PlayerSelectionDialogFragment().show(getSupportFragmentManager(), "squad_selector"); } - Log.d(TAG, "quickStart: Starting match with StartingScore = [" + startingScore - + "] and matchId = [" + matchId + "]"); - GameActivity.start(MainMenuActivity.this, startingScore, matchId); } /** diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/MatchDao.java b/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/MatchDao.java index 690dd26..de1cd5d 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/MatchDao.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/database/dao/MatchDao.java @@ -88,4 +88,12 @@ public interface MatchDao { */ @Query("SELECT * FROM matches WHERE state = :state ORDER BY timestamp DESC") List getMatchesByState(final String state); + + /** + * Retrieves ONLY the participant JSON string from the most recent match. + * Used for the "Auto-select last players" feature in the Squad Selector. + */ + @Query("SELECT participantData FROM matches ORDER BY timestamp DESC LIMIT 1") + String getLastMatchParticipantData(); + } diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/game/GameManager.java b/app/src/main/java/com/aldo/apps/ochecompanion/game/GameManager.java index 3e79cb0..ca58d05 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/game/GameManager.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/game/GameManager.java @@ -149,7 +149,69 @@ public class GameManager { */ public void setCallback(final GameStateCallback callback) { mCallback = callback; + //Send one initial callback + notifyGameStateChanged(); } + + public void initializeMatch(final int matchId,final int startingScore, final List players, final Runnable onComplete) { + mStartingScore = startingScore; + mMatchId = matchId; + + new Thread(() -> { + Match match = null; + if (matchId > 0) { + // Try to load existing match + match = mDatabaseHelper.getMatchById(matchId); + Log.d(TAG, "initializeMatch: Loaded match from DB: " + match); + + + if (match != null && match.participantData != null && !match.participantData.isEmpty()) { + // Load match progress from database + try { + final MatchProgress progress = MatchProgressConverter.fromString(match.participantData); + if (progress != null) { + Log.d(TAG, "initializeMatch: Found saved progress with " + progress.players.size() + " players"); + // Initialize player states + initializePlayerStates(players); + loadMatchProgress(progress); + + if (onComplete != null) { + onComplete.run(); + } + notifyGameStateChanged(); + return; + } else { + Log.w(TAG, "initializeMatch: Progress is null, treating as new match"); + match = null; + } + } catch (Exception e) { + Log.e(TAG, "initializeMatch: Failed to load match progress", e); + match = null; + } + } + } + + // Create new match if not found or invalid + if (match == null) { + final long newMatchId = mDatabaseHelper.createNewMatch(String.valueOf(startingScore), players); + if (newMatchId > 0) { + mMatchId = (int) newMatchId; + Log.d(TAG, "initializeMatch: Created new match with ID: " + mMatchId); + } else { + Log.e(TAG, "initializeMatch: Failed to create new match"); + } + + // Setup new game + initializePlayerStates(players); + } + + if (onComplete != null) { + onComplete.run(); + } + notifyGameStateChanged(); + }).start(); + } + /** * Initializes a new game or loads an existing match from the database. @@ -166,58 +228,7 @@ public class GameManager { new Thread(() -> { final List allPlayers = mDatabaseHelper.getAllPlayers(); Log.d(TAG, "initializeMatch: Loading players, count = " + (allPlayers != null ? allPlayers.size() : 0)); - - Match match = null; - if (matchId > 0) { - // Try to load existing match - match = mDatabaseHelper.getMatchById(matchId); - Log.d(TAG, "initializeMatch: Loaded match from DB: " + match); - - - if (match != null && match.participantData != null && !match.participantData.isEmpty()) { - // Load match progress from database - try { - final MatchProgress progress = MatchProgressConverter.fromString(match.participantData); - if (progress != null) { - Log.d(TAG, "initializeMatch: Found saved progress with " + progress.players.size() + " players"); - // Initialize player states - initializePlayerStates(allPlayers); - loadMatchProgress(progress); - - if (onComplete != null) { - onComplete.run(); - } - notifyGameStateChanged(); - return; - } else { - Log.w(TAG, "initializeMatch: Progress is null, treating as new match"); - match = null; - } - } catch (Exception e) { - Log.e(TAG, "initializeMatch: Failed to load match progress", e); - match = null; - } - } - } - - // Create new match if not found or invalid - if (match == null) { - final long newMatchId = mDatabaseHelper.createNewMatch(String.valueOf(startingScore), allPlayers); - if (newMatchId > 0) { - mMatchId = (int) newMatchId; - Log.d(TAG, "initializeMatch: Created new match with ID: " + mMatchId); - } else { - Log.e(TAG, "initializeMatch: Failed to create new match"); - } - - // Setup new game - initializePlayerStates(allPlayers); - } - - if (onComplete != null) { - onComplete.run(); - } - notifyGameStateChanged(); + initializeMatch(matchId, startingScore, allPlayers, onComplete); }).start(); } diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerSelectionDialogFragment.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerSelectionDialogFragment.java new file mode 100644 index 0000000..d05cc0c --- /dev/null +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerSelectionDialogFragment.java @@ -0,0 +1,134 @@ +package com.aldo.apps.ochecompanion.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.aldo.apps.ochecompanion.GameActivity; +import com.aldo.apps.ochecompanion.R; +import com.aldo.apps.ochecompanion.database.AppDatabase; +import com.aldo.apps.ochecompanion.database.objects.Player; +import com.aldo.apps.ochecompanion.ui.adapter.PlayerSelectionAdapter; +import com.aldo.apps.ochecompanion.utils.DartsConstants; +import com.aldo.apps.ochecompanion.utils.MatchProgress; +import com.aldo.apps.ochecompanion.utils.converters.MatchProgressConverter; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.android.material.button.MaterialButton; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * PlayerSelectionDialogFragment: A modern bottom-sheet selector for match participants. + * Automatically pre-selects players from the most recent match for speed. + */ +public class PlayerSelectionDialogFragment extends BottomSheetDialogFragment { + + /** + * Tag for debugging and logging purposes. + */ + private static final String TAG = "PlayerSelectionDialogFragment"; + + /** + * The {@link List} of the selected {@link Player} for the match. + */ + private final List mAllPlayers = new ArrayList<>(); + private final Set mSelectedIds = new HashSet<>(); + + private RecyclerView mRvSquad; + private MaterialButton mBtnStart; + private PlayerSelectionAdapter mAdapter; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.layout_player_selection_sheet, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mRvSquad = view.findViewById(R.id.rvSquadSelection); + mBtnStart = view.findViewById(R.id.btnConfirmSelection); + + mRvSquad.setLayoutManager(new LinearLayoutManager(getContext())); + mAdapter = new PlayerSelectionAdapter(mAllPlayers, mSelectedIds, this::onSelectionChanged); + mRvSquad.setAdapter(mAdapter); + + mBtnStart.setOnClickListener(v -> initiateMatch()); + + loadData(); + } + + private void loadData() { + new Thread(() -> { + AppDatabase db = AppDatabase.getDatabase(requireContext()); + + // 1. Get All Players + List players = db.playerDao().getAllPlayers(); + + // 2. Get Last Participants for Pre-selection + String lastJson = db.matchDao().getLastMatchParticipantData(); + MatchProgress lastProgress = MatchProgressConverter.fromString(lastJson); + + final Set lastPlayerIds = new HashSet<>(); + if (lastProgress != null && lastProgress.players != null) { + for (MatchProgress.PlayerStateSnapshot p : lastProgress.players) { + lastPlayerIds.add((int) p.playerId); + } + } + + if (getActivity() != null) { + getActivity().runOnUiThread(() -> { + mAllPlayers.clear(); + mAllPlayers.addAll(players); + + // Auto-select players from the previous session + mSelectedIds.clear(); + for (Player p : mAllPlayers) { + if (lastPlayerIds.contains((int) p.id)) { + mSelectedIds.add((int) p.id); + } + } + + mAdapter.notifyDataSetChanged(); + updateButtonState(); + }); + } + }).start(); + } + + private void onSelectionChanged() { + updateButtonState(); + } + + private void updateButtonState() { + int count = mSelectedIds.size(); + mBtnStart.setEnabled(count > 0); + mBtnStart.setText(count > 0 ? "START MATCH (" + count + ")" : "SELECT PLAYERS"); + } + + private void initiateMatch() { + ArrayList selectedList = new ArrayList<>(); + for (Player p : mAllPlayers) { + if (mSelectedIds.contains((int) p.id)) { + selectedList.add(p); + } + } + + if (selectedList.isEmpty()) return; + + // Start the game! + GameActivity.start(requireContext(), selectedList, DartsConstants.DEFAULT_GAME_SCORE); + dismiss(); + } +} diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/PlayerSelectionAdapter.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/PlayerSelectionAdapter.java new file mode 100644 index 0000000..69e86d7 --- /dev/null +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/PlayerSelectionAdapter.java @@ -0,0 +1,105 @@ +package com.aldo.apps.ochecompanion.ui.adapter; + +import android.content.Context; +import android.os.Vibrator; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; +import com.aldo.apps.ochecompanion.R; +import com.aldo.apps.ochecompanion.database.objects.Player; +import com.bumptech.glide.Glide; +import com.google.android.material.card.MaterialCardView; +import com.google.android.material.imageview.ShapeableImageView; +import java.util.List; +import java.util.Set; + +/** + * PlayerSelectionAdapter: Optimized for the "Squad Selector" Bottom Sheet. + * Features specialized selection visual states and haptic feedback. + */ +public class PlayerSelectionAdapter extends RecyclerView.Adapter { + + private final List mPlayers; + private final Set mSelectedIds; + private final Runnable mOnChanged; + + public PlayerSelectionAdapter(List players, Set selected, Runnable onChanged) { + this.mPlayers = players; + this.mSelectedIds = selected; + this.mOnChanged = onChanged; + } + + @NonNull + @Override + public SelectionHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_player_selection, parent, false); + return new SelectionHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull SelectionHolder holder, int position) { + Player p = mPlayers.get(position); + holder.bind(p, mSelectedIds.contains((int) p.id)); + + holder.itemView.setOnClickListener(v -> { + // Haptic Feedback + Vibrator vib = (Vibrator) v.getContext().getSystemService(Context.VIBRATOR_SERVICE); + if (vib != null) vib.vibrate(20); + + if (mSelectedIds.contains((int) p.id)) { + mSelectedIds.remove((int) p.id); + } else { + if (mSelectedIds.size() < 8) { // PDC/Standard limit + mSelectedIds.add((int) p.id); + } + } + notifyItemChanged(position); + mOnChanged.run(); + }); + } + + @Override + public int getItemCount() { + return mPlayers.size(); + } + + static class SelectionHolder extends RecyclerView.ViewHolder { + private final ShapeableImageView ivAvatar; + private final TextView tvName; + private final MaterialCardView card; + private final View selectionIndicator; + + public SelectionHolder(@NonNull View itemView) { + super(itemView); + ivAvatar = itemView.findViewById(R.id.ivPlayerProfile); + tvName = itemView.findViewById(R.id.tvPlayerName); + card = (MaterialCardView) itemView; + selectionIndicator = itemView.findViewById(R.id.selectionOverlay); + } + + public void bind(Player p, boolean isSelected) { + tvName.setText(p.username); + + if (p.profilePictureUri != null) { + Glide.with(itemView.getContext()).load(p.profilePictureUri).into(ivAvatar); + } else { + ivAvatar.setImageResource(R.drawable.ic_users); + } + + // Visual toggle + if (isSelected) { + card.setStrokeColor(ContextCompat.getColor(itemView.getContext(), R.color.volt_green)); + card.setStrokeWidth(4); + selectionIndicator.setVisibility(View.VISIBLE); + } else { + card.setStrokeColor(ContextCompat.getColor(itemView.getContext(), R.color.border_subtle)); + card.setStrokeWidth(1); + selectionIndicator.setVisibility(View.GONE); + } + } + } +} diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/utils/converters/MatchProgressConverter.java b/app/src/main/java/com/aldo/apps/ochecompanion/utils/converters/MatchProgressConverter.java index 05e49de..4a61870 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/utils/converters/MatchProgressConverter.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/utils/converters/MatchProgressConverter.java @@ -1,21 +1,31 @@ package com.aldo.apps.ochecompanion.utils.converters; +import android.util.Log; + import androidx.room.TypeConverter; import com.aldo.apps.ochecompanion.utils.MatchProgress; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; /** * MatchProgressConverter: Handles serialization of complex match states * into JSON strings for storage in the Room 'matches' table. */ public class MatchProgressConverter { + + private static final String TAG = "MatchProgressConverter"; private static final Gson gson = new Gson(); @TypeConverter public static MatchProgress fromString(String value) { if (value == null || value.isEmpty()) return null; - return gson.fromJson(value, MatchProgress.class); + try { + return gson.fromJson(value, MatchProgress.class); + } catch (final JsonSyntaxException ex) { + Log.e(TAG, "fromString: Failed to parse json, return null", ex); + } + return null; } @TypeConverter diff --git a/app/src/main/res/layout/item_player_selection.xml b/app/src/main/res/layout/item_player_selection.xml new file mode 100644 index 0000000..c48446a --- /dev/null +++ b/app/src/main/res/layout/item_player_selection.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_player_selection_sheet.xml b/app/src/main/res/layout/layout_player_selection_sheet.xml new file mode 100644 index 0000000..69767f6 --- /dev/null +++ b/app/src/main/res/layout/layout_player_selection_sheet.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file