From 4dc228d185e409779bd8d7f135c4f94364d59e27 Mon Sep 17 00:00:00 2001
From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Sun, 21 Jun 2026 01:42:51 -0700
Subject: [PATCH] Add option to display card ID in card list
---
.../card_locker/LoyaltyCardCursorAdapter.java | 9 ++-
.../LoyaltyCardListDisplayOptionsManager.java | 18 +++++
app/src/main/res/drawable/ic_card_id_24dp.xml | 10 +++
.../main/res/layout/loyalty_card_layout.xml | 21 +++++-
app/src/main/res/values/strings.xml | 2 +
.../protect/card_locker/MainActivityTest.java | 69 +++++++++++++++++++
6 files changed, 127 insertions(+), 2 deletions(-)
create mode 100644 app/src/main/res/drawable/ic_card_id_24dp.xml
diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java
index 84968d753b..537b85c071 100644
--- a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java
+++ b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java
@@ -123,6 +123,12 @@ public void onBindViewHolder(LoyaltyCardListItemViewHolder inputHolder, Cursor i
inputHolder.setExtraField(inputHolder.mExpiryField, null, null, false);
}
+ if (mLoyaltyCardListDisplayOptions.showingCardId() && !loyaltyCard.cardId.isEmpty()) {
+ inputHolder.setExtraField(inputHolder.mCardIdField, loyaltyCard.cardId, null, showDivider);
+ } else {
+ inputHolder.setExtraField(inputHolder.mCardIdField, null, null, false);
+ }
+
inputHolder.mCardIcon.setContentDescription(loyaltyCard.store);
Utils.setIconOrTextWithBackground(mContext, loyaltyCard, icon, inputHolder.mCardIcon, inputHolder.mCardText, new Settings(mContext).getPreferredColumnCount());
@@ -211,7 +217,7 @@ public interface CardAdapterListener {
public class LoyaltyCardListItemViewHolder extends RecyclerView.ViewHolder {
- public TextView mCardText, mStoreField, mNoteField, mBalanceField, mValidFromField, mExpiryField;
+ public TextView mCardText, mStoreField, mNoteField, mBalanceField, mValidFromField, mExpiryField, mCardIdField;
public ImageView mCardIcon, mTickIcon;
public MaterialCardView mRow;
public ConstraintLayout mStar, mArchived;
@@ -227,6 +233,7 @@ protected LoyaltyCardListItemViewHolder(LoyaltyCardLayoutBinding loyaltyCardLayo
mBalanceField = loyaltyCardLayoutBinding.balance;
mValidFromField = loyaltyCardLayoutBinding.validFrom;
mExpiryField = loyaltyCardLayoutBinding.expiry;
+ mCardIdField = loyaltyCardLayoutBinding.cardId;
mCardIcon = loyaltyCardLayoutBinding.thumbnail;
mCardText = loyaltyCardLayoutBinding.thumbnailText;
mStar = loyaltyCardLayoutBinding.star;
diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardListDisplayOptionsManager.java b/app/src/main/java/protect/card_locker/LoyaltyCardListDisplayOptionsManager.java
index 9ece34ed38..ce6eaa811d 100644
--- a/app/src/main/java/protect/card_locker/LoyaltyCardListDisplayOptionsManager.java
+++ b/app/src/main/java/protect/card_locker/LoyaltyCardListDisplayOptionsManager.java
@@ -37,6 +37,7 @@ public static class LoyaltyCardDisplayOption {
private boolean mShowNote;
private boolean mShowBalance;
private boolean mShowValidity;
+ private boolean mShowCardId;
private boolean mShowArchivedCards;
public LoyaltyCardListDisplayOptionsManager(Context context, @NonNull Runnable refreshCardsCallback, @Nullable Runnable swapCursorCallback) {
@@ -52,6 +53,7 @@ public LoyaltyCardListDisplayOptionsManager(Context context, @NonNull Runnable r
mShowNote = mCardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show_note), true);
mShowBalance = mCardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show_balance), true);
mShowValidity = mCardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show_validity), true);
+ mShowCardId = mCardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show_card_id), false);
mShowArchivedCards = mCardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show_archived_cards), true);
}
@@ -105,6 +107,17 @@ public boolean showingValidity() {
return mShowValidity;
}
+ public void showCardId(boolean show) {
+ mShowCardId = show;
+ mRefreshCardsCallback.run();
+
+ saveDetailState(R.string.sharedpreference_card_details_show_card_id, show);
+ }
+
+ public boolean showingCardId() {
+ return mShowCardId;
+ }
+
public void showArchivedCards(boolean show) {
if (mSwapCursorCallback == null) {
throw new IllegalStateException("No swap cursor callback is available, can not manage archive state");
@@ -147,6 +160,11 @@ public void showDisplayOptionsDialog() {
showingValidity(),
this::showValidity
));
+ displayOptions.add(new LoyaltyCardDisplayOption(
+ mContext.getString(R.string.show_card_id),
+ showingCardId(),
+ this::showCardId
+ ));
// Hide "Show archived cards" option unless the callback exists
if (mSwapCursorCallback != null) {
diff --git a/app/src/main/res/drawable/ic_card_id_24dp.xml b/app/src/main/res/drawable/ic_card_id_24dp.xml
new file mode 100644
index 0000000000..eddfb60f2d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_card_id_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/layout/loyalty_card_layout.xml b/app/src/main/res/layout/loyalty_card_layout.xml
index 34b745b167..2b2cd94cc5 100644
--- a/app/src/main/res/layout/loyalty_card_layout.xml
+++ b/app/src/main/res/layout/loyalty_card_layout.xml
@@ -213,11 +213,30 @@
android:drawablePadding="4dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/validFrom"
- app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/cardId"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible"
tools:text="Tomorrow"/>
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3cbdea0793..566e591e7d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -280,10 +280,12 @@
Show note
Show balance
Show validity
+ Show card ID
sharedpreference_card_details_show_name_below_thumbnail
sharedpreference_card_details_show_note
sharedpreference_card_details_show_balance
sharedpreference_card_details_show_validity
+ sharedpreference_card_details_show_card_id
sharedpreference_card_details_show_archived_cards
Card view
Cards overview
diff --git a/app/src/test/java/protect/card_locker/MainActivityTest.java b/app/src/test/java/protect/card_locker/MainActivityTest.java
index 754360f217..d0e8642d50 100644
--- a/app/src/test/java/protect/card_locker/MainActivityTest.java
+++ b/app/src/test/java/protect/card_locker/MainActivityTest.java
@@ -20,6 +20,7 @@
import androidx.appcompat.widget.SearchView;
import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.core.app.ApplicationProvider;
import com.google.android.material.tabs.TabLayout;
import com.google.zxing.BarcodeFormat;
@@ -116,6 +117,74 @@ public void addOneLoyaltyCard() {
database.close();
}
+ @Test
+ public void cardIdHiddenByDefault() {
+ ActivityController activityController = Robolectric.buildActivity(MainActivity.class).create();
+
+ Activity mainActivity = (Activity) activityController.get();
+ activityController.start();
+ activityController.resume();
+ activityController.visible();
+
+ RecyclerView list = mainActivity.findViewById(R.id.list);
+
+ SQLiteDatabase database = TestHelpers.getEmptyDb(mainActivity).getWritableDatabase();
+ DBHelper.insertLoyaltyCard(database, "store", "", null, null, new BigDecimal("0"), null, "1234567890", null, CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A), StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0);
+
+ activityController.pause();
+ activityController.resume();
+ activityController.visible();
+
+ assertEquals(1, list.getAdapter().getItemCount());
+
+ list.measure(0, 0);
+ list.layout(0, 0, 100, 1000);
+
+ // Card ID is hidden by default
+ TextView cardIdField = list.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.cardId);
+ assertEquals(View.GONE, cardIdField.getVisibility());
+
+ database.close();
+ }
+
+ @Test
+ public void showCardIdDisplayOption() {
+ // The display option is read in the adapter's constructor (during onCreate), so the
+ // preference must be set before the activity is built.
+ SharedPreferences cardDetailsPref = ApplicationProvider.getApplicationContext().getSharedPreferences(
+ ApplicationProvider.getApplicationContext().getString(R.string.sharedpreference_card_details),
+ Activity.MODE_PRIVATE);
+ cardDetailsPref.edit().putBoolean(
+ ApplicationProvider.getApplicationContext().getString(R.string.sharedpreference_card_details_show_card_id), true).apply();
+
+ ActivityController activityController = Robolectric.buildActivity(MainActivity.class).create();
+
+ Activity mainActivity = (Activity) activityController.get();
+ activityController.start();
+ activityController.resume();
+ activityController.visible();
+
+ RecyclerView list = mainActivity.findViewById(R.id.list);
+
+ SQLiteDatabase database = TestHelpers.getEmptyDb(mainActivity).getWritableDatabase();
+ DBHelper.insertLoyaltyCard(database, "store", "", null, null, new BigDecimal("0"), null, "1234567890", null, CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A), StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0);
+
+ activityController.pause();
+ activityController.resume();
+ activityController.visible();
+
+ assertEquals(1, list.getAdapter().getItemCount());
+
+ list.measure(0, 0);
+ list.layout(0, 0, 100, 1000);
+
+ TextView cardIdField = list.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.cardId);
+ assertEquals(View.VISIBLE, cardIdField.getVisibility());
+ assertEquals("1234567890", cardIdField.getText().toString());
+
+ database.close();
+ }
+
@Test
public void addFourLoyaltyCardsTwoStarred() // Main screen showing starred cards on top correctly
{