Added primaryPlayer functionality

Added a new functionality to mark one player as the primary player. This player is the main user of the app and the training data will be stored for that player only.
This commit is contained in:
Alexander Doerflinger
2026-02-17 08:09:15 +01:00
parent 796e0e4389
commit 0d71d93ea6
10 changed files with 160 additions and 17 deletions

View File

@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-01-26T12:37:47.296146997Z">
<DropdownSelection timestamp="2026-02-06T14:54:23.364337885Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/home/aldo270717/.android/avd/Pixel_9_Pro_API_36.avd" />
<DeviceId pluginId="LocalEmulator" identifier="path=/home/aldo270717/.android/avd/Pixel_9_Pro.avd" />
</handle>
</Target>
</DropdownSelection>

View File

@@ -4,9 +4,7 @@ plugins {
android {
namespace = "com.aldo.apps.ochecompanion"
compileSdk {
version = release(36)
}
compileSdk = 36
defaultConfig {
applicationId = "com.aldo.apps.ochecompanion"

View File

@@ -104,6 +104,16 @@ public class AddPlayerActivity extends BaseActivity {
*/
private ImageView mBtnDelete;
/**
* Switch to toggle primary user status.
*/
private com.google.android.material.switchmaterial.SwitchMaterial mPrimaryUserSwitch;
/**
* Icon that gets tinted when primary user is selected.
*/
private ImageView mPrimaryUserIcon;
// ========== UI - Cropper Views ==========
/**
@@ -276,6 +286,25 @@ public class AddPlayerActivity extends BaseActivity {
mIvCropPreview = findViewById(R.id.ivCropPreview);
mCropOverlay = findViewById(R.id.cropOverlay);
// Get references to primary user toggle
mPrimaryUserSwitch = findViewById(R.id.switchPrimaryUser);
mPrimaryUserIcon = findViewById(R.id.ivPrimaryUserIcon);
// Set up primary user toggle listener to tint icon
mPrimaryUserSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
// Tint icon with volt green when selected
mPrimaryUserIcon.setImageTintList(
android.content.res.ColorStateList.valueOf(
getColor(R.color.volt_green)));
} else {
// Reset to dim color when unselected
mPrimaryUserIcon.setImageTintList(
android.content.res.ColorStateList.valueOf(
getColor(R.color.text_dim)));
}
});
// Set up click listeners
mProfilePictureView.setOnClickListener(v -> checkForPermissionAndLaunchImagePicker());
mSaveButton.setOnClickListener(v -> savePlayer());
@@ -549,6 +578,9 @@ public class AddPlayerActivity extends BaseActivity {
mProfilePictureView.setImageTintList(null); // Remove placeholder tint
mProfilePictureView.setImageURI(Uri.fromFile(new File(mInternalImagePath)));
}
// Set primary user toggle state
mPrimaryUserSwitch.setChecked(mExistingPlayer.isPrimaryUser);
}
});
}).start();
@@ -584,16 +616,26 @@ public class AddPlayerActivity extends BaseActivity {
return;
}
// Get primary user toggle state
final boolean isPrimaryUser = mPrimaryUserSwitch.isChecked();
// Perform database operation on background thread
new Thread(() -> {
// If setting as primary user, clear all other primary flags first
if (isPrimaryUser) {
mDatabaseHelper.clearAllPrimaryFlags();
}
if (mExistingPlayer != null) {
// Update existing player
mExistingPlayer.username = name;
mExistingPlayer.profilePictureUri = mInternalImagePath;
mExistingPlayer.isPrimaryUser = isPrimaryUser;
mDatabaseHelper.updatePlayer(mExistingPlayer);
} else {
// Create and insert new player
final Player p = new Player(name, mInternalImagePath);
p.isPrimaryUser = isPrimaryUser;
final long newUserId = mDatabaseHelper.insertPlayer(p);
final Player dbPlayer = mDatabaseHelper.getPlayerById(newUserId);
if (dbPlayer != null) {

View File

@@ -16,7 +16,8 @@ import com.aldo.apps.ochecompanion.utils.converters.HitDistributionConverter;
/**
* Main Room database class for the Oche Companion darts application.
* Manages data persistence for players, matches, and statistics using the Singleton pattern.
* Manages data persistence for players, matches, and statistics using the
* Singleton pattern.
* Uses version 2 with destructive migration (data lost on schema changes).
* Database operations must be performed on background threads.
*
@@ -25,7 +26,7 @@ import com.aldo.apps.ochecompanion.utils.converters.HitDistributionConverter;
* @see Player
* @see Match
*/
@Database(entities = {Player.class, Match.class, Statistics.class}, version = 14, exportSchema = false)
@Database(entities = { Player.class, Match.class, Statistics.class }, version = 15, exportSchema = false)
@TypeConverters({ HitDistributionConverter.class })
public abstract class AppDatabase extends RoomDatabase {
@@ -64,9 +65,11 @@ public abstract class AppDatabase extends RoomDatabase {
/**
* Gets the singleton AppDatabase instance, creating it if necessary.
* Thread-safe using double-checked locking. Uses application context to prevent leaks.
* Thread-safe using double-checked locking. Uses application context to prevent
* leaks.
*
* @param context Context used to create the database (converted to application context)
* @param context Context used to create the database (converted to application
* context)
* @return Singleton AppDatabase instance
*/
public static AppDatabase getDatabase(final Context context) {
@@ -91,5 +94,3 @@ public abstract class AppDatabase extends RoomDatabase {
return sInstance;
}
}

View File

@@ -577,6 +577,34 @@ public class DatabaseHelper {
});
}
/**
* Retrieves the current primary user from the database.
*
* @return The primary user, or null if none is set
*/
public Player getPrimaryUser() {
try {
return mExecutor.submit(() -> mDatabase.playerDao().getPrimaryUser()).get();
} catch (Exception e) {
Log.e(TAG, "getPrimaryUser: Failed to get primary user", e);
return null;
}
}
/**
* Clears the primary user flag from all players.
* Should be called before setting a new primary user to ensure only one exists.
*/
public void clearAllPrimaryFlags() {
mExecutor.execute(() -> {
try {
mDatabase.playerDao().clearAllPrimaryFlags();
} catch (Exception e) {
Log.e(TAG, "clearAllPrimaryFlags: Failed to clear flags", e);
}
});
}
/**
* Retrieves a player by their database ID synchronously.
* Blocks until the operation completes to ensure consistency with any pending

View File

@@ -58,11 +58,29 @@ public interface PlayerDao {
Player getPlayerById(final long id);
/**
* Retrieves all players ordered alphabetically by username.
* Retrieves all players with primary user first, then alphabetically by
* username.
* Must be called on a background thread.
*
* @return List of all players sorted A-Z
* @return List of all players with primary user at top, rest sorted A-Z
*/
@Query("SELECT * FROM players ORDER BY username ASC")
@Query("SELECT * FROM players ORDER BY isPrimaryUser DESC, username ASC")
List<Player> getAllPlayers();
/**
* Retrieves the current primary user.
* Must be called on a background thread.
*
* @return The primary user, or null if none is set
*/
@Query("SELECT * FROM players WHERE isPrimaryUser = 1 LIMIT 1")
Player getPrimaryUser();
/**
* Clears the primary user flag from all players.
* Used before setting a new primary user to ensure only one exists.
* Must be called on a background thread.
*/
@Query("UPDATE players SET isPrimaryUser = 0")
void clearAllPrimaryFlags();
}

View File

@@ -46,6 +46,11 @@ public class Player implements Parcelable {
*/
public int matchesPlayed = 0;
/**
* Flag indicating whether this player is the primary user or not.
*/
public boolean isPrimaryUser = false;
/**
* Constructs a new Player ready for database insertion. ID is auto-generated
* by Room, statistical fields default to 0.
@@ -69,6 +74,7 @@ public class Player implements Parcelable {
profilePictureUri = in.readString();
careerAverage = in.readDouble();
matchesPlayed = in.readInt();
isPrimaryUser = in.readByte() != 0;
}
/**
@@ -104,6 +110,7 @@ public class Player implements Parcelable {
dest.writeString(profilePictureUri);
dest.writeDouble(careerAverage);
dest.writeInt(matchesPlayed);
dest.writeByte((byte) (isPrimaryUser ? 1 : 0));
}
/**
@@ -121,6 +128,7 @@ public class Player implements Parcelable {
", mProfilePictureUri='" + profilePictureUri + '\'' +
", mCareerAverage=" + careerAverage +
", mMatchesPlayed=" + matchesPlayed +
", isPrimaryUser=" + isPrimaryUser +
'}';
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- Outline version for unselected state -->
<path
android:strokeColor="@color/text_dim"
android:strokeWidth="1.5"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:pathData="M5,16 L3,5 L8.5,10 L12,4 L15.5,10 L21,5 L19,16 L5,16 Z M5,18 L19,18" />
</vector>

View File

@@ -61,6 +61,39 @@
android:textColor="@color/text_primary" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Primary User Toggle -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="@color/surface_secondary"
android:padding="16dp">
<ImageView
android:id="@+id/ivPrimaryUserIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_favorite_profile"
app:tint="@color/text_dim" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:text="@string/txt_primary_user_label"
android:textColor="@color/text_primary"
android:textSize="16sp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchPrimaryUser"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnShowStats"
style="@style/Widget.Oche_Button_Primary"

View File

@@ -18,6 +18,7 @@
<string name="txt_create_profile_username_hint">Add your username</string>
<string name="txt_create_profile_username_save">Save to Squad</string>
<string name="txt_create_profile_show_stats">Show Stats</string>
<string name="txt_primary_user_label">Set as Primary User</string>
<string name="txt_update_profile_header">Update Profile</string>
<string name="txt_update_profile_username_save">Update Squad</string>
<string name="txt_cancel_crop">Cancel</string>