Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 76 additions & 19 deletions app/src/main/java/protect/card_locker/DBHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@

import java.io.FileNotFoundException;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Currency;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class DBHelper extends SQLiteOpenHelper {
public static final String DATABASE_NAME = "Catima.db";
public static final int ORIGINAL_DATABASE_VERSION = 1;
public static final int DATABASE_VERSION = 17;
public static final int DATABASE_VERSION = 18;

// NB: changing these values requires a migration
public static final int DEFAULT_ZOOM_LEVEL = 100;
Expand Down Expand Up @@ -335,17 +335,70 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE
+ " ADD COLUMN " + LoyaltyCardDbIds.ZOOM_LEVEL_WIDTH + " INTEGER DEFAULT '100' ");
}

if (oldVersion < 18 && newVersion >= 18) {
db.beginTransaction();

try {
// Add new temporary columns
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " ADD COLUMN " + LoyaltyCardDbIds.VALID_FROM + "_new TEXT");
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " ADD COLUMN " + LoyaltyCardDbIds.EXPIRY + "_new TEXT");

// 2. Read old values, convert, and update new columns
String[] columnsToRead = {LoyaltyCardDbIds.ID, LoyaltyCardDbIds.VALID_FROM, LoyaltyCardDbIds.EXPIRY, LoyaltyCardDbIds.LAST_USED};
Cursor cursor = db.query(LoyaltyCardDbIds.TABLE, columnsToRead, null, null, null, null, null);

if (cursor.moveToFirst()) {
do {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(LoyaltyCardDbIds.ID));
long validFromLong = cursor.getLong(cursor.getColumnIndexOrThrow(LoyaltyCardDbIds.VALID_FROM));
long expiryLong = cursor.getLong(cursor.getColumnIndexOrThrow(LoyaltyCardDbIds.EXPIRY));

ContentValues values = new ContentValues();

// Convert epoch milliseconds to ISO 8601 string (e.g., "2025-09-28T16:30:30Z")
if (validFromLong > 0) {
values.put(LoyaltyCardDbIds.VALID_FROM + "_new", Instant.ofEpochMilli(validFromLong).toString());
}
if (expiryLong > 0) {
values.put(LoyaltyCardDbIds.EXPIRY + "_new", Instant.ofEpochMilli(expiryLong).toString());
}

if (values.size() > 0) {
db.update(LoyaltyCardDbIds.TABLE, values, LoyaltyCardDbIds.ID + " = ?", new String[]{String.valueOf(id)});
}
} while (cursor.moveToNext());
}
cursor.close();

// Drop the old integer columns
// Note: This requires a newer version of SQLite. For maximum compatibility,
// the old "create new table -> copy data -> drop old -> rename" method is safer.
// However, DROP COLUMN is supported on most modern Android devices.
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " DROP COLUMN " + LoyaltyCardDbIds.VALID_FROM);
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " DROP COLUMN " + LoyaltyCardDbIds.EXPIRY);

// Rename the new columns to their final names
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " RENAME COLUMN " + LoyaltyCardDbIds.VALID_FROM + "_new TO " + LoyaltyCardDbIds.VALID_FROM);
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " RENAME COLUMN " + LoyaltyCardDbIds.EXPIRY + "_new TO " + LoyaltyCardDbIds.EXPIRY);

db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
}

public static Set<String> imageFiles(Context context, final SQLiteDatabase database) {
Set<String> files = new HashSet<>();
Cursor cardCursor = getLoyaltyCardCursor(database);
while (cardCursor.moveToNext()) {
LoyaltyCard card = LoyaltyCard.fromCursor(context, cardCursor);
for (ImageLocationType imageLocationType : ImageLocationType.values()) {
String name = Utils.getCardImageFileName(card.id, imageLocationType);
if (card.getImageForImageLocationType(context, imageLocationType) != null) {
files.add(name);
try(Cursor cardCursor = getLoyaltyCardCursor(database)){
while (cardCursor.moveToNext()) {
LoyaltyCard card = LoyaltyCard.fromCursor(context, cardCursor);
for (ImageLocationType imageLocationType : ImageLocationType.values()) {
String name = Utils.getCardImageFileName(card.id, imageLocationType);
if (card.getImageForImageLocationType(context, imageLocationType) != null) {
files.add(name);
}
}
}
}
Expand Down Expand Up @@ -385,6 +438,7 @@ private static ContentValues generateFTSContentValues(final int id, final String
}

private static void insertFTS(final SQLiteDatabase db, final int id, final String store, final String note) {
Log.d("DB_DEBUG", "------ insertFTS called FOR id: " + id);
db.insert(LoyaltyCardDbFTS.TABLE, null, generateFTSContentValues(id, store, note));
}

Expand All @@ -394,18 +448,19 @@ private static void updateFTS(final SQLiteDatabase db, final int id, final Strin
}

public static long insertLoyaltyCard(
final SQLiteDatabase database, final String store, final String note, final Date validFrom,
final Date expiry, final BigDecimal balance, final Currency balanceType, final String cardId,
final SQLiteDatabase database, final String store, final String note, final Instant validFrom,
final Instant expiry, final BigDecimal balance, final Currency balanceType, final String cardId,
final String barcodeId, final CatimaBarcode barcodeType, final Integer headerColor,
final int starStatus, final Long lastUsed, final int archiveStatus) {
Log.d("DB_DEBUG", "--- insertLoyaltyCard called to GENERATE new id.");
database.beginTransaction();

// Card
ContentValues contentValues = new ContentValues();
contentValues.put(LoyaltyCardDbIds.STORE, store);
contentValues.put(LoyaltyCardDbIds.NOTE, note);
contentValues.put(LoyaltyCardDbIds.VALID_FROM, validFrom != null ? validFrom.getTime() : null);
contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null);
contentValues.put(LoyaltyCardDbIds.VALID_FROM, validFrom != null ? validFrom.toString() : null);
contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.toString() : null);
contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString());
contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType != null ? balanceType.getCurrencyCode() : null);
contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId);
Expand All @@ -428,19 +483,20 @@ public static long insertLoyaltyCard(

public static long insertLoyaltyCard(
final SQLiteDatabase database, final int id, final String store, final String note,
final Date validFrom, final Date expiry, final BigDecimal balance,
final Instant validFrom, final Instant expiry, final BigDecimal balance,
final Currency balanceType, final String cardId, final String barcodeId,
final CatimaBarcode barcodeType, final Integer headerColor, final int starStatus,
final Long lastUsed, final int archiveStatus) {
Log.d("DB_DEBUG", "--- insertLoyaltyCard called with PRESET id: " + id);
database.beginTransaction();

// Card
ContentValues contentValues = new ContentValues();
contentValues.put(LoyaltyCardDbIds.ID, id);
contentValues.put(LoyaltyCardDbIds.STORE, store);
contentValues.put(LoyaltyCardDbIds.NOTE, note);
contentValues.put(LoyaltyCardDbIds.VALID_FROM, validFrom != null ? validFrom.getTime() : null);
contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null);
contentValues.put(LoyaltyCardDbIds.VALID_FROM, validFrom != null ? validFrom.toString() : null);
contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.toString() : null);
contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString());
contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType != null ? balanceType.getCurrencyCode() : null);
contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId);
Expand All @@ -453,6 +509,7 @@ public static long insertLoyaltyCard(
database.insert(LoyaltyCardDbIds.TABLE, null, contentValues);

// FTS
Log.d("DBHelper", "insertFTS :: note => " + note + " Store: => "+ store);
insertFTS(database, id, store, note);

database.setTransactionSuccessful();
Expand All @@ -463,7 +520,7 @@ public static long insertLoyaltyCard(

public static boolean updateLoyaltyCard(
SQLiteDatabase database, final int id, final String store, final String note,
final Date validFrom, final Date expiry, final BigDecimal balance,
final Instant validFrom, final Instant expiry, final BigDecimal balance,
final Currency balanceType, final String cardId, final String barcodeId,
final CatimaBarcode barcodeType, final Integer headerColor, final int starStatus,
final Long lastUsed, final int archiveStatus) {
Expand All @@ -473,8 +530,8 @@ public static boolean updateLoyaltyCard(
ContentValues contentValues = new ContentValues();
contentValues.put(LoyaltyCardDbIds.STORE, store);
contentValues.put(LoyaltyCardDbIds.NOTE, note);
contentValues.put(LoyaltyCardDbIds.VALID_FROM, validFrom != null ? validFrom.getTime() : null);
contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null);
contentValues.put(LoyaltyCardDbIds.VALID_FROM, validFrom != null ? validFrom.toString() : null);
contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.toString() : null);
contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString());
contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType != null ? balanceType.getCurrencyCode() : null);
contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId);
Expand Down
84 changes: 84 additions & 0 deletions app/src/main/java/protect/card_locker/DateTimeUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package protect.card_locker;

import androidx.annotation.Nullable;

import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;

public class DateTimeUtils {
static public Instant longToInstant(Long value) {
if(value == null) return null;
return Instant.ofEpochMilli(value);
}

static public @Nullable ZonedDateTime instantToZonedDateTime(@Nullable Instant value) {
if(value == null) return null;
ZoneId systemZone = ZoneId.systemDefault();
return value.atZone(systemZone);
}

/**
* Returns an Instant representing the start of the current day (00:00:00)
* in the system's default timezone.
*/
private static Instant getStartOfTodayAsInstant() {
// Get the current date in the device's timezone (e.g., "2025-09-28")
LocalDate today = LocalDate.now(ZoneId.systemDefault());

// Get the start of that day (midnight) in the same timezone
ZonedDateTime startOfToday = today.atStartOfDay(ZoneId.systemDefault());

// Convert to an Instant for universal comparison
return startOfToday.toInstant();
}

/**
* Checks if an item is not yet valid based on exact date AND time.
* Different from original behavior - now considers exact timestamps.
*
* New behavior: Item becomes valid only at the exact validFrom instant
* - If validFrom = "2024-01-15 14:30:00" and current time is "2024-01-15 14:29:59" → NOT YET VALID (returns true)
* - If validFrom = "2024-01-15 14:30:00" and current time is "2024-01-15 14:30:00" → VALID (returns false)
*/
public static boolean isNotYetValid(Instant validFrom) {
if (validFrom == null) {
return false;
}
return validFrom.isAfter(Instant.now());
}

/**
* Checks if an item has expired, considering exact date AND time.
* @param expiry The Instant the item expires. If null, it never expires.
* @return true if the expiry date/time is in the past.
*/
public static boolean hasExpired(Instant expiry) {
if (expiry == null) {
return false; // Never expires
}
return expiry.isBefore(Instant.now());
}
static public DateTimeFormatter longDateShortTimeFormatter = DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);

static public DateTimeFormatter mediumDateShortTimeFormatter = DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT);

static public String formatMedium(Instant value) {
if(value == null) return null;
ZonedDateTime zoneDate = instantToZonedDateTime(value);
if(zoneDate == null) return null;
return zoneDate.format(mediumDateShortTimeFormatter);
}

static public String formatLong(Instant value) {
if(value == null) return null;
ZonedDateTime zoneDate = instantToZonedDateTime(value);
if(zoneDate == null) return null;
return zoneDate.format(longDateShortTimeFormatter);
}
}
16 changes: 8 additions & 8 deletions app/src/main/java/protect/card_locker/ImportURIHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Currency;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

Expand Down Expand Up @@ -66,8 +66,8 @@ public LoyaltyCard parse(Uri uri) throws InvalidObjectException {
try {
// These values are allowed to be null
CatimaBarcode barcodeType = null;
Date validFrom = null;
Date expiry = null;
Instant validFrom = null;
Instant expiry = null;
BigDecimal balance = new BigDecimal("0");
Currency balanceType = null;
Integer headerColor = null;
Expand Down Expand Up @@ -113,11 +113,11 @@ public LoyaltyCard parse(Uri uri) throws InvalidObjectException {
}
String unparsedValidFrom = kv.get(VALID_FROM);
if (unparsedValidFrom != null && !unparsedValidFrom.equals("")) {
validFrom = new Date(Long.parseLong(unparsedValidFrom));
validFrom = Instant.parse(unparsedValidFrom);
}
String unparsedExpiry = kv.get(EXPIRY);
if (unparsedExpiry != null && !unparsedExpiry.equals("")) {
expiry = new Date(Long.parseLong(unparsedExpiry));
if (unparsedExpiry != null && !unparsedExpiry.isEmpty()) {
expiry = Instant.parse(unparsedExpiry);
}

String unparsedHeaderColor = kv.get(HEADER_COLOR);
Expand Down Expand Up @@ -182,10 +182,10 @@ protected Uri toUri(LoyaltyCard loyaltyCard) throws UnsupportedEncodingException
fragment = appendFragment(fragment, BALANCE_TYPE, loyaltyCard.balanceType.getCurrencyCode());
}
if (loyaltyCard.validFrom != null) {
fragment = appendFragment(fragment, VALID_FROM, String.valueOf(loyaltyCard.validFrom.getTime()));
fragment = appendFragment(fragment, VALID_FROM, loyaltyCard.validFrom.toString());
}
if (loyaltyCard.expiry != null) {
fragment = appendFragment(fragment, EXPIRY, String.valueOf(loyaltyCard.expiry.getTime()));
fragment = appendFragment(fragment, EXPIRY, loyaltyCard.expiry.toString());
}
fragment = appendFragment(fragment, CARD_ID, loyaltyCard.cardId);
if (loyaltyCard.barcodeId != null) {
Expand Down
Loading